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.
- package/dist/scripts/support-matrix.js +0 -1
- package/dist/src/commands/plan/index.js +5 -1
- package/dist/src/commands/update.js +0 -1
- package/dist/src/commands/watch/generations.js +5 -5
- package/dist/src/commands/watch/index.js +14 -4
- package/dist/src/commands/watch/supervisor.js +5 -4
- 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/e2e-crash.test.js +2 -2
- package/dist/tests/core/admission/index.test.js +34 -2
- package/dist/tests/core/export/engine.test.js +0 -1
- package/dist/tests/core/export/pack.test.js +0 -1
- package/dist/tests/core/journal/controller-autonomy.test.js +89 -0
- package/dist/tests/structural/lint-covers-typescript.test.js +56 -0
- package/package.json +2 -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
|
@@ -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
|
-
|
|
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>',
|
|
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,
|
|
97
|
-
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`);
|
|
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 =
|
|
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 {
|
|
@@ -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
|
});
|
|
@@ -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.
|
|
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
|
|
Binary file
|
|
Binary file
|