agentic-workflow-manager 9.7.1 → 9.9.0

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.
Files changed (107) hide show
  1. package/README.md +5 -1
  2. package/dist/src/commands/job/exec-wrapper.js +53 -3
  3. package/dist/src/commands/job/gate.js +18 -0
  4. package/dist/src/commands/job/index.js +61 -0
  5. package/dist/src/commands/model-policy/index.js +73 -0
  6. package/dist/src/commands/plan/index.js +182 -7
  7. package/dist/src/commands/sensors/bootstrap.js +36 -5
  8. package/dist/src/commands/sensors/compatibility/contract.js +19 -4
  9. package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
  10. package/dist/src/commands/sensors/compatibility/live.js +18 -1
  11. package/dist/src/commands/sensors/compatibility/probe.js +8 -2
  12. package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
  13. package/dist/src/commands/sensors/coverage/evidence.js +19 -10
  14. package/dist/src/commands/sensors/coverage/index.js +11 -2
  15. package/dist/src/commands/sensors/coverage/resolve.js +10 -0
  16. package/dist/src/commands/sensors/exec.js +16 -1
  17. package/dist/src/commands/sensors/migrate.js +62 -2
  18. package/dist/src/commands/sensors/prepare.js +2 -0
  19. package/dist/src/commands/sensors/result.js +2 -0
  20. package/dist/src/commands/sensors/run.js +64 -1
  21. package/dist/src/commands/sensors/status.js +6 -4
  22. package/dist/src/commands/watch/apply.js +32 -0
  23. package/dist/src/commands/watch/archive-unused.js +138 -0
  24. package/dist/src/commands/watch/generations.js +32 -9
  25. package/dist/src/commands/watch/index.js +108 -2
  26. package/dist/src/commands/watch/init.js +149 -18
  27. package/dist/src/commands/watch/supervisor.js +167 -13
  28. package/dist/src/commands/watch/teardown-driver.js +13 -13
  29. package/dist/src/commands/watch/track-state.js +59 -0
  30. package/dist/src/commands/watch/tracks.js +38 -102
  31. package/dist/src/core/admission/index.js +231 -0
  32. package/dist/src/core/admission/registry-contracts.js +290 -0
  33. package/dist/src/core/journal/fingerprint.js +35 -1
  34. package/dist/src/core/journal/paths.js +11 -0
  35. package/dist/src/core/journal/requests.js +1 -1
  36. package/dist/src/core/journal/store.js +146 -4
  37. package/dist/src/core/journal/types.js +57 -2
  38. package/dist/src/core/migration/historical-provenance.js +18 -0
  39. package/dist/src/core/migration/index.js +322 -0
  40. package/dist/src/core/model-policy/canonical.js +18 -0
  41. package/dist/src/core/model-policy/capabilities.js +197 -0
  42. package/dist/src/core/model-policy/capability-types.js +2 -0
  43. package/dist/src/core/model-policy/journal.js +124 -0
  44. package/dist/src/core/model-policy/paths.js +12 -0
  45. package/dist/src/core/model-policy/resolve.js +116 -0
  46. package/dist/src/core/model-policy/store.js +182 -0
  47. package/dist/src/core/model-policy/types.js +2 -0
  48. package/dist/src/core/model-policy/validate.js +112 -0
  49. package/dist/src/core/plan/identity.js +77 -0
  50. package/dist/src/core/plan/validate.js +191 -78
  51. package/dist/src/index.js +2 -0
  52. package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
  53. package/dist/tests/commands/job/gate-reconcile.test.js +40 -10
  54. package/dist/tests/commands/job/gate.test.js +9 -0
  55. package/dist/tests/commands/job/routing-report-cli.test.js +46 -0
  56. package/dist/tests/commands/job/routing.test.js +11 -0
  57. package/dist/tests/commands/model-policy/index.test.js +57 -0
  58. package/dist/tests/commands/pin.test.js +23 -23
  59. package/dist/tests/commands/plan/index.test.js +682 -14
  60. package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
  61. package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
  62. package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
  63. package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
  64. package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
  65. package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
  66. package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
  67. package/dist/tests/commands/sensors/exec.test.js +112 -0
  68. package/dist/tests/commands/sensors/run-changed.test.js +11 -0
  69. package/dist/tests/commands/sensors/run.test.js +15 -2
  70. package/dist/tests/commands/sensors/status.test.js +3 -3
  71. package/dist/tests/commands/watch/apply.test.js +12 -0
  72. package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
  73. package/dist/tests/commands/watch/generations.test.js +113 -4
  74. package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
  75. package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
  76. package/dist/tests/commands/watch/track-finalize.test.js +20 -3
  77. package/dist/tests/commands/watch/track-freeze.test.js +43 -30
  78. package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
  79. package/dist/tests/commands/watch/watch-init.test.js +244 -0
  80. package/dist/tests/core/admission/index.test.js +140 -0
  81. package/dist/tests/core/journal/fingerprint.test.js +22 -0
  82. package/dist/tests/core/journal/store.test.js +53 -0
  83. package/dist/tests/core/journal/types.test.js +7 -0
  84. package/dist/tests/core/migration/facts.test.js +289 -0
  85. package/dist/tests/core/migration/historical-provenance.test.js +25 -0
  86. package/dist/tests/core/model-policy/capabilities.test.js +365 -0
  87. package/dist/tests/core/model-policy/journal.test.js +46 -0
  88. package/dist/tests/core/model-policy/policy.test.js +47 -0
  89. package/dist/tests/core/model-policy/resolve.test.js +111 -0
  90. package/dist/tests/core/model-policy/store.test.js +177 -0
  91. package/dist/tests/core/plan/validate.test.js +360 -8
  92. package/dist/tests/core/secure-fs/native-bridge.test.js +81 -4
  93. package/dist/tests/integration/pack-e2e.test.js +4 -1
  94. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
  95. package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
  96. package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
  97. package/dist/tests/providers/injection-config.test.js +14 -6
  98. package/dist/tests/structural/directory-fsync-is-single-source.test.js +97 -0
  99. package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
  100. package/dist/tests/structural/native-release-artifacts.test.js +30 -0
  101. package/package.json +4 -3
  102. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  103. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  104. package/prebuilds/linux-arm64/secure_fs.node +0 -0
  105. package/prebuilds/linux-x64/secure_fs.node +0 -0
  106. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  107. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -8,20 +8,24 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const os_1 = __importDefault(require("os"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const child_process_1 = require("child_process");
11
+ const validate_1 = require("../../../src/core/plan/validate");
11
12
  const plan_1 = require("../../../src/commands/plan");
13
+ const init_1 = require("../../../src/commands/watch/init");
14
+ const providers_1 = require("../../../src/providers");
15
+ const registry_1 = require("../../../src/core/renderers/registry");
12
16
  const stdoutWrite = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
13
- const valid = {
14
- state: 'valid', schema: 'compact-slices/v1', manifest: {
15
- schema: 'compact-slices/v1', planId: 'r4-plan', requirements: ['R4-VAL-1', 'R4-VAL-4'],
16
- sources: [], commands: [], slices: [], closureCommands: [],
17
- },
18
- };
17
+ const repositoryRoot = path_1.default.resolve(__dirname, '../../../..');
18
+ const approved = (0, validate_1.validatePlanFile)('docs/plans/2026-09-14-compact-only-bootstrap-plan.md', repositoryRoot);
19
+ if (approved.state !== 'valid')
20
+ throw new Error(`tracked bootstrap plan must validate before command tests: ${approved.state}`);
21
+ const valid = approved;
19
22
  const invalid = {
20
23
  state: 'invalid', diagnostics: [{ code: 'PLAN_MARKERS', message: 'compact markers must occur once in order' }],
21
24
  };
22
25
  const unsupported = {
23
26
  state: 'unsupported', schema: 'compact-slices/v2', diagnostics: [{ code: 'PLAN_UNSUPPORTED_SCHEMA', message: 'unsupported compact plan schema; update the CLI' }],
24
27
  };
28
+ const migration = { state: 'migration-required', reason: 'unmarked-plan' };
25
29
  function commandFor(report, calls = []) {
26
30
  const program = new commander_1.Command();
27
31
  program.exitOverride();
@@ -44,23 +48,83 @@ describe('plan validate Commander wiring', () => {
44
48
  stdoutWrite.mockRestore();
45
49
  process.exitCode = previousExitCode;
46
50
  });
51
+ it('returns v1 not-required before reading personal policy or capability state', async () => {
52
+ const policy = jest.fn();
53
+ const capabilities = jest.fn();
54
+ const program = new commander_1.Command();
55
+ program.exitOverride();
56
+ program.configureOutput({ writeErr: () => undefined });
57
+ (0, plan_1.registerPlanCommand)(program, { validatePlanFile: () => valid, readEffectivePolicy: policy, readCapabilities: capabilities });
58
+ await program.parseAsync(['node', 'awm', 'plan', 'resolve', 'plans/r4.md', '--provider', 'codex', '--runtime-kind', 'native', '--runtime-version', '1.0.0', '--account-scope-digest', 'a'.repeat(64), '--role', 'controller', '--json']);
59
+ expect(JSON.parse(String(stdoutWrite.mock.calls[0][0]))).toEqual({ state: 'not-required', reason: 'v1-without-opt-in' });
60
+ expect(policy).not.toHaveBeenCalled();
61
+ expect(capabilities).not.toHaveBeenCalled();
62
+ });
47
63
  it('emits deterministic human output for a valid compact plan', async () => {
48
64
  await commandFor(valid).parseAsync(['node', 'awm', 'plan', 'validate', 'plans/r4.md']);
49
- expect(String(stdoutWrite.mock.calls[0][0])).toBe('Plan validation: valid "plans/r4.md" (compact-slices/v1; 0 slices; 2 requirements; complete ownership)\n');
65
+ expect(String(stdoutWrite.mock.calls[0][0])).toBe('Plan validation: valid "plans/r4.md" (compact-slices/v1; 1 slices; 5 requirements; complete ownership)\n');
50
66
  expect(process.exitCode).toBe(0);
51
67
  });
52
68
  it('emits one stable JSON object without an update banner', async () => {
53
69
  await commandFor(valid).parseAsync(['node', 'awm', 'plan', 'validate', 'plans/r4.md', '--json']);
54
70
  expect(JSON.parse(String(stdoutWrite.mock.calls[0][0]))).toEqual({
55
- state: 'valid', path: 'plans/r4.md', schema: 'compact-slices/v1', planId: 'r4-plan',
56
- requirements: 2, sources: 0, commands: 0, slices: 0, completeOwnership: true,
71
+ state: 'valid', path: 'plans/r4.md', schema: 'compact-slices/v1', planId: 'issue-126-compact-only-bootstrap',
72
+ planDigest: valid.planDigest, requirements: 5, sources: 6, commands: 7, slices: 1, completeOwnership: true,
57
73
  });
58
74
  expect(String(stdoutWrite.mock.calls[0][0])).not.toContain('update');
59
75
  });
60
- it('keeps a legacy plan on the existing full-quality path with exit 0', async () => {
61
- await commandFor({ state: 'legacy' }).parseAsync(['node', 'awm', 'plan', 'validate', 'legacy.md']);
62
- expect(String(stdoutWrite.mock.calls[0][0])).toBe('Plan validation: legacy "legacy.md" — existing full-quality path applies; compact optimization was not requested.\n');
63
- expect(process.exitCode).toBe(0);
76
+ it('emits deterministic migration-only human guidance and exit 2', async () => {
77
+ await commandFor(migration).parseAsync(['node', 'awm', 'plan', 'validate', 'legacy.md']);
78
+ expect(String(stdoutWrite.mock.calls[0][0])).toBe('Plan validation: migration-required "legacy.md" (unmarked-plan)\nMigrate this plan to compact-slices/v1 before execution.\n');
79
+ expect(String(stdoutWrite.mock.calls[0][0])).not.toMatch(/full-quality|alternate|legacy path/i);
80
+ expect(process.exitCode).toBe(2);
81
+ });
82
+ it('writes one stable migration JSON object before assigning exit 2', async () => {
83
+ const observedExitCodes = [];
84
+ stdoutWrite.mockImplementation(() => { observedExitCodes.push(process.exitCode); return true; });
85
+ await commandFor(migration).parseAsync(['node', 'awm', 'plan', 'validate', 'legacy.md', '--json']);
86
+ const output = String(stdoutWrite.mock.calls[0][0]);
87
+ expect(output).toBe('{"state":"migration-required","path":"legacy.md","reason":"unmarked-plan"}\n');
88
+ expect(JSON.parse(output)).toEqual({ state: 'migration-required', path: 'legacy.md', reason: 'unmarked-plan' });
89
+ expect(output).not.toMatch(/full-quality|alternate|legacy path/i);
90
+ expect(observedExitCodes).toEqual([undefined]);
91
+ expect(process.exitCode).toBe(2);
92
+ });
93
+ it.each(['human', 'json'])('rejects a malformed injected migration report in %s mode before output', async (mode) => {
94
+ const malformed = { state: 'migration-required' };
95
+ const args = ['node', 'awm', 'plan', 'validate', 'unmarked.md', ...(mode === 'json' ? ['--json'] : [])];
96
+ await expect(commandFor(malformed).parseAsync(args)).rejects.toThrow('plan validator returned an invalid migration reason');
97
+ expect(stdoutWrite).not.toHaveBeenCalled();
98
+ expect(process.exitCode).toBeUndefined();
99
+ });
100
+ it('enforces migration-only output and exit 2 through the compiled CLI without mutating an unmarked plan', () => {
101
+ const repoRoot = path_1.default.resolve(__dirname, '../../../..');
102
+ const compiled = path_1.default.join(repoRoot, 'cli', 'dist', 'src', 'index.js');
103
+ const fixtureRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-plan-unmarked-e2e-'));
104
+ const plan = path_1.default.join(fixtureRoot, 'unmarked.md');
105
+ const isolatedHome = path_1.default.join(fixtureRoot, 'home');
106
+ fs_1.default.mkdirSync(isolatedHome);
107
+ fs_1.default.writeFileSync(plan, '# Unmarked plan\n');
108
+ const before = fs_1.default.readFileSync(plan);
109
+ const environment = { ...process.env, HOME: isolatedHome, AWM_HOME: path_1.default.join(isolatedHome, '.awm') };
110
+ try {
111
+ expect(fs_1.default.existsSync(compiled)).toBe(true);
112
+ const human = (0, child_process_1.spawnSync)(process.execPath, [compiled, 'plan', 'validate', 'unmarked.md', '--cwd', fixtureRoot], {
113
+ cwd: repoRoot, env: environment, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
114
+ });
115
+ const json = (0, child_process_1.spawnSync)(process.execPath, [compiled, 'plan', 'validate', 'unmarked.md', '--cwd', fixtureRoot, '--json'], {
116
+ cwd: repoRoot, env: environment, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
117
+ });
118
+ expect(human.status).toBe(2);
119
+ expect(human.stdout).toBe('Plan validation: migration-required "unmarked.md" (unmarked-plan)\nMigrate this plan to compact-slices/v1 before execution.\n');
120
+ expect(json.status).toBe(2);
121
+ expect(JSON.parse(json.stdout)).toEqual({ state: 'migration-required', path: 'unmarked.md', reason: 'unmarked-plan' });
122
+ expect(human.stdout + json.stdout).not.toMatch(/full-quality|alternate|legacy path/i);
123
+ expect(fs_1.default.readFileSync(plan).equals(before)).toBe(true);
124
+ }
125
+ finally {
126
+ fs_1.default.rmSync(fixtureRoot, { recursive: true, force: true });
127
+ }
64
128
  });
65
129
  it.each([
66
130
  ['invalid', invalid],
@@ -139,16 +203,620 @@ describe('plan validate Commander wiring', () => {
139
203
  }
140
204
  });
141
205
  });
206
+ describe('plan admit Commander wiring', () => {
207
+ it('does not read v2 routing facts after currentness blocks', async () => {
208
+ const policy = jest.fn();
209
+ const capabilities = jest.fn();
210
+ const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
211
+ const program = new commander_1.Command();
212
+ program.exitOverride();
213
+ program.configureOutput({ writeErr: () => undefined });
214
+ (0, plan_1.registerPlanCommand)(program, { validatePlanFile: () => ({ ...valid, schema: 'compact-slices/v2', manifest: { ...valid.manifest, schema: 'compact-slices/v2', slices: valid.manifest.slices.map(slice => ({ ...slice, implementerProfile: 'mechanical' })) } }), readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }), readEffectivePolicy: policy, readCapabilities: capabilities, listRegistries: () => [], checkCurrentness: async () => ({ checkedAt: 'x', compatibility: { status: 'not-checked' }, components: [{ component: 'cli', installed: '1', latest: '2', channel: 'stable', source: 'x', checkedAt: 'x', status: 'stale', detail: 'x', remedy: 'x' }] }) });
215
+ try {
216
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', repositoryRoot, '--require-current', '--runtime-kind', 'native', '--runtime-version', '1.0.0', '--account-scope-digest', 'a'.repeat(64), '--json']);
217
+ expect(policy).not.toHaveBeenCalled();
218
+ expect(capabilities).not.toHaveBeenCalled();
219
+ }
220
+ finally {
221
+ output.mockRestore();
222
+ process.exitCode = undefined;
223
+ }
224
+ });
225
+ it.each([
226
+ ['invalid plan', invalid, 'codex', ['codex']],
227
+ ['invalid provider', valid, 'not-a-provider', ['codex']],
228
+ ['disabled provider', valid, 'codex', ['cursor']],
229
+ ])('does not read routing facts for %s even with complete runtime flags', async (_name, report, provider, enabledAgents) => {
230
+ const policy = jest.fn();
231
+ const capabilities = jest.fn();
232
+ const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
233
+ const program = new commander_1.Command();
234
+ program.exitOverride();
235
+ program.configureOutput({ writeErr: () => undefined });
236
+ (0, plan_1.registerPlanCommand)(program, { validatePlanFile: () => report, readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: [...enabledAgents], installMethod: 'symlink', defaultScope: 'local' }), readEffectivePolicy: policy, readCapabilities: capabilities });
237
+ try {
238
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', provider, '--cwd', repositoryRoot, '--runtime-kind', 'native', '--runtime-version', '1.0.0', '--account-scope-digest', 'a'.repeat(64), '--json']);
239
+ expect(policy).not.toHaveBeenCalled();
240
+ expect(capabilities).not.toHaveBeenCalled();
241
+ }
242
+ finally {
243
+ output.mockRestore();
244
+ process.exitCode = undefined;
245
+ }
246
+ });
247
+ it('derives desatendido from the canonical plan header and requires a journal without a flag', async () => {
248
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-admit-header-'));
249
+ const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
250
+ try {
251
+ (0, child_process_1.spawnSync)('git', ['init', '-q', '-b', 'main'], { cwd: root });
252
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '**Modo de ejecución:** desatendido\n');
253
+ (0, child_process_1.spawnSync)('git', ['config', 'user.email', 'test@example.invalid'], { cwd: root });
254
+ (0, child_process_1.spawnSync)('git', ['config', 'user.name', 'Test'], { cwd: root });
255
+ (0, child_process_1.spawnSync)('git', ['add', 'plan.md'], { cwd: root });
256
+ (0, child_process_1.spawnSync)('git', ['commit', '-qm', 'fixture'], { cwd: root });
257
+ const program = new commander_1.Command();
258
+ program.exitOverride();
259
+ program.configureOutput({ writeErr: () => undefined });
260
+ (0, plan_1.registerPlanCommand)(program, {
261
+ validatePlanFile: () => valid,
262
+ readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }),
263
+ });
264
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', root, '--json']);
265
+ expect(JSON.parse(String(output.mock.calls[0][0]))).toMatchObject({ state: 'blocked', executionMode: 'desatendido', journal: expect.stringMatching(/missing|corrupt/) });
266
+ }
267
+ finally {
268
+ output.mockRestore();
269
+ fs_1.default.rmSync(root, { recursive: true, force: true });
270
+ process.exitCode = undefined;
271
+ }
272
+ });
273
+ it('uses executionMode from the validated snapshot when the plan file changes before admission', async () => {
274
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-admit-snapshot-mode-'));
275
+ const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
276
+ const admittedSnapshot = { ...valid, executionMode: 'desatendido' };
277
+ const admit = jest.fn().mockResolvedValue({ state: 'admitted', planState: 'valid', journal: 'current', currentness: 'not-checked', sensors: 'not-required', diagnostics: [] });
278
+ try {
279
+ // This is deliberately the opposite header: a second read here would
280
+ // silently downgrade the authenticated unattended snapshot.
281
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '**Modo de ejecución:** interactivo\n');
282
+ const program = new commander_1.Command();
283
+ program.exitOverride();
284
+ program.configureOutput({ writeErr: () => undefined });
285
+ (0, plan_1.registerPlanCommand)(program, {
286
+ validatePlanFile: () => admittedSnapshot,
287
+ admitPlan: admit,
288
+ readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }),
289
+ });
290
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', root, '--json']);
291
+ expect(admit).toHaveBeenCalledWith(expect.objectContaining({ executionMode: 'desatendido' }));
292
+ }
293
+ finally {
294
+ output.mockRestore();
295
+ fs_1.default.rmSync(root, { recursive: true, force: true });
296
+ process.exitCode = undefined;
297
+ }
298
+ });
299
+ it('uses an explicit validated execution-mode override over the canonical header', async () => {
300
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-admit-header-override-'));
301
+ const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
302
+ const admit = jest.fn().mockResolvedValue({ state: 'admitted', planState: 'valid', journal: 'not-required', currentness: 'not-checked', sensors: 'not-required', diagnostics: [] });
303
+ try {
304
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '**Modo de ejecución:** desatendido\n');
305
+ const program = new commander_1.Command();
306
+ program.exitOverride();
307
+ program.configureOutput({ writeErr: () => undefined });
308
+ (0, plan_1.registerPlanCommand)(program, { validatePlanFile: () => valid, admitPlan: admit, readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }) });
309
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', root, '--execution-mode', 'interactivo', '--json']);
310
+ expect(admit).toHaveBeenCalledWith(expect.objectContaining({ executionMode: 'interactivo' }));
311
+ }
312
+ finally {
313
+ output.mockRestore();
314
+ fs_1.default.rmSync(root, { recursive: true, force: true });
315
+ process.exitCode = undefined;
316
+ }
317
+ });
318
+ it('passes the current branch schema-2 journal and normalized plan path to unattended admission', async () => {
319
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-admit-journal-'));
320
+ const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
321
+ const admit = jest.fn().mockResolvedValue({ state: 'admitted', planState: 'valid', journal: 'current', currentness: 'not-checked', sensors: 'not-required', diagnostics: [] });
322
+ try {
323
+ (0, child_process_1.spawnSync)('git', ['init', '-q', '-b', 'main'], { cwd: root });
324
+ (0, init_1.initWatch)(root, 'main', { path: 'plans/current.md', report: valid });
325
+ const program = new commander_1.Command();
326
+ program.exitOverride();
327
+ program.configureOutput({ writeErr: () => undefined });
328
+ (0, plan_1.registerPlanCommand)(program, { validatePlanFile: () => valid, admitPlan: admit, readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }) });
329
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plans/current.md', '--provider', 'codex', '--cwd', root, '--execution-mode', 'desatendido', '--json']);
330
+ expect(admit).toHaveBeenCalledWith(expect.objectContaining({ executionMode: 'desatendido', planPath: 'plans/current.md', journalCorrupt: false, journalState: expect.objectContaining({ schema: 2, planBinding: expect.objectContaining({ digest: valid.planDigest }) }) }));
331
+ }
332
+ finally {
333
+ output.mockRestore();
334
+ fs_1.default.rmSync(root, { recursive: true, force: true });
335
+ process.exitCode = undefined;
336
+ }
337
+ });
338
+ it.each(['compatible', 'newer-cli-required', 'invalid-manifest', 'unconsumed', 'runtime-supplier', 'runtime-unowned', 'runtime-dangling', 'runtime-directory'])('checks the consumed registry CLI floor before sensors (%s)', async (scenario) => {
339
+ const outputSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
340
+ const oldHome = process.env.HOME;
341
+ const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pair-admission-')));
342
+ const registryRoot = path_1.default.join(root, 'registry');
343
+ fs_1.default.mkdirSync(registryRoot);
344
+ fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'source.md'), 'contract');
345
+ fs_1.default.writeFileSync(path_1.default.join(root, 'source.md'), 'project source');
346
+ if (scenario.startsWith('runtime-')) {
347
+ process.env.HOME = path_1.default.join(root, 'home');
348
+ const installed = path_1.default.join(process.env.HOME, '.agents', 'skills');
349
+ const source = path_1.default.join(scenario === 'runtime-unowned' ? root : registryRoot, 'skills', 'using-awm');
350
+ fs_1.default.mkdirSync(installed, { recursive: true });
351
+ fs_1.default.mkdirSync(source, { recursive: true });
352
+ if (scenario === 'runtime-directory')
353
+ fs_1.default.mkdirSync(path_1.default.join(source, 'SKILL.md'));
354
+ else if (scenario !== 'runtime-dangling')
355
+ fs_1.default.writeFileSync(path_1.default.join(source, 'SKILL.md'), 'runtime contract');
356
+ fs_1.default.symlinkSync(source, path_1.default.join(installed, 'using-awm'), process.platform === 'win32' ? 'junction' : 'dir');
357
+ }
358
+ fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'awm-registry.json'), scenario === 'invalid-manifest' ? '{bad' : JSON.stringify({ minCliVersion: scenario === 'compatible' ? '1.0.0' : '999.0.0' }));
359
+ const report = { ...valid, manifest: { ...valid.manifest, sources: [{ id: 'source', path: scenario === 'unconsumed' || scenario.startsWith('runtime-') ? 'source.md' : 'registry/source.md', locator: 'contract', fact: 'contract' }] } };
360
+ const sensors = jest.fn().mockResolvedValue({ overall: 'pass', sensors: [] });
361
+ const program = new commander_1.Command();
362
+ (0, plan_1.registerPlanCommand)(program, {
363
+ validatePlanFile: () => report,
364
+ listRegistries: () => [{ name: 'fixture', remote: 'https://example.invalid/fixture.git', contentRoot: registryRoot }],
365
+ readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }),
366
+ checkCurrentness: async () => ({ checkedAt: 'x', compatibility: { status: 'not-checked' }, components: ['cli', 'registry:fixture'].map(component => ({ component, installed: '1.0.0', latest: '1.0.0', channel: 'stable', source: 'fixture', checkedAt: 'x', status: 'current', detail: 'ok', remedy: 'none' })) }),
367
+ runSensors: sensors,
368
+ });
369
+ try {
370
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', root, '--execution-mode', 'interactivo', '--require-current', '--verify-sensors', '--json']);
371
+ const output = JSON.parse(String(outputSpy.mock.calls.at(-1)[0]));
372
+ if (scenario === 'compatible' || scenario === 'unconsumed') {
373
+ expect(output.state).toBe('admitted');
374
+ expect(sensors).toHaveBeenCalledTimes(1);
375
+ }
376
+ else {
377
+ expect(output.state).toBe('blocked');
378
+ const unknownSupplier = ['runtime-unowned', 'runtime-dangling', 'runtime-directory'].includes(scenario);
379
+ expect(output.diagnostics[0].code).toBe(unknownSupplier ? 'ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED' : scenario === 'invalid-manifest' ? 'ADMISSION_REGISTRY_COMPATIBILITY_UNVERIFIABLE' : 'ADMISSION_REGISTRY_CLI_INCOMPATIBLE');
380
+ if (!unknownSupplier)
381
+ expect(output.diagnostics[0].message).toContain('registry:fixture');
382
+ expect(sensors).not.toHaveBeenCalled();
383
+ expect(process.exitCode).toBe(2);
384
+ }
385
+ }
386
+ finally {
387
+ if (oldHome === undefined)
388
+ delete process.env.HOME;
389
+ else
390
+ process.env.HOME = oldHome;
391
+ outputSpy.mockRestore();
392
+ fs_1.default.rmSync(root, { recursive: true, force: true });
393
+ process.exitCode = undefined;
394
+ }
395
+ });
396
+ it.each([['cursor', 'global'], ['cursor', 'local'], ['copilot', 'local']])('does not prove absence for actual unowned rendered %s %s runtime contracts', async (target, scope) => {
397
+ const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-rendered-admission-')));
398
+ const oldHome = process.env.HOME;
399
+ process.env.HOME = path_1.default.join(root, 'home');
400
+ const outputSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
401
+ const registryRoot = path_1.default.join(root, 'registry');
402
+ const skillSource = path_1.default.join(registryRoot, 'skills', 'using-awm');
403
+ fs_1.default.mkdirSync(skillSource, { recursive: true });
404
+ fs_1.default.writeFileSync(path_1.default.join(skillSource, 'SKILL.md'), '---\nname: using-awm\ndescription: contract\n---\nRuntime contract.\n');
405
+ fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'awm-registry.json'), JSON.stringify({ minCliVersion: '1.0.0' }));
406
+ fs_1.default.writeFileSync(path_1.default.join(root, 'source.md'), 'project source');
407
+ const provider = (0, providers_1.providerFor)(target);
408
+ const directory = scope === 'global' ? provider.skill.global : path_1.default.resolve(root, provider.skill.local);
409
+ fs_1.default.mkdirSync(directory, { recursive: true });
410
+ const installedFile = path_1.default.join(directory, (0, registry_1.renderedFilename)('using-awm', provider.skill.renderer));
411
+ fs_1.default.writeFileSync(installedFile, (0, registry_1.renderArtifact)(provider.skill.renderer, skillSource));
412
+ const sensors = jest.fn();
413
+ const program = new commander_1.Command();
414
+ (0, plan_1.registerPlanCommand)(program, {
415
+ validatePlanFile: () => ({ ...valid, manifest: { ...valid.manifest, sources: [{ id: 'source', path: 'source.md', locator: 'contract', fact: 'contract' }] } }),
416
+ listRegistries: () => [{ name: 'fixture', remote: 'https://example.invalid/fixture.git', contentRoot: registryRoot }],
417
+ readPreferences: () => ({ defaultAgent: target, enabledAgents: [target], installMethod: 'symlink', defaultScope: 'local' }),
418
+ checkCurrentness: async () => ({ checkedAt: 'x', compatibility: { status: 'not-checked' }, components: [{ component: 'cli', installed: '1.0.0', latest: '1.0.0', channel: 'stable', source: 'fixture', checkedAt: 'x', status: 'current', detail: 'ok', remedy: 'none' }] }),
419
+ runSensors: sensors,
420
+ });
421
+ try {
422
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', target, '--cwd', root, '--execution-mode', 'interactivo', '--require-current', '--verify-sensors', '--json']);
423
+ const output = JSON.parse(String(outputSpy.mock.calls.at(-1)[0]));
424
+ expect(output.diagnostics[0].code).toBe('ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED');
425
+ expect(output.state).toBe('blocked');
426
+ // Global diagnostics may retain the declared path while local ones
427
+ // use a physical root. Windows 8.3/long names are equivalent: prove
428
+ // that the reported path names this exact file, not its spelling.
429
+ const reported = /^Runtime artifact (.+) has no unique physical registry owner/.exec(output.diagnostics[0].message);
430
+ expect(reported).not.toBeNull();
431
+ const reportedIdentity = fs_1.default.lstatSync(reported[1], { bigint: true });
432
+ const installedIdentity = fs_1.default.lstatSync(installedFile, { bigint: true });
433
+ expect(reportedIdentity.isFile()).toBe(true);
434
+ expect(reportedIdentity.ino).not.toBe(0n);
435
+ expect([reportedIdentity.dev, reportedIdentity.ino]).toEqual([installedIdentity.dev, installedIdentity.ino]);
436
+ expect(sensors).not.toHaveBeenCalled();
437
+ }
438
+ finally {
439
+ if (oldHome === undefined)
440
+ delete process.env.HOME;
441
+ else
442
+ process.env.HOME = oldHome;
443
+ outputSpy.mockRestore();
444
+ fs_1.default.rmSync(root, { recursive: true, force: true });
445
+ process.exitCode = undefined;
446
+ }
447
+ });
448
+ it.each(['symlink', 'directory', 'oversized', 'duplicate-floor', 'unsafe-floor', 'stale-before-compatibility'])('keeps registry manifest %s noncertifying before sensors', async (scenario) => {
449
+ const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-manifest-admission-')));
450
+ const registryRoot = path_1.default.join(root, 'registry');
451
+ fs_1.default.mkdirSync(registryRoot);
452
+ fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'source.md'), 'registry source');
453
+ const manifest = path_1.default.join(registryRoot, 'awm-registry.json');
454
+ if (scenario === 'symlink') {
455
+ fs_1.default.writeFileSync(path_1.default.join(root, 'outside-manifest'), '{}');
456
+ fs_1.default.symlinkSync(path_1.default.join(root, 'outside-manifest'), manifest, 'file');
457
+ }
458
+ else if (scenario === 'directory')
459
+ fs_1.default.mkdirSync(manifest);
460
+ else
461
+ fs_1.default.writeFileSync(manifest, scenario === 'oversized' ? ' '.repeat(256 * 1024 + 1)
462
+ : scenario === 'duplicate-floor' ? '{"minCliVersion":"999.0.0","minCliVersion":"1.0.0"}'
463
+ : scenario === 'unsafe-floor' ? '{"minCliVersion":"' + '9'.repeat(1024) + '.0.0"}' : '{bad json');
464
+ const outputSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
465
+ const sensors = jest.fn();
466
+ const program = new commander_1.Command();
467
+ (0, plan_1.registerPlanCommand)(program, {
468
+ validatePlanFile: () => ({ ...valid, manifest: { ...valid.manifest, sources: [{ id: 'source', path: 'registry/source.md', locator: 'contract', fact: 'contract' }] } }),
469
+ listRegistries: () => [{ name: 'fixture', remote: 'https://example.invalid/fixture.git', contentRoot: registryRoot }],
470
+ readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }),
471
+ checkCurrentness: async () => ({ checkedAt: 'x', compatibility: { status: 'not-checked' }, components: ['cli', 'registry:fixture'].map(component => ({ component, installed: '1.0.0', latest: '1.0.0', channel: 'stable', source: 'fixture', checkedAt: 'x', status: scenario === 'stale-before-compatibility' ? 'stale' : 'current', detail: 'observed', remedy: 'none' })) }),
472
+ runSensors: sensors,
473
+ });
474
+ try {
475
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', root, '--execution-mode', 'interactivo', '--require-current', '--verify-sensors', '--json']);
476
+ const output = JSON.parse(String(outputSpy.mock.calls.at(-1)[0]));
477
+ expect(output.state).toBe('blocked');
478
+ expect(output.diagnostics[0].code).toBe(scenario === 'stale-before-compatibility' ? 'ADMISSION_CURRENTNESS_BLOCKED' : 'ADMISSION_REGISTRY_COMPATIBILITY_UNVERIFIABLE');
479
+ expect(sensors).not.toHaveBeenCalled();
480
+ }
481
+ finally {
482
+ outputSpy.mockRestore();
483
+ fs_1.default.rmSync(root, { recursive: true, force: true });
484
+ process.exitCode = undefined;
485
+ }
486
+ });
487
+ it.each(['owner-currentness', 'inventory-currentness', 'owner-sensors', 'floor-sensors', 'registry-identity-currentness', 'body-currentness'])('rechecks actual runtime contract %s after awaits before admitted', async (scenario) => {
488
+ const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-supplier-drift-')));
489
+ const oldHome = process.env.HOME;
490
+ process.env.HOME = path_1.default.join(root, 'home');
491
+ const registryRoot = path_1.default.join(root, 'registry');
492
+ const owned = path_1.default.join(registryRoot, 'skills', 'using-awm');
493
+ const unowned = path_1.default.join(root, 'unowned', 'using-awm');
494
+ const replacementRoot = path_1.default.join(root, 'replacement-registry');
495
+ const replacementOwned = path_1.default.join(replacementRoot, 'skills', 'using-awm');
496
+ for (const directory of [owned, unowned, replacementOwned]) {
497
+ fs_1.default.mkdirSync(directory, { recursive: true });
498
+ fs_1.default.writeFileSync(path_1.default.join(directory, 'SKILL.md'), 'contract');
499
+ }
500
+ fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'awm-registry.json'), '{}');
501
+ fs_1.default.writeFileSync(path_1.default.join(replacementRoot, 'awm-registry.json'), '{}');
502
+ fs_1.default.writeFileSync(path_1.default.join(root, 'source.md'), 'project source');
503
+ const installed = (0, providers_1.providerFor)('codex').skill.global;
504
+ fs_1.default.mkdirSync(installed, { recursive: true });
505
+ const artifact = path_1.default.join(installed, 'using-awm');
506
+ fs_1.default.symlinkSync(owned, artifact, process.platform === 'win32' ? 'junction' : 'dir');
507
+ const outputSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
508
+ let inventoryChanged = false;
509
+ let registryChanged = false;
510
+ const replaceOwner = () => { fs_1.default.unlinkSync(artifact); fs_1.default.symlinkSync(unowned, artifact, process.platform === 'win32' ? 'junction' : 'dir'); };
511
+ const sensors = jest.fn().mockImplementation(async () => {
512
+ if (scenario === 'owner-sensors')
513
+ replaceOwner();
514
+ if (scenario === 'floor-sensors')
515
+ fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'awm-registry.json'), '{"minCliVersion":"999.0.0"}');
516
+ return { overall: 'pass', sensors: [] };
517
+ });
518
+ const program = new commander_1.Command();
519
+ (0, plan_1.registerPlanCommand)(program, {
520
+ validatePlanFile: () => ({ ...valid, manifest: { ...valid.manifest, sources: [{ id: 'source', path: 'source.md', locator: 'contract', fact: 'contract' }] } }),
521
+ listRegistries: () => [{ name: inventoryChanged ? 'replacement' : 'fixture', remote: registryChanged ? 'https://replacement.invalid/fixture.git' : 'https://example.invalid/fixture.git', contentRoot: registryChanged ? replacementRoot : registryRoot }],
522
+ readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }),
523
+ checkCurrentness: async () => {
524
+ if (scenario === 'owner-currentness')
525
+ replaceOwner();
526
+ if (scenario === 'inventory-currentness')
527
+ inventoryChanged = true;
528
+ if (scenario === 'registry-identity-currentness') {
529
+ registryChanged = true;
530
+ fs_1.default.unlinkSync(artifact);
531
+ fs_1.default.symlinkSync(replacementOwned, artifact, process.platform === 'win32' ? 'junction' : 'dir');
532
+ }
533
+ if (scenario === 'body-currentness')
534
+ fs_1.default.writeFileSync(path_1.default.join(owned, 'SKILL.md'), 'changed native runtime contract');
535
+ return { checkedAt: 'x', compatibility: { status: 'not-checked' }, components: ['cli', 'registry:fixture'].map(component => ({ component, installed: '1.0.0', latest: '1.0.0', channel: 'stable', source: 'fixture', checkedAt: 'x', status: 'current', detail: 'ok', remedy: 'none' })) };
536
+ }, runSensors: sensors,
537
+ });
538
+ try {
539
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', root, '--execution-mode', 'interactivo', '--require-current', '--verify-sensors', '--json']);
540
+ const output = JSON.parse(String(outputSpy.mock.calls.at(-1)[0]));
541
+ expect(output.state).toBe('blocked');
542
+ expect(output.diagnostics[0].code).toBe(scenario === 'floor-sensors' ? 'ADMISSION_REGISTRY_CLI_INCOMPATIBLE' : 'ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED');
543
+ expect(sensors).toHaveBeenCalledTimes(scenario.endsWith('sensors') ? 1 : 0);
544
+ }
545
+ finally {
546
+ if (oldHome === undefined)
547
+ delete process.env.HOME;
548
+ else
549
+ process.env.HOME = oldHome;
550
+ outputSpy.mockRestore();
551
+ fs_1.default.rmSync(root, { recursive: true, force: true });
552
+ process.exitCode = undefined;
553
+ }
554
+ });
555
+ it('keeps an unreadable inventory root actionable instead of silently proving it unconsumed', async () => {
556
+ const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-root-admission-')));
557
+ fs_1.default.writeFileSync(path_1.default.join(root, 'source.md'), 'project source');
558
+ const outputSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
559
+ const sensors = jest.fn();
560
+ const currentness = jest.fn();
561
+ const program = new commander_1.Command();
562
+ (0, plan_1.registerPlanCommand)(program, {
563
+ validatePlanFile: () => ({ ...valid, manifest: { ...valid.manifest, sources: [{ id: 'source', path: 'source.md', locator: 'contract', fact: 'contract' }] } }),
564
+ listRegistries: () => [{ name: 'unavailable-supplier', remote: 'https://example.invalid/fixture.git', contentRoot: path_1.default.join(root, 'missing-registry') }],
565
+ readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }),
566
+ checkCurrentness: currentness, runSensors: sensors,
567
+ });
568
+ try {
569
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', root, '--require-current', '--verify-sensors', '--json']);
570
+ const output = JSON.parse(String(outputSpy.mock.calls.at(-1)[0]));
571
+ expect(output.diagnostics[0].code).toBe('ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED');
572
+ expect(output.diagnostics[0].message).toContain('unavailable-supplier');
573
+ expect(currentness).not.toHaveBeenCalled();
574
+ expect(sensors).not.toHaveBeenCalled();
575
+ }
576
+ finally {
577
+ outputSpy.mockRestore();
578
+ fs_1.default.rmSync(root, { recursive: true, force: true });
579
+ process.exitCode = undefined;
580
+ }
581
+ });
582
+ it('does not run sensors after currentness blocks', async () => {
583
+ const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
584
+ const sensors = jest.fn();
585
+ const program = new commander_1.Command();
586
+ program.exitOverride();
587
+ program.configureOutput({ writeErr: () => undefined });
588
+ (0, plan_1.registerPlanCommand)(program, {
589
+ validatePlanFile: () => valid, listRegistries: () => [],
590
+ readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }),
591
+ checkCurrentness: async () => ({ checkedAt: 'x', compatibility: { status: 'not-checked' }, components: [{ component: 'cli', installed: '1.0.0', latest: '2.0.0', channel: 'stable', source: 'npm', checkedAt: 'x', status: 'stale', detail: 'stale', remedy: 'update' }] }),
592
+ runSensors: sensors,
593
+ });
594
+ try {
595
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', repositoryRoot, '--require-current', '--verify-sensors', '--json']);
596
+ expect(sensors).not.toHaveBeenCalled();
597
+ expect(JSON.parse(String(output.mock.calls[0][0]))).toMatchObject({ state: 'blocked', currentness: 'stale', sensors: 'not-required' });
598
+ }
599
+ finally {
600
+ output.mockRestore();
601
+ process.exitCode = undefined;
602
+ }
603
+ });
604
+ it('rejects a forged admission report before output or exit status', async () => {
605
+ const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
606
+ const program = new commander_1.Command();
607
+ program.exitOverride();
608
+ program.configureOutput({ writeErr: () => undefined });
609
+ (0, plan_1.registerPlanCommand)(program, {
610
+ validatePlanFile: () => valid,
611
+ readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }),
612
+ admitPlan: async () => ({ state: 'admitted', planState: 'valid', journal: 'not-required', currentness: 'current', sensors: 'pass', diagnostics: 'forged' }),
613
+ });
614
+ try {
615
+ await expect(program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', repositoryRoot, '--json'])).rejects.toThrow('admission returned an invalid report');
616
+ expect(output).not.toHaveBeenCalled();
617
+ expect(process.exitCode).toBeUndefined();
618
+ }
619
+ finally {
620
+ output.mockRestore();
621
+ process.exitCode = undefined;
622
+ }
623
+ });
624
+ it('does not prove registry irrelevance through a source symlink that escapes cwd', async () => {
625
+ const fixtureRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-admit-provenance-'));
626
+ const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-admit-outside-'));
627
+ const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
628
+ const currentness = jest.fn();
629
+ const admit = jest.fn().mockResolvedValue({ state: 'blocked', planState: 'valid', journal: 'not-required', currentness: 'unverifiable', sensors: 'not-required', diagnostics: [] });
630
+ try {
631
+ fs_1.default.writeFileSync(path_1.default.join(outside, 'contract.md'), 'outside');
632
+ fs_1.default.symlinkSync(outside, path_1.default.join(fixtureRoot, 'linked'), process.platform === 'win32' ? 'junction' : 'dir');
633
+ const escaped = { ...valid, manifest: { ...valid.manifest, sources: [{ id: 'SRC', path: 'linked/contract.md', locator: 'x', fact: 'x' }] } };
634
+ const program = new commander_1.Command();
635
+ program.exitOverride();
636
+ program.configureOutput({ writeErr: () => undefined });
637
+ (0, plan_1.registerPlanCommand)(program, {
638
+ validatePlanFile: () => escaped, admitPlan: admit,
639
+ readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }),
640
+ listRegistries: () => [], checkCurrentness: currentness,
641
+ });
642
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', fixtureRoot, '--require-current', '--json']);
643
+ expect(currentness).not.toHaveBeenCalled();
644
+ expect(admit).toHaveBeenCalledWith(expect.objectContaining({ provenance: 'unknown', consumedRegistryComponents: [] }));
645
+ }
646
+ finally {
647
+ output.mockRestore();
648
+ fs_1.default.rmSync(fixtureRoot, { recursive: true, force: true });
649
+ fs_1.default.rmSync(outside, { recursive: true, force: true });
650
+ process.exitCode = undefined;
651
+ }
652
+ });
653
+ it('bounds and terminal-sanitizes adversarial admission diagnostics in JSON', async () => {
654
+ const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
655
+ const program = new commander_1.Command();
656
+ program.exitOverride();
657
+ program.configureOutput({ writeErr: () => undefined });
658
+ (0, plan_1.registerPlanCommand)(program, {
659
+ validatePlanFile: () => valid,
660
+ readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }),
661
+ admitPlan: async () => ({ state: 'blocked', planState: 'valid', journal: 'not-required', currentness: 'not-checked', sensors: 'not-required', diagnostics: [{ code: `X\u001b${'c'.repeat(5000)}`, message: `Y\u001b${'m'.repeat(5000)}` }] }),
662
+ });
663
+ try {
664
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plans/r4.md', '--provider', 'codex', '--cwd', 'fixture-root', '--json']);
665
+ const json = String(output.mock.calls[0][0]);
666
+ expect(json.trimEnd()).not.toMatch(/[\u0000-\u001f\u007f-\u009f]/);
667
+ expect(JSON.parse(json).diagnostics[0]).toEqual({ code: expect.stringMatching(/^X\\u001b/), message: expect.stringMatching(/^Y\\u001b/) });
668
+ expect(JSON.parse(json).diagnostics[0].message.length).toBeLessThanOrEqual(4096);
669
+ }
670
+ finally {
671
+ output.mockRestore();
672
+ process.exitCode = undefined;
673
+ }
674
+ });
675
+ it('uses the bounded admission surface and preserves JSON output', async () => {
676
+ const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
677
+ process.exitCode = undefined;
678
+ const admit = jest.fn()
679
+ .mockResolvedValue({
680
+ state: 'blocked', planState: 'valid', planDigest: valid.planDigest, provider: 'codex', executionMode: 'interactivo',
681
+ journal: 'not-required', currentness: 'not-checked', sensors: 'not-required', diagnostics: [{ code: 'ADMISSION_CURRENTNESS_REQUIRED', message: 'required' }],
682
+ });
683
+ const sensorRun = jest.fn().mockResolvedValue({ overall: 'pass', sensors: [] });
684
+ const program = new commander_1.Command();
685
+ program.exitOverride();
686
+ program.configureOutput({ writeErr: () => undefined });
687
+ (0, plan_1.registerPlanCommand)(program, {
688
+ validatePlanFile: () => valid, admitPlan: admit,
689
+ readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }),
690
+ checkCurrentness: async () => ({ checkedAt: '2026-01-01T00:00:00.000Z', components: [], compatibility: { status: 'not-checked' } }),
691
+ runSensors: sensorRun,
692
+ listRegistries: () => [],
693
+ });
694
+ try {
695
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plans/r4.md', '--provider', 'codex', '--cwd', repositoryRoot, '--verify-sensors', '--json']);
696
+ expect(sensorRun).toHaveBeenCalledWith({ cwd: repositoryRoot, all: true, readOnly: true });
697
+ expect(admit).toHaveBeenLastCalledWith(expect.objectContaining({ provider: 'codex', cwd: repositoryRoot, requireCurrent: false, verifySensors: true, plan: valid, provenance: 'proven' }));
698
+ expect(JSON.parse(String(output.mock.calls[0][0]))).toMatchObject({ state: 'blocked', provider: 'codex' });
699
+ expect(process.exitCode).toBe(2);
700
+ }
701
+ finally {
702
+ output.mockRestore();
703
+ process.exitCode = undefined;
704
+ }
705
+ });
706
+ });
142
707
  describe('plan validation public boundaries', () => {
708
+ it.each([
709
+ ['empty red commands', { slices: [{ ...valid.manifest.slices[0], redCommands: [] }] }],
710
+ ['bogus review evidence', { slices: [{ ...valid.manifest.slices[0], reviewEvidence: ['specification', 'bogus'] }] }],
711
+ ['bogus risk', { slices: [{ ...valid.manifest.slices[0], risk: 'unbounded' }] }],
712
+ ['dangling closure command', { closureCommands: ['CMD-MISSING'] }],
713
+ ])('rejects a forged valid report with %s before output or exit 0', (_name, changes) => {
714
+ const forged = { ...valid, manifest: { ...valid.manifest, ...changes } };
715
+ expect(() => (0, plan_1.formatReport)(forged, 'plan.md')).toThrow('plan validator returned an invalid valid report');
716
+ expect(() => (0, plan_1.exitCodeFor)(forged)).toThrow('plan validator returned an invalid valid report');
717
+ });
718
+ it('rejects a structurally plausible forged or deserialized valid report', () => {
719
+ const deserialized = JSON.parse(JSON.stringify(valid));
720
+ const forged = { ...valid };
721
+ expect(() => (0, plan_1.formatReport)(deserialized, 'plan.md')).toThrow('plan validator returned an invalid valid report');
722
+ expect(() => (0, plan_1.exitCodeFor)(deserialized)).toThrow('plan validator returned an invalid valid report');
723
+ expect(() => (0, plan_1.exitCodeFor)(forged)).toThrow('plan validator returned an invalid valid report');
724
+ });
725
+ it.each([
726
+ 'docs/plans/2026-08-26-r4a-compact-plan-cli-plan.md',
727
+ 'docs/plans/2026-08-27-sensor-portability-publication-a-plan.md',
728
+ 'docs/plans/2026-09-07-retire-onsignal.md',
729
+ 'docs/plans/2026-09-14-compact-only-bootstrap-plan.md',
730
+ ])('accepts a validator-produced tracked v1 report at both public boundaries: %s', (file) => {
731
+ const report = (0, validate_1.validatePlanFile)(file, repositoryRoot);
732
+ expect(report.state).toBe('valid');
733
+ expect((0, plan_1.exitCodeFor)(report)).toBe(0);
734
+ expect((0, plan_1.formatReport)(report, file)).toContain('complete ownership');
735
+ });
143
736
  it.each([
144
737
  ['valid', valid, 0],
145
- ['legacy', { state: 'legacy' }, 0],
738
+ ['migration-required', migration, 2],
146
739
  ['invalid', { state: 'invalid', diagnostics: [] }, 2],
147
740
  ['unsupported', { state: 'unsupported', schema: 'compact-slices/v2', diagnostics: [] }, 2],
148
741
  ])('maps %s reports to exit %i', (_state, report, code) => {
149
742
  expect((0, plan_1.exitCodeFor)(report)).toBe(code);
150
743
  });
744
+ it('fails loudly rather than returning exit 0 for an injected valid state without a manifest', () => {
745
+ const malformed = { state: 'valid' };
746
+ expect(() => (0, plan_1.exitCodeFor)(malformed)).toThrow('plan validator returned an invalid valid report');
747
+ expect(() => (0, plan_1.formatReport)(malformed, 'plan.md')).toThrow('plan validator returned an invalid valid report');
748
+ });
749
+ it('rejects a valid report missing the required closureCommands collection', () => {
750
+ const malformed = { ...valid, manifest: { ...valid.manifest, closureCommands: undefined } };
751
+ expect(() => (0, plan_1.exitCodeFor)(malformed)).toThrow('plan validator returned an invalid valid report');
752
+ });
753
+ it.each(['requirements', 'sources', 'commands', 'slices', 'closureCommands'])('rejects a null %s entry before claiming complete ownership or exit 0', (collection) => {
754
+ const malformed = {
755
+ ...valid, manifest: { ...valid.manifest, [collection]: [null] },
756
+ };
757
+ expect(() => (0, plan_1.formatReport)(malformed, 'plan.md')).toThrow('plan validator returned an invalid valid report');
758
+ expect(() => (0, plan_1.exitCodeFor)(malformed)).toThrow('plan validator returned an invalid valid report');
759
+ });
760
+ it.each(['human', 'json'])('rejects a null manifest slice before %s output or exit 0', async (mode) => {
761
+ const malformed = {
762
+ ...valid, manifest: { ...valid.manifest, slices: [null] },
763
+ };
764
+ const args = ['node', 'awm', 'plan', 'validate', 'plan.md', ...(mode === 'json' ? ['--json'] : [])];
765
+ await expect(commandFor(malformed).parseAsync(args)).rejects.toThrow('plan validator returned an invalid valid report');
766
+ expect(stdoutWrite).not.toHaveBeenCalled();
767
+ expect(process.exitCode).toBeUndefined();
768
+ });
769
+ it('rejects an unowned requirement before claiming complete ownership or exit 0', () => {
770
+ const malformed = { ...valid, manifest: { ...valid.manifest, requirements: [...valid.manifest.requirements, 'R4-VAL-5'] } };
771
+ expect(() => (0, plan_1.formatReport)(malformed, 'plan.md')).toThrow('plan validator returned an invalid valid report');
772
+ expect(() => (0, plan_1.exitCodeFor)(malformed)).toThrow('plan validator returned an invalid valid report');
773
+ });
774
+ it('rejects a requirement owned by two slices', () => {
775
+ const malformed = {
776
+ ...valid, manifest: { ...valid.manifest, slices: [...valid.manifest.slices, {
777
+ ...valid.manifest.slices[0], id: 'S2', requirements: [valid.manifest.requirements[0]],
778
+ }] },
779
+ };
780
+ expect(() => (0, plan_1.formatReport)(malformed, 'plan.md')).toThrow('plan validator returned an invalid valid report');
781
+ expect(() => (0, plan_1.exitCodeFor)(malformed)).toThrow('plan validator returned an invalid valid report');
782
+ });
783
+ it.each(['requirements', 'sources', 'commands', 'slices', 'closureCommands'])('rejects an empty %s collection in an injected valid report', (collection) => {
784
+ const malformed = {
785
+ ...valid, manifest: { ...valid.manifest, [collection]: [] },
786
+ };
787
+ expect(() => (0, plan_1.formatReport)(malformed, 'plan.md')).toThrow('plan validator returned an invalid valid report');
788
+ expect(() => (0, plan_1.exitCodeFor)(malformed)).toThrow('plan validator returned an invalid valid report');
789
+ });
790
+ it('fails loudly for a migration state without its required reason at the public exit boundary', () => {
791
+ const malformed = { state: 'migration-required' };
792
+ expect(() => (0, plan_1.exitCodeFor)(malformed)).toThrow('plan validator returned an invalid migration reason');
793
+ });
151
794
  it('fails loudly for an invalid validator dependency', () => {
152
795
  expect(() => (0, plan_1.registerPlanCommand)(new commander_1.Command(), {})).toThrow('validatePlanFile must be a function');
153
796
  });
154
797
  });
798
+ describe('plan migration-facts command', () => {
799
+ it('emits deterministic JSON through the injected read-only collector', async () => {
800
+ const write = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
801
+ const program = new commander_1.Command();
802
+ program.exitOverride();
803
+ program.configureOutput({ writeErr: () => undefined });
804
+ (0, plan_1.registerPlanCommand)(program, { validatePlanFile: () => valid, collectMigrationFacts: () => ({ state: 'planning-required', issueLinks: ['https://github.com/Kodria/agentic-workflow/issues/126'], tasks: [], diagnostics: ['missing'], facts: [] }) });
805
+ try {
806
+ await program.parseAsync(['node', 'awm', 'plan', 'migration-facts', 'old.md', '--issue', 'https://github.com/Kodria/agentic-workflow/issues/126', '--json']);
807
+ expect(JSON.parse(String(write.mock.calls.at(-1)?.[0]))).toMatchObject({ state: 'planning-required', diagnostics: ['missing'] });
808
+ }
809
+ finally {
810
+ write.mockRestore();
811
+ }
812
+ });
813
+ it('rejects missing issue input before invoking collection', async () => {
814
+ const collectMigrationFacts = jest.fn();
815
+ const program = new commander_1.Command();
816
+ program.exitOverride();
817
+ program.configureOutput({ writeErr: () => undefined });
818
+ (0, plan_1.registerPlanCommand)(program, { validatePlanFile: () => valid, collectMigrationFacts });
819
+ await expect(program.parseAsync(['node', 'awm', 'plan', 'migration-facts', 'old.md'])).rejects.toThrow();
820
+ expect(collectMigrationFacts).not.toHaveBeenCalled();
821
+ });
822
+ });