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
@@ -224,7 +224,7 @@ describe('computeSensorStatus', () => {
224
224
  variants: [{
225
225
  id: 'eslint-10', priority: 10, certifiedRange: '>=10.0.0 <11.0.0',
226
226
  requirements: { tool: 'eslint', toolRange: '>=10.0.0 <11.0.0', runtime: 'node', runtimeRange: '>=0.0.0' },
227
- assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'package-script-present' },
227
+ assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'package-script-present', script: 'lint' },
228
228
  command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.', '--config', 'eslint.config.awm.mjs'] },
229
229
  }],
230
230
  } },
@@ -291,7 +291,7 @@ describe('computeSensorStatus', () => {
291
291
  const variant = (id, range) => ({
292
292
  id, priority: 10, certifiedRange: range,
293
293
  requirements: { tool: 'eslint', toolRange: range, runtime: 'node', runtimeRange: '>=0.0.0' },
294
- assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'package-script-present' },
294
+ assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'package-script-present', script: 'lint' },
295
295
  command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
296
296
  });
297
297
  fs_1.default.writeFileSync(path_1.default.join(registry, 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify({
@@ -381,7 +381,7 @@ describe('computeSensorStatus', () => {
381
381
  sensors: { lint: { applicability: { allFiles: ['package.json'] }, variants: [{
382
382
  id: 'eslint-10', priority: 1, certifiedRange: '>=10 <11',
383
383
  requirements: { tool: 'eslint', toolRange: '>=10 <11', runtime: 'node', runtimeRange: '>=0' },
384
- assets: [], formatter: 'generic', probe: { kind: 'package-script-present' },
384
+ assets: [], formatter: 'generic', probe: { kind: 'package-script-present', script: 'lint' },
385
385
  command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
386
386
  }] } },
387
387
  };
@@ -194,6 +194,18 @@ describe('aplicacion transaccional de requests', () => {
194
194
  expect(Object.values(s.appliedRequests).some((a) => a.outcome === 'rejected-stale-generation')).toBe(true);
195
195
  expect(s.controllerHeartbeatAt).toBeUndefined();
196
196
  });
197
+ test('routing reserve/observe es supervisado, idempotente y una generacion vieja no muta intentos', () => {
198
+ const envelope = { schema: 'routing-envelope/v1', runtime: { target: 'codex', kind: 'native', version: '1', accountScopeDigest: '0'.repeat(64) }, role: 'implementer', sliceId: 'S1', requestedProfile: 'mechanical', effectiveProfile: 'mechanical', resolved: { selector: { kind: 'model', id: 'm' }, effort: { kind: 'explicit', value: 'medium' } }, outcome: 'native', unavailableEvidence: [], policyDigest: 'a'.repeat(64), capabilityDigest: 'b'.repeat(64), planDigest: 'c'.repeat(64), executionDigest: 'd'.repeat(64) };
199
+ (0, requests_1.emitRequest)(repo, 'rama', { kind: 'routing-reserve', generationToken: 'g1', idempotencyKey: 'routing-1', payload: { obligationId: 'o1', lineageId: 'l1', envelope, fingerprint: 'e'.repeat(64) } });
200
+ (0, apply_1.consumePendingRequests)(repo, 'rama', 'g1');
201
+ const attempt = (0, store_1.readJournal)(repo, 'rama').state.routingAttempts[0];
202
+ (0, requests_1.emitRequest)(repo, 'rama', { kind: 'routing-observe', generationToken: 'g1', idempotencyKey: 'routing-2', payload: { attemptId: attempt.id, nativeAgentId: 'native-1' } });
203
+ (0, apply_1.consumePendingRequests)(repo, 'rama', 'g1');
204
+ expect((0, store_1.readJournal)(repo, 'rama').state.routingAttempts[0]).toMatchObject({ state: 'active', nativeAgentId: 'native-1' });
205
+ (0, requests_1.emitRequest)(repo, 'rama', { kind: 'routing-reserve', generationToken: 'old', idempotencyKey: 'routing-old', payload: { obligationId: 'o1', lineageId: 'l2', envelope, fingerprint: 'f'.repeat(64) } });
206
+ expect((0, apply_1.consumePendingRequests)(repo, 'rama', 'g1').rejectedStale).toBe(1);
207
+ expect((0, store_1.readJournal)(repo, 'rama').state.routingAttempts).toHaveLength(1);
208
+ });
197
209
  test('request corrupta se aparta VISIBLE como .corrupt, jamas se descarta (R1.6)', () => {
198
210
  fs_1.default.writeFileSync(path_1.default.join((0, paths_1.requestsDir)(repo, 'rama'), 'req-roto.json'), '{no-json');
199
211
  const out = (0, apply_1.consumePendingRequests)(repo, 'rama', 'g1');
@@ -9,6 +9,8 @@ const os_1 = __importDefault(require("os"));
9
9
  const child_process_1 = require("child_process");
10
10
  const process_1 = require("../../../src/core/journal/process");
11
11
  const paths_1 = require("../../../src/core/paths");
12
+ const run_1 = require("../../../src/commands/sensors/run");
13
+ const live_1 = require("../../../src/commands/sensors/compatibility/live");
12
14
  /** win32 has no POSIX process groups / negative-pid kill convention -- mirrors
13
15
  * killTreeWindows's taskkill pattern already established and tested in
14
16
  * core/journal/process.ts, applied here to this test's own cleanup (not
@@ -27,6 +29,36 @@ function killGroup(pgid) {
27
29
  }
28
30
  catch { /* ya muerto */ }
29
31
  }
32
+ function groupAlive(pgid) {
33
+ if ((0, paths_1.isWindowsNative)())
34
+ return false; // taskkill above is synchronous enough for this fixture.
35
+ try {
36
+ process.kill(-pgid, 0);
37
+ return true;
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ async function terminateFixtureGroups(groups) {
44
+ for (const pgid of groups)
45
+ killGroup(pgid);
46
+ await until(() => [...groups].every(pgid => !groupAlive(pgid)), 5_000, 'terminacion de grupos del fixture');
47
+ }
48
+ async function removeFixture(pathname) {
49
+ let last;
50
+ for (let attempt = 0; attempt < 20; attempt++) {
51
+ try {
52
+ fs_1.default.rmSync(pathname, { recursive: true, force: true, maxRetries: 1, retryDelay: 25 });
53
+ return;
54
+ }
55
+ catch (error) {
56
+ last = error;
57
+ await new Promise(resolve => setTimeout(resolve, 25));
58
+ }
59
+ }
60
+ throw last;
61
+ }
30
62
  jest.setTimeout(180000);
31
63
  // R1.8 promete "el wrapper sobrevive incluso si el supervisor muere" — en
32
64
  // POSIX esto se sostiene en `detached: true` (nueva sesion, sobrevive un
@@ -51,6 +83,37 @@ function readState(repo) {
51
83
  return null;
52
84
  }
53
85
  }
86
+ /** A physical capability-only registry is intentional: sensor pack resolution
87
+ * rejects a linked registry root, because a link could later escape its
88
+ * configured authority. Keep the v3 logical source name and the registry
89
+ * configuration aligned so this fixture exercises the real resolver. */
90
+ function writeFixtureBaselineRegistry(awmHome) {
91
+ const packDir = path_1.default.join(awmHome, 'registries', 'baseline', 'sensor-packs', 'js-ts');
92
+ fs_1.default.mkdirSync(packDir, { recursive: true });
93
+ fs_1.default.writeFileSync(path_1.default.join(packDir, 'pack.json'), JSON.stringify({
94
+ schemaVersion: 2,
95
+ name: 'js-ts',
96
+ description: 'E2E compact admission sensor fixture',
97
+ detects: ['package.json'],
98
+ sensors: {
99
+ test: {
100
+ applicability: { allFiles: ['package.json'] },
101
+ fast: false,
102
+ timeout: 30_000,
103
+ variants: [{
104
+ id: 'npm-script', priority: 1,
105
+ requirements: { tool: 'npm', toolRange: '>=1.0.0', runtime: 'node', runtimeRange: '>=20.0.0' },
106
+ certifiedRange: '>=1.0.0',
107
+ command: { executable: 'npm', resolution: 'path', args: ['test', '--', '--silent'], packageManager: 'npm' },
108
+ assets: [], formatter: 'test', probe: { kind: 'package-script-present', script: 'test' },
109
+ }],
110
+ },
111
+ },
112
+ coverage: { schemaVersion: 1, classes: {
113
+ tests: { description: 'Fixture test execution', detectors: [{ sensor: 'test' }], remedy: { summary: 'Run fixture tests', command: 'npm test' } },
114
+ } },
115
+ }, null, 2) + '\n');
116
+ }
54
117
  async function until(fn, ms = 60000, label = 'condicion') {
55
118
  const t0 = Date.now();
56
119
  while (!fn()) {
@@ -62,19 +125,40 @@ async function until(fn, ms = 60000, label = 'condicion') {
62
125
  describe('E2E real: crash/restart del supervisor', () => {
63
126
  let repo;
64
127
  let stubBin;
128
+ let fixtureAwmHome;
65
129
  let env;
66
130
  const children = [];
67
131
  beforeAll(() => {
68
132
  if (!fs_1.default.existsSync(CLI))
69
133
  throw new Error('dist ausente: corre `cd cli && npm run build` antes de esta suite (Task 20 Step 1)');
70
134
  });
71
- beforeEach(() => {
135
+ beforeEach(async () => {
72
136
  repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-e2e-'));
73
137
  git(repo, 'init', '-q', '-b', 'main');
74
- fs_1.default.writeFileSync(path_1.default.join(repo, 'package.json'), JSON.stringify({ name: 'fixture', scripts: { test: 'node -e "process.exit(0)"' } }));
138
+ const npmVersion = (0, child_process_1.execFileSync)('npm', ['--version'], { encoding: 'utf8' }).trim();
139
+ fs_1.default.writeFileSync(path_1.default.join(repo, 'package.json'), JSON.stringify({ name: 'fixture', packageManager: `npm@${npmVersion}`, scripts: { test: 'node -e "process.exit(0)" --' } }));
140
+ // The package manager is deliberately selected in package metadata.
141
+ // Discovery must certify the same bounded PATH npm that the sensor runs,
142
+ // rather than accept a shadow package under this fixture's node_modules.
143
+ fs_1.default.mkdirSync(path_1.default.join(repo, 'plans'), { recursive: true });
144
+ fs_1.default.writeFileSync(path_1.default.join(repo, 'plans', 'fixture.md'), fs_1.default.readFileSync(path_1.default.join(__dirname, '../../core/plan/fixtures/compact-slices-v1/valid.md'), 'utf8'));
145
+ fs_1.default.writeFileSync(path_1.default.join(repo, 'source.md'), '## Canonical source\nfixture source\n');
146
+ fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
147
+ fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), JSON.stringify({
148
+ schemaVersion: 3, mode: 'project-sensors', pack: 'js-ts', source: { registry: 'baseline' },
149
+ sensors: { test: {
150
+ enabled: true, variantId: 'npm-script',
151
+ command: { executable: 'npm', resolution: 'path', args: ['test', '--', '--silent'], packageManager: 'npm' },
152
+ initializedCompatibility: { state: 'certified', reason: 'fixture', variantId: 'npm-script', toolVersion: npmVersion, runtimeVersion: process.versions.node, certifiedRange: '>=8.0.0', evidence: [] },
153
+ } },
154
+ }));
75
155
  git(repo, 'add', '.');
76
156
  git(repo, 'commit', '-qm', 'c');
77
157
  stubBin = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-e2e-bin-'));
158
+ fixtureAwmHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-e2e-home-'));
159
+ writeFixtureBaselineRegistry(fixtureAwmHome);
160
+ fs_1.default.writeFileSync(path_1.default.join(fixtureAwmHome, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'fixture://baseline' }]));
161
+ fs_1.default.writeFileSync(path_1.default.join(fixtureAwmHome, 'preferences.json'), JSON.stringify({ defaultAgent: 'codex', enabledAgents: ['codex', 'claude-code'], installMethod: 'symlink', defaultScope: 'local' }));
78
162
  // A bare extensionless #!/bin/sh script only runs via POSIX kernel shebang
79
163
  // interpretation -- Windows CreateProcess has none, so spawnStructured
80
164
  // (shell:false, matching production) would silently fail to launch this stub
@@ -82,13 +166,41 @@ describe('E2E real: crash/restart del supervisor', () => {
82
166
  // on both platforms (Node's spawn on win32 resolves via PATHEXT and transparently
83
167
  // re-invokes a found .cmd through cmd.exe) without touching production code.
84
168
  for (const name of ['codex', 'claude']) {
85
- fs_1.default.writeFileSync(path_1.default.join(stubBin, name), '#!/bin/sh\nwhile true; do sleep 1; done\n', { mode: 0o755 });
169
+ fs_1.default.writeFileSync(path_1.default.join(stubBin, name), `#!/bin/sh
170
+ # The supervisor invokes Codex as \`codex exec <prompt>\`; retain a distinct
171
+ # path for that real adapter contract instead of accidentally treating it as
172
+ # a bare binary invocation. Both controller forms deliberately stay alive:
173
+ # the test's requested \`node -e\` job is the process that must complete and
174
+ # leave its sidecar while the supervisor is killed.
175
+ if [ "$1" = "exec" ]; then shift; fi
176
+ while true; do sleep 1; done
177
+ `, { mode: 0o755 });
86
178
  fs_1.default.writeFileSync(path_1.default.join(stubBin, `${name}.cmd`), '@echo off\r\n:loop\r\ntimeout /t 1 /nobreak >nul\r\ngoto loop\r\n');
87
179
  }
88
- env = { ...process.env, PATH: `${stubBin}${path_1.default.delimiter}${process.env.PATH}` };
89
- (0, child_process_1.execFileSync)(process.execPath, [CLI, 'watch', '--init'], { cwd: repo, env });
180
+ env = { ...process.env, AWM_HOME: fixtureAwmHome, PATH: `${stubBin}${path_1.default.delimiter}${process.env.PATH}` };
181
+ (0, child_process_1.execFileSync)(process.execPath, [CLI, 'watch', '--init', '--plan', 'plans/fixture.md'], { cwd: repo, env });
182
+ const priorAwmHome = process.env.AWM_HOME;
183
+ process.env.AWM_HOME = fixtureAwmHome;
184
+ const sensorReport = await (0, run_1.runSensors)({ cwd: repo, all: true, readOnly: true });
185
+ if (priorAwmHome === undefined)
186
+ delete process.env.AWM_HOME;
187
+ else
188
+ process.env.AWM_HOME = priorAwmHome;
189
+ if (sensorReport.overall !== 'pass') {
190
+ process.env.AWM_HOME = fixtureAwmHome;
191
+ const live = await (0, live_1.resolveLiveCompatibility)(repo, 'js-ts');
192
+ if (priorAwmHome === undefined)
193
+ delete process.env.AWM_HOME;
194
+ else
195
+ process.env.AWM_HOME = priorAwmHome;
196
+ throw new Error(`fixture runSensors failed: ${JSON.stringify({ sensorReport, live })}`);
197
+ }
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' });
200
+ if (admission.status !== 0)
201
+ throw new Error(`fixture admission failed: ${admission.stdout}${admission.stderr}`);
90
202
  });
91
- afterEach(() => {
203
+ afterEach(async () => {
92
204
  // higiene: terminar TODO grupo que hayamos originado (supervisores,
93
205
  // stubs de controlador, wrappers) — cero huerfanos entre tests
94
206
  const s = readState(repo);
@@ -107,14 +219,17 @@ describe('E2E real: crash/restart del supervisor', () => {
107
219
  groups.add(j.processRef.processGroup);
108
220
  }
109
221
  }
110
- for (const pgid of groups)
111
- killGroup(pgid);
222
+ await terminateFixtureGroups(groups);
112
223
  children.length = 0;
113
- fs_1.default.rmSync(repo, { recursive: true, force: true });
114
- fs_1.default.rmSync(stubBin, { recursive: true, force: true });
224
+ await removeFixture(repo);
225
+ await removeFixture(stubBin);
226
+ await removeFixture(fixtureAwmHome);
115
227
  });
116
228
  function startSupervisor(provider) {
117
- const out = fs_1.default.openSync(path_1.default.join(repo, `sup-${children.length}.log`), 'a');
229
+ // Supervisor stdout is test harness output, not project evidence. An
230
+ // in-repo log would make the real currentness/admission gate reject
231
+ // the next tick before it can collect an exited job sidecar.
232
+ const out = fs_1.default.openSync(path_1.default.join(stubBin, `sup-${children.length}.log`), 'a');
118
233
  const child = (0, child_process_1.spawn)(process.execPath, [CLI, 'watch', '--provider', provider], {
119
234
  cwd: repo, env, detached: true, stdio: ['ignore', out, out],
120
235
  });
@@ -12,6 +12,7 @@ const process_1 = require("../../../src/core/journal/process");
12
12
  const store_1 = require("../../../src/core/journal/store");
13
13
  const paths_1 = require("../../../src/core/journal/paths");
14
14
  const exec_wrapper_1 = require("../../../src/commands/job/exec-wrapper");
15
+ const recovery = { schema: 'controller-recovery/v1', kind: 'resume-cycle' };
15
16
  describe('generaciones', () => {
16
17
  let repo;
17
18
  beforeEach(() => { repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-gen-')); (0, store_1.initJournal)(repo, 'rama'); });
@@ -56,7 +57,7 @@ describe('generaciones', () => {
56
57
  const begun = (0, generations_1.beginGeneration)(repo, 'rama');
57
58
  let captured;
58
59
  const wrapperRef = { pid: 42, startTime: 't', spawnNonce: 'wrapper-nonce', argvDigest: 'a', processGroup: 42, psArgsDigest: 'p' };
59
- (0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', 'continua el ciclo', (job, nonce) => {
60
+ (0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', recovery, (job, nonce) => {
60
61
  const duringSpawn = (0, generations_1.activeGeneration)((0, store_1.readJournal)(repo, 'rama').state);
61
62
  expect(duringSpawn.controllerJobId).toBe(job.id); // persistido ANTES del spawn
62
63
  expect(duringSpawn.spawnNonce).toBe(nonce);
@@ -67,9 +68,42 @@ describe('generaciones', () => {
67
68
  expect(captured.argv.join(' ')).toContain(`--generation ${begun.token}`);
68
69
  expect((0, generations_1.activeGeneration)((0, store_1.readJournal)(repo, 'rama').state).wrapperRef).toEqual(wrapperRef);
69
70
  });
71
+ test('an arbitrary prompt is rejected before durable launch intent or dispatch', () => {
72
+ (0, generations_1.beginGeneration)(repo, 'rama');
73
+ const before = (0, store_1.readJournal)(repo, 'rama').raw;
74
+ let dispatched = false;
75
+ expect(() => (0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', 'private source body and password=sentinel', () => { dispatched = true; })).toThrow(/recovery|intent|acción/i);
76
+ expect(dispatched).toBe(false);
77
+ expect((0, store_1.readJournal)(repo, 'rama').raw).toBe(before);
78
+ expect(() => (0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', { ...recovery, prompt: 'private body' }, () => { })).toThrow(/recovery/);
79
+ expect((0, store_1.readJournal)(repo, 'rama').raw).toBe(before);
80
+ });
81
+ test('legacy prompt custody remains readable but cannot launch or replay', () => {
82
+ (0, generations_1.beginGeneration)(repo, 'rama');
83
+ const state = (0, store_1.readJournal)(repo, 'rama').state;
84
+ state.generations[0].resumePrompt = 'historical legacy body';
85
+ (0, store_1.writeJournal)(repo, 'rama', state);
86
+ const before = (0, store_1.readJournal)(repo, 'rama').raw;
87
+ expect(() => (0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', recovery, () => { })).toThrow(/legacy/);
88
+ expect(() => (0, generations_1.ensureControllerGeneration)(repo, 'rama', 'codex', recovery, () => { }, 10000)).toThrow(/legacy/);
89
+ expect((0, store_1.readJournal)(repo, 'rama').raw).toBe(before);
90
+ });
91
+ test('closed recovery intent launches deterministically and persists no prompt body', () => {
92
+ (0, generations_1.beginGeneration)(repo, 'rama');
93
+ const action = { schema: 'controller-recovery/v1', kind: 'resume-cycle' };
94
+ const attempts = [];
95
+ (0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', action, job => { attempts.push(job.argv); });
96
+ const state = (0, store_1.readJournal)(repo, 'rama').state;
97
+ expect(state.generations[0]).toMatchObject({ resumeAction: action });
98
+ expect(state.generations[0].resumePrompt).toBeUndefined();
99
+ expect(JSON.stringify(state)).not.toContain('Sos el orquestador');
100
+ (0, generations_1.ensureControllerGeneration)(repo, 'rama', 'codex', { schema: 'controller-recovery/v1', kind: 'reconcile-active-jobs' }, job => { attempts.push(job.argv); }, 10000);
101
+ expect(attempts).toHaveLength(2);
102
+ expect(attempts[1]).toEqual(attempts[0]);
103
+ });
70
104
  test('claim sin identidad/resultado conserva ownership ambiguo; usa la edad del claim, no la de begin', () => {
71
105
  (0, generations_1.beginGeneration)(repo, 'rama');
72
- (0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', 'continua', () => { });
106
+ (0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', recovery, () => { });
73
107
  let s = (0, store_1.readJournal)(repo, 'rama').state;
74
108
  const gen = (0, generations_1.activeGeneration)(s);
75
109
  gen.launchedAt = new Date(Date.now() - 3600000).toISOString();
@@ -77,15 +111,90 @@ describe('generaciones', () => {
77
111
  fs_1.default.mkdirSync((0, paths_1.logsDir)(repo, 'rama'), { recursive: true });
78
112
  fs_1.default.writeFileSync((0, exec_wrapper_1.claimPath)((0, paths_1.logsDir)(repo, 'rama'), gen.controllerJobId, gen.spawnNonce), '{}');
79
113
  expect((0, generations_1.controllerGenerationHasUnresolvedClaim)(repo, 'rama', gen)).toBe(true);
80
- (0, generations_1.ensureControllerGeneration)(repo, 'rama', 'codex', 'continua', () => { }, 10000);
114
+ (0, generations_1.ensureControllerGeneration)(repo, 'rama', 'codex', recovery, () => { }, 10000);
81
115
  expect((0, store_1.readJournal)(repo, 'rama').state.cycle.status).toBe('IN_PROGRESS'); // claim recien creado: aun dentro de gracia
82
116
  const old = new Date(Date.now() - 20000);
83
117
  fs_1.default.utimesSync((0, exec_wrapper_1.claimPath)((0, paths_1.logsDir)(repo, 'rama'), gen.controllerJobId, gen.spawnNonce), old, old);
84
- (0, generations_1.ensureControllerGeneration)(repo, 'rama', 'codex', 'continua', () => { }, 10000);
118
+ (0, generations_1.ensureControllerGeneration)(repo, 'rama', 'codex', recovery, () => { }, 10000);
85
119
  expect((0, store_1.readJournal)(repo, 'rama').state.cycle.status).toBe('BLOCKED');
86
120
  fs_1.default.writeFileSync((0, exec_wrapper_1.resultPath)((0, paths_1.logsDir)(repo, 'rama'), gen.controllerJobId, gen.spawnNonce), JSON.stringify({ exitCode: 1 }));
87
121
  expect((0, generations_1.controllerGenerationHasUnresolvedClaim)(repo, 'rama', gen)).toBe(false);
88
122
  });
123
+ test('replay preserves exact argv commitment and rejects environment command drift without mutation', () => {
124
+ (0, generations_1.beginGeneration)(repo, 'rama');
125
+ (0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', recovery, () => { });
126
+ const before = (0, store_1.readJournal)(repo, 'rama').raw;
127
+ const previous = process.env.AWM_CONTROLLER_ARGV;
128
+ process.env.AWM_CONTROLLER_ARGV = JSON.stringify(['different-controller']);
129
+ try {
130
+ expect(() => (0, generations_1.ensureControllerGeneration)(repo, 'rama', 'codex', recovery, () => { }, 10000)).toThrow(/argv cambio/);
131
+ expect((0, store_1.readJournal)(repo, 'rama').raw).toBe(before);
132
+ }
133
+ finally {
134
+ if (previous === undefined)
135
+ delete process.env.AWM_CONTROLLER_ARGV;
136
+ else
137
+ process.env.AWM_CONTROLLER_ARGV = previous;
138
+ }
139
+ });
140
+ test('a real launched controller generation retains metadata but no prompt or source output', async () => {
141
+ (0, generations_1.beginGeneration)(repo, 'rama');
142
+ const body = 'SOURCE_BODY_SENTINEL_private_controller_text';
143
+ const previous = process.env.AWM_CONTROLLER_ARGV;
144
+ process.env.AWM_CONTROLLER_ARGV = JSON.stringify([process.execPath, '-e', `process.stdout.write('${body}')`]);
145
+ let execution;
146
+ try {
147
+ (0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', recovery, (job, nonce, logsRoot, repoRoot) => {
148
+ execution = (0, exec_wrapper_1.runExecWrapper)({ logsRoot, jobId: job.id, nonce, argv: job.argv, cwd: job.cwd, repoRoot });
149
+ });
150
+ await execution;
151
+ const state = (0, store_1.readJournal)(repo, 'rama').state;
152
+ expect(state.generations[0].launchArgvDigest).toMatch(/^[a-f0-9]{16}$/);
153
+ const artifacts = fs_1.default.readdirSync((0, paths_1.logsDir)(repo, 'rama')).map(name => fs_1.default.readFileSync(path_1.default.join((0, paths_1.logsDir)(repo, 'rama'), name), 'utf8')).join('\n');
154
+ expect(artifacts).toContain('argvDigest');
155
+ expect(artifacts).toContain('exitCode');
156
+ expect(artifacts + JSON.stringify(state)).not.toContain(body);
157
+ expect(artifacts + JSON.stringify(state)).not.toContain('Retoma el plan');
158
+ }
159
+ finally {
160
+ if (previous === undefined)
161
+ delete process.env.AWM_CONTROLLER_ARGV;
162
+ else
163
+ process.env.AWM_CONTROLLER_ARGV = previous;
164
+ }
165
+ });
166
+ test('a user verifier named like a controller keeps ordinary bounded output without generation proof', async () => {
167
+ const logsRoot = (0, paths_1.logsDir)(repo, 'rama');
168
+ const argv = [process.execPath, '-e', "process.stdout.write('ordinary-verifier-output')"];
169
+ const state = (0, store_1.readJournal)(repo, 'rama').state;
170
+ state.jobs['controller-gen-1'] = { id: 'controller-gen-1', argv, spawnNonce: 'normal-verifier', fingerprint: 'fixture', commandDigest: 'fixture',
171
+ cwd: '.', paths: [], expandedPaths: [], executionState: 'spawn-intent', observationState: 'progressing', phaseTimestamps: {} };
172
+ (0, store_1.writeJournal)(repo, 'rama', state);
173
+ await (0, exec_wrapper_1.runExecWrapper)({ logsRoot, repoRoot: repo, jobId: 'controller-gen-1', nonce: 'normal-verifier', cwd: '.',
174
+ argv });
175
+ const artifacts = fs_1.default.readdirSync(logsRoot).map(name => fs_1.default.readFileSync(path_1.default.join(logsRoot, name), 'utf8')).join('\n');
176
+ expect(artifacts).toContain('ordinary-verifier-output');
177
+ });
178
+ test.each(['missing', 'corrupt', 'symlink', 'oversized', 'nonce-drift', 'argv-drift'])('controller %s custody fails closed before claim or launch artifacts', async (fault) => {
179
+ (0, generations_1.beginGeneration)(repo, 'rama');
180
+ let launched;
181
+ (0, generations_1.launchControllerGeneration)(repo, 'rama', 'codex', recovery, (job, nonce) => { launched = { id: job.id, nonce, argv: job.argv }; });
182
+ const stateFile = path_1.default.resolve((0, paths_1.logsDir)(repo, 'rama'), '..', 'state.json');
183
+ if (fault === 'missing')
184
+ fs_1.default.unlinkSync(stateFile);
185
+ if (fault === 'corrupt')
186
+ fs_1.default.writeFileSync(stateFile, '{bad json');
187
+ if (fault === 'oversized')
188
+ fs_1.default.writeFileSync(stateFile, ' '.repeat(1024 * 1024 + 1));
189
+ if (fault === 'symlink') {
190
+ fs_1.default.renameSync(stateFile, `${stateFile}.original`);
191
+ fs_1.default.symlinkSync(`${stateFile}.original`, stateFile, 'file');
192
+ }
193
+ const argv = fault === 'argv-drift' ? [process.execPath, '-e', "process.stdout.write('source body')"] : launched.argv;
194
+ const nonce = fault === 'nonce-drift' ? 'wrong-nonce' : launched.nonce;
195
+ await expect((0, exec_wrapper_1.runExecWrapper)({ logsRoot: (0, paths_1.logsDir)(repo, 'rama'), repoRoot: repo, jobId: launched.id, nonce, argv, cwd: '.' })).rejects.toThrow(/controller|custody|custodia|bounded|regular/i);
196
+ expect(fs_1.default.readdirSync((0, paths_1.logsDir)(repo, 'rama'))).toEqual([]);
197
+ });
89
198
  test('resolveGeneration: muerte probada => proven-dead; vivo+indeterminate => custodia con estado BLOCKED (R4.2b)', async () => {
90
199
  const adapter = (0, adapter_1.adapterFor)('codex');
91
200
  (0, generations_1.beginGeneration)(repo, 'rama');