@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.
Files changed (127) hide show
  1. package/dist/bin/internal-cli.js +5 -0
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +45 -4
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +21 -5
  7. package/dist/lib/defaults.json +10 -2
  8. package/dist/lib/git/workflow.js +30 -3
  9. package/dist/lib/internal/codex-lifecycle.js +69 -1
  10. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  11. package/dist/lib/internal/task-orchestration.js +37 -5
  12. package/dist/lib/platform/context.js +6 -4
  13. package/dist/lib/platform/github-client.js +55 -2
  14. package/dist/lib/process-data/checkpoint.js +319 -0
  15. package/dist/lib/process-data/index.js +128 -60
  16. package/dist/lib/process-data/materialize.js +84 -0
  17. package/dist/lib/process-data/normalize.js +34 -1
  18. package/dist/lib/process-data/reconcile.js +55 -1
  19. package/dist/lib/process-data/sources.js +422 -1
  20. package/dist/lib/process-data/store.js +341 -11
  21. package/dist/lib/sandbox/commands/create.js +103 -51
  22. package/dist/lib/sandbox/commands/rm.js +64 -15
  23. package/dist/lib/sandbox/commands/start.js +4 -3
  24. package/dist/lib/sandbox/control/container-identity.js +64 -0
  25. package/dist/lib/sandbox/control/executor.js +13 -7
  26. package/dist/lib/sandbox/control/lifecycle.js +238 -29
  27. package/dist/lib/sandbox/control/protocol.js +7 -0
  28. package/dist/lib/sandbox/control/server.js +202 -26
  29. package/dist/lib/sandbox/control/state.js +25 -15
  30. package/dist/lib/sandbox/control/timing.js +8 -0
  31. package/dist/lib/sandbox/mounts.js +5 -13
  32. package/dist/lib/sandbox/recovery.js +59 -42
  33. package/dist/lib/sandbox/workspace-view.js +57 -40
  34. package/dist/lib/server/daemon.js +1 -1
  35. package/dist/lib/server/process-control.js +11 -1
  36. package/dist/lib/server/process-state.js +57 -20
  37. package/dist/lib/task/artifact-lifecycle.js +82 -11
  38. package/dist/lib/task/artifact-receipts.js +128 -0
  39. package/dist/lib/task/codex-orchestration.js +81 -6
  40. package/dist/lib/task/commands/validate.js +1 -1
  41. package/dist/lib/task/create.js +10 -2
  42. package/dist/lib/task/delegation-receipts.js +84 -2
  43. package/dist/lib/task/events.js +139 -2
  44. package/dist/lib/task/ledger.js +13 -1
  45. package/dist/lib/task/orchestration.js +235 -14
  46. package/dist/lib/task/sections.js +5 -1
  47. package/dist/lib/task/task-execution-lock.js +5 -4
  48. package/dist/lib/task/verification-engine.js +40 -8
  49. package/dist/lifecycle-build-manifest.json +6 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +60 -3
  51. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  52. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  53. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  54. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  55. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +33 -4
  56. package/lib/defaults.json +10 -2
  57. package/lib/git/workflow.ts +25 -2
  58. package/lib/internal/codex-lifecycle.ts +68 -1
  59. package/lib/internal/codex-sandbox-controller.ts +109 -0
  60. package/lib/internal/task-orchestration.ts +31 -4
  61. package/lib/platform/context.ts +8 -5
  62. package/lib/platform/github-client.ts +61 -3
  63. package/lib/process-data/checkpoint.ts +335 -0
  64. package/lib/process-data/index.ts +112 -60
  65. package/lib/process-data/materialize.ts +83 -0
  66. package/lib/process-data/normalize.ts +29 -1
  67. package/lib/process-data/reconcile.ts +64 -2
  68. package/lib/process-data/sources.ts +449 -2
  69. package/lib/process-data/store.ts +304 -11
  70. package/lib/process-data/types.ts +124 -2
  71. package/lib/sandbox/commands/create.ts +56 -7
  72. package/lib/sandbox/commands/rm.ts +64 -14
  73. package/lib/sandbox/commands/start.ts +4 -3
  74. package/lib/sandbox/control/container-identity.ts +93 -0
  75. package/lib/sandbox/control/executor.ts +14 -10
  76. package/lib/sandbox/control/lifecycle.ts +264 -33
  77. package/lib/sandbox/control/protocol.ts +31 -5
  78. package/lib/sandbox/control/server.ts +184 -24
  79. package/lib/sandbox/control/state.ts +34 -17
  80. package/lib/sandbox/control/timing.ts +12 -0
  81. package/lib/sandbox/mounts.ts +5 -15
  82. package/lib/sandbox/recovery.ts +71 -42
  83. package/lib/sandbox/workspace-view.ts +61 -40
  84. package/lib/server/daemon.ts +1 -1
  85. package/lib/server/process-control.ts +13 -3
  86. package/lib/server/process-state.ts +82 -27
  87. package/lib/task/artifact-lifecycle.ts +73 -10
  88. package/lib/task/artifact-receipts.ts +161 -0
  89. package/lib/task/codex-orchestration.ts +91 -5
  90. package/lib/task/commands/validate.ts +1 -1
  91. package/lib/task/create.ts +11 -2
  92. package/lib/task/delegation-receipts.ts +160 -8
  93. package/lib/task/events.ts +122 -1
  94. package/lib/task/ledger.ts +16 -1
  95. package/lib/task/orchestration.ts +356 -17
  96. package/lib/task/sections.ts +6 -2
  97. package/lib/task/task-execution-lock.ts +11 -9
  98. package/lib/task/verification-engine.ts +51 -16
  99. package/package.json +1 -1
  100. package/templates/.agents/README.en.md +7 -0
  101. package/templates/.agents/README.zh-CN.md +5 -0
  102. package/templates/.agents/hooks/lifecycle-delegation.js +25 -1
  103. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  104. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  105. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  106. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  107. package/templates/.agents/rules/review-handshake.en.md +3 -1
  108. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  109. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  110. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  111. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  112. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  113. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  114. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  115. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  116. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  117. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  118. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  119. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  120. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  121. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  122. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  123. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -0
  124. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -0
  125. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  126. package/templates/.agents/workspace/README.en.md +0 -26
  127. 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 = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
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 && typeof broker.pid === 'number' && typeof broker.startTime === 'string'
68
- ? { version: 1, pid: broker.pid, startTime: broker.startTime }
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
- const live = owner !== null && processIdentityMatches(owner);
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 === 2
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 manifest = JSON.parse(fs.readFileSync(control.manifestPath, 'utf8'));
166
- if (manifest.version !== 3) {
167
- throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 3; container-only recreation is required');
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 === 2
179
+ if (broker.version === 3
173
180
  && typeof broker.pid === 'number'
174
- && typeof broker.startTime === 'string'
175
- && broker.token === manifest.token
176
- && broker.generation === manifest.generation
177
- && processIdentityMatches({ version: 1, pid: broker.pid, startTime: broker.startTime })) {
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 === manifest.generation && status.broker.pid === broker.pid
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: manifest.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 beforeInspection = inspectWorktree(existingWorktrees[0]);
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 = `Sandbox recovery failed in place. Replacing only the container. ${dataBoundary}`;
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: 3,
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
@@ -110,7 +110,7 @@ export async function runDaemon() {
110
110
  logger.close();
111
111
  if (ownStartTime !== null) {
112
112
  removePidRecordIfMatches(config.pidFile, {
113
- version: 1,
113
+ version: 2,
114
114
  pid: process.pid,
115
115
  startTime: ownStartTime
116
116
  });
@@ -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: 1, pid, startTime };
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
- return { state, startTime: startTime };
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, { encoding: 'utf8' }).trim();
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.toISOString();
90
+ return Number.isNaN(timestamp.getTime()) ? null : timestamp.getTime();
74
91
  }
75
- return output;
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) ? { kind: 'legacy', pid } : { kind: 'invalid' };
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 === 1 &&
91
- Number.isSafeInteger(value.pid) &&
92
- (value.pid ?? 0) > 0 &&
93
- typeof value.startTime === 'string' &&
94
- value.startTime.length > 0) {
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: 1, pid: value.pid, startTime: value.startTime }
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
- // Invalid JSON is an invalid PID record.
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: 'invalid', snapshot: '' };
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 === 'invalid')
118
- return { kind: 'invalid', snapshot };
119
- if (parsed.kind === 'legacy')
120
- return { kind: 'legacy', snapshot, pid: parsed.pid };
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(record) {
166
- return getProcessStartTime(record.pid) === record.startTime;
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 lines = content.split(/\r?\n/);
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
- if (stat.mtimeMs > review.mtimeMs) {
169
- diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${parsed.name} is newer than ${review.name}`));
170
- return null;
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
- if (verdict.ok && verdict.verdict === 'Approved' && reviewPlan.latest.mtimeMs > latestCode.mtimeMs) {
254
- return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, verdict.verdict, reviewPlan.latest.name, `Latest ${reviewPlan.latest.name} is approved and newer than the latest code artifact. Entering replan-driven init.`);
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