@fitlab-ai/agent-infra 0.9.3 → 0.9.5

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 (203) hide show
  1. package/README.md +3 -1
  2. package/README.zh-CN.md +3 -1
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +144 -123
  6. package/dist/lib/agent-clients/adapter.js +94 -4
  7. package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
  8. package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
  9. package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
  10. package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
  12. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
  13. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
  14. package/dist/lib/agent-clients/adapters/codex.js +2 -2
  15. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
  16. package/dist/lib/agent-clients/adapters/opencode.js +15 -3
  17. package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  18. package/dist/lib/agent-clients/registry.js +9 -1
  19. package/dist/lib/git/worktree-identity.js +64 -0
  20. package/dist/lib/internal/codex-lifecycle.js +202 -0
  21. package/dist/lib/internal/sandbox-control.js +24 -0
  22. package/dist/lib/internal/task-activity.js +45 -13
  23. package/dist/lib/internal/task-create.js +48 -0
  24. package/dist/lib/internal/task-ledger.js +12 -3
  25. package/dist/lib/internal/task-lifecycle.js +19 -1
  26. package/dist/lib/internal/task-orchestration.js +170 -27
  27. package/dist/lib/platform/github-client.js +19 -2
  28. package/dist/lib/platform/pr-checks.js +1 -1
  29. package/dist/lib/platform/pull-requests.js +47 -1
  30. package/dist/lib/platform/releases.js +13 -5
  31. package/dist/lib/process-data/index.js +239 -0
  32. package/dist/lib/process-data/normalize.js +198 -0
  33. package/dist/lib/process-data/privacy.js +33 -0
  34. package/dist/lib/process-data/reconcile.js +95 -0
  35. package/dist/lib/process-data/repair.js +58 -0
  36. package/dist/lib/process-data/sources.js +345 -0
  37. package/dist/lib/process-data/store.js +282 -0
  38. package/dist/lib/process-data/types.js +2 -0
  39. package/dist/lib/run/index.js +4 -3
  40. package/dist/lib/run/tui.js +7 -8
  41. package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
  42. package/dist/lib/sandbox/capture.js +7 -2
  43. package/dist/lib/sandbox/commands/create.js +110 -228
  44. package/dist/lib/sandbox/commands/enter.js +15 -47
  45. package/dist/lib/sandbox/commands/list-running.js +16 -6
  46. package/dist/lib/sandbox/commands/ls.js +10 -6
  47. package/dist/lib/sandbox/commands/rebuild.js +4 -3
  48. package/dist/lib/sandbox/commands/refresh.js +4 -84
  49. package/dist/lib/sandbox/commands/rm.js +24 -3
  50. package/dist/lib/sandbox/commands/show.js +12 -3
  51. package/dist/lib/sandbox/commands/start.js +7 -4
  52. package/dist/lib/sandbox/config.js +2 -0
  53. package/dist/lib/sandbox/constants.js +6 -0
  54. package/dist/lib/sandbox/control/client.js +58 -0
  55. package/dist/lib/sandbox/control/protocol.js +54 -0
  56. package/dist/lib/sandbox/control/server.js +144 -0
  57. package/dist/lib/sandbox/dockerfile.js +36 -13
  58. package/dist/lib/sandbox/mounts.js +17 -4
  59. package/dist/lib/sandbox/recovery.js +128 -5
  60. package/dist/lib/sandbox/redaction.js +12 -0
  61. package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
  62. package/dist/lib/sandbox/workspace-identity.js +95 -0
  63. package/dist/lib/sandbox/workspace-view.js +109 -0
  64. package/dist/lib/task/activity-intent.js +257 -51
  65. package/dist/lib/task/activity-log.js +32 -5
  66. package/dist/lib/task/commit-finalization.js +278 -0
  67. package/dist/lib/task/commit-intent.js +165 -0
  68. package/dist/lib/task/create-service.js +137 -0
  69. package/dist/lib/task/create.js +333 -0
  70. package/dist/lib/task/decision-intents.js +31 -17
  71. package/dist/lib/task/delegation-receipts.js +2 -2
  72. package/dist/lib/task/implementation-inputs.js +54 -7
  73. package/dist/lib/task/ledger-intents.js +35 -1
  74. package/dist/lib/task/orchestration.js +505 -34
  75. package/dist/lib/task/resolve-ref.js +41 -1
  76. package/dist/lib/task/review-fingerprint.js +4 -0
  77. package/dist/lib/task/workspace-snapshot.js +78 -3
  78. package/lib/agent-clients/adapter.ts +130 -6
  79. package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
  80. package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
  81. package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
  82. package/lib/agent-clients/adapters/claude-code.ts +21 -3
  83. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
  84. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
  85. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
  86. package/lib/agent-clients/adapters/codex.ts +2 -2
  87. package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
  88. package/lib/agent-clients/adapters/opencode.ts +18 -3
  89. package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  90. package/lib/agent-clients/registry.ts +9 -1
  91. package/lib/git/worktree-identity.ts +86 -0
  92. package/lib/internal/codex-lifecycle.ts +204 -0
  93. package/lib/internal/sandbox-control.ts +24 -0
  94. package/lib/internal/task-activity.ts +50 -14
  95. package/lib/internal/task-create.ts +49 -0
  96. package/lib/internal/task-ledger.ts +12 -3
  97. package/lib/internal/task-lifecycle.ts +22 -1
  98. package/lib/internal/task-orchestration.ts +117 -21
  99. package/lib/platform/github-client.ts +20 -2
  100. package/lib/platform/pr-checks.ts +1 -1
  101. package/lib/platform/pull-requests.ts +49 -1
  102. package/lib/platform/releases.ts +16 -4
  103. package/lib/process-data/index.ts +224 -0
  104. package/lib/process-data/normalize.ts +202 -0
  105. package/lib/process-data/privacy.ts +41 -0
  106. package/lib/process-data/reconcile.ts +110 -0
  107. package/lib/process-data/repair.ts +64 -0
  108. package/lib/process-data/sources.ts +354 -0
  109. package/lib/process-data/store.ts +315 -0
  110. package/lib/process-data/types.ts +135 -0
  111. package/lib/run/index.ts +5 -3
  112. package/lib/run/tui.ts +11 -8
  113. package/lib/sandbox/agent-client-reconciler.ts +44 -2
  114. package/lib/sandbox/capture.ts +9 -2
  115. package/lib/sandbox/commands/create.ts +127 -260
  116. package/lib/sandbox/commands/enter.ts +13 -59
  117. package/lib/sandbox/commands/list-running.ts +23 -6
  118. package/lib/sandbox/commands/ls.ts +22 -6
  119. package/lib/sandbox/commands/rebuild.ts +4 -3
  120. package/lib/sandbox/commands/refresh.ts +4 -126
  121. package/lib/sandbox/commands/rm.ts +24 -3
  122. package/lib/sandbox/commands/show.ts +29 -3
  123. package/lib/sandbox/commands/start.ts +6 -4
  124. package/lib/sandbox/config.ts +4 -0
  125. package/lib/sandbox/constants.ts +8 -0
  126. package/lib/sandbox/control/client.ts +79 -0
  127. package/lib/sandbox/control/protocol.ts +110 -0
  128. package/lib/sandbox/control/server.ts +155 -0
  129. package/lib/sandbox/dockerfile.ts +66 -13
  130. package/lib/sandbox/mounts.ts +24 -4
  131. package/lib/sandbox/recovery.ts +163 -6
  132. package/lib/sandbox/redaction.ts +14 -0
  133. package/lib/sandbox/runtimes/base.dockerfile +24 -74
  134. package/lib/sandbox/tool-types.ts +16 -0
  135. package/lib/sandbox/workspace-identity.ts +123 -0
  136. package/lib/sandbox/workspace-view.ts +150 -0
  137. package/lib/task/activity-intent.ts +350 -61
  138. package/lib/task/activity-log.ts +46 -7
  139. package/lib/task/commit-finalization.ts +376 -0
  140. package/lib/task/commit-intent.ts +230 -0
  141. package/lib/task/create-service.ts +171 -0
  142. package/lib/task/create.ts +372 -0
  143. package/lib/task/decision-intents.ts +32 -12
  144. package/lib/task/delegation-receipts.ts +2 -2
  145. package/lib/task/implementation-inputs.ts +62 -8
  146. package/lib/task/ledger-intents.ts +43 -3
  147. package/lib/task/orchestration.ts +673 -41
  148. package/lib/task/resolve-ref.ts +46 -1
  149. package/lib/task/review-fingerprint.ts +9 -0
  150. package/lib/task/workspace-snapshot.ts +98 -4
  151. package/package.json +7 -7
  152. package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
  153. package/templates/.agents/rules/README.en.md +1 -1
  154. package/templates/.agents/rules/README.zh-CN.md +1 -1
  155. package/templates/.agents/rules/human-decision-context.en.md +11 -2
  156. package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
  157. package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
  158. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  159. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
  160. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
  161. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  162. package/templates/.agents/rules/task-management.en.md +3 -0
  163. package/templates/.agents/rules/task-management.zh-CN.md +3 -0
  164. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  165. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  166. package/templates/.agents/skills/commit/SKILL.en.md +21 -17
  167. package/templates/.agents/skills/commit/SKILL.zh-CN.md +28 -16
  168. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +50 -0
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +56 -0
  170. package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
  171. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
  172. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  173. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  174. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  175. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  176. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  177. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  178. package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
  179. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
  180. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  181. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  182. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  183. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  184. package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
  185. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
  186. package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
  187. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
  188. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
  189. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  190. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  191. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
  192. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
  193. package/templates/.agents/templates/task.en.md +1 -1
  194. package/templates/.agents/templates/task.zh-CN.md +1 -1
  195. package/templates/.claude/commands/watch-pr.en.md +1 -1
  196. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  197. package/templates/.claude/rules/tool-preferences.en.md +20 -0
  198. package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
  199. package/templates/.codex/hooks.json +50 -0
  200. package/templates/.opencode/commands/watch-pr.en.md +1 -1
  201. package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
  202. package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
  203. package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
@@ -1,21 +1,28 @@
1
1
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
2
2
  import {
3
+ abortCommitIntent,
3
4
  activateMatchingOrchestrationDelegation,
4
5
  activateOrchestrationDelegation,
5
6
  advanceOrchestration,
7
+ beginCommitIntent,
6
8
  beginOrResumeOrchestration,
7
- completeCommitOrchestrationStage,
9
+ checkpointCommitIntent,
10
+ completeCommitIntent,
8
11
  pauseOrchestration,
9
12
  prepareOrchestrationDelegation,
13
+ recoverCommitIntent,
10
14
  routeOrchestration,
11
15
  sealMatchingOrchestrationDelegation,
12
16
  sealOrchestrationDelegation,
13
- statusOrchestration
17
+ startCommitAttempt,
18
+ statusCommitIntent,
19
+ statusOrchestration,
20
+ terminateCommitAttempt
14
21
  } from '../task/orchestration.ts';
15
22
  import { isAgentClientId } from '../agent-clients/types.ts';
16
23
  import type { AgentClientId } from '../agent-clients/types.ts';
17
24
 
18
- const USAGE = 'Usage: agent-infra-internal task-orchestration <task-ref|auto> <begin-or-resume|route|prepare|hook-start|stage-completed|hook-stop|advance|pause|status> [options]\n';
25
+ const USAGE = 'Usage: agent-infra-internal task-orchestration <task-ref|auto> <begin-or-resume|route|prepare|hook-start|hook-stop|advance|pause|status|commit-start|commit-begin|commit-checkpoint|commit-complete|commit-recover|commit-abort|commit-terminate|commit-status> [options]\n';
19
26
 
20
27
  function usageFailure(message: string): void {
21
28
  process.stdout.write(`${JSON.stringify({
@@ -36,21 +43,33 @@ function taskOrchestration(args: string[] = []): void {
36
43
  return;
37
44
  }
38
45
  const [taskRef, intent] = args;
39
- if (!['begin-or-resume', 'route', 'prepare', 'hook-start', 'stage-completed', 'hook-stop', 'advance', 'pause', 'status'].includes(intent!)) {
46
+ if (![
47
+ 'begin-or-resume', 'route', 'prepare', 'hook-start', 'hook-stop', 'advance', 'pause', 'status',
48
+ 'commit-start', 'commit-begin', 'commit-checkpoint', 'commit-complete', 'commit-recover', 'commit-abort', 'commit-terminate', 'commit-status'
49
+ ].includes(intent!)) {
40
50
  usageFailure(`unknown intent '${intent}'`);
41
51
  return;
42
52
  }
43
53
  const values: Record<string, string> = {};
44
54
  const seen = new Set<string>();
55
+ let orchestrated = false;
45
56
  for (let index = 2; index < args.length; index += 1) {
46
57
  const flag = args[index]!;
58
+ if (flag === '--orchestrated') {
59
+ if (seen.has(flag)) { usageFailure(`duplicate option '${flag}'`); return; }
60
+ seen.add(flag);
61
+ orchestrated = true;
62
+ continue;
63
+ }
47
64
  if (![
48
65
  '--max-steps', '--executor-model', '--executor-reasoning-effort',
49
66
  '--reviewer-model', '--reviewer-reasoning-effort',
50
67
  '--client', '--requested-model', '--requested-reasoning-effort', '--parent-id', '--before-fingerprint',
51
68
  '--native-agent', '--child-id', '--spawn-mode', '--actual-model', '--actual-reasoning-effort',
52
69
  '--model-fallback-reason', '--reasoning-effort-fallback-reason',
53
- '--exit-code', '--after-fingerprint', '--changed-paths', '--code', '--message', '--recoverable', '--agent'
70
+ '--exit-code', '--after-fingerprint', '--changed-paths', '--code', '--message', '--recoverable', '--agent',
71
+ '--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
72
+ , '--attempt', '--git-worktree-root'
54
73
  ].includes(flag)) {
55
74
  usageFailure(`unknown option '${flag}'`);
56
75
  return;
@@ -67,13 +86,95 @@ function taskOrchestration(args: string[] = []): void {
67
86
  }
68
87
  values[flag] = value;
69
88
  }
89
+ const commitIntentFlags: Record<string, readonly string[]> = {
90
+ 'commit-start': ['--agent', '--git-worktree-root'],
91
+ 'commit-begin': ['--agent', '--baseline-head', '--attempt', '--orchestrated', '--git-worktree-root'],
92
+ 'commit-checkpoint': ['--token', '--kind', '--head', '--remote', '--ref', '--git-worktree-root'],
93
+ 'commit-complete': ['--token', '--agent', '--git-worktree-root'],
94
+ 'commit-recover': ['--agent', '--git-worktree-root'],
95
+ 'commit-abort': ['--token', '--expected-head', '--git-worktree-root'],
96
+ 'commit-terminate': ['--attempt', '--agent', '--code', '--git-worktree-root'],
97
+ 'commit-status': ['--git-worktree-root']
98
+ };
99
+ if (intent! in commitIntentFlags) {
100
+ const allowed = commitIntentFlags[intent!]!;
101
+ const invalid = [...seen].find((flag) => !allowed.includes(flag));
102
+ if (invalid) { usageFailure(`intent '${intent}' does not accept '${invalid}'`); return; }
103
+ } else {
104
+ const commitOnly = [...seen].find((flag) => [
105
+ '--orchestrated', '--baseline-head', '--attempt', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
106
+ ].includes(flag));
107
+ if (commitOnly) { usageFailure(`intent '${intent}' does not accept '${commitOnly}'`); return; }
108
+ }
70
109
  const requireValues = (flags: string[]) => flags.find((flag) => values[flag] === undefined);
110
+ const coreOptions = values['--git-worktree-root'] === undefined
111
+ ? {}
112
+ : { gitWorktreeRoot: values['--git-worktree-root'] };
71
113
  if (values['--client'] !== undefined && !isAgentClientId(values['--client'])) {
72
114
  usageFailure(`unknown client '${values['--client']}'`);
73
115
  return;
74
116
  }
75
117
  let result;
76
- if (intent === 'begin-or-resume') {
118
+ if (intent === 'commit-start') {
119
+ const missing = requireValues(['--agent']);
120
+ if (missing) { usageFailure(`intent 'commit-start' requires '${missing}'`); return; }
121
+ const agent = normalizeAgentToken(values['--agent']!);
122
+ if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
123
+ result = startCommitAttempt(taskRef!, { agent }, coreOptions);
124
+ } else if (intent === 'commit-begin') {
125
+ const missing = requireValues(['--agent', '--baseline-head', '--attempt']);
126
+ if (missing) { usageFailure(`intent 'commit-begin' requires '${missing}'`); return; }
127
+ const agent = normalizeAgentToken(values['--agent']!);
128
+ if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
129
+ result = beginCommitIntent(taskRef!, {
130
+ agent, orchestrated, baselineHead: values['--baseline-head']!, attempt: values['--attempt']!
131
+ }, coreOptions);
132
+ } else if (intent === 'commit-checkpoint') {
133
+ const missing = requireValues(['--token', '--kind', '--head']);
134
+ if (missing) { usageFailure(`intent 'commit-checkpoint' requires '${missing}'`); return; }
135
+ if (!['committed', 'pushed'].includes(values['--kind']!)) {
136
+ usageFailure('--kind must be committed or pushed'); return;
137
+ }
138
+ if (values['--kind'] === 'pushed') {
139
+ const pushedMissing = requireValues(['--remote', '--ref']);
140
+ if (pushedMissing) { usageFailure(`pushed checkpoint requires '${pushedMissing}'`); return; }
141
+ } else if (values['--remote'] !== undefined || values['--ref'] !== undefined) {
142
+ usageFailure('committed checkpoint does not accept --remote or --ref'); return;
143
+ }
144
+ result = checkpointCommitIntent(taskRef!, {
145
+ token: values['--token']!, kind: values['--kind'] as 'committed' | 'pushed',
146
+ head: values['--head']!, remote: values['--remote'], ref: values['--ref']
147
+ }, coreOptions);
148
+ } else if (intent === 'commit-complete') {
149
+ const missing = requireValues(['--token', '--agent']);
150
+ if (missing) { usageFailure(`intent 'commit-complete' requires '${missing}'`); return; }
151
+ const agent = normalizeAgentToken(values['--agent']!);
152
+ if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
153
+ result = completeCommitIntent(taskRef!, { token: values['--token']!, agent }, coreOptions);
154
+ } else if (intent === 'commit-recover') {
155
+ const missing = requireValues(['--agent']);
156
+ if (missing) { usageFailure(`intent 'commit-recover' requires '${missing}'`); return; }
157
+ const agent = normalizeAgentToken(values['--agent']!);
158
+ if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
159
+ result = recoverCommitIntent(taskRef!, { agent }, coreOptions);
160
+ } else if (intent === 'commit-abort') {
161
+ const missing = requireValues(['--token', '--expected-head']);
162
+ if (missing) { usageFailure(`intent 'commit-abort' requires '${missing}'`); return; }
163
+ result = abortCommitIntent(taskRef!, { token: values['--token']!, expectedHead: values['--expected-head']! }, coreOptions);
164
+ } else if (intent === 'commit-terminate') {
165
+ const missing = requireValues(['--attempt', '--agent', '--code']);
166
+ if (missing) { usageFailure(`intent 'commit-terminate' requires '${missing}'`); return; }
167
+ const agent = normalizeAgentToken(values['--agent']!);
168
+ if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
169
+ if (!/^[A-Z][A-Z0-9_]{2,63}$/.test(values['--code']!)) {
170
+ usageFailure('--code must be a stable uppercase identifier'); return;
171
+ }
172
+ result = terminateCommitAttempt(taskRef!, {
173
+ attempt: values['--attempt']!, agent, code: values['--code']!
174
+ }, coreOptions);
175
+ } else if (intent === 'commit-status') {
176
+ result = statusCommitIntent(taskRef!, coreOptions);
177
+ } else if (intent === 'begin-or-resume') {
77
178
  const missing = requireValues(['--client']);
78
179
  if (missing) { usageFailure(`intent 'begin-or-resume' requires '${missing}'`); return; }
79
180
  const maxSteps = values['--max-steps'] === undefined ? undefined : Number(values['--max-steps']);
@@ -90,6 +191,7 @@ function taskOrchestration(args: string[] = []): void {
90
191
  return;
91
192
  }
92
193
  result = beginOrResumeOrchestration(taskRef!, {
194
+ ...coreOptions,
93
195
  maxSteps,
94
196
  client: values['--client'] as AgentClientId,
95
197
  modelPolicy: hasAnyPolicy ? {
@@ -104,9 +206,9 @@ function taskOrchestration(args: string[] = []): void {
104
206
  } : undefined
105
207
  });
106
208
  } else if (intent === 'route') {
107
- result = routeOrchestration(taskRef!);
209
+ result = routeOrchestration(taskRef!, coreOptions);
108
210
  } else if (intent === 'status') {
109
- result = statusOrchestration(taskRef!);
211
+ result = statusOrchestration(taskRef!, coreOptions);
110
212
  } else if (intent === 'prepare') {
111
213
  const missing = requireValues(['--client']);
112
214
  if (missing) { usageFailure(`intent 'prepare' requires '${missing}'`); return; }
@@ -114,7 +216,7 @@ function taskOrchestration(args: string[] = []): void {
114
216
  client: values['--client'] as AgentClientId,
115
217
  requestedModel: values['--requested-model'],
116
218
  requestedReasoningEffort: values['--requested-reasoning-effort']
117
- });
219
+ }, coreOptions);
118
220
  } else if (intent === 'hook-start') {
119
221
  const missing = requireValues([
120
222
  ...(taskRef === 'auto' ? ['--client'] : []),
@@ -130,15 +232,15 @@ function taskOrchestration(args: string[] = []): void {
130
232
  reasoningEffortFallbackReason: values['--reasoning-effort-fallback-reason']
131
233
  };
132
234
  result = taskRef === 'auto'
133
- ? activateMatchingOrchestrationDelegation(values['--client'] as AgentClientId, event)
134
- : activateOrchestrationDelegation(taskRef!, event);
235
+ ? activateMatchingOrchestrationDelegation(values['--client'] as AgentClientId, event, coreOptions)
236
+ : activateOrchestrationDelegation(taskRef!, event, coreOptions);
135
237
  } else if (intent === 'hook-stop') {
136
238
  if (taskRef === 'auto') {
137
239
  const missing = requireValues(['--client', '--native-agent', '--child-id']);
138
240
  if (missing) { usageFailure(`intent 'hook-stop' requires '${missing}'`); return; }
139
241
  result = sealMatchingOrchestrationDelegation(values['--client'] as AgentClientId, {
140
242
  nativeAgent: values['--native-agent']!, childId: values['--child-id']!
141
- });
243
+ }, coreOptions);
142
244
  } else {
143
245
  const missing = requireValues(['--child-id', '--exit-code', '--after-fingerprint']);
144
246
  if (missing) { usageFailure(`intent 'hook-stop' requires '${missing}'`); return; }
@@ -148,21 +250,15 @@ function taskOrchestration(args: string[] = []): void {
148
250
  childId: values['--child-id']!, exitCode,
149
251
  afterFingerprint: values['--after-fingerprint']!,
150
252
  changedPaths: values['--changed-paths'] ? values['--changed-paths']!.split(',').filter(Boolean) : []
151
- });
253
+ }, coreOptions);
152
254
  }
153
- } else if (intent === 'stage-completed') {
154
- const missing = requireValues(['--agent']);
155
- if (missing) { usageFailure(`intent 'stage-completed' requires '${missing}'`); return; }
156
- const agent = normalizeAgentToken(values['--agent']!);
157
- if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
158
- result = completeCommitOrchestrationStage(taskRef!, agent);
159
255
  } else if (intent === 'advance') {
160
- result = advanceOrchestration(taskRef!);
256
+ result = advanceOrchestration(taskRef!, coreOptions);
161
257
  } else {
162
258
  const missing = requireValues(['--code', '--message', '--recoverable']);
163
259
  if (missing) { usageFailure(`intent 'pause' requires '${missing}'`); return; }
164
260
  if (!['true', 'false'].includes(values['--recoverable']!)) { usageFailure('--recoverable must be true or false'); return; }
165
- result = pauseOrchestration(taskRef!, values['--code']!, values['--message']!, values['--recoverable'] === 'true');
261
+ result = pauseOrchestration(taskRef!, values['--code']!, values['--message']!, values['--recoverable'] === 'true', coreOptions);
166
262
  }
167
263
  process.stdout.write(`${JSON.stringify(result)}\n`);
168
264
  if (result.status === 'failed') process.exitCode = 1;
@@ -24,6 +24,15 @@ type ClientOptions = {
24
24
  };
25
25
 
26
26
  const MINIMUM_GITHUB_CLI_VERSION = '2.16.0';
27
+ const GITHUB_CLI_MAX_BUFFER = 64 * 1024 * 1024;
28
+ const ERROR_DETAIL_LIMIT = 4096;
29
+
30
+ function boundedFailureDetail(result: RunResult): string {
31
+ const diagnostic = `${result.stderr}\n${result.error?.message || ''}`.trim() || result.stdout.trim();
32
+ return diagnostic.length <= ERROR_DETAIL_LIMIT
33
+ ? diagnostic
34
+ : `${diagnostic.slice(0, ERROR_DETAIL_LIMIT)}… [truncated]`;
35
+ }
27
36
 
28
37
  function defaultRunner(args: string[], options: RunOptions): RunResult {
29
38
  const command = process.env.AGENT_INFRA_GH_BIN || 'gh';
@@ -38,6 +47,7 @@ function defaultRunner(args: string[], options: RunOptions): RunResult {
38
47
  encoding: 'utf8',
39
48
  env: process.env,
40
49
  input: options.input,
50
+ maxBuffer: GITHUB_CLI_MAX_BUFFER,
41
51
  shell: false
42
52
  });
43
53
  return {
@@ -60,7 +70,15 @@ function defaultSleep(delayMs: number): void {
60
70
  }
61
71
 
62
72
  function classifyGitHubFailure(result: RunResult): PlatformError {
63
- const detail = `${result.stderr}\n${result.stdout}\n${result.error?.message || ''}`.trim();
73
+ const errorCode = result.error && 'code' in result.error ? (result.error as NodeJS.ErrnoException).code : undefined;
74
+ if (errorCode === 'ENOBUFS') {
75
+ return {
76
+ code: 'PLATFORM_OUTPUT_TOO_LARGE',
77
+ message: 'GitHub CLI output exceeded the configured limit',
78
+ retryable: false
79
+ };
80
+ }
81
+ const detail = boundedFailureDetail(result);
64
82
  const lower = detail.toLowerCase();
65
83
  if (/\b401\b|bad credentials|authentication required|not logged into/.test(lower)) {
66
84
  return { code: 'AUTH_REQUIRED', message: detail || 'GitHub authentication is required', retryable: false };
@@ -77,7 +95,7 @@ function classifyGitHubFailure(result: RunResult): PlatformError {
77
95
  if (/\b422\b|validation failed/.test(lower)) {
78
96
  return { code: 'PLATFORM_REQUEST_INVALID', message: detail || 'GitHub rejected the request', retryable: false };
79
97
  }
80
- if (result.error && 'code' in result.error && (result.error as NodeJS.ErrnoException).code === 'ENOENT') {
98
+ if (errorCode === 'ENOENT') {
81
99
  return { code: 'PLATFORM_DEPENDENCY_MISSING', message: detail || 'GitHub CLI is unavailable', retryable: false };
82
100
  }
83
101
  return { code: 'PLATFORM_REQUEST_FAILED', message: detail || 'GitHub request failed', retryable: false };
@@ -109,7 +109,7 @@ registerPlatformCapabilities('github', {
109
109
  inspectRequiredChecks({ client, repository, number, cwd }) {
110
110
  const github = (client as GitHubClient | undefined) || createGitHubClient();
111
111
  const inspected = github.json<unknown>([
112
- 'pr', 'checks', String(number), '--repo', repository, '--required',
112
+ 'pr', 'checks', String(number), '--repo', repository,
113
113
  '--json', 'name,state,bucket,link,workflow,startedAt,completedAt'
114
114
  ], { cwd });
115
115
  return inspected.ok
@@ -19,6 +19,8 @@ import { planPullRequestMetadata } from './pull-request-metadata.ts';
19
19
  import { platformResult } from './types.ts';
20
20
  import type { PlatformOperation, PlatformResult } from './types.ts';
21
21
  import { inspectCompletionArtifacts } from '../task/finalization-artifacts.ts';
22
+ import { inspectCreatePrCommitGate } from '../task/commit-finalization.ts';
23
+ import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
22
24
 
23
25
  type PullRequestSnapshot = PlatformChangeRequestSnapshot;
24
26
 
@@ -747,13 +749,23 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
747
749
  }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
748
750
  }
749
751
 
750
- function createPlatformPullRequest(taskRef: string, options: CreateOptions): PullRequestResult {
752
+ function createPlatformPullRequestUnlocked(taskRef: string, options: CreateOptions): PullRequestResult {
751
753
  const base = resolvedContext(taskRef, options);
752
754
  if (!base.ok) return base.output;
753
755
  if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
754
756
  error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
755
757
  });
756
758
  if (base.prNumber) return inspectPlatformPullRequest(taskRef, options);
759
+ const gate = inspectCreatePrCommitGate(base.resolved.taskDir, base.resolved.repoRoot, base.resolved.taskId);
760
+ if (!gate.allowed) return result('blocked', base.resolved.taskId, base.issueNumber, null, {
761
+ platform: base.context.platform,
762
+ capabilities: base.context.capabilities,
763
+ error: {
764
+ code: gate.code!,
765
+ message: `${gate.message}; action=${gate.action}`,
766
+ retryable: gate.action === 'rerun-commit' || gate.action === 'rerun-review-code'
767
+ }
768
+ });
757
769
  const located = locatePullRequest(base, options.head, options.base);
758
770
  if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS') return result('failed', base.resolved.taskId, base.issueNumber, null, {
759
771
  platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
@@ -811,6 +823,42 @@ function createPlatformPullRequest(taskRef: string, options: CreateOptions): Pul
811
823
  });
812
824
  }
813
825
 
826
+ function createPlatformPullRequest(taskRef: string, options: CreateOptions): PullRequestResult {
827
+ const base = resolvedContext(taskRef, options);
828
+ if (!base.ok) return base.output;
829
+ if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
830
+ return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
831
+ error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
832
+ });
833
+ }
834
+ if (base.prNumber) return inspectPlatformPullRequest(taskRef, options);
835
+ try {
836
+ return withTaskExecutionLock(
837
+ base.resolved.repoRoot,
838
+ base.resolved.taskId,
839
+ 'platform-pr.create',
840
+ () => createPlatformPullRequestUnlocked(taskRef, options)
841
+ );
842
+ } catch (error) {
843
+ if (error instanceof TaskExecutionLockError) {
844
+ return result('blocked', base.resolved.taskId, base.issueNumber, null, {
845
+ platform: base.context.platform,
846
+ capabilities: base.context.capabilities,
847
+ error: { code: error.code, message: error.message, retryable: true }
848
+ });
849
+ }
850
+ return result('failed', base.resolved.taskId, base.issueNumber, null, {
851
+ platform: base.context.platform,
852
+ capabilities: base.context.capabilities,
853
+ error: {
854
+ code: 'PR_CREATE_FAILED',
855
+ message: error instanceof Error ? error.message : String(error),
856
+ retryable: false
857
+ }
858
+ });
859
+ }
860
+ }
861
+
814
862
  function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullRequestResult {
815
863
  const base = resolvedContext(taskRef, options);
816
864
  if (!base.ok) return base.output;
@@ -48,18 +48,30 @@ function reconcileReleaseMilestones(version: string, options: ReleaseOptions = {
48
48
  if (!listed.ok) return { ...platformResult(listed.error.retryable ? 'blocked' : 'failed'), changed: false, operations: [], error: listed.error };
49
49
  const milestones = flattenPages(listed.value).map((item) => ({ title: String(item.title || ''), number: Number(item.number), state: String(item.state || '') }));
50
50
  const major = Number(match[1]); const minor = Number(match[2]); const patch = Number(match[3]);
51
- const ensure = [`${major}.${minor}.${patch + 1}`, `${major}.${minor}.x`];
52
- if (patch === 0) ensure.push(`${major}.${minor + 1}.0`, `${major}.${minor + 1}.x`);
51
+ const nextPatch = `${major}.${minor}.${patch + 1}`;
52
+ const currentLine = `${major}.${minor}.x`;
53
+ const ensure = [
54
+ { title: nextPatch, description: `Issues that we want to release in v${nextPatch}.` },
55
+ { title: currentLine, description: `Issues that we want to resolve in ${major}.${minor} line.` }
56
+ ];
57
+ if (patch === 0) {
58
+ const nextMinor = `${major}.${minor + 1}.0`;
59
+ const nextLine = `${major}.${minor + 1}.x`;
60
+ ensure.push(
61
+ { title: nextMinor, description: `Issues that we want to release in v${nextMinor}.` },
62
+ { title: nextLine, description: `Issues that we want to resolve in ${major}.${minor + 1} line.` }
63
+ );
64
+ }
53
65
  const operations: Array<{ name: string; title: string; status: 'applied' | 'no-op' | 'failed' }> = [];
54
66
  const current = milestones.find((item) => item.title === version);
55
67
  if (current && current.state !== 'closed') {
56
68
  const closed = client.json(['api', '--method', 'PATCH', `repos/${context.platform.repository}/milestones/${current.number}`, '-f', 'state=closed'], { cwd: options.cwd });
57
69
  operations.push({ name: 'close-milestone', title: version, status: closed.ok ? 'applied' : 'failed' });
58
70
  } else operations.push({ name: 'close-milestone', title: version, status: 'no-op' });
59
- for (const title of ensure) {
71
+ for (const { title, description } of ensure) {
60
72
  if (milestones.some((item) => item.title === title)) operations.push({ name: 'ensure-milestone', title, status: 'no-op' });
61
73
  else {
62
- const created = client.json(['api', '--method', 'POST', `repos/${context.platform.repository}/milestones`, '-f', `title=${title}`], { cwd: options.cwd });
74
+ const created = client.json(['api', '--method', 'POST', `repos/${context.platform.repository}/milestones`, '-f', `title=${title}`, '-f', `description=${description}`], { cwd: options.cwd });
63
75
  operations.push({ name: 'ensure-milestone', title, status: created.ok ? 'applied' : 'failed' });
64
76
  }
65
77
  }
@@ -0,0 +1,224 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+
5
+ import { detectRepoRoot } from '../task/resolve-ref.ts';
6
+ import { normalizeObjects } from './normalize.ts';
7
+ import { reconcileRecords } from './reconcile.ts';
8
+ import { readAppliedOverlays, repairSnapshot } from './repair.ts';
9
+ import { collectGitHubObjects, collectLocalObjects } from './sources.ts';
10
+ import { canonicalJsonBytes, createObjectStore, publishSnapshot, verifySnapshot } from './store.ts';
11
+
12
+ import type { CapturedObject, NormalizedRecord, RestCollectionEvidence } from './types.ts';
13
+
14
+ const USAGE = `Usage: ai data <command> [options]
15
+
16
+ Commands:
17
+ capture [--source all|local|github] [--root <dir>] [--include-excerpts]
18
+ verify <snapshot-id> [--root <dir>]
19
+ audit <snapshot-id> [--root <dir>] [--format json|text]
20
+ repair <snapshot-id> [--root <dir>] [--apply]
21
+ export <snapshot-id> [--root <dir>] [--repairs none|applied] [--output <file|->]
22
+ `;
23
+
24
+ type ParsedArgs = { positionals: string[]; options: Map<string, string | true> };
25
+
26
+ function parseArgs(args: string[], valueOptions: Set<string>, booleanOptions: Set<string>): ParsedArgs {
27
+ const positionals: string[] = [];
28
+ const options = new Map<string, string | true>();
29
+ for (let index = 0; index < args.length; index += 1) {
30
+ const arg = args[index]!;
31
+ if (!arg.startsWith('--')) {
32
+ positionals.push(arg);
33
+ continue;
34
+ }
35
+ if (booleanOptions.has(arg)) {
36
+ if (options.has(arg)) throw new Error(`duplicate option: ${arg}`);
37
+ options.set(arg, true);
38
+ continue;
39
+ }
40
+ if (!valueOptions.has(arg)) throw new Error(`unknown option: ${arg}`);
41
+ const value = args[index + 1];
42
+ if (!value || value.startsWith('--')) throw new Error(`missing value for ${arg}`);
43
+ if (options.has(arg)) throw new Error(`duplicate option: ${arg}`);
44
+ options.set(arg, value);
45
+ index += 1;
46
+ }
47
+ return { positionals, options };
48
+ }
49
+
50
+ function dataRoot(repoRoot: string, requested?: string): string {
51
+ const workspace = path.resolve(repoRoot, '.agents', 'workspace');
52
+ const resolved = path.resolve(repoRoot, requested ?? path.join('.agents', 'workspace', 'process-data'));
53
+ const relative = path.relative(repoRoot, resolved);
54
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('data root must be inside the repository');
55
+ const taskRoots = ['active', 'blocked', 'completed', 'archive'].map((state) => path.join(workspace, state));
56
+ if (resolved === workspace || taskRoots.some((taskRoot) => resolved === taskRoot || resolved.startsWith(`${taskRoot}${path.sep}`))) {
57
+ throw new Error('data root cannot be the workspace root or a task directory');
58
+ }
59
+ let current = resolved;
60
+ while (current !== repoRoot) {
61
+ if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) throw new Error('data root cannot traverse a symlink');
62
+ current = path.dirname(current);
63
+ }
64
+ return resolved;
65
+ }
66
+
67
+ function repositoryIdentity(repoRoot: string): string {
68
+ try {
69
+ return execFileSync('git', ['config', '--get', 'remote.origin.url'], { cwd: repoRoot, encoding: 'utf8' }).trim() || path.basename(repoRoot);
70
+ } catch {
71
+ return path.basename(repoRoot);
72
+ }
73
+ }
74
+
75
+ function writeJson(value: unknown): void {
76
+ process.stdout.write(`${JSON.stringify(value)}\n`);
77
+ }
78
+
79
+ function optionString(parsed: ParsedArgs, name: string): string | undefined {
80
+ const value = parsed.options.get(name);
81
+ return typeof value === 'string' ? value : undefined;
82
+ }
83
+
84
+ function capture(args: string[]): number {
85
+ const parsed = parseArgs(args, new Set(['--source', '--root']), new Set(['--include-excerpts']));
86
+ if (parsed.positionals.length > 0) throw new Error('capture does not accept positional arguments');
87
+ const scope = optionString(parsed, '--source') ?? 'all';
88
+ if (!['all', 'local', 'github'].includes(scope)) throw new Error('--source must be all, local, or github');
89
+ const repoRoot = detectRepoRoot();
90
+ const root = dataRoot(repoRoot, optionString(parsed, '--root'));
91
+ const observedFrom = new Date().toISOString();
92
+ const objects: CapturedObject[] = [];
93
+ const endpoints: RestCollectionEvidence[] = [];
94
+ let repository = repositoryIdentity(repoRoot);
95
+
96
+ if (scope === 'all' || scope === 'local') {
97
+ const local = collectLocalObjects(repoRoot, { includeExcerpts: parsed.options.has('--include-excerpts') });
98
+ if (!local.ok) { writeJson({ ok: false, error: local.error }); return 2; }
99
+ objects.push(...local.value);
100
+ }
101
+ if (scope === 'all' || scope === 'github') {
102
+ const github = collectGitHubObjects(repoRoot);
103
+ if (!github.ok) { writeJson({ ok: false, error: github.error }); return 2; }
104
+ objects.push(...github.value.objects);
105
+ endpoints.push(...github.value.endpoints);
106
+ repository = github.value.repository;
107
+ }
108
+
109
+ const store = createObjectStore(root);
110
+ for (const object of objects) {
111
+ if (object.content !== undefined && object.disposition?.state === 'included') {
112
+ const stored = store.put(Buffer.from(object.content, 'utf8'));
113
+ if (stored.sha256 !== object.sha256) throw new Error(`source hash mismatch: ${object.sourceIdentity}`);
114
+ }
115
+ }
116
+ const records = normalizeObjects(objects);
117
+ const reconciliation = reconcileRecords(records, scope as 'all' | 'local' | 'github');
118
+ const observedTo = new Date().toISOString();
119
+ const published = publishSnapshot(root, {
120
+ scope: scope as 'all' | 'local' | 'github',
121
+ repository,
122
+ observedFrom,
123
+ observedTo,
124
+ objects: objects.map(({ content: _content, ...object }) => object),
125
+ endpoints,
126
+ excerptsEnabled: parsed.options.has('--include-excerpts'),
127
+ records,
128
+ quality: reconciliation.findings,
129
+ repairs: reconciliation.repairs
130
+ });
131
+ writeJson({ ok: true, snapshotId: published.snapshotId, scope, created: published.created, objects: objects.length, records: records.length });
132
+ return 0;
133
+ }
134
+
135
+ function verify(args: string[]): number {
136
+ const parsed = parseArgs(args, new Set(['--root']), new Set());
137
+ if (parsed.positionals.length !== 1) throw new Error('verify requires one snapshot id');
138
+ const repoRoot = detectRepoRoot();
139
+ const result = verifySnapshot(dataRoot(repoRoot, optionString(parsed, '--root')), parsed.positionals[0]!);
140
+ writeJson(result);
141
+ return result.ok ? 0 : 1;
142
+ }
143
+
144
+ function audit(args: string[]): number {
145
+ const parsed = parseArgs(args, new Set(['--root', '--format']), new Set());
146
+ if (parsed.positionals.length !== 1) throw new Error('audit requires one snapshot id');
147
+ const format = optionString(parsed, '--format') ?? 'text';
148
+ if (!['json', 'text'].includes(format)) throw new Error('--format must be json or text');
149
+ const repoRoot = detectRepoRoot();
150
+ const root = dataRoot(repoRoot, optionString(parsed, '--root'));
151
+ const verified = verifySnapshot(root, parsed.positionals[0]!);
152
+ if (!verified.ok || !verified.snapshotPath || !verified.manifest) { writeJson(verified); return 1; }
153
+ const quality = JSON.parse(fs.readFileSync(path.join(verified.snapshotPath, 'quality.json'), 'utf8')) as { findings: unknown[] };
154
+ const records = fs.readFileSync(path.join(verified.snapshotPath, 'normalized.jsonl'), 'utf8').split('\n').filter(Boolean).map((line) => {
155
+ const { schema: _schema, ...record } = JSON.parse(line) as { schema: string } & NormalizedRecord;
156
+ return record;
157
+ });
158
+ const recomputed = reconcileRecords(records, verified.manifest.scope);
159
+ if (!canonicalJsonBytes(recomputed.findings).equals(canonicalJsonBytes(quality.findings))) {
160
+ writeJson({ ok: false, error: { code: 'QUALITY_MISMATCH', message: 'stored quality baseline does not match recomputed findings' } });
161
+ return 1;
162
+ }
163
+ if (format === 'json') writeJson({ ok: true, snapshotId: parsed.positionals[0], findings: recomputed.findings });
164
+ else process.stdout.write(`Snapshot ${parsed.positionals[0]}: ${quality.findings.length} findings\n`);
165
+ return 0;
166
+ }
167
+
168
+ function repair(args: string[]): number {
169
+ const parsed = parseArgs(args, new Set(['--root']), new Set(['--apply']));
170
+ if (parsed.positionals.length !== 1) throw new Error('repair requires one snapshot id');
171
+ const repoRoot = detectRepoRoot();
172
+ const result = repairSnapshot(dataRoot(repoRoot, optionString(parsed, '--root')), parsed.positionals[0]!, parsed.options.has('--apply'));
173
+ writeJson({ ok: true, ...result });
174
+ return 0;
175
+ }
176
+
177
+ function exportSnapshot(args: string[]): number {
178
+ const parsed = parseArgs(args, new Set(['--root', '--repairs', '--output']), new Set());
179
+ if (parsed.positionals.length !== 1) throw new Error('export requires one snapshot id');
180
+ const repairMode = optionString(parsed, '--repairs') ?? 'none';
181
+ if (!['none', 'applied'].includes(repairMode)) throw new Error('--repairs must be none or applied');
182
+ const output = optionString(parsed, '--output') ?? '-';
183
+ const repoRoot = detectRepoRoot();
184
+ const root = dataRoot(repoRoot, optionString(parsed, '--root'));
185
+ const verified = verifySnapshot(root, parsed.positionals[0]!);
186
+ if (!verified.ok || !verified.snapshotPath) { writeJson(verified); return 1; }
187
+ let content = fs.readFileSync(path.join(verified.snapshotPath, 'normalized.jsonl'), 'utf8');
188
+ if (repairMode === 'applied') content += readAppliedOverlays(root, parsed.positionals[0]!);
189
+ if (output === '-') process.stdout.write(content);
190
+ else {
191
+ const destination = path.resolve(repoRoot, output);
192
+ const relative = path.relative(repoRoot, destination);
193
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('export output must be inside the repository');
194
+ fs.writeFileSync(destination, content, { flag: 'wx', mode: 0o600 });
195
+ writeJson({ ok: true, snapshotId: parsed.positionals[0], output: relative });
196
+ }
197
+ return 0;
198
+ }
199
+
200
+ async function cmdData(args: string[]): Promise<number> {
201
+ const [command, ...rest] = args;
202
+ if (!command || command === '--help' || command === '-h' || command === 'help') {
203
+ process.stdout.write(USAGE);
204
+ return command ? 0 : 1;
205
+ }
206
+ try {
207
+ switch (command) {
208
+ case 'capture': return capture(rest);
209
+ case 'verify': return verify(rest);
210
+ case 'audit': return audit(rest);
211
+ case 'repair': return repair(rest);
212
+ case 'export': return exportSnapshot(rest);
213
+ default:
214
+ process.stderr.write(`Unknown data command: ${command}\n`);
215
+ process.stdout.write(USAGE);
216
+ return 1;
217
+ }
218
+ } catch (error) {
219
+ writeJson({ ok: false, error: { code: 'DATA_COMMAND_FAILED', message: error instanceof Error ? error.message : String(error) } });
220
+ return 1;
221
+ }
222
+ }
223
+
224
+ export { cmdData, dataRoot };