* feat(#1213): Capability State Writer — write-side inverse of the resolver Adds src/capability-writer.cts (setCapabilityState + cmdCapabilitySet) and the `gsd-tools capability set` subcommand: the write-side inverse of the capability resolver (ADR-1213). One desired capability state projects onto the substrates — `enabled` drives the runtime surface (canonical on/off), `gates` drive federated config keys (hook granularity), install profile is a read-only floor — then re-resolves and reports divergence (assert-and-report), so "off means off" holds as a write-time invariant. Adds batched setConfigValues; routes gsd:settings capability hook-gates through the writer. Docs: CLI-TOOLS reference, how-to, ADR-1213, CONTEXT.md term. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#1213): add changeset for Capability State Writer (#1225) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -217,6 +217,7 @@ const verification = require('./lib/verification.cjs');
|
||||
const { routeInitCommand } = require('./lib/init-command-router.cjs');
|
||||
const loopResolver = require('./lib/loop-resolver.cjs');
|
||||
const capabilityState = require('./lib/capability-state.cjs');
|
||||
const capabilityWriter = require('./lib/capability-writer.cjs');
|
||||
const { routePhaseCommand } = require('./lib/phase-command-router.cjs');
|
||||
const { routePhasesCommand } = require('./lib/phases-command-router.cjs');
|
||||
const { routeValidateCommand } = require('./lib/validate-command-router.cjs');
|
||||
@@ -1304,9 +1305,85 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
||||
}
|
||||
const resolvedConfigDir = configDir ? path.resolve(configDir) : null;
|
||||
capabilityState.cmdCapabilityState(cwd, resolvedConfigDir, raw, {});
|
||||
} else if (capSubcommand === 'set') {
|
||||
// capability set <id> [--on|--off|--enable|--disable] [--gate <key>=<bool>]... [--config-dir <dir>] [--runtime <r>] [--scope <s>]
|
||||
const capId = args[2];
|
||||
if (!capId || capId.startsWith('--')) {
|
||||
error('Missing capability id for: capability set <id>', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
|
||||
}
|
||||
// Parse --config-dir
|
||||
const setConfigDirIdx = args.indexOf('--config-dir');
|
||||
let setConfigDir = null;
|
||||
if (setConfigDirIdx !== -1) {
|
||||
const setConfigDirVal = args[setConfigDirIdx + 1];
|
||||
if (!setConfigDirVal || setConfigDirVal.startsWith('--')) {
|
||||
error('Missing value for --config-dir', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
|
||||
}
|
||||
setConfigDir = setConfigDirVal;
|
||||
}
|
||||
const resolvedSetConfigDir = setConfigDir ? path.resolve(setConfigDir) : null;
|
||||
// Parse --on/--enable and --off/--disable (mutually exclusive)
|
||||
const hasOn = args.includes('--on') || args.includes('--enable');
|
||||
const hasOff = args.includes('--off') || args.includes('--disable');
|
||||
if (hasOn && hasOff) {
|
||||
error('Conflicting flags: --on/--enable and --off/--disable cannot both be present', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
|
||||
}
|
||||
let setEnabled;
|
||||
if (hasOn) {
|
||||
setEnabled = true;
|
||||
} else if (hasOff) {
|
||||
setEnabled = false;
|
||||
}
|
||||
// Parse --gate <key>=<bool> (repeatable)
|
||||
const setGates = {};
|
||||
for (let gi = 0; gi < args.length; gi++) {
|
||||
if (args[gi] === '--gate') {
|
||||
const gateVal = args[gi + 1];
|
||||
if (!gateVal || gateVal.startsWith('--')) {
|
||||
error('Missing value for --gate (expected <key>=<true|false>)', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
|
||||
}
|
||||
const eqIdx = gateVal.indexOf('=');
|
||||
if (eqIdx === -1) {
|
||||
error(`Malformed --gate value "${gateVal}": expected <key>=<true|false>`, core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
|
||||
}
|
||||
const gateKey = gateVal.slice(0, eqIdx);
|
||||
const gateBoolStr = gateVal.slice(eqIdx + 1);
|
||||
if (gateBoolStr !== 'true' && gateBoolStr !== 'false') {
|
||||
error(`Malformed --gate value "${gateVal}": bool must be true or false`, core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
|
||||
}
|
||||
setGates[gateKey] = gateBoolStr === 'true';
|
||||
gi++; // skip consumed value
|
||||
}
|
||||
}
|
||||
// Parse --runtime and --scope (validate that values are present and not flags)
|
||||
const runtimeIdx = args.indexOf('--runtime');
|
||||
let setRuntime;
|
||||
if (runtimeIdx !== -1) {
|
||||
const runtimeVal = args[runtimeIdx + 1];
|
||||
if (!runtimeVal || runtimeVal.startsWith('--')) {
|
||||
error('Missing value for --runtime', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
|
||||
}
|
||||
setRuntime = runtimeVal;
|
||||
}
|
||||
const scopeIdx = args.indexOf('--scope');
|
||||
let setScope;
|
||||
if (scopeIdx !== -1) {
|
||||
const scopeVal = args[scopeIdx + 1];
|
||||
if (!scopeVal || scopeVal.startsWith('--')) {
|
||||
error('Missing value for --scope', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
|
||||
}
|
||||
setScope = scopeVal;
|
||||
}
|
||||
capabilityWriter.cmdCapabilitySet(
|
||||
cwd,
|
||||
resolvedSetConfigDir,
|
||||
capId,
|
||||
{ enabled: setEnabled, gates: Object.keys(setGates).length > 0 ? setGates : undefined, runtime: setRuntime, scope: setScope },
|
||||
raw,
|
||||
);
|
||||
} else {
|
||||
error(
|
||||
`Unknown capability subcommand: ${capSubcommand}. Available: state`,
|
||||
`Unknown capability subcommand: ${capSubcommand}. Available: state, set`,
|
||||
core.ERROR_REASON ? core.ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user