@fitlab-ai/agent-infra 0.9.6 → 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 +5 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +45 -4
- 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/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +21 -5
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -3
- package/dist/lib/internal/codex-lifecycle.js +69 -1
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/task-orchestration.js +37 -5
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- 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/sandbox/commands/create.js +103 -51
- 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 +13 -7
- package/dist/lib/sandbox/control/lifecycle.js +238 -29
- 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 +25 -15
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/mounts.js +5 -13
- package/dist/lib/sandbox/recovery.js +59 -42
- package/dist/lib/sandbox/workspace-view.js +57 -40
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +11 -1
- package/dist/lib/server/process-state.js +57 -20
- 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/delegation-receipts.js +84 -2
- package/dist/lib/task/events.js +139 -2
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +235 -14
- 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 +40 -8
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +60 -3
- 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/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 +33 -4
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -2
- package/lib/internal/codex-lifecycle.ts +68 -1
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/task-orchestration.ts +31 -4
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -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/sandbox/commands/create.ts +56 -7
- package/lib/sandbox/commands/rm.ts +64 -14
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +14 -10
- package/lib/sandbox/control/lifecycle.ts +264 -33
- package/lib/sandbox/control/protocol.ts +31 -5
- package/lib/sandbox/control/server.ts +184 -24
- package/lib/sandbox/control/state.ts +34 -17
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/mounts.ts +5 -15
- package/lib/sandbox/recovery.ts +71 -42
- package/lib/sandbox/workspace-view.ts +61 -40
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +13 -3
- package/lib/server/process-state.ts +82 -27
- 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/delegation-receipts.ts +160 -8
- package/lib/task/events.ts +122 -1
- package/lib/task/ledger.ts +16 -1
- package/lib/task/orchestration.ts +356 -17
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +51 -16
- 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 +25 -1
- 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/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 +2 -0
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -0
- 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
|
@@ -11,13 +11,28 @@ import { sandboxCoreBindMounts } from './mounts.js';
|
|
|
11
11
|
import { assertSandboxTaskSource, sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
|
|
12
12
|
import { parseSandboxWorkspaceIdentity, sameSandboxWorkspaceIdentity } from './workspace-identity.js';
|
|
13
13
|
import { inspectWorktree } from './worktree-safety.js';
|
|
14
|
-
import { runEngine, runOkEngine, runVerboseEngine } from './shell.js';
|
|
14
|
+
import { commandForEngine, runEngine, runOkEngine, runProbe, runVerboseEngine } from './shell.js';
|
|
15
15
|
import { declaredTmpfsSeedEntries, resolveTools, toolConfigDirCandidates } from './tools.js';
|
|
16
16
|
import { fetchSandboxRows, selectSandboxContainer, startSandboxContainer } from './commands/list-running.js';
|
|
17
17
|
import { getProcessStartTime, processIdentityMatches, removePidFileIfMatches } from '../server/process-state.js';
|
|
18
|
-
import { acquireSandboxControlBrokerStartup, isSandboxControlRootQuiescing } from './control/lifecycle.js';
|
|
18
|
+
import { acquireSandboxControlBrokerStartup, garbageCollectSandboxControlRoot, isSandboxControlRootQuiescing, readSandboxControlManifest } from './control/lifecycle.js';
|
|
19
19
|
import { appendSandboxControlAudit, readSandboxControlStatus } from './control/state.js';
|
|
20
|
+
import { inspectSandboxControlContainer } from './control/container-identity.js';
|
|
20
21
|
import { SANDBOX_CONTROL_FUTURE_SKEW_MS, SANDBOX_CONTROL_STATUS_STALE_MS } from './control/protocol.js';
|
|
22
|
+
export function worktreeProbeForEngine(engine, probe = runProbe) {
|
|
23
|
+
return (cmd, args, opts = {}) => {
|
|
24
|
+
const engineArgs = [...args];
|
|
25
|
+
if (engine === 'wsl2' && cmd === 'git') {
|
|
26
|
+
for (let index = 0; index < engineArgs.length - 1; index += 1) {
|
|
27
|
+
if (engineArgs[index] === '-C') {
|
|
28
|
+
engineArgs[index + 1] = toEnginePath(engine, engineArgs[index + 1]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const command = commandForEngine(engine, cmd, engineArgs);
|
|
33
|
+
return probe(command.cmd, command.args, opts);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
21
36
|
async function waitForSandboxControlBrokerStatus(params, timeoutMs = 5_000) {
|
|
22
37
|
const deadline = Date.now() + timeoutMs;
|
|
23
38
|
while (Date.now() < deadline) {
|
|
@@ -42,7 +57,7 @@ export async function startSandboxControlBroker(repoRoot, manifestPath) {
|
|
|
42
57
|
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
43
58
|
const extension = path.extname(fileURLToPath(import.meta.url));
|
|
44
59
|
const internalCli = path.resolve(directory, '..', '..', 'bin', `internal-cli${extension}`);
|
|
45
|
-
const manifest =
|
|
60
|
+
const manifest = readSandboxControlManifest(manifestPath);
|
|
46
61
|
const root = path.dirname(manifestPath);
|
|
47
62
|
if (isSandboxControlRootQuiescing(root))
|
|
48
63
|
throw new Error('SANDBOX_CONTROL_QUIESCING');
|
|
@@ -64,14 +79,21 @@ export async function startSandboxControlBroker(repoRoot, manifestPath) {
|
|
|
64
79
|
catch {
|
|
65
80
|
// Malformed owner records are stale and replaced below.
|
|
66
81
|
}
|
|
67
|
-
const owner = broker !== null &&
|
|
68
|
-
|
|
82
|
+
const owner = broker !== null && broker.version === 3 && typeof broker.pid === 'number'
|
|
83
|
+
&& Number.isSafeInteger(broker.pid) && broker.pid > 0 && typeof broker.startTime === 'number'
|
|
84
|
+
&& Number.isSafeInteger(broker.startTime) && typeof broker.brokerId === 'string'
|
|
85
|
+
&& broker.brokerId.length > 0 && typeof broker.token === 'string'
|
|
86
|
+
&& typeof broker.generation === 'string'
|
|
87
|
+
? broker
|
|
69
88
|
: null;
|
|
70
|
-
|
|
89
|
+
if (broker === null || broker.version !== 3 || owner === null) {
|
|
90
|
+
throw new Error('SANDBOX_CONTROL_BROKER_VERSION_INVALID: rebuild the sandbox container before starting a new broker');
|
|
91
|
+
}
|
|
92
|
+
const live = owner !== null && processIdentityMatches({ pid: owner.pid, startTime: owner.startTime });
|
|
71
93
|
if (live
|
|
72
94
|
&& owner !== null
|
|
73
95
|
&& broker !== null
|
|
74
|
-
&& broker.version ===
|
|
96
|
+
&& broker.version === 3
|
|
75
97
|
&& broker.token === manifest.token
|
|
76
98
|
&& broker.generation === manifest.generation) {
|
|
77
99
|
await waitForSandboxControlBrokerStatus({
|
|
@@ -83,28 +105,6 @@ export async function startSandboxControlBroker(repoRoot, manifestPath) {
|
|
|
83
105
|
});
|
|
84
106
|
return;
|
|
85
107
|
}
|
|
86
|
-
if (live && owner) {
|
|
87
|
-
const deadline = Date.now() + 5_000;
|
|
88
|
-
while (Date.now() < deadline) {
|
|
89
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
90
|
-
try {
|
|
91
|
-
if (fs.readFileSync(brokerPath, 'utf8') !== brokerSnapshot) {
|
|
92
|
-
return startSandboxControlBroker(repoRoot, manifestPath);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
if (error.code === 'ENOENT') {
|
|
97
|
-
return startSandboxControlBroker(repoRoot, manifestPath);
|
|
98
|
-
}
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
101
|
-
if (!processIdentityMatches(owner)) {
|
|
102
|
-
removePidFileIfMatches(brokerPath, brokerSnapshot);
|
|
103
|
-
return startSandboxControlBroker(repoRoot, manifestPath);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
throw new Error('SANDBOX_CONTROL_BROKER_OWNER_ACTIVE');
|
|
107
|
-
}
|
|
108
108
|
}
|
|
109
109
|
const startupStartTime = getProcessStartTime(process.pid);
|
|
110
110
|
if (!startupStartTime)
|
|
@@ -162,22 +162,33 @@ async function ensureSandboxControlBroker(params) {
|
|
|
162
162
|
});
|
|
163
163
|
if (!fs.existsSync(control.manifestPath))
|
|
164
164
|
return;
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
const validatedManifest = readSandboxControlManifest(control.manifestPath);
|
|
166
|
+
const containerObservation = await inspectSandboxControlContainer(validatedManifest);
|
|
167
|
+
if (containerObservation.state === 'unknown') {
|
|
168
|
+
throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${containerObservation.reason}`);
|
|
169
|
+
}
|
|
170
|
+
if (containerObservation.state === 'absent') {
|
|
171
|
+
await garbageCollectSandboxControlRoot(control.root, {
|
|
172
|
+
inspectContainer: (timeoutMs) => inspectSandboxControlContainer(validatedManifest, { timeoutMs })
|
|
173
|
+
});
|
|
174
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_ABSENT: recreate the sandbox container before broker recovery');
|
|
168
175
|
}
|
|
169
176
|
const brokerPath = path.join(control.root, 'broker.json');
|
|
170
177
|
try {
|
|
171
178
|
const broker = JSON.parse(fs.readFileSync(brokerPath, 'utf8'));
|
|
172
|
-
if (broker.version ===
|
|
179
|
+
if (broker.version === 3
|
|
173
180
|
&& typeof broker.pid === 'number'
|
|
174
|
-
&&
|
|
175
|
-
&& broker.
|
|
176
|
-
&& broker.
|
|
177
|
-
&&
|
|
181
|
+
&& Number.isSafeInteger(broker.pid)
|
|
182
|
+
&& typeof broker.startTime === 'number'
|
|
183
|
+
&& Number.isSafeInteger(broker.startTime)
|
|
184
|
+
&& typeof broker.brokerId === 'string'
|
|
185
|
+
&& broker.brokerId.length > 0
|
|
186
|
+
&& broker.token === validatedManifest.token
|
|
187
|
+
&& broker.generation === validatedManifest.generation
|
|
188
|
+
&& processIdentityMatches({ pid: broker.pid, startTime: broker.startTime })) {
|
|
178
189
|
try {
|
|
179
190
|
const status = readSandboxControlStatus(control.statusDir);
|
|
180
|
-
if (status.generation ===
|
|
191
|
+
if (status.generation === validatedManifest.generation && status.broker.pid === broker.pid
|
|
181
192
|
&& status.broker.startTime === broker.startTime
|
|
182
193
|
&& Date.now() - status.updatedAt <= SANDBOX_CONTROL_STATUS_STALE_MS
|
|
183
194
|
&& status.updatedAt <= Date.now() + SANDBOX_CONTROL_FUTURE_SKEW_MS)
|
|
@@ -189,7 +200,7 @@ async function ensureSandboxControlBroker(params) {
|
|
|
189
200
|
await waitForSandboxControlBrokerStatus({
|
|
190
201
|
root: control.root,
|
|
191
202
|
statusDir: control.statusDir,
|
|
192
|
-
generation:
|
|
203
|
+
generation: validatedManifest.generation,
|
|
193
204
|
pid: broker.pid,
|
|
194
205
|
startTime: broker.startTime
|
|
195
206
|
});
|
|
@@ -729,6 +740,9 @@ export async function ensureSandboxReady(params) {
|
|
|
729
740
|
workspace: params.workspace ?? { mode: 'branch-only' }
|
|
730
741
|
});
|
|
731
742
|
}
|
|
743
|
+
if (params.forceRecreate) {
|
|
744
|
+
throw new Error('Explicit container recreation requested.');
|
|
745
|
+
}
|
|
732
746
|
if (!params.row.running) {
|
|
733
747
|
startFn(params.engine, params.row.name);
|
|
734
748
|
const initial = assess({
|
|
@@ -862,7 +876,8 @@ export async function ensureSandboxReady(params) {
|
|
|
862
876
|
+ 'keep the worktree in use, remove the stale directory, then retry.';
|
|
863
877
|
throw new Error(`SANDBOX_RECOVERY_WORKTREE_SNAPSHOT_INVALID: expected exactly one existing worktree for '${params.branch}', found ${existingWorktrees.length}. ${remediation}`);
|
|
864
878
|
}
|
|
865
|
-
const
|
|
879
|
+
const worktreeProbe = worktreeProbeForEngine(params.engine, deps?.probe);
|
|
880
|
+
const beforeInspection = inspectWorktree(existingWorktrees[0], { probe: worktreeProbe });
|
|
866
881
|
if (beforeInspection.status === 'failed') {
|
|
867
882
|
throw new Error(`SANDBOX_RECOVERY_WORKTREE_SNAPSHOT_INVALID: ${beforeInspection.message}`);
|
|
868
883
|
}
|
|
@@ -871,7 +886,9 @@ export async function ensureSandboxReady(params) {
|
|
|
871
886
|
throw new Error(`SANDBOX_RECOVERY_WORKTREE_SNAPSHOT_INVALID: expected branch '${params.branch}', found '${before.branch}'. `
|
|
872
887
|
+ `Run 'git -C ${JSON.stringify(before.worktree)} checkout ${params.branch}' to restore the expected branch, then retry.`);
|
|
873
888
|
}
|
|
874
|
-
const warning =
|
|
889
|
+
const warning = params.forceRecreate
|
|
890
|
+
? `Explicit sandbox recreation requested. Replacing only the container. ${dataBoundary}`
|
|
891
|
+
: `Sandbox recovery failed in place. Replacing only the container. ${dataBoundary}`;
|
|
875
892
|
warnings.push(warning);
|
|
876
893
|
(params.writeWarning ?? ((message) => process.stderr.write(`${message}\n`)))(warning);
|
|
877
894
|
const runVerboseFn = deps?.runVerbose ?? runVerboseEngine;
|
|
@@ -904,7 +921,7 @@ export async function ensureSandboxReady(params) {
|
|
|
904
921
|
catch (error) {
|
|
905
922
|
replacementFailure = error;
|
|
906
923
|
}
|
|
907
|
-
const afterInspection = inspectWorktree(before.worktree);
|
|
924
|
+
const afterInspection = inspectWorktree(before.worktree, { probe: worktreeProbe });
|
|
908
925
|
if (afterInspection.status === 'failed' || afterInspection.snapshot.identity !== before.identity) {
|
|
909
926
|
const detail = afterInspection.status === 'failed'
|
|
910
927
|
? afterInspection.message
|
|
@@ -50,44 +50,6 @@ function assertSafeDirectory(directory, expectedBase) {
|
|
|
50
50
|
throw new Error(`Sandbox workspace view must not be a symbolic link: ${directory}`);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
function assertSafeMountTargetPath(target, worktreeRoot, canonicalWorktreeRoot) {
|
|
54
|
-
const relative = path.relative(worktreeRoot, target);
|
|
55
|
-
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
56
|
-
throw new Error(`Sandbox workspace mount target escapes its worktree: ${target}`);
|
|
57
|
-
}
|
|
58
|
-
let current = worktreeRoot;
|
|
59
|
-
for (const segment of ['', ...relative.split(path.sep).filter(Boolean)]) {
|
|
60
|
-
current = segment ? path.join(current, segment) : current;
|
|
61
|
-
try {
|
|
62
|
-
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
63
|
-
throw new Error(`Sandbox workspace mount target must not contain a symbolic link: ${current}`);
|
|
64
|
-
}
|
|
65
|
-
const canonicalRelative = path.relative(canonicalWorktreeRoot, fs.realpathSync.native(current));
|
|
66
|
-
if (canonicalRelative.startsWith('..') || path.isAbsolute(canonicalRelative)) {
|
|
67
|
-
throw new Error(`Sandbox workspace mount target escapes its real worktree: ${current}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
if (error.code !== 'ENOENT')
|
|
72
|
-
throw error;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
export function prepareSandboxWorkspaceMountTargets(worktreeRoot) {
|
|
77
|
-
const resolvedWorktreeRoot = path.resolve(worktreeRoot);
|
|
78
|
-
const canonicalWorktreeRoot = fs.realpathSync.native(resolvedWorktreeRoot);
|
|
79
|
-
const workspaceRoot = path.resolve(resolvedWorktreeRoot, '.agents', 'workspace');
|
|
80
|
-
const statePaths = sandboxWorkspaceViewStatePaths(workspaceRoot);
|
|
81
|
-
const registryTarget = path.join(workspaceRoot, 'active', '.short-ids.json');
|
|
82
|
-
for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget]) {
|
|
83
|
-
assertSafeMountTargetPath(target, resolvedWorktreeRoot, canonicalWorktreeRoot);
|
|
84
|
-
}
|
|
85
|
-
fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
|
|
86
|
-
for (const { hostPath } of statePaths) {
|
|
87
|
-
fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
|
|
88
|
-
}
|
|
89
|
-
fs.closeSync(fs.openSync(registryTarget, 'a', 0o600));
|
|
90
|
-
}
|
|
91
53
|
export function assertSandboxTaskSource(repoRoot, taskId) {
|
|
92
54
|
if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
|
|
93
55
|
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
@@ -103,6 +65,43 @@ export function assertSandboxTaskSource(repoRoot, taskId) {
|
|
|
103
65
|
}
|
|
104
66
|
return canonical;
|
|
105
67
|
}
|
|
68
|
+
export function prepareSandboxWorkspaceMountTarget(worktreeRoot) {
|
|
69
|
+
const worktree = path.resolve(worktreeRoot);
|
|
70
|
+
const canonicalWorktree = fs.realpathSync.native(worktree);
|
|
71
|
+
const agentsRoot = path.join(worktree, '.agents');
|
|
72
|
+
if (fs.existsSync(agentsRoot)) {
|
|
73
|
+
const agentsStat = fs.lstatSync(agentsRoot);
|
|
74
|
+
if (!agentsStat.isDirectory() || agentsStat.isSymbolicLink()) {
|
|
75
|
+
throw new Error(`Sandbox workspace mount ancestor must be a real directory: ${agentsRoot}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
fs.mkdirSync(agentsRoot, { mode: 0o700 });
|
|
80
|
+
}
|
|
81
|
+
const canonicalAgents = fs.realpathSync.native(agentsRoot);
|
|
82
|
+
if (path.dirname(canonicalAgents) !== canonicalWorktree) {
|
|
83
|
+
throw new Error(`Sandbox workspace mount ancestor escapes the worktree: ${agentsRoot}`);
|
|
84
|
+
}
|
|
85
|
+
const target = path.join(agentsRoot, 'workspace');
|
|
86
|
+
if (fs.existsSync(target)) {
|
|
87
|
+
const stat = fs.lstatSync(target);
|
|
88
|
+
if (stat.isSymbolicLink()) {
|
|
89
|
+
throw new Error(`Sandbox workspace mount target must not be a symbolic link: ${target}`);
|
|
90
|
+
}
|
|
91
|
+
if (!stat.isDirectory()) {
|
|
92
|
+
throw new Error(`Sandbox workspace mount target must be a directory: ${target}`);
|
|
93
|
+
}
|
|
94
|
+
if (path.dirname(fs.realpathSync.native(target)) !== canonicalAgents) {
|
|
95
|
+
throw new Error(`Sandbox workspace mount target escapes the worktree: ${target}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
fs.mkdirSync(target, { mode: 0o700 });
|
|
100
|
+
}
|
|
101
|
+
fs.chmodSync(target, 0o700);
|
|
102
|
+
fs.accessSync(target, fs.constants.W_OK);
|
|
103
|
+
return target;
|
|
104
|
+
}
|
|
106
105
|
export function materializeSandboxWorkspaceView(params) {
|
|
107
106
|
const projectRoot = path.resolve(params.base, params.project);
|
|
108
107
|
const { root, taskMountPath } = sandboxWorkspaceViewPaths(params);
|
|
@@ -152,11 +151,13 @@ export function materializeSandboxControl(params) {
|
|
|
152
151
|
const generation = randomBytes(16).toString('hex');
|
|
153
152
|
const repoRoot = fs.realpathSync.native(params.repoRoot);
|
|
154
153
|
const manifest = {
|
|
155
|
-
version:
|
|
154
|
+
version: 4,
|
|
155
|
+
engine: params.engine ?? 'docker',
|
|
156
156
|
repoRoot,
|
|
157
157
|
worktreeRoot: fs.realpathSync.native(params.worktreeRoot),
|
|
158
158
|
project: params.project,
|
|
159
159
|
container: params.container,
|
|
160
|
+
containerIdentity: { id: '', labels: {} },
|
|
160
161
|
branch: params.branch,
|
|
161
162
|
mode: params.identity.mode,
|
|
162
163
|
taskId: params.identity.mode === 'task-bound' ? params.identity.taskId : null,
|
|
@@ -167,6 +168,22 @@ export function materializeSandboxControl(params) {
|
|
|
167
168
|
processingDir
|
|
168
169
|
};
|
|
169
170
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`, { mode: 0o600 });
|
|
170
|
-
return { channelDir, statusDir, manifestPath, token, generation };
|
|
171
|
+
return { root, channelDir, statusDir, manifestPath, token, generation };
|
|
172
|
+
}
|
|
173
|
+
export function finalizeSandboxControlManifest(setup, identity) {
|
|
174
|
+
if (!identity.engine || !identity.id)
|
|
175
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_ID_INVALID');
|
|
176
|
+
const manifest = JSON.parse(fs.readFileSync(setup.manifestPath, 'utf8'));
|
|
177
|
+
manifest.version = 4;
|
|
178
|
+
manifest.engine = identity.engine;
|
|
179
|
+
manifest.containerIdentity = { id: identity.id, labels: { ...identity.labels } };
|
|
180
|
+
const temporary = `${setup.manifestPath}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
181
|
+
try {
|
|
182
|
+
fs.writeFileSync(temporary, `${JSON.stringify(manifest)}\n`, { mode: 0o600, flag: 'wx' });
|
|
183
|
+
fs.renameSync(temporary, setup.manifestPath);
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
fs.rmSync(temporary, { force: true });
|
|
187
|
+
}
|
|
171
188
|
}
|
|
172
189
|
//# sourceMappingURL=workspace-view.js.map
|
|
@@ -26,7 +26,7 @@ async function waitForProcessIdentity(pid, timeoutMs = 2000) {
|
|
|
26
26
|
while (Date.now() < deadline) {
|
|
27
27
|
const startTime = getProcessStartTime(pid);
|
|
28
28
|
if (startTime !== null)
|
|
29
|
-
return { version:
|
|
29
|
+
return { version: 2, pid, startTime };
|
|
30
30
|
await delay(25);
|
|
31
31
|
}
|
|
32
32
|
return null;
|
|
@@ -58,6 +58,9 @@ export async function start({ foreground = false } = {}) {
|
|
|
58
58
|
process.stdout.write(`server already running (pid ${existing.record.pid})\n`);
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
|
+
if (existing.kind === 'legacy-json' || existing.kind === 'legacy-pid-only' || existing.kind === 'legacy-unknown') {
|
|
62
|
+
throw new Error('server: legacy pid record requires the matching previous version to stop it before starting');
|
|
63
|
+
}
|
|
61
64
|
if (existing.kind !== 'missing') {
|
|
62
65
|
removePidFileIfMatches(pidPath, existing.snapshot);
|
|
63
66
|
}
|
|
@@ -104,6 +107,9 @@ export async function stop() {
|
|
|
104
107
|
return;
|
|
105
108
|
}
|
|
106
109
|
if (state.kind !== 'running') {
|
|
110
|
+
if (state.kind === 'legacy-json' || state.kind === 'legacy-pid-only' || state.kind === 'legacy-unknown') {
|
|
111
|
+
throw new Error('server: legacy pid record requires the matching previous version to stop it');
|
|
112
|
+
}
|
|
107
113
|
removePidFileIfMatches(config.pidFile, state.snapshot);
|
|
108
114
|
process.stdout.write('server is not running (removed stale pid file)\n');
|
|
109
115
|
return;
|
|
@@ -135,6 +141,10 @@ export function status() {
|
|
|
135
141
|
const config = loadServerConfig();
|
|
136
142
|
const state = readProcessState(config.pidFile);
|
|
137
143
|
if (state.kind !== 'running') {
|
|
144
|
+
if (state.kind === 'legacy-json' || state.kind === 'legacy-pid-only' || state.kind === 'legacy-unknown') {
|
|
145
|
+
process.stdout.write('server: legacy pid record requires the matching previous version to stop it\n');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
138
148
|
if (state.kind !== 'missing')
|
|
139
149
|
removePidFileIfMatches(config.pidFile, state.snapshot);
|
|
140
150
|
process.stdout.write('server: stopped\n');
|
|
@@ -11,7 +11,10 @@ export function parseLinuxProcessStat(raw) {
|
|
|
11
11
|
if (typeof state !== 'string' || state.length !== 1 || !/^\d+$/.test(startTime ?? '')) {
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
const numericStartTime = Number(startTime);
|
|
15
|
+
if (!Number.isSafeInteger(numericStartTime))
|
|
16
|
+
return null;
|
|
17
|
+
return { state, startTime: numericStartTime };
|
|
15
18
|
}
|
|
16
19
|
export function buildProcessStartTimeQuery(pid, platform) {
|
|
17
20
|
if (platform === 'darwin') {
|
|
@@ -47,6 +50,17 @@ export function isProcessAlive(pid, platform = process.platform) {
|
|
|
47
50
|
return error.code === 'EPERM';
|
|
48
51
|
}
|
|
49
52
|
}
|
|
53
|
+
function parseDarwinStartTime(output) {
|
|
54
|
+
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})$/);
|
|
55
|
+
if (!match)
|
|
56
|
+
return null;
|
|
57
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
58
|
+
const month = months.indexOf(match[1]);
|
|
59
|
+
if (month < 0)
|
|
60
|
+
return null;
|
|
61
|
+
const timestamp = Date.UTC(Number(match[6]), month, Number(match[2]), Number(match[3]), Number(match[4]), Number(match[5]));
|
|
62
|
+
return Number.isNaN(timestamp) ? null : timestamp;
|
|
63
|
+
}
|
|
50
64
|
export function getProcessStartTime(pid, platform = process.platform) {
|
|
51
65
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
52
66
|
return null;
|
|
@@ -65,14 +79,19 @@ export function getProcessStartTime(pid, platform = process.platform) {
|
|
|
65
79
|
if (query === null)
|
|
66
80
|
return null;
|
|
67
81
|
try {
|
|
68
|
-
const output = execFileSync(query.command, query.args, {
|
|
82
|
+
const output = execFileSync(query.command, query.args, {
|
|
83
|
+
encoding: 'utf8',
|
|
84
|
+
env: platform === 'darwin' ? { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } : process.env
|
|
85
|
+
}).trim();
|
|
69
86
|
if (output.length === 0)
|
|
70
87
|
return null;
|
|
71
88
|
if (platform === 'win32') {
|
|
72
89
|
const timestamp = new Date(output);
|
|
73
|
-
return Number.isNaN(timestamp.getTime()) ? null : timestamp.
|
|
90
|
+
return Number.isNaN(timestamp.getTime()) ? null : timestamp.getTime();
|
|
74
91
|
}
|
|
75
|
-
|
|
92
|
+
if (platform === 'darwin')
|
|
93
|
+
return parseDarwinStartTime(output);
|
|
94
|
+
return null;
|
|
76
95
|
}
|
|
77
96
|
catch {
|
|
78
97
|
return null;
|
|
@@ -82,26 +101,42 @@ function parsePidRecord(raw) {
|
|
|
82
101
|
const trimmed = raw.trim();
|
|
83
102
|
if (/^[1-9]\d*$/.test(trimmed)) {
|
|
84
103
|
const pid = Number(trimmed);
|
|
85
|
-
return Number.isSafeInteger(pid)
|
|
104
|
+
return Number.isSafeInteger(pid)
|
|
105
|
+
? { kind: 'legacy-pid-only', legacy: { pid, raw, format: 'pid-only' } }
|
|
106
|
+
: { kind: 'legacy-unknown', legacy: { raw, format: 'malformed' }, reason: 'pid is not a safe integer' };
|
|
86
107
|
}
|
|
87
108
|
try {
|
|
88
109
|
const value = JSON.parse(trimmed);
|
|
110
|
+
const pidValue = value?.pid;
|
|
111
|
+
const startTimeValue = value?.startTime;
|
|
89
112
|
if (value !== null &&
|
|
90
|
-
value.version ===
|
|
91
|
-
|
|
92
|
-
(
|
|
93
|
-
|
|
94
|
-
|
|
113
|
+
value.version === 2 &&
|
|
114
|
+
typeof pidValue === 'number' &&
|
|
115
|
+
Number.isSafeInteger(pidValue) &&
|
|
116
|
+
pidValue > 0 &&
|
|
117
|
+
typeof startTimeValue === 'number' &&
|
|
118
|
+
Number.isSafeInteger(startTimeValue) &&
|
|
119
|
+
startTimeValue >= 0) {
|
|
120
|
+
const startTime = startTimeValue;
|
|
95
121
|
return {
|
|
96
122
|
kind: 'record',
|
|
97
|
-
record: { version:
|
|
123
|
+
record: { version: 2, pid: pidValue, startTime }
|
|
98
124
|
};
|
|
99
125
|
}
|
|
126
|
+
if (value !== null &&
|
|
127
|
+
value.version === 1 &&
|
|
128
|
+
typeof pidValue === 'number' &&
|
|
129
|
+
Number.isSafeInteger(pidValue) &&
|
|
130
|
+
pidValue > 0 &&
|
|
131
|
+
typeof value.startTime === 'string' &&
|
|
132
|
+
value.startTime.length > 0) {
|
|
133
|
+
return { kind: 'legacy-json', legacy: { pid: pidValue, raw, format: 'json-v1' } };
|
|
134
|
+
}
|
|
135
|
+
return { kind: 'legacy-unknown', legacy: { raw, format: 'malformed' }, reason: 'unsupported PID record schema' };
|
|
100
136
|
}
|
|
101
137
|
catch {
|
|
102
|
-
|
|
138
|
+
return { kind: 'legacy-unknown', legacy: { raw, format: 'malformed' }, reason: 'invalid PID record JSON' };
|
|
103
139
|
}
|
|
104
|
-
return { kind: 'invalid' };
|
|
105
140
|
}
|
|
106
141
|
export function readProcessState(pidFile) {
|
|
107
142
|
let snapshot;
|
|
@@ -111,13 +146,15 @@ export function readProcessState(pidFile) {
|
|
|
111
146
|
catch (error) {
|
|
112
147
|
if (error.code === 'ENOENT')
|
|
113
148
|
return { kind: 'missing', snapshot: null };
|
|
114
|
-
return { kind: '
|
|
149
|
+
return { kind: 'legacy-unknown', snapshot: '', legacy: { raw: '', format: 'malformed' }, reason: 'PID record cannot be read' };
|
|
115
150
|
}
|
|
116
151
|
const parsed = parsePidRecord(snapshot);
|
|
117
|
-
if (parsed.kind === '
|
|
118
|
-
return { kind:
|
|
119
|
-
if (parsed.kind === 'legacy')
|
|
120
|
-
return { kind:
|
|
152
|
+
if (parsed.kind === 'legacy-json')
|
|
153
|
+
return { kind: parsed.kind, snapshot, legacy: parsed.legacy };
|
|
154
|
+
if (parsed.kind === 'legacy-pid-only')
|
|
155
|
+
return { kind: parsed.kind, snapshot, legacy: parsed.legacy };
|
|
156
|
+
if (parsed.kind === 'legacy-unknown')
|
|
157
|
+
return { kind: parsed.kind, snapshot, legacy: parsed.legacy, reason: parsed.reason };
|
|
121
158
|
const currentStartTime = getProcessStartTime(parsed.record.pid);
|
|
122
159
|
if (currentStartTime === null || currentStartTime !== parsed.record.startTime) {
|
|
123
160
|
return { kind: 'stale', snapshot, record: parsed.record };
|
|
@@ -162,7 +199,7 @@ export function removePidRecordIfMatches(pidFile, expected) {
|
|
|
162
199
|
return false;
|
|
163
200
|
return removePidFileIfMatches(pidFile, state.snapshot);
|
|
164
201
|
}
|
|
165
|
-
export function processIdentityMatches(
|
|
166
|
-
return getProcessStartTime(
|
|
202
|
+
export function processIdentityMatches(identity) {
|
|
203
|
+
return getProcessStartTime(identity.pid) === identity.startTime;
|
|
167
204
|
}
|
|
168
205
|
//# sourceMappingURL=process-state.js.map
|
|
@@ -5,6 +5,7 @@ import { locateActivityLog } from './activity-log.js';
|
|
|
5
5
|
import { parseImplementationInputs, selectPendingImplementationInput } from './implementation-inputs.js';
|
|
6
6
|
import { parseVerdict } from './review-artifacts.js';
|
|
7
7
|
import { extractSection, findSectionHeading } from './sections.js';
|
|
8
|
+
import { receiptForOutput, sha256File } from './artifact-receipts.js';
|
|
8
9
|
const artifactFamilyCatalog = [
|
|
9
10
|
{ family: 'analysis', sectionAliases: ['分析', 'Analysis'], heading: '分析', labels: ['需求分析报告', 'Requirements Analysis'] },
|
|
10
11
|
{ family: 'review-analysis', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['需求分析审查', 'Analysis Review'] },
|
|
@@ -151,11 +152,7 @@ function resolveReviewedInput(taskDir, review, expectedFamily, diagnostics) {
|
|
|
151
152
|
diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, String(error)));
|
|
152
153
|
return null;
|
|
153
154
|
}
|
|
154
|
-
const
|
|
155
|
-
const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
|
|
156
|
-
const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
|
|
157
|
-
const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]);
|
|
158
|
-
const parsed = candidates.map((name) => parseArtifactName(name)).find((item) => item?.family === expectedFamily);
|
|
155
|
+
const parsed = parseReviewedInputReference(content, expectedFamily);
|
|
159
156
|
if (!parsed) {
|
|
160
157
|
diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${review.name} does not reference a canonical ${expectedFamily} artifact`));
|
|
161
158
|
return null;
|
|
@@ -165,9 +162,17 @@ function resolveReviewedInput(taskDir, review, expectedFamily, diagnostics) {
|
|
|
165
162
|
const stat = fs.lstatSync(abs);
|
|
166
163
|
if (!stat.isFile() || stat.isSymbolicLink())
|
|
167
164
|
throw new Error('referenced input is not a regular file');
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
166
|
+
const receipt = receiptForOutput(taskContent, review.name);
|
|
167
|
+
if (!receipt)
|
|
168
|
+
throw new Error(`receipt for ${review.name} is missing`);
|
|
169
|
+
const expectedEvent = reviewEventName(review.family);
|
|
170
|
+
if (receipt.event !== expectedEvent || receipt.input !== parsed.name) {
|
|
171
|
+
throw new Error(`receipt for ${review.name} does not match ${parsed.name}`);
|
|
172
|
+
}
|
|
173
|
+
const actualSha256 = sha256File(abs);
|
|
174
|
+
if (actualSha256 !== receipt.inputSha256) {
|
|
175
|
+
throw new Error(`${parsed.name} content digest does not match receipt`);
|
|
171
176
|
}
|
|
172
177
|
return { ...parsed, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
|
|
173
178
|
}
|
|
@@ -176,6 +181,64 @@ function resolveReviewedInput(taskDir, review, expectedFamily, diagnostics) {
|
|
|
176
181
|
return null;
|
|
177
182
|
}
|
|
178
183
|
}
|
|
184
|
+
function parseReviewedInputReference(content, expectedFamily) {
|
|
185
|
+
const lines = content.split(/\r?\n/);
|
|
186
|
+
const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
|
|
187
|
+
const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
|
|
188
|
+
const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]);
|
|
189
|
+
return candidates
|
|
190
|
+
.map((name) => parseArtifactName(name))
|
|
191
|
+
.find((item) => item?.family === expectedFamily) ?? null;
|
|
192
|
+
}
|
|
193
|
+
function parseCodePlanInputReference(content) {
|
|
194
|
+
const lines = content.split(/\r?\n/);
|
|
195
|
+
const sectionStart = lines.findIndex((line) => /^##\s+(?:实现输入|Implementation Input)\s*$/.test(line.trim()));
|
|
196
|
+
if (sectionStart < 0)
|
|
197
|
+
return null;
|
|
198
|
+
const bodyLines = lines.slice(sectionStart + 1);
|
|
199
|
+
const nextSection = bodyLines.findIndex((line) => /^##\s+/.test(line.trim()));
|
|
200
|
+
const body = (nextSection < 0 ? bodyLines : bodyLines.slice(0, nextSection)).join('\n');
|
|
201
|
+
const match = body.match(/\*\*(?:方案输入|Plan Input)\*\*[::]\s*`([^`]+\.md)`/);
|
|
202
|
+
if (!match)
|
|
203
|
+
return null;
|
|
204
|
+
const parsed = parseArtifactName(match[1]);
|
|
205
|
+
return parsed?.family === 'plan' ? { ...parsed, family: 'plan' } : null;
|
|
206
|
+
}
|
|
207
|
+
function reviewEventName(family) {
|
|
208
|
+
if (family === 'review-analysis')
|
|
209
|
+
return 'review-analysis.completed';
|
|
210
|
+
if (family === 'review-plan')
|
|
211
|
+
return 'review-plan.completed';
|
|
212
|
+
if (family === 'review-code')
|
|
213
|
+
return 'review-code.completed';
|
|
214
|
+
throw new Error(`unsupported review family '${family}'`);
|
|
215
|
+
}
|
|
216
|
+
function resolveCodeInputReceipt(taskDir, code) {
|
|
217
|
+
let taskContent;
|
|
218
|
+
try {
|
|
219
|
+
taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
let receipt;
|
|
225
|
+
try {
|
|
226
|
+
receipt = receiptForOutput(taskContent, code.name);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
if (!receipt || receipt.event !== 'code.completed')
|
|
232
|
+
return null;
|
|
233
|
+
const plan = inspectArtifactDirectory(taskDir, 'plan');
|
|
234
|
+
if (plan.status !== 'ready')
|
|
235
|
+
return null;
|
|
236
|
+
const input = plan.artifacts.find((artifact) => artifact.name === receipt.input);
|
|
237
|
+
return input ? { input, inputSha256: receipt.inputSha256 } : null;
|
|
238
|
+
}
|
|
239
|
+
function resolveCodePlanInput(taskDir, code) {
|
|
240
|
+
return resolveCodeInputReceipt(taskDir, code);
|
|
241
|
+
}
|
|
179
242
|
function assertWritableInventory(inventory) {
|
|
180
243
|
if (inventory.status === 'failed')
|
|
181
244
|
return inventory.error;
|
|
@@ -248,10 +311,18 @@ function resolveCodeContext(inventory, options) {
|
|
|
248
311
|
}
|
|
249
312
|
return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null, 'No prior code artifact. Starting initial implementation (round 1 -> code.md).');
|
|
250
313
|
}
|
|
314
|
+
const codePlanInput = resolveCodePlanInput(inventory.taskDir, latestCode);
|
|
315
|
+
if (!codePlanInput) {
|
|
316
|
+
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `code completion receipt for ${latestCode.name} is missing or does not match the current plan`, latestCode.name);
|
|
317
|
+
}
|
|
318
|
+
if (reviewPlan.latest && !reviewPlan.reviewedInput) {
|
|
319
|
+
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `${reviewPlan.latest.name} has no valid reviewed input`, reviewPlan.latest.name);
|
|
320
|
+
}
|
|
251
321
|
if (reviewPlan.latest && reviewPlan.reviewedInput?.name === plan.latest.name) {
|
|
252
322
|
const verdict = parseVerdict(reviewPlan.latest.path);
|
|
253
|
-
|
|
254
|
-
|
|
323
|
+
const reviewedPlanSha256 = sha256File(reviewPlan.reviewedInput.path);
|
|
324
|
+
if (verdict.ok && verdict.verdict === 'Approved' && (codePlanInput.input.name !== plan.latest.name || codePlanInput.inputSha256 !== reviewedPlanSha256)) {
|
|
325
|
+
return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, verdict.verdict, reviewPlan.latest.name, `Latest ${reviewPlan.latest.name} approves plan content not captured by the latest code input receipt. Entering replan-driven init.`);
|
|
255
326
|
}
|
|
256
327
|
}
|
|
257
328
|
if (reviewMax < codeMax) {
|
|
@@ -377,5 +448,5 @@ function buildArtifactLinkSection(content, artifact) {
|
|
|
377
448
|
body = lines.join('\n').replace(/^\n+|\n+$/g, '');
|
|
378
449
|
return { aliases, heading, body };
|
|
379
450
|
}
|
|
380
|
-
export { artifactFamilyCatalog, artifactName, parseArtifactName, familySpec, inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext, assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection };
|
|
451
|
+
export { artifactFamilyCatalog, artifactName, parseArtifactName, familySpec, inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext, parseReviewedInputReference, parseCodePlanInputReference, resolveCodePlanInput, reviewEventName, assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection };
|
|
381
452
|
//# sourceMappingURL=artifact-lifecycle.js.map
|