agentic-workflow-manager 9.10.2 → 9.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@ const config_1 = require("../../utils/config");
14
14
  const providers_1 = require("../../providers");
15
15
  const registries_1 = require("../../core/registries");
16
16
  const registry_contracts_1 = require("../../core/admission/registry-contracts");
17
+ const adapter_1 = require("../../core/journal/adapter");
17
18
  const path_1 = __importDefault(require("path"));
18
19
  const child_process_1 = require("child_process");
19
20
  const store_1 = require("../../core/journal/store");
@@ -262,6 +263,7 @@ function registerPlanCommand(program, deps = { validatePlanFile: validate_1.vali
262
263
  .option('--runtime-kind <kind>', 'routing runtime kind for compact v2')
263
264
  .option('--runtime-version <version>', 'routing runtime version for compact v2')
264
265
  .option('--account-scope-digest <sha>', 'routing account scope digest for compact v2')
266
+ .option('--controller-autonomy <posture>', 'unattended controller autonomy posture (#168)')
265
267
  .option('--json', 'emit one stable JSON report')
266
268
  .action(async (planPath, options) => {
267
269
  assertText(planPath, 'plan path');
@@ -294,7 +296,9 @@ function registerPlanCommand(program, deps = { validatePlanFile: validate_1.vali
294
296
  const readRouting = () => { if (!options.runtimeKind || !options.runtimeVersion || !options.accountScopeDigest)
295
297
  return undefined; const runtime = (0, capabilities_1.validateRuntimeKey)({ target: options.provider, kind: options.runtimeKind, version: options.runtimeVersion, accountScopeDigest: options.accountScopeDigest }); const policy = (deps.readEffectivePolicy ?? store_2.readEffectivePolicy)(options.cwd); const at = deps.routingNow?.() ?? new Date(); if (!(at instanceof Date) || !Number.isFinite(at.getTime()))
296
298
  throw new Error('routingNow must return a finite Date'); const capabilities = (deps.readCapabilities ?? capabilities_1.readCapabilities)(runtime, at); return { runtime, policy: policy.state === 'approved' ? policy.policy : undefined, capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined, now: at }; };
297
- const report = await (0, registry_contracts_1.admitRegistryPlan)({ plan: planReport, provider: options.provider, cwd: options.cwd, enabledAgents, executionMode, planPath: normalizedPlanPath, ...journal, requireCurrent: options.requireCurrent === true, verifySensors: options.verifySensors === true }, { admitPlan: admission, listRegistries: registryInventory, checkCurrentness: currentnessCheck, runSensors: sensorRun, readRouting });
299
+ if (options.controllerAutonomy !== undefined && !(0, adapter_1.isControllerAutonomy)(options.controllerAutonomy))
300
+ throw new Error(`--controller-autonomy is invalid: ${options.controllerAutonomy} (valid: ${adapter_1.CONTROLLER_AUTONOMIES.join(', ')})`);
301
+ const report = await (0, registry_contracts_1.admitRegistryPlan)({ plan: planReport, provider: options.provider, cwd: options.cwd, enabledAgents, executionMode, planPath: normalizedPlanPath, ...journal, requireCurrent: options.requireCurrent === true, verifySensors: options.verifySensors === true, ...(options.controllerAutonomy === undefined ? {} : { controllerAutonomy: options.controllerAutonomy }) }, { admitPlan: admission, listRegistries: registryInventory, checkCurrentness: currentnessCheck, runSensors: sensorRun, readRouting });
298
302
  process.stdout.write(admissionOutput(report, options.json === true));
299
303
  if (report.state !== 'admitted')
300
304
  process.exitCode = 2;
@@ -105,7 +105,7 @@ function recoveryAction(action) {
105
105
  /** Persiste el intent completo ANTES de delegarlo al wrapper. El wrapper usa
106
106
  * claim exclusivo por (controllerJobId, spawnNonce), por lo que reemitir este
107
107
  * mismo intent tras un crash nunca lanza dos controllers. */
108
- function launchControllerGeneration(repoRoot, branch, provider, action, spawner = (0, runner_1.defaultWrapperSpawner)()) {
108
+ function launchControllerGeneration(repoRoot, branch, provider, action, spawner = (0, runner_1.defaultWrapperSpawner)(), autonomy) {
109
109
  const requestedAction = recoveryAction(action);
110
110
  const s = requireState(repoRoot, branch);
111
111
  const gen = activeGeneration(s);
@@ -118,7 +118,7 @@ function launchControllerGeneration(repoRoot, branch, provider, action, spawner
118
118
  gen.spawnNonce = gen.spawnNonce ?? crypto_1.default.randomBytes(8).toString('hex');
119
119
  gen.provider = gen.provider ?? provider;
120
120
  gen.resumeAction = gen.resumeAction ?? requestedAction;
121
- const argv = adapter.launchArgv(promptForGeneration(gen));
121
+ const argv = adapter.launchArgv(promptForGeneration(gen), autonomy);
122
122
  const digest = (0, process_1.argvDigest)(argv);
123
123
  if (gen.launchArgvDigest !== undefined && gen.launchArgvDigest !== digest)
124
124
  throw new Error('controller argv cambio: no se reemite el mismo nonce con otro comando');
@@ -182,7 +182,7 @@ function controllerGenerationHasUnresolvedClaim(repoRoot, branch, gen) {
182
182
  /** Recupera tanto crash-before-spawn como crash-after-spawn-before-journal.
183
183
  * Claim sin identidad queda ambiguo y se conserva bajo custodia tras la gracia;
184
184
  * nunca se resuelve lanzando otro token/nonce a ciegas. */
185
- function ensureControllerGeneration(repoRoot, branch, provider, action, spawner, ambiguityGraceMs) {
185
+ function ensureControllerGeneration(repoRoot, branch, provider, action, spawner, ambiguityGraceMs, autonomy) {
186
186
  const requestedAction = recoveryAction(action);
187
187
  if (collectControllerGeneration(repoRoot, branch))
188
188
  return;
@@ -194,7 +194,7 @@ function ensureControllerGeneration(repoRoot, branch, provider, action, spawner,
194
194
  if (gen.processRef !== undefined || gen.wrapperRef !== undefined)
195
195
  return;
196
196
  if (gen.controllerJobId === undefined || gen.spawnNonce === undefined) {
197
- launchControllerGeneration(repoRoot, branch, provider, requestedAction, spawner);
197
+ launchControllerGeneration(repoRoot, branch, provider, requestedAction, spawner, autonomy);
198
198
  return;
199
199
  }
200
200
  const logs = (0, paths_1.logsDir)(repoRoot, branch);
@@ -217,7 +217,7 @@ function ensureControllerGeneration(repoRoot, branch, provider, action, spawner,
217
217
  // El intent ya estaba durable pero el spawn no ocurrio: es seguro reemitir
218
218
  // exactamente el mismo nonce. Si el wrapper original solo estaba demorado,
219
219
  // su claim wx arbitra cual de ambos ejecuta.
220
- launchControllerGeneration(repoRoot, branch, gen.provider ?? provider, gen.resumeAction ?? requestedAction, spawner);
220
+ launchControllerGeneration(repoRoot, branch, gen.provider ?? provider, gen.resumeAction ?? requestedAction, spawner, autonomy);
221
221
  }
222
222
  /** Custodia (R4.5): ciclo BLOCKED con razon auditada. QUIEN NO HACE NADA:
223
223
  * no mata, no relanza, no libera lock — el loop sigue vivo auditando. */
@@ -7,8 +7,10 @@ exports.registerWatchCommand = registerWatchCommand;
7
7
  const child_process_1 = require("child_process");
8
8
  const init_1 = require("./init");
9
9
  const supervisor_1 = require("./supervisor");
10
- const process_1 = require("../../core/journal/process");
10
+ const capabilities_1 = require("../../core/model-policy/capabilities");
11
11
  const adapter_1 = require("../../core/journal/adapter");
12
+ const process_1 = require("../../core/journal/process");
13
+ const adapter_2 = require("../../core/journal/adapter");
12
14
  const context_1 = require("../../core/tracks/context");
13
15
  const concurrency_1 = require("../../core/tracks/concurrency");
14
16
  const validate_1 = require("../../core/plan/validate");
@@ -44,10 +46,19 @@ function registerWatchCommand(program) {
44
46
  .description('supervisor durable: ejecuta jobs, releva controladores caidos, nunca mata trabajo vivo')
45
47
  .option('--init', 'bootstrap: crea el journal de la rama actual, detecta verificadores y sale')
46
48
  .option('--plan <path>', 'plan compacto desatendido que se vincula al inicializar')
47
- .option('--provider <p>', adapter_1.WATCH_PROVIDERS.join(' | '), 'codex')
49
+ .option('--provider <p>', adapter_2.WATCH_PROVIDERS.join(' | '), 'codex')
48
50
  .option('--heartbeat-timeout <min>', 'minutos de silencio de heartbeat', '5')
49
51
  .option('--activity-window <min>', 'minutos extra sin actividad de proceso', '10')
50
52
  .option('--max-parallel <n>', 'tope de tracks ACTIVE simultáneos (default: derivado del benchmark empaquetado)')
53
+ // #166: la admisión compact v2 exige la identidad de runtime. Se declara
54
+ // acá, igual que en `plan admit`/`plan resolve`: el operador la asserta y
55
+ // el receipt debe coincidir, nunca al revés.
56
+ .option('--runtime-kind <kind>', 'identidad de runtime para despacho compact v2')
57
+ .option('--runtime-version <version>', 'identidad de runtime para despacho compact v2')
58
+ .option('--account-scope-digest <sha>', 'identidad de runtime para despacho compact v2')
59
+ // #168: sin esto el controller desatendido arranca y se cuelga en la
60
+ // primera herramienta pidiendo aprobación. Se declara, no se adivina.
61
+ .option('--controller-autonomy <postura>', `postura del controller desatendido (${adapter_1.CONTROLLER_AUTONOMIES.join(' | ')})`)
51
62
  .action(async (opts) => {
52
63
  const repo = process.cwd();
53
64
  const branch = currentBranch(repo);
@@ -86,8 +97,37 @@ function registerWatchCommand(program) {
86
97
  // desconocido, pero recien en el primer tick — con el journal escrito y el
87
98
  // lock tomado, y el error saliendo del supervisor en vez de del flag que lo
88
99
  // causo. Un typo en `--provider` tiene que costar un mensaje, no un ciclo.
89
- if (!(0, adapter_1.isWatchProvider)(opts.provider)) {
90
- process.stderr.write(`--provider invalido: ${String(opts.provider)} (validos: ${adapter_1.WATCH_PROVIDERS.join(', ')})\n`);
100
+ if (!(0, adapter_2.isWatchProvider)(opts.provider)) {
101
+ process.stderr.write(`--provider invalido: ${String(opts.provider)} (validos: ${adapter_2.WATCH_PROVIDERS.join(', ')})\n`);
102
+ process.exitCode = 1;
103
+ return;
104
+ }
105
+ // Los tres viajan juntos o ninguno: una identidad parcial produciría
106
+ // exactamente el bloqueo silencioso que este flag existe para evitar.
107
+ const identityFlags = [['--runtime-kind', opts.runtimeKind], ['--runtime-version', opts.runtimeVersion], ['--account-scope-digest', opts.accountScopeDigest]];
108
+ const suppliedIdentity = identityFlags.filter(([, value]) => value !== undefined);
109
+ if (suppliedIdentity.length !== 0 && suppliedIdentity.length !== identityFlags.length) {
110
+ const missing = identityFlags.filter(([, value]) => value === undefined).map(([flag]) => flag);
111
+ process.stderr.write(`la identidad de runtime requiere los tres flags juntos; faltan: ${missing.join(', ')}\n`);
112
+ process.exitCode = 1;
113
+ return;
114
+ }
115
+ let routingIdentity;
116
+ if (suppliedIdentity.length === identityFlags.length) {
117
+ try {
118
+ // Valida acá, antes de tomar el lock: un digest mal formado
119
+ // tiene que costar un mensaje, no un ciclo de custodia.
120
+ (0, capabilities_1.validateRuntimeKey)({ target: opts.provider, kind: opts.runtimeKind, version: opts.runtimeVersion, accountScopeDigest: opts.accountScopeDigest });
121
+ }
122
+ catch (e) {
123
+ process.stderr.write(`${e.message}\n`);
124
+ process.exitCode = 1;
125
+ return;
126
+ }
127
+ routingIdentity = { kind: String(opts.runtimeKind), version: String(opts.runtimeVersion), accountScopeDigest: String(opts.accountScopeDigest) };
128
+ }
129
+ if (opts.controllerAutonomy !== undefined && !(0, adapter_1.isControllerAutonomy)(opts.controllerAutonomy)) {
130
+ process.stderr.write(`--controller-autonomy invalido: ${String(opts.controllerAutonomy)} (validos: ${adapter_1.CONTROLLER_AUTONOMIES.join(', ')})\n`);
91
131
  process.exitCode = 1;
92
132
  return;
93
133
  }
@@ -97,6 +137,8 @@ function registerWatchCommand(program) {
97
137
  heartbeatTimeoutMs: minutes('--heartbeat-timeout', opts.heartbeatTimeout),
98
138
  activityWindowMs: minutes('--activity-window', opts.activityWindow),
99
139
  maxParallelTracks: opts.maxParallel !== undefined ? (0, concurrency_1.parseMaxParallel)(opts.maxParallel) : (0, concurrency_1.loadDefaultParallelism)(),
140
+ ...(routingIdentity === undefined ? {} : { routingIdentity }),
141
+ ...(opts.controllerAutonomy === undefined ? {} : { controllerAutonomy: opts.controllerAutonomy }),
100
142
  };
101
143
  process.stdout.write(`awm watch: supervisor activo (${cfg.provider}) — Ctrl-C para terminar\n`);
102
144
  await (0, supervisor_1.runSupervisorLoop)(repo, branch, cfg);
@@ -1,12 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Supervisor = exports.MAX_SENSOR_DEFERRALS = exports.DEFAULT_SUPERVISOR_CONFIG = void 0;
4
+ exports.routingFacts = routingFacts;
5
+ exports.defaultDispatchAdmission = defaultDispatchAdmission;
6
+ exports.admissionForConfig = admissionForConfig;
4
7
  exports.runSupervisorLoop = runSupervisorLoop;
5
8
  const store_1 = require("../../core/journal/store");
6
9
  const fingerprint_1 = require("../../core/journal/fingerprint");
7
10
  const validate_1 = require("../../core/plan/validate");
8
11
  const registry_contracts_1 = require("../../core/admission/registry-contracts");
9
12
  const config_1 = require("../../utils/config");
13
+ const store_2 = require("../../core/model-policy/store");
14
+ const capabilities_1 = require("../../core/model-policy/capabilities");
10
15
  const adapter_1 = require("../../core/journal/adapter");
11
16
  const process_1 = require("../../core/journal/process");
12
17
  const gate_1 = require("../job/gate");
@@ -16,7 +21,30 @@ const apply_1 = require("./apply");
16
21
  const runner_1 = require("./runner");
17
22
  const tracks_1 = require("./tracks");
18
23
  const generations_1 = require("./generations");
19
- function defaultDispatchAdmission(repoRoot, branch, provider) {
24
+ /** #166: compact v2 admission requires the runtime identity, and the supervisor
25
+ * never supplied it — so `awm watch` blocked on every tick with
26
+ * ADMISSION_ROUTING_FACTS_REQUIRED even with every other gate green.
27
+ *
28
+ * The direction of attestation is the one `plan admit`/`plan resolve` already
29
+ * use, and is the reason the identity is a flag rather than something derived:
30
+ * the OPERATOR asserts the runtime, and the receipt reader is keyed BY that
31
+ * assertion. Picking a runtime by hunting for a receipt would let whatever
32
+ * receipt happens to be on disk decide what the supervisor claims to run on. */
33
+ function routingFacts(provider, identity, cwd, deps = {}) {
34
+ if (identity === undefined)
35
+ return undefined;
36
+ const runtime = (0, capabilities_1.validateRuntimeKey)({ target: provider, kind: identity.kind, version: identity.version, accountScopeDigest: identity.accountScopeDigest });
37
+ const at = deps.now?.() ?? new Date();
38
+ if (!(at instanceof Date) || !Number.isFinite(at.getTime()))
39
+ throw new Error('routing observation time must be a finite Date');
40
+ const policy = (deps.readEffectivePolicy ?? store_2.readEffectivePolicy)(cwd);
41
+ const capabilities = (deps.readCapabilities ?? capabilities_1.readCapabilities)(runtime, at);
42
+ // Fail open on neither: an unapproved policy or a non-current receipt is
43
+ // withheld, and admission blocks on the absence rather than on a stale fact.
44
+ return { runtime, policy: policy.state === 'approved' ? policy.policy : undefined,
45
+ capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined, now: at };
46
+ }
47
+ function defaultDispatchAdmission(repoRoot, branch, provider, identity, deps = {}, autonomy) {
20
48
  return async () => {
21
49
  const observed = (0, store_1.readJournal)(repoRoot, branch);
22
50
  const binding = observed.state?.schema === 2 ? observed.state.planBinding : undefined;
@@ -25,9 +53,11 @@ function defaultDispatchAdmission(repoRoot, branch, provider) {
25
53
  }
26
54
  const plan = (0, validate_1.validatePlanFile)(binding.path, repoRoot);
27
55
  const preferences = (0, config_1.readPreferences)();
28
- return (0, registry_contracts_1.admitRegistryPlan)({ plan, provider, cwd: repoRoot, enabledAgents: preferences.enabledAgents,
56
+ return (deps.admit ?? registry_contracts_1.admitRegistryPlan)({ plan, provider, cwd: repoRoot, enabledAgents: preferences.enabledAgents,
29
57
  executionMode: 'desatendido', requireCurrent: true, verifySensors: true,
30
- journalState: observed.state, journalCorrupt: observed.corrupt, planPath: binding.path });
58
+ journalState: observed.state, journalCorrupt: observed.corrupt, planPath: binding.path,
59
+ routing: routingFacts(provider, identity, repoRoot, deps),
60
+ controllerAutonomy: autonomy });
31
61
  };
32
62
  }
33
63
  /** R7/C3/C4 (Task 12) + fix post-review #2 (re-derivado desde cero tras
@@ -120,6 +150,11 @@ exports.DEFAULT_SUPERVISOR_CONFIG = {
120
150
  jobStallObservationMs: 5 * 60000, // R3.5 default: mismo orden de magnitud que heartbeatTimeoutMs, concern independiente
121
151
  maxParallelTracks: 1, // overridden en tiempo de ejecución con loadDefaultParallelism() (watch/index.ts)
122
152
  };
153
+ /** Single place the supervisor's config becomes its admission, so the runtime
154
+ * identity cannot be dropped silently between the flag and the gate (#166). */
155
+ function admissionForConfig(repoRoot, branch, cfg, deps = {}) {
156
+ return defaultDispatchAdmission(repoRoot, branch, cfg.provider, cfg.routingIdentity, deps, cfg.controllerAutonomy);
157
+ }
123
158
  // 'frozen' (R5.2/R6.3, Task 10): SOLO puede ocurrir en el journal de un
124
159
  // TRACK individual (nunca en el del plan) — el track terminó, con las 6
125
160
  // observaciones demostrables (cero jobs vivos, gate local verde, worktree
@@ -165,7 +200,7 @@ class Supervisor {
165
200
  lastActivity = null;
166
201
  lastGenerationToken = null;
167
202
  trackRuntime;
168
- constructor(repoRoot, branch, cfg, spawner, trackRuntime, dispatchAdmission = defaultDispatchAdmission(repoRoot, branch, cfg.provider)) {
203
+ constructor(repoRoot, branch, cfg, spawner, trackRuntime, dispatchAdmission = admissionForConfig(repoRoot, branch, cfg)) {
169
204
  this.repoRoot = repoRoot;
170
205
  this.branch = branch;
171
206
  this.cfg = cfg;
@@ -189,7 +224,7 @@ class Supervisor {
189
224
  return 'custody';
190
225
  }
191
226
  try {
192
- (0, generations_1.ensureControllerGeneration)(this.repoRoot, this.branch, this.cfg.provider, action, this.spawner, this.cfg.reconcileGraceMs);
227
+ (0, generations_1.ensureControllerGeneration)(this.repoRoot, this.branch, this.cfg.provider, action, this.spawner, this.cfg.reconcileGraceMs, this.cfg.controllerAutonomy);
193
228
  return 'ok';
194
229
  }
195
230
  catch (error) {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PROVIDER_EXECUTION_CAPABILITIES = void 0;
4
+ exports.unattendedCapabilities = unattendedCapabilities;
4
5
  exports.sanitizeAdmissionReport = sanitizeAdmissionReport;
5
6
  exports.admitPlan = admitPlan;
6
7
  const providers_1 = require("../../providers");
@@ -11,7 +12,13 @@ const UNKNOWN = {
11
12
  artifactDelivery: 'unverified', interactiveExecution: 'unverified', unattendedController: 'unverified', nativeSubagents: 'unverified',
12
13
  modelOverride: 'unverified', effortOverride: 'unverified', observedModelEvidence: 'unverified', durableResume: 'unverified',
13
14
  };
14
- /** Deliberate R1 evidence table. Artifact delivery is never used as execution evidence. */
15
+ /** Deliberate R1 evidence table. Artifact delivery is never used as execution evidence.
16
+ *
17
+ * `unattendedController: 'supported'` describes the supervisor<->controller
18
+ * CONTRACT, which D-016 certifies with a scripted controller. It does NOT say a
19
+ * real agent runtime can occupy the role — D-016 marks that explicitly
20
+ * unverified. Read this table through `unattendedCapabilities()`, neverdirectly:
21
+ * the raw row overstates what has been established (#168). */
15
22
  exports.PROVIDER_EXECUTION_CAPABILITIES = {
16
23
  antigravity: { ...UNKNOWN },
17
24
  opencode: { ...UNKNOWN },
@@ -20,6 +27,21 @@ exports.PROVIDER_EXECUTION_CAPABILITIES = {
20
27
  cursor: { ...UNKNOWN },
21
28
  copilot: { ...UNKNOWN },
22
29
  };
30
+ /** #168: the controller can only act if it was launched with an autonomy
31
+ * posture. That posture is an OPERATOR ASSERTION, exactly like
32
+ * `--expected-digest` or the compact v2 runtime identity: AWM cannot verify it
33
+ * independently (a codex operator may carry approval policy in their own
34
+ * config), so it refuses to guess rather than refusing the operator. Without a
35
+ * declaration `unattendedController` degrades to 'unverified' — which is what
36
+ * D-016 already said, and what the raw table contradicted. */
37
+ function unattendedCapabilities(provider, autonomy) {
38
+ const base = exports.PROVIDER_EXECUTION_CAPABILITIES[provider];
39
+ if (base.unattendedController !== 'supported')
40
+ return base;
41
+ if (autonomy !== undefined)
42
+ return base;
43
+ return { ...base, unattendedController: 'unverified' };
44
+ }
23
45
  const MAX_DIAGNOSTICS = 20;
24
46
  const MAX_DIAGNOSTIC_LENGTH = 4096;
25
47
  function safe(value) { return value.replace(/[\u0000-\u001F\u007F-\u009F]/g, character => `\\u${character.codePointAt(0).toString(16).padStart(4, '0')}`).slice(0, MAX_DIAGNOSTIC_LENGTH); }
@@ -190,10 +212,18 @@ async function admitPlan(input) {
190
212
  const journal = journalStatus(input, plan);
191
213
  if (journal !== 'current')
192
214
  return blocked(input, [diagnostic('ADMISSION_JOURNAL_BINDING_REQUIRED', 'Unattended execution requires a healthy schema-2 journal binding for this exact plan; run watch --init --plan.')], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', sensors: input.verifySensors ? 'pass' : 'not-required' });
193
- const capabilities = exports.PROVIDER_EXECUTION_CAPABILITIES[provider];
215
+ const capabilities = unattendedCapabilities(provider, input.controllerAutonomy);
194
216
  const resolution = { outcome: capabilities.unattendedController === 'supported' ? 'native' : 'blocked', provider, capabilities, evidenceVersion: 'r1-v1', diagnostics: [] };
195
- if (resolution.outcome === 'blocked')
196
- return blocked(input, [diagnostic('ADMISSION_CAPABILITY_UNVERIFIED', `Provider ${provider} has no verified unattended execution capability.`)], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', sensors: input.verifySensors ? 'pass' : 'not-required', capabilityResolution: resolution });
217
+ if (resolution.outcome === 'blocked') {
218
+ // Distinguir las dos razones: el provider nunca tuvo la capacidad,
219
+ // o el operador no declaro postura. Un solo diagnostico las
220
+ // confundiria y el operador no sabria si le falta una flag o si el
221
+ // provider directamente no sirve (#168).
222
+ const reason = exports.PROVIDER_EXECUTION_CAPABILITIES[provider].unattendedController !== 'supported'
223
+ ? diagnostic('ADMISSION_CAPABILITY_UNVERIFIED', `Provider ${provider} has no verified unattended execution capability.`)
224
+ : diagnostic('ADMISSION_CONTROLLER_AUTONOMY_REQUIRED', `Unattended dispatch on ${provider} requires an explicit controller autonomy posture; without it the controller blocks on its first tool call.`);
225
+ return blocked(input, [reason], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', sensors: input.verifySensors ? 'pass' : 'not-required', capabilityResolution: resolution });
226
+ }
197
227
  return completeAdmission(input, plan, provider, mode, journal, input.requireCurrent ? 'current' : 'not-checked', input.verifySensors ? 'pass' : 'not-required', resolution);
198
228
  }
199
229
  const capabilities = exports.PROVIDER_EXECUTION_CAPABILITIES[provider];
@@ -1,22 +1,41 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CONTROLLER_ARGV_ENV = exports.WATCH_PROVIDERS = void 0;
3
+ exports.CONTROLLER_ARGV_ENV = exports.WATCH_PROVIDERS = exports.CONTROLLER_AUTONOMIES = void 0;
4
+ exports.isControllerAutonomy = isControllerAutonomy;
4
5
  exports.isWatchProvider = isWatchProvider;
6
+ exports.controllerAutonomyMapping = controllerAutonomyMapping;
5
7
  exports.adapterFor = adapterFor;
6
8
  const process_1 = require("./process");
9
+ exports.CONTROLLER_AUTONOMIES = ['approval-free'];
10
+ function isControllerAutonomy(value) {
11
+ return typeof value === 'string' && exports.CONTROLLER_AUTONOMIES.includes(value);
12
+ }
7
13
  const RESUME_PROMPT_PREFIX = 'Sos el orquestador SDD de este repo. Corre `awm job reconcile` y ejecuta ';
8
14
  function baseSafeToReplace(ref) {
9
15
  return (0, process_1.refIsAlive)(ref) ? 'indeterminate' : 'safe';
10
16
  }
17
+ // `codex exec` tiene flags de aprobacion (`-a/--ask-for-approval never`,
18
+ // `-s/--sandbox`), pero la evidencia R0 capturo el `--help` de `codex`, NO el
19
+ // del subcomando `exec`, asi que no consta donde van ni como se llaman ahi.
20
+ // Declarar el mapeo como 'unverified' y bloquear es honesto; adivinar la flag
21
+ // dejaria al controller sin arrancar, que es peor que el stall que arreglamos.
11
22
  const codexAdapter = {
12
23
  provider: 'codex',
13
24
  launchArgv: (resumePrompt) => ['codex', 'exec', `${RESUME_PROMPT_PREFIX}${resumePrompt}`],
25
+ autonomyMapping: 'unverified',
14
26
  activity: process_1.activitySnapshot,
15
27
  safeToReplace: baseSafeToReplace,
16
28
  };
29
+ // Leido del `--help` de claude 2.1.278:
30
+ // --permission-mode <mode> (choices: "acceptEdits", "auto", "bypassPermissions",
31
+ // "manual", "dontAsk", "plan")
32
+ // Las flags van ANTES de `-p`, que toma el prompt como posicional.
17
33
  const claudeAdapter = {
18
34
  provider: 'claude-code',
19
- launchArgv: (resumePrompt) => ['claude', '-p', `${RESUME_PROMPT_PREFIX}${resumePrompt}`],
35
+ launchArgv: (resumePrompt, autonomy) => ['claude',
36
+ ...(autonomy === 'approval-free' ? ['--permission-mode', 'bypassPermissions'] : []),
37
+ '-p', `${RESUME_PROMPT_PREFIX}${resumePrompt}`],
38
+ autonomyMapping: 'verified',
20
39
  activity: process_1.activitySnapshot,
21
40
  safeToReplace: baseSafeToReplace,
22
41
  };
@@ -63,6 +82,13 @@ function controllerArgvOverride() {
63
82
  }
64
83
  return parsed;
65
84
  }
85
+ /** Lookup PURO del mapeo de postura por provider: sin env, sin throw, para que
86
+ * la admision pueda preguntarlo sin construir un adapter ni honrar overrides. */
87
+ function controllerAutonomyMapping(provider) {
88
+ return provider === 'claude-code' ? claudeAdapter.autonomyMapping
89
+ : provider === 'codex' ? codexAdapter.autonomyMapping
90
+ : 'unverified';
91
+ }
66
92
  function adapterFor(provider) {
67
93
  const base = provider === 'codex' ? codexAdapter
68
94
  : provider === 'claude-code' ? claudeAdapter
@@ -72,5 +98,8 @@ function adapterFor(provider) {
72
98
  const override = controllerArgvOverride();
73
99
  if (override === null)
74
100
  return base;
101
+ // El operador es dueno del argv completo: no se le inyecta la postura
102
+ // (duplicaria o contradiria sus propias flags). Sigue necesitando
103
+ // declararla para la admision, que es una afirmacion, no un argv.
75
104
  return { ...base, launchArgv: (resumePrompt) => [...override, resumePrompt] };
76
105
  }
@@ -384,7 +384,7 @@ describe('plan admit Commander wiring', () => {
384
384
  program.exitOverride();
385
385
  program.configureOutput({ writeErr: () => undefined });
386
386
  (0, plan_1.registerPlanCommand)(program, { validatePlanFile: () => valid, admitPlan: admit, readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }) });
387
- await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plans/current.md', '--provider', 'codex', '--cwd', root, '--execution-mode', 'desatendido', '--json']);
387
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plans/current.md', '--provider', 'codex', '--cwd', root, '--execution-mode', 'desatendido', '--controller-autonomy', 'approval-free', '--json']);
388
388
  expect(admit).toHaveBeenCalledWith(expect.objectContaining({ executionMode: 'desatendido', planPath: 'plans/current.md', journalCorrupt: false, journalState: expect.objectContaining({ schema: 2, planBinding: expect.objectContaining({ digest: valid.planDigest }) }) }));
389
389
  }
390
390
  finally {
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const os_1 = __importDefault(require("os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const child_process_1 = require("child_process");
10
+ const paths_1 = require("../../../src/core/journal/paths");
11
+ const git_fixture_1 = require("../../helpers/git-fixture");
12
+ const supervisor_1 = require("../../../src/commands/watch/supervisor");
13
+ const init_1 = require("../../../src/commands/watch/init");
14
+ const validate_1 = require("../../../src/core/plan/validate");
15
+ // #166: the supervisor's own admission must carry the runtime identity that
16
+ // compact v2 demands. Every other gate could be green and dispatch still blocks
17
+ // with ADMISSION_ROUTING_FACTS_REQUIRED, because these three fields were never
18
+ // supplied. The identity is operator-asserted (as on `plan admit`/`plan resolve`)
19
+ // and the receipt must match it — never the other way round.
20
+ const DIGEST = 'a'.repeat(64);
21
+ function receiptFor(runtime, now) {
22
+ return {
23
+ schema: 'routing-capabilities/v1', runtime,
24
+ recordedAt: new Date(now - 60_000).toISOString(),
25
+ expiresAt: new Date(now + 3_600_000).toISOString(),
26
+ capabilities: { artifactDelivery: 'supported', interactiveExecution: 'supported', unattendedController: 'supported',
27
+ nativeSubagents: 'supported', modelOverride: 'supported', effortOverride: 'unsupported',
28
+ observedModelEvidence: 'supported', durableResume: 'supported' },
29
+ availableSelections: [{ selector: { kind: 'model', id: 'test-model' }, effort: { kind: 'runtime-default' } }],
30
+ runtimeDefaultSelection: { selector: { kind: 'model', id: 'test-model' }, effort: { kind: 'runtime-default' } },
31
+ evidence: [], approval: { approvalId: 'approval-1', snapshotDigest: DIGEST },
32
+ };
33
+ }
34
+ describe('unattended dispatch routing identity (#166)', () => {
35
+ let cwd;
36
+ beforeEach(() => { cwd = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-dispatch-routing-')); });
37
+ afterEach(() => { fs_1.default.rmSync(cwd, { recursive: true, force: true }); });
38
+ test('asserts the operator-supplied runtime and attaches the matching receipt', () => {
39
+ const now = Date.now();
40
+ const seen = [];
41
+ const facts = (0, supervisor_1.routingFacts)('claude-code', { kind: 'native', version: '2.0.0', accountScopeDigest: DIGEST }, cwd, {
42
+ now: () => new Date(now),
43
+ readEffectivePolicy: () => ({ state: 'absent' }),
44
+ readCapabilities: (runtime) => { seen.push(runtime); return { state: 'current', receipt: receiptFor(runtime, now) }; },
45
+ });
46
+ // The identity reaches admission intact — this is the whole defect.
47
+ expect(facts?.runtime).toEqual({ target: 'claude-code', kind: 'native', version: '2.0.0', accountScopeDigest: DIGEST });
48
+ // The receipt is looked up BY the asserted runtime, not used to choose one.
49
+ expect(seen).toEqual([{ target: 'claude-code', kind: 'native', version: '2.0.0', accountScopeDigest: DIGEST }]);
50
+ expect(facts?.capabilities?.runtime).toEqual(facts?.runtime);
51
+ expect(facts?.now?.getTime()).toBe(now);
52
+ });
53
+ test('supplies no routing facts when the operator asserted no identity', () => {
54
+ expect((0, supervisor_1.routingFacts)('claude-code', undefined, cwd, {
55
+ readEffectivePolicy: () => { throw new Error('must not read policy without an asserted runtime'); },
56
+ readCapabilities: () => { throw new Error('must not read capabilities without an asserted runtime'); },
57
+ })).toBeUndefined();
58
+ });
59
+ test('refuses an identity that is not a valid runtime key', () => {
60
+ expect(() => (0, supervisor_1.routingFacts)('claude-code', { kind: 'native', version: 'not-semver', accountScopeDigest: DIGEST }, cwd, {}))
61
+ .toThrow(/runtime\.version/);
62
+ expect(() => (0, supervisor_1.routingFacts)('claude-code', { kind: 'native', version: '2.0.0', accountScopeDigest: 'short' }, cwd, {}))
63
+ .toThrow(/accountScopeDigest/);
64
+ });
65
+ test('withholds a stale receipt rather than admitting on it', () => {
66
+ const now = Date.now();
67
+ const facts = (0, supervisor_1.routingFacts)('claude-code', { kind: 'native', version: '2.0.0', accountScopeDigest: DIGEST }, cwd, {
68
+ now: () => new Date(now),
69
+ readEffectivePolicy: () => ({ state: 'absent' }),
70
+ readCapabilities: () => ({ state: 'expired' }),
71
+ });
72
+ expect(facts?.runtime).toBeDefined();
73
+ expect(facts?.capabilities).toBeUndefined();
74
+ });
75
+ });
76
+ describe('awm watch runtime identity flags (#166)', () => {
77
+ test('the compiled CLI declares the three identity flags', () => {
78
+ const result = (0, child_process_1.spawnSync)(process.execPath, [path_1.default.resolve(__dirname, '../../../dist/src/index.js'), 'watch', '--help'], {
79
+ encoding: 'utf8', env: { ...process.env, AWM_NO_UPDATE_CHECK: '1' },
80
+ });
81
+ expect(result.status).toBe(0);
82
+ for (const flag of ['--runtime-kind', '--runtime-version', '--account-scope-digest'])
83
+ expect(result.stdout).toContain(flag);
84
+ });
85
+ test('a partial identity is refused by name, before the lock is taken', () => {
86
+ const repo = (0, git_fixture_1.initRepo)();
87
+ try {
88
+ const result = (0, child_process_1.spawnSync)(process.execPath, [path_1.default.resolve(__dirname, '../../../dist/src/index.js'),
89
+ 'watch', '--provider', 'claude-code', '--runtime-kind', 'native'], {
90
+ cwd: repo, encoding: 'utf8', env: { ...process.env, AWM_NO_UPDATE_CHECK: '1' },
91
+ });
92
+ expect(result.status).toBe(1);
93
+ expect(result.stderr).toContain('--runtime-version');
94
+ expect(result.stderr).toContain('--account-scope-digest');
95
+ expect(result.stderr).not.toContain('--runtime-kind');
96
+ // The refusal happens before any journal/lock work.
97
+ expect(fs_1.default.existsSync((0, paths_1.supervisorLockPath)(repo))).toBe(false);
98
+ }
99
+ finally {
100
+ fs_1.default.rmSync(repo, { recursive: true, force: true });
101
+ }
102
+ });
103
+ test('a malformed digest is refused by the runtime key validator, not by a custody cycle', () => {
104
+ const repo = (0, git_fixture_1.initRepo)();
105
+ try {
106
+ const result = (0, child_process_1.spawnSync)(process.execPath, [path_1.default.resolve(__dirname, '../../../dist/src/index.js'),
107
+ 'watch', '--provider', 'claude-code', '--runtime-kind', 'native',
108
+ '--runtime-version', '2.0.0', '--account-scope-digest', 'not-a-digest'], {
109
+ cwd: repo, encoding: 'utf8', env: { ...process.env, AWM_NO_UPDATE_CHECK: '1' },
110
+ });
111
+ expect(result.status).toBe(1);
112
+ expect(result.stderr).toMatch(/accountScopeDigest/);
113
+ expect(fs_1.default.existsSync((0, paths_1.supervisorLockPath)(repo))).toBe(false);
114
+ }
115
+ finally {
116
+ fs_1.default.rmSync(repo, { recursive: true, force: true });
117
+ }
118
+ });
119
+ });
120
+ describe('the supervisor admission actually carries the identity (#166)', () => {
121
+ // The two suites above would both still pass if `routingFacts` were computed
122
+ // and then never attached to the admission call — which is precisely the shape
123
+ // of every defect in this chain: a test that starts one step past the gap.
124
+ // This one fails if the wiring is removed.
125
+ function boundRepo() {
126
+ const repo = (0, git_fixture_1.initRepo)();
127
+ const planPath = path_1.default.join(repo, 'docs', 'plan.md');
128
+ fs_1.default.mkdirSync(path_1.default.dirname(planPath), { recursive: true });
129
+ fs_1.default.writeFileSync(planPath, `**Modo de ejecución:** desatendido\n\n${fs_1.default.readFileSync(path_1.default.join(__dirname, '../../core/plan/fixtures/compact-slices-v2/valid.md'), 'utf8')}`);
130
+ fs_1.default.copyFileSync(path_1.default.join(__dirname, '../../core/plan/fixtures/compact-slices-v2/source.md'), path_1.default.join(repo, 'source.md'));
131
+ (0, init_1.initWatch)(repo, 'main', { path: 'docs/plan.md', report: (0, validate_1.validatePlanFile)('docs/plan.md', repo) });
132
+ return repo;
133
+ }
134
+ test('attaches the asserted runtime to the admission input it dispatches on', async () => {
135
+ const repo = boundRepo();
136
+ try {
137
+ const seen = [];
138
+ const admission = (0, supervisor_1.defaultDispatchAdmission)(repo, 'main', 'claude-code', { kind: 'native', version: '2.0.0', accountScopeDigest: DIGEST }, {
139
+ admit: (async (input) => { seen.push(input); return { state: 'admitted' }; }),
140
+ readEffectivePolicy: () => ({ state: 'absent' }),
141
+ readCapabilities: () => ({ state: 'absent' }),
142
+ });
143
+ await admission();
144
+ expect(seen).toHaveLength(1);
145
+ expect(seen[0].routing).toBeDefined();
146
+ expect(seen[0].routing.runtime).toEqual({ target: 'claude-code', kind: 'native', version: '2.0.0', accountScopeDigest: DIGEST });
147
+ }
148
+ finally {
149
+ fs_1.default.rmSync(repo, { recursive: true, force: true });
150
+ }
151
+ });
152
+ test('dispatches without routing facts when no identity was asserted', async () => {
153
+ const repo = boundRepo();
154
+ try {
155
+ const seen = [];
156
+ const admission = (0, supervisor_1.defaultDispatchAdmission)(repo, 'main', 'claude-code', undefined, {
157
+ admit: (async (input) => { seen.push(input); return { state: 'admitted' }; }),
158
+ });
159
+ await admission();
160
+ expect(seen).toHaveLength(1);
161
+ expect(seen[0].routing).toBeUndefined();
162
+ }
163
+ finally {
164
+ fs_1.default.rmSync(repo, { recursive: true, force: true });
165
+ }
166
+ });
167
+ });
168
+ describe('the config carries the identity to the admission (#166)', () => {
169
+ // Covers the plumbing between `awm watch --runtime-*` and the gate. Without
170
+ // it, `cfg.routingIdentity` could be dropped at the constructor and every
171
+ // other test here would still pass.
172
+ function boundRepo() {
173
+ const repo = (0, git_fixture_1.initRepo)();
174
+ const planPath = path_1.default.join(repo, 'docs', 'plan.md');
175
+ fs_1.default.mkdirSync(path_1.default.dirname(planPath), { recursive: true });
176
+ fs_1.default.writeFileSync(planPath, `**Modo de ejecución:** desatendido\n\n${fs_1.default.readFileSync(path_1.default.join(__dirname, '../../core/plan/fixtures/compact-slices-v2/valid.md'), 'utf8')}`);
177
+ fs_1.default.copyFileSync(path_1.default.join(__dirname, '../../core/plan/fixtures/compact-slices-v2/source.md'), path_1.default.join(repo, 'source.md'));
178
+ (0, init_1.initWatch)(repo, 'main', { path: 'docs/plan.md', report: (0, validate_1.validatePlanFile)('docs/plan.md', repo) });
179
+ return repo;
180
+ }
181
+ test('admissionForConfig forwards cfg.routingIdentity to the admission input', async () => {
182
+ const repo = boundRepo();
183
+ try {
184
+ const seen = [];
185
+ const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, provider: 'claude-code', routingIdentity: { kind: 'native', version: '2.0.0', accountScopeDigest: DIGEST } };
186
+ await (0, supervisor_1.admissionForConfig)(repo, 'main', cfg, {
187
+ admit: (async (input) => { seen.push(input); return { state: 'admitted' }; }),
188
+ readEffectivePolicy: () => ({ state: 'absent' }),
189
+ readCapabilities: () => ({ state: 'absent' }),
190
+ })();
191
+ expect(seen[0].routing?.runtime)
192
+ .toEqual({ target: 'claude-code', kind: 'native', version: '2.0.0', accountScopeDigest: DIGEST });
193
+ }
194
+ finally {
195
+ fs_1.default.rmSync(repo, { recursive: true, force: true });
196
+ }
197
+ });
198
+ });
@@ -196,7 +196,7 @@ while true; do sleep 1; done
196
196
  throw new Error(`fixture runSensors failed: ${JSON.stringify({ sensorReport, live })}`);
197
197
  }
198
198
  expect(sensorReport).toMatchObject({ source: { kind: 'logical', registry: 'baseline' } });
199
- const admission = (0, child_process_1.spawnSync)(process.execPath, [CLI, 'plan', 'admit', 'plans/fixture.md', '--provider', 'codex', '--cwd', '.', '--execution-mode', 'desatendido', '--require-current', '--verify-sensors', '--json'], { cwd: repo, env, encoding: 'utf8' });
199
+ const admission = (0, child_process_1.spawnSync)(process.execPath, [CLI, 'plan', 'admit', 'plans/fixture.md', '--provider', 'codex', '--cwd', '.', '--execution-mode', 'desatendido', '--controller-autonomy', 'approval-free', '--require-current', '--verify-sensors', '--json'], { cwd: repo, env, encoding: 'utf8' });
200
200
  if (admission.status !== 0)
201
201
  throw new Error(`fixture admission failed: ${admission.stdout}${admission.stderr}`);
202
202
  });
@@ -230,7 +230,7 @@ while true; do sleep 1; done
230
230
  // in-repo log would make the real currentness/admission gate reject
231
231
  // the next tick before it can collect an exited job sidecar.
232
232
  const out = fs_1.default.openSync(path_1.default.join(stubBin, `sup-${children.length}.log`), 'a');
233
- const child = (0, child_process_1.spawn)(process.execPath, [CLI, 'watch', '--provider', provider], {
233
+ const child = (0, child_process_1.spawn)(process.execPath, [CLI, 'watch', '--provider', provider, '--controller-autonomy', 'approval-free'], {
234
234
  cwd: repo, env, detached: true, stdio: ['ignore', out, out],
235
235
  });
236
236
  children.push(child);
@@ -132,9 +132,41 @@ describe('admitPlan', () => {
132
132
  it('admits unattended work only with an exact schema-2 plan binding', async () => {
133
133
  const unattended = { ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } };
134
134
  const journal = { ...(0, types_1.emptyState)('main'), schema: 2, planBinding: { path: 'docs/plan.md', digest: 'a'.repeat(64), schema: 'compact-slices/v1', executionMode: 'desatendido', boundAt: '2026-09-15T00:00:00.000Z' } };
135
- const admitted = await (0, admission_1.admitPlan)({ plan: unattended, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], journalState: journal, planPath: 'docs/plan.md' });
135
+ // #168: the posture is now part of what makes unattended work admissible,
136
+ // so this case declares it; the cases below pin what happens without it.
137
+ const base = { plan: unattended, cwd: process.cwd(), enabledAgents: ['claude-code'], planPath: 'docs/plan.md', controllerAutonomy: 'approval-free' };
138
+ const admitted = await (0, admission_1.admitPlan)({ ...base, provider: 'claude-code', journalState: journal });
136
139
  expect(admitted).toMatchObject({ state: 'admitted', journal: 'current', executionMode: 'desatendido' });
137
- const stale = await (0, admission_1.admitPlan)({ plan: unattended, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], journalState: { ...journal, planBinding: { ...journal.planBinding, digest: 'b'.repeat(64) } }, planPath: 'docs/plan.md' });
140
+ const stale = await (0, admission_1.admitPlan)({ ...base, provider: 'claude-code', journalState: { ...journal, planBinding: { ...journal.planBinding, digest: 'b'.repeat(64) } } });
138
141
  expect(stale).toMatchObject({ state: 'blocked', journal: 'stale' });
139
142
  });
143
+ describe('unattended dispatch needs a controller that can actually act (#168)', () => {
144
+ const unattended = () => ({ ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } });
145
+ const journal = () => ({ ...(0, types_1.emptyState)('main'), schema: 2, planBinding: { path: 'docs/plan.md', digest: 'a'.repeat(64), schema: 'compact-slices/v1', executionMode: 'desatendido', boundAt: '2026-09-15T00:00:00.000Z' } });
146
+ it('blocks claude-code when the operator declared no posture', async () => {
147
+ const report = await (0, admission_1.admitPlan)({ plan: unattended(), provider: 'claude-code', cwd: process.cwd(), enabledAgents: ['claude-code'], journalState: journal(), planPath: 'docs/plan.md' });
148
+ expect(report.state).toBe('blocked');
149
+ expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CONTROLLER_AUTONOMY_REQUIRED' });
150
+ expect(report.capabilityResolution?.capabilities.unattendedController).toBe('unverified');
151
+ });
152
+ it('blocks codex too when no posture was declared', async () => {
153
+ const report = await (0, admission_1.admitPlan)({ plan: unattended(), provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], journalState: journal(), planPath: 'docs/plan.md' });
154
+ expect(report.state).toBe('blocked');
155
+ expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CONTROLLER_AUTONOMY_REQUIRED' });
156
+ });
157
+ it('admits codex on the declaration alone, since AWM cannot verify its posture either way', async () => {
158
+ const report = await (0, admission_1.admitPlan)({ plan: unattended(), provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], journalState: journal(), planPath: 'docs/plan.md', controllerAutonomy: 'approval-free' });
159
+ expect(report.state).toBe('admitted');
160
+ });
161
+ it('keeps the original diagnostic for a provider that never had the capability', async () => {
162
+ const report = await (0, admission_1.admitPlan)({ plan: unattended(), provider: 'cursor', cwd: process.cwd(), enabledAgents: ['cursor'], journalState: journal(), planPath: 'docs/plan.md', controllerAutonomy: 'approval-free' });
163
+ expect(report.state).toBe('blocked');
164
+ expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CAPABILITY_UNVERIFIED' });
165
+ });
166
+ it('admits claude-code once the posture is declared', async () => {
167
+ const report = await (0, admission_1.admitPlan)({ plan: unattended(), provider: 'claude-code', cwd: process.cwd(), enabledAgents: ['claude-code'], journalState: journal(), planPath: 'docs/plan.md', controllerAutonomy: 'approval-free' });
168
+ expect(report.state).toBe('admitted');
169
+ expect(report.capabilityResolution?.outcome).toBe('native');
170
+ });
171
+ });
140
172
  });
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const child_process_1 = require("child_process");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const git_fixture_1 = require("../../helpers/git-fixture");
9
+ const path_1 = __importDefault(require("path"));
10
+ const adapter_1 = require("../../../src/core/journal/adapter");
11
+ const admission_1 = require("../../../src/core/admission");
12
+ // #168: `unattendedController: 'supported'` was a hardcoded constant while
13
+ // D-016 explicitly marked "a real LLM can occupy the controller role" as
14
+ // UNVERIFIED. Admission authorized native dispatch on that constant, and the
15
+ // launch path passed no autonomy posture — so the controller blocked on its
16
+ // first tool call and died as a stall. These tests pin both halves.
17
+ describe('controller autonomy posture (#168)', () => {
18
+ test('claude-code maps the posture onto the flag its own --help documents', () => {
19
+ const argv = (0, adapter_1.adapterFor)('claude-code').launchArgv('resume-next-action', 'approval-free');
20
+ expect(argv.slice(0, 4)).toEqual(['claude', '--permission-mode', 'bypassPermissions', '-p']);
21
+ // The prompt stays the LAST argument, as every adapter promises.
22
+ expect(argv[argv.length - 1]).toContain('resume-next-action');
23
+ });
24
+ test('without a declared posture the launch argv is unchanged — the shipped bug', () => {
25
+ expect((0, adapter_1.adapterFor)('claude-code').launchArgv('resume-next-action')).toEqual([
26
+ 'claude', '-p', expect.stringContaining('resume-next-action'),
27
+ ]);
28
+ });
29
+ test('codex declares its mapping unverified rather than inventing a flag', () => {
30
+ // The R0 evidence captured `codex --help`, never `codex exec --help`, so
31
+ // where the approval flag goes on the subcommand is not established.
32
+ expect((0, adapter_1.controllerAutonomyMapping)('codex')).toBe('unverified');
33
+ expect((0, adapter_1.controllerAutonomyMapping)('claude-code')).toBe('verified');
34
+ expect((0, adapter_1.adapterFor)('codex').launchArgv('resume-next-action', 'approval-free'))
35
+ .toEqual(['codex', 'exec', expect.stringContaining('resume-next-action')]);
36
+ });
37
+ test('the posture vocabulary refuses anything it has not measured', () => {
38
+ expect(adapter_1.CONTROLLER_AUTONOMIES).toEqual(['approval-free']);
39
+ for (const invalid of ['acceptEdits', 'bypassPermissions', 'never', '', 'full', undefined, 7]) {
40
+ expect((0, adapter_1.isControllerAutonomy)(invalid)).toBe(false);
41
+ }
42
+ expect((0, adapter_1.isControllerAutonomy)('approval-free')).toBe(true);
43
+ });
44
+ });
45
+ describe('unattendedController stops being asserted for free (#168)', () => {
46
+ test('degrades to unverified when no posture was declared', () => {
47
+ expect(admission_1.PROVIDER_EXECUTION_CAPABILITIES['claude-code'].unattendedController).toBe('supported');
48
+ expect((0, admission_1.unattendedCapabilities)('claude-code', undefined).unattendedController).toBe('unverified');
49
+ });
50
+ test('honours the operator declaration even where AWM injects no flag', () => {
51
+ // AWM cannot verify codex's posture mapping, but the operator may carry
52
+ // approval policy in their own config. Refusing to guess the flag is not
53
+ // the same as refusing the operator — same doctrine as --expected-digest.
54
+ expect((0, adapter_1.controllerAutonomyMapping)('codex')).toBe('unverified');
55
+ expect((0, admission_1.unattendedCapabilities)('codex', 'approval-free').unattendedController).toBe('supported');
56
+ });
57
+ test('is supported once the posture is declared', () => {
58
+ expect((0, admission_1.unattendedCapabilities)('claude-code', 'approval-free').unattendedController).toBe('supported');
59
+ });
60
+ test('never promotes a provider that never had the capability', () => {
61
+ expect(admission_1.PROVIDER_EXECUTION_CAPABILITIES.cursor.unattendedController).toBe('unverified');
62
+ expect((0, admission_1.unattendedCapabilities)('cursor', 'approval-free').unattendedController).toBe('unverified');
63
+ });
64
+ });
65
+ describe('awm watch surfaces the posture on the compiled binary (#168)', () => {
66
+ const cli = path_1.default.resolve(__dirname, '../../../dist/src/index.js');
67
+ test('declares the flag and its vocabulary', () => {
68
+ const help = (0, child_process_1.spawnSync)(process.execPath, [cli, 'watch', '--help'], { encoding: 'utf8', env: { ...process.env, AWM_NO_UPDATE_CHECK: '1' } });
69
+ expect(help.status).toBe(0);
70
+ expect(help.stdout).toContain('--controller-autonomy');
71
+ expect(help.stdout).toContain('approval-free');
72
+ });
73
+ test('refuses a posture it cannot honour instead of launching a controller that stalls', () => {
74
+ // Hermetic repo on a real branch: without a cwd of its own this inherits
75
+ // the checkout, and a detached HEAD makes `currentBranch()` fail first —
76
+ // which is exactly how this test passed locally and failed on all six CI
77
+ // legs. The assertion must depend on the flag, not on ambient git state.
78
+ const repo = (0, git_fixture_1.initRepo)();
79
+ try {
80
+ const result = (0, child_process_1.spawnSync)(process.execPath, [cli, 'watch', '--provider', 'claude-code', '--controller-autonomy', 'bypassPermissions'], { cwd: repo, encoding: 'utf8', env: { ...process.env, AWM_NO_UPDATE_CHECK: '1' } });
81
+ expect(result.status).toBe(1);
82
+ expect(result.stderr).toContain('--controller-autonomy invalido');
83
+ expect(result.stderr).toContain('approval-free');
84
+ }
85
+ finally {
86
+ fs_1.default.rmSync(repo, { recursive: true, force: true });
87
+ }
88
+ });
89
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "9.10.2",
3
+ "version": "9.11.1",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
Binary file
Binary file