@fitlab-ai/agent-infra 0.9.5 → 0.9.6

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 (106) hide show
  1. package/dist/bin/internal-cli.js +3 -3
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +126 -16
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +48 -4
  5. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  6. package/dist/lib/git/workflow.js +0 -1
  7. package/dist/lib/internal/codex-lifecycle.js +100 -2
  8. package/dist/lib/internal/release-workflow.js +108 -11
  9. package/dist/lib/internal/sandbox-control.js +43 -6
  10. package/dist/lib/internal/task-create.js +12 -4
  11. package/dist/lib/internal/task-orchestration.js +5 -4
  12. package/dist/lib/platform/issue-comments.js +2 -1
  13. package/dist/lib/release/workflow.js +29 -21
  14. package/dist/lib/sandbox/commands/create.js +15 -16
  15. package/dist/lib/sandbox/commands/prune.js +40 -16
  16. package/dist/lib/sandbox/commands/rm.js +318 -89
  17. package/dist/lib/sandbox/control/client.js +138 -22
  18. package/dist/lib/sandbox/control/executor.js +136 -0
  19. package/dist/lib/sandbox/control/lifecycle.js +348 -0
  20. package/dist/lib/sandbox/control/protocol.js +62 -26
  21. package/dist/lib/sandbox/control/server.js +222 -102
  22. package/dist/lib/sandbox/control/state.js +181 -0
  23. package/dist/lib/sandbox/index.js +2 -2
  24. package/dist/lib/sandbox/managed-fs.js +14 -2
  25. package/dist/lib/sandbox/mounts.js +35 -24
  26. package/dist/lib/sandbox/recovery.js +262 -45
  27. package/dist/lib/sandbox/workspace-view.js +73 -10
  28. package/dist/lib/sandbox/worktree-safety.js +196 -0
  29. package/dist/lib/server/process-control.js +6 -0
  30. package/dist/lib/task/artifact-lifecycle.js +5 -4
  31. package/dist/lib/task/codex-orchestration.js +161 -0
  32. package/dist/lib/task/commands/status.js +3 -0
  33. package/dist/lib/task/commands/validate.js +289 -0
  34. package/dist/lib/task/commit-finalization.js +36 -9
  35. package/dist/lib/task/delegation-receipts.js +25 -0
  36. package/dist/lib/task/events.js +10 -4
  37. package/dist/lib/task/finalization-artifacts.js +2 -1
  38. package/dist/lib/task/implementation-inputs.js +1 -7
  39. package/dist/lib/task/index.js +9 -0
  40. package/dist/lib/task/orchestration.js +133 -4
  41. package/dist/lib/task/verification-engine.js +38 -6
  42. package/dist/lib/task/verification.js +1 -0
  43. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +142 -15
  44. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
  45. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +51 -3
  46. package/lib/agent-clients/adapters/codex.ts +1 -1
  47. package/lib/git/workflow.ts +0 -1
  48. package/lib/internal/codex-lifecycle.ts +107 -3
  49. package/lib/internal/release-workflow.ts +107 -11
  50. package/lib/internal/sandbox-control.ts +38 -6
  51. package/lib/internal/task-create.ts +16 -4
  52. package/lib/internal/task-orchestration.ts +4 -4
  53. package/lib/platform/issue-comments.ts +2 -1
  54. package/lib/release/workflow.ts +69 -20
  55. package/lib/sandbox/commands/create.ts +17 -21
  56. package/lib/sandbox/commands/prune.ts +53 -15
  57. package/lib/sandbox/commands/rm.ts +368 -103
  58. package/lib/sandbox/control/client.ts +152 -28
  59. package/lib/sandbox/control/executor.ts +163 -0
  60. package/lib/sandbox/control/lifecycle.ts +342 -0
  61. package/lib/sandbox/control/protocol.ts +96 -69
  62. package/lib/sandbox/control/server.ts +239 -110
  63. package/lib/sandbox/control/state.ts +196 -0
  64. package/lib/sandbox/index.ts +2 -2
  65. package/lib/sandbox/managed-fs.ts +17 -2
  66. package/lib/sandbox/mounts.ts +31 -13
  67. package/lib/sandbox/recovery.ts +301 -60
  68. package/lib/sandbox/workspace-view.ts +81 -11
  69. package/lib/sandbox/worktree-safety.ts +234 -0
  70. package/lib/server/process-control.ts +11 -0
  71. package/lib/task/artifact-lifecycle.ts +5 -4
  72. package/lib/task/codex-orchestration.ts +233 -0
  73. package/lib/task/commands/status.ts +2 -0
  74. package/lib/task/commands/validate.ts +282 -0
  75. package/lib/task/commit-finalization.ts +52 -9
  76. package/lib/task/delegation-receipts.ts +52 -3
  77. package/lib/task/events.ts +9 -4
  78. package/lib/task/finalization-artifacts.ts +2 -1
  79. package/lib/task/implementation-inputs.ts +1 -5
  80. package/lib/task/index.ts +8 -0
  81. package/lib/task/orchestration.ts +199 -4
  82. package/lib/task/verification-engine.ts +42 -8
  83. package/lib/task/verification.ts +2 -1
  84. package/package.json +1 -1
  85. package/templates/.agents/hooks/lifecycle-delegation.js +48 -12
  86. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  87. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  88. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  89. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  90. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  91. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  92. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  93. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  94. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
  95. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
  96. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
  97. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
  98. package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
  99. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
  100. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
  101. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
  102. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  103. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  104. package/templates/.codex/hooks.json +8 -8
  105. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  106. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
@@ -74,7 +74,7 @@ type ModelPolicySource = Readonly<{
74
74
  client: AgentClientId;
75
75
  resolvedAt: string;
76
76
  }>;
77
- type OrchestrationRecovery = Readonly<{
77
+ type ModelPolicyRecovery = Readonly<{
78
78
  code: 'MODEL_POLICY_SUPPLEMENTED';
79
79
  recoveredAt: string;
80
80
  previousSchemaVersion: 1;
@@ -85,6 +85,26 @@ type OrchestrationRecovery = Readonly<{
85
85
  pendingDelegation: false;
86
86
  resultingStatus: OrchestrationStatus;
87
87
  }>;
88
+ type ClientCapabilityRecovery = Readonly<{
89
+ code: 'CLIENT_CAPABILITY_ENABLED';
90
+ recoveredAt: string;
91
+ previousSchemaVersion: 2;
92
+ previousStatus: 'paused';
93
+ previousPause: Readonly<{ code: 'ORCHESTRATION_CLIENT_UNSUPPORTED'; message: string; recoverable: boolean }>;
94
+ client: 'codex';
95
+ guards: Readonly<{
96
+ stepCount: 0;
97
+ nextStage: null;
98
+ baselineEmpty: true;
99
+ receiptCount: 0;
100
+ pendingDelegation: false;
101
+ commitAuthorizationUnused: true;
102
+ completionEvidenceAbsent: true;
103
+ commitIntentAbsent: true;
104
+ }>;
105
+ resultingStatus: 'running';
106
+ }>;
107
+ type OrchestrationRecovery = ModelPolicyRecovery | ClientCapabilityRecovery;
88
108
  type CleanCompletionEvidence = Readonly<{
89
109
  kind: 'reviewed-head-clean';
90
110
  observedAt: string;
@@ -369,6 +389,23 @@ function resolveProjectPolicy(repoRoot: string, client: AgentClientId): Orchestr
369
389
  return normalizeAgentClients(raw).state[client].orchestration;
370
390
  }
371
391
 
392
+ function canRecoverCodexUnsupportedPause(run: OrchestrationRun, taskDir: string): boolean {
393
+ return run.schemaVersion === 2
394
+ && run.status === 'paused'
395
+ && run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
396
+ && run.modelPolicySource?.client === 'codex'
397
+ && run.stepCount === 0
398
+ && run.nextStage === null
399
+ && run.baseline === ''
400
+ && run.pendingDelegation === null
401
+ && run.receipts.length === 0
402
+ && run.commitAuthorization?.issuedAt === null
403
+ && run.commitAuthorization?.consumedAt === null
404
+ && run.completionEvidence == null
405
+ && !fs.existsSync(commitIntentPath(taskDir))
406
+ && (run.recoveryHistory ?? []).every((entry) => entry.code === 'MODEL_POLICY_SUPPLEMENTED');
407
+ }
408
+
372
409
  function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
373
410
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
374
411
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
@@ -415,6 +452,39 @@ function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptio
415
452
  return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
416
453
  }
417
454
  }
455
+ if (existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED') {
456
+ if (!canRecoverCodexUnsupportedPause(existing, resolved.taskDir)) {
457
+ return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
458
+ }
459
+ const now = (options.now ?? (() => new Date().toISOString()))();
460
+ const previousPause = existing.pause as ClientCapabilityRecovery['previousPause'];
461
+ const recovery: ClientCapabilityRecovery = {
462
+ code: 'CLIENT_CAPABILITY_ENABLED',
463
+ recoveredAt: now,
464
+ previousSchemaVersion: 2,
465
+ previousStatus: 'paused',
466
+ previousPause,
467
+ client: 'codex',
468
+ guards: {
469
+ stepCount: 0,
470
+ nextStage: null,
471
+ baselineEmpty: true,
472
+ receiptCount: 0,
473
+ pendingDelegation: false,
474
+ commitAuthorizationUnused: true,
475
+ completionEvidenceAbsent: true,
476
+ commitIntentAbsent: true
477
+ },
478
+ resultingStatus: 'running'
479
+ };
480
+ const resumed = withUpdatedRun(existing, {
481
+ status: 'running',
482
+ pause: null,
483
+ recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery])
484
+ }, () => now);
485
+ saveRun(resolved.taskDir, resumed);
486
+ return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
487
+ }
418
488
  if (existing.status === 'paused' && existing.pause?.recoverable && existing.pendingDelegation === null) {
419
489
  const resumed = withUpdatedRun(existing, { status: 'running', pause: null });
420
490
  saveRun(resolved.taskDir, resumed);
@@ -1377,7 +1447,7 @@ function matchingDelegations(
1377
1447
  .filter((entry) => entry.isDirectory())
1378
1448
  .flatMap((entry) => {
1379
1449
  const run = readRun(path.join(activeRoot, entry.name));
1380
- if (!run?.pendingDelegation) return [];
1450
+ if (run?.status !== 'running' || !run.pendingDelegation) return [];
1381
1451
  const receipt = run.pendingDelegation;
1382
1452
  return predicate(receipt)
1383
1453
  ? [{ taskId: entry.name, run }]
@@ -1411,13 +1481,44 @@ function activateMatchingOrchestrationDelegation(
1411
1481
  (receipt) => receipt.status === 'prepared',
1412
1482
  options
1413
1483
  );
1414
- if ('status' in matched) return matched;
1484
+ if ('status' in matched) {
1485
+ if (matched.error?.code !== 'ORCHESTRATION_DELEGATION_MISSING') return matched;
1486
+ const replay = uniqueMatchingDelegation(
1487
+ client,
1488
+ (receipt) => receipt.childId === event.childId && ['activated', 'stage-completed'].includes(receipt.status),
1489
+ options
1490
+ );
1491
+ if ('status' in replay) return matched;
1492
+ const receipt = replay.run.pendingDelegation!;
1493
+ const sameEvidence = receipt.parentId === event.parentId
1494
+ && receipt.spawnMode === event.spawnMode
1495
+ && receipt.actualModel === event.actualModel
1496
+ && receipt.actualReasoningEffort === event.actualReasoningEffort
1497
+ && receipt.modelFallbackReason === (event.modelFallbackReason ?? null)
1498
+ && receipt.reasoningEffortFallbackReason === (event.reasoningEffortFallbackReason ?? null)
1499
+ && receipt.hostEvidence?.hookDefinitionHash === event.hostEvidence?.hookDefinitionHash;
1500
+ if (!sameEvidence) {
1501
+ return pauseOrchestration(replay.taskId, 'DELEGATION_REPLAY_CONFLICT', 'replayed native start evidence conflicts with the active receipt', true, options);
1502
+ }
1503
+ return { status: replay.run.status, changed: false, taskId: replay.taskId, run: replay.run, next: null, error: null };
1504
+ }
1415
1505
  if (matched.run.receipts.some((receipt) => receipt.childId === event.childId)) {
1416
1506
  return pauseOrchestration(matched.taskId, 'DELEGATION_IDENTITY_REUSED', 'native child identity was already used by this run', true, options);
1417
1507
  }
1418
1508
  return activateOrchestrationDelegation(matched.taskId, event, options);
1419
1509
  }
1420
1510
 
1511
+ function pauseMatchingOrchestrationDelegation(
1512
+ client: AgentClientId,
1513
+ code: string,
1514
+ message: string,
1515
+ options: OrchestrationOptions = {}
1516
+ ): OrchestrationResult {
1517
+ const matched = uniqueMatchingDelegation(client, () => true, options);
1518
+ if ('status' in matched) return matched;
1519
+ return pauseOrchestration(matched.taskId, code, message, true, options);
1520
+ }
1521
+
1421
1522
  function sealMatchingOrchestrationDelegation(
1422
1523
  client: AgentClientId,
1423
1524
  event: Readonly<{ nativeAgent: string; childId: string }>,
@@ -1462,6 +1563,96 @@ function sealMatchingOrchestrationDelegation(
1462
1563
  }
1463
1564
  }
1464
1565
 
1566
+ function sealMatchingOrchestrationDelegationWithHostEvidence(
1567
+ client: AgentClientId,
1568
+ event: Readonly<{ nativeAgent: string; childId: string }>,
1569
+ consumeEvidence: (receipt: DelegationReceipt) => Readonly<{
1570
+ stopRevision: number;
1571
+ consumer: string;
1572
+ consumedAt: string;
1573
+ }>,
1574
+ options: OrchestrationOptions = {}
1575
+ ): OrchestrationResult {
1576
+ const role = managedDelegationRole(event.nativeAgent);
1577
+ if (!role) return failed('DELEGATION_IGNORED', `subagent '${event.nativeAgent}' is not lifecycle-managed`);
1578
+ const matched = uniqueMatchingDelegation(
1579
+ client,
1580
+ (receipt) => (
1581
+ (receipt.status === 'stage-completed' || receipt.status === 'sealed')
1582
+ && receipt.childId === event.childId
1583
+ ),
1584
+ options
1585
+ );
1586
+ if ('status' in matched) return matched;
1587
+ const receipt = matched.run.pendingDelegation!;
1588
+ if (receipt.status === 'sealed') {
1589
+ return reconcileMatchingOrchestrationDelegation(client, event.childId, options);
1590
+ }
1591
+ if (receipt.role !== role) {
1592
+ return pauseOrchestration(matched.taskId, 'DELEGATION_ROLE_MISMATCH', `managed role ${role} does not match ${receipt.role}`, true, options);
1593
+ }
1594
+ const repoRoot = options.repoRoot ?? process.cwd();
1595
+ try {
1596
+ const gitRoot = gitRootFor(repoRoot, options);
1597
+ const snapshotTaskId = receipt.workspaceSnapshotScope === 'task' ? receipt.taskId : null;
1598
+ const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
1599
+ gitRoot,
1600
+ stateRoot: repoRoot,
1601
+ taskId: snapshotTaskId
1602
+ });
1603
+ const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(gitRoot, receipt.beforeFingerprint, afterFingerprint);
1604
+ const baseEvent = { childId: event.childId, exitCode: 0, afterFingerprint, changedPaths };
1605
+ const validated = sealDelegation(receipt, baseEvent, { now: options.now });
1606
+ if (!validated.ok) {
1607
+ return pauseOrchestration(matched.taskId, validated.code, validated.message, true, options);
1608
+ }
1609
+ const hostEvidence = consumeEvidence(receipt);
1610
+ const sealed = sealDelegation(receipt, { ...baseEvent, hostEvidence }, { now: options.now });
1611
+ if (!sealed.ok) return pauseOrchestration(matched.taskId, sealed.code, sealed.message, true, options);
1612
+ const resolved = resolveTaskRef(matched.taskId, { repoRoot });
1613
+ if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
1614
+ const updated = withUpdatedRun(matched.run, { pendingDelegation: sealed.receipt }, options.now);
1615
+ saveRun(resolved.taskDir, updated);
1616
+ return { status: 'running', changed: true, taskId: matched.taskId, run: updated, next: null, error: null };
1617
+ } catch (error) {
1618
+ return pauseOrchestration(
1619
+ matched.taskId,
1620
+ 'ORCHESTRATION_CODEX_EVIDENCE_FAILED',
1621
+ error instanceof Error ? error.message : String(error),
1622
+ true,
1623
+ options
1624
+ );
1625
+ }
1626
+ }
1627
+
1628
+ function reconcileMatchingOrchestrationDelegation(
1629
+ client: AgentClientId,
1630
+ childId: string,
1631
+ options: OrchestrationOptions = {}
1632
+ ): OrchestrationResult {
1633
+ const matched = uniqueMatchingDelegation(
1634
+ client,
1635
+ (receipt) => receipt.childId === childId,
1636
+ options
1637
+ );
1638
+ if ('status' in matched) return matched;
1639
+ const receipt = matched.run.pendingDelegation!;
1640
+ if (
1641
+ receipt.status !== 'sealed'
1642
+ || receipt.hostEvidence?.consumer !== receipt.id
1643
+ || receipt.hostEvidence.consumedAt === null
1644
+ ) {
1645
+ return pauseOrchestration(
1646
+ matched.taskId,
1647
+ 'ORCHESTRATION_CODEX_RECONCILIATION_FAILED',
1648
+ 'Codex native spawn did not produce one sealed receipt with consumed host evidence',
1649
+ true,
1650
+ options
1651
+ );
1652
+ }
1653
+ return { status: matched.run.status, changed: false, taskId: matched.taskId, run: matched.run, next: null, error: null };
1654
+ }
1655
+
1465
1656
  function activateOrchestrationDelegation(
1466
1657
  taskRef: string,
1467
1658
  event: Parameters<typeof activateDelegation>[1],
@@ -1628,13 +1819,16 @@ export {
1628
1819
  completeOrchestrationStage,
1629
1820
  inspectOrchestrationStage,
1630
1821
  orchestrationPath,
1822
+ pauseMatchingOrchestrationDelegation,
1631
1823
  pauseOrchestration,
1632
1824
  planOrchestrationStageCompletion,
1633
1825
  prepareOrchestrationDelegation,
1634
1826
  readRun,
1827
+ reconcileMatchingOrchestrationDelegation,
1635
1828
  recoverCommitIntent,
1636
1829
  routeOrchestration,
1637
1830
  sealMatchingOrchestrationDelegation,
1831
+ sealMatchingOrchestrationDelegationWithHostEvidence,
1638
1832
  sealOrchestrationDelegation,
1639
1833
  startCommitAttempt,
1640
1834
  statusCommitIntent,
@@ -1650,5 +1844,6 @@ export type {
1650
1844
  OrchestrationRun,
1651
1845
  OrchestrationStageCompletion,
1652
1846
  OrchestrationStageIdentity,
1653
- OrchestrationStatus
1847
+ OrchestrationStatus,
1848
+ OrchestrationOptions
1654
1849
  };
@@ -268,14 +268,29 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
268
268
  return failResult('orchestration-evidence', 'Run model policy source or recovery history is invalid');
269
269
  }
270
270
  for (const recovery of run.recoveryHistory) {
271
- if (
272
- recovery.code !== 'MODEL_POLICY_SUPPLEMENTED'
273
- || recovery.previousSchemaVersion !== 1
274
- || recovery.receiptCount !== 0
275
- || recovery.pendingDelegation !== false
276
- || !exactText(recovery.recoveredAt)
277
- || !recovery.policySource
278
- ) {
271
+ const validModelRecovery = recovery.code === 'MODEL_POLICY_SUPPLEMENTED'
272
+ && recovery.previousSchemaVersion === 1
273
+ && recovery.receiptCount === 0
274
+ && recovery.pendingDelegation === false
275
+ && exactText(recovery.recoveredAt)
276
+ && recovery.policySource;
277
+ const guards = recovery.guards;
278
+ const validCapabilityRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED'
279
+ && recovery.previousSchemaVersion === 2
280
+ && recovery.previousStatus === 'paused'
281
+ && recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
282
+ && recovery.client === 'codex'
283
+ && recovery.resultingStatus === 'running'
284
+ && exactText(recovery.recoveredAt)
285
+ && guards?.stepCount === 0
286
+ && guards?.nextStage === null
287
+ && guards?.baselineEmpty === true
288
+ && guards?.receiptCount === 0
289
+ && guards?.pendingDelegation === false
290
+ && guards?.commitAuthorizationUnused === true
291
+ && guards?.completionEvidenceAbsent === true
292
+ && guards?.commitIntentAbsent === true;
293
+ if (!validModelRecovery && !validCapabilityRecovery) {
279
294
  return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
280
295
  }
281
296
  }
@@ -333,6 +348,25 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
333
348
  ) {
334
349
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
335
350
  }
351
+ if (receipt.client === 'codex') {
352
+ const host = receipt.hostEvidence;
353
+ if (
354
+ host?.kind !== 'codex-lifecycle-v1'
355
+ || !exactText(host.hookDefinitionHash)
356
+ || !Number.isSafeInteger(host.startRevision)
357
+ || host.startRevision < 1
358
+ ) {
359
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex start evidence`);
360
+ }
361
+ if (['sealed', 'consumed'].includes(receipt.status) && (
362
+ !Number.isSafeInteger(host.stopRevision)
363
+ || host.stopRevision <= host.startRevision
364
+ || host.consumer !== receipt.id
365
+ || !exactText(host.consumedAt)
366
+ )) {
367
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex consumed stop evidence`);
368
+ }
369
+ }
336
370
  }
337
371
  }
338
372
  if (run.receipts.some((receipt: any) => receipt.status !== 'consumed')) {
@@ -11,7 +11,7 @@ import type { TaskWorkspaceState } from './resolve-ref.ts';
11
11
  type VerificationEvent =
12
12
  | 'analyze.awaiting-input' | 'analyze.completed'
13
13
  | 'review-analysis.completed' | 'plan.completed' | 'review-plan.completed'
14
- | 'code.completed' | 'review-code.completed' | 'manual-validation.completed'
14
+ | 'code.completed' | 'review-code.completed' | 'manual-validation.completed' | 'validation-run.completed'
15
15
  | 'block-task.completed' | 'cancel-task.completed' | 'commit.completed'
16
16
  | 'complete-task.preflight' | 'complete-task.completed'
17
17
  | 'create-pr.completed' | 'create-task.completed'
@@ -62,6 +62,7 @@ const VERIFICATION_CATALOG: Readonly<Record<VerificationEvent, VerificationSpec>
62
62
  'code.completed': gate('code-task', 'active', 'code'),
63
63
  'review-code.completed': gate('review-code', 'active', 'review-code'),
64
64
  'manual-validation.completed': gate('complete-manual-validation', 'active', 'manual-validation'),
65
+ 'validation-run.completed': gate('run-manual-validation', 'active', 'validation-run'),
65
66
  'block-task.completed': gate('block-task', 'blocked'),
66
67
  'cancel-task.completed': gate('cancel-task', 'completed'),
67
68
  'commit.completed': gate('commit', 'active'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -5,6 +5,8 @@ import { readFileSync } from 'node:fs';
5
5
  import { fileURLToPath } from 'node:url';
6
6
 
7
7
  const MAX_INPUT_BYTES = 64 * 1024;
8
+ const repoPackageJson = fileURLToPath(new URL('../../package.json', import.meta.url));
9
+ const sourceInternalCli = fileURLToPath(new URL('../../bin/internal-cli.ts', import.meta.url));
8
10
  const localInternalCli = fileURLToPath(new URL('../../dist/bin/internal-cli.js', import.meta.url));
9
11
  const codexHooks = fileURLToPath(new URL('../../.codex/hooks.json', import.meta.url));
10
12
  const clientIndex = process.argv.indexOf('--client');
@@ -13,6 +15,27 @@ if (client !== 'claude-code' && client !== 'codex') {
13
15
  process.stderr.write('Lifecycle delegation hook requires a supported --client value\n');
14
16
  process.exit(1);
15
17
  }
18
+
19
+ function internalCliInvocation(args) {
20
+ let isAgentInfraSourceCheckout = false;
21
+ try {
22
+ isAgentInfraSourceCheckout = JSON.parse(readFileSync(repoPackageJson, 'utf8')).name === '@fitlab-ai/agent-infra';
23
+ } catch {
24
+ // Installed project hooks do not require a repository package.json.
25
+ }
26
+ if (isAgentInfraSourceCheckout && existsSync(sourceInternalCli)) {
27
+ return {
28
+ command: process.execPath,
29
+ commandArgs: ['--experimental-strip-types', sourceInternalCli, ...args],
30
+ shell: false
31
+ };
32
+ }
33
+ if (existsSync(localInternalCli)) {
34
+ return { command: process.execPath, commandArgs: [localInternalCli, ...args], shell: false };
35
+ }
36
+ return { command: 'agent-infra-internal', commandArgs: args, shell: process.platform === 'win32' };
37
+ }
38
+
16
39
  let input = '';
17
40
  process.stdin.setEncoding('utf8');
18
41
  process.stdin.on('data', (chunk) => {
@@ -34,15 +57,28 @@ process.stdin.on('end', () => {
34
57
  const explicitPhase = phaseIndex >= 0 ? process.argv[phaseIndex + 1] : '';
35
58
  const hook = String(explicitPhase || event.hook_event_name || event.event || '').toLowerCase();
36
59
  const toolInput = event.tool_input && typeof event.tool_input === 'object' ? event.tool_input : {};
60
+ const toolName = String(event.tool_name || event.tool || '').toLowerCase();
61
+ let toolResponse = {};
62
+ try {
63
+ toolResponse = typeof event.tool_response === 'string'
64
+ ? JSON.parse(event.tool_response)
65
+ : (event.tool_response || {});
66
+ } catch {
67
+ // Core ignores opaque tool responses; only the host timeout bit is forwarded.
68
+ }
37
69
  const nativeAgent = event.agent_name || event.agent_type || event.agent?.name
38
70
  || toolInput.agent_type || toolInput.agent || toolInput.name;
39
- if (!String(nativeAgent || '').startsWith('agent-infra-lifecycle-')) process.exit(0);
40
71
  if (client === 'codex') {
41
72
  const phases = new Set(['pre-tool', 'subagent-start', 'post-tool', 'subagent-stop']);
42
73
  if (!phases.has(hook)) {
43
74
  process.stderr.write('Managed Codex lifecycle hook event has an unknown type\n');
44
75
  process.exit(1);
45
76
  }
77
+ const managedAgent = /^agent-infra-lifecycle-(executor|reviewer)$/.test(String(nativeAgent || ''));
78
+ const parentReconcile = hook === 'post-tool'
79
+ && toolName === 'collaborationwait_agent'
80
+ && toolResponse.timed_out !== true;
81
+ if (!managedAgent && !parentReconcile) process.exit(0);
46
82
  const hookDefinitionHash = createHash('sha256').update(readFileSync(codexHooks)).digest('hex');
47
83
  const normalized = {
48
84
  sessionId: String(event.session_id || ''),
@@ -52,19 +88,20 @@ process.stdin.on('end', () => {
52
88
  nativeAgent: String(nativeAgent),
53
89
  requestedModel: String(toolInput.model || ''),
54
90
  requestedReasoningEffort: String(toolInput.reasoning_effort || toolInput.reasoningEffort || ''),
55
- hookDefinitionHash
91
+ hookDefinitionHash,
92
+ toolName,
93
+ taskName: String(toolInput.task_name || ''),
94
+ transcriptPath: String(event.transcript_path || '')
56
95
  };
57
- const args = ['codex-lifecycle', 'hook-event', '--event', hook];
96
+ const args = ['codex-lifecycle', 'hook-event', '--event', hook, '--bridge', 'true'];
58
97
  try {
59
- const useLocal = existsSync(localInternalCli);
60
- const command = useLocal ? process.execPath : 'agent-infra-internal';
61
- const commandArgs = useLocal ? [localInternalCli, ...args] : args;
98
+ const { command, commandArgs, shell } = internalCliInvocation(args);
62
99
  const output = execFileSync(command, commandArgs, {
63
100
  encoding: 'utf8',
64
101
  input: JSON.stringify(normalized),
65
- shell: !useLocal && process.platform === 'win32',
102
+ shell,
66
103
  stdio: ['pipe', 'pipe', 'pipe'],
67
- timeout: 5_000
104
+ timeout: 15_000
68
105
  });
69
106
  process.stdout.write(output);
70
107
  } catch (error) {
@@ -74,6 +111,7 @@ process.stdin.on('end', () => {
74
111
  }
75
112
  return;
76
113
  }
114
+ if (!/^agent-infra-lifecycle-(executor|reviewer)$/.test(String(nativeAgent || ''))) process.exit(0);
77
115
  const start = hook.includes('start');
78
116
  const stop = hook.includes('stop');
79
117
  if (!start && !stop) {
@@ -102,12 +140,10 @@ process.stdin.on('end', () => {
102
140
  );
103
141
  }
104
142
  try {
105
- const useLocal = existsSync(localInternalCli);
106
- const command = useLocal ? process.execPath : 'agent-infra-internal';
107
- const commandArgs = useLocal ? [localInternalCli, ...args] : args;
143
+ const { command, commandArgs, shell } = internalCliInvocation(args);
108
144
  const output = execFileSync(command, commandArgs, {
109
145
  encoding: 'utf8',
110
- shell: !useLocal && process.platform === 'win32',
146
+ shell,
111
147
  stdio: ['ignore', 'pipe', 'pipe']
112
148
  });
113
149
  process.stdout.write(output);
@@ -10,7 +10,7 @@
10
10
 
11
11
  ## Recovery
12
12
 
13
- `orchestration.json` is the detailed state source. A v2 run persists complete policy, source, and append-only recovery history. Only a v1 run with no pending delegation and zero receipts may be upgraded in place after supplying a complete policy; any historical receipt stays paused because effort is unverifiable. Migration is forward-only: old binaries must not advance active v2 runs.
13
+ `orchestration.json` is the detailed state source. A v2 run persists complete policy, source, and append-only recovery history. Only a v1 run with no pending delegation and zero receipts may be upgraded in place after supplying a complete policy; any historical receipt stays paused because effort is unverifiable. A historical Codex `ORCHESTRATION_CLIENT_UNSUPPORTED` v2 pause appends `CLIENT_CAPABILITY_ENABLED` and resumes only when step count, next stage, baseline, receipts, pending delegation, commit authorization, completion evidence, and commit intent all prove that execution never advanced; unknown or non-empty evidence preserves the pause. Migration is forward-only: old binaries must not advance active v2 runs.
14
14
 
15
15
  ## Model Policy
16
16
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  ## 恢复语义
12
12
 
13
- `orchestration.json` 是详细状态源。v2 run 保存完整策略、来源与 append-only 恢复历史。只有无 pending delegation 且零历史 receipt 的 v1 run 可在补齐完整策略后原地升级;任一历史 receipt 因缺 effort 证据保持暂停。其他阻塞、未封存 receipt 或基线漂移继续保持暂停。迁移是 forward-only,旧二进制不得推进 v2 active run。
13
+ `orchestration.json` 是详细状态源。v2 run 保存完整策略、来源与 append-only 恢复历史。只有无 pending delegation 且零历史 receipt 的 v1 run 可在补齐完整策略后原地升级;任一历史 receipt 因缺 effort 证据保持暂停。Codex 的历史 `ORCHESTRATION_CLIENT_UNSUPPORTED` v2 pause 仅在 stepCount、nextStage、baseline、receipts、pending delegation、commit authorization、completion evidence 与 commit intent 全部证明从未推进时追加 `CLIENT_CAPABILITY_ENABLED` 并恢复;任一未知或非空证据保持原暂停。迁移是 forward-only,旧二进制不得推进 v2 active run。
14
14
 
15
15
  ## 模型策略
16
16
 
@@ -47,6 +47,7 @@ SKILLs currently covered by this exemption:
47
47
 
48
48
  - `release`: may confirm publish after presenting the latest release snapshot
49
49
  - `create-release-note`: may confirm publish after presenting exact staged notes and their digest
50
+ - `post-release`: may confirm a normal push after preparing the local post commit and presenting the complete `postConfirmation` and digest
50
51
 
51
52
  ### Exemption 5: Complete model policy missing at lifecycle start
52
53
 
@@ -47,6 +47,7 @@
47
47
 
48
48
  - `release`:展示最新 release snapshot 后确认是否 publish
49
49
  - `create-release-note`:展示 stage 后的精确 notes 与摘要后确认是否 publish
50
+ - `post-release`:完成本地 post commit 并展示完整 `postConfirmation` 与摘要后确认是否普通 push
50
51
 
51
52
  ### 例外 5:生命周期启动缺少完整模型策略
52
53
 
@@ -15,16 +15,42 @@ Require one explicit canonical SemVer `{version}`. Do not fall back to the lates
15
15
  agent-infra-internal release-workflow inspect {version}
16
16
  ```
17
17
 
18
- Pending or unknown is blocked; a definite failure is failed.
18
+ Pending or unknown is blocked; a definite failure is failed. Report `complete` directly from external facts.
19
19
 
20
- ## 2. Run Post-release
20
+ ## 2. Prepare Local Post-release Work
21
+
22
+ When incomplete, run:
21
23
 
22
24
  ```bash
23
- agent-infra-internal release-workflow post {version}
25
+ agent-infra-internal release-workflow post-prepare {version}
26
+ agent-infra-internal release-workflow inspect {version}
24
27
  ```
25
28
 
26
- The core owns build, next development version, inline artifacts, explicit-path commit, normal push, and post-write inspection.
29
+ The core owns build, the optional demo, next development version, inline artifacts, and the explicit-path post commit. This action must not push. Replays recover from Git and channel facts without repeating completed work.
30
+
31
+ ## 3. Check the Confirmation Snapshot
32
+
33
+ - `complete`: report completion without asking or publishing.
34
+ - `postConfirmation` present: display every field and its `sha256`, then continue to step 4.
35
+ - `postConfirmation` absent: report diagnostics such as `isHead=false` or a dirty worktree/index and stop without asking or publishing.
27
36
 
28
- ## 3. Report the Snapshot
37
+ ## 4. Authorize the Current Snapshot
38
+
39
+ Only after displaying the complete `postConfirmation` in the current session, ask once whether to publish. Continue only for an unambiguous affirmative answer. Denial, adjustment, a question, ambiguity, or interruption stops. Authorization never carries across sessions or snapshots.
40
+
41
+ ## 5. Publish the Confirmed Snapshot
42
+
43
+ ```bash
44
+ agent-infra-internal release-workflow post-publish {version} \
45
+ --expected-sha256 "{post-confirmation-sha256}"
46
+ ```
47
+
48
+ The core re-inspects and verifies the digest before one normal branch push; force pushes are forbidden. On snapshot drift, stop and return to step 1 for a new display and authorization.
49
+
50
+ ## 6. Re-inspect and Report
51
+
52
+ ```bash
53
+ agent-infra-internal release-workflow inspect {version}
54
+ ```
29
55
 
30
- Report channel, smoke, commit, and push facts without presenting degraded or blocked as complete.
56
+ Report all channel, released/new version, smoke, post commit, and remote branch facts. Describe completion only for phase `complete`; never present failed, degraded, or blocked as success.
@@ -15,16 +15,42 @@ description: >
15
15
  agent-infra-internal release-workflow inspect {version}
16
16
  ```
17
17
 
18
- pending/unknown 必须 blocked,确定失败必须 failed。
18
+ pending/unknown 必须 blocked,确定失败必须 failed。`complete` 直接按外部事实报告完成。
19
19
 
20
- ## 2. 执行后处理
20
+ ## 2. 准备本地后处理
21
+
22
+ 尚未完成时执行:
21
23
 
22
24
  ```bash
23
- agent-infra-internal release-workflow post {version}
25
+ agent-infra-internal release-workflow post-prepare {version}
26
+ agent-infra-internal release-workflow inspect {version}
24
27
  ```
25
28
 
26
- core 负责 build、下一开发版本、内联产物、显式路径 commit 与普通 push,并在写后复核。
29
+ core 负责 build、可选 demo、下一开发版本、内联产物和显式路径 post commit;该动作不得 push。重跑从 Git 与渠道事实恢复,不重复已完成操作。
30
+
31
+ ## 3. 检查确认快照
32
+
33
+ - `complete`:报告完成,不询问、不 publish。
34
+ - 存在 `postConfirmation`:展示完整字段和 `sha256`,进入步骤 4。
35
+ - 不存在 `postConfirmation`:报告 `isHead=false`、工作树或暂存区非空等诊断并停止,不询问、不 publish。
27
36
 
28
- ## 3. 输出摘要
37
+ ## 4. 获取当前快照授权
38
+
39
+ 仅在当前会话已展示完整 `postConfirmation` 后询问一次是否发布。只有无歧义肯定答复才继续;否定、调整、疑问、歧义或中断均停止。授权不跨会话或快照复用。
40
+
41
+ ## 5. 发布已确认快照
42
+
43
+ ```bash
44
+ agent-infra-internal release-workflow post-publish {version} \
45
+ --expected-sha256 "{post-confirmation-sha256}"
46
+ ```
47
+
48
+ core 重新 inspect 并校验摘要后仅执行普通 branch push,禁止 force push。摘要漂移时停止并回到步骤 1 重新展示、重新确认。
49
+
50
+ ## 6. 写后复核并报告
51
+
52
+ ```bash
53
+ agent-infra-internal release-workflow inspect {version}
54
+ ```
29
55
 
30
- 报告全部渠道、smoke、commit 与 push 事实,不把 degraded/blocked 表述为完成。
56
+ 报告全部渠道、released/new version、smoke、post commit 与远端 branch 事实;只有阶段为 `complete` 才表述为完成,不把 failed/degraded/blocked 表述为成功。