@fitlab-ai/agent-infra 0.9.5 → 0.9.7

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 (172) hide show
  1. package/dist/bin/internal-cli.js +8 -3
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
  8. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -4
  11. package/dist/lib/internal/codex-lifecycle.js +169 -3
  12. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  13. package/dist/lib/internal/release-workflow.js +108 -11
  14. package/dist/lib/internal/sandbox-control.js +43 -6
  15. package/dist/lib/internal/task-create.js +12 -4
  16. package/dist/lib/internal/task-orchestration.js +40 -7
  17. package/dist/lib/platform/context.js +6 -4
  18. package/dist/lib/platform/github-client.js +55 -2
  19. package/dist/lib/platform/issue-comments.js +2 -1
  20. package/dist/lib/process-data/checkpoint.js +319 -0
  21. package/dist/lib/process-data/index.js +128 -60
  22. package/dist/lib/process-data/materialize.js +84 -0
  23. package/dist/lib/process-data/normalize.js +34 -1
  24. package/dist/lib/process-data/reconcile.js +55 -1
  25. package/dist/lib/process-data/sources.js +422 -1
  26. package/dist/lib/process-data/store.js +341 -11
  27. package/dist/lib/release/workflow.js +29 -21
  28. package/dist/lib/sandbox/commands/create.js +111 -60
  29. package/dist/lib/sandbox/commands/prune.js +40 -16
  30. package/dist/lib/sandbox/commands/rm.js +372 -94
  31. package/dist/lib/sandbox/commands/start.js +4 -3
  32. package/dist/lib/sandbox/control/client.js +138 -22
  33. package/dist/lib/sandbox/control/container-identity.js +64 -0
  34. package/dist/lib/sandbox/control/executor.js +142 -0
  35. package/dist/lib/sandbox/control/lifecycle.js +557 -0
  36. package/dist/lib/sandbox/control/protocol.js +69 -26
  37. package/dist/lib/sandbox/control/server.js +395 -99
  38. package/dist/lib/sandbox/control/state.js +191 -0
  39. package/dist/lib/sandbox/control/timing.js +8 -0
  40. package/dist/lib/sandbox/index.js +2 -2
  41. package/dist/lib/sandbox/managed-fs.js +14 -2
  42. package/dist/lib/sandbox/mounts.js +22 -19
  43. package/dist/lib/sandbox/recovery.js +284 -50
  44. package/dist/lib/sandbox/workspace-view.js +90 -10
  45. package/dist/lib/sandbox/worktree-safety.js +196 -0
  46. package/dist/lib/server/daemon.js +1 -1
  47. package/dist/lib/server/process-control.js +17 -1
  48. package/dist/lib/server/process-state.js +57 -20
  49. package/dist/lib/task/artifact-lifecycle.js +87 -15
  50. package/dist/lib/task/artifact-receipts.js +128 -0
  51. package/dist/lib/task/codex-orchestration.js +236 -0
  52. package/dist/lib/task/commands/status.js +3 -0
  53. package/dist/lib/task/commands/validate.js +289 -0
  54. package/dist/lib/task/commit-finalization.js +36 -9
  55. package/dist/lib/task/create.js +10 -2
  56. package/dist/lib/task/delegation-receipts.js +109 -2
  57. package/dist/lib/task/events.js +149 -6
  58. package/dist/lib/task/finalization-artifacts.js +2 -1
  59. package/dist/lib/task/implementation-inputs.js +1 -7
  60. package/dist/lib/task/index.js +9 -0
  61. package/dist/lib/task/ledger.js +13 -1
  62. package/dist/lib/task/orchestration.js +365 -15
  63. package/dist/lib/task/sections.js +5 -1
  64. package/dist/lib/task/task-execution-lock.js +5 -4
  65. package/dist/lib/task/verification-engine.js +78 -14
  66. package/dist/lib/task/verification.js +1 -0
  67. package/dist/lifecycle-build-manifest.json +6 -0
  68. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
  69. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  70. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  71. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
  72. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  73. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  74. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
  75. package/lib/agent-clients/adapters/codex.ts +1 -1
  76. package/lib/defaults.json +10 -2
  77. package/lib/git/workflow.ts +25 -3
  78. package/lib/internal/codex-lifecycle.ts +175 -4
  79. package/lib/internal/codex-sandbox-controller.ts +109 -0
  80. package/lib/internal/release-workflow.ts +107 -11
  81. package/lib/internal/sandbox-control.ts +38 -6
  82. package/lib/internal/task-create.ts +16 -4
  83. package/lib/internal/task-orchestration.ts +34 -7
  84. package/lib/platform/context.ts +8 -5
  85. package/lib/platform/github-client.ts +61 -3
  86. package/lib/platform/issue-comments.ts +2 -1
  87. package/lib/process-data/checkpoint.ts +335 -0
  88. package/lib/process-data/index.ts +112 -60
  89. package/lib/process-data/materialize.ts +83 -0
  90. package/lib/process-data/normalize.ts +29 -1
  91. package/lib/process-data/reconcile.ts +64 -2
  92. package/lib/process-data/sources.ts +449 -2
  93. package/lib/process-data/store.ts +304 -11
  94. package/lib/process-data/types.ts +124 -2
  95. package/lib/release/workflow.ts +69 -20
  96. package/lib/sandbox/commands/create.ts +70 -25
  97. package/lib/sandbox/commands/prune.ts +53 -15
  98. package/lib/sandbox/commands/rm.ts +423 -108
  99. package/lib/sandbox/commands/start.ts +4 -3
  100. package/lib/sandbox/control/client.ts +152 -28
  101. package/lib/sandbox/control/container-identity.ts +93 -0
  102. package/lib/sandbox/control/executor.ts +167 -0
  103. package/lib/sandbox/control/lifecycle.ts +573 -0
  104. package/lib/sandbox/control/protocol.ts +121 -68
  105. package/lib/sandbox/control/server.ts +398 -109
  106. package/lib/sandbox/control/state.ts +213 -0
  107. package/lib/sandbox/control/timing.ts +12 -0
  108. package/lib/sandbox/index.ts +2 -2
  109. package/lib/sandbox/managed-fs.ts +17 -2
  110. package/lib/sandbox/mounts.ts +16 -8
  111. package/lib/sandbox/recovery.ts +335 -65
  112. package/lib/sandbox/workspace-view.ts +102 -11
  113. package/lib/sandbox/worktree-safety.ts +234 -0
  114. package/lib/server/daemon.ts +1 -1
  115. package/lib/server/process-control.ts +24 -3
  116. package/lib/server/process-state.ts +82 -27
  117. package/lib/task/artifact-lifecycle.ts +78 -14
  118. package/lib/task/artifact-receipts.ts +161 -0
  119. package/lib/task/codex-orchestration.ts +319 -0
  120. package/lib/task/commands/status.ts +2 -0
  121. package/lib/task/commands/validate.ts +282 -0
  122. package/lib/task/commit-finalization.ts +52 -9
  123. package/lib/task/create.ts +11 -2
  124. package/lib/task/delegation-receipts.ts +208 -7
  125. package/lib/task/events.ts +131 -5
  126. package/lib/task/finalization-artifacts.ts +2 -1
  127. package/lib/task/implementation-inputs.ts +1 -5
  128. package/lib/task/index.ts +8 -0
  129. package/lib/task/ledger.ts +16 -1
  130. package/lib/task/orchestration.ts +552 -18
  131. package/lib/task/sections.ts +6 -2
  132. package/lib/task/task-execution-lock.ts +11 -9
  133. package/lib/task/verification-engine.ts +93 -24
  134. package/lib/task/verification.ts +2 -1
  135. package/package.json +1 -1
  136. package/templates/.agents/README.en.md +7 -0
  137. package/templates/.agents/README.zh-CN.md +5 -0
  138. package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
  139. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  140. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  141. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  142. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  143. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  144. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  145. package/templates/.agents/rules/review-handshake.en.md +3 -1
  146. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  147. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  148. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  149. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  150. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  151. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  152. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  153. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  154. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  155. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
  156. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
  157. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  158. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  159. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
  160. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
  161. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  162. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  163. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
  164. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
  165. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  166. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  167. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  168. package/templates/.codex/hooks.json +8 -8
  169. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  170. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
  171. package/templates/.agents/workspace/README.en.md +0 -26
  172. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -2,22 +2,34 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
4
 
5
- export type PidRecord = {
6
- version: 1;
5
+ export type ProcessStartTime = number;
6
+
7
+ export type ProcessIdentity = Readonly<{
7
8
  pid: number;
8
- startTime: string;
9
+ startTime: ProcessStartTime;
10
+ }>;
11
+
12
+ export type PidRecord = ProcessIdentity & {
13
+ version: 2;
9
14
  };
10
15
 
11
16
  export type ProcessStartTimeQuery = { command: string; args: string[] };
12
17
 
13
18
  export type ProcessState =
14
19
  | { kind: 'missing'; snapshot: null }
15
- | { kind: 'invalid'; snapshot: string }
16
- | { kind: 'legacy'; snapshot: string; pid: number }
20
+ | { kind: 'legacy-json'; snapshot: string; legacy: LegacyProcessSnapshot }
21
+ | { kind: 'legacy-pid-only'; snapshot: string; legacy: LegacyProcessSnapshot }
22
+ | { kind: 'legacy-unknown'; snapshot: string; legacy: LegacyProcessSnapshot; reason: string }
17
23
  | { kind: 'stale'; snapshot: string; record: PidRecord }
18
24
  | { kind: 'running'; snapshot: string; record: PidRecord };
19
25
 
20
- export type LinuxProcessStat = { state: string; startTime: string };
26
+ export type LegacyProcessSnapshot = Readonly<{
27
+ pid?: number;
28
+ raw: string;
29
+ format: 'json-v1' | 'pid-only' | 'malformed';
30
+ }>;
31
+
32
+ export type LinuxProcessStat = { state: string; startTime: number };
21
33
 
22
34
  export function parseLinuxProcessStat(raw: string): LinuxProcessStat | null {
23
35
  const closeParen = raw.lastIndexOf(')');
@@ -28,7 +40,9 @@ export function parseLinuxProcessStat(raw: string): LinuxProcessStat | null {
28
40
  if (typeof state !== 'string' || state.length !== 1 || !/^\d+$/.test(startTime ?? '')) {
29
41
  return null;
30
42
  }
31
- return { state, startTime: startTime! };
43
+ const numericStartTime = Number(startTime);
44
+ if (!Number.isSafeInteger(numericStartTime)) return null;
45
+ return { state, startTime: numericStartTime };
32
46
  }
33
47
 
34
48
  export function buildProcessStartTimeQuery(
@@ -68,10 +82,22 @@ export function isProcessAlive(pid: number, platform: NodeJS.Platform = process.
68
82
  }
69
83
  }
70
84
 
85
+ function parseDarwinStartTime(output: string): number | null {
86
+ const match = output.trim().match(/^(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat) ([A-Z][a-z]{2}) (\d{1,2}) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/);
87
+ if (!match) return null;
88
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
89
+ const month = months.indexOf(match[1]!);
90
+ if (month < 0) return null;
91
+ const timestamp = Date.UTC(
92
+ Number(match[6]), month, Number(match[2]), Number(match[3]), Number(match[4]), Number(match[5])
93
+ );
94
+ return Number.isNaN(timestamp) ? null : timestamp;
95
+ }
96
+
71
97
  export function getProcessStartTime(
72
98
  pid: number,
73
99
  platform: NodeJS.Platform = process.platform
74
- ): string | null {
100
+ ): ProcessStartTime | null {
75
101
  if (!Number.isInteger(pid) || pid <= 0) return null;
76
102
  if (platform === 'linux') {
77
103
  try {
@@ -85,43 +111,71 @@ export function getProcessStartTime(
85
111
  const query = buildProcessStartTimeQuery(pid, platform);
86
112
  if (query === null) return null;
87
113
  try {
88
- const output = execFileSync(query.command, query.args, { encoding: 'utf8' }).trim();
114
+ const output = execFileSync(query.command, query.args, {
115
+ encoding: 'utf8',
116
+ env: platform === 'darwin' ? { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } : process.env
117
+ }).trim();
89
118
  if (output.length === 0) return null;
90
119
  if (platform === 'win32') {
91
120
  const timestamp = new Date(output);
92
- return Number.isNaN(timestamp.getTime()) ? null : timestamp.toISOString();
121
+ return Number.isNaN(timestamp.getTime()) ? null : timestamp.getTime();
93
122
  }
94
- return output;
123
+ if (platform === 'darwin') return parseDarwinStartTime(output);
124
+ return null;
95
125
  } catch {
96
126
  return null;
97
127
  }
98
128
  }
99
129
 
100
- function parsePidRecord(raw: string): { kind: 'invalid' } | { kind: 'legacy'; pid: number } | { kind: 'record'; record: PidRecord } {
130
+ function parsePidRecord(
131
+ raw: string
132
+ ):
133
+ | { kind: 'legacy-json'; legacy: LegacyProcessSnapshot }
134
+ | { kind: 'legacy-pid-only'; legacy: LegacyProcessSnapshot }
135
+ | { kind: 'legacy-unknown'; legacy: LegacyProcessSnapshot; reason: string }
136
+ | { kind: 'record'; record: PidRecord } {
101
137
  const trimmed = raw.trim();
102
138
  if (/^[1-9]\d*$/.test(trimmed)) {
103
139
  const pid = Number(trimmed);
104
- return Number.isSafeInteger(pid) ? { kind: 'legacy', pid } : { kind: 'invalid' };
140
+ return Number.isSafeInteger(pid)
141
+ ? { kind: 'legacy-pid-only', legacy: { pid, raw, format: 'pid-only' } }
142
+ : { kind: 'legacy-unknown', legacy: { raw, format: 'malformed' }, reason: 'pid is not a safe integer' };
105
143
  }
106
144
  try {
107
- const value = JSON.parse(trimmed) as Partial<PidRecord> | null;
145
+ const value = JSON.parse(trimmed) as { version?: unknown; pid?: unknown; startTime?: unknown } | null;
146
+ const pidValue: unknown = value?.pid;
147
+ const startTimeValue: unknown = value?.startTime;
108
148
  if (
109
149
  value !== null &&
110
- value.version === 1 &&
111
- Number.isSafeInteger(value.pid) &&
112
- (value.pid ?? 0) > 0 &&
113
- typeof value.startTime === 'string' &&
114
- value.startTime.length > 0
150
+ value.version === 2 &&
151
+ typeof pidValue === 'number' &&
152
+ Number.isSafeInteger(pidValue) &&
153
+ pidValue > 0 &&
154
+ typeof startTimeValue === 'number' &&
155
+ Number.isSafeInteger(startTimeValue) &&
156
+ startTimeValue >= 0
115
157
  ) {
158
+ const startTime = startTimeValue as number;
116
159
  return {
117
160
  kind: 'record',
118
- record: { version: 1, pid: value.pid!, startTime: value.startTime }
161
+ record: { version: 2, pid: pidValue, startTime }
119
162
  };
120
163
  }
164
+ if (
165
+ value !== null &&
166
+ value.version === 1 &&
167
+ typeof pidValue === 'number' &&
168
+ Number.isSafeInteger(pidValue) &&
169
+ pidValue > 0 &&
170
+ typeof value.startTime === 'string' &&
171
+ value.startTime.length > 0
172
+ ) {
173
+ return { kind: 'legacy-json', legacy: { pid: pidValue, raw, format: 'json-v1' } };
174
+ }
175
+ return { kind: 'legacy-unknown', legacy: { raw, format: 'malformed' }, reason: 'unsupported PID record schema' };
121
176
  } catch {
122
- // Invalid JSON is an invalid PID record.
177
+ return { kind: 'legacy-unknown', legacy: { raw, format: 'malformed' }, reason: 'invalid PID record JSON' };
123
178
  }
124
- return { kind: 'invalid' };
125
179
  }
126
180
 
127
181
  export function readProcessState(pidFile: string): ProcessState {
@@ -130,12 +184,13 @@ export function readProcessState(pidFile: string): ProcessState {
130
184
  snapshot = fs.readFileSync(pidFile, 'utf8');
131
185
  } catch (error) {
132
186
  if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { kind: 'missing', snapshot: null };
133
- return { kind: 'invalid', snapshot: '' };
187
+ return { kind: 'legacy-unknown', snapshot: '', legacy: { raw: '', format: 'malformed' }, reason: 'PID record cannot be read' };
134
188
  }
135
189
 
136
190
  const parsed = parsePidRecord(snapshot);
137
- if (parsed.kind === 'invalid') return { kind: 'invalid', snapshot };
138
- if (parsed.kind === 'legacy') return { kind: 'legacy', snapshot, pid: parsed.pid };
191
+ if (parsed.kind === 'legacy-json') return { kind: parsed.kind, snapshot, legacy: parsed.legacy };
192
+ if (parsed.kind === 'legacy-pid-only') return { kind: parsed.kind, snapshot, legacy: parsed.legacy };
193
+ if (parsed.kind === 'legacy-unknown') return { kind: parsed.kind, snapshot, legacy: parsed.legacy, reason: parsed.reason };
139
194
 
140
195
  const currentStartTime = getProcessStartTime(parsed.record.pid);
141
196
  if (currentStartTime === null || currentStartTime !== parsed.record.startTime) {
@@ -178,6 +233,6 @@ export function removePidRecordIfMatches(pidFile: string, expected: PidRecord):
178
233
  return removePidFileIfMatches(pidFile, state.snapshot);
179
234
  }
180
235
 
181
- export function processIdentityMatches(record: PidRecord): boolean {
182
- return getProcessStartTime(record.pid) === record.startTime;
236
+ export function processIdentityMatches(identity: ProcessIdentity): boolean {
237
+ return getProcessStartTime(identity.pid) === identity.startTime;
183
238
  }
@@ -7,6 +7,7 @@ import { locateActivityLog } from './activity-log.ts';
7
7
  import { parseImplementationInputs, selectPendingImplementationInput } from './implementation-inputs.ts';
8
8
  import { parseVerdict } from './review-artifacts.ts';
9
9
  import { extractSection, findSectionHeading } from './sections.ts';
10
+ import { receiptForOutput, sha256File } from './artifact-receipts.ts';
10
11
 
11
12
  const artifactFamilyCatalog = [
12
13
  { family: 'analysis', sectionAliases: ['分析', 'Analysis'], heading: '分析', labels: ['需求分析报告', 'Requirements Analysis'] },
@@ -16,6 +17,7 @@ const artifactFamilyCatalog = [
16
17
  { family: 'code', sectionAliases: ['实现备注', 'Implementation Notes'], heading: '实现备注', labels: ['实现报告', 'Implementation Report'] },
17
18
  { family: 'review-code', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['代码审查', 'Code Review'] },
18
19
  { family: 'manual-validation', sectionAliases: ['实现备注', 'Implementation Notes'], heading: '实现备注', labels: ['人工验证', 'Manual Validation'] },
20
+ { family: 'validation-run', sectionAliases: ['实现备注', 'Implementation Notes'], heading: '实现备注', labels: ['验证运行证据', 'Validation Run Evidence'] },
19
21
  { family: 'pr-review', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['PR 审查报告', 'PR Review Report'] }
20
22
  ] as const;
21
23
 
@@ -217,11 +219,7 @@ function resolveReviewedInput(
217
219
  diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, String(error)));
218
220
  return null;
219
221
  }
220
- const lines = content.split(/\r?\n/);
221
- const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
222
- const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
223
- const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]!);
224
- const parsed = candidates.map((name) => parseArtifactName(name)).find((item) => item?.family === expectedFamily);
222
+ const parsed = parseReviewedInputReference(content, expectedFamily);
225
223
  if (!parsed) {
226
224
  diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${review.name} does not reference a canonical ${expectedFamily} artifact`));
227
225
  return null;
@@ -230,9 +228,16 @@ function resolveReviewedInput(
230
228
  try {
231
229
  const stat = fs.lstatSync(abs);
232
230
  if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('referenced input is not a regular file');
233
- if (stat.mtimeMs > review.mtimeMs) {
234
- diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${parsed.name} is newer than ${review.name}`));
235
- return null;
231
+ const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
232
+ const receipt = receiptForOutput(taskContent, review.name);
233
+ if (!receipt) throw new Error(`receipt for ${review.name} is missing`);
234
+ const expectedEvent = reviewEventName(review.family);
235
+ if (receipt.event !== expectedEvent || receipt.input !== parsed.name) {
236
+ throw new Error(`receipt for ${review.name} does not match ${parsed.name}`);
237
+ }
238
+ const actualSha256 = sha256File(abs);
239
+ if (actualSha256 !== receipt.inputSha256) {
240
+ throw new Error(`${parsed.name} content digest does not match receipt`);
236
241
  }
237
242
  return { ...parsed, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
238
243
  } catch (error) {
@@ -241,6 +246,54 @@ function resolveReviewedInput(
241
246
  }
242
247
  }
243
248
 
249
+ function parseReviewedInputReference(content: string, expectedFamily: ArtifactFamily): { family: ArtifactFamily; round: number; name: string } | null {
250
+ const lines = content.split(/\r?\n/);
251
+ const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
252
+ const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
253
+ const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]!);
254
+ return candidates
255
+ .map((name) => parseArtifactName(name))
256
+ .find((item) => item?.family === expectedFamily) ?? null;
257
+ }
258
+
259
+ function parseCodePlanInputReference(content: string): { family: 'plan'; round: number; name: string } | null {
260
+ const lines = content.split(/\r?\n/);
261
+ const sectionStart = lines.findIndex((line) => /^##\s+(?:实现输入|Implementation Input)\s*$/.test(line.trim()));
262
+ if (sectionStart < 0) return null;
263
+ const bodyLines = lines.slice(sectionStart + 1);
264
+ const nextSection = bodyLines.findIndex((line) => /^##\s+/.test(line.trim()));
265
+ const body = (nextSection < 0 ? bodyLines : bodyLines.slice(0, nextSection)).join('\n');
266
+ const match = body.match(/\*\*(?:方案输入|Plan Input)\*\*[::]\s*`([^`]+\.md)`/);
267
+ if (!match) return null;
268
+ const parsed = parseArtifactName(match[1]!);
269
+ return parsed?.family === 'plan' ? { ...parsed, family: 'plan' } : null;
270
+ }
271
+
272
+ function reviewEventName(family: ArtifactFamily): 'review-analysis.completed' | 'review-plan.completed' | 'review-code.completed' {
273
+ if (family === 'review-analysis') return 'review-analysis.completed';
274
+ if (family === 'review-plan') return 'review-plan.completed';
275
+ if (family === 'review-code') return 'review-code.completed';
276
+ throw new Error(`unsupported review family '${family}'`);
277
+ }
278
+
279
+ function resolveCodeInputReceipt(taskDir: string, code: ArtifactIdentity): { input: ArtifactIdentity; inputSha256: string } | null {
280
+ let taskContent: string;
281
+ try { taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'); }
282
+ catch { return null; }
283
+ let receipt;
284
+ try { receipt = receiptForOutput(taskContent, code.name); }
285
+ catch { return null; }
286
+ if (!receipt || receipt.event !== 'code.completed') return null;
287
+ const plan = inspectArtifactDirectory(taskDir, 'plan');
288
+ if (plan.status !== 'ready') return null;
289
+ const input = plan.artifacts.find((artifact) => artifact.name === receipt.input);
290
+ return input ? { input, inputSha256: receipt.inputSha256 } : null;
291
+ }
292
+
293
+ function resolveCodePlanInput(taskDir: string, code: ArtifactIdentity): { input: ArtifactIdentity; inputSha256: string } | null {
294
+ return resolveCodeInputReceipt(taskDir, code);
295
+ }
296
+
244
297
  function assertWritableInventory(inventory: ArtifactInventoryResult): ArtifactError | null {
245
298
  if (inventory.status === 'failed') return inventory.error;
246
299
  const blockers = inventory.diagnostics.filter((item) => BLOCKING_DIAGNOSTICS.has(item.code));
@@ -257,7 +310,8 @@ const OPTIONAL_CONTEXT: Partial<Record<ArtifactFamily, { family: ArtifactFamily;
257
310
  analysis: { family: 'review-analysis', requireReference: true },
258
311
  plan: { family: 'review-plan', requireReference: true },
259
312
  'review-code': { family: 'review-plan' },
260
- 'manual-validation': { family: 'review-code' }
313
+ 'manual-validation': { family: 'review-code' },
314
+ 'validation-run': { family: 'review-code' }
261
315
  };
262
316
 
263
317
  function resolveArtifactContext(taskRef: string, family: string, options: InspectOptions = {}): ArtifactContextResult {
@@ -310,11 +364,21 @@ function resolveCodeContext(inventory: ArtifactInventoryResult, options: Inspect
310
364
  return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null,
311
365
  'No prior code artifact. Starting initial implementation (round 1 -> code.md).');
312
366
  }
367
+ const codePlanInput = resolveCodePlanInput(inventory.taskDir!, latestCode);
368
+ if (!codePlanInput) {
369
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `code completion receipt for ${latestCode.name} is missing or does not match the current plan`, latestCode.name);
370
+ }
371
+ if (reviewPlan.latest && !reviewPlan.reviewedInput) {
372
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `${reviewPlan.latest.name} has no valid reviewed input`, reviewPlan.latest.name);
373
+ }
313
374
  if (reviewPlan.latest && reviewPlan.reviewedInput?.name === plan.latest.name) {
314
375
  const verdict = parseVerdict(reviewPlan.latest.path);
315
- if (verdict.ok && verdict.verdict === 'Approved' && reviewPlan.latest.mtimeMs > latestCode.mtimeMs) {
376
+ const reviewedPlanSha256 = sha256File(reviewPlan.reviewedInput.path);
377
+ if (verdict.ok && verdict.verdict === 'Approved' && (
378
+ codePlanInput.input.name !== plan.latest.name || codePlanInput.inputSha256 !== reviewedPlanSha256
379
+ )) {
316
380
  return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, verdict.verdict, reviewPlan.latest.name,
317
- `Latest ${reviewPlan.latest.name} is approved and newer than the latest code artifact. Entering replan-driven init.`);
381
+ `Latest ${reviewPlan.latest.name} approves plan content not captured by the latest code input receipt. Entering replan-driven init.`);
318
382
  }
319
383
  }
320
384
  if (reviewMax < codeMax) {
@@ -376,12 +440,11 @@ function resolveDecisionImplementationInput(
376
440
  if (pending.length === 0) return { input: null };
377
441
  const activity = locateActivityLog(content);
378
442
  if (!activity) return { error: 'task has no unique Activity Log section' };
379
- const reviewEntries = activity.entries.filter((entry) =>
443
+ const completed = activity.entries.some((entry) =>
380
444
  /^Review Code \(Round \d+\)$/.test(entry.step) && entry.note.includes(`→ ${review.name}`)
381
445
  );
382
- const completed = reviewEntries.at(-1);
383
446
  if (!completed) return { error: `cannot find completed Activity Log identity for ${review.name}` };
384
- try { return { input: selectPendingImplementationInput(rows, completed.time) }; }
447
+ try { return { input: selectPendingImplementationInput(rows) }; }
385
448
  catch (error) { return { error: error instanceof Error ? error.message : String(error) }; }
386
449
  }
387
450
 
@@ -439,6 +502,7 @@ function buildArtifactLinkSection(content: string, artifact: ArtifactIdentity):
439
502
  export {
440
503
  artifactFamilyCatalog, artifactName, parseArtifactName, familySpec,
441
504
  inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext,
505
+ parseReviewedInputReference, parseCodePlanInputReference, resolveCodePlanInput, reviewEventName,
442
506
  assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection
443
507
  };
444
508
  export type {
@@ -0,0 +1,161 @@
1
+ import fs from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+
4
+ import { extractSection, findSectionHeading, parseTable } from './sections.ts';
5
+
6
+ const RECEIPT_SECTION_ALIASES = ['产物生命周期收据', 'Artifact Lifecycle Receipts'] as const;
7
+ const RECEIPT_COLUMNS = ['event', 'output', 'input', 'input_sha256', 'completed_at'] as const;
8
+ const RECEIPT_EVENTS = new Set([
9
+ 'review-analysis.completed',
10
+ 'review-plan.completed',
11
+ 'review-code.completed',
12
+ 'code.completed'
13
+ ]);
14
+ const SHA256_RE = /^[a-f0-9]{64}$/;
15
+ const ARTIFACT_NAME_RE = /^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|validation-run|pr-review)(?:-r([2-9]|[1-9]\d+))?\.md$/;
16
+ const RECEIPT_SHAPES = {
17
+ 'review-analysis.completed': { output: 'review-analysis', input: 'analysis' },
18
+ 'review-plan.completed': { output: 'review-plan', input: 'plan' },
19
+ 'review-code.completed': { output: 'review-code', input: 'code' },
20
+ 'code.completed': { output: 'code', input: 'plan' }
21
+ } as const;
22
+
23
+ type ArtifactReceiptEvent =
24
+ | 'review-analysis.completed'
25
+ | 'review-plan.completed'
26
+ | 'review-code.completed'
27
+ | 'code.completed';
28
+ type ArtifactReceipt = {
29
+ event: ArtifactReceiptEvent;
30
+ output: string;
31
+ input: string;
32
+ inputSha256: string;
33
+ completedAt: string;
34
+ };
35
+ type ArtifactReceiptParseResult = {
36
+ present: boolean;
37
+ rows: readonly ArtifactReceipt[];
38
+ };
39
+ type ReceiptSectionMutation = {
40
+ aliases: readonly string[];
41
+ heading: string;
42
+ body: string;
43
+ };
44
+
45
+ class ArtifactReceiptError extends Error {
46
+ readonly code = 'ARTIFACT_RECEIPT_INVALID';
47
+
48
+ constructor(message: string) {
49
+ super(message);
50
+ this.name = 'ArtifactReceiptError';
51
+ }
52
+ }
53
+
54
+ function sha256Bytes(value: Buffer): string {
55
+ return createHash('sha256').update(value).digest('hex');
56
+ }
57
+
58
+ function sha256File(filePath: string): string {
59
+ return sha256Bytes(fs.readFileSync(filePath));
60
+ }
61
+
62
+ function parseReceiptArtifactName(name: string): { family: string } | null {
63
+ const match = ARTIFACT_NAME_RE.exec(name);
64
+ if (!match) return null;
65
+ const roundText = match[2];
66
+ if (roundText !== undefined) {
67
+ const round = Number(roundText);
68
+ if (!Number.isSafeInteger(round) || String(round) !== roundText) return null;
69
+ }
70
+ return { family: match[1]! };
71
+ }
72
+
73
+ function validateReceiptShape(event: ArtifactReceiptEvent, output: string, input: string): void {
74
+ const outputIdentity = parseReceiptArtifactName(output);
75
+ const inputIdentity = parseReceiptArtifactName(input);
76
+ if (!outputIdentity || !inputIdentity) throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
77
+ const shape = RECEIPT_SHAPES[event];
78
+ if (outputIdentity.family !== shape.output || inputIdentity.family !== shape.input) {
79
+ throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
80
+ }
81
+ }
82
+
83
+ function parseArtifactReceipts(content: string): ArtifactReceiptParseResult {
84
+ const section = extractSection(content, [...RECEIPT_SECTION_ALIASES]);
85
+ if (!section) return { present: false, rows: [] };
86
+
87
+ let table;
88
+ try {
89
+ table = parseTable(content, {
90
+ sectionAliases: [...RECEIPT_SECTION_ALIASES],
91
+ columns: [...RECEIPT_COLUMNS],
92
+ keyColumn: 'output'
93
+ });
94
+ } catch (error) {
95
+ throw new ArtifactReceiptError(error instanceof Error ? error.message : String(error));
96
+ }
97
+ if (!table) throw new ArtifactReceiptError('receipt section has no receipt table');
98
+
99
+ const rows = table.rows.map((row) => {
100
+ const event = row.values.event ?? '';
101
+ const output = row.values.output ?? '';
102
+ const input = row.values.input ?? '';
103
+ const inputSha256 = row.values.input_sha256 ?? '';
104
+ const completedAt = row.values.completed_at ?? '';
105
+ if (!RECEIPT_EVENTS.has(event)) throw new ArtifactReceiptError(`unknown receipt event '${event}'`);
106
+ if (!output || !input) throw new ArtifactReceiptError('receipt output and input are required');
107
+ validateReceiptShape(event as ArtifactReceiptEvent, output, input);
108
+ if (!SHA256_RE.test(inputSha256)) throw new ArtifactReceiptError(`receipt digest for '${output}' is invalid`);
109
+ if (!completedAt || Number.isNaN(Date.parse(completedAt.replace(' ', 'T')))) {
110
+ throw new ArtifactReceiptError(`receipt completion time for '${output}' is invalid`);
111
+ }
112
+ return {
113
+ event: event as ArtifactReceiptEvent,
114
+ output,
115
+ input,
116
+ inputSha256,
117
+ completedAt
118
+ };
119
+ });
120
+ return { present: true, rows };
121
+ }
122
+
123
+ function receiptForOutput(content: string, output: string): ArtifactReceipt | null {
124
+ const parsed = parseArtifactReceipts(content);
125
+ return parsed.rows.find((row) => row.output === output) ?? null;
126
+ }
127
+
128
+ function upsertArtifactReceipt(content: string, receipt: ArtifactReceipt): ReceiptSectionMutation {
129
+ if (!RECEIPT_EVENTS.has(receipt.event)) throw new ArtifactReceiptError(`unknown receipt event '${receipt.event}'`);
130
+ if (!receipt.output || !receipt.input) throw new ArtifactReceiptError('receipt output and input are required');
131
+ validateReceiptShape(receipt.event, receipt.output, receipt.input);
132
+ if (!SHA256_RE.test(receipt.inputSha256)) throw new ArtifactReceiptError(`receipt digest for '${receipt.output}' is invalid`);
133
+ if (!receipt.completedAt || Number.isNaN(Date.parse(receipt.completedAt.replace(' ', 'T')))) {
134
+ throw new ArtifactReceiptError(`receipt completion time for '${receipt.output}' is invalid`);
135
+ }
136
+
137
+ const existing = parseArtifactReceipts(content).rows;
138
+ const previous = existing.find((row) => row.output === receipt.output);
139
+ if (previous && JSON.stringify(previous) !== JSON.stringify(receipt)) {
140
+ throw new ArtifactReceiptError(`receipt for '${receipt.output}' already exists with different evidence`);
141
+ }
142
+ const rows = previous ? existing : [...existing, receipt];
143
+ const heading = findSectionHeading(content, [...RECEIPT_SECTION_ALIASES]);
144
+ const body = [
145
+ `| ${RECEIPT_COLUMNS.join(' | ')} |`,
146
+ `| ${RECEIPT_COLUMNS.map(() => '---').join(' | ')} |`,
147
+ ...rows.map((row) => `| ${row.event} | ${row.output} | ${row.input} | ${row.inputSha256} | ${row.completedAt} |`)
148
+ ].join('\n');
149
+ return { aliases: [...RECEIPT_SECTION_ALIASES], heading, body };
150
+ }
151
+
152
+ export {
153
+ ArtifactReceiptError,
154
+ RECEIPT_SECTION_ALIASES,
155
+ parseArtifactReceipts,
156
+ receiptForOutput,
157
+ sha256Bytes,
158
+ sha256File,
159
+ upsertArtifactReceipt
160
+ };
161
+ export type { ArtifactReceipt, ArtifactReceiptEvent, ArtifactReceiptParseResult, ReceiptSectionMutation };