agentic-workflow-manager 9.11.0 → 9.11.2

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.
@@ -156,7 +156,6 @@ function spliceGenerated(markdown, generated) {
156
156
  exports.DOC_PATH = path_1.default.join(__dirname, '..', '..', 'docs', 'support-matrix.md');
157
157
  /* istanbul ignore next — entrypoint de CLI, ejercitado por el test via las funciones puras */
158
158
  if (require.main === module) {
159
- // eslint-disable-next-line @typescript-eslint/no-var-requires
160
159
  const fs = require('fs');
161
160
  const current = fs.readFileSync(exports.DOC_PATH, 'utf-8');
162
161
  fs.writeFileSync(exports.DOC_PATH, spliceGenerated(current, renderProviderTables()), 'utf-8');
@@ -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;
@@ -40,7 +40,6 @@ const defaultDeps = {
40
40
  applyInstallPlan: install_transaction_1.applyInstallPlan,
41
41
  resyncInstalledHooks: resync_1.resyncInstalledHooks,
42
42
  offerSelfUpdate: async (mode) => {
43
- // eslint-disable-next-line @typescript-eslint/no-var-requires
44
43
  const { offerSelfUpdate: real } = require('../core/update-check');
45
44
  await real({ mode });
46
45
  },
@@ -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. */
@@ -8,8 +8,9 @@ const child_process_1 = require("child_process");
8
8
  const init_1 = require("./init");
9
9
  const supervisor_1 = require("./supervisor");
10
10
  const capabilities_1 = require("../../core/model-policy/capabilities");
11
- const process_1 = require("../../core/journal/process");
12
11
  const adapter_1 = require("../../core/journal/adapter");
12
+ const process_1 = require("../../core/journal/process");
13
+ const adapter_2 = require("../../core/journal/adapter");
13
14
  const context_1 = require("../../core/tracks/context");
14
15
  const concurrency_1 = require("../../core/tracks/concurrency");
15
16
  const validate_1 = require("../../core/plan/validate");
@@ -45,7 +46,7 @@ function registerWatchCommand(program) {
45
46
  .description('supervisor durable: ejecuta jobs, releva controladores caidos, nunca mata trabajo vivo')
46
47
  .option('--init', 'bootstrap: crea el journal de la rama actual, detecta verificadores y sale')
47
48
  .option('--plan <path>', 'plan compacto desatendido que se vincula al inicializar')
48
- .option('--provider <p>', adapter_1.WATCH_PROVIDERS.join(' | '), 'codex')
49
+ .option('--provider <p>', adapter_2.WATCH_PROVIDERS.join(' | '), 'codex')
49
50
  .option('--heartbeat-timeout <min>', 'minutos de silencio de heartbeat', '5')
50
51
  .option('--activity-window <min>', 'minutos extra sin actividad de proceso', '10')
51
52
  .option('--max-parallel <n>', 'tope de tracks ACTIVE simultáneos (default: derivado del benchmark empaquetado)')
@@ -55,6 +56,9 @@ function registerWatchCommand(program) {
55
56
  .option('--runtime-kind <kind>', 'identidad de runtime para despacho compact v2')
56
57
  .option('--runtime-version <version>', 'identidad de runtime para despacho compact v2')
57
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(' | ')})`)
58
62
  .action(async (opts) => {
59
63
  const repo = process.cwd();
60
64
  const branch = currentBranch(repo);
@@ -93,8 +97,8 @@ function registerWatchCommand(program) {
93
97
  // desconocido, pero recien en el primer tick — con el journal escrito y el
94
98
  // lock tomado, y el error saliendo del supervisor en vez de del flag que lo
95
99
  // causo. Un typo en `--provider` tiene que costar un mensaje, no un ciclo.
96
- if (!(0, adapter_1.isWatchProvider)(opts.provider)) {
97
- 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`);
98
102
  process.exitCode = 1;
99
103
  return;
100
104
  }
@@ -122,6 +126,11 @@ function registerWatchCommand(program) {
122
126
  }
123
127
  routingIdentity = { kind: String(opts.runtimeKind), version: String(opts.runtimeVersion), accountScopeDigest: String(opts.accountScopeDigest) };
124
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`);
131
+ process.exitCode = 1;
132
+ return;
133
+ }
125
134
  const cfg = {
126
135
  ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG,
127
136
  provider: opts.provider,
@@ -129,6 +138,7 @@ function registerWatchCommand(program) {
129
138
  activityWindowMs: minutes('--activity-window', opts.activityWindow),
130
139
  maxParallelTracks: opts.maxParallel !== undefined ? (0, concurrency_1.parseMaxParallel)(opts.maxParallel) : (0, concurrency_1.loadDefaultParallelism)(),
131
140
  ...(routingIdentity === undefined ? {} : { routingIdentity }),
141
+ ...(opts.controllerAutonomy === undefined ? {} : { controllerAutonomy: opts.controllerAutonomy }),
132
142
  };
133
143
  process.stdout.write(`awm watch: supervisor activo (${cfg.provider}) — Ctrl-C para terminar\n`);
134
144
  await (0, supervisor_1.runSupervisorLoop)(repo, branch, cfg);
@@ -44,7 +44,7 @@ function routingFacts(provider, identity, cwd, deps = {}) {
44
44
  return { runtime, policy: policy.state === 'approved' ? policy.policy : undefined,
45
45
  capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined, now: at };
46
46
  }
47
- function defaultDispatchAdmission(repoRoot, branch, provider, identity, deps = {}) {
47
+ function defaultDispatchAdmission(repoRoot, branch, provider, identity, deps = {}, autonomy) {
48
48
  return async () => {
49
49
  const observed = (0, store_1.readJournal)(repoRoot, branch);
50
50
  const binding = observed.state?.schema === 2 ? observed.state.planBinding : undefined;
@@ -56,7 +56,8 @@ function defaultDispatchAdmission(repoRoot, branch, provider, identity, deps = {
56
56
  return (deps.admit ?? registry_contracts_1.admitRegistryPlan)({ plan, provider, cwd: repoRoot, enabledAgents: preferences.enabledAgents,
57
57
  executionMode: 'desatendido', requireCurrent: true, verifySensors: true,
58
58
  journalState: observed.state, journalCorrupt: observed.corrupt, planPath: binding.path,
59
- routing: routingFacts(provider, identity, repoRoot, deps) });
59
+ routing: routingFacts(provider, identity, repoRoot, deps),
60
+ controllerAutonomy: autonomy });
60
61
  };
61
62
  }
62
63
  /** R7/C3/C4 (Task 12) + fix post-review #2 (re-derivado desde cero tras
@@ -152,7 +153,7 @@ exports.DEFAULT_SUPERVISOR_CONFIG = {
152
153
  /** Single place the supervisor's config becomes its admission, so the runtime
153
154
  * identity cannot be dropped silently between the flag and the gate (#166). */
154
155
  function admissionForConfig(repoRoot, branch, cfg, deps = {}) {
155
- return defaultDispatchAdmission(repoRoot, branch, cfg.provider, cfg.routingIdentity, deps);
156
+ return defaultDispatchAdmission(repoRoot, branch, cfg.provider, cfg.routingIdentity, deps, cfg.controllerAutonomy);
156
157
  }
157
158
  // 'frozen' (R5.2/R6.3, Task 10): SOLO puede ocurrir en el journal de un
158
159
  // TRACK individual (nunca en el del plan) — el track terminó, con las 6
@@ -223,7 +224,7 @@ class Supervisor {
223
224
  return 'custody';
224
225
  }
225
226
  try {
226
- (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);
227
228
  return 'ok';
228
229
  }
229
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 {
@@ -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
  });
@@ -77,7 +77,6 @@ describe('runExport (engine end-to-end)', () => {
77
77
  // La lectura sale exclusivamente de roots: un root vacío no resuelve nada.
78
78
  // Control negativo real: contentRoots() (el fallback al registry instalado)
79
79
  // no debe invocarse en absoluto cuando opts.roots viene dado explícitamente.
80
- // eslint-disable-next-line @typescript-eslint/no-var-requires
81
80
  const registries = require('../../../src/core/registries');
82
81
  const spy = jest.spyOn(registries, 'contentRoots');
83
82
  const empty = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-empty-root-'));
@@ -7,7 +7,6 @@ const fs_1 = __importDefault(require("fs"));
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const os_1 = __importDefault(require("os"));
9
9
  const pack_1 = require("../../../src/core/export/pack");
10
- // eslint-disable-next-line @typescript-eslint/no-var-requires
11
10
  const childProcess = require('child_process');
12
11
  const okZip = (cwd, zipName) => {
13
12
  fs_1.default.writeFileSync(path_1.default.join(cwd, zipName), 'fake-zip');
@@ -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
+ });
@@ -0,0 +1,56 @@
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 path_1 = __importDefault(require("path"));
8
+ // #171: the lint gate linted 14 files — all js/mjs — and exited 0 on a
9
+ // TypeScript CLI. `eslint.config.awm.mjs` imports a project config that does
10
+ // not exist, swallows the failure, and its remaining objects declare no
11
+ // `files`, so eslint's default universe (js/mjs/cjs) left every .ts out.
12
+ //
13
+ // The gate was green for months and the `lint` sensor certified on it, which
14
+ // `plan admit --verify-sensors` then consumes as the unattended handoff gate.
15
+ //
16
+ // This asserts on the SET OF FILES LINTED, never on the exit code. An exit-code
17
+ // assertion is exactly what let this survive: a lint that examines nothing
18
+ // exits 0.
19
+ const CLI_ROOT = path_1.default.resolve(__dirname, '../..');
20
+ /** One eslint run for the whole suite. Invoked through node against eslint's own
21
+ * js entrypoint rather than `npx`: on Windows `npx` resolves to `npx.cmd`, and
22
+ * `execFileSync` cannot execute a `.cmd` without a shell — the exact trap this
23
+ * repo already documents in tests/integration/published-doctor-evidence.e2e.test.ts,
24
+ * and the one this test hit on both Windows legs of its first CI run. Going
25
+ * through `process.execPath` needs no shell, so no argument escaping either. */
26
+ function lint() {
27
+ const raw = (0, child_process_1.execFileSync)(process.execPath, [
28
+ path_1.default.join(CLI_ROOT, 'node_modules', 'eslint', 'bin', 'eslint.js'),
29
+ '.', '--config', 'eslint.config.awm.mjs', '--format', 'json',
30
+ ], { cwd: CLI_ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
31
+ return JSON.parse(raw);
32
+ }
33
+ describe('the lint gate covers TypeScript (#171)', () => {
34
+ jest.setTimeout(10 * 60_000);
35
+ const results = lint();
36
+ const files = results.map(entry => entry.filePath);
37
+ const typescript = files.filter(file => file.endsWith('.ts'));
38
+ test('lints the TypeScript sources, not only the js/mjs tooling', () => {
39
+ // A floor, not a lock: adding sources must not fail this.
40
+ expect(typescript.length).toBeGreaterThan(400);
41
+ expect(typescript.some(file => file.includes(`${path_1.default.sep}src${path_1.default.sep}`))).toBe(true);
42
+ expect(typescript.some(file => file.includes(`${path_1.default.sep}tests${path_1.default.sep}`))).toBe(true);
43
+ });
44
+ test('lints this very file, so the assertion cannot be vacuous', () => {
45
+ expect(files).toContain(path_1.default.join(CLI_ROOT, 'tests', 'structural', 'lint-covers-typescript.test.ts'));
46
+ });
47
+ test('reports no unresolved rule references', () => {
48
+ // Four stale `eslint-disable @typescript-eslint/no-var-requires` comments
49
+ // survived precisely because .ts was never linted: eslint errors on a rule
50
+ // it cannot resolve, and nobody ever saw it. A new one must fail here
51
+ // rather than wait for the next person to turn the gate on.
52
+ const unresolved = results.flatMap(entry => entry.messages.filter(m => /Definition for rule .* was not found/.test(m.message))
53
+ .map(m => `${entry.filePath}: ${m.message}`));
54
+ expect(unresolved).toEqual([]);
55
+ });
56
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "9.11.0",
3
+ "version": "9.11.2",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
@@ -59,6 +59,7 @@
59
59
  "@types/js-yaml": "4.0.9",
60
60
  "@types/node": "^25.3.0",
61
61
  "@types/semver": "^7.7.1",
62
+ "@typescript-eslint/parser": "^8.70.0",
62
63
  "dependency-cruiser": "16.10.4",
63
64
  "eslint": "10.8.1",
64
65
  "jest": "^30.2.0",
Binary file
Binary file