@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.
- package/dist/bin/internal-cli.js +8 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -4
- package/dist/lib/internal/codex-lifecycle.js +169 -3
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +12 -4
- package/dist/lib/internal/task-orchestration.js +40 -7
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +111 -60
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +372 -94
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/client.js +138 -22
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +142 -0
- package/dist/lib/sandbox/control/lifecycle.js +557 -0
- package/dist/lib/sandbox/control/protocol.js +69 -26
- package/dist/lib/sandbox/control/server.js +395 -99
- package/dist/lib/sandbox/control/state.js +191 -0
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +22 -19
- package/dist/lib/sandbox/recovery.js +284 -50
- package/dist/lib/sandbox/workspace-view.js +90 -10
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +17 -1
- package/dist/lib/server/process-state.js +57 -20
- package/dist/lib/task/artifact-lifecycle.js +87 -15
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +236 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +36 -9
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/delegation-receipts.js +109 -2
- package/dist/lib/task/events.js +149 -6
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +365 -15
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +78 -14
- package/dist/lib/task/verification.js +1 -0
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -3
- package/lib/internal/codex-lifecycle.ts +175 -4
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +16 -4
- package/lib/internal/task-orchestration.ts +34 -7
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +70 -25
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +423 -108
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/client.ts +152 -28
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +167 -0
- package/lib/sandbox/control/lifecycle.ts +573 -0
- package/lib/sandbox/control/protocol.ts +121 -68
- package/lib/sandbox/control/server.ts +398 -109
- package/lib/sandbox/control/state.ts +213 -0
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +16 -8
- package/lib/sandbox/recovery.ts +335 -65
- package/lib/sandbox/workspace-view.ts +102 -11
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +24 -3
- package/lib/server/process-state.ts +82 -27
- package/lib/task/artifact-lifecycle.ts +78 -14
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +319 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +52 -9
- package/lib/task/create.ts +11 -2
- package/lib/task/delegation-receipts.ts +208 -7
- package/lib/task/events.ts +131 -5
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/ledger.ts +16 -1
- package/lib/task/orchestration.ts +552 -18
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -24
- package/lib/task/verification.ts +2 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +8 -8
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
- package/templates/.agents/workspace/README.en.md +0 -26
- 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
|
|
6
|
-
|
|
5
|
+
export type ProcessStartTime = number;
|
|
6
|
+
|
|
7
|
+
export type ProcessIdentity = Readonly<{
|
|
7
8
|
pid: number;
|
|
8
|
-
startTime:
|
|
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: '
|
|
16
|
-
| { kind: 'legacy'; snapshot: string;
|
|
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
|
|
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
|
-
|
|
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
|
-
):
|
|
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, {
|
|
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.
|
|
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(
|
|
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)
|
|
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
|
|
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 ===
|
|
111
|
-
|
|
112
|
-
(
|
|
113
|
-
|
|
114
|
-
|
|
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:
|
|
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
|
-
|
|
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: '
|
|
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 === '
|
|
138
|
-
if (parsed.kind === 'legacy') return { kind:
|
|
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(
|
|
182
|
-
return getProcessStartTime(
|
|
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
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
|
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
|
|
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 };
|