@fitlab-ai/agent-infra 0.9.6 → 0.9.8

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 (178) hide show
  1. package/dist/bin/internal-cli.js +10 -0
  2. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +47 -5
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +245 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +398 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +23 -6
  8. package/dist/lib/decide.js +68 -14
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -3
  11. package/dist/lib/init.js +1 -1
  12. package/dist/lib/internal/codex-lifecycle.js +69 -1
  13. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  14. package/dist/lib/internal/release-workflow.js +1 -3
  15. package/dist/lib/internal/task-activity.js +57 -7
  16. package/dist/lib/internal/task-event.js +57 -4
  17. package/dist/lib/internal/task-ledger.js +59 -5
  18. package/dist/lib/internal/task-lifecycle.js +59 -8
  19. package/dist/lib/internal/task-orchestration.js +48 -8
  20. package/dist/lib/internal/task-override.js +131 -0
  21. package/dist/lib/internal/task-review.js +64 -6
  22. package/dist/lib/internal/task-verify.js +65 -3
  23. package/dist/lib/internal/task-warning.js +57 -4
  24. package/dist/lib/platform/context.js +6 -4
  25. package/dist/lib/platform/github-client.js +55 -2
  26. package/dist/lib/platform/pr-summary.js +5 -3
  27. package/dist/lib/process-data/checkpoint.js +319 -0
  28. package/dist/lib/process-data/index.js +128 -60
  29. package/dist/lib/process-data/materialize.js +84 -0
  30. package/dist/lib/process-data/normalize.js +34 -1
  31. package/dist/lib/process-data/reconcile.js +55 -1
  32. package/dist/lib/process-data/sources.js +422 -1
  33. package/dist/lib/process-data/store.js +341 -11
  34. package/dist/lib/runtime/agent-runtime.js +56 -0
  35. package/dist/lib/sandbox/commands/create.js +394 -264
  36. package/dist/lib/sandbox/commands/rm.js +64 -15
  37. package/dist/lib/sandbox/commands/start.js +4 -3
  38. package/dist/lib/sandbox/control/container-identity.js +64 -0
  39. package/dist/lib/sandbox/control/executor.js +23 -9
  40. package/dist/lib/sandbox/control/lifecycle.js +590 -61
  41. package/dist/lib/sandbox/control/protocol.js +7 -0
  42. package/dist/lib/sandbox/control/server.js +202 -26
  43. package/dist/lib/sandbox/control/state.js +51 -25
  44. package/dist/lib/sandbox/control/timing.js +8 -0
  45. package/dist/lib/sandbox/engine.js +14 -7
  46. package/dist/lib/sandbox/mounts.js +7 -0
  47. package/dist/lib/sandbox/recovery.js +135 -50
  48. package/dist/lib/sandbox/workspace-view.js +59 -20
  49. package/dist/lib/server/daemon.js +1 -1
  50. package/dist/lib/server/process-control.js +11 -1
  51. package/dist/lib/server/process-state.js +107 -20
  52. package/dist/lib/task/activity-intent.js +4 -1
  53. package/dist/lib/task/artifact-lifecycle.js +82 -11
  54. package/dist/lib/task/artifact-receipts.js +128 -0
  55. package/dist/lib/task/codex-orchestration.js +81 -6
  56. package/dist/lib/task/commands/validate.js +1 -1
  57. package/dist/lib/task/create.js +10 -2
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +120 -17
  60. package/dist/lib/task/events.js +148 -7
  61. package/dist/lib/task/guard-override.js +31 -0
  62. package/dist/lib/task/human-override.js +1240 -0
  63. package/dist/lib/task/ledger-intents.js +6 -3
  64. package/dist/lib/task/ledger.js +13 -1
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/orchestration.js +293 -18
  67. package/dist/lib/task/review-finalization.js +3 -2
  68. package/dist/lib/task/sections.js +5 -1
  69. package/dist/lib/task/task-execution-lock.js +5 -4
  70. package/dist/lib/task/verification-engine.js +80 -22
  71. package/dist/lib/task/verification.js +13 -1
  72. package/dist/lib/task/workflow-warning-intents.js +4 -2
  73. package/dist/lib/task/write.js +2 -1
  74. package/dist/lifecycle-build-manifest.json +6 -0
  75. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  76. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +62 -4
  77. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  78. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +322 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  80. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +480 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +35 -5
  82. package/lib/decide.ts +59 -14
  83. package/lib/defaults.json +10 -2
  84. package/lib/git/workflow.ts +25 -2
  85. package/lib/init.ts +1 -1
  86. package/lib/internal/codex-lifecycle.ts +68 -1
  87. package/lib/internal/codex-sandbox-controller.ts +109 -0
  88. package/lib/internal/release-workflow.ts +1 -3
  89. package/lib/internal/task-activity.ts +46 -6
  90. package/lib/internal/task-event.ts +53 -4
  91. package/lib/internal/task-ledger.ts +48 -4
  92. package/lib/internal/task-lifecycle.ts +56 -12
  93. package/lib/internal/task-orchestration.ts +43 -8
  94. package/lib/internal/task-override.ts +138 -0
  95. package/lib/internal/task-review.ts +51 -6
  96. package/lib/internal/task-verify.ts +52 -3
  97. package/lib/internal/task-warning.ts +44 -3
  98. package/lib/platform/context.ts +8 -5
  99. package/lib/platform/github-client.ts +61 -3
  100. package/lib/platform/pr-summary.ts +10 -3
  101. package/lib/process-data/checkpoint.ts +335 -0
  102. package/lib/process-data/index.ts +112 -60
  103. package/lib/process-data/materialize.ts +83 -0
  104. package/lib/process-data/normalize.ts +29 -1
  105. package/lib/process-data/reconcile.ts +64 -2
  106. package/lib/process-data/sources.ts +449 -2
  107. package/lib/process-data/store.ts +304 -11
  108. package/lib/process-data/types.ts +124 -2
  109. package/lib/runtime/agent-runtime.ts +76 -0
  110. package/lib/sandbox/commands/create.ts +139 -6
  111. package/lib/sandbox/commands/rm.ts +64 -14
  112. package/lib/sandbox/commands/start.ts +4 -3
  113. package/lib/sandbox/config.ts +2 -1
  114. package/lib/sandbox/control/container-identity.ts +93 -0
  115. package/lib/sandbox/control/executor.ts +24 -12
  116. package/lib/sandbox/control/lifecycle.ts +685 -67
  117. package/lib/sandbox/control/protocol.ts +35 -6
  118. package/lib/sandbox/control/server.ts +184 -24
  119. package/lib/sandbox/control/state.ts +67 -33
  120. package/lib/sandbox/control/timing.ts +12 -0
  121. package/lib/sandbox/engine.ts +20 -9
  122. package/lib/sandbox/mounts.ts +8 -0
  123. package/lib/sandbox/recovery.ts +159 -49
  124. package/lib/sandbox/workspace-view.ts +73 -22
  125. package/lib/server/daemon.ts +1 -1
  126. package/lib/server/process-control.ts +13 -3
  127. package/lib/server/process-state.ts +129 -27
  128. package/lib/task/activity-intent.ts +4 -2
  129. package/lib/task/artifact-lifecycle.ts +73 -10
  130. package/lib/task/artifact-receipts.ts +161 -0
  131. package/lib/task/codex-orchestration.ts +91 -5
  132. package/lib/task/commands/validate.ts +1 -1
  133. package/lib/task/create.ts +11 -2
  134. package/lib/task/decision-intents.ts +4 -2
  135. package/lib/task/delegation-receipts.ts +205 -25
  136. package/lib/task/events.ts +132 -6
  137. package/lib/task/guard-override.ts +50 -0
  138. package/lib/task/human-override.ts +1503 -0
  139. package/lib/task/ledger-intents.ts +6 -3
  140. package/lib/task/ledger.ts +16 -1
  141. package/lib/task/lifecycle.ts +71 -14
  142. package/lib/task/orchestration.ts +443 -22
  143. package/lib/task/review-finalization.ts +6 -2
  144. package/lib/task/sections.ts +6 -2
  145. package/lib/task/task-execution-lock.ts +11 -9
  146. package/lib/task/verification-engine.ts +93 -32
  147. package/lib/task/verification.ts +15 -1
  148. package/lib/task/workflow-warning-intents.ts +4 -2
  149. package/lib/task/write.ts +4 -1
  150. package/package.json +1 -1
  151. package/templates/.agents/README.en.md +7 -0
  152. package/templates/.agents/README.zh-CN.md +5 -0
  153. package/templates/.agents/hooks/lifecycle-delegation.js +33 -4
  154. package/templates/.agents/rules/lifecycle-orchestration.en.md +10 -3
  155. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +10 -3
  156. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  157. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  158. package/templates/.agents/rules/review-handshake.en.md +3 -1
  159. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  160. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  161. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  162. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  163. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  164. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  165. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  166. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  167. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  168. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  169. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  170. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  171. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  172. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  173. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  174. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -1
  175. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -1
  176. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  177. package/templates/.agents/workspace/README.en.md +0 -26
  178. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -151,7 +151,7 @@ function checkOrchestrationState({ taskDir }) {
151
151
  if (isFailClosedLegacyPause(run)) {
152
152
  return passResult('orchestration-state', 'Legacy orchestration run is safely paused');
153
153
  }
154
- if (run.schemaVersion !== 2 || !['paused', 'completed'].includes(run.status)) {
154
+ if (![2, 3].includes(run.schemaVersion) || !['paused', 'completed'].includes(run.status)) {
155
155
  return failResult('orchestration-state', `Expected paused or completed run, received '${run.status}'`);
156
156
  }
157
157
  if (run.status === 'paused' && (!run.pause?.code || !run.pause?.message)) {
@@ -261,7 +261,22 @@ function checkOrchestrationEvidence({ taskDir }) {
261
261
  && guards?.commitAuthorizationUnused === true
262
262
  && guards?.completionEvidenceAbsent === true
263
263
  && guards?.commitIntentAbsent === true;
264
- if (!validModelRecovery && !validCapabilityRecovery) {
264
+ const validClaudeCodeRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED_NO_MIGRATION'
265
+ && recovery.previousSchemaVersion === 3
266
+ && recovery.previousStatus === 'paused'
267
+ && recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
268
+ && recovery.client === 'claude-code'
269
+ && recovery.resultingStatus === 'running'
270
+ && exactText(recovery.recoveredAt)
271
+ && guards?.stepCount === 0
272
+ && guards?.nextStage === null
273
+ && guards?.baselineEmpty === true
274
+ && guards?.receiptCount === 0
275
+ && guards?.pendingDelegation === false
276
+ && guards?.commitAuthorizationUnused === true
277
+ && guards?.completionEvidenceAbsent === true
278
+ && guards?.commitIntentAbsent === true;
279
+ if (!validModelRecovery && !validCapabilityRecovery && !validClaudeCodeRecovery) {
265
280
  return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
266
281
  }
267
282
  }
@@ -292,26 +307,37 @@ function checkOrchestrationEvidence({ taskDir }) {
292
307
  }
293
308
  const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status);
294
309
  if (activated) {
295
- if (!exactText(receipt.actualModel)) {
296
- return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual model`);
310
+ const isClaudeCode = receipt.client === 'claude-code';
311
+ if (!isClaudeCode) {
312
+ if (!exactText(receipt.actualModel)) {
313
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual model`);
314
+ }
315
+ if (!exactText(receipt.actualReasoningEffort)) {
316
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual reasoning effort`);
317
+ }
297
318
  }
298
- if (!exactText(receipt.actualReasoningEffort)) {
299
- return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual reasoning effort`);
319
+ // parentId/childId 校验对所有 client 保持统一(不新增分支);spawnMode 检查按 client 判断
320
+ if (!exactText(receipt.parentId) || !exactText(receipt.childId) || receipt.parentId === receipt.childId) {
321
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid delegation identity`);
300
322
  }
301
- if (!exactText(receipt.parentId) || !exactText(receipt.childId) || receipt.parentId === receipt.childId || receipt.spawnMode !== 'fresh') {
323
+ if (!isClaudeCode && receipt.spawnMode !== 'fresh') {
302
324
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid fresh delegation identity`);
303
325
  }
304
- if (receipt.actualModel !== receipt.requestedModel && !exactText(receipt.modelFallbackReason)) {
305
- return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' model fallback is not justified`);
326
+ if (receipt.actualModel != null && receipt.actualModel !== receipt.requestedModel) {
327
+ if (!isClaudeCode && !exactText(receipt.modelFallbackReason)) {
328
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' model fallback is not justified`);
329
+ }
306
330
  }
307
- if (receipt.actualModel === receipt.requestedModel && receipt.modelFallbackReason !== null) {
331
+ if ((receipt.actualModel == null || receipt.actualModel === receipt.requestedModel)
332
+ && receipt.modelFallbackReason !== null) {
308
333
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated model fallback reason`);
309
334
  }
310
- if (receipt.actualReasoningEffort !== receipt.requestedReasoningEffort
311
- && !exactText(receipt.reasoningEffortFallbackReason)) {
312
- return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' reasoning-effort fallback is not justified`);
335
+ if (receipt.actualReasoningEffort != null && receipt.actualReasoningEffort !== receipt.requestedReasoningEffort) {
336
+ if (!isClaudeCode && !exactText(receipt.reasoningEffortFallbackReason)) {
337
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' reasoning-effort fallback is not justified`);
338
+ }
313
339
  }
314
- if (receipt.actualReasoningEffort === receipt.requestedReasoningEffort
340
+ if ((receipt.actualReasoningEffort == null || receipt.actualReasoningEffort === receipt.requestedReasoningEffort)
315
341
  && receipt.reasoningEffortFallbackReason !== null) {
316
342
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
317
343
  }
@@ -774,7 +800,7 @@ function checkReviewLedger({ taskDir, config }) {
774
800
  return passResult("review-ledger", "Disagreement ledger has no entries");
775
801
  }
776
802
  const stageScope = Array.isArray(config.stage_scope) ? config.stage_scope : null;
777
- const maxRounds = Number(resolveReviewSetting(config, "max_handshake_rounds", DEFAULT_MAX_HANDSHAKE_ROUNDS));
803
+ const maxRounds = Number(resolveReviewSetting(config, "maxHandshakeRounds", DEFAULT_MAX_HANDSHAKE_ROUNDS));
778
804
  const problems = [];
779
805
  let inScopeCount = 0;
780
806
  for (const cells of rows) {
@@ -916,7 +942,13 @@ function checkPostReviewCommit({ taskDir, config }) {
916
942
  return blockedResult("post-review-commit", relation.message, relation.code);
917
943
  }
918
944
  if (relation.status === "strict") {
919
- return failResult("post-review-commit", "Merged PR evidence did not establish squash merge equivalence", "PR_MERGE_IDENTITY_INVALID");
945
+ return resolvePostReviewIdentityFailure(task.content, {
946
+ code: "PR_MERGE_IDENTITY_INVALID",
947
+ message: "Merged PR evidence did not establish squash merge equivalence"
948
+ });
949
+ }
950
+ if (relation.code === "PR_MERGE_IDENTITY_INVALID") {
951
+ return resolvePostReviewIdentityFailure(task.content, relation);
920
952
  }
921
953
  return failResult("post-review-commit", relation.message, relation.code);
922
954
  }
@@ -981,21 +1013,47 @@ function checkPostReviewCommit({ taskDir, config }) {
981
1013
  }
982
1014
  const exemption = resolvePostReviewExemption(task.content);
983
1015
  if (!exemption.ok)
984
- return failResult("post-review-commit", exemption.message);
985
- if (exemption.exempt) {
986
- return passResult("post-review-commit", `${commits.length} post-review commit(s) covered by a human-decided exemption`);
1016
+ return failResult("post-review-commit", exemption.message, exemption.code);
1017
+ if (exemption.exemptions.length > 0) {
1018
+ return passResult("post-review-commit", `${commits.length} post-review commit(s) covered by a human-decided exemption: ${formatPostReviewExemptions(exemption.exemptions)}`);
987
1019
  }
988
1020
  return failResult("post-review-commit", `${commits.length} commit(s) to code/rule paths after reviewed commit ${sha.slice(0, 8)}; re-run review-code or record a human-decided exemption`);
989
1021
  }
1022
+ function resolvePostReviewIdentityFailure(content, failure) {
1023
+ const exemption = resolvePostReviewExemption(content);
1024
+ if (!exemption.ok) {
1025
+ return failResult("post-review-commit", exemption.message, exemption.code);
1026
+ }
1027
+ if (exemption.exemptions.length === 0) {
1028
+ return failResult("post-review-commit", failure.message, failure.code);
1029
+ }
1030
+ return passResult("post-review-commit", `Human-decided post-review exemption overrode ${failure.code}: ${failure.message}; ${formatPostReviewExemptions(exemption.exemptions)}`);
1031
+ }
1032
+ function formatPostReviewExemptions(exemptions) {
1033
+ return exemptions.map((row) => `${row.id}: ${row.evidence}`).join("; ");
1034
+ }
990
1035
  function resolvePostReviewExemption(content) {
991
1036
  try {
1037
+ const rows = parseLedger(content);
1038
+ const invalid = validateLedgerRows(rows);
1039
+ if (invalid) {
1040
+ return {
1041
+ ok: false,
1042
+ code: "POST_REVIEW_EXEMPTION_INVALID",
1043
+ message: `${invalid.code}: ${invalid.message}`
1044
+ };
1045
+ }
992
1046
  return {
993
1047
  ok: true,
994
- exempt: parseLedger(content).some((row) => row.stage === POST_REVIEW_COMMIT_STAGE && row.status === "human-decided")
1048
+ exemptions: rows.filter((row) => row.stage === POST_REVIEW_COMMIT_STAGE)
995
1049
  };
996
1050
  }
997
- catch {
998
- return { ok: false, message: "Invalid disagreement ledger" };
1051
+ catch (error) {
1052
+ return {
1053
+ ok: false,
1054
+ code: "POST_REVIEW_EXEMPTION_INVALID",
1055
+ message: `Invalid disagreement ledger: ${error instanceof Error ? error.message : String(error)}`
1056
+ };
999
1057
  }
1000
1058
  }
1001
1059
  function checkReviewFact({ taskDir, artifactFile }) {
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { verifyInProcess } from './verification-engine.js';
5
5
  import { parseArtifactName } from './artifact-lifecycle.js';
6
6
  import { resolveTaskRef } from './resolve-ref.js';
7
+ import { allowsManualOverride } from './guard-override.js';
7
8
  const gate = (skill, expectedState, artifactFamily) => ({
8
9
  skill, expectedState, mode: 'gate', ...(artifactFamily ? { artifactFamily } : {})
9
10
  });
@@ -47,7 +48,7 @@ function verifyTaskEvent(request, options = {}) {
47
48
  if (!resolved.ok)
48
49
  return failure(request, resolved.code, resolved.message, { taskId: resolved.taskId, skill: spec.skill, mode: spec.mode });
49
50
  const identity = { taskId: resolved.taskId, taskDir: resolved.taskDir, taskState: resolved.state, skill: spec.skill, mode: spec.mode };
50
- if (resolved.state !== spec.expectedState) {
51
+ if (resolved.state !== spec.expectedState && !allowsManualOverride(options.manualOverride, 'task-verify', 'VERIFY_TASK_STATE_MISMATCH')) {
51
52
  return failure(request, 'VERIFY_TASK_STATE_MISMATCH', `event '${request.event}' requires workspace '${spec.expectedState}', received '${resolved.state}'`, identity);
52
53
  }
53
54
  if (spec.artifactFamily) {
@@ -66,12 +67,23 @@ function verifyTaskEvent(request, options = {}) {
66
67
  if (spec.mode === 'gate') {
67
68
  const payload = engine({ mode: 'gate', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [], repositoryRoot: resolved.repoRoot });
68
69
  const status = payload.gate;
70
+ if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
71
+ invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, gate: 'pass', humanOverride: 'human-approved', originalGate: status } });
72
+ return {
73
+ status: 'pass', changed: false, event: request.event, requestRef: request.taskRef,
74
+ ...identity, artifact: request.artifact ?? null, invocations, error: null
75
+ };
76
+ }
69
77
  invocations.push({ status, exitCode: { pass: 0, fail: 1, blocked: 2 }[status], payload });
70
78
  }
71
79
  else {
72
80
  for (const check of spec.checks ?? []) {
73
81
  const payload = engine({ mode: 'checks', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [check], repositoryRoot: resolved.repoRoot });
74
82
  const status = payload.status;
83
+ if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
84
+ invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, status: 'pass', humanOverride: 'human-approved', originalStatus: status } });
85
+ break;
86
+ }
75
87
  invocations.push({ status, exitCode: { pass: 0, fail: 1, blocked: 2 }[status], payload });
76
88
  if (status !== 'pass')
77
89
  break;
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import { resolveTaskRef } from './resolve-ref.js';
3
3
  import { WORKFLOW_WARNING_COLUMNS, WORKFLOW_WARNING_HEADINGS, WORKFLOW_WARNING_SEVERITIES, WORKFLOW_WARNING_STATUSES, parseWorkflowWarnings } from './workflow-warnings.js';
4
4
  import { captureTaskWriteMetadata, writeTask } from './write.js';
5
+ import { allowsManualOverride } from './guard-override.js';
5
6
  function failed(intent, code, message, taskId = null, entityId = null) {
6
7
  return { status: 'failed', changed: false, intent: intent.kind, taskId, entityId, before: null, after: null, warnings: [], operations: [], error: { code, message } };
7
8
  }
@@ -54,7 +55,8 @@ function applyWorkflowWarningIntent(intent, options = {}) {
54
55
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
55
56
  if (!resolved.ok)
56
57
  return failed(intent, resolved.code, resolved.message, resolved.taskId);
57
- if (resolved.state !== 'active')
58
+ const stateOverride = allowsManualOverride(options.manualOverride, 'workflow-warning', 'TASK_STATE_MISMATCH');
59
+ if (resolved.state !== 'active' && !stateOverride)
58
60
  return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
59
61
  let content;
60
62
  let rows;
@@ -114,7 +116,7 @@ function applyWorkflowWarningIntent(intent, options = {}) {
114
116
  after = { ...before, status: intent.status, resolvedAt: metadata.timestamp, resolution };
115
117
  }
116
118
  const writeResult = writeTask({
117
- taskRef: intent.taskRef, expectedState: 'active', mutations: [...sectionMutation(content), rowMutation(after)], dryRun: intent.dryRun
119
+ taskRef: intent.taskRef, expectedState: stateOverride ? resolved.state : 'active', mutations: [...sectionMutation(content), rowMutation(after)], dryRun: intent.dryRun
118
120
  }, {
119
121
  ...options,
120
122
  taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state },
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { VERSION } from '../version.js';
4
4
  import { parseTypedTaskFrontmatter, updateTaskFrontmatter } from './frontmatter.js';
5
5
  import { resolveTaskRef } from './resolve-ref.js';
6
+ import { allowsManualOverride } from './guard-override.js';
6
7
  import { mutateTableRow, upsertSection } from './sections.js';
7
8
  const DEFAULT_FILE_SYSTEM = {
8
9
  readFileSync: (file) => fs.readFileSync(file, 'utf8'),
@@ -70,7 +71,7 @@ function writeTask(request, options = {}) {
70
71
  taskMdPath: resolved.taskMdPath,
71
72
  actualState: resolved.state
72
73
  };
73
- if (resolved.state !== request.expectedState) {
74
+ if (resolved.state !== request.expectedState && !allowsManualOverride(options.manualOverride, 'task.write', 'TASK_STATE_MISMATCH')) {
74
75
  return failure(request, identity, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected ${request.expectedState}`);
75
76
  }
76
77
  const io = { ...DEFAULT_FILE_SYSTEM, ...options.fileSystem };
@@ -0,0 +1,6 @@
1
+ {
2
+ "protocolVersion": 3,
3
+ "packageVersion": "0.9.8",
4
+ "sourceInputHash": "fb481e6579519b3fd10c9ca9bf284d65498f935c034a14c047d023345abeb87c",
5
+ "internalExecutableBuildHash": "d8dcdf602b61001ba9829aa2027cccadcc9c371ccf9e29c3d681fd02d5a3a5af"
6
+ }
@@ -16,7 +16,7 @@ const claudeCodeAdapter = defineAgentClientAdapter({
16
16
  commands: { level: 'integrated' },
17
17
  hooks: { level: 'integrated' },
18
18
  subagents: { level: 'experimental' },
19
- orchestration: { level: 'unsupported' },
19
+ orchestration: { level: 'experimental' },
20
20
  sandbox: { level: 'integrated' },
21
21
  verification: { level: 'compatible' }
22
22
  },
@@ -26,8 +26,8 @@ const claudeCodeAdapter = defineAgentClientAdapter({
26
26
  guidance: 'Use the host model picker for the complete model and reasoning-effort catalog.'
27
27
  },
28
28
  delegationEvidence: {
29
- actualModel: 'unavailable',
30
- actualReasoningEffort: 'unavailable'
29
+ actualModel: 'host-event',
30
+ actualReasoningEffort: 'spawn-ack'
31
31
  },
32
32
  project: {
33
33
  ownedPathPrefixes: ['.claude/'],
@@ -7,6 +7,7 @@ import semver from 'semver';
7
7
 
8
8
  import type { ChildProcessWithoutNullStreams } from 'node:child_process';
9
9
  import type { CodexLifecycleEvent } from './evidence.ts';
10
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.ts';
10
11
 
11
12
  type JsonObject = Record<string, unknown>;
12
13
  type ThreadResolution = Readonly<{
@@ -19,6 +20,12 @@ type DiscoveredHook = Readonly<{
19
20
  matcher: string | null;
20
21
  command: string;
21
22
  enabled: boolean;
23
+ source: string;
24
+ sourcePathDigest: string;
25
+ trustStatus: string;
26
+ currentHash: string;
27
+ isManaged: boolean;
28
+ pluginId: string | null;
22
29
  }>;
23
30
 
24
31
  type AppServerTransportOptions = Readonly<{
@@ -134,7 +141,11 @@ function validateCodexLifecycleHookConfig(value: unknown): void {
134
141
  if (!valid) throw new Error('CODEX_PREFLIGHT_HOOKS_INVALID: lifecycle hooks are invalid');
135
142
  }
136
143
 
137
- function parseCodexHooksList(value: unknown, repoRoot: string): readonly DiscoveredHook[] {
144
+ function parseCodexHooksList(
145
+ value: unknown,
146
+ repoRoot: string,
147
+ expectedSource: 'direct-host' | 'isolated-user' = 'direct-host'
148
+ ): readonly DiscoveredHook[] {
138
149
  const root = object(value);
139
150
  const entries = Array.isArray(root?.data) ? root.data : [];
140
151
  const entry = entries.map(object).find((candidate) => candidate?.cwd === repoRoot);
@@ -147,13 +158,28 @@ function parseCodexHooksList(value: unknown, repoRoot: string): readonly Discove
147
158
  eventName: String(hook.eventName ?? ''),
148
159
  matcher: typeof hook.matcher === 'string' ? hook.matcher : null,
149
160
  command: String(hook.command ?? ''),
150
- enabled: hook.enabled === true
161
+ enabled: hook.enabled === true,
162
+ source: String(hook.source ?? ''),
163
+ sourcePathDigest: typeof hook.sourcePath === 'string' && path.isAbsolute(hook.sourcePath)
164
+ ? crypto.createHash('sha256').update(hook.sourcePath).digest('hex')
165
+ : '',
166
+ trustStatus: String(hook.trustStatus ?? ''),
167
+ currentHash: String(hook.currentHash ?? ''),
168
+ isManaged: hook.isManaged === true,
169
+ pluginId: typeof hook.pluginId === 'string' ? hook.pluginId : null
151
170
  }));
152
171
  const valid = LIFECYCLE_HOOKS.every(({ event, matcher, phase }) => hooks.some((hook) =>
153
172
  hook.eventName === event.replace(/^./, (character) => character.toLowerCase())
154
173
  && hook.matcher === matcher
155
174
  && hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`
156
175
  && hook.enabled
176
+ && hook.currentHash.length > 0
177
+ && hook.sourcePathDigest.length === 64
178
+ && hook.pluginId === null
179
+ && (expectedSource === 'isolated-user'
180
+ ? hook.source === 'user' && hook.trustStatus !== 'modified'
181
+ : (hook.source === 'project' && hook.trustStatus === 'trusted')
182
+ || hook.isManaged && hook.trustStatus === 'managed')
157
183
  ));
158
184
  if (!valid) throw new Error('CODEX_PREFLIGHT_HOOKS_NOT_LOADED: lifecycle hooks are not loaded for this workspace');
159
185
  return Object.freeze(hooks);
@@ -533,7 +559,7 @@ async function preflightCodexLifecycleEvidence(
533
559
  }
534
560
 
535
561
  const hookDefinitionHash = crypto.createHash('sha256').update(hooksRaw).digest('hex');
536
- const runtimeRoot = path.join(repoRoot, '.agents', 'workspace', '.runtime', 'codex-lifecycle');
562
+ const runtimeRoot = resolveAgentRuntimeStoreRoot({ repoRoot, store: 'lifecycle' });
537
563
  const runtimeLiveness = hasCodexRuntimeLiveness(runtimeRoot, hookDefinitionHash, runtimeIdentity);
538
564
 
539
565
  const transport = new CodexAppServerTransport();
@@ -541,13 +567,45 @@ async function preflightCodexLifecycleEvidence(
541
567
  let discoveredHooks: readonly DiscoveredHook[];
542
568
  try {
543
569
  await transport.initialize();
544
- discoveredHooks = parseCodexHooksList(await transport.request('hooks/list', { cwds: [repoRoot] }), repoRoot);
570
+ discoveredHooks = parseCodexHooksList(
571
+ await transport.request('hooks/list', { cwds: [repoRoot] }),
572
+ repoRoot,
573
+ process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT ? 'isolated-user' : 'direct-host'
574
+ );
545
575
  } finally { transport.close(); }
576
+ const lifecycleHooks = LIFECYCLE_HOOKS.map(({ event, matcher, phase }) => discoveredHooks.filter((hook) =>
577
+ hook.eventName === event.replace(/^./, (character) => character.toLowerCase())
578
+ && hook.matcher === matcher
579
+ && hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`
580
+ ));
581
+ if (lifecycleHooks.some((matches) => matches.length !== 1)) {
582
+ throw new Error('CODEX_PREFLIGHT_HOOK_SOURCE_AMBIGUOUS: lifecycle hook source is not unique');
583
+ }
584
+ const matchedHooks = lifecycleHooks.flat();
585
+ const sources = new Set(matchedHooks.map((hook) => hook.isManaged ? 'managed' : hook.source));
586
+ const sourcePathDigests = new Set(matchedHooks.map((hook) => hook.sourcePathDigest));
587
+ if (sources.size !== 1 || sourcePathDigests.size !== 1) {
588
+ throw new Error('CODEX_PREFLIGHT_HOOK_SOURCE_AMBIGUOUS: lifecycle hooks do not share one exact source');
589
+ }
590
+ const source = [...sources][0];
591
+ const hookSource = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT
592
+ ? 'isolated-user'
593
+ : source === 'managed' ? 'managed' : 'project';
546
594
  return Object.freeze({
547
595
  cliVersion: match[1],
548
596
  hookDefinitionHash,
549
597
  staticReady: true,
550
598
  discoveredHooks,
599
+ hookProvenance: Object.freeze({
600
+ hookSource,
601
+ hookSourcePathDigest: [...sourcePathDigests][0]!,
602
+ hookSourceHash: crypto.createHash('sha256').update(JSON.stringify(matchedHooks.map((hook) => ({
603
+ eventName: hook.eventName,
604
+ matcher: hook.matcher,
605
+ command: hook.command,
606
+ currentHash: hook.currentHash
607
+ })))).digest('hex')
608
+ }),
551
609
  runtimeLiveness,
552
610
  diagnostics: transport.diagnostics
553
611
  });
@@ -0,0 +1,227 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const LIFECYCLE_PROTOCOL_VERSION = 3;
7
+
8
+ type LifecycleBuildIdentity = Readonly<{
9
+ protocolVersion: 3;
10
+ packageVersion: string;
11
+ internalExecutableBuildHash: string;
12
+ lifecycleContractHash: string;
13
+ }>;
14
+
15
+ type LifecycleBuildIdentityOptions = Readonly<{
16
+ executableFiles?: readonly string[];
17
+ contractFiles?: readonly string[];
18
+ }>;
19
+
20
+ type LifecycleManifestFiles = Readonly<{
21
+ executableFiles: readonly string[];
22
+ contractFiles: readonly string[];
23
+ }>;
24
+
25
+ type IdentityVerification = Readonly<{
26
+ ok: boolean;
27
+ code: string | null;
28
+ message: string | null;
29
+ }>;
30
+
31
+ function exactText(value: unknown): value is string {
32
+ return typeof value === 'string' && value.trim() === value && value.length > 0;
33
+ }
34
+
35
+ function normalizeRelativePath(value: string): string {
36
+ return value.replaceAll('\\', '/');
37
+ }
38
+
39
+ function readLifecycleManifestFiles(packageRoot: string): LifecycleManifestFiles {
40
+ const file = path.join(
41
+ packageRoot,
42
+ 'lib',
43
+ 'agent-clients',
44
+ 'adapters',
45
+ 'codex-lifecycle',
46
+ 'manifest-files.json'
47
+ );
48
+ const value = JSON.parse(fs.readFileSync(file, 'utf8')) as {
49
+ executableFiles?: unknown;
50
+ contractFiles?: unknown;
51
+ };
52
+ if (!Array.isArray(value.executableFiles)
53
+ || !Array.isArray(value.contractFiles)
54
+ || !value.executableFiles.every(exactText)
55
+ || !value.contractFiles.every(exactText)
56
+ || new Set(value.executableFiles).size !== value.executableFiles.length
57
+ || new Set(value.contractFiles).size !== value.contractFiles.length) {
58
+ throw new Error('Lifecycle manifest file list is invalid');
59
+ }
60
+ return Object.freeze({
61
+ executableFiles: Object.freeze([...value.executableFiles] as string[]),
62
+ contractFiles: Object.freeze([...value.contractFiles] as string[])
63
+ });
64
+ }
65
+
66
+ function executablePackageRoot(): string {
67
+ let current = path.dirname(fileURLToPath(import.meta.url));
68
+ while (true) {
69
+ const candidate = path.join(current, 'package.json');
70
+ if (fs.existsSync(candidate)) {
71
+ const value = JSON.parse(fs.readFileSync(candidate, 'utf8')) as { name?: unknown };
72
+ if (value.name === '@fitlab-ai/agent-infra') return current;
73
+ }
74
+ const parent = path.dirname(current);
75
+ if (parent === current) throw new Error('Lifecycle executable package root is unavailable');
76
+ current = parent;
77
+ }
78
+ }
79
+
80
+ function hashFiles(root: string, files: readonly string[]): string {
81
+ const hash = crypto.createHash('sha256');
82
+ for (const relative of [...new Set(files.map(normalizeRelativePath))].sort()) {
83
+ if (path.isAbsolute(relative) || relative.split(/[\\/]/u).includes('..')) {
84
+ throw new Error(`Lifecycle manifest path '${relative}' is unsafe`);
85
+ }
86
+ const file = path.join(root, relative);
87
+ const stat = fs.lstatSync(file);
88
+ if (!stat.isFile() || stat.isSymbolicLink()) {
89
+ throw new Error(`Lifecycle manifest entry '${relative}' must be a regular file`);
90
+ }
91
+ hash.update(relative);
92
+ hash.update('\0');
93
+ hash.update(fs.readFileSync(file));
94
+ hash.update('\0');
95
+ }
96
+ return hash.digest('hex');
97
+ }
98
+
99
+ function resolveLifecycleExecutableFiles(root: string, entries: readonly string[]): string[] {
100
+ const pending = [...entries];
101
+ const resolved = new Set<string>();
102
+ while (pending.length > 0) {
103
+ const relative = normalizeRelativePath(pending.pop()!);
104
+ if (resolved.has(relative)) continue;
105
+ resolved.add(relative);
106
+ if (!/\.[cm]?[jt]s$/u.test(relative)) continue;
107
+ const file = path.join(root, relative);
108
+ const source = fs.readFileSync(file, 'utf8');
109
+ const imports = source.matchAll(/(?:\bfrom\s*|\bimport\s*(?:\(\s*)?)["'](\.[^"']+)["']/gu);
110
+ for (const match of imports) {
111
+ const requested = match[1]!;
112
+ const base = path.resolve(path.dirname(file), requested);
113
+ const candidates = [
114
+ base,
115
+ `${base}.ts`, `${base}.js`,
116
+ path.join(base, 'index.ts'), path.join(base, 'index.js')
117
+ ];
118
+ const dependency = candidates.find((candidate) => fs.existsSync(candidate) && fs.lstatSync(candidate).isFile());
119
+ if (!dependency) throw new Error(`Lifecycle executable dependency '${requested}' from '${relative}' is unavailable`);
120
+ const dependencyRelative = path.relative(root, dependency);
121
+ if (dependencyRelative.startsWith('..') || path.isAbsolute(dependencyRelative)) {
122
+ throw new Error(`Lifecycle executable dependency '${requested}' escapes the package root`);
123
+ }
124
+ pending.push(normalizeRelativePath(dependencyRelative));
125
+ }
126
+ }
127
+ return [...resolved].sort();
128
+ }
129
+
130
+ function computeLifecycleBuildIdentity(
131
+ repoRoot: string,
132
+ options: LifecycleBuildIdentityOptions = {}
133
+ ): LifecycleBuildIdentity {
134
+ const packageRoot = executablePackageRoot();
135
+ const executableRoot = options.executableFiles ? repoRoot : packageRoot;
136
+ const defaults = readLifecycleManifestFiles(packageRoot);
137
+ const packageJson = JSON.parse(fs.readFileSync(path.join(executableRoot, 'package.json'), 'utf8')) as {
138
+ version?: unknown;
139
+ };
140
+ if (!exactText(packageJson.version)) throw new Error('Lifecycle package version is invalid');
141
+ let internalExecutableBuildHash: string;
142
+ if (options.executableFiles) {
143
+ internalExecutableBuildHash = hashFiles(repoRoot, options.executableFiles);
144
+ } else {
145
+ const manifestFile = path.join(executableRoot, 'dist', 'lifecycle-build-manifest.json');
146
+ if (fs.existsSync(manifestFile)) {
147
+ const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8')) as {
148
+ protocolVersion?: unknown;
149
+ packageVersion?: unknown;
150
+ sourceInputHash?: unknown;
151
+ internalExecutableBuildHash?: unknown;
152
+ };
153
+ if (manifest.protocolVersion !== LIFECYCLE_PROTOCOL_VERSION
154
+ || manifest.packageVersion !== packageJson.version
155
+ || !exactText(manifest.sourceInputHash)
156
+ || !exactText(manifest.internalExecutableBuildHash)) {
157
+ throw new Error('Lifecycle executable build manifest is stale or invalid');
158
+ }
159
+ if (fileURLToPath(import.meta.url).split(path.sep).includes('dist')) {
160
+ const compiledFiles = defaults.executableFiles.map((file) =>
161
+ file.endsWith('.ts') ? path.join('dist', file.replace(/\.ts$/u, '.js')) : file
162
+ );
163
+ if (hashFiles(executableRoot, resolveLifecycleExecutableFiles(executableRoot, compiledFiles)) !== manifest.internalExecutableBuildHash) {
164
+ throw new Error('Lifecycle compiled executable build does not match its manifest');
165
+ }
166
+ } else if (hashFiles(executableRoot, resolveLifecycleExecutableFiles(executableRoot, defaults.executableFiles)) !== manifest.sourceInputHash) {
167
+ throw new Error('Lifecycle executable build manifest is stale or invalid');
168
+ }
169
+ internalExecutableBuildHash = manifest.internalExecutableBuildHash;
170
+ } else internalExecutableBuildHash = hashFiles(
171
+ executableRoot,
172
+ resolveLifecycleExecutableFiles(executableRoot, defaults.executableFiles)
173
+ );
174
+ }
175
+ return Object.freeze({
176
+ protocolVersion: LIFECYCLE_PROTOCOL_VERSION,
177
+ packageVersion: packageJson.version,
178
+ internalExecutableBuildHash,
179
+ lifecycleContractHash: hashFiles(
180
+ repoRoot,
181
+ options.contractFiles ?? defaults.contractFiles
182
+ )
183
+ });
184
+ }
185
+
186
+ function verifyLifecycleBuildIdentity(
187
+ expected: LifecycleBuildIdentity,
188
+ actual: LifecycleBuildIdentity
189
+ ): IdentityVerification {
190
+ if (actual.protocolVersion !== expected.protocolVersion) {
191
+ return {
192
+ ok: false,
193
+ code: 'CODEX_LIFECYCLE_PROTOCOL_MISMATCH',
194
+ message: 'Codex lifecycle protocol version does not match'
195
+ };
196
+ }
197
+ if (actual.packageVersion !== expected.packageVersion
198
+ || actual.internalExecutableBuildHash !== expected.internalExecutableBuildHash) {
199
+ return {
200
+ ok: false,
201
+ code: 'CODEX_LIFECYCLE_BUILD_MISMATCH',
202
+ message: 'Codex lifecycle executable build identity does not match'
203
+ };
204
+ }
205
+ if (actual.lifecycleContractHash !== expected.lifecycleContractHash) {
206
+ return {
207
+ ok: false,
208
+ code: 'CODEX_LIFECYCLE_CONTRACT_MISMATCH',
209
+ message: 'Codex lifecycle contract identity does not match'
210
+ };
211
+ }
212
+ return { ok: true, code: null, message: null };
213
+ }
214
+
215
+ export {
216
+ LIFECYCLE_PROTOCOL_VERSION,
217
+ computeLifecycleBuildIdentity,
218
+ readLifecycleManifestFiles,
219
+ resolveLifecycleExecutableFiles,
220
+ verifyLifecycleBuildIdentity
221
+ };
222
+ export type {
223
+ IdentityVerification,
224
+ LifecycleBuildIdentity,
225
+ LifecycleManifestFiles,
226
+ LifecycleBuildIdentityOptions
227
+ };