agentic-workflow-manager 9.7.0 → 9.8.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 (114) hide show
  1. package/README.md +5 -1
  2. package/dist/src/commands/doctor.js +4 -2
  3. package/dist/src/commands/export.js +3 -0
  4. package/dist/src/commands/init.js +10 -2
  5. package/dist/src/commands/job/exec-wrapper.js +53 -3
  6. package/dist/src/commands/plan/index.js +124 -6
  7. package/dist/src/commands/registry/add.js +2 -2
  8. package/dist/src/commands/registry/index.js +7 -5
  9. package/dist/src/commands/registry/install-bundles.js +4 -4
  10. package/dist/src/commands/registry/status.js +4 -4
  11. package/dist/src/commands/sensors/bootstrap.js +36 -5
  12. package/dist/src/commands/sensors/compatibility/contract.js +19 -4
  13. package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
  14. package/dist/src/commands/sensors/compatibility/live.js +18 -1
  15. package/dist/src/commands/sensors/compatibility/probe.js +8 -2
  16. package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
  17. package/dist/src/commands/sensors/coverage/evidence.js +19 -10
  18. package/dist/src/commands/sensors/coverage/index.js +11 -2
  19. package/dist/src/commands/sensors/coverage/resolve.js +10 -0
  20. package/dist/src/commands/sensors/exec.js +16 -1
  21. package/dist/src/commands/sensors/migrate.js +62 -2
  22. package/dist/src/commands/sensors/prepare.js +2 -0
  23. package/dist/src/commands/sensors/result.js +2 -0
  24. package/dist/src/commands/sensors/run.js +64 -1
  25. package/dist/src/commands/sensors/status.js +6 -4
  26. package/dist/src/commands/sync.js +2 -1
  27. package/dist/src/commands/update.js +3 -1
  28. package/dist/src/commands/watch/archive-unused.js +138 -0
  29. package/dist/src/commands/watch/generations.js +32 -9
  30. package/dist/src/commands/watch/index.js +108 -2
  31. package/dist/src/commands/watch/init.js +149 -18
  32. package/dist/src/commands/watch/supervisor.js +167 -13
  33. package/dist/src/commands/watch/teardown-driver.js +13 -13
  34. package/dist/src/commands/watch/track-state.js +59 -0
  35. package/dist/src/commands/watch/tracks.js +38 -102
  36. package/dist/src/core/admission/index.js +181 -0
  37. package/dist/src/core/admission/registry-contracts.js +290 -0
  38. package/dist/src/core/bundle-install.js +2 -2
  39. package/dist/src/core/bundles.js +92 -19
  40. package/dist/src/core/diagnostics/context.js +1 -1
  41. package/dist/src/core/export/index.js +1 -1
  42. package/dist/src/core/export/resolve.js +2 -2
  43. package/dist/src/core/journal/fingerprint.js +35 -1
  44. package/dist/src/core/journal/paths.js +11 -0
  45. package/dist/src/core/journal/store.js +146 -4
  46. package/dist/src/core/journal/types.js +35 -1
  47. package/dist/src/core/migration/historical-provenance.js +18 -0
  48. package/dist/src/core/migration/index.js +322 -0
  49. package/dist/src/core/plan/identity.js +75 -0
  50. package/dist/src/core/plan/validate.js +178 -64
  51. package/dist/src/core/reconciliation.js +1 -1
  52. package/dist/src/index.js +46 -39
  53. package/dist/src/utils/grouping.js +1 -1
  54. package/dist/src/utils/registry-view.js +1 -1
  55. package/dist/tests/commands/bundle-diagnostics.test.js +145 -0
  56. package/dist/tests/commands/export.test.js +28 -2
  57. package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
  58. package/dist/tests/commands/job/gate-reconcile.test.js +24 -10
  59. package/dist/tests/commands/multi-agent-targeting.test.js +1 -1
  60. package/dist/tests/commands/pin.test.js +23 -23
  61. package/dist/tests/commands/plan/index.test.js +630 -14
  62. package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
  63. package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
  64. package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
  65. package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
  66. package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
  67. package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
  68. package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
  69. package/dist/tests/commands/sensors/exec.test.js +112 -0
  70. package/dist/tests/commands/sensors/run-changed.test.js +11 -0
  71. package/dist/tests/commands/sensors/run.test.js +15 -2
  72. package/dist/tests/commands/sensors/status.test.js +3 -3
  73. package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
  74. package/dist/tests/commands/watch/generations.test.js +113 -4
  75. package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
  76. package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
  77. package/dist/tests/commands/watch/track-finalize.test.js +20 -3
  78. package/dist/tests/commands/watch/track-freeze.test.js +43 -30
  79. package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
  80. package/dist/tests/commands/watch/watch-init.test.js +244 -0
  81. package/dist/tests/core/admission/index.test.js +87 -0
  82. package/dist/tests/core/bundle-install.test.js +36 -0
  83. package/dist/tests/core/bundles-multiroot.test.js +24 -2
  84. package/dist/tests/core/bundles-overrides.test.js +10 -7
  85. package/dist/tests/core/bundles.test.js +75 -6
  86. package/dist/tests/core/diagnostics/context.test.js +2 -2
  87. package/dist/tests/core/export/engine.test.js +1 -1
  88. package/dist/tests/core/export/resolve.test.js +1 -1
  89. package/dist/tests/core/init/mutation-targets.test.js +1 -1
  90. package/dist/tests/core/init/orchestrator.test.js +1 -1
  91. package/dist/tests/core/init/steps.test.js +1 -1
  92. package/dist/tests/core/journal/fingerprint.test.js +22 -0
  93. package/dist/tests/core/journal/store.test.js +53 -0
  94. package/dist/tests/core/journal/types.test.js +7 -0
  95. package/dist/tests/core/migration/facts.test.js +289 -0
  96. package/dist/tests/core/migration/historical-provenance.test.js +25 -0
  97. package/dist/tests/core/plan/validate.test.js +320 -6
  98. package/dist/tests/core/reconciliation.test.js +1 -1
  99. package/dist/tests/core/sync-profile-atomicity.test.js +1 -1
  100. package/dist/tests/integration/pack-e2e.test.js +4 -1
  101. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
  102. package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
  103. package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
  104. package/dist/tests/providers/injection-config.test.js +14 -6
  105. package/dist/tests/structural/bundle-skill-reference-contract.test.js +56 -0
  106. package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
  107. package/dist/tests/structural/native-release-artifacts.test.js +16 -0
  108. package/dist/tests/utils/grouping.test.js +2 -2
  109. package/dist/tests/utils/registry-view.test.js +7 -7
  110. package/package.json +2 -2
  111. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  112. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  113. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  114. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -17,7 +50,14 @@ const store_1 = require("../../../src/core/journal/store");
17
50
  const paths_1 = require("../../../src/core/journal/paths");
18
51
  const process_1 = require("../../../src/core/journal/process");
19
52
  const fingerprint_1 = require("../../../src/core/journal/fingerprint");
53
+ const fingerprint = __importStar(require("../../../src/core/journal/fingerprint"));
20
54
  const tracks_1 = require("../../../src/commands/watch/tracks");
55
+ const validate_1 = require("../../../src/core/plan/validate");
56
+ const registryInventory = __importStar(require("../../../src/core/registries"));
57
+ const currentnessCheck = __importStar(require("../../../src/core/currentness/check"));
58
+ const sensorCommands = __importStar(require("../../../src/commands/sensors/run"));
59
+ const preferencesConfig = __importStar(require("../../../src/utils/config"));
60
+ const providers_1 = require("../../../src/providers");
21
61
  jest.setTimeout(60000);
22
62
  // runSupervisorLoop's full external-controller lifecycle (spawn stub codex ->
23
63
  // identity captured by the wrapper -> adopted via collectControllerGeneration's
@@ -49,6 +89,28 @@ function setupRepo() {
49
89
  fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), '{}');
50
90
  return repo;
51
91
  }
92
+ const admitted = async () => ({
93
+ state: 'admitted', planState: 'valid', executionMode: 'desatendido',
94
+ journal: 'current', currentness: 'current', sensors: 'pass', diagnostics: [],
95
+ });
96
+ /** Legacy fixtures predate compact-only admission; make their execution contract explicit. */
97
+ function initUnattendedFixture(repo) {
98
+ (0, init_1.initWatch)(repo, 'main');
99
+ const planPath = path_1.default.join(repo, 'plans', 'fixture.md');
100
+ fs_1.default.mkdirSync(path_1.default.dirname(planPath), { recursive: true });
101
+ fs_1.default.writeFileSync(planPath, fs_1.default.readFileSync(path_1.default.join(__dirname, '../../core/plan/fixtures/compact-slices-v1/valid.md'), 'utf8'));
102
+ fs_1.default.writeFileSync(path_1.default.join(repo, 'source.md'), '## Canonical source\nfixture source\n');
103
+ const plan = (0, validate_1.validatePlanFile)('plans/fixture.md', repo);
104
+ if (plan.state !== 'valid')
105
+ throw new Error('compact fixture must validate');
106
+ const state = (0, store_1.readJournal)(repo, 'main').state;
107
+ state.schema = 2;
108
+ state.planBinding = {
109
+ path: 'plans/fixture.md', digest: plan.planDigest, schema: plan.schema,
110
+ executionMode: 'desatendido', boundAt: new Date().toISOString(),
111
+ };
112
+ (0, store_1.writeJournal)(repo, 'main', state);
113
+ }
52
114
  function emitVerdict(repo, token, obligationId, verdictId) {
53
115
  const argv = ['awm-review', obligationId];
54
116
  const fp = (0, fingerprint_1.computeFingerprint)(repo, argv, [], '.').fingerprint;
@@ -160,43 +222,70 @@ describe('supervisor loop', () => {
160
222
  process.env.PATH = `${stubBin}${path_1.default.delimiter}${process.env.PATH}`;
161
223
  });
162
224
  afterEach(() => {
225
+ jest.restoreAllMocks();
163
226
  process.env.PATH = oldPath;
164
227
  fs_1.default.rmSync(repo, { recursive: true, force: true });
165
228
  fs_1.default.rmSync(stubBin, { recursive: true, force: true });
166
229
  });
167
230
  test('ticks drenan y declaran COMPLETE solo con gate verde + cero vivos (R4.5)', async () => {
168
- (0, init_1.initWatch)(repo, 'main'); // sin package.json => requiredVerifiers []
231
+ initUnattendedFixture(repo);
169
232
  const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, provider: 'codex', tickMs: 50, reconcileGraceMs: 10000 };
170
- const sup = new supervisor_1.Supervisor(repo, 'main', cfg, fakeSpawner);
233
+ // Recovery classification has its own unit suite. This lifecycle test
234
+ // owns draining/COMPLETE, so pin the recovered schema-2 fixture green
235
+ // and do not leak the external controller stub merely to test wrappers.
236
+ const recovery = jest.spyOn(fingerprint, 'reconcileUnattendedRecovery').mockReturnValue({
237
+ state: 'ready', nextAction: 'select-work', activeJobIds: [], diagnostics: [],
238
+ });
239
+ let controllerSpawnAttempts = 0;
240
+ const lifecycleSpawner = (job, nonce, logsRoot, repoRoot) => {
241
+ if (job.argv[0] === 'codex') {
242
+ controllerSpawnAttempts++;
243
+ return;
244
+ }
245
+ fakeSpawner(job, nonce, logsRoot, repoRoot);
246
+ };
247
+ const sup = new supervisor_1.Supervisor(repo, 'main', cfg, lifecycleSpawner, undefined, admitted);
248
+ // The schema-2 recovery path only consumes requests authorized by the
249
+ // active generation. Establish it before issuing the scenario facts;
250
+ // the legacy fixture's hard-coded `g0` was never consumed and left the
251
+ // fake controller running forever in `select-work`.
252
+ const generation = (0, generations_1.beginGeneration)(repo, 'main');
171
253
  // el controlador (aqui: el test) registra plan de ciclo + task + jobs enlazados
172
- (0, requests_1.emitRequest)(repo, 'main', { kind: 'register-entity', generationToken: 'g0', idempotencyKey: 'e1',
254
+ (0, requests_1.emitRequest)(repo, 'main', { kind: 'register-entity', generationToken: generation.token, idempotencyKey: 'e1',
173
255
  payload: { entity: 'task', taskId: 'T1', title: 't', verificationPlan: [{ id: 'v1', kind: 'test' }, { id: 'v-sensors', kind: 'sensors' }], reviewObligations: [{ id: 'o-spec', kind: 'spec' }, { id: 'o-quality', kind: 'quality' }] } });
174
- (0, requests_1.emitRequest)(repo, 'main', { kind: 'register-entity', generationToken: 'g0', idempotencyKey: 'e2',
256
+ (0, requests_1.emitRequest)(repo, 'main', { kind: 'register-entity', generationToken: generation.token, idempotencyKey: 'e2',
175
257
  payload: { entity: 'cycle-plan', items: [{ id: 'cv1', kind: 'qa' }, { id: 'cv-interlock', kind: 'interlock' }] } });
176
- (0, request_1.requestJob)(repo, 'main', 'g0', ['node', '-e', 'setTimeout(()=>process.exit(0), 400)'], [], '.', { satisfies: 'v1' });
177
- (0, request_1.requestJob)(repo, 'main', 'g0', ['node', '-e', 'process.exit(0)'], [], '.', { satisfies: 'v-sensors' });
178
- (0, request_1.requestJob)(repo, 'main', 'g0', ['node', '-e', 'process.exit(0)'], [], '.', { satisfies: 'cv1' });
179
- (0, request_1.requestJob)(repo, 'main', 'g0', ['node', '-e', 'process.exit(0)'], [], '.', { satisfies: 'cv-interlock' });
180
- emitVerdict(repo, 'g0', 'o-spec', 'verd-spec');
181
- emitVerdict(repo, 'g0', 'o-quality', 'verd-quality');
182
- (0, requests_1.emitRequest)(repo, 'main', { kind: 'register-entity', generationToken: 'g0', idempotencyKey: 'e3',
258
+ (0, request_1.requestJob)(repo, 'main', generation.token, ['node', '-e', 'setTimeout(()=>process.exit(0), 400)'], [], '.', { satisfies: 'v1' });
259
+ (0, request_1.requestJob)(repo, 'main', generation.token, ['node', '-e', 'process.exit(0)'], [], '.', { satisfies: 'v-sensors' });
260
+ (0, request_1.requestJob)(repo, 'main', generation.token, ['node', '-e', 'process.exit(0)'], [], '.', { satisfies: 'cv1' });
261
+ (0, request_1.requestJob)(repo, 'main', generation.token, ['node', '-e', 'process.exit(0)'], [], '.', { satisfies: 'cv-interlock' });
262
+ emitVerdict(repo, generation.token, 'o-spec', 'verd-spec');
263
+ emitVerdict(repo, generation.token, 'o-quality', 'verd-quality');
264
+ (0, requests_1.emitRequest)(repo, 'main', { kind: 'register-entity', generationToken: generation.token, idempotencyKey: 'e3',
183
265
  payload: { entity: 'task-status', taskId: 'T1', status: 'done' } });
184
- let sawContinueWithLiveJob = false;
185
- let outcome = 'continue';
186
- for (let i = 0; i < 400 && outcome !== 'complete'; i++) {
187
- outcome = await sup.tick();
188
- const s = (0, store_1.readJournal)(repo, 'main').state;
189
- const live = Object.values(s.jobs).some((j) => ['received', 'spawn-intent', 'claimed', 'running'].includes(j.executionState));
190
- if (outcome === 'continue' && live)
191
- sawContinueWithLiveJob = true; // drenaje ANTES de COMPLETE
192
- await new Promise((r) => setTimeout(r, 50));
266
+ try {
267
+ let sawContinueWithLiveJob = false;
268
+ let outcome = 'continue';
269
+ for (let i = 0; i < 400 && outcome !== 'complete'; i++) {
270
+ outcome = await sup.tick();
271
+ const s = (0, store_1.readJournal)(repo, 'main').state;
272
+ const live = Object.values(s.jobs).some((j) => ['received', 'spawn-intent', 'claimed', 'running'].includes(j.executionState));
273
+ if (outcome === 'continue' && live)
274
+ sawContinueWithLiveJob = true; // drenaje ANTES de COMPLETE
275
+ await new Promise((r) => setTimeout(r, 50));
276
+ }
277
+ expect(outcome).toBe('complete');
278
+ expect(sawContinueWithLiveJob).toBe(true);
279
+ const final = (0, store_1.readJournal)(repo, 'main').state;
280
+ expect(final.cycle.status).toBe('COMPLETE');
281
+ expect(typeof final.cycle.completedAt).toBe('string');
282
+ expect(Object.values(final.jobs).every((j) => j.executionState === 'exited' && j.verdict === 'pass')).toBe(true);
283
+ expect(final.generations.every((entry) => entry.state === 'terminated' && entry.processRef === undefined)).toBe(true);
284
+ expect(controllerSpawnAttempts).toBeGreaterThan(0);
285
+ }
286
+ finally {
287
+ recovery.mockRestore();
193
288
  }
194
- expect(outcome).toBe('complete');
195
- expect(sawContinueWithLiveJob).toBe(true);
196
- const final = (0, store_1.readJournal)(repo, 'main').state;
197
- expect(final.cycle.status).toBe('COMPLETE');
198
- expect(typeof final.cycle.completedAt).toBe('string');
199
- expect(Object.values(final.jobs).every((j) => j.executionState === 'exited' && j.verdict === 'pass')).toBe(true);
200
289
  });
201
290
  // Regresion (post-review #2, Task 12/R7 — Finding 2 de la segunda ronda
202
291
  // de re-review): la version anterior de esta prueba armaba el escenario
@@ -215,7 +304,7 @@ describe('supervisor loop', () => {
215
304
  // vivos) y confirma que `cycle.status` llega a COMPLETE por ese camino
216
305
  // generico, exactamente como lo haria un plan sin tracks en absoluto.
217
306
  test('una cohorte que cayo a fallback SERIAL via el reducer real no cuelga el ciclo: COMPLETE llega igual por el camino generico (regresion post-review R7, ronda 2)', async () => {
218
- (0, init_1.initWatch)(repo, 'main');
307
+ initUnattendedFixture(repo);
219
308
  const tracksRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-loop-tracks-'));
220
309
  try {
221
310
  const baseSha = (0, child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim();
@@ -240,20 +329,32 @@ describe('supervisor loop', () => {
240
329
  // --- Fase 2: sobre ESE MISMO journal, completar el ciclo por el
241
330
  // camino generico (task done + qa/interlock satisfechos + cero
242
331
  // jobs vivos) — la misma receta que la prueba R4.5 de arriba.
243
- (0, requests_1.emitRequest)(repo, 'main', { kind: 'register-entity', generationToken: 'g0', idempotencyKey: 'e1',
332
+ const generation = (0, generations_1.beginGeneration)(repo, 'main');
333
+ (0, requests_1.emitRequest)(repo, 'main', { kind: 'register-entity', generationToken: generation.token, idempotencyKey: 'e1',
244
334
  payload: { entity: 'task', taskId: 'T1', title: 't', verificationPlan: [{ id: 'v1', kind: 'test' }, { id: 'v-sensors', kind: 'sensors' }], reviewObligations: [{ id: 'o-spec', kind: 'spec' }, { id: 'o-quality', kind: 'quality' }] } });
245
- (0, requests_1.emitRequest)(repo, 'main', { kind: 'register-entity', generationToken: 'g0', idempotencyKey: 'e2',
335
+ (0, requests_1.emitRequest)(repo, 'main', { kind: 'register-entity', generationToken: generation.token, idempotencyKey: 'e2',
246
336
  payload: { entity: 'cycle-plan', items: [{ id: 'cv1', kind: 'qa' }, { id: 'cv-interlock', kind: 'interlock' }] } });
247
- (0, request_1.requestJob)(repo, 'main', 'g0', ['node', '-e', 'process.exit(0)'], [], '.', { satisfies: 'v1' });
248
- (0, request_1.requestJob)(repo, 'main', 'g0', ['node', '-e', 'process.exit(0)'], [], '.', { satisfies: 'v-sensors' });
249
- (0, request_1.requestJob)(repo, 'main', 'g0', ['node', '-e', 'process.exit(0)'], [], '.', { satisfies: 'cv1' });
250
- (0, request_1.requestJob)(repo, 'main', 'g0', ['node', '-e', 'process.exit(0)'], [], '.', { satisfies: 'cv-interlock' });
251
- emitVerdict(repo, 'g0', 'o-spec', 'verd-spec');
252
- emitVerdict(repo, 'g0', 'o-quality', 'verd-quality');
253
- (0, requests_1.emitRequest)(repo, 'main', { kind: 'register-entity', generationToken: 'g0', idempotencyKey: 'e3',
337
+ (0, request_1.requestJob)(repo, 'main', generation.token, ['node', '-e', 'process.exit(0)'], [], '.', { satisfies: 'v1' });
338
+ (0, request_1.requestJob)(repo, 'main', generation.token, ['node', '-e', 'process.exit(0)'], [], '.', { satisfies: 'v-sensors' });
339
+ (0, request_1.requestJob)(repo, 'main', generation.token, ['node', '-e', 'process.exit(0)'], [], '.', { satisfies: 'cv1' });
340
+ (0, request_1.requestJob)(repo, 'main', generation.token, ['node', '-e', 'process.exit(0)'], [], '.', { satisfies: 'cv-interlock' });
341
+ emitVerdict(repo, generation.token, 'o-spec', 'verd-spec');
342
+ emitVerdict(repo, generation.token, 'o-quality', 'verd-quality');
343
+ (0, requests_1.emitRequest)(repo, 'main', { kind: 'register-entity', generationToken: generation.token, idempotencyKey: 'e3',
254
344
  payload: { entity: 'task-status', taskId: 'T1', status: 'done' } });
255
345
  const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, provider: 'codex', tickMs: 50, reconcileGraceMs: 10000 };
256
- const sup = new supervisor_1.Supervisor(repo, 'main', cfg, fakeSpawner);
346
+ // The compact recovery reducer is verified independently. This test
347
+ // exercises the fallback SERIAL completion path, so avoid an
348
+ // external controller process while keeping real job wrappers.
349
+ jest.spyOn(fingerprint, 'reconcileUnattendedRecovery').mockReturnValue({
350
+ state: 'ready', nextAction: 'select-work', activeJobIds: [], diagnostics: [],
351
+ });
352
+ const lifecycleSpawner = (job, nonce, logsRoot, repoRoot) => {
353
+ if (job.argv[0] === 'codex')
354
+ return;
355
+ fakeSpawner(job, nonce, logsRoot, repoRoot);
356
+ };
357
+ const sup = new supervisor_1.Supervisor(repo, 'main', cfg, lifecycleSpawner, undefined, admitted);
257
358
  let outcome = 'continue';
258
359
  for (let i = 0; i < 400 && outcome !== 'complete'; i++) {
259
360
  outcome = await sup.tick();
@@ -265,34 +366,120 @@ describe('supervisor loop', () => {
265
366
  expect(final.cohortPhase).toBe('SERIAL'); // nunca inventa una transicion SERIAL -> COMPLETE
266
367
  for (const t of final.tracks)
267
368
  expect(['REMOVED', 'DECLARED']).toContain(t.phase);
369
+ const { refIsAlive } = require('../../../src/core/journal/process');
370
+ expect(final.generations.every((entry) => entry.state === 'terminated' && (entry.processRef === undefined || !refIsAlive(entry.processRef)))).toBe(true);
268
371
  }
269
372
  finally {
270
373
  fs_1.default.rmSync(tracksRoot, { recursive: true, force: true });
271
374
  }
272
375
  });
273
376
  test('tick verifica branch antes del launch y un ciclo COMPLETE no lanza otro controller', async () => {
274
- (0, init_1.initWatch)(repo, 'main');
377
+ initUnattendedFixture(repo);
275
378
  let calls = 0;
276
379
  const spy = () => { calls++; };
277
380
  const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, tickMs: 10 };
278
381
  const s = (0, store_1.readJournal)(repo, 'main').state;
279
382
  s.cycle.status = 'COMPLETE';
280
383
  (0, store_1.writeJournal)(repo, 'main', s);
281
- expect(await new supervisor_1.Supervisor(repo, 'main', cfg, spy).tick()).toBe('complete');
384
+ expect(await new supervisor_1.Supervisor(repo, 'main', cfg, spy, undefined, admitted).tick()).toBe('complete');
282
385
  expect(calls).toBe(0);
283
386
  const reset = (0, store_1.readJournal)(repo, 'main').state;
284
387
  reset.cycle.status = 'IN_PROGRESS';
285
388
  (0, store_1.writeJournal)(repo, 'main', reset);
286
389
  git(repo, 'checkout', '-qb', 'otra');
287
- await expect(new supervisor_1.Supervisor(repo, 'main', cfg, spy).tick()).rejects.toThrow(/rama|branch/i);
390
+ await expect(new supervisor_1.Supervisor(repo, 'main', cfg, spy, undefined, admitted).tick()).rejects.toThrow(/rama|branch/i);
288
391
  expect(calls).toBe(0);
289
392
  });
393
+ test('una admisión compacta bloqueada no lanza controlador ni wrappers', async () => {
394
+ initUnattendedFixture(repo);
395
+ const state = (0, store_1.readJournal)(repo, 'main').state;
396
+ state.schema = 2;
397
+ state.planBinding = { path: 'plans/exact.md', digest: 'a'.repeat(64), schema: 'compact-slices/v1', executionMode: 'desatendido', boundAt: new Date().toISOString() };
398
+ (0, store_1.writeJournal)(repo, 'main', state);
399
+ (0, request_1.requestJob)(repo, 'main', 'g0', ['node', '-e', 'process.exit(0)'], [], '.');
400
+ let spawns = 0;
401
+ const blocked = async () => ({ state: 'blocked', planState: 'valid', journal: 'current', currentness: 'stale', sensors: 'not-required', diagnostics: [{ code: 'ADMISSION_CURRENTNESS_BLOCKED', message: 'stale' }] });
402
+ const outcome = await new supervisor_1.Supervisor(repo, 'main', supervisor_1.DEFAULT_SUPERVISOR_CONFIG, () => { spawns++; }, undefined, blocked).tick();
403
+ expect(outcome).toBe('custody');
404
+ expect(spawns).toBe(0);
405
+ expect((0, store_1.readJournal)(repo, 'main').state.cycle.status).toBe('BLOCKED');
406
+ });
407
+ test('native default admission blocks an actual incompatible runtime supplier on a project-only plan before sensors or dispatch', async () => {
408
+ initUnattendedFixture(repo);
409
+ const oldHome = process.env.HOME;
410
+ process.env.HOME = path_1.default.join(repo, 'isolated-home');
411
+ const registryRoot = path_1.default.join(repo, 'supplier-registry');
412
+ const skillSource = path_1.default.join(registryRoot, 'skills', 'using-awm');
413
+ fs_1.default.mkdirSync(skillSource, { recursive: true });
414
+ fs_1.default.writeFileSync(path_1.default.join(skillSource, 'SKILL.md'), 'runtime contract');
415
+ fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'awm-registry.json'), '{"minCliVersion":"999.0.0"}');
416
+ const installed = (0, providers_1.providerFor)('codex').skill.global;
417
+ fs_1.default.mkdirSync(installed, { recursive: true });
418
+ fs_1.default.symlinkSync(skillSource, path_1.default.join(installed, 'using-awm'), process.platform === 'win32' ? 'junction' : 'dir');
419
+ const inventory = jest.spyOn(registryInventory, 'listRegistries').mockReturnValue([{ name: 'supplier', remote: 'https://example.invalid/supplier.git', contentRoot: registryRoot }]);
420
+ const preferences = jest.spyOn(preferencesConfig, 'readPreferences').mockReturnValue({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' });
421
+ const currentness = jest.spyOn(currentnessCheck, 'checkCurrentness').mockResolvedValue({ checkedAt: 'x', compatibility: { status: 'not-checked' }, components: ['cli', 'registry:supplier'].map(component => ({ component, installed: '1.0.0', latest: '1.0.0', channel: 'stable', source: 'fixture', checkedAt: 'x', status: 'current', detail: 'ok', remedy: 'none' })) });
422
+ const sensors = jest.spyOn(sensorCommands, 'runSensors').mockResolvedValue({ overall: 'pass', sensors: [] });
423
+ let dispatches = 0;
424
+ try {
425
+ const outcome = await new supervisor_1.Supervisor(repo, 'main', supervisor_1.DEFAULT_SUPERVISOR_CONFIG, () => { dispatches++; }).tick();
426
+ expect(outcome).toBe('custody');
427
+ expect(sensors).not.toHaveBeenCalled();
428
+ expect(dispatches).toBe(0);
429
+ expect((0, store_1.readJournal)(repo, 'main').state.cycle.status).toBe('BLOCKED');
430
+ expect((0, store_1.readJournal)(repo, 'main').state.cycle.blockedReason).toContain('ADMISSION_REGISTRY_CLI_INCOMPATIBLE');
431
+ expect((0, store_1.readJournal)(repo, 'main').state.cycle.blockedReason).toContain('999.0.0');
432
+ }
433
+ finally {
434
+ inventory.mockRestore();
435
+ preferences.mockRestore();
436
+ currentness.mockRestore();
437
+ sensors.mockRestore();
438
+ if (oldHome === undefined)
439
+ delete process.env.HOME;
440
+ else
441
+ process.env.HOME = oldHome;
442
+ }
443
+ });
444
+ test.each([
445
+ ['cycle BLOCKED', (s) => { s.cycle.status = 'BLOCKED'; }],
446
+ ['request problem durable', (s) => { s.requestProblems.push({ file: 'x.request.json', kind: 'rejected', detail: 'x', at: new Date().toISOString() }); }],
447
+ ['verificador requerido ausente tras tasks completadas', (s) => {
448
+ s.tasks.push({ id: 'T1', title: 'terminada', status: 'done', attempts: 1, verificationPlan: [], reviewObligations: [] });
449
+ s.requiredVerifiers = ['test'];
450
+ }],
451
+ ])('recovery fail-closed: %s entra en custodia sin controller ni dispatch', async (_name, mutate) => {
452
+ initUnattendedFixture(repo);
453
+ const state = (0, store_1.readJournal)(repo, 'main').state;
454
+ mutate(state);
455
+ (0, store_1.writeJournal)(repo, 'main', state);
456
+ let spawns = 0;
457
+ const outcome = await new supervisor_1.Supervisor(repo, 'main', supervisor_1.DEFAULT_SUPERVISOR_CONFIG, () => { spawns++; }, undefined, admitted).tick();
458
+ expect(outcome).toBe('custody');
459
+ expect(spawns).toBe(0);
460
+ expect((0, store_1.readJournal)(repo, 'main').state.cycle.status).toBe('BLOCKED');
461
+ });
462
+ test('un journal schema-1 legacy no despacha controller ni jobs aunque contenga trabajo pendiente', async () => {
463
+ (0, store_1.initJournal)(repo, 'main');
464
+ const legacy = (0, store_1.readJournal)(repo, 'main').state;
465
+ legacy.jobs.pending = {
466
+ id: 'pending', fingerprint: '', commandDigest: '', argv: ['node', '-e', 'process.exit(0)'], cwd: '.', paths: [], expandedPaths: [],
467
+ executionState: 'received', observationState: 'progressing', phaseTimestamps: {},
468
+ };
469
+ (0, store_1.writeJournal)(repo, 'main', legacy);
470
+ let spawns = 0;
471
+ const outcome = await new supervisor_1.Supervisor(repo, 'main', supervisor_1.DEFAULT_SUPERVISOR_CONFIG, () => { spawns++; }).tick();
472
+ expect(outcome).toBe('custody');
473
+ expect(spawns).toBe(0);
474
+ expect((0, store_1.readJournal)(repo, 'main').state.generations).toEqual([]);
475
+ expect((0, store_1.readJournal)(repo, 'main').state.jobs.pending.executionState).toBe('received');
476
+ });
290
477
  test('fallo de launch queda durable y entra en backoff sin tumbar el supervisor (R4.3)', async () => {
291
- (0, init_1.initWatch)(repo, 'main');
478
+ initUnattendedFixture(repo);
292
479
  (0, generations_1.beginGeneration)(repo, 'main');
293
480
  let calls = 0;
294
481
  const failing = () => { calls++; throw new Error('provider unavailable'); };
295
- const sup = new supervisor_1.Supervisor(repo, 'main', { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, tickMs: 10 }, failing);
482
+ const sup = new supervisor_1.Supervisor(repo, 'main', { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, tickMs: 10 }, failing, undefined, admitted);
296
483
  await expect(sup.tick()).resolves.toBe('continue');
297
484
  expect(calls).toBe(1);
298
485
  const intent = (0, generations_1.activeGeneration)((0, store_1.readJournal)(repo, 'main').state);
@@ -302,7 +489,7 @@ describe('supervisor loop', () => {
302
489
  expect(calls).toBe(1); // primer backoff es 60 s: no hace hot-loop
303
490
  });
304
491
  test('custodia: doble senial + indeterminate => tick custody, lock retenido, proceso intacto (R4.2b/R4.5)', async () => {
305
- (0, store_1.initJournal)(repo, 'main');
492
+ initUnattendedFixture(repo);
306
493
  (0, generations_1.beginGeneration)(repo, 'main');
307
494
  const { child, ref } = (0, process_1.spawnStructured)(['node', '-e', 'setTimeout(()=>{}, 20000)'], process.cwd(), 'nCtl');
308
495
  let s = (0, store_1.readJournal)(repo, 'main').state;
@@ -312,7 +499,7 @@ describe('supervisor loop', () => {
312
499
  fs_1.default.mkdirSync(path_1.default.dirname((0, paths_1.supervisorLockPath)(repo)), { recursive: true });
313
500
  fs_1.default.writeFileSync((0, paths_1.supervisorLockPath)(repo), 'lock-del-loop'); // el loop lo tendria: NO debe borrarse
314
501
  const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, provider: 'codex', heartbeatTimeoutMs: 1, activityWindowMs: 50, tickMs: 20 };
315
- const sup = new supervisor_1.Supervisor(repo, 'main', cfg, fakeSpawner);
502
+ const sup = new supervisor_1.Supervisor(repo, 'main', cfg, fakeSpawner, undefined, admitted);
316
503
  await sup.tick(); // primer tick: arranca el tracking de actividad
317
504
  await new Promise((r) => setTimeout(r, 150)); // actividad congelada > ventana
318
505
  const out = await sup.tick();
@@ -324,9 +511,12 @@ describe('supervisor loop', () => {
324
511
  child.kill('SIGKILL');
325
512
  });
326
513
  itPosix('runSupervisorLoop: bootstrap gen-1 con stub codex, COMPLETE => libera lock y termina su generacion (R4.1/R4.5/R2.4)', async () => {
327
- (0, init_1.initWatch)(repo, 'main');
514
+ initUnattendedFixture(repo);
515
+ jest.spyOn(fingerprint, 'reconcileUnattendedRecovery').mockReturnValue({
516
+ state: 'ready', nextAction: 'select-work', activeJobIds: [], diagnostics: [],
517
+ });
328
518
  const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, provider: 'codex', tickMs: 50, termGraceMs: 300, killGraceMs: 300 };
329
- const loop = (0, supervisor_1.runSupervisorLoop)(repo, 'main', cfg, fakeSpawner);
519
+ const loop = (0, supervisor_1.runSupervisorLoop)(repo, 'main', cfg, fakeSpawner, undefined, admitted);
330
520
  await until(() => {
331
521
  const r = (0, store_1.readJournal)(repo, 'main');
332
522
  return r.state !== null && (0, generations_1.activeGeneration)(r.state) !== undefined && fs_1.default.existsSync((0, paths_1.supervisorLockPath)(repo));
@@ -346,13 +536,17 @@ describe('supervisor loop', () => {
346
536
  // generacion propia terminada: cero procesos codex huerfanos (R2.4)
347
537
  const { refIsAlive } = require('../../../src/core/journal/process');
348
538
  expect(gen.processRef === undefined || !refIsAlive(gen.processRef)).toBe(true);
539
+ expect(gen.state).toBe('terminated');
349
540
  });
350
541
  itPosix('reinicio tras crash entre beginGeneration y spawn recupera la misma generacion sin quedar wedged', async () => {
351
- (0, init_1.initWatch)(repo, 'main');
542
+ initUnattendedFixture(repo);
543
+ jest.spyOn(fingerprint, 'reconcileUnattendedRecovery').mockReturnValue({
544
+ state: 'ready', nextAction: 'select-work', activeJobIds: [], diagnostics: [],
545
+ });
352
546
  const begun = (0, generations_1.beginGeneration)(repo, 'main'); // crash simulado: intent durable, sin ProcessRef
353
547
  const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, provider: 'codex', tickMs: 25, reconcileGraceMs: 300,
354
548
  termGraceMs: 300, killGraceMs: 300 };
355
- const loop = (0, supervisor_1.runSupervisorLoop)(repo, 'main', cfg, fakeSpawner);
549
+ const loop = (0, supervisor_1.runSupervisorLoop)(repo, 'main', cfg, fakeSpawner, undefined, admitted);
356
550
  let recovered = false;
357
551
  try {
358
552
  await until(() => {
@@ -369,6 +563,10 @@ describe('supervisor loop', () => {
369
563
  }
370
564
  await loop;
371
565
  expect(recovered).toBe(true);
372
- expect((0, store_1.readJournal)(repo, 'main').state.generations).toHaveLength(1);
566
+ const final = (0, store_1.readJournal)(repo, 'main').state;
567
+ expect(final.generations).toHaveLength(1);
568
+ expect(final.generations[0].state).toBe('terminated');
569
+ const { refIsAlive } = require('../../../src/core/journal/process');
570
+ expect(final.generations[0].processRef === undefined || !refIsAlive(final.generations[0].processRef)).toBe(true);
373
571
  });
374
572
  });
@@ -26,6 +26,7 @@ const supervisor_1 = require("../../../src/commands/watch/supervisor");
26
26
  const tracks_1 = require("../../../src/commands/watch/tracks");
27
27
  const exec_wrapper_1 = require("../../../src/commands/job/exec-wrapper");
28
28
  const init_1 = require("../../../src/commands/watch/init");
29
+ const validate_1 = require("../../../src/core/plan/validate");
29
30
  const requests_1 = require("../../../src/core/journal/requests");
30
31
  const apply_1 = require("../../../src/commands/watch/apply");
31
32
  const generations_1 = require("../../../src/commands/watch/generations");
@@ -35,6 +36,22 @@ const fingerprint_1 = require("../../../src/core/journal/fingerprint");
35
36
  const paths_1 = require("../../../src/core/journal/paths");
36
37
  const types_1 = require("../../../src/core/journal/types");
37
38
  const git_fixture_1 = require("../../helpers/git-fixture");
39
+ const admitted = async () => ({
40
+ state: 'admitted', planState: 'valid', executionMode: 'desatendido',
41
+ journal: 'current', currentness: 'current', sensors: 'pass', diagnostics: [],
42
+ });
43
+ function initCompactWatch(repoRoot, branch) {
44
+ const planPath = path_1.default.join(repoRoot, 'plans', 'fixture.md');
45
+ fs_1.default.mkdirSync(path_1.default.dirname(planPath), { recursive: true });
46
+ fs_1.default.writeFileSync(planPath, fs_1.default.readFileSync(path_1.default.join(__dirname, '../../core/plan/fixtures/compact-slices-v1/valid.md'), 'utf8'));
47
+ fs_1.default.writeFileSync(path_1.default.join(repoRoot, 'source.md'), '## Canonical source\nfixture source\n');
48
+ git(repoRoot, 'add', 'plans/fixture.md', 'source.md');
49
+ git(repoRoot, 'commit', '-qm', 'compact plan fixture');
50
+ const report = (0, validate_1.validatePlanFile)('plans/fixture.md', repoRoot);
51
+ if (report.state !== 'valid')
52
+ throw new Error(`compact fixture must validate: ${JSON.stringify(report)}`);
53
+ (0, init_1.initWatch)(repoRoot, branch, { path: 'plans/fixture.md', report });
54
+ }
38
55
  function git(cwd, ...args) {
39
56
  (0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', ...args], { cwd, stdio: 'pipe' });
40
57
  }
@@ -242,7 +259,7 @@ function finalizerHarness(trackIds) {
242
259
  git(repo, 'commit', '-qm', 'seed');
243
260
  fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
244
261
  fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), '{}');
245
- (0, init_1.initWatch)(repo, 'main'); // gitignora .awm, detecta requiredVerifiers (test+sensors, R3.6)
262
+ initCompactWatch(repo, 'main'); // gitignora .awm, detecta requiredVerifiers (test+sensors, R3.6)
246
263
  git(repo, 'add', '.gitignore');
247
264
  git(repo, 'commit', '-qm', 'gitignore .awm');
248
265
  const baseSha = (0, child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim();
@@ -282,7 +299,7 @@ function finalizerHarness(trackIds) {
282
299
  payload: { entity: 'track-integration', argv: INTEGRATION_ARGV, paths: INTEGRATION_PATHS, planDigest: 'plan-1' },
283
300
  });
284
301
  const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, tickMs: 10, provider: 'codex' };
285
- let sup = new supervisor_1.Supervisor(repo, 'main', cfg, spawner);
302
+ let sup = new supervisor_1.Supervisor(repo, 'main', cfg, spawner, undefined, admitted);
286
303
  const realFingerprintNow = (argv, paths, cwd) => {
287
304
  try {
288
305
  return (0, fingerprint_1.computeFingerprint)(repo, argv, paths, cwd).fingerprint;
@@ -360,7 +377,7 @@ function finalizerHarness(trackIds) {
360
377
  .map((j) => ({ argv: j.argv, paths: j.paths, cwd: j.cwd, satisfies: [...(j.satisfies ?? [])].sort() }));
361
378
  },
362
379
  integrationWrapperCalls() { return integrationCalls; },
363
- crashAndRestart() { sup = new supervisor_1.Supervisor(repo, 'main', cfg, spawner); },
380
+ crashAndRestart() { sup = new supervisor_1.Supervisor(repo, 'main', cfg, spawner, undefined, admitted); },
364
381
  interlock() {
365
382
  const s = (0, store_1.readJournal)(repo, 'main').state;
366
383
  return (0, gate_1.computeGate)(s, false, realFingerprintNow);