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.
- package/dist/src/commands/plan/index.js +5 -1
- package/dist/src/commands/watch/generations.js +5 -5
- package/dist/src/commands/watch/index.js +46 -4
- package/dist/src/commands/watch/supervisor.js +40 -5
- package/dist/src/core/admission/index.js +34 -4
- package/dist/src/core/journal/adapter.js +31 -2
- package/dist/tests/commands/plan/index.test.js +1 -1
- package/dist/tests/commands/watch/dispatch-routing-identity.test.js +198 -0
- package/dist/tests/commands/watch/e2e-crash.test.js +2 -2
- package/dist/tests/core/admission/index.test.js +34 -2
- package/dist/tests/core/journal/controller-autonomy.test.js +89 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -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
|
-
|
|
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
|
|
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>',
|
|
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,
|
|
90
|
-
process.stderr.write(`--provider invalido: ${String(opts.provider)} (validos: ${
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
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)({
|
|
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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|