@fitlab-ai/agent-infra 0.9.7 → 0.9.9

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 (195) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +11 -1
  4. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
  12. package/dist/lib/decide.js +68 -14
  13. package/dist/lib/git/workflow.js +10 -1
  14. package/dist/lib/init.js +1 -1
  15. package/dist/lib/internal/codex-lifecycle.js +19 -5
  16. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  17. package/dist/lib/internal/git-workflow.js +5 -7
  18. package/dist/lib/internal/platform-pr.js +20 -7
  19. package/dist/lib/internal/release-workflow.js +3 -4
  20. package/dist/lib/internal/sandbox-control.js +79 -2
  21. package/dist/lib/internal/task-activity.js +57 -7
  22. package/dist/lib/internal/task-event.js +57 -4
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-ledger.js +59 -5
  25. package/dist/lib/internal/task-lifecycle.js +59 -8
  26. package/dist/lib/internal/task-orchestration.js +161 -288
  27. package/dist/lib/internal/task-override.js +131 -0
  28. package/dist/lib/internal/task-review.js +64 -6
  29. package/dist/lib/internal/task-verify.js +65 -3
  30. package/dist/lib/internal/task-warning.js +57 -4
  31. package/dist/lib/platform/pr-summary.js +53 -13
  32. package/dist/lib/platform/pull-requests.js +205 -41
  33. package/dist/lib/runtime/agent-runtime.js +56 -0
  34. package/dist/lib/sandbox/commands/create.js +389 -311
  35. package/dist/lib/sandbox/commands/rm.js +316 -55
  36. package/dist/lib/sandbox/control/client.js +190 -6
  37. package/dist/lib/sandbox/control/container-identity.js +1 -1
  38. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  39. package/dist/lib/sandbox/control/executor.js +145 -5
  40. package/dist/lib/sandbox/control/lifecycle.js +395 -73
  41. package/dist/lib/sandbox/control/protocol.js +67 -6
  42. package/dist/lib/sandbox/control/state.js +27 -11
  43. package/dist/lib/sandbox/engine.js +14 -7
  44. package/dist/lib/sandbox/index.js +5 -3
  45. package/dist/lib/sandbox/managed-fs.js +5 -0
  46. package/dist/lib/sandbox/mounts.js +20 -5
  47. package/dist/lib/sandbox/recovery.js +78 -10
  48. package/dist/lib/sandbox/task-resolver.js +50 -6
  49. package/dist/lib/sandbox/workspace-identity.js +27 -0
  50. package/dist/lib/sandbox/workspace-view.js +62 -40
  51. package/dist/lib/sandbox/worktree-safety.js +215 -2
  52. package/dist/lib/server/process-state.js +51 -1
  53. package/dist/lib/task/activity-intent.js +4 -1
  54. package/dist/lib/task/codex-orchestration.js +158 -33
  55. package/dist/lib/task/commands/status.js +59 -1
  56. package/dist/lib/task/commit-operation.js +335 -0
  57. package/dist/lib/task/commit-policy.js +18 -0
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +279 -17
  60. package/dist/lib/task/events.js +9 -5
  61. package/dist/lib/task/finalization.js +567 -0
  62. package/dist/lib/task/guard-override.js +31 -0
  63. package/dist/lib/task/human-override.js +1240 -0
  64. package/dist/lib/task/ledger-intents.js +6 -3
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/operation-outcome.js +33 -0
  67. package/dist/lib/task/orchestration.js +199 -719
  68. package/dist/lib/task/review-finalization.js +3 -2
  69. package/dist/lib/task/short-id.js +59 -1
  70. package/dist/lib/task/task-execution-lock.js +6 -1
  71. package/dist/lib/task/verification-engine.js +138 -59
  72. package/dist/lib/task/verification.js +15 -2
  73. package/dist/lib/task/workflow-warning-intents.js +54 -3
  74. package/dist/lib/task/write.js +2 -1
  75. package/dist/lifecycle-build-manifest.json +3 -3
  76. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  77. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
  78. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
  80. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  82. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  83. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
  84. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
  85. package/lib/decide.ts +59 -14
  86. package/lib/git/workflow.ts +11 -2
  87. package/lib/init.ts +1 -1
  88. package/lib/internal/codex-lifecycle.ts +18 -5
  89. package/lib/internal/codex-sandbox-controller.ts +6 -12
  90. package/lib/internal/git-workflow.ts +5 -6
  91. package/lib/internal/platform-pr.ts +14 -7
  92. package/lib/internal/release-workflow.ts +3 -4
  93. package/lib/internal/sandbox-control.ts +92 -2
  94. package/lib/internal/task-activity.ts +46 -6
  95. package/lib/internal/task-event.ts +53 -4
  96. package/lib/internal/task-finalization.ts +68 -0
  97. package/lib/internal/task-ledger.ts +48 -4
  98. package/lib/internal/task-lifecycle.ts +56 -12
  99. package/lib/internal/task-orchestration.ts +32 -107
  100. package/lib/internal/task-override.ts +138 -0
  101. package/lib/internal/task-review.ts +51 -6
  102. package/lib/internal/task-verify.ts +52 -3
  103. package/lib/internal/task-warning.ts +44 -3
  104. package/lib/platform/pr-summary.ts +65 -14
  105. package/lib/platform/pull-requests.ts +211 -41
  106. package/lib/runtime/agent-runtime.ts +76 -0
  107. package/lib/sandbox/commands/create.ts +88 -4
  108. package/lib/sandbox/commands/rm.ts +413 -61
  109. package/lib/sandbox/config.ts +2 -1
  110. package/lib/sandbox/control/client.ts +255 -6
  111. package/lib/sandbox/control/container-identity.ts +3 -3
  112. package/lib/sandbox/control/controller-registration.ts +347 -0
  113. package/lib/sandbox/control/executor.ts +172 -6
  114. package/lib/sandbox/control/lifecycle.ts +465 -77
  115. package/lib/sandbox/control/protocol.ts +85 -9
  116. package/lib/sandbox/control/state.ts +35 -18
  117. package/lib/sandbox/engine.ts +20 -9
  118. package/lib/sandbox/index.ts +5 -3
  119. package/lib/sandbox/managed-fs.ts +5 -0
  120. package/lib/sandbox/mounts.ts +23 -5
  121. package/lib/sandbox/recovery.ts +90 -9
  122. package/lib/sandbox/task-resolver.ts +58 -7
  123. package/lib/sandbox/workspace-identity.ts +50 -2
  124. package/lib/sandbox/workspace-view.ts +73 -43
  125. package/lib/sandbox/worktree-safety.ts +236 -2
  126. package/lib/server/process-state.ts +48 -1
  127. package/lib/task/activity-intent.ts +4 -2
  128. package/lib/task/codex-orchestration.ts +167 -33
  129. package/lib/task/commands/status.ts +78 -1
  130. package/lib/task/commit-operation.ts +430 -0
  131. package/lib/task/commit-policy.ts +30 -0
  132. package/lib/task/decision-intents.ts +4 -2
  133. package/lib/task/delegation-receipts.ts +304 -19
  134. package/lib/task/events.ts +10 -5
  135. package/lib/task/finalization.ts +719 -0
  136. package/lib/task/guard-override.ts +50 -0
  137. package/lib/task/human-override.ts +1503 -0
  138. package/lib/task/ledger-intents.ts +6 -3
  139. package/lib/task/lifecycle.ts +71 -14
  140. package/lib/task/operation-outcome.ts +48 -0
  141. package/lib/task/orchestration.ts +239 -922
  142. package/lib/task/review-finalization.ts +6 -2
  143. package/lib/task/short-id.ts +65 -1
  144. package/lib/task/task-execution-lock.ts +10 -1
  145. package/lib/task/verification-engine.ts +145 -64
  146. package/lib/task/verification.ts +18 -3
  147. package/lib/task/workflow-warning-intents.ts +61 -4
  148. package/lib/task/write.ts +4 -1
  149. package/package.json +2 -2
  150. package/templates/.agents/QUICKSTART.en.md +2 -2
  151. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  152. package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
  153. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
  154. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
  155. package/templates/.agents/rules/task-management.en.md +2 -2
  156. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  157. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  158. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  159. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  160. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  161. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  162. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  163. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  164. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  165. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  166. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  167. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  168. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  170. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  171. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  172. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  173. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  174. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  175. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  176. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  177. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  178. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  179. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  180. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  181. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  182. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  183. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  184. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  185. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  186. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  187. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
  188. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
  189. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  190. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  191. package/templates/.codex/hooks.json +4 -4
  192. package/dist/lib/task/commit-finalization.js +0 -305
  193. package/dist/lib/task/commit-intent.js +0 -165
  194. package/lib/task/commit-finalization.ts +0 -419
  195. package/lib/task/commit-intent.ts +0 -230
@@ -38,6 +38,7 @@ export function sandboxControlPaths(params) {
38
38
  channelDir: path.join(root, 'channel'),
39
39
  statusDir: path.join(root, 'public'),
40
40
  processingDir: path.join(root, 'processing'),
41
+ runtimeDir: path.join(root, 'runtime'),
41
42
  manifestPath: path.join(root, 'manifest.json')
42
43
  };
43
44
  }
@@ -50,6 +51,29 @@ function assertSafeDirectory(directory, expectedBase) {
50
51
  throw new Error(`Sandbox workspace view must not be a symbolic link: ${directory}`);
51
52
  }
52
53
  }
54
+ function assertSafeMountTargetPath(target, worktreeRoot, canonicalWorktreeRoot) {
55
+ const relative = path.relative(worktreeRoot, target);
56
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
57
+ throw new Error(`Sandbox workspace mount target escapes its worktree: ${target}`);
58
+ }
59
+ let current = worktreeRoot;
60
+ for (const segment of ['', ...relative.split(path.sep).filter(Boolean)]) {
61
+ current = segment ? path.join(current, segment) : current;
62
+ try {
63
+ if (fs.lstatSync(current).isSymbolicLink()) {
64
+ throw new Error(`Sandbox workspace mount target must not contain a symbolic link: ${current}`);
65
+ }
66
+ const canonicalRelative = path.relative(canonicalWorktreeRoot, fs.realpathSync.native(current));
67
+ if (canonicalRelative.startsWith('..') || path.isAbsolute(canonicalRelative)) {
68
+ throw new Error(`Sandbox workspace mount target escapes its real worktree: ${current}`);
69
+ }
70
+ }
71
+ catch (error) {
72
+ if (error.code !== 'ENOENT')
73
+ throw error;
74
+ }
75
+ }
76
+ }
53
77
  export function assertSandboxTaskSource(repoRoot, taskId) {
54
78
  if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
55
79
  throw new Error('SANDBOX_TASK_SOURCE_INVALID');
@@ -65,42 +89,22 @@ export function assertSandboxTaskSource(repoRoot, taskId) {
65
89
  }
66
90
  return canonical;
67
91
  }
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}`);
92
+ export function prepareSandboxWorkspaceMountTargets(worktreeRoot) {
93
+ const resolvedWorktreeRoot = path.resolve(worktreeRoot);
94
+ const canonicalWorktreeRoot = fs.realpathSync.native(resolvedWorktreeRoot);
95
+ const workspaceRoot = path.resolve(resolvedWorktreeRoot, '.agents', 'workspace');
96
+ const statePaths = sandboxWorkspaceViewStatePaths(workspaceRoot);
97
+ const registryTarget = path.join(workspaceRoot, 'active', '.short-ids.json');
98
+ for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget]) {
99
+ assertSafeMountTargetPath(target, resolvedWorktreeRoot, canonicalWorktreeRoot);
84
100
  }
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 });
101
+ fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
102
+ for (const { hostPath } of statePaths) {
103
+ fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
104
+ fs.chmodSync(hostPath, 0o700);
100
105
  }
101
- fs.chmodSync(target, 0o700);
102
- fs.accessSync(target, fs.constants.W_OK);
103
- return target;
106
+ fs.chmodSync(workspaceRoot, 0o700);
107
+ fs.closeSync(fs.openSync(registryTarget, 'a', 0o600));
104
108
  }
105
109
  export function materializeSandboxWorkspaceView(params) {
106
110
  const projectRoot = path.resolve(params.base, params.project);
@@ -131,15 +135,32 @@ export function materializeSandboxWorkspaceView(params) {
131
135
  return { root, taskMountPath };
132
136
  }
133
137
  export function materializeSandboxControl(params) {
134
- const { root, channelDir, statusDir, processingDir, manifestPath } = sandboxControlPaths(params);
138
+ const { root, channelDir, statusDir, processingDir, runtimeDir, manifestPath } = sandboxControlPaths(params);
135
139
  assertSafeDirectory(root, path.resolve(params.base));
140
+ if (fs.existsSync(root)) {
141
+ if (!params.replacementLease || path.resolve(params.replacementLease.root) !== root) {
142
+ throw new Error('SANDBOX_CONTROL_REPLACEMENT_REQUIRED');
143
+ }
144
+ params.replacementLease.assertOwned();
145
+ }
136
146
  const consumedDir = path.join(root, 'consumed');
137
147
  assertSafeDirectory(consumedDir, root);
138
148
  fs.rmSync(consumedDir, { recursive: true, force: true });
139
149
  fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
140
- for (const directory of [statusDir, processingDir]) {
150
+ for (const directory of [statusDir, processingDir, runtimeDir]) {
141
151
  assertSafeDirectory(directory, root);
152
+ if (directory === runtimeDir)
153
+ fs.rmSync(directory, { recursive: true, force: true });
154
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
155
+ }
156
+ for (const directory of [
157
+ path.join(runtimeDir, 'clients'),
158
+ path.join(runtimeDir, 'clients', 'codex', 'capabilities'),
159
+ path.join(runtimeDir, 'clients', 'codex', 'lifecycle')
160
+ ]) {
161
+ assertSafeDirectory(directory, runtimeDir);
142
162
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
163
+ fs.chmodSync(directory, 0o700);
143
164
  }
144
165
  for (const queue of ['requests', 'responses']) {
145
166
  const directory = path.join(channelDir, queue);
@@ -151,7 +172,7 @@ export function materializeSandboxControl(params) {
151
172
  const generation = randomBytes(16).toString('hex');
152
173
  const repoRoot = fs.realpathSync.native(params.repoRoot);
153
174
  const manifest = {
154
- version: 4,
175
+ version: 5,
155
176
  engine: params.engine ?? 'docker',
156
177
  repoRoot,
157
178
  worktreeRoot: fs.realpathSync.native(params.worktreeRoot),
@@ -165,16 +186,17 @@ export function materializeSandboxControl(params) {
165
186
  generation,
166
187
  channelDir,
167
188
  publicStatusDir: statusDir,
168
- processingDir
189
+ processingDir,
190
+ runtimeDir
169
191
  };
170
192
  fs.writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`, { mode: 0o600 });
171
- return { root, channelDir, statusDir, manifestPath, token, generation };
193
+ return { root, channelDir, statusDir, runtimeDir, manifestPath, token, generation };
172
194
  }
173
195
  export function finalizeSandboxControlManifest(setup, identity) {
174
196
  if (!identity.engine || !identity.id)
175
197
  throw new Error('SANDBOX_CONTROL_CONTAINER_ID_INVALID');
176
198
  const manifest = JSON.parse(fs.readFileSync(setup.manifestPath, 'utf8'));
177
- manifest.version = 4;
199
+ manifest.version = 5;
178
200
  manifest.engine = identity.engine;
179
201
  manifest.containerIdentity = { id: identity.id, labels: { ...identity.labels } };
180
202
  const temporary = `${setup.manifestPath}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
@@ -1,7 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import fs from 'node:fs';
3
+ import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import { runProbe } from './shell.js';
6
+ import { resolveSandboxTarget } from './workspace-identity.js';
5
7
  function probeText(probe, cwd, args) {
6
8
  const result = probe('git', ['-C', cwd, ...args], { encoding: 'utf8' });
7
9
  if (result.error)
@@ -138,6 +140,197 @@ function captureOnce(worktree, probe) {
138
140
  }
139
141
  return { worktree: resolvedWorktree, branch, head, changes, identity: hash.digest('hex') };
140
142
  }
143
+ function probeTextWithEnv(probe, repoRoot, args, env) {
144
+ const result = probe('git', args, { cwd: repoRoot, encoding: 'utf8', env });
145
+ if (result.error)
146
+ throw result.error;
147
+ if (result.status !== 0) {
148
+ const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : result.stderr.toString('utf8').trim();
149
+ throw new Error(stderr || `git ${args[0] ?? 'command'} failed with exit code ${result.status ?? 'unknown'}`);
150
+ }
151
+ return typeof result.stdout === 'string' ? result.stdout : result.stdout.toString('utf8');
152
+ }
153
+ function probeMetadataCommand(probe, repoRoot, args, env) {
154
+ const result = probe('git', args, { cwd: repoRoot, encoding: 'utf8', env });
155
+ if (result.error)
156
+ throw result.error;
157
+ return result.status === 0;
158
+ }
159
+ function hasValidHeadContent(content) {
160
+ return /^(?:ref:\s+refs\/\S+|[0-9a-f]{40}|[0-9a-f]{64})$/i.test(content.trim());
161
+ }
162
+ function assertRecoverableMetadata(worktree, repoRoot, probe) {
163
+ const dotGit = path.join(worktree, '.git');
164
+ let dotGitStat;
165
+ try {
166
+ dotGitStat = fs.lstatSync(dotGit);
167
+ }
168
+ catch (error) {
169
+ if (error.code === 'ENOENT')
170
+ return;
171
+ throw error;
172
+ }
173
+ if (dotGitStat.isSymbolicLink() || dotGitStat.isDirectory()) {
174
+ throw new Error('WORKTREE_RECOVERY_METADATA_NOT_ELIGIBLE');
175
+ }
176
+ if (!dotGitStat.isFile())
177
+ throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
178
+ const content = fs.readFileSync(dotGit, 'utf8').trim();
179
+ const match = /^gitdir:\s*(.+)$/i.exec(content);
180
+ if (!match)
181
+ throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
182
+ const commonDir = fs.realpathSync.native(path.resolve(repoRoot, probeText(probe, repoRoot, [
183
+ 'rev-parse', '--git-common-dir'
184
+ ]).trim()));
185
+ const adminRoot = path.join(commonDir, 'worktrees');
186
+ const adminPath = path.resolve(path.dirname(dotGit), match[1]);
187
+ const relative = path.relative(adminRoot, adminPath);
188
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative) || relative.includes(path.sep)) {
189
+ throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
190
+ }
191
+ if (!fs.existsSync(adminPath))
192
+ return;
193
+ const adminStat = fs.lstatSync(adminPath);
194
+ if (adminStat.isSymbolicLink())
195
+ throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
196
+ if (!adminStat.isDirectory())
197
+ return;
198
+ const requiredFiles = ['HEAD', 'gitdir', 'commondir', 'index'];
199
+ if (requiredFiles.some((name) => !fs.existsSync(path.join(adminPath, name))))
200
+ return;
201
+ for (const name of requiredFiles) {
202
+ const filePath = path.join(adminPath, name);
203
+ const stat = fs.lstatSync(filePath);
204
+ if (stat.isSymbolicLink() || !stat.isFile())
205
+ throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
206
+ }
207
+ const adminGitdir = fs.readFileSync(path.join(adminPath, 'gitdir'), 'utf8').trim();
208
+ const adminCommonDir = fs.readFileSync(path.join(adminPath, 'commondir'), 'utf8').trim();
209
+ const adminGitdirPath = adminGitdir
210
+ ? fs.realpathSync.native(path.resolve(adminPath, adminGitdir))
211
+ : '';
212
+ const canonicalDotGit = fs.realpathSync.native(dotGit);
213
+ const adminCommonDirPath = adminCommonDir
214
+ ? fs.realpathSync.native(path.resolve(adminPath, adminCommonDir))
215
+ : '';
216
+ if (!adminGitdir || adminGitdirPath !== canonicalDotGit
217
+ || !adminCommonDir || adminCommonDirPath !== commonDir) {
218
+ throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
219
+ }
220
+ const headContent = fs.readFileSync(path.join(adminPath, 'HEAD'), 'utf8');
221
+ const indexContent = fs.readFileSync(path.join(adminPath, 'index'));
222
+ if (!hasValidHeadContent(headContent) || indexContent.subarray(0, 4).toString('ascii') !== 'DIRC')
223
+ return;
224
+ const validationEnv = { ...process.env };
225
+ for (const key of ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE', 'GIT_WORK_TREE', 'GIT_PREFIX']) {
226
+ delete validationEnv[key];
227
+ }
228
+ validationEnv.GIT_DIR = adminPath;
229
+ validationEnv.GIT_WORK_TREE = path.resolve(worktree);
230
+ if (!probeMetadataCommand(probe, repoRoot, ['rev-parse', '--verify', 'HEAD^{commit}'], validationEnv))
231
+ return;
232
+ if (!probeMetadataCommand(probe, repoRoot, ['ls-files', '--stage', '-z'], validationEnv))
233
+ return;
234
+ throw new Error('WORKTREE_RECOVERY_METADATA_REGISTERED');
235
+ }
236
+ function parseRecoveredStatus(raw) {
237
+ const records = raw.split('\0');
238
+ const changes = [];
239
+ for (let index = 0; index < records.length;) {
240
+ const status = records[index++] ?? '';
241
+ if (!status)
242
+ continue;
243
+ const code = status[0] ?? '';
244
+ if (code === 'R' || code === 'C') {
245
+ const originalPath = records[index++];
246
+ const nextPath = records[index++];
247
+ if (!originalPath || !nextPath)
248
+ throw new Error('Unable to parse recovered Git rename record');
249
+ changes.push({ indexStatus: code, worktreeStatus: ' ', path: nextPath, originalPath });
250
+ continue;
251
+ }
252
+ const filePath = records[index++];
253
+ if (!filePath)
254
+ throw new Error('Unable to parse recovered Git status record');
255
+ changes.push({
256
+ indexStatus: code === 'D' ? ' ' : code,
257
+ worktreeStatus: code === 'D' ? 'D' : ' ',
258
+ path: filePath
259
+ });
260
+ }
261
+ return changes;
262
+ }
263
+ function captureRecoveredOnce(worktree, recovery, probe) {
264
+ const resolvedWorktree = path.resolve(worktree);
265
+ const resolvedBase = path.resolve(recovery.worktreeBase);
266
+ const relative = path.relative(resolvedBase, resolvedWorktree);
267
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
268
+ throw new Error(`WORKTREE_RECOVERY_PATH_INVALID: ${resolvedWorktree}`);
269
+ }
270
+ const stat = fs.lstatSync(resolvedWorktree);
271
+ if (!stat.isDirectory() || stat.isSymbolicLink())
272
+ throw new Error('WORKTREE_RECOVERY_PATH_INVALID');
273
+ const canonicalBase = fs.realpathSync.native(resolvedBase);
274
+ const canonicalWorktree = fs.realpathSync.native(resolvedWorktree);
275
+ const canonicalRelative = path.relative(canonicalBase, canonicalWorktree);
276
+ if (!canonicalRelative || canonicalRelative.startsWith('..') || path.isAbsolute(canonicalRelative)) {
277
+ throw new Error('WORKTREE_RECOVERY_PATH_INVALID');
278
+ }
279
+ const resolvedWorkspace = resolveSandboxTarget(recovery.branch, recovery.repoRoot).workspace;
280
+ if (recovery.identitySource === 'branch-only' && resolvedWorkspace.mode !== 'branch-only') {
281
+ throw new Error('WORKTREE_RECOVERY_IDENTITY_CHANGED');
282
+ }
283
+ if (recovery.identitySource === 'task-bound'
284
+ && (resolvedWorkspace.mode !== 'task-bound' || resolvedWorkspace.taskId !== recovery.taskId)) {
285
+ throw new Error('WORKTREE_RECOVERY_IDENTITY_CHANGED');
286
+ }
287
+ assertRecoverableMetadata(resolvedWorktree, recovery.repoRoot, probe);
288
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-recovered-index-'));
289
+ const env = { ...process.env };
290
+ for (const key of ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_PREFIX'])
291
+ delete env[key];
292
+ env.GIT_INDEX_FILE = path.join(tempDir, 'index');
293
+ env.GIT_WORK_TREE = resolvedWorktree;
294
+ try {
295
+ const baseline = probeTextWithEnv(probe, recovery.repoRoot, [
296
+ 'rev-parse', '--verify', `refs/heads/${recovery.branch}^{commit}`
297
+ ], env).trim();
298
+ if (!baseline)
299
+ throw new Error(`WORKTREE_RECOVERY_BRANCH_INVALID: ${recovery.branch}`);
300
+ probeTextWithEnv(probe, recovery.repoRoot, ['read-tree', baseline], env);
301
+ probeTextWithEnv(probe, recovery.repoRoot, ['add', '-A', '--', '.'], env);
302
+ const status = probeTextWithEnv(probe, recovery.repoRoot, [
303
+ 'diff', '--cached', '--name-status', '--find-renames', '-z', '--', '.'
304
+ ], env);
305
+ const changes = parseRecoveredStatus(status);
306
+ const hash = createHash('sha256');
307
+ updatePart(hash, 'source', 'recovered');
308
+ updatePart(hash, 'worktree', resolvedWorktree);
309
+ updatePart(hash, 'repo-root', path.resolve(recovery.repoRoot));
310
+ updatePart(hash, 'branch', recovery.branch);
311
+ updatePart(hash, 'baseline', baseline);
312
+ updatePart(hash, 'identity-source', recovery.identitySource);
313
+ updatePart(hash, 'task-id', recovery.taskId ?? '');
314
+ updatePart(hash, 'status', status);
315
+ for (const change of changes) {
316
+ hashPath(hash, resolvedWorktree, change.path, probe);
317
+ if (change.originalPath)
318
+ hashPath(hash, resolvedWorktree, change.originalPath, probe);
319
+ }
320
+ return {
321
+ worktree: resolvedWorktree,
322
+ branch: recovery.branch,
323
+ head: baseline,
324
+ changes,
325
+ identity: hash.digest('hex'),
326
+ source: 'recovered',
327
+ recovery: { ...recovery }
328
+ };
329
+ }
330
+ finally {
331
+ fs.rmSync(tempDir, { recursive: true, force: true });
332
+ }
333
+ }
141
334
  function inspectWorktree(worktree, { probe = runProbe } = {}) {
142
335
  const resolvedWorktree = path.resolve(worktree);
143
336
  try {
@@ -156,6 +349,24 @@ function inspectWorktree(worktree, { probe = runProbe } = {}) {
156
349
  };
157
350
  }
158
351
  }
352
+ function inspectRecoveredWorktree(worktree, recovery, { probe = runProbe } = {}) {
353
+ const resolvedWorktree = path.resolve(worktree);
354
+ try {
355
+ const first = captureRecoveredOnce(resolvedWorktree, recovery, probe);
356
+ const second = captureRecoveredOnce(resolvedWorktree, recovery, probe);
357
+ if (first.identity !== second.identity) {
358
+ return { status: 'failed', worktree: resolvedWorktree, message: 'Snapshot changed while recovering worktree' };
359
+ }
360
+ return { status: second.changes.length === 0 ? 'clean' : 'dirty', snapshot: second };
361
+ }
362
+ catch (error) {
363
+ return {
364
+ status: 'failed',
365
+ worktree: resolvedWorktree,
366
+ message: error instanceof Error ? error.message : String(error)
367
+ };
368
+ }
369
+ }
159
370
  function inspectWorktrees(worktrees) {
160
371
  return [...new Set(worktrees.map((worktree) => path.resolve(worktree)))].sort()
161
372
  .map((worktree) => inspectWorktree(worktree));
@@ -171,7 +382,9 @@ function createDiscardPermit(snapshot) {
171
382
  return { mode: 'discard', snapshot };
172
383
  }
173
384
  function verifyWorktreePermit(permit, options = {}) {
174
- const current = inspectWorktree(permit.snapshot.worktree, options);
385
+ const current = permit.snapshot.source === 'recovered' && permit.snapshot.recovery
386
+ ? inspectRecoveredWorktree(permit.snapshot.worktree, permit.snapshot.recovery, options)
387
+ : inspectWorktree(permit.snapshot.worktree, options);
175
388
  if (current.status === 'failed')
176
389
  throw new Error(`Unable to verify worktree permit: ${current.message}`);
177
390
  if (current.snapshot.identity !== permit.snapshot.identity || (permit.mode === 'clean' && current.status !== 'clean')) {
@@ -192,5 +405,5 @@ function formatWorktreeSnapshot(snapshot) {
192
405
  `Snapshot identity: ${snapshot.identity}`
193
406
  ].join('\n');
194
407
  }
195
- export { createCleanPermit, createDiscardPermit, formatWorktreeSnapshot, inspectWorktree, inspectWorktrees, verifyWorktreePermit };
408
+ export { createCleanPermit, createDiscardPermit, formatWorktreeSnapshot, inspectRecoveredWorktree, inspectWorktree, inspectWorktrees, verifyWorktreePermit };
196
409
  //# sourceMappingURL=worktree-safety.js.map
@@ -97,6 +97,56 @@ export function getProcessStartTime(pid, platform = process.platform) {
97
97
  return null;
98
98
  }
99
99
  }
100
+ export function getProcessIdentityState(identity, platform = process.platform) {
101
+ if (!Number.isInteger(identity.pid) || identity.pid <= 0 || !Number.isSafeInteger(identity.startTime)) {
102
+ return 'dead';
103
+ }
104
+ if (platform === 'linux') {
105
+ try {
106
+ const stat = parseLinuxProcessStat(fs.readFileSync(`/proc/${identity.pid}/stat`, 'utf8'));
107
+ if (stat === null)
108
+ return 'unknown';
109
+ if (stat.state === 'Z')
110
+ return 'dead';
111
+ return stat.startTime === identity.startTime ? 'alive' : 'dead';
112
+ }
113
+ catch (error) {
114
+ return error.code === 'ENOENT' ? 'dead' : 'unknown';
115
+ }
116
+ }
117
+ try {
118
+ process.kill(identity.pid, 0);
119
+ }
120
+ catch (error) {
121
+ const code = error.code;
122
+ if (code === 'ESRCH')
123
+ return 'dead';
124
+ if (code !== 'EPERM')
125
+ return 'unknown';
126
+ }
127
+ const query = buildProcessStartTimeQuery(identity.pid, platform);
128
+ if (query === null)
129
+ return 'unknown';
130
+ try {
131
+ const output = execFileSync(query.command, query.args, {
132
+ encoding: 'utf8',
133
+ env: platform === 'darwin' ? { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } : process.env
134
+ }).trim();
135
+ if (output.length === 0)
136
+ return 'unknown';
137
+ const startTime = platform === 'win32'
138
+ ? new Date(output).getTime()
139
+ : platform === 'darwin'
140
+ ? parseDarwinStartTime(output)
141
+ : null;
142
+ if (startTime === null || Number.isNaN(startTime))
143
+ return 'unknown';
144
+ return startTime === identity.startTime ? 'alive' : 'dead';
145
+ }
146
+ catch {
147
+ return 'unknown';
148
+ }
149
+ }
100
150
  function parsePidRecord(raw) {
101
151
  const trimmed = raw.trim();
102
152
  if (/^[1-9]\d*$/.test(trimmed)) {
@@ -200,6 +250,6 @@ export function removePidRecordIfMatches(pidFile, expected) {
200
250
  return removePidFileIfMatches(pidFile, state.snapshot);
201
251
  }
202
252
  export function processIdentityMatches(identity) {
203
- return getProcessStartTime(identity.pid) === identity.startTime;
253
+ return getProcessIdentityState(identity) === 'alive';
204
254
  }
205
255
  //# sourceMappingURL=process-state.js.map
@@ -5,6 +5,7 @@ import { locateActivityLog, appendActivityEntry } from './activity-log.js';
5
5
  import { assertWritableInventory, buildArtifactLinkSection, inspectTaskArtifacts, parseArtifactName } from './artifact-lifecycle.js';
6
6
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
7
7
  import { captureTaskWriteMetadata, writeTask } from './write.js';
8
+ import { allowsManualOverride } from './guard-override.js';
8
9
  const HEAD_RE = /^[0-9a-f]{40}$/;
9
10
  const REVIEW_ACTION_RE = /^Review PR \(Round ([1-9]\d*)\)( \[started\])?$/;
10
11
  const VERDICTS = new Set(['approved', 'changes-requested', 'commented']);
@@ -292,9 +293,11 @@ export function applyPrReviewActivityIntent(intent, options = {}) {
292
293
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
293
294
  if (!resolved.ok)
294
295
  return failed(intent, resolved.code, resolved.message, resolved.taskId);
295
- if (resolved.state !== 'active') {
296
+ if (resolved.state !== 'active' && !allowsManualOverride(options.manualOverride, 'activity-intent', 'TASK_STATE_MISMATCH')) {
296
297
  return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
297
298
  }
299
+ if (options.lockAlreadyHeld)
300
+ return applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot });
298
301
  try {
299
302
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-activity:${intent.kind}`, () => applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot }), options.lockOptions);
300
303
  }