@fitlab-ai/agent-infra 0.9.15 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +6 -2
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
  3. package/dist/lib/fs/durable-write.js +2 -0
  4. package/dist/lib/internal/cli-route-inventory.js +1 -1
  5. package/dist/lib/internal/manual-validation.js +17 -8
  6. package/dist/lib/internal/task-artifact.js +1 -1
  7. package/dist/lib/internal/task-lifecycle.js +2 -2
  8. package/dist/lib/internal/task-operation-registry.js +0 -1
  9. package/dist/lib/internal/task-review.js +4 -4
  10. package/dist/lib/platform/issue-comments.js +16 -10
  11. package/dist/lib/platform/issues.js +1 -1
  12. package/dist/lib/platform/pr-change-report.js +5 -4
  13. package/dist/lib/platform/pr-summary.js +64 -32
  14. package/dist/lib/platform/pull-requests.js +3 -3
  15. package/dist/lib/platform/task-identities.js +2 -9
  16. package/dist/lib/platform/verification-sync.js +6 -2
  17. package/dist/lib/pr-review/evidence-grading.js +4 -8
  18. package/dist/lib/sandbox/commands/rm.js +21 -2
  19. package/dist/lib/sandbox/control/server.js +29 -9
  20. package/dist/lib/sandbox/control/state.js +7 -1
  21. package/dist/lib/sandbox/control/workflow-executor.js +4 -14
  22. package/dist/lib/sandbox/index.js +1 -2
  23. package/dist/lib/sandbox/removal.js +116 -1011
  24. package/dist/lib/sandbox/workspace-identity.js +17 -1
  25. package/dist/lib/task/artifact-command.js +6 -28
  26. package/dist/lib/task/artifact-operations.js +16 -230
  27. package/dist/lib/task/artifact-recovery.js +430 -0
  28. package/dist/lib/task/artifact-repair-intent.js +73 -39
  29. package/dist/lib/task/commit-operation.js +31 -4
  30. package/dist/lib/task/control-authority.js +38 -7
  31. package/dist/lib/task/control-recovery.js +14 -2
  32. package/dist/lib/task/create-service.js +1 -8
  33. package/dist/lib/task/delegation-receipts.js +64 -3
  34. package/dist/lib/task/events.js +164 -22
  35. package/dist/lib/task/human-override.js +5 -8
  36. package/dist/lib/task/intermediate-cleanup.js +66 -8
  37. package/dist/lib/task/lifecycle-recovery.js +549 -0
  38. package/dist/lib/task/lifecycle.js +12 -3
  39. package/dist/lib/task/local-artifact-finalization.js +148 -123
  40. package/dist/lib/task/manual-validation-receipt.js +5 -1
  41. package/dist/lib/task/orchestration.js +52 -2
  42. package/dist/lib/task/pr-delivery-fact.js +12 -56
  43. package/dist/lib/task/recovery-warning.js +19 -0
  44. package/dist/lib/task/review-command.js +1 -0
  45. package/dist/lib/task/review-finalization.js +104 -77
  46. package/dist/lib/task/short-id.js +15 -7
  47. package/dist/lib/task/task-execution-lock.js +1 -1
  48. package/dist/lib/task/verification-engine.js +3 -14
  49. package/dist/lib/task/workflow-command.js +0 -1
  50. package/dist/lib/version.js +1 -11
  51. package/dist/lifecycle-build-manifest.json +3 -3
  52. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +7 -2
  53. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
  54. package/lib/fs/durable-write.ts +1 -0
  55. package/lib/internal/cli-route-inventory.ts +1 -1
  56. package/lib/internal/manual-validation.ts +17 -8
  57. package/lib/internal/task-artifact.ts +1 -1
  58. package/lib/internal/task-lifecycle.ts +4 -3
  59. package/lib/internal/task-operation-registry.ts +0 -1
  60. package/lib/internal/task-review.ts +4 -4
  61. package/lib/platform/issue-comments.ts +18 -10
  62. package/lib/platform/issues.ts +1 -1
  63. package/lib/platform/pr-change-report.ts +7 -5
  64. package/lib/platform/pr-summary.ts +64 -32
  65. package/lib/platform/pull-requests.ts +3 -3
  66. package/lib/platform/task-identities.ts +3 -14
  67. package/lib/platform/verification-sync.ts +6 -2
  68. package/lib/pr-review/evidence-grading.ts +4 -9
  69. package/lib/sandbox/commands/rm.ts +34 -2
  70. package/lib/sandbox/control/protocol.ts +5 -0
  71. package/lib/sandbox/control/server.ts +30 -10
  72. package/lib/sandbox/control/state.ts +9 -1
  73. package/lib/sandbox/control/workflow-executor.ts +4 -14
  74. package/lib/sandbox/index.ts +1 -2
  75. package/lib/sandbox/removal.ts +126 -1128
  76. package/lib/sandbox/workspace-identity.ts +19 -3
  77. package/lib/task/artifact-command.ts +8 -27
  78. package/lib/task/artifact-operations.ts +21 -251
  79. package/lib/task/artifact-recovery.ts +519 -0
  80. package/lib/task/artifact-repair-intent.ts +99 -51
  81. package/lib/task/commit-operation.ts +35 -4
  82. package/lib/task/control-authority.ts +47 -14
  83. package/lib/task/control-recovery.ts +13 -1
  84. package/lib/task/create-service.ts +1 -10
  85. package/lib/task/delegation-receipts.ts +73 -2
  86. package/lib/task/events.ts +187 -23
  87. package/lib/task/human-override.ts +5 -8
  88. package/lib/task/intermediate-cleanup.ts +50 -12
  89. package/lib/task/lifecycle-recovery.ts +691 -0
  90. package/lib/task/lifecycle.ts +13 -4
  91. package/lib/task/local-artifact-finalization.ts +176 -132
  92. package/lib/task/manual-validation-receipt.ts +5 -1
  93. package/lib/task/orchestration.ts +65 -0
  94. package/lib/task/pr-delivery-fact.ts +15 -62
  95. package/lib/task/recovery-warning.ts +21 -0
  96. package/lib/task/review-command.ts +1 -0
  97. package/lib/task/review-finalization.ts +106 -87
  98. package/lib/task/short-id.ts +27 -5
  99. package/lib/task/task-execution-lock.ts +1 -1
  100. package/lib/task/verification-engine.ts +3 -17
  101. package/lib/task/verification-types.ts +0 -2
  102. package/lib/task/workflow-command.ts +0 -1
  103. package/lib/version.ts +1 -15
  104. package/package.json +1 -1
  105. package/templates/.agents/rules/create-issue.en.md +1 -1
  106. package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
  107. package/templates/.agents/rules/issue-sync.en.md +4 -2
  108. package/templates/.agents/rules/issue-sync.zh-CN.md +4 -3
  109. package/templates/.agents/rules/local-artifact-repair.en.md +46 -39
  110. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +43 -36
  111. package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
  112. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
  113. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
  114. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
  115. package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
  116. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  117. package/templates/.agents/skills/block-task/config/verify.json +1 -1
  118. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  119. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  120. package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
  121. package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
  122. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +7 -7
  123. package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
  124. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
  125. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
  126. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
  127. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  128. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  129. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  130. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  131. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  132. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  133. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  134. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  135. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
  136. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
  137. package/templates/.agents/skills/create-pr/config/verify.json +1 -1
  138. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
  139. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
  140. package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
  141. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
  142. package/templates/.agents/skills/create-task/config/verify.json +1 -1
  143. package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
  144. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
  145. package/templates/.agents/skills/import-issue/config/verify.json +2 -2
  146. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
  147. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
  148. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  149. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  150. package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
  151. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
  152. package/templates/.agents/skills/restore-task/config/verify.json +1 -1
  153. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  154. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  155. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
  156. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
  157. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
  158. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
  159. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  160. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  161. package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
  162. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
  163. package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
  164. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
  165. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  166. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  167. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
  168. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
  169. package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
  170. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
  171. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
  172. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
  173. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
  174. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
  175. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
  176. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
  177. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  178. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  179. package/templates/.agents/templates/task.en.md +1 -1
  180. package/templates/.agents/templates/task.zh-CN.md +1 -1
  181. package/templates/.claude/commands/import-issue.en.md +1 -1
  182. package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
  183. package/templates/.claude/commands/restore-task.en.md +1 -1
  184. package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
@@ -1,6 +1,4 @@
1
1
  import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { randomUUID } from 'node:crypto';
4
2
  import { parseArtifactName } from './artifact-name.js';
5
3
  import { validateCompletedArtifact, hasOpenArtifactRound, validateArtifactPublication } from './artifact-lifecycle.js';
6
4
  import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
@@ -12,18 +10,15 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
12
10
  import { allowsManualOverride } from './guard-override.js';
13
11
  import { getArtifactSchema } from './artifact-schema.js';
14
12
  import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.js';
15
- import { readArtifactRepairIntent, writeArtifactRepairIntent } from './artifact-repair-intent.js';
13
+ import { readArtifactRecoveryIntent } from './artifact-repair-intent.js';
14
+ import { beginArtifactRecovery, commitArtifactRecovery, prepareArtifactRecoveryCommit, recoveryContextFromIntent, stageArtifactCandidate } from './artifact-recovery.js';
16
15
  const STAGES = {
17
16
  analysis: { family: 'review-analysis' },
18
17
  plan: { family: 'review-plan' },
19
18
  code: { family: 'review-code' }
20
19
  };
21
20
  const DEFAULT_FILE_SYSTEM = {
22
- readFileSync: (file) => fs.readFileSync(file, 'utf8'),
23
- statModeSync: (file) => fs.statSync(file).mode,
24
- writeFileSync: (file, content, mode) => fs.writeFileSync(file, content, { encoding: 'utf8', flag: 'wx', mode }),
25
- renameSync: (from, to) => fs.renameSync(from, to),
26
- unlinkSync: (file) => fs.unlinkSync(file)
21
+ readFileSync: (file) => fs.readFileSync(file, 'utf8')
27
22
  };
28
23
  function failed(request, code, message, taskId = null, stageStatus = null, extra = {}) {
29
24
  return {
@@ -37,21 +32,11 @@ function failed(request, code, message, taskId = null, stageStatus = null, extra
37
32
  stageStatus,
38
33
  artifactSha256: null,
39
34
  semanticDigest: null,
40
- repairable: false,
41
- operation: null,
42
35
  operations: [],
43
36
  error: { code, message },
44
37
  ...extra
45
38
  };
46
39
  }
47
- function cleanupTemp(fileSystem, tempPath) {
48
- try {
49
- fileSystem.unlinkSync(tempPath);
50
- }
51
- catch {
52
- // Best-effort cleanup preserves the primary failure.
53
- }
54
- }
55
40
  /** Prepare domain validation, summary bytes and provenance before any publication. */
56
41
  function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
57
42
  let taskId = null;
@@ -85,43 +70,37 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
85
70
  catch (error) {
86
71
  return reject('REVIEW_ARTIFACT_NOT_REGULAR', String(error));
87
72
  }
73
+ let recovery;
74
+ if (request.recoveryId) {
75
+ try {
76
+ const intent = readArtifactRecoveryIntent(resolved.repoRoot, taskId, spec.family, request.artifact);
77
+ if (!intent || intent.recoveryOperationId !== request.recoveryId)
78
+ return reject('REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal');
79
+ if (!['awaiting-recovery', 'finalize-ready', 'commit-started'].includes(intent.state))
80
+ return reject('REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`);
81
+ recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
82
+ artifactContent = fs.existsSync(recovery.stagingPath)
83
+ ? fs.readFileSync(recovery.stagingPath, 'utf8')
84
+ : fs.readFileSync(recovery.formalPath, 'utf8');
85
+ }
86
+ catch (error) {
87
+ return reject('REVIEW_PROVENANCE_INVALID', String(error));
88
+ }
89
+ }
88
90
  const artifactSha256 = sha256Content(artifactContent);
89
91
  const semanticDigest = canonicalSemanticDigest(artifactContent);
90
92
  const digests = { artifactSha256, semanticDigest };
91
93
  let repairIntent;
92
94
  try {
93
- repairIntent = readArtifactRepairIntent(resolved.repoRoot, taskId, spec.family, request.artifact);
95
+ repairIntent = readArtifactRecoveryIntent(resolved.repoRoot, taskId, spec.family, request.artifact);
94
96
  }
95
97
  catch (error) {
96
98
  return reject('REVIEW_PROVENANCE_INVALID', String(error), digests);
97
99
  }
98
- const schema = getArtifactSchema(spec.family);
99
- const structure = inspectArtifactContract(artifactContent, schema);
100
- if (!structure.ok) {
101
- const repairable = structure.repair !== null && structure.diagnostics.length === 1 && structure.diagnostics[0]?.repairable === true;
102
- if (repairable && repairIntent && (repairIntent.state !== 'awaiting-repair'
103
- || repairIntent.artifactSha256 !== artifactSha256 || repairIntent.semanticDigest !== structure.semanticDigest)) {
104
- return reject('REVIEW_PROVENANCE_INVALID', 'a different review repair baseline is already recorded for this artifact', digests);
100
+ if (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed') {
101
+ if (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest) {
102
+ return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
105
103
  }
106
- return {
107
- ...reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
108
- artifactSha256, semanticDigest: structure.semanticDigest, repairable, operation: structure.repair
109
- }),
110
- ...(repairable && !repairIntent ? { provenance: {
111
- version: 2, taskId: taskId, family: spec.family, artifact: request.artifact,
112
- state: 'awaiting-repair', baselineSemanticDigest: structure.semanticDigest,
113
- artifactSha256, semanticDigest: structure.semanticDigest,
114
- recoveryOperationId: null, phase: null, authorityDigest: null,
115
- requestId: `review-finalize:${taskId}`, createdAt: Date.now(), updatedAt: Date.now()
116
- }, expectedIntent: null } : {})
117
- };
118
- }
119
- if (repairIntent?.state === 'awaiting-repair' && repairIntent.baselineSemanticDigest !== semanticDigest) {
120
- return reject('REVIEW_PROVENANCE_INVALID', 'the repaired review artifact semantic digest does not match the recorded repair baseline', digests);
121
- }
122
- if ((repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')
123
- && (repairIntent.artifactSha256 !== artifactSha256 || repairIntent.semanticDigest !== semanticDigest)) {
124
- return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
125
104
  }
126
105
  if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) {
127
106
  return reject('REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`);
@@ -149,10 +128,62 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
149
128
  const detail = inspectDecisionDetailDuplicates(artifactContent);
150
129
  if (!detail.ok)
151
130
  return reject('REVIEW_DECISION_DETAIL_INVALID', `${detail.code}: ${detail.message}`);
131
+ const schema = getArtifactSchema(spec.family);
132
+ const structure = inspectArtifactContract(artifactContent, schema);
133
+ if (!structure.ok) {
134
+ if (!recovery && !request.dryRun && options.startRecovery) {
135
+ try {
136
+ recovery = beginArtifactRecovery({
137
+ taskId: taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
138
+ requestId: `review-finalize:${taskId}:${stage}:${parsed.round}`
139
+ }, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
140
+ }
141
+ catch (error) {
142
+ return reject('REVIEW_PROVENANCE_INVALID', String(error), digests);
143
+ }
144
+ }
145
+ return {
146
+ ...reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
147
+ artifactSha256, semanticDigest: structure.semanticDigest,
148
+ ...(recovery ? { recovery: {
149
+ recoveryId: recovery.recoveryId,
150
+ candidatePath: recovery.stagingPath,
151
+ baselineSha256: recovery.baselineSha256,
152
+ baselineSemanticDigest: recovery.baselineSemanticDigest
153
+ } } : {})
154
+ }),
155
+ ...(recovery ? { recovery } : {})
156
+ };
157
+ }
152
158
  const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts);
153
159
  if (!transformed.ok)
154
160
  return reject(transformed.code, transformed.message);
155
161
  const finalDigests = { artifactSha256: sha256Content(transformed.content), semanticDigest: canonicalSemanticDigest(transformed.content) };
162
+ if (!recovery && (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')) {
163
+ if (transformed.changed)
164
+ return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact requires changes after its completed recovery was consumed', finalDigests);
165
+ return {
166
+ content: transformed.content,
167
+ result: {
168
+ ...failed(request, 'REVIEW_ARTIFACT_CONFLICT', '', taskId, stageStatus, finalDigests),
169
+ status: 'no-op',
170
+ changed: false,
171
+ error: null
172
+ },
173
+ lockAlreadyHeld: options.lockAlreadyHeld
174
+ };
175
+ }
176
+ if (!recovery && !request.dryRun && options.startRecovery) {
177
+ try {
178
+ recovery = beginArtifactRecovery({
179
+ taskId: taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
180
+ requestId: `review-finalize:${taskId}:${stage}:${parsed.round}`
181
+ }, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
182
+ }
183
+ catch (error) {
184
+ return reject('REVIEW_PROVENANCE_INVALID', String(error), finalDigests);
185
+ }
186
+ }
156
187
  return {
157
188
  content: transformed.content,
158
189
  result: {
@@ -162,22 +193,38 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
162
193
  operations: transformed.changed ? [{ kind: 'artifact', artifact: request.artifact, operation: 'update' }] : [],
163
194
  error: null
164
195
  },
165
- ...(repairIntent?.state === 'awaiting-repair' ? {
166
- provenance: { ...repairIntent, state: 'passed', ...finalDigests },
167
- expectedIntent: repairIntent
168
- } : {})
196
+ ...(recovery ? { recovery } : {}),
197
+ lockAlreadyHeld: options.lockAlreadyHeld
169
198
  };
170
199
  }
171
200
  function commitReviewSummaryProvenance(prepared, repoRoot) {
201
+ if (prepared.result.status === 'failed' || !prepared.recovery)
202
+ return prepared.result;
172
203
  try {
173
- if (prepared.provenance) {
174
- writeArtifactRepairIntent(repoRoot, prepared.provenance, { expected: prepared.expectedIntent ?? null });
204
+ const intent = readArtifactRecoveryIntent(prepared.recovery.repoRoot, prepared.recovery.taskId, prepared.recovery.family, prepared.recovery.artifact);
205
+ if (!intent)
206
+ throw new Error('ARTIFACT_RECOVERY_INTENT_MISSING');
207
+ let committed;
208
+ if (intent.state === 'finalize-ready' || intent.state === 'commit-started') {
209
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
175
210
  }
176
- return prepared.result;
211
+ else {
212
+ const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
213
+ prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
214
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
215
+ }
216
+ if (committed.state !== 'passed')
217
+ throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed.state}'`);
218
+ return {
219
+ ...prepared.result,
220
+ artifactSha256: committed.finalArtifactSha256,
221
+ semanticDigest: committed.finalSemanticDigest,
222
+ error: null
223
+ };
177
224
  }
178
225
  catch (error) {
179
226
  return { ...prepared.result, status: 'failed', error: {
180
- code: 'REVIEW_PROVENANCE_INVALID', message: `cannot record review provenance: ${String(error)}`
227
+ code: 'REVIEW_RECOVERY_COMMIT_FAILED', message: `cannot publish review candidate: ${String(error)}`
181
228
  } };
182
229
  }
183
230
  }
@@ -203,37 +250,17 @@ function finalizeReviewSummaryUnlocked(request, options = {}) {
203
250
  const prepared = prepareReviewSummaryCandidate(request, content, options);
204
251
  if (request.dryRun)
205
252
  return prepared.result;
206
- if (prepared.result.status !== 'applied')
207
- return commitReviewSummaryProvenance(prepared, resolved.repoRoot);
208
- const tempPath = path.join(resolved.taskDir, `.${request.artifact}.tmp-${process.pid}-${(options.randomSuffix ?? randomUUID)()}`);
209
- try {
210
- fileSystem.writeFileSync(tempPath, prepared.content, fileSystem.statModeSync(validated.artifact.path));
211
- }
212
- catch (error) {
213
- cleanupTemp(fileSystem, tempPath);
214
- return failed(request, 'REVIEW_TEMP_WRITE_FAILED', String(error), resolved.taskId, prepared.result.stageStatus);
215
- }
216
- try {
217
- if (fileSystem.readFileSync(validated.artifact.path) !== content) {
218
- cleanupTemp(fileSystem, tempPath);
219
- return failed(request, 'REVIEW_ARTIFACT_CONFLICT', `${request.artifact} changed during finalization`, resolved.taskId, prepared.result.stageStatus);
220
- }
221
- fileSystem.renameSync(tempPath, validated.artifact.path);
222
- }
223
- catch (error) {
224
- cleanupTemp(fileSystem, tempPath);
225
- return failed(request, 'REVIEW_RENAME_FAILED', String(error), resolved.taskId, prepared.result.stageStatus);
226
- }
227
253
  return commitReviewSummaryProvenance(prepared, resolved.repoRoot);
228
254
  }
229
255
  function finalizeReviewSummary(request, options = {}) {
256
+ const effectiveOptions = { ...options, startRecovery: !request.dryRun };
230
257
  if (request.dryRun || options.lockAlreadyHeld)
231
- return finalizeReviewSummaryUnlocked(request, options);
258
+ return finalizeReviewSummaryUnlocked(request, effectiveOptions);
232
259
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
233
260
  if (!resolved.ok)
234
- return finalizeReviewSummaryUnlocked(request, options);
261
+ return finalizeReviewSummaryUnlocked(request, effectiveOptions);
235
262
  try {
236
- return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-review.finalize-summary', () => finalizeReviewSummaryUnlocked(request, options));
263
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-review.finalize-summary', () => finalizeReviewSummaryUnlocked(request, { ...effectiveOptions, lockAlreadyHeld: true }));
237
264
  }
238
265
  catch (error) {
239
266
  if (!(error instanceof TaskExecutionLockError))
@@ -199,14 +199,16 @@ function mutateShortIdRegistry(repoRoot, taskId, requested) {
199
199
  const activeDir = path.join(repoRoot, '.agents', 'workspace', 'active');
200
200
  return mutateShortIdRegistryAt(activeDir, configuredShortIdLength(repoRoot), taskId, requested);
201
201
  }
202
- function mutateShortIdRegistryAt(activeDir, width, taskId, requested) {
202
+ function mutateShortIdRegistryAt(activeDir, width, taskId, requested, onlyReleaseWhenInactive = false, pruneOrphans = true) {
203
203
  const registryPath = path.join(activeDir, REGISTRY_NAME);
204
204
  return withRegistryLock(activeDir, () => {
205
205
  const registry = readRegistryStrict(registryPath, width);
206
206
  const original = JSON.stringify(registry.ids);
207
- for (const [key, candidate] of Object.entries(registry.ids)) {
208
- if (candidate !== taskId && !fs.existsSync(path.join(activeDir, candidate, 'task.md')))
209
- delete registry.ids[key];
207
+ if (pruneOrphans) {
208
+ for (const [key, candidate] of Object.entries(registry.ids)) {
209
+ if (candidate !== taskId && !fs.existsSync(path.join(activeDir, candidate, 'task.md')))
210
+ delete registry.ids[key];
211
+ }
210
212
  }
211
213
  const existing = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId);
212
214
  let result;
@@ -232,8 +234,10 @@ function mutateShortIdRegistryAt(activeDir, width, taskId, requested) {
232
234
  }
233
235
  }
234
236
  else if (requested === 'release') {
235
- if (!existing)
236
- result = { effect: 'unchanged', shortId: null, changed: false };
237
+ const taskIsActive = fs.existsSync(path.join(activeDir, taskId, 'task.md'));
238
+ if (!existing || (onlyReleaseWhenInactive && taskIsActive)) {
239
+ result = { effect: 'unchanged', shortId: existing?.[0] ?? null, changed: false };
240
+ }
237
241
  else {
238
242
  delete registry.ids[existing[0]];
239
243
  result = { effect: 'released', shortId: existing[0], changed: true };
@@ -247,6 +251,10 @@ function mutateShortIdRegistryAt(activeDir, width, taskId, requested) {
247
251
  return { ...result, changed };
248
252
  });
249
253
  }
254
+ function releaseStaleShortIdRegistry(repoRoot, taskId) {
255
+ const activeDir = path.join(repoRoot, '.agents', 'workspace', 'active');
256
+ return mutateShortIdRegistryAt(activeDir, configuredShortIdLength(repoRoot), taskId, 'release', true, false);
257
+ }
250
258
  function executeShortIdCommand(request) {
251
259
  try {
252
260
  if (!Number.isInteger(request.shortIdLength) || request.shortIdLength < 1) {
@@ -429,5 +437,5 @@ function lookupShortIdByBranch(branch, repoRoot, _opts) {
429
437
  }
430
438
  return matches[0];
431
439
  }
432
- export { normalizeShortIdInput, isRemovedHashShortIdInput, resolveShortIdReadOnly, lookupShortIdByBranch, loadShortIdByTaskId, inspectShortIdRegistry, configuredShortIdLength, mutateShortIdRegistry, executeShortIdCommand };
440
+ export { normalizeShortIdInput, isRemovedHashShortIdInput, resolveShortIdReadOnly, lookupShortIdByBranch, loadShortIdByTaskId, inspectShortIdRegistry, configuredShortIdLength, mutateShortIdRegistry, releaseStaleShortIdRegistry, executeShortIdCommand };
433
441
  //# sourceMappingURL=short-id.js.map
@@ -231,7 +231,7 @@ function withTaskExecutionLock(repoRoot, taskId, ownerName, callback, options =
231
231
  catch (error) {
232
232
  if (error instanceof TaskExecutionLockError)
233
233
  throw error;
234
- if (error instanceof Error && error.name === 'OrchestrationStateError')
234
+ if (error instanceof Error && ['OrchestrationStateError', 'ArtifactRecoveryError'].includes(error.name))
235
235
  throw error;
236
236
  throw new TaskExecutionLockError('ORCHESTRATION_LOCK_FAILED', 'lifecycle task lock operation failed', { operation: ownsFixed ? 'callback' : 'acquire', errno: errno(error), key });
237
237
  }
@@ -33,6 +33,7 @@ import { manualValidationFinalSummaryProjectionMatches } from "./manual-validati
33
33
  import { readManualValidationCompletion } from "./manual-validation-completion.js";
34
34
  import { sha256File } from "./artifact-receipts.js";
35
35
  import { summaryCommentState } from "../platform/pr-summary.js";
36
+ import { taskIssueIdentity } from "../platform/task-identities.js";
36
37
  const TASK_ENUMS = {
37
38
  type: ["feature", "bugfix", "refactor", "docs", "chore"],
38
39
  workflow: ["feature-development", "bug-fix", "refactoring"],
@@ -78,8 +79,6 @@ const sharedUtils = {
78
79
  failResult,
79
80
  blockedResult,
80
81
  safeStat,
81
- parseIssueNumber,
82
- parsePrNumber
83
82
  };
84
83
  async function runCheck(type, context, shared) {
85
84
  switch (type) {
@@ -314,8 +313,8 @@ function checkTaskMeta({ taskDir, config, repositoryRoot }) {
314
313
  if (expectedStatus && metadata.status !== expectedStatus) {
315
314
  return failResult("task-meta", `Expected status '${expectedStatus}', got '${metadata.status || "(empty)"}'`);
316
315
  }
317
- if (config.require_issue_number && !parseIssueNumber(metadata.issue_number)) {
318
- return failResult("task-meta", "Expected a valid issue_number in task metadata");
316
+ if (config.require_platform_issue_identity && !taskIssueIdentity(metadata)) {
317
+ return failResult("task-meta", "Expected a valid platform_issue_identity in task metadata");
319
318
  }
320
319
  if (config.require_completed_at && isBlank(metadata.completed_at)) {
321
320
  return failResult("task-meta", "Expected completed_at to be present");
@@ -1185,16 +1184,6 @@ function getCheckedRequirements(content) {
1185
1184
  .filter(Boolean)
1186
1185
  .map((match) => match[1].trim());
1187
1186
  }
1188
- function parseIssueNumber(value) {
1189
- if (isBlank(value) || value === "N/A") {
1190
- return null;
1191
- }
1192
- const match = String(value).match(/\d+/);
1193
- return match ? Number(match[0]) : null;
1194
- }
1195
- function parsePrNumber(value) {
1196
- return parseIssueNumber(value);
1197
- }
1198
1187
  // === Utilities ===
1199
1188
  function normalizeContent(text) {
1200
1189
  return String(text || "")
@@ -1,7 +1,6 @@
1
1
  export const TASK_WORKFLOW_COMMANDS = Object.freeze({
2
2
  'artifact-inspect': ['task-artifact', 'inspect'],
3
3
  'artifact-init': ['task-artifact', 'init'],
4
- 'artifact-repair': ['task-artifact', 'repair'],
5
4
  'artifact-finalize-local': ['task-artifact', 'finalize-local'],
6
5
  'review-finalize-summary': ['task-review', 'finalize-summary'],
7
6
  event: ['task-event', null],
@@ -1,5 +1,4 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
- import semver from 'semver';
3
2
  const packageJsonUrl = [
4
3
  new URL('../package.json', import.meta.url),
5
4
  new URL('../../package.json', import.meta.url),
@@ -10,17 +9,8 @@ if (!packageJsonUrl) {
10
9
  const { version } = JSON.parse(readFileSync(packageJsonUrl, 'utf8'));
11
10
  const VERSION = `v${version}`;
12
11
  const AGENT_INFRA_VERSION_PATTERN = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
13
- const LEGACY_COMPATIBILITY_CUTOFF_VERSION = '1.0.0';
14
12
  function isValidAgentInfraVersion(value) {
15
13
  return typeof value === 'string' && AGENT_INFRA_VERSION_PATTERN.test(value);
16
14
  }
17
- function isLegacyCompatibilityEnabled(runtimeVersion = VERSION) {
18
- const normalized = runtimeVersion.startsWith('v') ? runtimeVersion.slice(1) : runtimeVersion;
19
- const parsed = semver.valid(normalized);
20
- return parsed !== null && semver.lt(parsed, LEGACY_COMPATIBILITY_CUTOFF_VERSION);
21
- }
22
- function legacyCompatibilityError(resource, runtimeVersion = VERSION) {
23
- return Object.assign(new Error(`PLATFORM_IDENTITY_LEGACY_UNSUPPORTED: ${resource} requires migration to the current schema before v${LEGACY_COMPATIBILITY_CUTOFF_VERSION} runtime ${runtimeVersion}`), { code: 'PLATFORM_IDENTITY_LEGACY_UNSUPPORTED' });
24
- }
25
- export { LEGACY_COMPATIBILITY_CUTOFF_VERSION, VERSION, isLegacyCompatibilityEnabled, isValidAgentInfraVersion, legacyCompatibilityError };
15
+ export { VERSION, isValidAgentInfraVersion };
26
16
  //# sourceMappingURL=version.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocolVersion": 3,
3
- "packageVersion": "0.9.15",
4
- "sourceInputHash": "d6521519c8d772a6d81c177d8c6e6795c080b04d9f8afcec19bca45c486a70df",
5
- "internalExecutableBuildHash": "f0ab80c423acc4def5130e075a209a64349296985701506e7d9e73f8151bcb2a"
3
+ "packageVersion": "0.10.0",
4
+ "sourceInputHash": "d7af8de4bd6c1996a920335dfc766e23b8d391cd4db2965d7d9dfaa2cb112059",
5
+ "internalExecutableBuildHash": "63a6d2d9113dd353dfce165efccf82a5ea25398c2aad9f1b2c5543dc28e8b833"
6
6
  }
@@ -26,6 +26,7 @@ import {
26
26
  type ControllerControl = Readonly<{
27
27
  token: string;
28
28
  generation: string;
29
+ rootId?: string;
29
30
  channelDir: string;
30
31
  statusDir: string;
31
32
  runtimeDir: string;
@@ -83,13 +84,14 @@ function copyRegular(source: string, destination: string, mode: number): void {
83
84
  function controlFromEnvironment(): ControllerControl {
84
85
  const token = process.env.AGENT_INFRA_CONTROL_TOKEN;
85
86
  const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
87
+ const rootId = process.env.AGENT_INFRA_CONTROL_ROOT_ID;
86
88
  const channelDir = process.env.AGENT_INFRA_CONTROL_DIR;
87
89
  const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
88
90
  const runtimeDir = process.env.AGENT_INFRA_RUNTIME_DIR;
89
- if (!token || !generation || !channelDir || !statusDir || !runtimeDir) {
91
+ if (!token || !generation || !rootId || !channelDir || !statusDir || !runtimeDir) {
90
92
  throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_MISSING');
91
93
  }
92
- return { token, generation, channelDir, statusDir, runtimeDir };
94
+ return { token, generation, rootId, channelDir, statusDir, runtimeDir };
93
95
  }
94
96
 
95
97
  function verifyRuntime(runtimeDir: string): void {
@@ -255,6 +257,9 @@ function isolatedEnvironment(
255
257
  AGENT_INFRA_TASK_ID: taskId,
256
258
  AGENT_INFRA_CONTROL_TOKEN: control.token,
257
259
  AGENT_INFRA_CONTROL_GENERATION: control.generation,
260
+ ...(control.rootId || sourceEnvironment.AGENT_INFRA_CONTROL_ROOT_ID
261
+ ? { AGENT_INFRA_CONTROL_ROOT_ID: control.rootId ?? sourceEnvironment.AGENT_INFRA_CONTROL_ROOT_ID }
262
+ : {}),
258
263
  AGENT_INFRA_CONTROL_DIR: control.channelDir,
259
264
  AGENT_INFRA_CONTROL_STATUS_DIR: control.statusDir,
260
265
  AGENT_INFRA_RUNTIME_DIR: control.runtimeDir,
@@ -43,11 +43,23 @@ type ActiveCodexLifecycleEvidenceQuery = Readonly<{
43
43
  const LOCK_RETRY_MS = 10;
44
44
  const LOCK_TIMEOUT_MS = 1_000;
45
45
  const LOCK_STALE_MS = 30_000;
46
+ const RECOVERY_CONSUMER_RE = /^lifecycle-recovery:([^:\r\n]+):([^:\r\n]+)$/u;
46
47
 
47
48
  function digest(value: string): string {
48
49
  return crypto.createHash('sha256').update(value).digest('hex');
49
50
  }
50
51
 
52
+ function isRecoveryConsumer(value: string): boolean {
53
+ return RECOVERY_CONSUMER_RE.test(value);
54
+ }
55
+
56
+ function recoveryConsumer(taskId: string, receiptId: string): string {
57
+ if (!/^[^:\r\n]+$/u.test(taskId) || !/^[^:\r\n]+$/u.test(receiptId)) {
58
+ throw new Error('Codex lifecycle recovery claim identity is invalid');
59
+ }
60
+ return `lifecycle-recovery:${taskId}:${receiptId}`;
61
+ }
62
+
51
63
  function readRecord(file: string): StoredCodexLifecycle {
52
64
  const value = JSON.parse(fs.readFileSync(file, 'utf8')) as StoredCodexLifecycle;
53
65
  if (
@@ -249,11 +261,12 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
249
261
 
250
262
  function read(childThreadId: string): StoredCodexLifecycle {
251
263
  const matches = findByChild(childThreadId);
252
- if (matches.length !== 1) throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
264
+ if (matches.length === 0) throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
265
+ if (matches.length > 1) throw new Error(`Codex lifecycle child '${childThreadId}' is ambiguous`);
253
266
  return readRecord(matches[0]!);
254
267
  }
255
268
 
256
- function consume(
269
+ function consumeInternal(
257
270
  childThreadId: string,
258
271
  consumer: string,
259
272
  expectedHookDefinitionHash?: string
@@ -291,12 +304,51 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
291
304
  });
292
305
  }
293
306
 
307
+ function consume(
308
+ childThreadId: string,
309
+ consumer: string,
310
+ expectedHookDefinitionHash?: string
311
+ ): StoredCodexLifecycle {
312
+ if (!consumer.trim()) throw new Error('Codex lifecycle consumer is required');
313
+ if (consumer.startsWith('lifecycle-recovery:')) {
314
+ throw new Error('Codex lifecycle recovery claims require claimRecovery');
315
+ }
316
+ return consumeInternal(childThreadId, consumer, expectedHookDefinitionHash);
317
+ }
318
+
319
+ function claimRecovery(
320
+ childThreadId: string,
321
+ taskId: string,
322
+ receiptId: string,
323
+ expectedHookDefinitionHash?: string
324
+ ): StoredCodexLifecycle {
325
+ return consumeInternal(
326
+ childThreadId,
327
+ recoveryConsumer(taskId, receiptId),
328
+ expectedHookDefinitionHash
329
+ );
330
+ }
331
+
332
+ function releaseRecovery(childThreadId: string, consumer: string): boolean {
333
+ if (!/^lifecycle-recovery:[^:\r\n]+:[^:\r\n]+$/.test(consumer)) return false;
334
+ return withWriteLock(() => {
335
+ const matches = findByChild(childThreadId);
336
+ if (matches.length !== 1) return false;
337
+ const file = matches[0]!;
338
+ const current = readRecord(file);
339
+ if (current.state.status !== 'stop-ready' || current.consumer !== consumer || !current.consumedAt) return false;
340
+ fs.unlinkSync(file);
341
+ return true;
342
+ });
343
+ }
344
+
294
345
  function expireBefore(cutoff: string): number {
295
346
  return withWriteLock(() => {
296
347
  let changed = 0;
297
348
  for (const file of recordFiles(root)) {
298
349
  const current = readRecord(file);
299
350
  if (current.updatedAt >= cutoff) continue;
351
+ if (current.consumer && isRecoveryConsumer(current.consumer)) continue;
300
352
  if (current.consumer || ['invalid', 'expired', 'stop-ready'].includes(current.state.status)) {
301
353
  fs.unlinkSync(file);
302
354
  changed += 1;
@@ -316,7 +368,7 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
316
368
  });
317
369
  }
318
370
 
319
- return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
371
+ return Object.freeze({ root, apply, applyToSpawn, consume, claimRecovery, releaseRecovery, expireBefore, findByParent, read });
320
372
  }
321
373
 
322
374
  export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence };
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
 
5
5
  export function fsyncDirectory(directory: string): void {
6
+ if (process.platform === 'win32') return;
6
7
  const descriptor = fs.openSync(directory, 'r');
7
8
  try { fs.fsyncSync(descriptor); } finally { fs.closeSync(descriptor); }
8
9
  }
@@ -57,7 +57,7 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
57
57
  'task-ledger': ['decision-next-id', 'stage-status', 'finding-upsert', 'finding-respond', 'finding-review', 'decision-upsert', 'rework-intent-upsert'],
58
58
  'task-warning': ['list', 'add', 'set-status'],
59
59
  'task-activity': ['pr-review-inspect', 'pr-review-start', 'pr-review-complete', 'pr-review-terminate'],
60
- 'task-artifact': ['inspect', 'init', 'repair', 'finalize-local'],
60
+ 'task-artifact': ['inspect', 'init', 'finalize-local'],
61
61
  'task-orchestration': ['status', 'progress'],
62
62
  'task-review': ['finalize-summary'],
63
63
  'task-event': ['event'],
@@ -43,15 +43,24 @@ function printFailure(format: OutputFormat, error: { code: string; message: stri
43
43
  process.exitCode = 1;
44
44
  }
45
45
 
46
+ const MANUAL_VALIDATION_STATUS_SECTION = /^###\s+(?:⚠️\s+(?:需人工校验|Manual Validation Required)|✅\s+(?:人工验证已通过|无需人工校验|Manual Validation Passed|No Manual Validation Required)|⏳\s+(?:人工验证待收尾|Manual Validation Pending))\s*$[\s\S]*?(?=^#{1,3}\s|(?![\s\S]))/gmu;
47
+
46
48
  function manualSummaryBody(body: string, phase: 'pending' | 'final', transactionId: string, receiptDigest = '', evidenceDigest = '', prHeadSha = ''): string {
47
- const withoutPrevious = body
48
- .replace(/^###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)\s*$/gmu, '')
49
- .replace(/^###\s+⏳\s+(?:Manual Validation Pending|人工验证待收尾)\s*$/gmu, '')
50
- .replace(/Manual validation passed(?:\s+→)?/giu, 'Manual validation pending');
49
+ const chinese = /###\s+(?:⚠️\s+需人工校验|✅\s+(?:人工验证已通过|无需人工校验)|⏳\s+人工验证待收尾)\s*$/mu.test(body);
51
50
  const section = phase === 'pending'
52
- ? `### ⏳ Manual Validation Pending\n\nManual validation evidence is staged; transaction ${transactionId} is awaiting final promotion.`
53
- : `### ✅ Manual Validation Passed\n\nManual validation passed; transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`;
54
- return `${withoutPrevious.replace(/\s+$/u, '')}\n\n${section}\n`;
51
+ ? chinese
52
+ ? `### ⏳ 人工验证待收尾\n\n人工验证证据已暂存;事务 ${transactionId} 正在等待最终完成。`
53
+ : `### ⏳ Manual Validation Pending\n\nManual validation evidence is staged; transaction ${transactionId} is awaiting final promotion.`
54
+ : chinese
55
+ ? `### ✅ 人工验证已通过\n\n人工验证已通过;transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`
56
+ : `### ✅ Manual Validation Passed\n\nManual validation passed; transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`;
57
+ let inserted = false;
58
+ const updated = body.replace(MANUAL_VALIDATION_STATUS_SECTION, () => {
59
+ if (inserted) return '';
60
+ inserted = true;
61
+ return `${section}\n\n`;
62
+ });
63
+ return inserted ? `${updated.replace(/\s+$/u, '')}\n` : `${body.replace(/\s+$/u, '')}\n\n${section}\n`;
55
64
  }
56
65
 
57
66
  function openManualValidationStarted(taskMdPath: string): { present: boolean; transactionId: string | null } {
@@ -246,7 +255,7 @@ async function executeManualValidationTransactionLocked(
246
255
  });
247
256
  if (!['applied', 'no-op'].includes(promoted.status)) transactionFailure(promoted.error ?? { code: 'MANUAL_VALIDATION_TRANSACTION_RECOVERY_REQUIRED', message: 'final summary promotion failed' });
248
257
  const postWrite = await summaryCommentState(taskRef, { cwd, client: options.client });
249
- if (!postWrite.comment?.body.includes('### ✅ Manual Validation Passed') || postWrite.pullRequest?.head.sha !== receipt.prHeadSha || !manualValidationFinalSummaryProjectionMatches(postWrite.comment.body, receipt)) transactionFailure({ code: 'MANUAL_VALIDATION_TRANSACTION_RECOVERY_REQUIRED', message: 'final summary post-write verification failed' });
258
+ if (!/^###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)\s*$/mu.test(postWrite.comment?.body ?? '') || postWrite.pullRequest?.head.sha !== receipt.prHeadSha || !manualValidationFinalSummaryProjectionMatches(postWrite.comment?.body ?? '', receipt)) transactionFailure({ code: 'MANUAL_VALIDATION_TRANSACTION_RECOVERY_REQUIRED', message: 'final summary post-write verification failed' });
250
259
  transitionAndPersist('committed', { postWriteVerified: true });
251
260
  return result('applied', null, { transaction, receipt, idempotent: false });
252
261
  } catch (error) {
@@ -1,7 +1,7 @@
1
1
  import { parseArtifactCommand, executeArtifactCommand } from '../task/artifact-command.ts';
2
2
  import { ensureInternalHandlerRoute } from './cli-route-inventory.ts';
3
3
 
4
- const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family>\n agent-infra-internal task-artifact <N | TASK-id> init --family <family> --artifact <artifact> [--locale <zh-CN|en>]\n agent-infra-internal task-artifact <N | TASK-id> repair --family <family> --artifact <artifact> --expected-sha256 <sha256> --expected-semantic-digest <digest>\n agent-infra-internal task-artifact <N | TASK-id> finalize-local --family <analysis|plan|code> --artifact <artifact>\n\nInspect, initialize, repair, or finalize a workflow artifact without changing task state.\n`;
4
+ const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family>\n agent-infra-internal task-artifact <N | TASK-id> init --family <family> --artifact <artifact> [--locale <zh-CN|en>]\n agent-infra-internal task-artifact <N | TASK-id> finalize-local --family <analysis|plan|code> --artifact <artifact> [--recovery-id <id>]\n\nInspect, initialize, or finalize a workflow artifact without changing task state.\n`;
5
5
 
6
6
 
7
7
  function taskArtifact(args: string[] = []): void {