@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
package/dist/execute.js CHANGED
@@ -8,10 +8,12 @@
8
8
  import { createGitHubSourceControl, routeByComplexity, evaluatePromotion, evaluateComplexity, selectModel, withSpan, getMeter, SPAN_NAMES, METRIC_NAMES, ATTRIBUTE_KEYS, } from '@ai-sdlc/reference';
9
9
  import { loadConfigAsync } from './config.js';
10
10
  import { validateIssue, validateIssueWithExtensions, parseComplexity } from './validate-issue.js';
11
+ import { validateAgentOutput } from './validate-agent-output.js';
11
12
  import { createLogger } from './logger.js';
12
13
  import { createStructuredConsoleLogger, createStructuredBufferLogger, } from './structured-logger.js';
13
14
  import { ClaudeCodeRunner } from './runners/claude-code.js';
14
- import { execFileAsync, getGitHubConfig, resolveRepoRoot, createDefaultAuditLog, resolveAutonomyLevel, resolveConstraints, isAutonomousStrategy, recordMetric, validateAndAuditOutput, evaluatePipelineCompliance, authorizeFilesChanged, interpolateBranchPattern, interpolatePRTitle, issueIdToNumber, formatIssueRef, } from './shared.js';
15
+ import { execFileAsync, getGitHubConfig, resolveRepoRoot, createDefaultAuditLog, resolveAutonomyLevel, resolveConstraints, isAutonomousStrategy, recordMetric, evaluatePipelineCompliance, authorizeFilesChanged, interpolateBranchPattern, interpolatePRTitle, issueIdToNumber, formatIssueRef, buildIssueTemplateVars, } from './shared.js';
16
+ import { cleanGitEnv } from './runtime/git-env.js';
15
17
  import { checkKillSwitch, issueAgentCredentials, revokeAgentCredentials, classifyAndSubmitApproval, createPipelineSecurity, } from './security.js';
16
18
  import { createPipelineOrchestration, executePipelineOrchestration, validatePipelineHandoffs, } from './orchestration.js';
17
19
  import { createPipelineProvenance, attachProvenanceToPR, validatePipelineProvenance, } from './provenance.js';
@@ -22,9 +24,9 @@ import { verifyAuditIntegrity, createFileAuditLog, loadAuditEntries, computeAudi
22
24
  import { renderTemplate } from './notifications.js';
23
25
  import { admitIssueResource, createPipelineAdmission, } from './admission.js';
24
26
  import { createPipelineAdapterRegistry, createPipelineWebhookBridge, resolveAdapterFromGit, scanPipelineAdapters, resolveIssueTrackerFromConfig, } from './adapters.js';
25
- import { existsSync, readFileSync, appendFileSync } from 'node:fs';
27
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
26
28
  import { join } from 'node:path';
27
- import { defaultSandboxConstraints, DEFAULT_CONFIG_DIR_NAME, DEFAULT_PR_FOOTER, NOTIFICATION_TITLES, } from './defaults.js';
29
+ import { defaultSandboxConstraints, DEFAULT_CONFIG_DIR_NAME, DEFAULT_PR_FOOTER, DEFAULT_LINT_COMMAND, DEFAULT_FORMAT_COMMAND, DEFAULT_COMMIT_MESSAGE_TEMPLATE, DEFAULT_COMMIT_CO_AUTHOR, NOTIFICATION_TITLES, } from './defaults.js';
28
30
  import { checkFrameworkCompliance, getControlCatalog, getFrameworkMappings, listSupportedFrameworks, } from './compliance-extended.js';
29
31
  import { createSilentLogger, withPipelineSpanSync, getPipelineTracer, validateResourceSchema, } from './telemetry-extended.js';
30
32
  import { createPipelineMemory } from './shared.js';
@@ -32,6 +34,7 @@ import { hasResourceChanged, fingerprintResource } from './reconcilers.js';
32
34
  import { CostTracker } from './cost-tracker.js';
33
35
  import { enrichAgentContext } from './context-enrichment.js';
34
36
  import { reportGateCheckRuns } from './check-runs.js';
37
+ import { WorktreePoolManager, readParallelismMode } from './runtime/index.js';
35
38
  /**
36
39
  * Execute the full AI-SDLC pipeline for a given issue ID.
37
40
  */
@@ -42,10 +45,29 @@ export async function executePipeline(issueId, options = {}) {
42
45
  (options.useStructuredLogger ? createStructuredConsoleLogger() : createLogger());
43
46
  const auditLog = options.auditLog ?? createDefaultAuditLog(workDir);
44
47
  const metricStore = options.metricStore;
48
+ // RFC-0010 §6/§7 Phase 2: parallelism opt-in. When AI_SDLC_PARALLELISM is unset (default),
49
+ // execution proceeds serially exactly as today. When set to 'experimental' or 'on', a
50
+ // WorktreePoolManager is instantiated for the worker-pool dispatcher landing in Phase 3
51
+ // (RFC-0010 §9). For now the manager is constructed but not yet routed through — Phase 2
52
+ // ships the wire-in surface; Phase 3 wires the worker pool to consume it.
53
+ const parallelismMode = readParallelismMode();
54
+ let worktreePool;
55
+ if (parallelismMode !== 'off') {
56
+ worktreePool = new WorktreePoolManager(workDir);
57
+ log.info(`[parallelism] mode=${parallelismMode}, pool root=${worktreePool.rootDir}`);
58
+ }
59
+ void worktreePool; // referenced by Phase 3 dispatcher; silences unused-var lint until then
45
60
  // 1. Load .ai-sdlc/ config (async: includes adapter scanning + manifest distribution)
46
61
  log.stage('load-config');
47
62
  const config = await loadConfigAsync(configDir);
48
63
  log.stageEnd('load-config');
64
+ // If the caller (e.g., cli-watch's pipeline dispatcher) selected a specific
65
+ // Pipeline resource, override the on-disk default. Without this, multi-
66
+ // pipeline configs (RFC-0010 dual workflow: pipeline.yaml + pipeline-backlog.yaml)
67
+ // always fall back to pipeline.yaml because loadConfig prefers it as canonical.
68
+ if (options.pipelineOverride) {
69
+ config.pipeline = options.pipelineOverride;
70
+ }
49
71
  // Kill switch check (before any work)
50
72
  if (options.security) {
51
73
  await checkKillSwitch(options.security);
@@ -101,6 +123,12 @@ export async function executePipeline(issueId, options = {}) {
101
123
  description: issue.description,
102
124
  });
103
125
  }
126
+ // Capture the user's current HEAD so we can restore it after the pipeline.
127
+ // Without this, `git checkout <issue-branch>` inside the pipeline body
128
+ // leaves the user's working tree on the issue branch even after the run
129
+ // completes (the AISDLC-68 incident). Long-term fix is a worktree pool
130
+ // (RFC-0010 §7); this is the pragmatic save+restore.
131
+ const originalHead = await captureCurrentBranch(workDir);
104
132
  // Wrap pipeline body in try/catch to record failure episodes
105
133
  try {
106
134
  return await executePipelineBody(issueId, issue, config, qualityGate, agentRole, autonomyPolicy, tracker, sc, auditLog, metricStore, options, log, workDir);
@@ -121,10 +149,155 @@ export async function executePipeline(issueId, options = {}) {
121
149
  }
122
150
  throw err;
123
151
  }
152
+ finally {
153
+ await restoreOriginalBranch(workDir, originalHead, log);
154
+ }
155
+ }
156
+ /**
157
+ * Capture the current branch name (or commit SHA in detached-HEAD state) so the
158
+ * pipeline can restore the user's worktree after the run. Returns null on git
159
+ * failure — callers treat null as "don't try to restore".
160
+ *
161
+ * @internal — exported for unit tests.
162
+ */
163
+ export async function captureCurrentBranch(workDir) {
164
+ // cleanGitEnv() strips GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE so git
165
+ // resolves against `workDir`'s own .git rather than whatever a parent
166
+ // process (e.g. husky pre-push hook running in another worktree) leaked
167
+ // into the env (AISDLC-72).
168
+ try {
169
+ const { stdout } = await execFileAsync('git', ['symbolic-ref', '--short', 'HEAD'], {
170
+ cwd: workDir,
171
+ env: cleanGitEnv(),
172
+ });
173
+ return stdout.trim();
174
+ }
175
+ catch {
176
+ // Detached HEAD or no git repo. Try the SHA fallback.
177
+ try {
178
+ const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
179
+ cwd: workDir,
180
+ env: cleanGitEnv(),
181
+ });
182
+ return stdout.trim();
183
+ }
184
+ catch {
185
+ return null;
186
+ }
187
+ }
188
+ }
189
+ /**
190
+ * Restore the user's original HEAD if the pipeline switched branches AND the
191
+ * working tree is clean. If the working tree has uncommitted changes (from a
192
+ * partial pipeline run), log a warning and leave HEAD where it is — better to
193
+ * preserve the pipeline's state than to clobber whatever the user has.
194
+ *
195
+ * @internal — exported for unit tests.
196
+ */
197
+ export async function restoreOriginalBranch(workDir, originalHead, log) {
198
+ if (!originalHead)
199
+ return;
200
+ try {
201
+ const current = await captureCurrentBranch(workDir);
202
+ if (current === originalHead)
203
+ return;
204
+ // Refuse to checkout if the worktree has uncommitted changes — git would
205
+ // either block the checkout or carry the changes across, both bad.
206
+ // cleanGitEnv() prevents leaked GIT_DIR from corrupting these calls (AISDLC-72).
207
+ const { stdout } = await execFileAsync('git', ['status', '--porcelain'], {
208
+ cwd: workDir,
209
+ env: cleanGitEnv(),
210
+ });
211
+ if (stdout.trim().length > 0) {
212
+ log?.info(`[pipeline] worktree dirty after pipeline; HEAD left at \`${current}\`. ` +
213
+ `To restore: \`git stash && git checkout ${originalHead}\``);
214
+ return;
215
+ }
216
+ await execFileAsync('git', ['checkout', originalHead], {
217
+ cwd: workDir,
218
+ env: cleanGitEnv(),
219
+ });
220
+ }
221
+ catch (err) {
222
+ log?.info(`[pipeline] failed to restore HEAD to \`${originalHead}\`: ${err.message}`);
223
+ }
224
+ }
225
+ /**
226
+ * Push a branch to origin, rebasing onto the remote tip if it has advanced.
227
+ * Pre-AISDLC-68-rerun the pipeline did a plain `git push` and rejected with
228
+ * non-fast-forward when the remote branch had drifted (e.g. a previous run's
229
+ * commits or a manual edit). The fix: try push; on non-fast-forward, fetch +
230
+ * rebase HEAD onto origin/<branch>, retry once. Rebase conflicts surface as
231
+ * the original error so the operator can resolve manually.
232
+ *
233
+ * @internal — exported for unit tests.
234
+ */
235
+ export async function pushBranchWithRebase(workDir, branchName, log) {
236
+ // cleanGitEnv() ensures these git calls bind to `workDir`'s own .git, not
237
+ // a leaked GIT_DIR from a parent process (AISDLC-72).
238
+ const env = cleanGitEnv();
239
+ try {
240
+ await execFileAsync('git', ['push', 'origin', branchName], { cwd: workDir, env });
241
+ return;
242
+ }
243
+ catch (err) {
244
+ const stderr = err.stderr ?? err.message;
245
+ if (!/non-fast-forward|rejected/i.test(stderr)) {
246
+ throw err;
247
+ }
248
+ log?.info(`[pipeline] push rejected (non-fast-forward); rebasing onto origin/${branchName}`);
249
+ }
250
+ // Fetch + rebase + retry. If the rebase fails, surface a clear error.
251
+ try {
252
+ await execFileAsync('git', ['fetch', 'origin', branchName], { cwd: workDir, env });
253
+ await execFileAsync('git', ['rebase', `origin/${branchName}`], { cwd: workDir, env });
254
+ }
255
+ catch (rebaseErr) {
256
+ // Abort the partial rebase so the worktree isn't left in a half-merged state.
257
+ try {
258
+ await execFileAsync('git', ['rebase', '--abort'], { cwd: workDir, env });
259
+ }
260
+ catch {
261
+ /* nothing to abort */
262
+ }
263
+ throw new Error(`Push rebase failed for branch ${branchName}: ${rebaseErr.message}. ` +
264
+ `Resolve conflicts manually and re-run the pipeline.`);
265
+ }
266
+ await execFileAsync('git', ['push', 'origin', branchName], { cwd: workDir, env });
124
267
  }
125
268
  async function executePipelineBody(issueId, issue, config, qualityGate, agentRole, autonomyPolicy, tracker, sc, auditLog, metricStore, options, log, workDir) {
126
269
  const issueNumber = issueIdToNumber(issueId);
127
270
  const meter = getMeter();
271
+ // Slack notifications (optional — activates when SLACK_BOT_TOKEN is set)
272
+ let slackThreadId;
273
+ const slackToken = process.env.SLACK_BOT_TOKEN;
274
+ const slackChannel = process.env.SLACK_CHANNEL;
275
+ const notifySlack = async (message) => {
276
+ if (!slackToken || !slackChannel)
277
+ return;
278
+ try {
279
+ const body = { channel: slackChannel, text: message };
280
+ if (slackThreadId)
281
+ body.thread_ts = slackThreadId;
282
+ const res = await fetch('https://slack.com/api/chat.postMessage', {
283
+ method: 'POST',
284
+ headers: {
285
+ 'Content-Type': 'application/json',
286
+ Authorization: `Bearer ${slackToken}`,
287
+ },
288
+ body: JSON.stringify(body),
289
+ });
290
+ const data = (await res.json());
291
+ // Capture the first message's timestamp as thread ID
292
+ if (data.ok && !slackThreadId && data.ts) {
293
+ slackThreadId = data.ts;
294
+ }
295
+ }
296
+ catch {
297
+ // Best-effort — don't fail the pipeline for Slack
298
+ }
299
+ };
300
+ await notifySlack(`:rocket: *Pipeline started* for <https://github.com/${process.env.GITHUB_REPOSITORY ?? ''}/issues/${issueId}|#${issueId}: ${issue.title}>`);
128
301
  // Discovery: register agent and resolve by issue labels
129
302
  const discovery = createPipelineDiscovery();
130
303
  discovery.register(agentRole);
@@ -231,11 +404,15 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
231
404
  const currentLevel = resolveAutonomyLevel(autonomyPolicy);
232
405
  log.stageEnd('validate-issue');
233
406
  // 7. Create branch and checkout locally (read pattern from pipeline config)
234
- const branchVars = { issueNumber: issueId, issueTitle: issue.title };
407
+ const branchVars = buildIssueTemplateVars(issueId, issue.title);
235
408
  const branchName = interpolateBranchPattern(config.pipeline?.spec.branching?.pattern, branchVars);
236
409
  await sc.createBranch({ name: branchName });
237
- await execFileAsync('git', ['fetch', 'origin', branchName], { cwd: workDir });
238
- await execFileAsync('git', ['checkout', branchName], { cwd: workDir });
410
+ // cleanGitEnv() prevents leaked GIT_DIR from corrupting these calls (AISDLC-72).
411
+ await execFileAsync('git', ['fetch', 'origin', branchName], {
412
+ cwd: workDir,
413
+ env: cleanGitEnv(),
414
+ });
415
+ await execFileAsync('git', ['checkout', branchName], { cwd: workDir, env: cleanGitEnv() });
239
416
  // 8. Resolve agent constraints
240
417
  const resolved = resolveConstraints(agentRole.spec.constraints, currentLevel);
241
418
  // 8b. Model selection: choose model based on complexity and budget pressure
@@ -256,7 +433,26 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
256
433
  ensureRuntimeGitignore(workDir);
257
434
  // 9. Invoke agent (with sandbox + JIT credential lifecycle when security is provided)
258
435
  log.stage('agent');
436
+ // Post progress comment so users can see the pipeline is working
437
+ await tracker.addComment(issueId, `## AI-SDLC: Agent Started\n\n` +
438
+ `The AI agent is now working on this issue on branch \`${branchName}\`.\n\n` +
439
+ `| Detail | Value |\n|---|---|\n` +
440
+ `| Model | ${selectedModel ?? 'default'} |\n` +
441
+ `| Complexity | ${complexity} |\n` +
442
+ `| Strategy | ${strategy} |\n`);
443
+ await notifySlack(`:hammer_and_wrench: Agent working on \`${branchName}\` (model: ${selectedModel ?? 'default'}, complexity: ${complexity})`);
259
444
  const runner = options.runner ?? new ClaudeCodeRunner();
445
+ // Set up progress tracking — collects streaming events for the activity log
446
+ const progressLog = [];
447
+ const onProgress = (event) => {
448
+ const timestamp = new Date().toISOString().slice(11, 19);
449
+ if (event.type === 'tool_start' && event.tool) {
450
+ progressLog.push(`[${timestamp}] ${event.tool}${event.file ? `: ${event.file}` : ''}`);
451
+ }
452
+ else if (event.type === 'cost') {
453
+ progressLog.push(`[${timestamp}] Cost: $${event.costUsd?.toFixed(4)}`);
454
+ }
455
+ };
260
456
  // Sandbox isolation around agent execution
261
457
  const codeStage = config.pipeline?.spec.stages.find((s) => s.name === 'code');
262
458
  let sandboxId;
@@ -304,6 +500,12 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
304
500
  codebaseContext: options.codebaseContext,
305
501
  episodicContext,
306
502
  sandboxId,
503
+ lintCommand: DEFAULT_LINT_COMMAND,
504
+ formatCommand: DEFAULT_FORMAT_COMMAND,
505
+ typecheckCommand: process.env.AI_SDLC_TYPECHECK_COMMAND,
506
+ commitMessageTemplate: DEFAULT_COMMIT_MESSAGE_TEMPLATE,
507
+ commitCoAuthor: DEFAULT_COMMIT_CO_AUTHOR,
508
+ onProgress,
307
509
  }));
308
510
  });
309
511
  // Extract the AgentResult from orchestration output
@@ -359,11 +561,35 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
359
561
  if (currentLevel.permissions.write.length > 0) {
360
562
  authorizeFilesChanged(result.filesChanged, currentLevel.permissions, agentRole.spec.constraints, auditLog, agentRole.metadata.name);
361
563
  }
362
- // 11. Validate agent output against guardrails
363
- await withSpan(SPAN_NAMES.PIPELINE_STAGE, {
564
+ // 11. Post activity log so users can see what the agent did
565
+ const activitySection = progressLog.length > 0
566
+ ? `### Activity Log\n\n\`\`\`\n${progressLog.slice(-40).join('\n')}\n\`\`\``
567
+ : '';
568
+ await tracker.addComment(issueId, `## AI-SDLC: Agent Complete\n\n` +
569
+ `**${result.filesChanged.length} files changed.** Pushing branch...\n\n` +
570
+ activitySection);
571
+ await notifySlack(`:white_check_mark: Agent complete — ${result.filesChanged.length} files changed. Pushing...`);
572
+ // 12. Push branch (before validation to preserve work even if validation fails).
573
+ // Fetch first + rebase if the remote has diverged so push fast-forwards
574
+ // cleanly. Without this, re-runs against an existing remote branch (e.g. when
575
+ // the issue branch was updated by another pipeline run or by a hand-edit)
576
+ // reject with non-fast-forward and the agent's work is stranded locally.
577
+ log.stage('push');
578
+ await pushBranchWithRebase(workDir, branchName, log);
579
+ log.stageEnd('push');
580
+ auditLog.record({
581
+ actor: 'system',
582
+ action: 'push',
583
+ resource: `branch/${branchName}`,
584
+ decision: 'allowed',
585
+ details: { filesChanged: result.filesChanged.length, issueId },
586
+ });
587
+ // 13. Validate agent output against guardrails (after push)
588
+ const validation = await withSpan(SPAN_NAMES.PIPELINE_STAGE, {
364
589
  [ATTRIBUTE_KEYS.STAGE]: 'validate-output',
365
590
  }, async () => {
366
- await validateAndAuditOutput({
591
+ log.stage('validate-output');
592
+ const validationResult = await validateAgentOutput({
367
593
  filesChanged: result.filesChanged,
368
594
  workDir,
369
595
  constraints: {
@@ -372,18 +598,54 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
372
598
  blockedPaths: resolved.blockedPaths,
373
599
  },
374
600
  guardrails: { maxLinesPerPR: currentLevel.guardrails.maxLinesPerPR },
375
- auditLog,
376
- log,
377
- onViolation: async (violationList) => {
378
- await tracker.addComment(issueId, `## ${NOTIFICATION_TITLES.guardrailViolations}\n\n${violationList}`);
601
+ });
602
+ log.stageEnd('validate-output');
603
+ return validationResult;
604
+ });
605
+ if (!validation.passed) {
606
+ // One-line summary suitable for log lines + thrown error message.
607
+ const violationSummary = validation.violations.map((v) => `${v.rule}: ${v.message}`).join('; ');
608
+ // Record validation failure in audit log with full violation detail (rule
609
+ // + message), not just the rule names — the rule name alone doesn't tell
610
+ // the operator what to fix.
611
+ auditLog.record({
612
+ actor: 'system',
613
+ action: 'check',
614
+ resource: 'agent-output',
615
+ decision: 'denied',
616
+ details: {
617
+ violations: validation.violations.map((v) => ({ rule: v.rule, message: v.message })),
379
618
  },
380
619
  });
620
+ // Surface the rejection detail to the structured log so the operator sees
621
+ // it in stderr immediately. Without this the only signal was the
622
+ // (detail-free) thrown error message.
623
+ log.error(`[validate-output] rejected: ${violationSummary}`);
624
+ // Post comment explaining the violations
625
+ const violationList = validation.violations
626
+ .map((v) => `- **${v.rule}**: ${v.message}`)
627
+ .join('\n');
628
+ await tracker.addComment(issueId, `## ${NOTIFICATION_TITLES.guardrailViolations}\n\n${violationList}\n\n` +
629
+ `The branch \`${branchName}\` has been pushed with your changes. You can review the work, ` +
630
+ `cherry-pick valid changes, or adjust the guardrails as needed.`);
631
+ // Exit without creating PR. Include the violation summary in the error
632
+ // message so cli-watch and downstream loggers see the actual rejection.
633
+ throw new Error(`Agent output failed guardrail validation: ${violationSummary}. ` +
634
+ `Branch ${branchName} preserved for review.`);
635
+ }
636
+ // Validation passed — record success
637
+ auditLog.record({
638
+ actor: 'system',
639
+ action: 'check',
640
+ resource: 'agent-output',
641
+ decision: 'allowed',
381
642
  });
382
- // 11b. Evaluate quality gates and report as GitHub Check Runs
643
+ // 13b. Evaluate quality gates and report as GitHub Check Runs
383
644
  if (qualityGate.spec.gates.length > 0) {
384
645
  try {
385
646
  const { stdout: headSha } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
386
647
  cwd: workDir,
648
+ env: cleanGitEnv(),
387
649
  });
388
650
  const gateResults = qualityGate.spec.gates.map((gate) => {
389
651
  const evalResult = evaluatePipelineGate(gate, {
@@ -416,10 +678,11 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
416
678
  log.info('Post-agent gate evaluation skipped');
417
679
  }
418
680
  }
419
- // 11c. Post-agent complexity evaluation (non-blocking)
681
+ // 13c. Post-agent complexity evaluation (non-blocking)
420
682
  try {
421
683
  const { stdout: diffStat } = await execFileAsync('git', ['diff', '--stat', 'HEAD~1'], {
422
684
  cwd: workDir,
685
+ env: cleanGitEnv(),
423
686
  });
424
687
  const insertMatch = diffStat.match(/(\d+) insertions?\(\+\)/);
425
688
  const deleteMatch = diffStat.match(/(\d+) deletions?\(-\)/);
@@ -445,11 +708,7 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
445
708
  catch {
446
709
  log.info('Post-agent complexity evaluation skipped');
447
710
  }
448
- // 12. Push branch
449
- log.stage('push');
450
- await execFileAsync('git', ['push', 'origin', branchName], { cwd: workDir });
451
- log.stageEnd('push');
452
- // 12b. Compute cost receipt for provenance (before PR creation)
711
+ // 14. Compute cost receipt for provenance (before PR creation)
453
712
  let costReceipt;
454
713
  if (result.tokenUsage) {
455
714
  const tu = result.tokenUsage;
@@ -467,7 +726,7 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
467
726
  },
468
727
  };
469
728
  }
470
- // 13. Create PR (with optional provenance, reading config from pipeline)
729
+ // 15. Create PR (with optional provenance, reading config from pipeline)
471
730
  const prConfig = config.pipeline?.spec.pullRequest;
472
731
  const shouldIncludeProvenance = options.includeProvenance ?? prConfig?.includeProvenance ?? options.security !== undefined;
473
732
  let provenanceBlock = '';
@@ -483,7 +742,7 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
483
742
  }
484
743
  provenanceBlock = '\n\n' + provenanceText;
485
744
  }
486
- const prVars = { issueNumber: issueId, issueTitle: issue.title };
745
+ const prVars = buildIssueTemplateVars(issueId, issue.title);
487
746
  const prTitle = interpolatePRTitle(prConfig?.titleTemplate, prVars);
488
747
  const closeKeyword = prConfig?.closeKeyword ?? 'Closes';
489
748
  const targetBranch = config.pipeline?.spec.branching?.targetBranch ?? 'main';
@@ -531,6 +790,7 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
531
790
  })
532
791
  : { title: NOTIFICATION_TITLES.prCreated, body: `Pull request created: ${pr.url}` };
533
792
  await tracker.addComment(issueId, `## ${prCreatedComment.title}\n\n${prCreatedComment.body}`);
793
+ await notifySlack(`:pull_request: PR created: ${pr.url}`);
534
794
  // 14b. Record cost from agent result
535
795
  if (options.costTracker && result.tokenUsage) {
536
796
  const tu = result.tokenUsage;
@@ -769,25 +1029,30 @@ function runPipelineDiagnostics(input) {
769
1029
  }
770
1030
  }
771
1031
  // ── Gitignore helper ─────────────────────────────────────────────────
772
- const RUNTIME_GITIGNORE_ENTRIES = [
773
- '# AI-SDLC runtime artifacts',
774
- '.ai-sdlc/state.db',
775
- '.ai-sdlc/state/',
776
- '.ai-sdlc/audit.jsonl',
777
- ];
1032
+ const RUNTIME_GITIGNORE_PATHS = ['.ai-sdlc/state.db', '.ai-sdlc/state/', '.ai-sdlc/audit.jsonl'];
778
1033
  /**
779
1034
  * Ensure .gitignore in the working directory covers AI-SDLC runtime artifacts.
780
1035
  * Without this the agent sees untracked runtime files and appends duplicate
781
1036
  * gitignore entries on every run.
1037
+ *
1038
+ * Only checks path entries (not the comment header) to avoid false mismatches.
1039
+ * Writes the block once with any missing paths.
782
1040
  */
783
1041
  function ensureRuntimeGitignore(workDir) {
784
1042
  try {
785
1043
  const gitignorePath = join(workDir, '.gitignore');
786
1044
  const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf-8') : '';
787
- const missing = RUNTIME_GITIGNORE_ENTRIES.filter((entry) => !existing.includes(entry));
1045
+ const SENTINEL = '# ai-sdlc:runtime-gitignore';
1046
+ if (existing.includes(SENTINEL))
1047
+ return;
1048
+ const missing = RUNTIME_GITIGNORE_PATHS.filter((entry) => !existing.split('\n').some((line) => line.trim() === entry));
788
1049
  if (missing.length === 0)
789
1050
  return;
790
- appendFileSync(gitignorePath, '\n' + missing.join('\n') + '\n', 'utf-8');
1051
+ // Write atomically (writeFileSync, not appendFileSync) to avoid race conditions
1052
+ // when parallel test processes both read before either writes.
1053
+ const block = `${SENTINEL}\n` + missing.join('\n') + '\n';
1054
+ const newContent = existing.length > 0 ? existing.trimEnd() + '\n' + block : block;
1055
+ writeFileSync(gitignorePath, newContent, 'utf-8');
791
1056
  }
792
1057
  catch {
793
1058
  // Best-effort — workDir may not exist yet in tests or dry-run scenarios
package/dist/fix-ci.js CHANGED
@@ -13,6 +13,8 @@ import { renderTemplate } from './notifications.js';
13
13
  import { parseDuration } from './policy-evaluators.js';
14
14
  import { checkKillSwitch, issueAgentCredentials, revokeAgentCredentials, } from './security.js';
15
15
  import { DEFAULT_MAX_FIX_ATTEMPTS, DEFAULT_MAX_LOG_LINES, 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';
16
18
  export const MAX_FIX_ATTEMPTS = DEFAULT_MAX_FIX_ATTEMPTS;
17
19
  export const MAX_LOG_LINES = DEFAULT_MAX_LOG_LINES;
18
20
  export const RETRY_MARKER = '<!-- ai-sdlc-fix-ci-attempt -->';
@@ -107,6 +109,8 @@ export async function executeFixCI(prNumber, runId, options = {}) {
107
109
  }
108
110
  // Tracker is available if injected directly or if we're not in test mode
109
111
  const trackerAvailable = !!options.tracker || options._prComments === undefined;
112
+ // Create cycle detector (marker generated after all guard conditions pass)
113
+ const cycleDetector = createCycleDetectorFromConfig(config.pipeline?.spec ?? {});
110
114
  // 2. Count retry attempts (via injected comments or IssueTracker)
111
115
  log.stage('check-retries');
112
116
  let comments;
@@ -126,6 +130,27 @@ export async function executeFixCI(prNumber, runId, options = {}) {
126
130
  await getTracker().addComment(String(prNumber), body);
127
131
  }
128
132
  };
133
+ // Check for pipeline-level cycles AFTER retry counting
134
+ // (so legitimate retries under the limit aren't blocked)
135
+ if (trackerAvailable) {
136
+ const cycleCheck = await checkAndHandleCycle({
137
+ issueOrPrId: String(prNumber),
138
+ stage: 'fix-ci',
139
+ tracker: getTracker(),
140
+ detector: cycleDetector,
141
+ });
142
+ if (cycleCheck.cycleDetected) {
143
+ log.info('Pipeline cycle detected. Halting fix-ci execution.');
144
+ auditLog.record({
145
+ actor: 'system',
146
+ action: 'evaluate',
147
+ resource: `pr#${prNumber}`,
148
+ decision: 'denied',
149
+ details: { reason: 'pipeline-cycle-detected' },
150
+ });
151
+ return;
152
+ }
153
+ }
129
154
  if (attempts >= maxFixAttempts) {
130
155
  log.info(`Fix-CI retry limit reached (${maxFixAttempts}). Commenting and stopping.`);
131
156
  auditLog.record({
@@ -148,13 +173,18 @@ export async function executeFixCI(prNumber, runId, options = {}) {
148
173
  await addComment(`## ${limitComment.title}\n\n${limitComment.body}`);
149
174
  return;
150
175
  }
176
+ // Generate cycle marker AFTER all guard conditions (retry limit + cycle detection)
177
+ // so it's only recorded when we actually proceed with execution
178
+ const cycleMarker = cycleDetector.recordInvocation('fix-ci');
151
179
  // 3. Fetch CI logs
152
180
  log.stage('fetch-logs');
153
181
  const ciLogs = await fetchCILogs(runId, options._ciLogs, options.ciAdapter);
154
182
  log.stageEnd('fetch-logs');
155
183
  // 4. Determine branch and issue number
184
+ // cleanGitEnv() prevents leaked GIT_DIR from binding to a parent repo (AISDLC-72).
156
185
  const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], {
157
186
  cwd: workDir,
187
+ env: cleanGitEnv(),
158
188
  });
159
189
  const currentBranch = branchStdout.trim();
160
190
  const issueId = extractIssueId(currentBranch);
@@ -259,7 +289,8 @@ export async function executeFixCI(prNumber, runId, options = {}) {
259
289
  const agentFailComment = agentFailTpl
260
290
  ? renderTemplate(agentFailTpl, { stageName: 'fix-ci', details: errorDetail })
261
291
  : { title: NOTIFICATION_TITLES.fixCIAgentFailed, body: errorDetail };
262
- await addComment(`## ${agentFailComment.title}\n\n${agentFailComment.body}\n\n${RETRY_MARKER}`);
292
+ // Use pre-created marker (one per execution, prevents double-counting)
293
+ await addComment(`## ${agentFailComment.title}\n\n${agentFailComment.body}\n\n${RETRY_MARKER}\n${cycleMarker}`);
263
294
  throw new Error(`Fix-CI agent failed on PR #${prNumber}: ${r.error}`);
264
295
  }
265
296
  log.stageEnd('agent');
@@ -308,13 +339,17 @@ export async function executeFixCI(prNumber, runId, options = {}) {
308
339
  auditLog,
309
340
  log,
310
341
  onViolation: async (violationList) => {
311
- await addComment(`## ${NOTIFICATION_TITLES.fixCIGuardrailViolations}\n\n${violationList}\n\n${RETRY_MARKER}`);
342
+ await addComment(`## ${NOTIFICATION_TITLES.fixCIGuardrailViolations}\n\n${violationList}\n\n${RETRY_MARKER}\n${cycleMarker}`);
312
343
  },
313
344
  });
314
345
  });
315
346
  // 10. Push to the same branch (CI re-runs automatically)
347
+ // cleanGitEnv() prevents leaked GIT_DIR from binding to a parent repo (AISDLC-72).
316
348
  log.stage('push');
317
- await execFileAsync('git', ['push', 'origin', currentBranch], { cwd: workDir });
349
+ await execFileAsync('git', ['push', 'origin', currentBranch], {
350
+ cwd: workDir,
351
+ env: cleanGitEnv(),
352
+ });
318
353
  log.stageEnd('push');
319
354
  auditLog.record({
320
355
  actor: 'system',
@@ -335,6 +370,7 @@ export async function executeFixCI(prNumber, runId, options = {}) {
335
370
  title: NOTIFICATION_TITLES.fixCIApplied,
336
371
  body: `Attempt ${attempts + 1} of ${maxFixAttempts} — pushed fixes to \`${currentBranch}\`.`,
337
372
  };
373
+ // Use pre-created marker (one per execution, prevents double-counting)
338
374
  await addComment([
339
375
  `## ${successComment.title}`,
340
376
  '',
@@ -344,6 +380,7 @@ export async function executeFixCI(prNumber, runId, options = {}) {
344
380
  result.filesChanged.map((f) => `- \`${f}\``).join('\n'),
345
381
  '',
346
382
  RETRY_MARKER,
383
+ cycleMarker,
347
384
  ].join('\n'));
348
385
  // 12. Record episodic memory (success)
349
386
  if (options.memory) {
@@ -0,0 +1,66 @@
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 { type IssueTracker, type AuditLog, type MetricStore, type AgentMemory, type SecretStore } from '@ai-sdlc/reference';
7
+ import { type Logger } from './logger.js';
8
+ import type { AgentRunner } from './runners/types.js';
9
+ import { type SecurityContext } from './security.js';
10
+ export declare const MAX_REVIEW_FIX_ATTEMPTS = 2;
11
+ export declare const RETRY_MARKER = "<!-- ai-sdlc-fix-review-attempt -->";
12
+ export interface FixReviewOptions {
13
+ /** Override the config directory (defaults to `.ai-sdlc`). */
14
+ configDir?: string;
15
+ /** Override the working directory (defaults to repo root). */
16
+ workDir?: string;
17
+ /** Inject a custom runner (for testing). */
18
+ runner?: AgentRunner;
19
+ /** Inject a custom logger (for testing). */
20
+ logger?: Logger;
21
+ /** Inject PR comments for testing (bypasses IssueTracker call). */
22
+ _prComments?: string[];
23
+ /** Inject review findings for testing (bypasses GitHub API call). */
24
+ _reviewFindings?: string;
25
+ /** Inject a custom audit log (for testing). */
26
+ auditLog?: AuditLog;
27
+ /** Inject a custom issue tracker (for testing). */
28
+ tracker?: IssueTracker;
29
+ /** In-process metric store for testable telemetry. */
30
+ metricStore?: MetricStore;
31
+ /** Agent memory for episodic recall. */
32
+ memory?: AgentMemory;
33
+ /** Security context for kill switch and JIT credentials. */
34
+ security?: SecurityContext;
35
+ /** Use the reference structured logger instead of the plain console logger. */
36
+ useStructuredLogger?: boolean;
37
+ /** Secret store adapter for resolving credentials (defaults to process.env). */
38
+ secretStore?: SecretStore;
39
+ }
40
+ /**
41
+ * Count how many fix-review retry attempts have been made on a PR
42
+ * by scanning comments for the hidden retry marker.
43
+ */
44
+ export declare function countRetryAttempts(comments: string[]): number;
45
+ /**
46
+ * Validate that a PR number is a positive integer.
47
+ */
48
+ export declare function validatePrNumber(prNumber: number): void;
49
+ /**
50
+ * Sanitize a git branch name to prevent command injection.
51
+ * Allows only alphanumeric characters, slashes, dashes, underscores, and dots.
52
+ */
53
+ export declare function sanitizeBranchName(branch: string): string;
54
+ /**
55
+ * Fetch review findings from PR reviews that requested changes.
56
+ * Returns a formatted string with all findings from review agents.
57
+ */
58
+ export declare function fetchReviewFindings(prNumber: number, injectedFindings?: string, _secretStore?: SecretStore): Promise<string>;
59
+ /**
60
+ * Execute the fix-review pipeline for a PR with review findings.
61
+ *
62
+ * Returns gracefully (no throw) when the retry limit is reached.
63
+ * Throws on agent failure or guardrail violations.
64
+ */
65
+ export declare function executeFixReview(prNumber: number, options?: FixReviewOptions): Promise<void>;
66
+ //# sourceMappingURL=fix-review.d.ts.map