@ai-sdlc/orchestrator 0.5.0 → 0.9.0

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 (227) hide show
  1. package/dist/action-enforcement.d.ts +26 -0
  2. package/dist/action-enforcement.js +70 -0
  3. package/dist/admission-composite.d.ts +67 -0
  4. package/dist/admission-composite.js +158 -0
  5. package/dist/admission-enrichment.d.ts +142 -0
  6. package/dist/admission-enrichment.js +331 -0
  7. package/dist/admission-hc.d.ts +62 -0
  8. package/dist/admission-hc.js +83 -0
  9. package/dist/admission-score.d.ts +148 -0
  10. package/dist/admission-score.js +237 -0
  11. package/dist/analysis/analyzer.js +3 -2
  12. package/dist/analysis/convention-detector.d.ts +85 -2
  13. package/dist/analysis/convention-detector.js +375 -70
  14. package/dist/analysis/diff-analyzer.d.ts +33 -0
  15. package/dist/analysis/diff-analyzer.js +122 -0
  16. package/dist/analysis/hotspot-analyzer.js +3 -1
  17. package/dist/analysis/index.d.ts +2 -1
  18. package/dist/analysis/index.js +2 -1
  19. package/dist/artifacts/index.d.ts +65 -0
  20. package/dist/artifacts/index.js +142 -0
  21. package/dist/backlog-adapter.d.ts +121 -0
  22. package/dist/backlog-adapter.js +438 -0
  23. package/dist/calibration.d.ts +43 -0
  24. package/dist/calibration.js +76 -0
  25. package/dist/cli/commands/agents.d.ts +24 -0
  26. package/dist/cli/commands/agents.js +66 -1
  27. package/dist/cli/commands/git-remote.d.ts +49 -0
  28. package/dist/cli/commands/git-remote.js +91 -0
  29. package/dist/cli/commands/health.d.ts +4 -0
  30. package/dist/cli/commands/health.js +13 -1
  31. package/dist/cli/commands/init.d.ts +26 -0
  32. package/dist/cli/commands/init.js +164 -22
  33. package/dist/cli/commands/mcp-setup.d.ts +31 -0
  34. package/dist/cli/commands/mcp-setup.js +78 -8
  35. package/dist/cli/formatters/table.js +15 -2
  36. package/dist/cli/index.d.ts +14 -1
  37. package/dist/cli/index.js +81 -20
  38. package/dist/cli/versions.d.ts +57 -0
  39. package/dist/cli/versions.js +128 -0
  40. package/dist/code-area-classifier.d.ts +21 -0
  41. package/dist/code-area-classifier.js +48 -0
  42. package/dist/config.d.ts +33 -1
  43. package/dist/config.js +78 -8
  44. package/dist/cycle-utils.d.ts +51 -0
  45. package/dist/cycle-utils.js +77 -0
  46. package/dist/database/adapters/external.d.ts +24 -0
  47. package/dist/database/adapters/external.js +80 -0
  48. package/dist/database/adapters/neon.d.ts +41 -0
  49. package/dist/database/adapters/neon.js +98 -0
  50. package/dist/database/adapters/pg-snapshot-restore.d.ts +28 -0
  51. package/dist/database/adapters/pg-snapshot-restore.js +68 -0
  52. package/dist/database/adapters/sqlite-copy.d.ts +32 -0
  53. package/dist/database/adapters/sqlite-copy.js +145 -0
  54. package/dist/database/connection-injection.d.ts +35 -0
  55. package/dist/database/connection-injection.js +93 -0
  56. package/dist/database/index.d.ts +12 -0
  57. package/dist/database/index.js +23 -0
  58. package/dist/database/registry.d.ts +13 -0
  59. package/dist/database/registry.js +27 -0
  60. package/dist/database/topology.d.ts +52 -0
  61. package/dist/database/topology.js +44 -0
  62. package/dist/database/types.d.ts +89 -0
  63. package/dist/database/types.js +26 -0
  64. package/dist/defaults.d.ts +5 -0
  65. package/dist/defaults.js +5 -0
  66. package/dist/design-authority.d.ts +40 -0
  67. package/dist/design-authority.js +71 -0
  68. package/dist/design-lookahead.d.ts +64 -0
  69. package/dist/design-lookahead.js +86 -0
  70. package/dist/design-quality-trend.d.ts +87 -0
  71. package/dist/design-quality-trend.js +190 -0
  72. package/dist/design-system-context.d.ts +46 -0
  73. package/dist/design-system-context.js +80 -0
  74. package/dist/design-system-correction-loop.d.ts +64 -0
  75. package/dist/design-system-correction-loop.js +128 -0
  76. package/dist/design-system-metrics.d.ts +61 -0
  77. package/dist/design-system-metrics.js +104 -0
  78. package/dist/design-system-stewardship.d.ts +22 -0
  79. package/dist/design-system-stewardship.js +85 -0
  80. package/dist/design-system-validation.d.ts +37 -0
  81. package/dist/design-system-validation.js +88 -0
  82. package/dist/dispatch/index.d.ts +4 -0
  83. package/dist/dispatch/index.js +4 -0
  84. package/dist/dispatch/merge-gate.d.ts +46 -0
  85. package/dist/dispatch/merge-gate.js +90 -0
  86. package/dist/dispatch/requeue.d.ts +57 -0
  87. package/dist/dispatch/requeue.js +131 -0
  88. package/dist/dispatch/worker-pool.d.ts +62 -0
  89. package/dist/dispatch/worker-pool.js +60 -0
  90. package/dist/execute.d.ts +40 -0
  91. package/dist/execute.js +295 -30
  92. package/dist/fix-ci.js +40 -3
  93. package/dist/fix-review.d.ts +66 -0
  94. package/dist/fix-review.js +448 -0
  95. package/dist/harness/adapters/claude-code.d.ts +29 -0
  96. package/dist/harness/adapters/claude-code.js +191 -0
  97. package/dist/harness/adapters/codex.d.ts +25 -0
  98. package/dist/harness/adapters/codex.js +61 -0
  99. package/dist/harness/independence.d.ts +51 -0
  100. package/dist/harness/independence.js +67 -0
  101. package/dist/harness/index.d.ts +14 -0
  102. package/dist/harness/index.js +20 -0
  103. package/dist/harness/registry.d.ts +17 -0
  104. package/dist/harness/registry.js +31 -0
  105. package/dist/harness/types.d.ts +123 -0
  106. package/dist/harness/types.js +8 -0
  107. package/dist/harness/version-probe.d.ts +14 -0
  108. package/dist/harness/version-probe.js +123 -0
  109. package/dist/index.d.ts +39 -5
  110. package/dist/index.js +48 -3
  111. package/dist/models/classifier.d.ts +76 -0
  112. package/dist/models/classifier.js +221 -0
  113. package/dist/models/index.d.ts +3 -0
  114. package/dist/models/index.js +3 -0
  115. package/dist/models/registry.d.ts +97 -0
  116. package/dist/models/registry.js +173 -0
  117. package/dist/pillar-breakdown.d.ts +63 -0
  118. package/dist/pillar-breakdown.js +153 -0
  119. package/dist/pipeline-cycle-detector.d.ts +70 -0
  120. package/dist/pipeline-cycle-detector.js +111 -0
  121. package/dist/priority.d.ts +7 -76
  122. package/dist/priority.js +18 -5
  123. package/dist/review-meta.d.ts +65 -0
  124. package/dist/review-meta.js +149 -0
  125. package/dist/review.d.ts +35 -0
  126. package/dist/review.js +84 -0
  127. package/dist/runners/claude-code-sdk.d.ts +31 -0
  128. package/dist/runners/claude-code-sdk.js +238 -0
  129. package/dist/runners/claude-code.d.ts +3 -0
  130. package/dist/runners/claude-code.js +319 -73
  131. package/dist/runners/codex.js +4 -1
  132. package/dist/runners/copilot.js +4 -1
  133. package/dist/runners/cursor.js +4 -1
  134. package/dist/runners/git-utils.d.ts +81 -0
  135. package/dist/runners/git-utils.js +201 -0
  136. package/dist/runners/index.d.ts +5 -1
  137. package/dist/runners/index.js +4 -0
  138. package/dist/runners/review-agent.d.ts +67 -0
  139. package/dist/runners/review-agent.js +350 -0
  140. package/dist/runners/runner-registry.js +10 -0
  141. package/dist/runners/sdk-review-runner.d.ts +65 -0
  142. package/dist/runners/sdk-review-runner.js +185 -0
  143. package/dist/runners/security-triage.d.ts +20 -4
  144. package/dist/runners/security-triage.js +42 -14
  145. package/dist/runners/types.d.ts +25 -0
  146. package/dist/runtime/attestations.d.ts +274 -0
  147. package/dist/runtime/attestations.js +460 -0
  148. package/dist/runtime/git-env.d.ts +53 -0
  149. package/dist/runtime/git-env.js +60 -0
  150. package/dist/runtime/index.d.ts +7 -0
  151. package/dist/runtime/index.js +7 -0
  152. package/dist/runtime/parallelism-flag.d.ts +10 -0
  153. package/dist/runtime/parallelism-flag.js +18 -0
  154. package/dist/runtime/port-allocator.d.ts +21 -0
  155. package/dist/runtime/port-allocator.js +66 -0
  156. package/dist/runtime/worktree-pool.d.ts +86 -0
  157. package/dist/runtime/worktree-pool.js +204 -0
  158. package/dist/runtime/worktree.d.ts +25 -0
  159. package/dist/runtime/worktree.js +111 -0
  160. package/dist/sa-scoring/auto-calibrate.d.ts +69 -0
  161. package/dist/sa-scoring/auto-calibrate.js +107 -0
  162. package/dist/sa-scoring/c1-sa2-computable.d.ts +42 -0
  163. package/dist/sa-scoring/c1-sa2-computable.js +59 -0
  164. package/dist/sa-scoring/composite.d.ts +107 -0
  165. package/dist/sa-scoring/composite.js +139 -0
  166. package/dist/sa-scoring/depparse-client.d.ts +79 -0
  167. package/dist/sa-scoring/depparse-client.js +187 -0
  168. package/dist/sa-scoring/did-compiler.d.ts +122 -0
  169. package/dist/sa-scoring/did-compiler.js +286 -0
  170. package/dist/sa-scoring/drift-monitor.d.ts +84 -0
  171. package/dist/sa-scoring/drift-monitor.js +186 -0
  172. package/dist/sa-scoring/exemplar-bank.d.ts +78 -0
  173. package/dist/sa-scoring/exemplar-bank.js +154 -0
  174. package/dist/sa-scoring/feedback-store.d.ts +100 -0
  175. package/dist/sa-scoring/feedback-store.js +156 -0
  176. package/dist/sa-scoring/index.d.ts +71 -0
  177. package/dist/sa-scoring/index.js +158 -0
  178. package/dist/sa-scoring/layer1-deterministic.d.ts +115 -0
  179. package/dist/sa-scoring/layer1-deterministic.js +298 -0
  180. package/dist/sa-scoring/layer2-structural.d.ts +71 -0
  181. package/dist/sa-scoring/layer2-structural.js +151 -0
  182. package/dist/sa-scoring/layer3-llm.d.ts +86 -0
  183. package/dist/sa-scoring/layer3-llm.js +282 -0
  184. package/dist/sa-scoring/rescore-orchestrator.d.ts +52 -0
  185. package/dist/sa-scoring/rescore-orchestrator.js +47 -0
  186. package/dist/scheduling/burn-down.d.ts +27 -0
  187. package/dist/scheduling/burn-down.js +43 -0
  188. package/dist/scheduling/calibration.d.ts +42 -0
  189. package/dist/scheduling/calibration.js +150 -0
  190. package/dist/scheduling/index.d.ts +8 -0
  191. package/dist/scheduling/index.js +8 -0
  192. package/dist/scheduling/ledger.d.ts +59 -0
  193. package/dist/scheduling/ledger.js +216 -0
  194. package/dist/scheduling/off-peak.d.ts +27 -0
  195. package/dist/scheduling/off-peak.js +112 -0
  196. package/dist/scheduling/schedule-decision.d.ts +39 -0
  197. package/dist/scheduling/schedule-decision.js +89 -0
  198. package/dist/scheduling/tier-analysis.d.ts +47 -0
  199. package/dist/scheduling/tier-analysis.js +81 -0
  200. package/dist/scheduling/types.d.ts +140 -0
  201. package/dist/scheduling/types.js +11 -0
  202. package/dist/shared.d.ts +13 -0
  203. package/dist/shared.js +32 -0
  204. package/dist/state/index.d.ts +1 -1
  205. package/dist/state/schema.d.ts +6 -1
  206. package/dist/state/schema.js +276 -1
  207. package/dist/state/store.d.ts +71 -1
  208. package/dist/state/store.js +464 -7
  209. package/dist/state/types.d.ts +174 -0
  210. package/dist/types.d.ts +1 -1
  211. package/dist/validate-agent-output.js +4 -1
  212. package/dist/watch.js +6 -0
  213. package/dist/workflow-patterns/artifact-writer.d.ts +16 -0
  214. package/dist/workflow-patterns/artifact-writer.js +34 -0
  215. package/dist/workflow-patterns/classifiers.d.ts +10 -0
  216. package/dist/workflow-patterns/classifiers.js +72 -0
  217. package/dist/workflow-patterns/detector.d.ts +27 -0
  218. package/dist/workflow-patterns/detector.js +186 -0
  219. package/dist/workflow-patterns/index.d.ts +8 -0
  220. package/dist/workflow-patterns/index.js +7 -0
  221. package/dist/workflow-patterns/proposal-generator.d.ts +15 -0
  222. package/dist/workflow-patterns/proposal-generator.js +183 -0
  223. package/dist/workflow-patterns/telemetry-ingest.d.ts +27 -0
  224. package/dist/workflow-patterns/telemetry-ingest.js +103 -0
  225. package/dist/workflow-patterns/types.d.ts +61 -0
  226. package/dist/workflow-patterns/types.js +11 -0
  227. package/package.json +2 -2
@@ -2,14 +2,35 @@
2
2
  * Claude Code runner — invokes Claude Code CLI in --print mode
3
3
  * and collects the result via git diff.
4
4
  */
5
- import { spawn, execFile } from 'node:child_process';
6
- import { promisify } from 'node:util';
5
+ import { spawn } from 'node:child_process';
7
6
  import { DEFAULT_MODEL, DEFAULT_ALLOWED_TOOLS, DEFAULT_RUNNER_TIMEOUT_MS, DEFAULT_LINT_COMMAND, DEFAULT_FORMAT_COMMAND, DEFAULT_COMMIT_MESSAGE_TEMPLATE, DEFAULT_COMMIT_CO_AUTHOR, } from '../defaults.js';
8
7
  import { formatContextForPrompt } from '../analysis/context-builder.js';
9
- const execFileAsync = promisify(execFile);
8
+ import { gitExec, detectChangedFiles, runAutoFix, snapshotWorktree, detectCrossRepoWrites, } from './git-utils.js';
9
+ /**
10
+ * Build verification step instructions (lint, format, typecheck).
11
+ * Returns an array of numbered step strings starting from the given step number.
12
+ */
13
+ function buildVerificationSteps(startStep, lintCmd, fmtCmd, typecheckCmd) {
14
+ let step = startStep;
15
+ const lines = [];
16
+ if (lintCmd && fmtCmd) {
17
+ lines.push(`${++step}. After making code changes, run \`${lintCmd}\` and \`${fmtCmd}\` to catch issues before committing.`);
18
+ }
19
+ else if (lintCmd) {
20
+ lines.push(`${++step}. After making code changes, run \`${lintCmd}\` to catch issues before committing.`);
21
+ }
22
+ else if (fmtCmd) {
23
+ lines.push(`${++step}. After making code changes, run \`${fmtCmd}\` to catch issues before committing.`);
24
+ }
25
+ if (typecheckCmd) {
26
+ lines.push(`${++step}. IMPORTANT: Run \`${typecheckCmd}\` to verify there are no TypeScript errors. The pre-commit hook will reject your commit if there are type errors. Fix ALL type errors before committing.`);
27
+ }
28
+ return { lines, nextStep: step };
29
+ }
10
30
  export function buildPrompt(ctx) {
11
31
  const lintCmd = ctx.lintCommand ?? DEFAULT_LINT_COMMAND;
12
32
  const fmtCmd = ctx.formatCommand ?? DEFAULT_FORMAT_COMMAND;
33
+ const typecheckCmd = ctx.typecheckCommand;
13
34
  const lines = [
14
35
  `You are fixing issue ${/^\d+$/.test(ctx.issueId) ? '#' : ''}${ctx.issueId}: ${ctx.issueTitle}`,
15
36
  '',
@@ -23,29 +44,25 @@ export function buildPrompt(ctx) {
23
44
  if (fmtCmd) {
24
45
  lines.push(`${++step}. If the failure is a formatting/prettier error, run \`${fmtCmd}\` to auto-fix it.`);
25
46
  }
26
- if (lintCmd && fmtCmd) {
27
- lines.push(`${++step}. After making ANY code changes, always run \`${lintCmd}\` and \`${fmtCmd}\` to catch issues before committing.`);
28
- }
29
- else if (lintCmd) {
30
- lines.push(`${++step}. After making ANY code changes, always run \`${lintCmd}\` to catch issues before committing.`);
31
- }
32
- else if (fmtCmd) {
33
- lines.push(`${++step}. After making ANY code changes, always run \`${fmtCmd}\` to catch issues before committing.`);
34
- }
47
+ const ciVerify = buildVerificationSteps(step, lintCmd, fmtCmd, typecheckCmd);
48
+ lines.push(...ciVerify.lines);
49
+ step = ciVerify.nextStep;
35
50
  lines.push(`${++step}. Write or update tests if needed to cover your fix.`, `${++step}. NEVER modify files matching the blocked paths below — violations will be automatically detected and the change will be rejected.`, `${++step}. Keep your changes to at most ${ctx.constraints.maxFilesPerChange} files.`);
36
51
  }
52
+ else if (ctx.reviewFindings) {
53
+ let step = 0;
54
+ lines.push('## Review Findings', '', ctx.reviewFindings, '', '## Instructions', `${++step}. Read the review findings above carefully.`, `${++step}. Read the relevant source files to understand the context.`, `${++step}. Address all the review findings by making necessary code changes.`, `${++step}. Write or update tests if requested by the reviewers.`);
55
+ const reviewVerify = buildVerificationSteps(step, lintCmd, fmtCmd, typecheckCmd);
56
+ lines.push(...reviewVerify.lines);
57
+ step = reviewVerify.nextStep;
58
+ lines.push(`${++step}. NEVER modify files matching the blocked paths below — violations will be automatically detected and the change will be rejected.`, `${++step}. Keep your changes to at most ${ctx.constraints.maxFilesPerChange} files.`);
59
+ }
37
60
  else {
38
61
  let step = 0;
39
62
  lines.push('## Instructions', `${++step}. Read the relevant source files to understand the codebase.`, `${++step}. Implement the fix or feature described in the issue.`, `${++step}. Write or update tests to cover your changes.`);
40
- if (lintCmd && fmtCmd) {
41
- lines.push(`${++step}. After making code changes, run \`${lintCmd}\` and \`${fmtCmd}\` to ensure CI will pass.`);
42
- }
43
- else if (lintCmd) {
44
- lines.push(`${++step}. After making code changes, run \`${lintCmd}\` to ensure CI will pass.`);
45
- }
46
- else if (fmtCmd) {
47
- lines.push(`${++step}. After making code changes, run \`${fmtCmd}\` to ensure CI will pass.`);
48
- }
63
+ const defaultVerify = buildVerificationSteps(step, lintCmd, fmtCmd, typecheckCmd);
64
+ lines.push(...defaultVerify.lines);
65
+ step = defaultVerify.nextStep;
49
66
  lines.push(`${++step}. NEVER modify files matching the blocked paths below — violations will be automatically detected and the change will be rejected.`, `${++step}. Keep your changes to at most ${ctx.constraints.maxFilesPerChange} files.`);
50
67
  }
51
68
  lines.push('', '## Constraints (enforced — violations will be automatically rejected)', `- Maximum files to change: ${ctx.constraints.maxFilesPerChange}`, `- Tests required: ${ctx.constraints.requireTests}`, `- Blocked paths (NEVER modify — changes will be rejected): ${ctx.constraints.blockedPaths.join(', ') || 'none'}`);
@@ -72,20 +89,161 @@ export function buildPrompt(ctx) {
72
89
  }
73
90
  return lines.join('\n');
74
91
  }
75
- async function gitExec(workDir, args) {
76
- const { stdout } = await execFileAsync('git', args, { cwd: workDir });
77
- return stdout.trim();
92
+ /**
93
+ * Format a stream-json event into a concise, human-readable log line.
94
+ */
95
+ function formatEventForLog(line) {
96
+ let parsed;
97
+ try {
98
+ parsed = JSON.parse(line);
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ const type = parsed.type;
104
+ if (type === 'system') {
105
+ return `init session=${parsed.session_id?.slice(0, 8)}`;
106
+ }
107
+ if (type === 'assistant') {
108
+ const message = parsed.message;
109
+ const content = message?.content;
110
+ if (!content?.length)
111
+ return null;
112
+ const parts = [];
113
+ for (const block of content) {
114
+ if (block.type === 'thinking') {
115
+ const text = String(block.thinking ?? '');
116
+ parts.push(`thinking: ${text.slice(0, 120)}${text.length > 120 ? '...' : ''}`);
117
+ }
118
+ else if (block.type === 'text') {
119
+ const text = String(block.text ?? '');
120
+ parts.push(`text: ${text.slice(0, 120)}${text.length > 120 ? '...' : ''}`);
121
+ }
122
+ else if (block.type === 'tool_use') {
123
+ const name = String(block.name ?? '');
124
+ const input = (block.input ?? {});
125
+ const file = extractFilePath(input);
126
+ parts.push(file ? `${name}: ${file}` : name);
127
+ }
128
+ }
129
+ return parts.join(' | ');
130
+ }
131
+ if (type === 'user') {
132
+ const message = parsed.message;
133
+ const content = message?.content;
134
+ if (!content?.length)
135
+ return null;
136
+ const block = content[0];
137
+ if (block.type === 'tool_result') {
138
+ const text = String(block.content ?? '');
139
+ const truncated = text.slice(0, 80);
140
+ return `result: ${truncated}${text.length > 80 ? '...' : ''}`;
141
+ }
142
+ return null;
143
+ }
144
+ if (type === 'result') {
145
+ const cost = parsed.total_cost_usd;
146
+ const turns = parsed.num_turns;
147
+ const duration = parsed.duration_ms;
148
+ return `done — ${turns ?? '?'} turns, ${duration ? Math.round(duration / 1000) + 's' : '?'}, $${cost?.toFixed(4) ?? '?'}`;
149
+ }
150
+ return null;
151
+ }
152
+ /**
153
+ * Extract a file path from a tool_use input object.
154
+ */
155
+ function extractFilePath(input) {
156
+ return ((typeof input.file_path === 'string' ? input.file_path : undefined) ??
157
+ (typeof input.path === 'string' ? input.path : undefined) ??
158
+ (typeof input.pattern === 'string' ? input.pattern : undefined) ??
159
+ (typeof input.command === 'string' ? input.command.slice(0, 80) : undefined));
160
+ }
161
+ /**
162
+ * Parse a single NDJSON line from Claude Code stream-json output
163
+ * and emit a progress event if applicable.
164
+ */
165
+ function parseStreamEvent(line, onProgress) {
166
+ let parsed;
167
+ try {
168
+ parsed = JSON.parse(line);
169
+ }
170
+ catch {
171
+ return undefined;
172
+ }
173
+ const type = parsed.type;
174
+ if (type === 'assistant') {
175
+ const message = parsed.message;
176
+ const content = message?.content;
177
+ if (!content)
178
+ return undefined;
179
+ for (const block of content) {
180
+ if (block.type === 'tool_use') {
181
+ const toolName = String(block.name ?? '');
182
+ const input = (block.input ?? {});
183
+ const filePath = extractFilePath(input);
184
+ onProgress({
185
+ type: 'tool_start',
186
+ tool: toolName,
187
+ file: filePath,
188
+ message: filePath ? `${toolName}: ${filePath}` : toolName,
189
+ });
190
+ }
191
+ else if (block.type === 'text') {
192
+ const text = String(block.text ?? '');
193
+ if (text.length > 0) {
194
+ onProgress({
195
+ type: 'text',
196
+ message: text.slice(0, 200),
197
+ });
198
+ }
199
+ }
200
+ }
201
+ }
202
+ if (type === 'result') {
203
+ const resultText = parsed.result;
204
+ const costUsd = parsed.total_cost_usd;
205
+ // Extract token usage from result event
206
+ const modelUsage = parsed.modelUsage;
207
+ let tokenUsage;
208
+ if (modelUsage) {
209
+ const firstModel = Object.keys(modelUsage)[0];
210
+ if (firstModel) {
211
+ const usage = modelUsage[firstModel];
212
+ tokenUsage = {
213
+ inputTokens: usage.inputTokens ?? 0,
214
+ outputTokens: usage.outputTokens ?? 0,
215
+ cacheReadTokens: usage.cacheReadInputTokens ?? undefined,
216
+ model: firstModel,
217
+ };
218
+ }
219
+ }
220
+ if (costUsd !== undefined) {
221
+ onProgress({ type: 'cost', costUsd, message: `Total cost: $${costUsd.toFixed(4)}` });
222
+ }
223
+ return { resultText: resultText ?? undefined, costUsd, tokenUsage };
224
+ }
225
+ return undefined;
78
226
  }
79
227
  function runClaude(prompt, workDir, opts) {
80
228
  const tools = opts?.allowedTools?.join(',') ?? DEFAULT_ALLOWED_TOOLS;
81
229
  const timeoutMs = opts?.timeoutMs ?? DEFAULT_RUNNER_TIMEOUT_MS;
82
230
  return new Promise((resolve, reject) => {
83
231
  const model = opts?.model ?? process.env.AI_SDLC_MODEL ?? DEFAULT_MODEL;
84
- const claudeArgs = ['-p', '--model', model, '--allowedTools', tools];
85
- // When running inside an OpenShell sandbox, prefix with sandbox connect
232
+ const claudeArgs = [
233
+ '-p',
234
+ '--output-format',
235
+ 'stream-json',
236
+ '--verbose',
237
+ '--model',
238
+ model,
239
+ '--allowedTools',
240
+ tools,
241
+ ];
242
+ // When running inside an OpenShell sandbox, prefix with sandbox connect.
243
+ const useOpenShell = opts?.sandboxId && process.env.AI_SDLC_SANDBOX_PROVIDER === 'openshell';
86
244
  let cmd;
87
245
  let args;
88
- if (opts?.sandboxId) {
246
+ if (useOpenShell) {
89
247
  cmd = 'openshell';
90
248
  args = ['sandbox', 'connect', opts.sandboxId, '--', 'claude', ...claudeArgs];
91
249
  }
@@ -93,27 +251,114 @@ function runClaude(prompt, workDir, opts) {
93
251
  cmd = 'claude';
94
252
  args = claudeArgs;
95
253
  }
254
+ const startTime = Date.now();
255
+ const logPrefix = `[ai-sdlc:runner]`;
256
+ process.stderr.write(`${logPrefix} spawning: ${cmd} ${args.join(' ')}\n`);
257
+ process.stderr.write(`${logPrefix} workDir: ${workDir}\n`);
258
+ process.stderr.write(`${logPrefix} timeout: ${timeoutMs}ms\n`);
96
259
  const child = spawn(cmd, args, {
97
260
  cwd: workDir,
98
261
  stdio: ['pipe', 'pipe', 'pipe'],
99
262
  env: { ...process.env },
100
263
  timeout: timeoutMs,
101
264
  });
102
- const chunks = [];
265
+ process.stderr.write(`${logPrefix} pid: ${child.pid}\n`);
266
+ let lastActivity = Date.now();
267
+ let resultText;
268
+ let resultCost;
269
+ let resultTokenUsage;
270
+ // Buffer for incomplete NDJSON lines
271
+ let lineBuffer = '';
272
+ const onProgress = opts?.onProgress;
273
+ child.stdout.on('data', (data) => {
274
+ lastActivity = Date.now();
275
+ // Parse NDJSON lines from stream-json output
276
+ lineBuffer += data.toString('utf-8');
277
+ const lines = lineBuffer.split('\n');
278
+ // Keep the last (possibly incomplete) line in the buffer
279
+ lineBuffer = lines.pop() ?? '';
280
+ for (const line of lines) {
281
+ const trimmed = line.trim();
282
+ if (!trimmed)
283
+ continue;
284
+ // Log formatted events to stderr for CI visibility
285
+ const formatted = formatEventForLog(trimmed);
286
+ if (formatted) {
287
+ process.stderr.write(`${logPrefix} ${formatted}\n`);
288
+ }
289
+ if (onProgress) {
290
+ const result = parseStreamEvent(trimmed, onProgress);
291
+ if (result) {
292
+ if (result.resultText !== undefined)
293
+ resultText = result.resultText;
294
+ if (result.costUsd !== undefined)
295
+ resultCost = result.costUsd;
296
+ if (result.tokenUsage)
297
+ resultTokenUsage = result.tokenUsage;
298
+ }
299
+ }
300
+ else {
301
+ // Still parse result event even without progress callback
302
+ try {
303
+ const parsed = JSON.parse(trimmed);
304
+ if (parsed.type === 'result') {
305
+ resultText = parsed.result;
306
+ resultCost = parsed.total_cost_usd;
307
+ const modelUsage = parsed.modelUsage;
308
+ if (modelUsage) {
309
+ const firstModel = Object.keys(modelUsage)[0];
310
+ if (firstModel) {
311
+ const usage = modelUsage[firstModel];
312
+ resultTokenUsage = {
313
+ inputTokens: usage.inputTokens ?? 0,
314
+ outputTokens: usage.outputTokens ?? 0,
315
+ cacheReadTokens: usage.cacheReadInputTokens ?? undefined,
316
+ model: firstModel,
317
+ };
318
+ }
319
+ }
320
+ }
321
+ }
322
+ catch {
323
+ // Not JSON — ignore
324
+ }
325
+ }
326
+ }
327
+ });
103
328
  const errChunks = [];
104
- child.stdout.on('data', (data) => chunks.push(data));
105
- child.stderr.on('data', (data) => errChunks.push(data));
329
+ child.stderr.on('data', (data) => {
330
+ errChunks.push(data);
331
+ lastActivity = Date.now();
332
+ process.stderr.write(data);
333
+ });
334
+ // Heartbeat: log progress every 30s so CI knows the process is alive
335
+ const heartbeat = setInterval(() => {
336
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
337
+ const idle = Math.round((Date.now() - lastActivity) / 1000);
338
+ process.stderr.write(`${logPrefix} heartbeat: ${elapsed}s elapsed, ${idle}s idle\n`);
339
+ }, 30_000);
106
340
  child.on('close', (code) => {
107
- const stdout = Buffer.concat(chunks).toString('utf-8');
341
+ clearInterval(heartbeat);
342
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
108
343
  const stderr = Buffer.concat(errChunks).toString('utf-8');
344
+ process.stderr.write(`${logPrefix} exited: code=${code} elapsed=${elapsed}s\n`);
109
345
  if (code === 0) {
110
- resolve({ stdout, stderr, model });
346
+ resolve({
347
+ stdout: resultText ?? '',
348
+ stderr,
349
+ model: resultTokenUsage?.model ?? model,
350
+ costUsd: resultCost,
351
+ });
111
352
  }
112
353
  else {
113
- reject(new Error(`claude exited with code ${code}: ${stderr || stdout}`));
354
+ reject(new Error(`claude exited with code ${code}: ${stderr || resultText || ''}`));
114
355
  }
115
356
  });
116
- child.on('error', reject);
357
+ child.on('error', (err) => {
358
+ clearInterval(heartbeat);
359
+ process.stderr.write(`${logPrefix} spawn error: ${err.message}\n`);
360
+ reject(err);
361
+ });
117
362
  // Send prompt via stdin and close it
118
363
  child.stdin.write(prompt);
119
364
  child.stdin.end();
@@ -152,50 +397,28 @@ export function parseTokenUsage(stderr, model) {
152
397
  /**
153
398
  * Run lint --fix and format commands (best-effort) so pre-commit hooks pass.
154
399
  */
155
- async function runAutoFix(workDir, lintCmd, fmtCmd) {
156
- if (fmtCmd) {
157
- try {
158
- const [bin, ...args] = fmtCmd.split(' ');
159
- await execFileAsync(bin, args, { cwd: workDir });
160
- }
161
- catch {
162
- // Format failures are non-fatal — the commit hook will catch remaining issues
163
- }
164
- }
165
- if (lintCmd) {
166
- try {
167
- const [bin, ...args] = lintCmd.split(' ');
168
- await execFileAsync(bin, args, { cwd: workDir });
169
- }
170
- catch {
171
- // Lint --fix failures are non-fatal
172
- }
173
- }
174
- }
175
400
  export class ClaudeCodeRunner {
176
401
  async run(ctx) {
177
402
  const prompt = buildPrompt(ctx);
178
403
  try {
179
- // Invoke Claude Code CLI in print mode, sending prompt via stdin
404
+ // Snapshot the worktree BEFORE invoking the agent so we can subtract any
405
+ // pre-existing untracked noise (sqlite working files, draft files the user
406
+ // hasn't decided to commit yet, in-flight edits) from the agent's diff.
407
+ // Without this, `git add` would sweep them into the agent's commit.
408
+ const baseline = await snapshotWorktree(ctx.workDir);
409
+ // Invoke Claude Code CLI with stream-json for real-time progress
180
410
  const result = await runClaude(prompt, ctx.workDir, {
181
411
  allowedTools: ctx.allowedTools,
182
412
  timeoutMs: ctx.timeoutMs,
183
413
  model: ctx.model,
184
414
  sandboxId: ctx.sandboxId,
415
+ onProgress: ctx.onProgress,
185
416
  });
186
- // Parse token usage from stderr
417
+ // Token usage from stream-json result event (falls back to stderr parsing)
187
418
  const tokenUsage = parseTokenUsage(result.stderr, result.model);
188
- // Collect changed files
189
- const diffOutput = await gitExec(ctx.workDir, ['diff', '--name-only']);
190
- const untrackedOutput = await gitExec(ctx.workDir, [
191
- 'ls-files',
192
- '--others',
193
- '--exclude-standard',
194
- ]);
195
- const filesChanged = [
196
- ...diffOutput.split('\n').filter(Boolean),
197
- ...untrackedOutput.split('\n').filter(Boolean),
198
- ];
419
+ // Collect changed files (uncommitted + agent-committed), excluding
420
+ // pre-existing untracked noise.
421
+ const { filesChanged, agentAlreadyCommitted } = await detectChangedFiles(ctx.workDir, baseline);
199
422
  if (filesChanged.length === 0) {
200
423
  return {
201
424
  success: false,
@@ -205,14 +428,26 @@ export class ClaudeCodeRunner {
205
428
  tokenUsage,
206
429
  };
207
430
  }
208
- // Stage, lint/format, and commit
209
- await gitExec(ctx.workDir, ['add', '-A']);
431
+ // If the agent already committed, skip our commit step
432
+ if (agentAlreadyCommitted) {
433
+ return {
434
+ success: true,
435
+ filesChanged,
436
+ summary: result.stdout.slice(0, 2000),
437
+ tokenUsage,
438
+ };
439
+ }
440
+ // Stage only the files the agent touched (NOT `git add -A`, which
441
+ // would include pre-existing untracked noise per the AISDLC-68 incident).
442
+ await gitExec(ctx.workDir, ['add', '--', ...filesChanged]);
210
443
  // Run lint and format before committing to avoid pre-commit hook failures
211
444
  const lintCmd = ctx.lintCommand ?? DEFAULT_LINT_COMMAND;
212
445
  const fmtCmd = ctx.formatCommand ?? DEFAULT_FORMAT_COMMAND;
213
446
  await runAutoFix(ctx.workDir, lintCmd, fmtCmd);
214
- // Re-stage after auto-fix may have modified files
215
- await gitExec(ctx.workDir, ['add', '-A']);
447
+ // Re-stage the same set in case auto-fix re-modified them. Auto-fix should
448
+ // not touch files outside the agent's diff, but if it does, those changes
449
+ // are the user's problem to commit separately.
450
+ await gitExec(ctx.workDir, ['add', '--', ...filesChanged]);
216
451
  const tmpl = ctx.commitMessageTemplate ?? DEFAULT_COMMIT_MESSAGE_TEMPLATE;
217
452
  const coAuthor = ctx.commitCoAuthor ?? DEFAULT_COMMIT_CO_AUTHOR;
218
453
  const commitMsg = tmpl
@@ -222,11 +457,22 @@ export class ClaudeCodeRunner {
222
457
  await gitExec(ctx.workDir, ['commit', '-m', `${commitMsg}\n\nCo-Authored-By: ${coAuthor}`]);
223
458
  }
224
459
  catch {
225
- // Pre-commit hook may have auto-fixed files — re-stage and retry once
460
+ // Pre-commit hook may have auto-fixed files — re-stage the agent's
461
+ // file set and retry once.
226
462
  await runAutoFix(ctx.workDir, lintCmd, fmtCmd);
227
- await gitExec(ctx.workDir, ['add', '-A']);
463
+ await gitExec(ctx.workDir, ['add', '--', ...filesChanged]);
228
464
  await gitExec(ctx.workDir, ['commit', '-m', `${commitMsg}\n\nCo-Authored-By: ${coAuthor}`]);
229
465
  }
466
+ // Detect cross-repo writes (e.g., agent edited files in ../sister-repo)
467
+ // and surface them as a stderr warning so the operator knows to commit
468
+ // them in the sibling repo separately. Not a hard failure — sometimes
469
+ // cross-repo work is intended (the AISDLC-68 docs sync, for example).
470
+ const crossRepoWrites = await detectCrossRepoWrites(ctx.workDir);
471
+ for (const write of crossRepoWrites) {
472
+ process.stderr.write(`[ai-sdlc:runner] WARNING: agent wrote ${write.files.length} file(s) into sibling repo ` +
473
+ `${write.repoPath} — those changes are NOT in this PR. ` +
474
+ `Files: ${write.files.slice(0, 5).join(', ')}${write.files.length > 5 ? ` (+${write.files.length - 5} more)` : ''}\n`);
475
+ }
230
476
  return {
231
477
  success: true,
232
478
  filesChanged,
@@ -6,10 +6,13 @@ import { spawn, execFile } from 'node:child_process';
6
6
  import { promisify } from 'node:util';
7
7
  import { DEFAULT_RUNNER_TIMEOUT_MS, DEFAULT_CODEX_MODEL, DEFAULT_COMMIT_MESSAGE_TEMPLATE, DEFAULT_COMMIT_CO_AUTHOR, } from '../defaults.js';
8
8
  import { buildPrompt } from './claude-code.js';
9
+ import { cleanGitEnv } from '../runtime/git-env.js';
9
10
  export { buildPrompt };
10
11
  const execFileAsync = promisify(execFile);
12
+ // Strip GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE so git resolves against
13
+ // `workDir`'s own .git rather than whatever a parent process leaked (AISDLC-72).
11
14
  async function gitExec(workDir, args) {
12
- const { stdout } = await execFileAsync('git', args, { cwd: workDir });
15
+ const { stdout } = await execFileAsync('git', args, { cwd: workDir, env: cleanGitEnv() });
13
16
  return stdout.trim();
14
17
  }
15
18
  /**
@@ -6,10 +6,13 @@ import { spawn, execFile } from 'node:child_process';
6
6
  import { promisify } from 'node:util';
7
7
  import { DEFAULT_RUNNER_TIMEOUT_MS, DEFAULT_COPILOT_MODEL, DEFAULT_COMMIT_MESSAGE_TEMPLATE, DEFAULT_COMMIT_CO_AUTHOR, } from '../defaults.js';
8
8
  import { buildPrompt } from './claude-code.js';
9
+ import { cleanGitEnv } from '../runtime/git-env.js';
9
10
  export { buildPrompt };
10
11
  const execFileAsync = promisify(execFile);
12
+ // Strip GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE so git resolves against
13
+ // `workDir`'s own .git rather than whatever a parent process leaked (AISDLC-72).
11
14
  async function gitExec(workDir, args) {
12
- const { stdout } = await execFileAsync('git', args, { cwd: workDir });
15
+ const { stdout } = await execFileAsync('git', args, { cwd: workDir, env: cleanGitEnv() });
13
16
  return stdout.trim();
14
17
  }
15
18
  /**
@@ -6,10 +6,13 @@ import { spawn, execFile } from 'node:child_process';
6
6
  import { promisify } from 'node:util';
7
7
  import { DEFAULT_RUNNER_TIMEOUT_MS, DEFAULT_CURSOR_MODEL, DEFAULT_COMMIT_MESSAGE_TEMPLATE, DEFAULT_COMMIT_CO_AUTHOR, } from '../defaults.js';
8
8
  import { buildPrompt } from './claude-code.js';
9
+ import { cleanGitEnv } from '../runtime/git-env.js';
9
10
  export { buildPrompt };
10
11
  const execFileAsync = promisify(execFile);
12
+ // Strip GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE so git resolves against
13
+ // `workDir`'s own .git rather than whatever a parent process leaked (AISDLC-72).
11
14
  async function gitExec(workDir, args) {
12
- const { stdout } = await execFileAsync('git', args, { cwd: workDir });
15
+ const { stdout } = await execFileAsync('git', args, { cwd: workDir, env: cleanGitEnv() });
13
16
  return stdout.trim();
14
17
  }
15
18
  /**
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Shared git utilities for agent runners.
3
+ */
4
+ /**
5
+ * Run a git command in the given directory.
6
+ *
7
+ * Always passes `-c core.quotePath=false` so paths containing non-ASCII
8
+ * characters (e.g. ↔, é, 中) come back as raw UTF-8 instead of git's default
9
+ * octal-escaped form (`"file with \342\206\224.md"`). Without this, the AISDLC-68
10
+ * task file (which has ↔ in its name) showed up in `git diff --name-only`
11
+ * as a quoted+escaped string and the subsequent `git add -- <file>` rejected
12
+ * with "pathspec did not match".
13
+ *
14
+ * Uses `cleanGitEnv()` (AISDLC-72) so git resolves against `workDir`'s own
15
+ * .git rather than whatever a parent process (husky pre-push hook running
16
+ * in another worktree) leaked into GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE.
17
+ */
18
+ export declare function gitExec(workDir: string, args: string[]): Promise<string>;
19
+ export interface DetectedChanges {
20
+ filesChanged: string[];
21
+ agentAlreadyCommitted: boolean;
22
+ }
23
+ export interface WorktreeBaseline {
24
+ /** Set of paths that were untracked BEFORE the agent ran. */
25
+ untracked: Set<string>;
26
+ /** Set of paths that had unstaged modifications BEFORE the agent ran. */
27
+ modified: Set<string>;
28
+ }
29
+ /**
30
+ * Snapshot the untracked + modified file lists in the worktree. Captured before
31
+ * the agent runs so detectChangedFiles can subtract pre-existing noise (SQLite
32
+ * working files, draft RFCs the user hasn't decided to commit yet, in-flight
33
+ * edits on the previous branch). Without this, `git add -A` sweeps everything
34
+ * into the agent's commit (the AISDLC-68 incident).
35
+ *
36
+ * Failures degrade gracefully to an empty baseline — never block the pipeline
37
+ * because the snapshot couldn't run.
38
+ */
39
+ export declare function snapshotWorktree(workDir: string): Promise<WorktreeBaseline>;
40
+ /**
41
+ * Detect files changed by an agent. Three signals contribute:
42
+ *
43
+ * - **Unstaged working-tree changes** (`git diff --name-only`)
44
+ * - **Staged but uncommitted changes** (`git diff --name-only --cached`) — the
45
+ * agent sometimes runs `git add` itself before yielding control. Without
46
+ * this signal the orchestrator returned "Agent made no changes" and bailed
47
+ * even though the agent's work was sitting in the index (the AISDLC-68
48
+ * fourth-rerun bug).
49
+ * - **Untracked files** (`git ls-files --others`)
50
+ *
51
+ * When `baseline` is provided, untracked files that existed BEFORE the agent
52
+ * ran are excluded — they belong to the user, not the agent's diff.
53
+ *
54
+ * Also checks whether the agent self-committed (compares HEAD to merge-base
55
+ * with origin/main). Self-committed runs short-circuit the orchestrator's
56
+ * commit step.
57
+ */
58
+ export declare function detectChangedFiles(workDir: string, baseline?: WorktreeBaseline): Promise<DetectedChanges>;
59
+ export interface CrossRepoWrite {
60
+ /** Sibling repository absolute path. */
61
+ repoPath: string;
62
+ /** Files modified or added in that sibling, relative to its root. */
63
+ files: string[];
64
+ }
65
+ /**
66
+ * Detect writes the agent made into sibling git repositories (i.e., directories
67
+ * adjacent to `workDir` that are themselves git repos). Surfaced as a warning,
68
+ * not a hard failure — the AISDLC-68 task LEGITIMATELY needed to sync into
69
+ * `../ai-sdlc-io/`, and we don't want to forbid that. The orchestrator just
70
+ * needs to surface that the changes exist so the operator knows to commit them
71
+ * separately in the sibling repo.
72
+ *
73
+ * Returns one entry per dirty sibling repo, with the list of changed files.
74
+ * Empty array when no siblings are dirty (the common case).
75
+ */
76
+ export declare function detectCrossRepoWrites(workDir: string): Promise<CrossRepoWrite[]>;
77
+ /**
78
+ * Run lint and format auto-fix commands (best-effort, non-fatal).
79
+ */
80
+ export declare function runAutoFix(workDir: string, lintCmd?: string, fmtCmd?: string): Promise<void>;
81
+ //# sourceMappingURL=git-utils.d.ts.map