agentic-workflow-manager 9.12.2 → 9.12.3

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.
@@ -251,6 +251,67 @@ function registerJobCommand(program) {
251
251
  assertAuthenticatedCwd(repo, branch);
252
252
  process.stdout.write(JSON.stringify((0, query_1.queryPs)(repo, branch), null, 2) + '\n');
253
253
  });
254
+ job.command('ack')
255
+ .description('consulta read-only el acuse durable de una request por ID exacto')
256
+ .argument('<requestId>')
257
+ .action((requestId) => {
258
+ if (!/^req-[A-Za-z0-9-]{1,124}$/.test(requestId))
259
+ throw new Error('requestId invalido');
260
+ const repo = process.cwd();
261
+ const branch = branchOf(repo);
262
+ assertAuthenticatedCwd(repo, branch);
263
+ const read = (0, store_1.readJournal)(repo, branch);
264
+ if (read.corrupt || !read.state) {
265
+ process.stdout.write(JSON.stringify({ requestId, state: 'unverifiable', journal: read.corrupt ? 'corrupt' : 'absent' }) + '\n');
266
+ process.exitCode = 1;
267
+ return;
268
+ }
269
+ (0, lock_1.verifyBranchInvariant)(repo, read.state.branch);
270
+ const ack = (0, requests_1.ackFor)(read.state, requestId);
271
+ if (ack) {
272
+ process.stdout.write(JSON.stringify({ requestId, state: ack.outcome === 'applied' ? 'applied' : 'rejected',
273
+ outcome: ack.outcome, ...(ack.resultRef ? { resultRef: ack.resultRef } : {}) }) + '\n');
274
+ if (ack.outcome !== 'applied')
275
+ process.exitCode = 1;
276
+ return;
277
+ }
278
+ const problem = read.state.requestProblems.find(item => item.requestId === requestId
279
+ || (item.kind === 'corrupt' && path_1.default.basename(item.file) === `${requestId}.json`));
280
+ if (problem) {
281
+ process.stdout.write(JSON.stringify({ requestId, state: problem.kind === 'corrupt' ? 'unverifiable' : 'rejected', reasonCode: problem.kind }) + '\n');
282
+ process.exitCode = 1;
283
+ return;
284
+ }
285
+ let pending;
286
+ try {
287
+ pending = (0, requests_1.listPendingRequests)(repo, branch).find(item => item.requestId === requestId
288
+ || (item.corrupt && path_1.default.basename(item.file) === `${requestId}.json`));
289
+ }
290
+ catch {
291
+ process.stdout.write(JSON.stringify({ requestId, state: 'unverifiable', reasonCode: 'request-directory' }) + '\n');
292
+ process.exitCode = 1;
293
+ return;
294
+ }
295
+ // The supervisor may have consumed the request between the first
296
+ // journal snapshot and the queue read. Recheck before saying unknown.
297
+ const latest = (0, store_1.readJournal)(repo, branch);
298
+ if (latest.corrupt || !latest.state) {
299
+ process.stdout.write(JSON.stringify({ requestId, state: 'unverifiable', journal: latest.corrupt ? 'corrupt' : 'absent' }) + '\n');
300
+ process.exitCode = 1;
301
+ return;
302
+ }
303
+ const latestAck = (0, requests_1.ackFor)(latest.state, requestId);
304
+ if (latestAck) {
305
+ process.stdout.write(JSON.stringify({ requestId, state: latestAck.outcome === 'applied' ? 'applied' : 'rejected',
306
+ outcome: latestAck.outcome, ...(latestAck.resultRef ? { resultRef: latestAck.resultRef } : {}) }) + '\n');
307
+ if (latestAck.outcome !== 'applied')
308
+ process.exitCode = 1;
309
+ return;
310
+ }
311
+ process.stdout.write(JSON.stringify({ requestId, state: pending?.corrupt ? 'unverifiable' : pending ? 'pending' : 'unknown' }) + '\n');
312
+ if (!pending || pending.corrupt)
313
+ process.exitCode = 1;
314
+ });
254
315
  job.command('list').action(() => {
255
316
  const repo = process.cwd();
256
317
  const branch = branchOf(repo);
@@ -155,6 +155,14 @@ function applyRequestToState(s, env, digest, repoRoot, eventScope) {
155
155
  const capabilities = (0, capabilities_1.readCapabilities)((0, capabilities_1.validateRuntimeKey)(envelope.runtime), checkedAt);
156
156
  return { checkedAt, policy: policy.state === 'approved' ? policy.policy : undefined, capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined };
157
157
  } }, p.at ?? now());
158
+ const routingAttempt = reserved.state.routingAttempts?.find(item => item.id === reserved.attemptId);
159
+ if (!routingAttempt)
160
+ throw new Error('routing reservation produced no attempt');
161
+ const priorReservationAck = routingAttempt.reservationRequestId === undefined ? undefined
162
+ : reserved.state.appliedRequests[routingAttempt.reservationRequestId];
163
+ if (!priorReservationAck || priorReservationAck.outcome !== 'applied' || priorReservationAck.resultRef !== routingAttempt.id) {
164
+ routingAttempt.reservationRequestId = env.requestId;
165
+ }
158
166
  Object.assign(s, reserved.state);
159
167
  (0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: reserved.attemptId });
160
168
  return;
@@ -166,6 +174,15 @@ function applyRequestToState(s, env, digest, repoRoot, eventScope) {
166
174
  const attempt = s.routingAttempts?.find(item => item.id === p.attemptId);
167
175
  if (!attempt)
168
176
  throw new Error('routing-observe attempt is unknown');
177
+ if (s.planBinding?.schema === 'compact-slices/v2' && attempt.envelope.role === 'implementer') {
178
+ if (attempt.nativeEvidenceRequired !== true)
179
+ throw new Error('routing-observe v2 requires a native event receipt');
180
+ const dispatch = s.dispatches.find(item => item.routingAttemptId === attempt.id);
181
+ const ack = dispatch?.dispatchRequestId ? s.appliedRequests[dispatch.dispatchRequestId] : undefined;
182
+ if (!dispatch || ack?.outcome !== 'applied' || ack.requestId !== dispatch.dispatchRequestId
183
+ || ack.resultRef !== dispatch.id)
184
+ throw new Error('routing-observe requires applied dispatch ack');
185
+ }
169
186
  let key = null;
170
187
  if (attempt.nativeEvidenceRequired) {
171
188
  try {
@@ -454,8 +471,40 @@ function applyRequestToState(s, env, digest, repoRoot, eventScope) {
454
471
  const taskId = p.taskId;
455
472
  if (!s.tasks.some((task) => task.id === taskId))
456
473
  throw new Error('register --entity dispatch: taskId desconocido');
457
- if (!s.dispatches.some((d) => d.id === dispatchId)) {
458
- s.dispatches.push({ id: dispatchId, taskId, at: now() });
474
+ const v2 = s.planBinding?.schema === 'compact-slices/v2';
475
+ let routingAttemptId;
476
+ let reservedForNewDispatch = true;
477
+ if (v2) {
478
+ if (typeof p.routingAttemptId !== 'string' || p.routingAttemptId.length === 0)
479
+ throw new Error('register --entity dispatch v2 requiere routingAttemptId');
480
+ const attempt = s.routingAttempts?.find(candidate => candidate.id === p.routingAttemptId);
481
+ if (!attempt || attempt.envelope.role !== 'implementer'
482
+ || attempt.envelope.sliceId !== taskId || attempt.envelope.planDigest !== s.planBinding.digest
483
+ || attempt.envelope.executionDigest !== s.planBinding.executionDigest
484
+ || !attempt.reservationRequestId
485
+ || s.appliedRequests[attempt.reservationRequestId]?.outcome !== 'applied'
486
+ || s.appliedRequests[attempt.reservationRequestId]?.requestId !== attempt.reservationRequestId
487
+ || s.appliedRequests[attempt.reservationRequestId]?.resultRef !== attempt.id) {
488
+ throw new Error('register --entity dispatch: routing reservation for this slice and plan lacks applied ack');
489
+ }
490
+ routingAttemptId = attempt.id;
491
+ if (attempt.nativeEvidenceRequired !== true)
492
+ throw new Error('register --entity dispatch v2 requires native event receipt');
493
+ reservedForNewDispatch = attempt.state === 'reserved';
494
+ }
495
+ else if (p.routingAttemptId !== undefined)
496
+ throw new Error('register --entity dispatch: routingAttemptId solo para compact v2');
497
+ const existing = s.dispatches.find(d => d.id === dispatchId);
498
+ if (existing && (existing.taskId !== taskId || existing.routingAttemptId !== routingAttemptId)) {
499
+ throw new Error('register --entity dispatch: dispatchId conflictivo');
500
+ }
501
+ if (!existing) {
502
+ if (routingAttemptId && s.dispatches.some(d => d.routingAttemptId === routingAttemptId)) {
503
+ throw new Error('register --entity dispatch: routing reservation ya tiene un dispatch');
504
+ }
505
+ if (!reservedForNewDispatch)
506
+ throw new Error('register --entity dispatch: routing attempt ya no esta reservado');
507
+ s.dispatches.push({ id: dispatchId, taskId, at: now(), ...(routingAttemptId ? { routingAttemptId, dispatchRequestId: env.requestId } : {}) });
459
508
  const task = s.tasks.find((t) => t.id === taskId);
460
509
  if (task !== undefined)
461
510
  task.attempts += 1;
@@ -95,7 +95,7 @@ function promptForGeneration(gen) {
95
95
  'resume-next-action': 'Retoma el next_action vigente del journal.',
96
96
  'reconcile-active-jobs': 'Reconcilia los jobs activos existentes del journal sin duplicarlos.',
97
97
  };
98
- return `${instructions[gen.resumeAction.kind]}\nGeneracion activa: ${gen.token}. Incluye --generation ${gen.token} en cada comando awm job.`;
98
+ return `${instructions[gen.resumeAction.kind]}\nGeneracion activa: ${gen.token}. Incluye --generation ${gen.token} solo en awm job request, register, verdict, controller-heartbeat, routing-reserve y routing-observe. Ejecuta awm job reconcile sin --generation; awm job ack <requestId> tambien es de solo lectura y no acepta ese flag. En compact v2: espera el ack applied de routing-reserve, toma su resultRef como routingAttemptId, registra dispatch con ese routingAttemptId y espera su ack applied antes de lanzar al implementador nativo. Ningun implementador escribe antes de ese ack. Luego emite routing-observe y espera su ack; un recibo de emision no es un ack aplicado.`;
99
99
  }
100
100
  function recoveryAction(action) {
101
101
  if (!(0, types_1.isControllerRecoveryAction)(action))
@@ -143,8 +143,18 @@ function registerWatchCommand(program) {
143
143
  ...(opts.controllerAutonomy === undefined ? {} : { controllerAutonomy: opts.controllerAutonomy }),
144
144
  };
145
145
  process.stdout.write(`awm watch: supervisor activo (${cfg.provider}) — Ctrl-C para terminar\n`);
146
- await (0, supervisor_1.runSupervisorLoop)(repo, branch, cfg);
147
- process.stdout.write('gate verde: ciclo COMPLETE — drenado, lock liberado, apagando\n');
146
+ const outcome = await (0, supervisor_1.runSupervisorLoop)(repo, branch, cfg);
147
+ const final = (0, archive_unused_1.watchJournalStatus)(repo, branch);
148
+ if (outcome === 'complete' && final.state === 'present' && final.cycleState === 'COMPLETE') {
149
+ process.stdout.write('gate verde: ciclo COMPLETE — drenado, lock liberado, apagando\n');
150
+ }
151
+ else {
152
+ process.stdout.write(`awm watch: ${outcome === 'stopped' ? 'detenido' : 'track congelado'}; ciclo ${final.cycleState ?? final.state} — sin certificacion de cierre\n`);
153
+ if (outcome === 'stopped')
154
+ process.exitCode = 130;
155
+ else if (outcome === 'complete' || final.state !== 'present')
156
+ process.exitCode = 1;
157
+ }
148
158
  });
149
159
  watch.command('recover-request')
150
160
  .description('reconcilia una task rechazada con una request correctiva aplicada bajo lock exclusivo')
@@ -341,12 +341,61 @@ class Supervisor {
341
341
  if (before0.corrupt || !before0.state)
342
342
  throw new Error('journal no verificable tras vincular identidad de admisión');
343
343
  }
344
- if (before0.state.admissionRetry && Date.now() < Date.parse(before0.state.admissionRetry.nextRetryAt))
344
+ // Full sensor admission belongs to the launch boundary. Once that
345
+ // generation has a durable controller launch intent, RED edits are
346
+ // normal work, not a new pre-dispatch baseline. Preserve binding
347
+ // checks while it runs; the next generation gets a fresh admission.
348
+ const binding = before0.state.planBinding;
349
+ if (!binding)
350
+ throw new Error('binding compacto ausente tras lectura del journal');
351
+ const launched = (0, generations_1.activeGeneration)(before0.state);
352
+ const ownedLaunch = launched !== undefined && (launched.processRef !== undefined || launched.wrapperRef !== undefined
353
+ || (0, generations_1.controllerGenerationHasUnresolvedClaim)(this.repoRoot, this.branch, launched));
354
+ const generationAdmitted = launched?.controllerJobId !== undefined && launched.launchArgvDigest !== undefined && ownedLaunch;
355
+ if (generationAdmitted) {
356
+ const plan = (0, validate_1.validatePlanFile)(binding.path, this.repoRoot);
357
+ if (plan.state !== 'valid' || plan.planDigest !== binding.digest || plan.schema !== binding.schema
358
+ || (binding.executionDigest !== undefined && plan.executionDigest !== binding.executionDigest)) {
359
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, 'binding del plan alterado durante generacion admitida');
360
+ return 'custody';
361
+ }
362
+ if (binding.schema === 'compact-slices/v2') {
363
+ const asserted = assertedContext.runtime;
364
+ if (!asserted) {
365
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, 'runtime de routing ausente durante generacion v2');
366
+ return 'custody';
367
+ }
368
+ const expected = (0, capabilities_1.validateRuntimeKey)({ target: assertedContext.provider, ...asserted });
369
+ let local;
370
+ try {
371
+ local = await (0, local_event_scope_1.queryLocalEventScope)(this.repoRoot, expected.target, expected.kind);
372
+ }
373
+ catch (error) {
374
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, `runtime local no verificable: ${error.message}`);
375
+ return 'custody';
376
+ }
377
+ if (local.state !== 'current' || JSON.stringify(local.scope.runtime) !== JSON.stringify(expected)) {
378
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, 'runtime local cambió durante generacion admitida');
379
+ return 'custody';
380
+ }
381
+ const stored = (0, event_store_1.readStoredEventReceipt)(expected);
382
+ if (stored.state === 'invalid' || (stored.state === 'present' &&
383
+ (stored.receipt.binaryDigest !== local.scope.binaryDigest || stored.receipt.configDigest !== local.scope.configDigest))) {
384
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, 'recibo nativo cambió durante generacion admitida');
385
+ return 'custody';
386
+ }
387
+ if (stored.state === 'absent' && (0, capabilities_1.readCapabilities)(expected, new Date()).state !== 'current') {
388
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, 'capacidades de routing no verificables durante generacion admitida');
389
+ return 'custody';
390
+ }
391
+ }
392
+ }
393
+ if (!generationAdmitted && before0.state.admissionRetry && Date.now() < Date.parse(before0.state.admissionRetry.nextRetryAt))
345
394
  return 'continue';
346
395
  // A controller can create jobs and runnerTick can start them. Both are
347
396
  // downstream of the same full compact unattended admission, so it must
348
397
  // complete before any reconciliation path that could dispatch either.
349
- if (before0.state.schema === 2 && before0.state.planBinding) {
398
+ if (!generationAdmitted) {
350
399
  let admission;
351
400
  try {
352
401
  admission = await this.dispatchAdmission();
@@ -359,7 +408,6 @@ class Supervisor {
359
408
  || admission.currentness !== 'current' || admission.sensors !== 'pass' || admission.journal !== 'current') {
360
409
  const kind = transientAdmission(admission);
361
410
  if (kind) {
362
- const binding = before0.state.planBinding;
363
411
  const plan = (0, validate_1.validatePlanFile)(binding.path, this.repoRoot);
364
412
  if (plan.state !== 'valid' || plan.planDigest !== binding.digest || plan.schema !== binding.schema
365
413
  || (binding.executionDigest !== undefined && plan.executionDigest !== binding.executionDigest)
@@ -565,6 +613,10 @@ class Supervisor {
565
613
  const finalizing = afterRequests.state?.cohortPhase === 'FINAL_INTEGRATION' || afterRequests.state?.cohortPhase === 'FINAL_INTERLOCK';
566
614
  if (!finalizing && afterRequests.state !== null && (0, generations_1.activeGeneration)(afterRequests.state) === undefined
567
615
  && afterRequests.state.generations.length > 0 && afterRequests.state.cycle.status === 'IN_PROGRESS') {
616
+ // The current tick used its launched generation's admission. A
617
+ // replacement must wait until the next tick's full admission.
618
+ if (generationAdmitted)
619
+ return 'continue';
568
620
  (0, generations_1.beginGeneration)(this.repoRoot, this.branch);
569
621
  if (this.ensureController(resumeAction) === 'custody')
570
622
  return 'custody';
@@ -577,12 +629,15 @@ class Supervisor {
577
629
  const freezeCheck = (0, store_1.readJournal)(this.repoRoot, this.branch);
578
630
  const freezing = !freezeCheck.corrupt && freezeCheck.state !== null
579
631
  && freezeCheck.state.freezeRequested === true && freezeCheck.state.frozen === undefined;
632
+ // Reconcile controller ownership and, if needed, freshly admit its
633
+ // replacement before runnerTick can launch received jobs.
634
+ const supervision = await this.superviseController();
635
+ if (supervision === 'custody')
636
+ return 'custody';
580
637
  (0, runner_1.runnerTick)(this.repoRoot, this.branch, this.spawner, {
581
- reconcileGraceMs: this.cfg.reconcileGraceMs, stallObservationMs: this.cfg.jobStallObservationMs, dispatch: !freezing,
638
+ reconcileGraceMs: this.cfg.reconcileGraceMs, stallObservationMs: this.cfg.jobStallObservationMs,
639
+ dispatch: !freezing && supervision === 'ok',
582
640
  });
583
- const custody = await this.superviseController();
584
- if (custody)
585
- return 'custody';
586
641
  if (freezing)
587
642
  return this.attemptFreeze();
588
643
  const r = (0, store_1.readJournal)(this.repoRoot, this.branch);
@@ -695,15 +750,23 @@ class Supervisor {
695
750
  (0, store_1.appendEvent)(this.repoRoot, this.branch, { kind: 'track-frozen', headSha: s.frozen.headSha });
696
751
  return 'frozen'; // paso 6 ("libera el lock y sale"): runSupervisorLoop trata 'frozen' igual que 'complete'
697
752
  }
698
- /** true => custodia (el caller NO libera lock ni sale). */
753
+ /** Deferred ownership drains existing jobs but never starts new work. */
699
754
  async superviseController() {
700
755
  const r = (0, store_1.readJournal)(this.repoRoot, this.branch);
701
756
  if (r.corrupt || r.state === null)
702
757
  throw new Error('journal corrupto (R1.6)');
703
758
  const s = r.state;
704
759
  const gen = (0, generations_1.activeGeneration)(s);
705
- if (gen?.processRef === undefined)
706
- return false; // sin controlador propio: nada que supervisar
760
+ // Track worktrees may be driven by the plan controller without a
761
+ // local generation. The canonical final-integration job also runs
762
+ // after the plan controller is deliberately paused. An existing but
763
+ // unadopted launch is different: claim recovery/backoff must never
764
+ // dispatch received jobs under that generation.
765
+ if (gen === undefined)
766
+ return s.trackContext || s.cohortPhase === 'FINAL_INTEGRATION'
767
+ || s.cohortPhase === 'FINAL_INTERLOCK' ? 'ok' : 'deferred';
768
+ if (gen.processRef === undefined)
769
+ return 'deferred';
707
770
  if (this.lastGenerationToken !== gen.token) {
708
771
  this.lastGenerationToken = gen.token;
709
772
  this.lastActivity = null;
@@ -716,10 +779,13 @@ class Supervisor {
716
779
  this.lastActivity = { key, changedAt: Date.now() };
717
780
  }
718
781
  const activityFrozenMs = Date.now() - this.lastActivity.changedAt;
719
- const decision = (0, generations_1.decideStall)({ heartbeatAgeMs, activityFrozenMs, safeToReplace: adapter.safeToReplace(gen.processRef) }, { heartbeatTimeoutMs: this.cfg.heartbeatTimeoutMs, activityWindowMs: this.cfg.activityWindowMs });
782
+ const safeToReplace = adapter.safeToReplace(gen.processRef);
783
+ // A dead process with proven identity is not healthy merely because
784
+ // its last heartbeat was recent. Resolve it before any new job starts.
785
+ const decision = snap === null && safeToReplace === 'safe' ? 'resolve-generation' : (0, generations_1.decideStall)({ heartbeatAgeMs, activityFrozenMs, safeToReplace }, { heartbeatTimeoutMs: this.cfg.heartbeatTimeoutMs, activityWindowMs: this.cfg.activityWindowMs });
720
786
  if (decision === 'healthy') {
721
787
  this.backoff.reset();
722
- return false;
788
+ return 'ok';
723
789
  }
724
790
  if (decision === 'suspected-stall-observe') {
725
791
  if (gen.state !== 'controller-suspected-stall') {
@@ -727,39 +793,53 @@ class Supervisor {
727
793
  (0, store_1.writeJournal)(this.repoRoot, this.branch, s);
728
794
  (0, store_1.appendEvent)(this.repoRoot, this.branch, { kind: 'controller-suspected-stall', n: gen.n });
729
795
  }
730
- return false;
796
+ return 'ok';
731
797
  }
732
798
  if (decision === 'custody-blocked') {
733
799
  (0, generations_1.enterCustody)(this.repoRoot, this.branch, 'doble senial de stall sin safeToReplace positivo del adapter (R4.2b)');
734
- return true;
800
+ return 'custody';
735
801
  }
736
802
  // resolve-generation
737
803
  const resolved = await (0, generations_1.resolveGeneration)(this.repoRoot, this.branch, adapter, { termGraceMs: this.cfg.termGraceMs, killGraceMs: this.cfg.killGraceMs });
738
804
  if (resolved === 'custody-blocked')
739
- return true;
805
+ return 'custody';
740
806
  if (this.backoff.exhausted()) {
741
807
  (0, generations_1.enterCustody)(this.repoRoot, this.branch, 'tope de relanzamientos por hora alcanzado (R4.3)');
742
- return true;
808
+ return 'custody';
743
809
  }
744
810
  if (Date.now() < this.relaunchNotBefore)
745
- return false; // esperando backoff, auditando
811
+ return 'deferred'; // esperando backoff, auditando
812
+ // This is a different controller generation, even though the tick
813
+ // began under the old one's admission. Do not reuse that admission
814
+ // after RED edits, runtime drift, or a currentness change.
815
+ let replacementAdmission;
816
+ try {
817
+ replacementAdmission = await this.dispatchAdmission();
818
+ }
819
+ catch (error) {
820
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, `admisión de reemplazo no verificable: ${error.message}`);
821
+ return 'custody';
822
+ }
823
+ if (replacementAdmission.state !== 'admitted' || replacementAdmission.executionMode !== 'desatendido'
824
+ || replacementAdmission.currentness !== 'current' || replacementAdmission.sensors !== 'pass'
825
+ || replacementAdmission.journal !== 'current') {
826
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, `admisión de reemplazo bloqueada antes de dispatch: ${replacementAdmission.diagnostics.map(d => `${d.code}: ${d.message}`).join('; ')}`);
827
+ return 'custody';
828
+ }
746
829
  (0, generations_1.beginGeneration)(this.repoRoot, this.branch);
747
830
  const nextAction = (0, store_1.readJournal)(this.repoRoot, this.branch).state.cycle.nextAction;
748
831
  const action = { schema: 'controller-recovery/v1', kind: nextAction !== undefined ? 'resume-next-action' : 'resume-cycle' };
749
832
  const launched = this.ensureController(action);
750
833
  if (launched === 'custody')
751
- return true;
834
+ return 'custody';
752
835
  if (launched === 'ok') {
753
836
  this.backoff.recordRelaunch();
754
837
  this.relaunchNotBefore = Date.now() + this.backoff.nextMs();
755
838
  }
756
- return false;
839
+ return launched === 'deferred' ? 'deferred' : 'ok';
757
840
  }
758
841
  }
759
842
  exports.Supervisor = Supervisor;
760
- /** Foreground, visible, terminable (R2.4): sin daemons. SIGINT/SIGTERM libera
761
- * el lock y sale; COMPLETE => auto-exit liberando lock y terminando la
762
- * generacion propia (cero huerfanos). */
763
843
  async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.defaultWrapperSpawner)(), trackRuntime, dispatchAdmission) {
764
844
  const r = (0, store_1.readJournal)(repoRoot, branch);
765
845
  if (r.corrupt || r.state === null) {
@@ -772,11 +852,12 @@ async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.d
772
852
  }
773
853
  (0, lock_1.verifyBranchInvariant)(repoRoot, r.state.branch);
774
854
  if (r.state.cycle.status === 'COMPLETE')
775
- return;
855
+ return 'complete';
776
856
  const handle = (0, lock_1.acquireLock)(repoRoot);
777
857
  let shutdownRequested = false;
778
858
  let wakeSleep = null;
779
859
  let safeToRelease = false;
860
+ let terminal = 'stopped';
780
861
  const onSignal = () => { shutdownRequested = true; wakeSleep?.(); };
781
862
  process.on('SIGINT', onSignal);
782
863
  process.on('SIGTERM', onSignal);
@@ -790,6 +871,7 @@ async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.d
790
871
  // drenar ownership y liberar el lock, el track ya cumplió su
791
872
  // freeze y no debe seguir despachando ni corriendo su loop.
792
873
  if (out === 'complete' || out === 'frozen') {
874
+ terminal = out;
793
875
  const finalState = (0, store_1.readJournal)(repoRoot, branch);
794
876
  if (!finalState.corrupt && finalState.state?.routingIncidents?.length)
795
877
  process.stderr.write(`AWM final routing summary ${JSON.stringify((0, journal_1.routingReport)(finalState.state))}\n`);
@@ -831,6 +913,7 @@ async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.d
831
913
  if (shutdownRequested)
832
914
  (0, store_1.writeJournal)(repoRoot, branch, sEnd);
833
915
  safeToRelease = true;
916
+ return terminal;
834
917
  }
835
918
  finally {
836
919
  process.removeListener('SIGINT', onSignal);
@@ -85,9 +85,15 @@ function isWellFormedEnvelope(x) {
85
85
  }
86
86
  function listPendingRequests(repoRoot, branch) {
87
87
  const dir = (0, paths_1.requestsDir)(repoRoot, branch);
88
+ const directory = fs_1.default.lstatSync(dir);
89
+ if (directory.isSymbolicLink() || !directory.isDirectory())
90
+ throw new Error('request directory is not a controlled directory');
88
91
  return fs_1.default.readdirSync(dir).filter((f) => f.endsWith('.json')).sort().map((f) => {
89
92
  const file = path_1.default.join(dir, f);
90
93
  try {
94
+ const stat = fs_1.default.lstatSync(file);
95
+ if (stat.isSymbolicLink() || !stat.isFile())
96
+ return { requestId: f, envelope: null, file, corrupt: true };
91
97
  const parsed = JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
92
98
  // Shape completa antes de cualquier uso downstream. JSON valido no
93
99
  // implica envelope valido: payload/generation/idempotency ausentes
@@ -99,6 +99,8 @@ function isWellFormedState(x) {
99
99
  return false;
100
100
  if (!Array.isArray(x.generations) || !Array.isArray(x.tasks))
101
101
  return false;
102
+ if (!x.tasks.every(isWellFormedTask))
103
+ return false;
102
104
  if (!Array.isArray(x.cycleVerificationPlan) || !Array.isArray(x.verdicts) || !Array.isArray(x.fixes))
103
105
  return false;
104
106
  if (!Array.isArray(x.requiredVerifiers) || !x.requiredVerifiers.every((kind) => VERIFICATION_KINDS.includes(kind))
@@ -108,8 +110,40 @@ function isWellFormedState(x) {
108
110
  return false;
109
111
  if (!isObj(x.appliedRequests) || !Object.values(x.appliedRequests).every(isWellFormedAppliedRequest))
110
112
  return false;
113
+ if (!Object.entries(x.appliedRequests).every(([requestId, ack]) => requestId === ack.requestId))
114
+ return false;
111
115
  if (x.routingAttempts !== undefined && (!Array.isArray(x.routingAttempts) || x.routingAttempts.length > 4096 || !x.routingAttempts.every(isWellFormedRoutingAttempt)))
112
116
  return false;
117
+ // A linked v2 dispatch is evidence of a native handoff. Reject a broken
118
+ // link or a second dispatch for one reservation on journal read as well as
119
+ // at request-application time. Historical unlinked records remain readable
120
+ // but cannot masquerade as this new acknowledged handoff.
121
+ if (x.planBinding?.schema === 'compact-slices/v2') {
122
+ const binding = x.planBinding;
123
+ const attempts = (x.routingAttempts ?? []);
124
+ const dispatches = x.dispatches;
125
+ const linked = new Set();
126
+ for (const dispatch of dispatches) {
127
+ if (!dispatch.routingAttemptId)
128
+ continue;
129
+ if (linked.has(dispatch.routingAttemptId))
130
+ return false;
131
+ linked.add(dispatch.routingAttemptId);
132
+ const attempt = attempts.find(candidate => candidate.id === dispatch.routingAttemptId);
133
+ if (!attempt || !(x.tasks.some(task => task.id === dispatch.taskId))
134
+ || attempt.envelope.role !== 'implementer' || attempt.envelope.sliceId !== dispatch.taskId
135
+ || attempt.envelope.planDigest !== binding.digest || attempt.envelope.executionDigest !== binding.executionDigest
136
+ || !attempt.reservationRequestId || !dispatch.dispatchRequestId)
137
+ return false;
138
+ const acknowledgements = x.appliedRequests;
139
+ const reservationAck = acknowledgements[attempt.reservationRequestId];
140
+ const dispatchAck = acknowledgements[dispatch.dispatchRequestId];
141
+ if (reservationAck?.outcome !== 'applied' || reservationAck.requestId !== attempt.reservationRequestId
142
+ || reservationAck.resultRef !== attempt.id || dispatchAck?.outcome !== 'applied'
143
+ || dispatchAck.requestId !== dispatch.dispatchRequestId || dispatchAck.resultRef !== dispatch.id)
144
+ return false;
145
+ }
146
+ }
113
147
  if (x.routingIncidents !== undefined && (!Array.isArray(x.routingIncidents) || x.routingIncidents.length > 4096 || !x.routingIncidents.every(isWellFormedRoutingIncident)))
114
148
  return false;
115
149
  if (x.implementationLineages !== undefined && (!Array.isArray(x.implementationLineages) || x.implementationLineages.length > 4096 || !x.implementationLineages.every(isWellFormedImplementationLineage)))
@@ -183,7 +217,10 @@ function isWellFormedNextAction(x) {
183
217
  && (x.state === 'pending' || x.state === 'in-progress');
184
218
  }
185
219
  function isWellFormedDispatch(x) {
186
- return isObj(x) && typeof x.id === 'string' && typeof x.taskId === 'string' && typeof x.at === 'string';
220
+ return isObj(x) && Object.keys(x).every(key => ['id', 'taskId', 'at', 'routingAttemptId', 'dispatchRequestId'].includes(key))
221
+ && typeof x.id === 'string' && x.id.length > 0 && typeof x.taskId === 'string' && x.taskId.length > 0
222
+ && typeof x.at === 'string' && (x.routingAttemptId === undefined || (typeof x.routingAttemptId === 'string' && x.routingAttemptId.length > 0))
223
+ && (x.dispatchRequestId === undefined || (typeof x.dispatchRequestId === 'string' && x.dispatchRequestId.length > 0));
187
224
  }
188
225
  function strings(x) {
189
226
  return Array.isArray(x) && x.every((item) => typeof item === 'string');
@@ -235,11 +272,11 @@ function isWellFormedAppliedRequest(x) {
235
272
  && (x.resultRef === undefined || typeof x.resultRef === 'string');
236
273
  }
237
274
  function isWellFormedRoutingAttempt(x) {
238
- return isObj(x) && Object.keys(x).every(key => ['schema', 'id', 'obligationId', 'lineageId', 'attempt', 'envelope', 'envelopeDigest', 'fingerprint', 'state', 'reservedAt', 'nativeEvidenceRequired', 'nativeEventDigest', 'nativeAgentId', 'observed', 'observedBackendModel', 'verdict', 'reasonCode', 'administrativeRepair'].includes(key))
275
+ return isObj(x) && Object.keys(x).every(key => ['schema', 'id', 'obligationId', 'lineageId', 'attempt', 'envelope', 'envelopeDigest', 'fingerprint', 'state', 'reservedAt', 'reservationRequestId', 'nativeEvidenceRequired', 'nativeEventDigest', 'nativeAgentId', 'observed', 'observedBackendModel', 'verdict', 'reasonCode', 'administrativeRepair'].includes(key))
239
276
  && x.schema === 'routing-attempt/v1'
240
277
  && typeof x.id === 'string' && typeof x.obligationId === 'string' && typeof x.lineageId === 'string' && Number.isSafeInteger(x.attempt) && x.attempt > 0
241
278
  && typeof x.envelopeDigest === 'string' && /^[a-f0-9]{64}$/.test(x.envelopeDigest) && typeof x.fingerprint === 'string' && /^[a-f0-9]{64}$/.test(x.fingerprint)
242
- && ['reserved', 'active', 'complete', 'blocked', 'unknown'].includes(String(x.state)) && (x.reservedAt === undefined || (typeof x.reservedAt === 'string' && Number.isFinite(Date.parse(x.reservedAt)))) && (x.nativeEvidenceRequired === undefined || x.nativeEvidenceRequired === true) && (x.nativeEventDigest === undefined || isDigest(x.nativeEventDigest)) && (x.nativeAgentId === undefined || typeof x.nativeAgentId === 'string') && (x.observed === undefined || isRoutingSelection(x.observed)) && (x.observedBackendModel === undefined || (typeof x.observedBackendModel === 'string' && x.observedBackendModel.length > 0 && x.observedBackendModel.length <= 128)) && (x.verdict === undefined || ['pass', 'fail', 'inconclusive'].includes(String(x.verdict))) && (x.reasonCode === undefined || typeof x.reasonCode === 'string') && (x.administrativeRepair === undefined || x.administrativeRepair === true) && isRoutingEnvelope(x.envelope);
279
+ && ['reserved', 'active', 'complete', 'blocked', 'unknown'].includes(String(x.state)) && (x.reservedAt === undefined || (typeof x.reservedAt === 'string' && Number.isFinite(Date.parse(x.reservedAt)))) && (x.reservationRequestId === undefined || (typeof x.reservationRequestId === 'string' && x.reservationRequestId.length > 0 && x.reservationRequestId.length <= 128)) && (x.nativeEvidenceRequired === undefined || x.nativeEvidenceRequired === true) && (x.nativeEventDigest === undefined || isDigest(x.nativeEventDigest)) && (x.nativeAgentId === undefined || typeof x.nativeAgentId === 'string') && (x.observed === undefined || isRoutingSelection(x.observed)) && (x.observedBackendModel === undefined || (typeof x.observedBackendModel === 'string' && x.observedBackendModel.length > 0 && x.observedBackendModel.length <= 128)) && (x.verdict === undefined || ['pass', 'fail', 'inconclusive'].includes(String(x.verdict))) && (x.reasonCode === undefined || typeof x.reasonCode === 'string') && (x.administrativeRepair === undefined || x.administrativeRepair === true) && isRoutingEnvelope(x.envelope);
243
280
  }
244
281
  function isWellFormedRoutingIncident(x) {
245
282
  if (!isObj(x) || Object.keys(x).length !== 12 || !Object.keys(x).every(key => ['schema', 'id', 'runStartedAt', 'target', 'selection', 'reasonCode', 'firstAt', 'lastAt', 'affectedObligations', 'fallbackCount', 'blockedCount', 'alertState'].includes(key)))
@@ -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
  };
@@ -19,6 +52,7 @@ const supervisor_1 = require("../../../src/commands/watch/supervisor");
19
52
  const exec_wrapper_1 = require("../../../src/commands/job/exec-wrapper");
20
53
  const lock_1 = require("../../../src/commands/watch/lock");
21
54
  const archive_unused_1 = require("../../../src/commands/watch/archive-unused");
55
+ const adapterModule = __importStar(require("../../../src/core/journal/adapter"));
22
56
  jest.setTimeout(30000);
23
57
  function valid(repo) {
24
58
  const report = (0, validate_1.validatePlanFile)('docs/plan.md', repo);
@@ -43,7 +77,7 @@ describe('compact lifecycle proof and unused bootstrap archival', () => {
43
77
  fs_1.default.writeFileSync(path_1.default.join(repo, 'docs/plan.md'), `${fs_1.default.readFileSync(path_1.default.join(__dirname, '../../core/plan/fixtures/compact-slices-v1/valid.md'), 'utf8')}\n- [ ] Reviewed task.\n`);
44
78
  (0, init_1.initWatch)(repo, 'main', { path: 'docs/plan.md', report: valid(repo) });
45
79
  });
46
- afterEach(() => { fs_1.default.rmSync(repo, { recursive: true, force: true }); });
80
+ afterEach(() => { jest.restoreAllMocks(); fs_1.default.rmSync(repo, { recursive: true, force: true }); });
47
81
  test('durable journal retains hash commitments only, never the validated plan source body', () => {
48
82
  const contents = (directory) => fs_1.default.readdirSync(directory, { withFileTypes: true }).map(entry => entry.isDirectory() ? contents(path_1.default.join(directory, entry.name)) : fs_1.default.readFileSync(path_1.default.join(directory, entry.name), 'utf8')).join('\n');
49
83
  expect(contents((0, paths_1.journalDir)(repo, 'main'))).not.toContain('Reviewed task.');
@@ -138,6 +172,18 @@ describe('compact lifecycle proof and unused bootstrap archival', () => {
138
172
  fingerprint: (0, fingerprint_1.computeFingerprint)(repo, argv, ['source.md'], '.').fingerprint, argv, paths: ['source.md'], cwd: '.',
139
173
  } });
140
174
  }
175
+ // This fixture acts as the external controller. Give it an adopted
176
+ // identity before expecting the supervisor to dispatch verification
177
+ // jobs; launch intent alone is deliberately insufficient.
178
+ expect(await supervisor.tick()).toBe('continue');
179
+ const launched = (0, store_1.readJournal)(repo, 'main').state;
180
+ const gen = launched.generations.find(entry => entry.token === generation.token);
181
+ gen.processRef = { pid: process.pid, processGroup: process.pid, startTime: 'fixture',
182
+ spawnNonce: gen.spawnNonce, argvDigest: gen.launchArgvDigest, psArgsDigest: 'b'.repeat(64) };
183
+ (0, store_1.writeJournal)(repo, 'main', launched);
184
+ const adapter = adapterModule.adapterFor('codex');
185
+ jest.spyOn(adapterModule, 'adapterFor').mockReturnValue({ ...adapter,
186
+ activity: () => ({ cpuTime: '0', groupSize: 1 }), safeToReplace: () => 'indeterminate' });
141
187
  for (let i = 0; i < 100; i++) {
142
188
  expect(await supervisor.tick()).toBe('continue');
143
189
  if (Object.values((0, store_1.readJournal)(repo, 'main').state.jobs).some(job => job.verdict === 'pass'))