@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
@@ -0,0 +1,448 @@
1
+ /**
2
+ * Fix-Review orchestrator — detects review findings on agent-created PRs,
3
+ * fetches review comments, and re-invokes the agent with review context.
4
+ * Capped at MAX_FIX_ATTEMPTS to prevent infinite loops.
5
+ */
6
+ import { createGitHubIssueTracker, evaluateDemotion, withSpan, getMeter, SPAN_NAMES, METRIC_NAMES, ATTRIBUTE_KEYS, } from '@ai-sdlc/reference';
7
+ import { loadConfig } from './config.js';
8
+ import { createLogger } from './logger.js';
9
+ import { createStructuredConsoleLogger } from './structured-logger.js';
10
+ import { ClaudeCodeRunner } from './runners/claude-code.js';
11
+ import { execFileAsync, getGitHubConfig, extractIssueId, resolveRepoRoot, createDefaultAuditLog, resolveAutonomyLevel, resolveConstraints, recordMetric, validateAndAuditOutput, authorizeFilesChanged, issueIdToNumber, } from './shared.js';
12
+ import { renderTemplate } from './notifications.js';
13
+ import { parseDuration } from './policy-evaluators.js';
14
+ import { checkKillSwitch, issueAgentCredentials, revokeAgentCredentials, } from './security.js';
15
+ import { DEFAULT_GH_CLI_TIMEOUT_MS, DEFAULT_CONFIG_DIR_NAME, defaultSandboxConstraints, NOTIFICATION_TITLES, } from './defaults.js';
16
+ import { createCycleDetectorFromConfig, checkAndHandleCycle } from './cycle-utils.js';
17
+ import { cleanGitEnv } from './runtime/git-env.js';
18
+ // Default max review-fix attempts (lower than CI-fix since reviews are more deterministic)
19
+ export const MAX_REVIEW_FIX_ATTEMPTS = 2;
20
+ export const RETRY_MARKER = '<!-- ai-sdlc-fix-review-attempt -->';
21
+ /**
22
+ * Count how many fix-review retry attempts have been made on a PR
23
+ * by scanning comments for the hidden retry marker.
24
+ */
25
+ export function countRetryAttempts(comments) {
26
+ let count = 0;
27
+ for (const body of comments) {
28
+ const matches = body.match(new RegExp(RETRY_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'));
29
+ if (matches) {
30
+ count += matches.length;
31
+ }
32
+ }
33
+ return count;
34
+ }
35
+ /**
36
+ * Validate that a PR number is a positive integer.
37
+ */
38
+ export function validatePrNumber(prNumber) {
39
+ if (!Number.isInteger(prNumber) || prNumber <= 0) {
40
+ throw new Error(`Invalid PR number: ${prNumber} (must be a positive integer)`);
41
+ }
42
+ }
43
+ /**
44
+ * Sanitize a git branch name to prevent command injection.
45
+ * Allows only alphanumeric characters, slashes, dashes, underscores, and dots.
46
+ */
47
+ export function sanitizeBranchName(branch) {
48
+ if (!/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
49
+ throw new Error(`Invalid branch name: "${branch}" (only alphanumeric, /, -, _, . allowed)`);
50
+ }
51
+ return branch;
52
+ }
53
+ /**
54
+ * Fetch review findings from PR reviews that requested changes.
55
+ * Returns a formatted string with all findings from review agents.
56
+ */
57
+ export async function fetchReviewFindings(prNumber, injectedFindings, _secretStore) {
58
+ if (injectedFindings !== undefined) {
59
+ return injectedFindings;
60
+ }
61
+ // Validate PR number to prevent command injection
62
+ validatePrNumber(prNumber);
63
+ // Fetch reviews using gh CLI
64
+ const { stdout } = await execFileAsync('gh', ['pr', 'review', String(prNumber), '--json', 'state,body,author'], { timeout: DEFAULT_GH_CLI_TIMEOUT_MS });
65
+ let reviews;
66
+ try {
67
+ reviews = JSON.parse(stdout);
68
+ }
69
+ catch (err) {
70
+ throw new Error(`Failed to parse review data from gh CLI: ${err instanceof Error ? err.message : String(err)}`);
71
+ }
72
+ const changesRequestedReviews = reviews.filter((r) => r.state === 'CHANGES_REQUESTED');
73
+ if (changesRequestedReviews.length === 0) {
74
+ return 'No review findings (all reviews approved or pending)';
75
+ }
76
+ // Format findings from all reviews
77
+ const findings = changesRequestedReviews
78
+ .map((r) => `### Review by ${r.author.login}\n\n${r.body}\n\n---\n`)
79
+ .join('\n');
80
+ return findings;
81
+ }
82
+ /**
83
+ * Execute the fix-review pipeline for a PR with review findings.
84
+ *
85
+ * Returns gracefully (no throw) when the retry limit is reached.
86
+ * Throws on agent failure or guardrail violations.
87
+ */
88
+ export async function executeFixReview(prNumber, options = {}) {
89
+ const workDir = options.workDir ?? (await resolveRepoRoot());
90
+ const configDir = options.configDir ?? `${workDir}/${DEFAULT_CONFIG_DIR_NAME}`;
91
+ const log = options.logger ??
92
+ (options.useStructuredLogger ? createStructuredConsoleLogger() : createLogger());
93
+ const auditLog = options.auditLog ?? createDefaultAuditLog(workDir);
94
+ const metricStore = options.metricStore;
95
+ // 1. Load config
96
+ log.stage('load-config');
97
+ const config = loadConfig(configDir);
98
+ log.stageEnd('load-config');
99
+ // Kill switch check (before any work)
100
+ if (options.security) {
101
+ await checkKillSwitch(options.security);
102
+ }
103
+ if (!config.agentRole) {
104
+ throw new Error('No AgentRole resource found in .ai-sdlc/');
105
+ }
106
+ if (!config.autonomyPolicy) {
107
+ throw new Error('No AutonomyPolicy resource found in .ai-sdlc/');
108
+ }
109
+ const agentRole = config.agentRole;
110
+ const autonomyPolicy = config.autonomyPolicy;
111
+ // Derive max fix attempts from pipeline config (review stage onFailure.maxRetries)
112
+ const reviewStage = config.pipeline?.spec.stages.find((s) => s.name === 'review');
113
+ const maxFixAttempts = reviewStage?.onFailure?.maxRetries ?? MAX_REVIEW_FIX_ATTEMPTS;
114
+ // Notification templates
115
+ const notifTemplates = config.pipeline?.spec.notifications?.templates;
116
+ // Create default tracker when needed (lazy to avoid resolving secrets in test environments).
117
+ // In production (no _prComments injected), the tracker is always available.
118
+ let _tracker = options.tracker;
119
+ function getTracker() {
120
+ if (!_tracker) {
121
+ const { org, repo } = getGitHubConfig(options.secretStore);
122
+ const ghConfig = { org, repo, token: { secretRef: 'github-token' } };
123
+ _tracker = createGitHubIssueTracker(ghConfig);
124
+ }
125
+ return _tracker;
126
+ }
127
+ // Tracker is available if injected directly or if we're not in test mode
128
+ const trackerAvailable = !!options.tracker || options._prComments === undefined;
129
+ // Create cycle detector (marker generated after all guard conditions pass)
130
+ const cycleDetector = createCycleDetectorFromConfig(config.pipeline?.spec ?? {});
131
+ // 2. Count retry attempts (via injected comments or IssueTracker)
132
+ log.stage('check-retries');
133
+ let comments;
134
+ if (options._prComments !== undefined) {
135
+ comments = options._prComments;
136
+ }
137
+ else {
138
+ const issueComments = await getTracker().getComments(String(prNumber));
139
+ comments = issueComments.map((c) => c.body);
140
+ }
141
+ const attempts = countRetryAttempts(comments);
142
+ log.info(`Fix-review attempt ${attempts + 1} of ${maxFixAttempts}`);
143
+ log.stageEnd('check-retries');
144
+ // Helper to add a comment via tracker (uses default tracker in production)
145
+ const addComment = async (body) => {
146
+ if (trackerAvailable) {
147
+ await getTracker().addComment(String(prNumber), body);
148
+ }
149
+ };
150
+ if (attempts >= maxFixAttempts) {
151
+ log.info(`Fix-review retry limit reached (${maxFixAttempts}). Commenting and stopping.`);
152
+ auditLog.record({
153
+ actor: 'system',
154
+ action: 'evaluate',
155
+ resource: `pr#${prNumber}`,
156
+ decision: 'denied',
157
+ details: { reason: 'retry-limit-reached', attempts, max: maxFixAttempts },
158
+ });
159
+ const limitTpl = notifTemplates?.['fix-review-limit'];
160
+ const limitComment = limitTpl
161
+ ? renderTemplate(limitTpl, {
162
+ attempts: String(attempts),
163
+ max: String(maxFixAttempts),
164
+ })
165
+ : {
166
+ title: NOTIFICATION_TITLES.fixReviewRetryLimit,
167
+ body: `This PR has reached the maximum number of automated review-fix attempts (${maxFixAttempts}). Manual intervention is needed.`,
168
+ };
169
+ await addComment(`## ${limitComment.title}\n\n${limitComment.body}`);
170
+ return;
171
+ }
172
+ // Check for pipeline-level cycles AFTER retry counting
173
+ if (trackerAvailable) {
174
+ const cycleCheck = await checkAndHandleCycle({
175
+ issueOrPrId: String(prNumber),
176
+ stage: 'fix-review',
177
+ tracker: getTracker(),
178
+ detector: cycleDetector,
179
+ });
180
+ if (cycleCheck.cycleDetected) {
181
+ log.info('Pipeline cycle detected. Halting fix-review execution.');
182
+ auditLog.record({
183
+ actor: 'system',
184
+ action: 'evaluate',
185
+ resource: `pr#${prNumber}`,
186
+ decision: 'denied',
187
+ details: { reason: 'pipeline-cycle-detected' },
188
+ });
189
+ return;
190
+ }
191
+ }
192
+ // Generate cycle marker AFTER all guard conditions pass
193
+ const cycleMarker = cycleDetector.recordInvocation('fix-review');
194
+ // 3. Fetch review findings
195
+ log.stage('fetch-findings');
196
+ const reviewFindings = await fetchReviewFindings(prNumber, options._reviewFindings, options.secretStore);
197
+ log.stageEnd('fetch-findings');
198
+ // Check if review findings are actionable (not a generic "no findings" message)
199
+ const isActionable = reviewFindings &&
200
+ !reviewFindings.startsWith('No review findings') &&
201
+ reviewFindings.trim().length > 0;
202
+ if (!isActionable) {
203
+ log.info('No actionable review findings. Skipping fix-review execution.');
204
+ auditLog.record({
205
+ actor: 'system',
206
+ action: 'evaluate',
207
+ resource: `pr#${prNumber}`,
208
+ decision: 'allowed',
209
+ details: { reason: 'no-actionable-findings', reviewFindings },
210
+ });
211
+ return;
212
+ }
213
+ // 4. Determine branch and issue number
214
+ // cleanGitEnv() prevents leaked GIT_DIR from binding to a parent repo (AISDLC-72).
215
+ const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], {
216
+ cwd: workDir,
217
+ env: cleanGitEnv(),
218
+ });
219
+ const currentBranch = sanitizeBranchName(branchStdout.trim());
220
+ const issueId = extractIssueId(currentBranch);
221
+ if (issueId === null) {
222
+ throw new Error(`Branch "${currentBranch}" does not match ai-sdlc/issue-<id> pattern`);
223
+ }
224
+ const issueNumber = issueIdToNumber(issueId);
225
+ // 5. Resolve autonomy level and constraints
226
+ const currentLevel = resolveAutonomyLevel(autonomyPolicy);
227
+ const resolved = resolveConstraints(agentRole.spec.constraints, currentLevel);
228
+ // 6. Fetch issue data (via tracker when available)
229
+ let issueTitle = `Issue ${issueId}`;
230
+ let issueBody = '';
231
+ if (trackerAvailable) {
232
+ const issueData = await getTracker().getIssue(issueId);
233
+ issueTitle = issueData.title;
234
+ issueBody = issueData.description ?? '';
235
+ }
236
+ // Store issue context in working memory
237
+ if (options.memory) {
238
+ options.memory.working.set('currentIssue', { prNumber, issueId, currentBranch });
239
+ }
240
+ // Query episodic memory for previous fix-review attempts
241
+ if (options.memory) {
242
+ const previousAttempts = options.memory.episodic.search('fix-review-execution');
243
+ if (previousAttempts.length > 0) {
244
+ log.info(`Found ${previousAttempts.length} previous fix-review episodes in memory`);
245
+ }
246
+ }
247
+ const meter = getMeter();
248
+ // Wrap agent+validation+push in try/catch for failure episodes
249
+ try {
250
+ // 7. Invoke agent with review findings (with sandbox + JIT credential lifecycle)
251
+ log.stage('agent');
252
+ const runner = options.runner ?? new ClaudeCodeRunner();
253
+ // Sandbox isolation around agent execution
254
+ let sandboxId;
255
+ let result;
256
+ try {
257
+ if (options.security) {
258
+ const timeoutMs = reviewStage?.timeout ? parseDuration(reviewStage.timeout) : undefined;
259
+ sandboxId = await options.security.sandbox.isolate(`issue-${issueId}`, defaultSandboxConstraints(workDir, timeoutMs));
260
+ }
261
+ // Issue JIT credentials before agent execution
262
+ const jitCred = options.security
263
+ ? await issueAgentCredentials(options.security, agentRole.metadata.name)
264
+ : undefined;
265
+ try {
266
+ result = await withSpan(SPAN_NAMES.AGENT_TASK, {
267
+ [ATTRIBUTE_KEYS.AGENT]: agentRole.metadata.name,
268
+ [ATTRIBUTE_KEYS.RESOURCE_NAME]: `pr#${prNumber}`,
269
+ }, async () => {
270
+ const r = await runner.run({
271
+ issueId,
272
+ issueNumber: issueNumber ?? undefined,
273
+ issueTitle,
274
+ issueBody,
275
+ workDir,
276
+ branch: currentBranch,
277
+ constraints: {
278
+ maxFilesPerChange: resolved.maxFiles,
279
+ requireTests: resolved.requireTests,
280
+ blockedPaths: resolved.blockedPaths,
281
+ },
282
+ reviewFindings,
283
+ });
284
+ if (!r.success) {
285
+ log.stageEnd('agent');
286
+ auditLog.record({
287
+ actor: 'system',
288
+ action: 'execute',
289
+ resource: `agent/${agentRole.metadata.name}`,
290
+ decision: 'denied',
291
+ details: { error: r.error },
292
+ });
293
+ meter.createCounter(METRIC_NAMES.TASK_FAILURE_TOTAL).add(1);
294
+ recordMetric(metricStore, METRIC_NAMES.TASK_FAILURE_TOTAL, 1);
295
+ // Evaluate demotion on agent failure
296
+ const agentMetrics = {
297
+ name: agentRole.metadata.name,
298
+ currentLevel: currentLevel.level,
299
+ totalTasksCompleted: 0,
300
+ metrics: {},
301
+ approvals: [],
302
+ };
303
+ const demotion = evaluateDemotion(autonomyPolicy, agentMetrics, 'failed-test');
304
+ log.info(`Demotion evaluation: ${demotion.demoted ? `demoted from ${demotion.fromLevel} to ${demotion.toLevel}` : 'no demotion'}`);
305
+ auditLog.record({
306
+ actor: 'system',
307
+ action: 'evaluate',
308
+ resource: `agent/${agentRole.metadata.name}`,
309
+ policy: 'demotion',
310
+ decision: demotion.demoted ? 'denied' : 'allowed',
311
+ details: {
312
+ trigger: demotion.trigger,
313
+ fromLevel: demotion.fromLevel,
314
+ toLevel: demotion.toLevel,
315
+ },
316
+ });
317
+ const agentFailTpl = notifTemplates?.['agent-failure'];
318
+ const errorDetail = r.error ?? 'Unknown error';
319
+ const agentFailComment = agentFailTpl
320
+ ? renderTemplate(agentFailTpl, { stageName: 'fix-review', details: errorDetail })
321
+ : { title: NOTIFICATION_TITLES.fixReviewAgentFailed, body: errorDetail };
322
+ // Use pre-created marker (one per execution, prevents double-counting)
323
+ await addComment(`## ${agentFailComment.title}\n\n${agentFailComment.body}\n\n${RETRY_MARKER}\n${cycleMarker}`);
324
+ throw new Error(`Fix-review agent failed on PR #${prNumber}: ${r.error}`);
325
+ }
326
+ log.stageEnd('agent');
327
+ auditLog.record({
328
+ actor: 'system',
329
+ action: 'execute',
330
+ resource: `agent/${agentRole.metadata.name}`,
331
+ decision: 'allowed',
332
+ details: { filesChanged: r.filesChanged.length },
333
+ });
334
+ meter.createCounter(METRIC_NAMES.TASK_SUCCESS_TOTAL).add(1);
335
+ recordMetric(metricStore, METRIC_NAMES.TASK_SUCCESS_TOTAL, 1);
336
+ return r;
337
+ });
338
+ }
339
+ finally {
340
+ // Revoke JIT credentials after agent execution (success or failure)
341
+ if (jitCred && options.security) {
342
+ await revokeAgentCredentials(options.security, jitCred.id);
343
+ }
344
+ }
345
+ }
346
+ finally {
347
+ // Destroy sandbox after agent execution
348
+ if (sandboxId && options.security) {
349
+ await options.security.sandbox.destroy(sandboxId);
350
+ }
351
+ }
352
+ // 8. ABAC authorization check (if write permissions are defined)
353
+ if (currentLevel.permissions.write.length > 0) {
354
+ authorizeFilesChanged(result.filesChanged, currentLevel.permissions, agentRole.spec.constraints, auditLog, agentRole.metadata.name);
355
+ }
356
+ // 9. Validate agent output against guardrails
357
+ await withSpan(SPAN_NAMES.PIPELINE_STAGE, {
358
+ [ATTRIBUTE_KEYS.STAGE]: 'validate-output',
359
+ }, async () => {
360
+ await validateAndAuditOutput({
361
+ filesChanged: result.filesChanged,
362
+ workDir,
363
+ constraints: {
364
+ maxFilesPerChange: resolved.maxFiles,
365
+ requireTests: resolved.requireTests,
366
+ blockedPaths: resolved.blockedPaths,
367
+ },
368
+ guardrails: { maxLinesPerPR: currentLevel.guardrails.maxLinesPerPR },
369
+ auditLog,
370
+ log,
371
+ onViolation: async (violationList) => {
372
+ await addComment(`## ${NOTIFICATION_TITLES.fixReviewGuardrailViolations}\n\n${violationList}\n\n${RETRY_MARKER}\n${cycleMarker}`);
373
+ },
374
+ });
375
+ });
376
+ // 10. Push to the same branch (review agents re-run automatically via pull_request.synchronize)
377
+ // cleanGitEnv() prevents leaked GIT_DIR from binding to a parent repo (AISDLC-72).
378
+ log.stage('push');
379
+ await execFileAsync('git', ['push', 'origin', currentBranch], {
380
+ cwd: workDir,
381
+ env: cleanGitEnv(),
382
+ });
383
+ log.stageEnd('push');
384
+ auditLog.record({
385
+ actor: 'system',
386
+ action: 'create',
387
+ resource: `push/${currentBranch}`,
388
+ decision: 'allowed',
389
+ details: { prNumber, attempt: attempts + 1 },
390
+ });
391
+ // 11. Comment on PR with success details
392
+ const successTpl = notifTemplates?.['fix-review-success'];
393
+ const successComment = successTpl
394
+ ? renderTemplate(successTpl, {
395
+ attempt: String(attempts + 1),
396
+ max: String(maxFixAttempts),
397
+ branch: currentBranch,
398
+ })
399
+ : {
400
+ title: NOTIFICATION_TITLES.fixReviewApplied,
401
+ body: `Attempt ${attempts + 1} of ${maxFixAttempts} — pushed review fixes to \`${currentBranch}\`.`,
402
+ };
403
+ await addComment([
404
+ `## ${successComment.title}`,
405
+ '',
406
+ successComment.body,
407
+ '',
408
+ '### Changes',
409
+ result.filesChanged.map((f) => `- \`${f}\``).join('\n'),
410
+ '',
411
+ RETRY_MARKER,
412
+ cycleMarker,
413
+ ].join('\n'));
414
+ // 12. Record episodic memory (success)
415
+ if (options.memory) {
416
+ options.memory.episodic.append({
417
+ key: 'fix-review-execution',
418
+ value: {
419
+ prNumber,
420
+ issueId,
421
+ filesChanged: result.filesChanged.length,
422
+ outcome: 'success',
423
+ },
424
+ metadata: { summary: `Fix-review for PR #${prNumber} (attempt ${attempts + 1})` },
425
+ });
426
+ options.memory.working.clear();
427
+ }
428
+ }
429
+ catch (err) {
430
+ // Record failure episode before rethrowing
431
+ if (options.memory) {
432
+ options.memory.episodic.append({
433
+ key: 'fix-review-execution',
434
+ value: {
435
+ prNumber,
436
+ issueId,
437
+ outcome: 'failure',
438
+ error: err instanceof Error ? err.message : String(err),
439
+ },
440
+ metadata: { summary: `Failed fix-review for PR #${prNumber}` },
441
+ });
442
+ options.memory.working.clear();
443
+ }
444
+ throw err;
445
+ }
446
+ log.summary();
447
+ }
448
+ //# sourceMappingURL=fix-review.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * ClaudeCodeAdapter — wraps Claude Code CLI behind the HarnessAdapter contract.
3
+ * Uses the local `claude` CLI in `-p --output-format stream-json` mode, authenticating
4
+ * via whatever credential the CLI is logged into (Pro/Max subscription or API key).
5
+ */
6
+ import { spawn } from 'node:child_process';
7
+ import type { HarnessAdapter, HarnessAvailability, HarnessCapabilities, HarnessEvent, HarnessInput, HarnessName, HarnessRequires, HarnessResult } from '../types.js';
8
+ export interface ClaudeCodeAdapterDeps {
9
+ env?: NodeJS.ProcessEnv;
10
+ /** Override the actual invocation path; tests inject a stub. */
11
+ invoke?: (input: HarnessInput, onEvent?: (e: HarnessEvent) => void) => Promise<HarnessResult>;
12
+ /** Override version probe for tests. */
13
+ probe?: () => Promise<HarnessAvailability>;
14
+ /** Override the spawn function (tests inject a fake child process). */
15
+ spawn?: typeof spawn;
16
+ }
17
+ export declare class ClaudeCodeAdapter implements HarnessAdapter {
18
+ private readonly deps;
19
+ readonly name: HarnessName;
20
+ readonly capabilities: HarnessCapabilities;
21
+ readonly requires: HarnessRequires;
22
+ private cachedAvailability;
23
+ constructor(deps?: ClaudeCodeAdapterDeps);
24
+ getAccountId(): Promise<string | null>;
25
+ isAvailable(): Promise<HarnessAvailability>;
26
+ invoke(input: HarnessInput, onEvent?: (e: HarnessEvent) => void): Promise<HarnessResult>;
27
+ availableModels(): Promise<string[]>;
28
+ }
29
+ //# sourceMappingURL=claude-code.d.ts.map
@@ -0,0 +1,191 @@
1
+ /**
2
+ * ClaudeCodeAdapter — wraps Claude Code CLI behind the HarnessAdapter contract.
3
+ * Uses the local `claude` CLI in `-p --output-format stream-json` mode, authenticating
4
+ * via whatever credential the CLI is logged into (Pro/Max subscription or API key).
5
+ */
6
+ import { spawn } from 'node:child_process';
7
+ import { createHash } from 'node:crypto';
8
+ import { probeVersion } from '../version-probe.js';
9
+ const DEFAULT_AVAILABLE_MODELS = [
10
+ 'claude-haiku-4-5-20251001',
11
+ 'claude-sonnet-4-6',
12
+ 'claude-opus-4-7',
13
+ 'claude-opus-4-7[1m]',
14
+ ];
15
+ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
16
+ const HEARTBEAT_INTERVAL_MS = 30_000;
17
+ export class ClaudeCodeAdapter {
18
+ deps;
19
+ name = 'claude-code';
20
+ capabilities = {
21
+ freshContext: true,
22
+ customTools: true,
23
+ streaming: true,
24
+ worktreeAwareCwd: true,
25
+ skills: true,
26
+ artifactWrites: true,
27
+ maxContextTokens: 1_000_000,
28
+ };
29
+ requires = {
30
+ binary: 'claude',
31
+ versionRange: '>=2.0.0',
32
+ versionProbe: {
33
+ args: ['--version'],
34
+ parse: (stdout) => stdout.match(/(\d+\.\d+\.\d+)/)?.[1] ?? '',
35
+ },
36
+ };
37
+ cachedAvailability = null;
38
+ constructor(deps = {}) {
39
+ this.deps = deps;
40
+ }
41
+ async getAccountId() {
42
+ const env = this.deps.env ?? process.env;
43
+ const tokenSources = [env.ANTHROPIC_API_KEY, env.CLAUDE_CODE_API_KEY, env.ANTHROPIC_AUTH_TOKEN];
44
+ for (const source of tokenSources) {
45
+ if (source && source.length > 0) {
46
+ return createHash('sha256').update(`claude-code:${source}`).digest('hex').slice(0, 16);
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+ async isAvailable() {
52
+ if (this.cachedAvailability)
53
+ return this.cachedAvailability;
54
+ const result = this.deps.probe ? await this.deps.probe() : await probeVersion(this.requires);
55
+ this.cachedAvailability = result;
56
+ return result;
57
+ }
58
+ async invoke(input, onEvent) {
59
+ if (this.deps.invoke)
60
+ return this.deps.invoke(input, onEvent);
61
+ return runClaudeCli(input, this.deps.spawn ?? spawn, onEvent);
62
+ }
63
+ async availableModels() {
64
+ return DEFAULT_AVAILABLE_MODELS;
65
+ }
66
+ }
67
+ function runClaudeCli(input, spawnFn, onEvent) {
68
+ const timeoutMs = parseDurationMs(input.timeout) ?? DEFAULT_TIMEOUT_MS;
69
+ const args = ['-p', '--output-format', 'stream-json', '--verbose', '--model', input.model];
70
+ return new Promise((resolve) => {
71
+ const startTs = Date.now();
72
+ onEvent?.({ type: 'started', timestamp: new Date().toISOString() });
73
+ let child;
74
+ try {
75
+ child = spawnFn('claude', args, {
76
+ cwd: input.cwd,
77
+ stdio: ['pipe', 'pipe', 'pipe'],
78
+ env: process.env,
79
+ timeout: timeoutMs,
80
+ });
81
+ }
82
+ catch (err) {
83
+ resolve({
84
+ status: 'unavailable',
85
+ exitCode: -1,
86
+ costUsd: 0,
87
+ inputTokens: 0,
88
+ outputTokens: 0,
89
+ artifactPaths: [],
90
+ errorDetail: `failed to spawn 'claude': ${err.message}`,
91
+ });
92
+ return;
93
+ }
94
+ let resultText = '';
95
+ let inputTokens = 0;
96
+ let outputTokens = 0;
97
+ let costUsd = 0;
98
+ let lineBuf = '';
99
+ const errBuf = [];
100
+ const heartbeat = setInterval(() => {
101
+ onEvent?.({
102
+ type: 'heartbeat',
103
+ timestamp: new Date().toISOString(),
104
+ message: `${Math.round((Date.now() - startTs) / 1000)}s elapsed`,
105
+ });
106
+ }, HEARTBEAT_INTERVAL_MS);
107
+ child.stdout?.on('data', (chunk) => {
108
+ lineBuf += chunk.toString('utf-8');
109
+ const lines = lineBuf.split('\n');
110
+ lineBuf = lines.pop() ?? '';
111
+ for (const line of lines) {
112
+ const trimmed = line.trim();
113
+ if (!trimmed)
114
+ continue;
115
+ try {
116
+ const ev = JSON.parse(trimmed);
117
+ if (ev.type === 'result') {
118
+ resultText = ev.result ?? '';
119
+ costUsd = ev.total_cost_usd ?? 0;
120
+ const modelUsage = ev.modelUsage;
121
+ if (modelUsage) {
122
+ const firstModel = Object.keys(modelUsage)[0];
123
+ if (firstModel) {
124
+ const usage = modelUsage[firstModel];
125
+ inputTokens = usage.inputTokens ?? 0;
126
+ outputTokens = usage.outputTokens ?? 0;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ catch {
132
+ // Non-JSON line (claude occasionally emits status text); ignore.
133
+ }
134
+ }
135
+ });
136
+ child.stderr?.on('data', (chunk) => {
137
+ errBuf.push(chunk);
138
+ });
139
+ child.on('error', (err) => {
140
+ clearInterval(heartbeat);
141
+ resolve({
142
+ status: 'unavailable',
143
+ exitCode: -1,
144
+ costUsd,
145
+ inputTokens,
146
+ outputTokens,
147
+ artifactPaths: [],
148
+ errorDetail: `claude spawn error: ${err.message}`,
149
+ });
150
+ });
151
+ child.on('close', (code, signal) => {
152
+ clearInterval(heartbeat);
153
+ const stderr = Buffer.concat(errBuf).toString('utf-8');
154
+ const status = code === 0
155
+ ? 'success'
156
+ : signal === 'SIGTERM' || signal === 'SIGKILL'
157
+ ? 'timeout'
158
+ : 'failure';
159
+ onEvent?.({
160
+ type: 'completed',
161
+ timestamp: new Date().toISOString(),
162
+ status,
163
+ });
164
+ resolve({
165
+ status,
166
+ exitCode: code ?? -1,
167
+ costUsd,
168
+ inputTokens,
169
+ outputTokens,
170
+ artifactPaths: [],
171
+ outputText: resultText,
172
+ errorDetail: status === 'success' ? undefined : stderr.slice(-500) || undefined,
173
+ });
174
+ });
175
+ child.stdin?.write(input.prompt);
176
+ child.stdin?.end();
177
+ });
178
+ }
179
+ function parseDurationMs(duration) {
180
+ if (!duration)
181
+ return null;
182
+ const m = duration.match(/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/);
183
+ if (!m)
184
+ return null;
185
+ const days = Number.parseInt(m[1] ?? '0', 10);
186
+ const hours = Number.parseInt(m[2] ?? '0', 10);
187
+ const minutes = Number.parseInt(m[3] ?? '0', 10);
188
+ const seconds = Number.parseInt(m[4] ?? '0', 10);
189
+ return (((days * 24 + hours) * 60 + minutes) * 60 + seconds) * 1000;
190
+ }
191
+ //# sourceMappingURL=claude-code.js.map