@fitlab-ai/agent-infra 0.9.6 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +10 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +47 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +245 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +398 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +23 -6
- package/dist/lib/decide.js +68 -14
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -3
- package/dist/lib/init.js +1 -1
- package/dist/lib/internal/codex-lifecycle.js +69 -1
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/release-workflow.js +1 -3
- package/dist/lib/internal/task-activity.js +57 -7
- package/dist/lib/internal/task-event.js +57 -4
- package/dist/lib/internal/task-ledger.js +59 -5
- package/dist/lib/internal/task-lifecycle.js +59 -8
- package/dist/lib/internal/task-orchestration.js +48 -8
- package/dist/lib/internal/task-override.js +131 -0
- package/dist/lib/internal/task-review.js +64 -6
- package/dist/lib/internal/task-verify.js +65 -3
- package/dist/lib/internal/task-warning.js +57 -4
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/pr-summary.js +5 -3
- 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/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +394 -264
- package/dist/lib/sandbox/commands/rm.js +64 -15
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +23 -9
- package/dist/lib/sandbox/control/lifecycle.js +590 -61
- package/dist/lib/sandbox/control/protocol.js +7 -0
- package/dist/lib/sandbox/control/server.js +202 -26
- package/dist/lib/sandbox/control/state.js +51 -25
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/mounts.js +7 -0
- package/dist/lib/sandbox/recovery.js +135 -50
- package/dist/lib/sandbox/workspace-view.js +59 -20
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +11 -1
- package/dist/lib/server/process-state.js +107 -20
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/artifact-lifecycle.js +82 -11
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +81 -6
- package/dist/lib/task/commands/validate.js +1 -1
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +120 -17
- package/dist/lib/task/events.js +148 -7
- package/dist/lib/task/guard-override.js +31 -0
- package/dist/lib/task/human-override.js +1240 -0
- package/dist/lib/task/ledger-intents.js +6 -3
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/lifecycle.js +69 -14
- package/dist/lib/task/orchestration.js +293 -18
- package/dist/lib/task/review-finalization.js +3 -2
- 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 +80 -22
- package/dist/lib/task/verification.js +13 -1
- package/dist/lib/task/workflow-warning-intents.js +4 -2
- package/dist/lib/task/write.js +2 -1
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +62 -4
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +322 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +480 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +35 -5
- package/lib/decide.ts +59 -14
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -2
- package/lib/init.ts +1 -1
- package/lib/internal/codex-lifecycle.ts +68 -1
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/release-workflow.ts +1 -3
- package/lib/internal/task-activity.ts +46 -6
- package/lib/internal/task-event.ts +53 -4
- package/lib/internal/task-ledger.ts +48 -4
- package/lib/internal/task-lifecycle.ts +56 -12
- package/lib/internal/task-orchestration.ts +43 -8
- package/lib/internal/task-override.ts +138 -0
- package/lib/internal/task-review.ts +51 -6
- package/lib/internal/task-verify.ts +52 -3
- package/lib/internal/task-warning.ts +44 -3
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/pr-summary.ts +10 -3
- 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/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +139 -6
- package/lib/sandbox/commands/rm.ts +64 -14
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +24 -12
- package/lib/sandbox/control/lifecycle.ts +685 -67
- package/lib/sandbox/control/protocol.ts +35 -6
- package/lib/sandbox/control/server.ts +184 -24
- package/lib/sandbox/control/state.ts +67 -33
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/mounts.ts +8 -0
- package/lib/sandbox/recovery.ts +159 -49
- package/lib/sandbox/workspace-view.ts +73 -22
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +13 -3
- package/lib/server/process-state.ts +129 -27
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/artifact-lifecycle.ts +73 -10
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +91 -5
- package/lib/task/commands/validate.ts +1 -1
- package/lib/task/create.ts +11 -2
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +205 -25
- package/lib/task/events.ts +132 -6
- package/lib/task/guard-override.ts +50 -0
- package/lib/task/human-override.ts +1503 -0
- package/lib/task/ledger-intents.ts +6 -3
- package/lib/task/ledger.ts +16 -1
- package/lib/task/lifecycle.ts +71 -14
- package/lib/task/orchestration.ts +443 -22
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -32
- package/lib/task/verification.ts +15 -1
- package/lib/task/workflow-warning-intents.ts +4 -2
- package/lib/task/write.ts +4 -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 +33 -4
- package/templates/.agents/rules/lifecycle-orchestration.en.md +10 -3
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +10 -3
- 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/run-manual-validation/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
- 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 +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -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 -1
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
package/lib/server/daemon.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
removePidFileIfMatches,
|
|
12
12
|
writePidRecord
|
|
13
13
|
} from './process-state.ts';
|
|
14
|
-
import type { PidRecord } from './process-state.ts';
|
|
14
|
+
import type { PidRecord, ProcessIdentity, ProcessState } from './process-state.ts';
|
|
15
15
|
|
|
16
16
|
export { isProcessAlive } from './process-state.ts';
|
|
17
17
|
|
|
@@ -51,13 +51,13 @@ async function waitForProcessIdentity(pid: number, timeoutMs = 2000): Promise<Pi
|
|
|
51
51
|
const deadline = Date.now() + timeoutMs;
|
|
52
52
|
while (Date.now() < deadline) {
|
|
53
53
|
const startTime = getProcessStartTime(pid);
|
|
54
|
-
if (startTime !== null) return { version:
|
|
54
|
+
if (startTime !== null) return { version: 2, pid, startTime };
|
|
55
55
|
await delay(25);
|
|
56
56
|
}
|
|
57
57
|
return null;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function terminateIfMatching(record:
|
|
60
|
+
function terminateIfMatching(record: ProcessIdentity): void {
|
|
61
61
|
if (!processIdentityMatches(record)) return;
|
|
62
62
|
const command = buildStopCommand(record.pid, process.platform);
|
|
63
63
|
try {
|
|
@@ -83,6 +83,9 @@ export async function start({ foreground = false }: StartOptions = {}): Promise<
|
|
|
83
83
|
process.stdout.write(`server already running (pid ${existing.record.pid})\n`);
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
|
+
if (existing.kind === 'legacy-json' || existing.kind === 'legacy-pid-only' || existing.kind === 'legacy-unknown') {
|
|
87
|
+
throw new Error('server: legacy pid record requires the matching previous version to stop it before starting');
|
|
88
|
+
}
|
|
86
89
|
if (existing.kind !== 'missing') {
|
|
87
90
|
removePidFileIfMatches(pidPath, existing.snapshot);
|
|
88
91
|
}
|
|
@@ -135,6 +138,9 @@ export async function stop(): Promise<void> {
|
|
|
135
138
|
return;
|
|
136
139
|
}
|
|
137
140
|
if (state.kind !== 'running') {
|
|
141
|
+
if (state.kind === 'legacy-json' || state.kind === 'legacy-pid-only' || state.kind === 'legacy-unknown') {
|
|
142
|
+
throw new Error('server: legacy pid record requires the matching previous version to stop it');
|
|
143
|
+
}
|
|
138
144
|
removePidFileIfMatches(config.pidFile, state.snapshot);
|
|
139
145
|
process.stdout.write('server is not running (removed stale pid file)\n');
|
|
140
146
|
return;
|
|
@@ -170,6 +176,10 @@ export function status(): void {
|
|
|
170
176
|
const state = readProcessState(config.pidFile);
|
|
171
177
|
|
|
172
178
|
if (state.kind !== 'running') {
|
|
179
|
+
if (state.kind === 'legacy-json' || state.kind === 'legacy-pid-only' || state.kind === 'legacy-unknown') {
|
|
180
|
+
process.stdout.write('server: legacy pid record requires the matching previous version to stop it\n');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
173
183
|
if (state.kind !== 'missing') removePidFileIfMatches(config.pidFile, state.snapshot);
|
|
174
184
|
process.stdout.write('server: stopped\n');
|
|
175
185
|
return;
|
|
@@ -2,22 +2,37 @@ 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 ProcessIdentityState = 'alive' | 'dead' | 'unknown';
|
|
13
|
+
export type ProcessIdentityProbe = (identity: ProcessIdentity) => ProcessIdentityState;
|
|
14
|
+
|
|
15
|
+
export type PidRecord = ProcessIdentity & {
|
|
16
|
+
version: 2;
|
|
9
17
|
};
|
|
10
18
|
|
|
11
19
|
export type ProcessStartTimeQuery = { command: string; args: string[] };
|
|
12
20
|
|
|
13
21
|
export type ProcessState =
|
|
14
22
|
| { kind: 'missing'; snapshot: null }
|
|
15
|
-
| { kind: '
|
|
16
|
-
| { kind: 'legacy'; snapshot: string;
|
|
23
|
+
| { kind: 'legacy-json'; snapshot: string; legacy: LegacyProcessSnapshot }
|
|
24
|
+
| { kind: 'legacy-pid-only'; snapshot: string; legacy: LegacyProcessSnapshot }
|
|
25
|
+
| { kind: 'legacy-unknown'; snapshot: string; legacy: LegacyProcessSnapshot; reason: string }
|
|
17
26
|
| { kind: 'stale'; snapshot: string; record: PidRecord }
|
|
18
27
|
| { kind: 'running'; snapshot: string; record: PidRecord };
|
|
19
28
|
|
|
20
|
-
export type
|
|
29
|
+
export type LegacyProcessSnapshot = Readonly<{
|
|
30
|
+
pid?: number;
|
|
31
|
+
raw: string;
|
|
32
|
+
format: 'json-v1' | 'pid-only' | 'malformed';
|
|
33
|
+
}>;
|
|
34
|
+
|
|
35
|
+
export type LinuxProcessStat = { state: string; startTime: number };
|
|
21
36
|
|
|
22
37
|
export function parseLinuxProcessStat(raw: string): LinuxProcessStat | null {
|
|
23
38
|
const closeParen = raw.lastIndexOf(')');
|
|
@@ -28,7 +43,9 @@ export function parseLinuxProcessStat(raw: string): LinuxProcessStat | null {
|
|
|
28
43
|
if (typeof state !== 'string' || state.length !== 1 || !/^\d+$/.test(startTime ?? '')) {
|
|
29
44
|
return null;
|
|
30
45
|
}
|
|
31
|
-
|
|
46
|
+
const numericStartTime = Number(startTime);
|
|
47
|
+
if (!Number.isSafeInteger(numericStartTime)) return null;
|
|
48
|
+
return { state, startTime: numericStartTime };
|
|
32
49
|
}
|
|
33
50
|
|
|
34
51
|
export function buildProcessStartTimeQuery(
|
|
@@ -68,10 +85,22 @@ export function isProcessAlive(pid: number, platform: NodeJS.Platform = process.
|
|
|
68
85
|
}
|
|
69
86
|
}
|
|
70
87
|
|
|
88
|
+
function parseDarwinStartTime(output: string): number | null {
|
|
89
|
+
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})$/);
|
|
90
|
+
if (!match) return null;
|
|
91
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
92
|
+
const month = months.indexOf(match[1]!);
|
|
93
|
+
if (month < 0) return null;
|
|
94
|
+
const timestamp = Date.UTC(
|
|
95
|
+
Number(match[6]), month, Number(match[2]), Number(match[3]), Number(match[4]), Number(match[5])
|
|
96
|
+
);
|
|
97
|
+
return Number.isNaN(timestamp) ? null : timestamp;
|
|
98
|
+
}
|
|
99
|
+
|
|
71
100
|
export function getProcessStartTime(
|
|
72
101
|
pid: number,
|
|
73
102
|
platform: NodeJS.Platform = process.platform
|
|
74
|
-
):
|
|
103
|
+
): ProcessStartTime | null {
|
|
75
104
|
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
76
105
|
if (platform === 'linux') {
|
|
77
106
|
try {
|
|
@@ -85,43 +114,115 @@ export function getProcessStartTime(
|
|
|
85
114
|
const query = buildProcessStartTimeQuery(pid, platform);
|
|
86
115
|
if (query === null) return null;
|
|
87
116
|
try {
|
|
88
|
-
const output = execFileSync(query.command, query.args, {
|
|
117
|
+
const output = execFileSync(query.command, query.args, {
|
|
118
|
+
encoding: 'utf8',
|
|
119
|
+
env: platform === 'darwin' ? { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } : process.env
|
|
120
|
+
}).trim();
|
|
89
121
|
if (output.length === 0) return null;
|
|
90
122
|
if (platform === 'win32') {
|
|
91
123
|
const timestamp = new Date(output);
|
|
92
|
-
return Number.isNaN(timestamp.getTime()) ? null : timestamp.
|
|
124
|
+
return Number.isNaN(timestamp.getTime()) ? null : timestamp.getTime();
|
|
93
125
|
}
|
|
94
|
-
return output;
|
|
126
|
+
if (platform === 'darwin') return parseDarwinStartTime(output);
|
|
127
|
+
return null;
|
|
95
128
|
} catch {
|
|
96
129
|
return null;
|
|
97
130
|
}
|
|
98
131
|
}
|
|
99
132
|
|
|
100
|
-
function
|
|
133
|
+
export function getProcessIdentityState(
|
|
134
|
+
identity: ProcessIdentity,
|
|
135
|
+
platform: NodeJS.Platform = process.platform
|
|
136
|
+
): ProcessIdentityState {
|
|
137
|
+
if (!Number.isInteger(identity.pid) || identity.pid <= 0 || !Number.isSafeInteger(identity.startTime)) {
|
|
138
|
+
return 'dead';
|
|
139
|
+
}
|
|
140
|
+
if (platform === 'linux') {
|
|
141
|
+
try {
|
|
142
|
+
const stat = parseLinuxProcessStat(fs.readFileSync(`/proc/${identity.pid}/stat`, 'utf8'));
|
|
143
|
+
if (stat === null) return 'unknown';
|
|
144
|
+
if (stat.state === 'Z') return 'dead';
|
|
145
|
+
return stat.startTime === identity.startTime ? 'alive' : 'dead';
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return (error as NodeJS.ErrnoException).code === 'ENOENT' ? 'dead' : 'unknown';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
process.kill(identity.pid, 0);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
154
|
+
if (code === 'ESRCH') return 'dead';
|
|
155
|
+
if (code !== 'EPERM') return 'unknown';
|
|
156
|
+
}
|
|
157
|
+
const query = buildProcessStartTimeQuery(identity.pid, platform);
|
|
158
|
+
if (query === null) return 'unknown';
|
|
159
|
+
try {
|
|
160
|
+
const output = execFileSync(query.command, query.args, {
|
|
161
|
+
encoding: 'utf8',
|
|
162
|
+
env: platform === 'darwin' ? { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } : process.env
|
|
163
|
+
}).trim();
|
|
164
|
+
if (output.length === 0) return 'unknown';
|
|
165
|
+
const startTime = platform === 'win32'
|
|
166
|
+
? new Date(output).getTime()
|
|
167
|
+
: platform === 'darwin'
|
|
168
|
+
? parseDarwinStartTime(output)
|
|
169
|
+
: null;
|
|
170
|
+
if (startTime === null || Number.isNaN(startTime)) return 'unknown';
|
|
171
|
+
return startTime === identity.startTime ? 'alive' : 'dead';
|
|
172
|
+
} catch {
|
|
173
|
+
return 'unknown';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function parsePidRecord(
|
|
178
|
+
raw: string
|
|
179
|
+
):
|
|
180
|
+
| { kind: 'legacy-json'; legacy: LegacyProcessSnapshot }
|
|
181
|
+
| { kind: 'legacy-pid-only'; legacy: LegacyProcessSnapshot }
|
|
182
|
+
| { kind: 'legacy-unknown'; legacy: LegacyProcessSnapshot; reason: string }
|
|
183
|
+
| { kind: 'record'; record: PidRecord } {
|
|
101
184
|
const trimmed = raw.trim();
|
|
102
185
|
if (/^[1-9]\d*$/.test(trimmed)) {
|
|
103
186
|
const pid = Number(trimmed);
|
|
104
|
-
return Number.isSafeInteger(pid)
|
|
187
|
+
return Number.isSafeInteger(pid)
|
|
188
|
+
? { kind: 'legacy-pid-only', legacy: { pid, raw, format: 'pid-only' } }
|
|
189
|
+
: { kind: 'legacy-unknown', legacy: { raw, format: 'malformed' }, reason: 'pid is not a safe integer' };
|
|
105
190
|
}
|
|
106
191
|
try {
|
|
107
|
-
const value = JSON.parse(trimmed) as
|
|
192
|
+
const value = JSON.parse(trimmed) as { version?: unknown; pid?: unknown; startTime?: unknown } | null;
|
|
193
|
+
const pidValue: unknown = value?.pid;
|
|
194
|
+
const startTimeValue: unknown = value?.startTime;
|
|
108
195
|
if (
|
|
109
196
|
value !== null &&
|
|
110
|
-
value.version ===
|
|
111
|
-
|
|
112
|
-
(
|
|
113
|
-
|
|
114
|
-
|
|
197
|
+
value.version === 2 &&
|
|
198
|
+
typeof pidValue === 'number' &&
|
|
199
|
+
Number.isSafeInteger(pidValue) &&
|
|
200
|
+
pidValue > 0 &&
|
|
201
|
+
typeof startTimeValue === 'number' &&
|
|
202
|
+
Number.isSafeInteger(startTimeValue) &&
|
|
203
|
+
startTimeValue >= 0
|
|
115
204
|
) {
|
|
205
|
+
const startTime = startTimeValue as number;
|
|
116
206
|
return {
|
|
117
207
|
kind: 'record',
|
|
118
|
-
record: { version:
|
|
208
|
+
record: { version: 2, pid: pidValue, startTime }
|
|
119
209
|
};
|
|
120
210
|
}
|
|
211
|
+
if (
|
|
212
|
+
value !== null &&
|
|
213
|
+
value.version === 1 &&
|
|
214
|
+
typeof pidValue === 'number' &&
|
|
215
|
+
Number.isSafeInteger(pidValue) &&
|
|
216
|
+
pidValue > 0 &&
|
|
217
|
+
typeof value.startTime === 'string' &&
|
|
218
|
+
value.startTime.length > 0
|
|
219
|
+
) {
|
|
220
|
+
return { kind: 'legacy-json', legacy: { pid: pidValue, raw, format: 'json-v1' } };
|
|
221
|
+
}
|
|
222
|
+
return { kind: 'legacy-unknown', legacy: { raw, format: 'malformed' }, reason: 'unsupported PID record schema' };
|
|
121
223
|
} catch {
|
|
122
|
-
|
|
224
|
+
return { kind: 'legacy-unknown', legacy: { raw, format: 'malformed' }, reason: 'invalid PID record JSON' };
|
|
123
225
|
}
|
|
124
|
-
return { kind: 'invalid' };
|
|
125
226
|
}
|
|
126
227
|
|
|
127
228
|
export function readProcessState(pidFile: string): ProcessState {
|
|
@@ -130,12 +231,13 @@ export function readProcessState(pidFile: string): ProcessState {
|
|
|
130
231
|
snapshot = fs.readFileSync(pidFile, 'utf8');
|
|
131
232
|
} catch (error) {
|
|
132
233
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { kind: 'missing', snapshot: null };
|
|
133
|
-
return { kind: '
|
|
234
|
+
return { kind: 'legacy-unknown', snapshot: '', legacy: { raw: '', format: 'malformed' }, reason: 'PID record cannot be read' };
|
|
134
235
|
}
|
|
135
236
|
|
|
136
237
|
const parsed = parsePidRecord(snapshot);
|
|
137
|
-
if (parsed.kind === '
|
|
138
|
-
if (parsed.kind === 'legacy') return { kind:
|
|
238
|
+
if (parsed.kind === 'legacy-json') return { kind: parsed.kind, snapshot, legacy: parsed.legacy };
|
|
239
|
+
if (parsed.kind === 'legacy-pid-only') return { kind: parsed.kind, snapshot, legacy: parsed.legacy };
|
|
240
|
+
if (parsed.kind === 'legacy-unknown') return { kind: parsed.kind, snapshot, legacy: parsed.legacy, reason: parsed.reason };
|
|
139
241
|
|
|
140
242
|
const currentStartTime = getProcessStartTime(parsed.record.pid);
|
|
141
243
|
if (currentStartTime === null || currentStartTime !== parsed.record.startTime) {
|
|
@@ -178,6 +280,6 @@ export function removePidRecordIfMatches(pidFile: string, expected: PidRecord):
|
|
|
178
280
|
return removePidFileIfMatches(pidFile, state.snapshot);
|
|
179
281
|
}
|
|
180
282
|
|
|
181
|
-
export function processIdentityMatches(
|
|
182
|
-
return
|
|
283
|
+
export function processIdentityMatches(identity: ProcessIdentity): boolean {
|
|
284
|
+
return getProcessIdentityState(identity) === 'alive';
|
|
183
285
|
}
|
|
@@ -14,6 +14,7 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
|
|
|
14
14
|
import type { TaskExecutionLockOptions } from './task-execution-lock.ts';
|
|
15
15
|
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
16
16
|
import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
|
|
17
|
+
import { allowsManualOverride } from './guard-override.ts';
|
|
17
18
|
|
|
18
19
|
type PrReviewVerdict = 'approved' | 'changes-requested' | 'commented';
|
|
19
20
|
type PrReviewOutcome = 'aborted' | 'superseded';
|
|
@@ -95,7 +96,7 @@ export type PrReviewInspectionResult = {
|
|
|
95
96
|
error: { code: string; message: string } | null;
|
|
96
97
|
};
|
|
97
98
|
|
|
98
|
-
type ActivityIntentOptions = TaskWriteOptions & { lockOptions?: TaskExecutionLockOptions };
|
|
99
|
+
type ActivityIntentOptions = TaskWriteOptions & { lockOptions?: TaskExecutionLockOptions; lockAlreadyHeld?: boolean };
|
|
99
100
|
type ArtifactRecord = { identity: ArtifactIdentity; head: string; status: PrReviewArtifactStatus };
|
|
100
101
|
type RoundState = ArtifactRecord & {
|
|
101
102
|
startedAgent: string | null;
|
|
@@ -397,9 +398,10 @@ export function applyPrReviewActivityIntent(intent: PrReviewActivityIntent, opti
|
|
|
397
398
|
if ('code' in validated) return failed(intent, validated.code, validated.message);
|
|
398
399
|
const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
|
|
399
400
|
if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
|
|
400
|
-
if (resolved.state !== 'active') {
|
|
401
|
+
if (resolved.state !== 'active' && !allowsManualOverride(options.manualOverride, 'activity-intent', 'TASK_STATE_MISMATCH')) {
|
|
401
402
|
return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
402
403
|
}
|
|
404
|
+
if (options.lockAlreadyHeld) return applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot });
|
|
403
405
|
try {
|
|
404
406
|
return withTaskExecutionLock(
|
|
405
407
|
resolved.repoRoot,
|
|
@@ -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'] },
|
|
@@ -218,11 +219,7 @@ function resolveReviewedInput(
|
|
|
218
219
|
diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, String(error)));
|
|
219
220
|
return null;
|
|
220
221
|
}
|
|
221
|
-
const
|
|
222
|
-
const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
|
|
223
|
-
const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
|
|
224
|
-
const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]!);
|
|
225
|
-
const parsed = candidates.map((name) => parseArtifactName(name)).find((item) => item?.family === expectedFamily);
|
|
222
|
+
const parsed = parseReviewedInputReference(content, expectedFamily);
|
|
226
223
|
if (!parsed) {
|
|
227
224
|
diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${review.name} does not reference a canonical ${expectedFamily} artifact`));
|
|
228
225
|
return null;
|
|
@@ -231,9 +228,16 @@ function resolveReviewedInput(
|
|
|
231
228
|
try {
|
|
232
229
|
const stat = fs.lstatSync(abs);
|
|
233
230
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('referenced input is not a regular file');
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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`);
|
|
237
241
|
}
|
|
238
242
|
return { ...parsed, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
|
|
239
243
|
} catch (error) {
|
|
@@ -242,6 +246,54 @@ function resolveReviewedInput(
|
|
|
242
246
|
}
|
|
243
247
|
}
|
|
244
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
|
+
|
|
245
297
|
function assertWritableInventory(inventory: ArtifactInventoryResult): ArtifactError | null {
|
|
246
298
|
if (inventory.status === 'failed') return inventory.error;
|
|
247
299
|
const blockers = inventory.diagnostics.filter((item) => BLOCKING_DIAGNOSTICS.has(item.code));
|
|
@@ -312,11 +364,21 @@ function resolveCodeContext(inventory: ArtifactInventoryResult, options: Inspect
|
|
|
312
364
|
return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null,
|
|
313
365
|
'No prior code artifact. Starting initial implementation (round 1 -> code.md).');
|
|
314
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
|
+
}
|
|
315
374
|
if (reviewPlan.latest && reviewPlan.reviewedInput?.name === plan.latest.name) {
|
|
316
375
|
const verdict = parseVerdict(reviewPlan.latest.path);
|
|
317
|
-
|
|
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
|
+
)) {
|
|
318
380
|
return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, verdict.verdict, reviewPlan.latest.name,
|
|
319
|
-
`Latest ${reviewPlan.latest.name}
|
|
381
|
+
`Latest ${reviewPlan.latest.name} approves plan content not captured by the latest code input receipt. Entering replan-driven init.`);
|
|
320
382
|
}
|
|
321
383
|
}
|
|
322
384
|
if (reviewMax < codeMax) {
|
|
@@ -440,6 +502,7 @@ function buildArtifactLinkSection(content: string, artifact: ArtifactIdentity):
|
|
|
440
502
|
export {
|
|
441
503
|
artifactFamilyCatalog, artifactName, parseArtifactName, familySpec,
|
|
442
504
|
inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext,
|
|
505
|
+
parseReviewedInputReference, parseCodePlanInputReference, resolveCodePlanInput, reviewEventName,
|
|
443
506
|
assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection
|
|
444
507
|
};
|
|
445
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 };
|