@ai-sdlc/orchestrator 0.6.0 → 0.10.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 (209) hide show
  1. package/dist/__test-helpers/git-env.d.ts +40 -0
  2. package/dist/__test-helpers/git-env.js +73 -0
  3. package/dist/admission-composite.d.ts +89 -0
  4. package/dist/admission-composite.js +241 -0
  5. package/dist/admission-enrichment.d.ts +142 -0
  6. package/dist/admission-enrichment.js +341 -0
  7. package/dist/admission-hc.d.ts +81 -0
  8. package/dist/admission-hc.js +94 -0
  9. package/dist/admission-score.d.ts +109 -5
  10. package/dist/admission-score.js +90 -11
  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 +74 -0
  28. package/dist/cli/commands/git-remote.js +170 -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-features.d.ts +208 -0
  32. package/dist/cli/commands/init-features.js +473 -0
  33. package/dist/cli/commands/init-templates.d.ts +104 -0
  34. package/dist/cli/commands/init-templates.js +399 -0
  35. package/dist/cli/commands/init.d.ts +48 -0
  36. package/dist/cli/commands/init.js +322 -23
  37. package/dist/cli/commands/mcp-setup.d.ts +31 -0
  38. package/dist/cli/commands/mcp-setup.js +78 -8
  39. package/dist/cli/formatters/table.js +15 -2
  40. package/dist/cli/index.d.ts +14 -1
  41. package/dist/cli/index.js +81 -20
  42. package/dist/cli/versions.d.ts +57 -0
  43. package/dist/cli/versions.js +128 -0
  44. package/dist/code-area-classifier.d.ts +21 -0
  45. package/dist/code-area-classifier.js +48 -0
  46. package/dist/config.d.ts +33 -1
  47. package/dist/config.js +78 -8
  48. package/dist/database/adapters/external.d.ts +24 -0
  49. package/dist/database/adapters/external.js +80 -0
  50. package/dist/database/adapters/neon.d.ts +41 -0
  51. package/dist/database/adapters/neon.js +98 -0
  52. package/dist/database/adapters/pg-snapshot-restore.d.ts +28 -0
  53. package/dist/database/adapters/pg-snapshot-restore.js +68 -0
  54. package/dist/database/adapters/sqlite-copy.d.ts +32 -0
  55. package/dist/database/adapters/sqlite-copy.js +145 -0
  56. package/dist/database/connection-injection.d.ts +35 -0
  57. package/dist/database/connection-injection.js +93 -0
  58. package/dist/database/index.d.ts +12 -0
  59. package/dist/database/index.js +23 -0
  60. package/dist/database/registry.d.ts +13 -0
  61. package/dist/database/registry.js +27 -0
  62. package/dist/database/topology.d.ts +52 -0
  63. package/dist/database/topology.js +44 -0
  64. package/dist/database/types.d.ts +89 -0
  65. package/dist/database/types.js +26 -0
  66. package/dist/design-authority.d.ts +53 -0
  67. package/dist/design-authority.js +84 -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 +188 -15
  92. package/dist/fix-ci.js +8 -1
  93. package/dist/fix-review.js +8 -1
  94. package/dist/harness/adapters/claude-code.d.ts +29 -0
  95. package/dist/harness/adapters/claude-code.js +191 -0
  96. package/dist/harness/adapters/codex.d.ts +25 -0
  97. package/dist/harness/adapters/codex.js +61 -0
  98. package/dist/harness/independence.d.ts +51 -0
  99. package/dist/harness/independence.js +67 -0
  100. package/dist/harness/index.d.ts +14 -0
  101. package/dist/harness/index.js +20 -0
  102. package/dist/harness/registry.d.ts +17 -0
  103. package/dist/harness/registry.js +31 -0
  104. package/dist/harness/types.d.ts +123 -0
  105. package/dist/harness/types.js +8 -0
  106. package/dist/harness/version-probe.d.ts +14 -0
  107. package/dist/harness/version-probe.js +123 -0
  108. package/dist/index.d.ts +30 -4
  109. package/dist/index.js +35 -2
  110. package/dist/models/classifier.d.ts +78 -0
  111. package/dist/models/classifier.js +277 -0
  112. package/dist/models/index.d.ts +3 -0
  113. package/dist/models/index.js +3 -0
  114. package/dist/models/registry.d.ts +97 -0
  115. package/dist/models/registry.js +173 -0
  116. package/dist/pillar-breakdown.d.ts +85 -0
  117. package/dist/pillar-breakdown.js +162 -0
  118. package/dist/priority.d.ts +5 -0
  119. package/dist/priority.js +18 -5
  120. package/dist/review-meta.d.ts +65 -0
  121. package/dist/review-meta.js +149 -0
  122. package/dist/review.d.ts +4 -0
  123. package/dist/review.js +12 -2
  124. package/dist/runners/claude-code-sdk.d.ts +31 -0
  125. package/dist/runners/claude-code-sdk.js +238 -0
  126. package/dist/runners/claude-code.d.ts +3 -0
  127. package/dist/runners/claude-code.js +30 -66
  128. package/dist/runners/codex.js +4 -1
  129. package/dist/runners/copilot.js +4 -1
  130. package/dist/runners/cursor.js +4 -1
  131. package/dist/runners/git-utils.d.ts +81 -0
  132. package/dist/runners/git-utils.js +201 -0
  133. package/dist/runners/index.d.ts +3 -0
  134. package/dist/runners/index.js +3 -0
  135. package/dist/runners/review-agent.d.ts +20 -0
  136. package/dist/runners/review-agent.js +171 -41
  137. package/dist/runners/runner-registry.js +10 -0
  138. package/dist/runners/sdk-review-runner.d.ts +65 -0
  139. package/dist/runners/sdk-review-runner.js +185 -0
  140. package/dist/runners/security-triage.d.ts +20 -4
  141. package/dist/runners/security-triage.js +39 -15
  142. package/dist/runners/types.d.ts +6 -0
  143. package/dist/runtime/attestations.d.ts +766 -0
  144. package/dist/runtime/attestations.js +1195 -0
  145. package/dist/runtime/git-env.d.ts +53 -0
  146. package/dist/runtime/git-env.js +60 -0
  147. package/dist/runtime/index.d.ts +7 -0
  148. package/dist/runtime/index.js +7 -0
  149. package/dist/runtime/parallelism-flag.d.ts +28 -0
  150. package/dist/runtime/parallelism-flag.js +39 -0
  151. package/dist/runtime/port-allocator.d.ts +32 -0
  152. package/dist/runtime/port-allocator.js +96 -0
  153. package/dist/runtime/worktree-pool.d.ts +86 -0
  154. package/dist/runtime/worktree-pool.js +204 -0
  155. package/dist/runtime/worktree.d.ts +25 -0
  156. package/dist/runtime/worktree.js +111 -0
  157. package/dist/sa-scoring/auto-calibrate.d.ts +69 -0
  158. package/dist/sa-scoring/auto-calibrate.js +107 -0
  159. package/dist/sa-scoring/c1-sa2-computable.d.ts +42 -0
  160. package/dist/sa-scoring/c1-sa2-computable.js +59 -0
  161. package/dist/sa-scoring/composite.d.ts +107 -0
  162. package/dist/sa-scoring/composite.js +139 -0
  163. package/dist/sa-scoring/depparse-client.d.ts +79 -0
  164. package/dist/sa-scoring/depparse-client.js +187 -0
  165. package/dist/sa-scoring/did-compiler.d.ts +122 -0
  166. package/dist/sa-scoring/did-compiler.js +286 -0
  167. package/dist/sa-scoring/drift-monitor.d.ts +84 -0
  168. package/dist/sa-scoring/drift-monitor.js +186 -0
  169. package/dist/sa-scoring/exemplar-bank.d.ts +78 -0
  170. package/dist/sa-scoring/exemplar-bank.js +154 -0
  171. package/dist/sa-scoring/feedback-store.d.ts +100 -0
  172. package/dist/sa-scoring/feedback-store.js +156 -0
  173. package/dist/sa-scoring/index.d.ts +71 -0
  174. package/dist/sa-scoring/index.js +158 -0
  175. package/dist/sa-scoring/layer1-deterministic.d.ts +115 -0
  176. package/dist/sa-scoring/layer1-deterministic.js +298 -0
  177. package/dist/sa-scoring/layer2-structural.d.ts +71 -0
  178. package/dist/sa-scoring/layer2-structural.js +151 -0
  179. package/dist/sa-scoring/layer3-llm.d.ts +86 -0
  180. package/dist/sa-scoring/layer3-llm.js +282 -0
  181. package/dist/sa-scoring/rescore-orchestrator.d.ts +52 -0
  182. package/dist/sa-scoring/rescore-orchestrator.js +47 -0
  183. package/dist/scheduling/burn-down.d.ts +27 -0
  184. package/dist/scheduling/burn-down.js +43 -0
  185. package/dist/scheduling/calibration.d.ts +42 -0
  186. package/dist/scheduling/calibration.js +150 -0
  187. package/dist/scheduling/index.d.ts +8 -0
  188. package/dist/scheduling/index.js +8 -0
  189. package/dist/scheduling/ledger.d.ts +59 -0
  190. package/dist/scheduling/ledger.js +216 -0
  191. package/dist/scheduling/off-peak.d.ts +27 -0
  192. package/dist/scheduling/off-peak.js +112 -0
  193. package/dist/scheduling/schedule-decision.d.ts +39 -0
  194. package/dist/scheduling/schedule-decision.js +89 -0
  195. package/dist/scheduling/tier-analysis.d.ts +47 -0
  196. package/dist/scheduling/tier-analysis.js +81 -0
  197. package/dist/scheduling/types.d.ts +140 -0
  198. package/dist/scheduling/types.js +11 -0
  199. package/dist/shared.d.ts +13 -0
  200. package/dist/shared.js +32 -0
  201. package/dist/state/schema.d.ts +5 -1
  202. package/dist/state/schema.js +223 -1
  203. package/dist/state/store.d.ts +55 -1
  204. package/dist/state/store.js +342 -7
  205. package/dist/state/types.d.ts +139 -0
  206. package/dist/types.d.ts +1 -1
  207. package/dist/validate-agent-output.js +4 -1
  208. package/dist/watch.js +6 -0
  209. package/package.json +3 -2
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Bounded-concurrency worker pool per RFC-0010 §9. Accepts a stream of work items in
3
+ * PPA-priority order, dispatches up to `maxConcurrent` simultaneously, and surfaces
4
+ * structured admission/dispatch events.
5
+ */
6
+ /**
7
+ * Process all items with bounded concurrency. Items are dispatched in PPA score order
8
+ * (descending). Items that the admission gate rejects are returned in `deferred` for
9
+ * the caller to requeue or escalate. Failed executions are returned in `failed`; the
10
+ * pool does NOT auto-retry — that's the caller's responsibility (see RFC §9.4).
11
+ */
12
+ export async function runWorkerPool(items, deps) {
13
+ if (deps.maxConcurrent < 1) {
14
+ throw new Error(`maxConcurrent must be >= 1 (got ${deps.maxConcurrent})`);
15
+ }
16
+ const sorted = [...items].sort((a, b) => b.ppaScore - a.ppaScore);
17
+ const result = { succeeded: [], failed: [], deferred: [] };
18
+ const onEvent = deps.onEvent ?? (() => { });
19
+ for (const item of sorted) {
20
+ onEvent({ type: 'queued', item, queueSize: sorted.length });
21
+ }
22
+ let inFlight = 0;
23
+ const queue = [...sorted];
24
+ // Spawn N workers that pull from the shared queue until empty.
25
+ const worker = async () => {
26
+ for (;;) {
27
+ const item = queue.shift();
28
+ if (!item)
29
+ return;
30
+ inFlight++;
31
+ try {
32
+ if (deps.admit) {
33
+ const admitted = await deps.admit(item);
34
+ if (!admitted) {
35
+ onEvent({ type: 'admission-deferred', item });
36
+ result.deferred.push(item);
37
+ continue;
38
+ }
39
+ }
40
+ const start = Date.now();
41
+ onEvent({ type: 'started', item, inFlight });
42
+ const r = await deps.execute(item);
43
+ onEvent({ type: 'completed', item, durationMs: Date.now() - start });
44
+ result.succeeded.push({ item, result: r });
45
+ }
46
+ catch (err) {
47
+ const error = err instanceof Error ? err : new Error(String(err));
48
+ onEvent({ type: 'failed', item, error });
49
+ result.failed.push({ item, error });
50
+ }
51
+ finally {
52
+ inFlight--;
53
+ }
54
+ }
55
+ };
56
+ const workers = Array.from({ length: Math.min(deps.maxConcurrent, sorted.length) }, () => worker());
57
+ await Promise.all(workers);
58
+ return result;
59
+ }
60
+ //# sourceMappingURL=worker-pool.js.map
package/dist/execute.d.ts CHANGED
@@ -76,9 +76,49 @@ export interface ExecuteOptions {
76
76
  stateStore?: StateStore;
77
77
  /** Priority score from PPA scoring (set by watch loop or caller). */
78
78
  priorityScore?: PriorityScore;
79
+ /**
80
+ * Override the Pipeline resource selected for this execution. When set, takes
81
+ * precedence over the on-disk `config.pipeline` loaded from `.ai-sdlc/`. The
82
+ * watch loop uses this to pass the dispatcher-selected pipeline (e.g., the
83
+ * backlog pipeline for AISDLC-* issues) instead of letting executePipeline
84
+ * fall back to the canonical pipeline.yaml.
85
+ */
86
+ pipelineOverride?: import('@ai-sdlc/reference').Pipeline;
79
87
  }
80
88
  /**
81
89
  * Execute the full AI-SDLC pipeline for a given issue ID.
82
90
  */
83
91
  export declare function executePipeline(issueId: string, options?: ExecuteOptions): Promise<PipelineResult>;
92
+ /**
93
+ * Capture the current branch name (or commit SHA in detached-HEAD state) so the
94
+ * pipeline can restore the user's worktree after the run. Returns null on git
95
+ * failure — callers treat null as "don't try to restore".
96
+ *
97
+ * @internal — exported for unit tests.
98
+ */
99
+ export declare function captureCurrentBranch(workDir: string): Promise<string | null>;
100
+ /**
101
+ * Restore the user's original HEAD if the pipeline switched branches AND the
102
+ * working tree is clean. If the working tree has uncommitted changes (from a
103
+ * partial pipeline run), log a warning and leave HEAD where it is — better to
104
+ * preserve the pipeline's state than to clobber whatever the user has.
105
+ *
106
+ * @internal — exported for unit tests.
107
+ */
108
+ export declare function restoreOriginalBranch(workDir: string, originalHead: string | null, log: {
109
+ info: (msg: string) => void;
110
+ } | undefined): Promise<void>;
111
+ /**
112
+ * Push a branch to origin, rebasing onto the remote tip if it has advanced.
113
+ * Pre-AISDLC-68-rerun the pipeline did a plain `git push` and rejected with
114
+ * non-fast-forward when the remote branch had drifted (e.g. a previous run's
115
+ * commits or a manual edit). The fix: try push; on non-fast-forward, fetch +
116
+ * rebase HEAD onto origin/<branch>, retry once. Rebase conflicts surface as
117
+ * the original error so the operator can resolve manually.
118
+ *
119
+ * @internal — exported for unit tests.
120
+ */
121
+ export declare function pushBranchWithRebase(workDir: string, branchName: string, log: {
122
+ info: (msg: string) => void;
123
+ } | undefined): Promise<void>;
84
124
  //# sourceMappingURL=execute.d.ts.map
package/dist/execute.js CHANGED
@@ -12,7 +12,8 @@ import { validateAgentOutput } from './validate-agent-output.js';
12
12
  import { createLogger } from './logger.js';
13
13
  import { createStructuredConsoleLogger, createStructuredBufferLogger, } from './structured-logger.js';
14
14
  import { ClaudeCodeRunner } from './runners/claude-code.js';
15
- import { execFileAsync, getGitHubConfig, resolveRepoRoot, createDefaultAuditLog, resolveAutonomyLevel, resolveConstraints, isAutonomousStrategy, recordMetric, 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';
16
17
  import { checkKillSwitch, issueAgentCredentials, revokeAgentCredentials, classifyAndSubmitApproval, createPipelineSecurity, } from './security.js';
17
18
  import { createPipelineOrchestration, executePipelineOrchestration, validatePipelineHandoffs, } from './orchestration.js';
18
19
  import { createPipelineProvenance, attachProvenanceToPR, validatePipelineProvenance, } from './provenance.js';
@@ -23,7 +24,7 @@ import { verifyAuditIntegrity, createFileAuditLog, loadAuditEntries, computeAudi
23
24
  import { renderTemplate } from './notifications.js';
24
25
  import { admitIssueResource, createPipelineAdmission, } from './admission.js';
25
26
  import { createPipelineAdapterRegistry, createPipelineWebhookBridge, resolveAdapterFromGit, scanPipelineAdapters, resolveIssueTrackerFromConfig, } from './adapters.js';
26
- import { existsSync, readFileSync, appendFileSync } from 'node:fs';
27
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
27
28
  import { join } from 'node:path';
28
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';
29
30
  import { checkFrameworkCompliance, getControlCatalog, getFrameworkMappings, listSupportedFrameworks, } from './compliance-extended.js';
@@ -33,6 +34,7 @@ import { hasResourceChanged, fingerprintResource } from './reconcilers.js';
33
34
  import { CostTracker } from './cost-tracker.js';
34
35
  import { enrichAgentContext } from './context-enrichment.js';
35
36
  import { reportGateCheckRuns } from './check-runs.js';
37
+ import { WorktreePoolManager, readParallelismMode } from './runtime/index.js';
36
38
  /**
37
39
  * Execute the full AI-SDLC pipeline for a given issue ID.
38
40
  */
@@ -43,10 +45,32 @@ export async function executePipeline(issueId, options = {}) {
43
45
  (options.useStructuredLogger ? createStructuredConsoleLogger() : createLogger());
44
46
  const auditLog = options.auditLog ?? createDefaultAuditLog(workDir);
45
47
  const metricStore = options.metricStore;
48
+ // RFC-0010 §6/§7 Phase 2: parallelism. Per AISDLC-116 (maintainer directive 2026-05-01),
49
+ // AI_SDLC_PARALLELISM now defaults to 'on' — corpus-driven (no parallelism-related incidents
50
+ // in the trailing observation window) rather than calendar-driven. Explicit
51
+ // 'experimental' is preserved for callers pinning the pre-promotion mode; explicit
52
+ // 'off' / 'disabled' / 'false' / '0' is the opt-out path. When the resolved mode is
53
+ // not 'off', a WorktreePoolManager is instantiated for the worker-pool dispatcher
54
+ // (RFC-0010 §9). The manager is constructed but not yet routed through — Phase 2
55
+ // shipped the wire-in surface; Phase 3 wires the worker pool to consume it.
56
+ const parallelismMode = readParallelismMode();
57
+ let worktreePool;
58
+ if (parallelismMode !== 'off') {
59
+ worktreePool = new WorktreePoolManager(workDir);
60
+ log.info(`[parallelism] mode=${parallelismMode}, pool root=${worktreePool.rootDir}`);
61
+ }
62
+ void worktreePool; // referenced by Phase 3 dispatcher; silences unused-var lint until then
46
63
  // 1. Load .ai-sdlc/ config (async: includes adapter scanning + manifest distribution)
47
64
  log.stage('load-config');
48
65
  const config = await loadConfigAsync(configDir);
49
66
  log.stageEnd('load-config');
67
+ // If the caller (e.g., cli-watch's pipeline dispatcher) selected a specific
68
+ // Pipeline resource, override the on-disk default. Without this, multi-
69
+ // pipeline configs (RFC-0010 dual workflow: pipeline.yaml + pipeline-backlog.yaml)
70
+ // always fall back to pipeline.yaml because loadConfig prefers it as canonical.
71
+ if (options.pipelineOverride) {
72
+ config.pipeline = options.pipelineOverride;
73
+ }
50
74
  // Kill switch check (before any work)
51
75
  if (options.security) {
52
76
  await checkKillSwitch(options.security);
@@ -102,6 +126,12 @@ export async function executePipeline(issueId, options = {}) {
102
126
  description: issue.description,
103
127
  });
104
128
  }
129
+ // Capture the user's current HEAD so we can restore it after the pipeline.
130
+ // Without this, `git checkout <issue-branch>` inside the pipeline body
131
+ // leaves the user's working tree on the issue branch even after the run
132
+ // completes (the AISDLC-68 incident). Long-term fix is a worktree pool
133
+ // (RFC-0010 §7); this is the pragmatic save+restore.
134
+ const originalHead = await captureCurrentBranch(workDir);
105
135
  // Wrap pipeline body in try/catch to record failure episodes
106
136
  try {
107
137
  return await executePipelineBody(issueId, issue, config, qualityGate, agentRole, autonomyPolicy, tracker, sc, auditLog, metricStore, options, log, workDir);
@@ -122,6 +152,121 @@ export async function executePipeline(issueId, options = {}) {
122
152
  }
123
153
  throw err;
124
154
  }
155
+ finally {
156
+ await restoreOriginalBranch(workDir, originalHead, log);
157
+ }
158
+ }
159
+ /**
160
+ * Capture the current branch name (or commit SHA in detached-HEAD state) so the
161
+ * pipeline can restore the user's worktree after the run. Returns null on git
162
+ * failure — callers treat null as "don't try to restore".
163
+ *
164
+ * @internal — exported for unit tests.
165
+ */
166
+ export async function captureCurrentBranch(workDir) {
167
+ // cleanGitEnv() strips GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE so git
168
+ // resolves against `workDir`'s own .git rather than whatever a parent
169
+ // process (e.g. husky pre-push hook running in another worktree) leaked
170
+ // into the env (AISDLC-72).
171
+ try {
172
+ const { stdout } = await execFileAsync('git', ['symbolic-ref', '--short', 'HEAD'], {
173
+ cwd: workDir,
174
+ env: cleanGitEnv(),
175
+ });
176
+ return stdout.trim();
177
+ }
178
+ catch {
179
+ // Detached HEAD or no git repo. Try the SHA fallback.
180
+ try {
181
+ const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
182
+ cwd: workDir,
183
+ env: cleanGitEnv(),
184
+ });
185
+ return stdout.trim();
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ }
191
+ }
192
+ /**
193
+ * Restore the user's original HEAD if the pipeline switched branches AND the
194
+ * working tree is clean. If the working tree has uncommitted changes (from a
195
+ * partial pipeline run), log a warning and leave HEAD where it is — better to
196
+ * preserve the pipeline's state than to clobber whatever the user has.
197
+ *
198
+ * @internal — exported for unit tests.
199
+ */
200
+ export async function restoreOriginalBranch(workDir, originalHead, log) {
201
+ if (!originalHead)
202
+ return;
203
+ try {
204
+ const current = await captureCurrentBranch(workDir);
205
+ if (current === originalHead)
206
+ return;
207
+ // Refuse to checkout if the worktree has uncommitted changes — git would
208
+ // either block the checkout or carry the changes across, both bad.
209
+ // cleanGitEnv() prevents leaked GIT_DIR from corrupting these calls (AISDLC-72).
210
+ const { stdout } = await execFileAsync('git', ['status', '--porcelain'], {
211
+ cwd: workDir,
212
+ env: cleanGitEnv(),
213
+ });
214
+ if (stdout.trim().length > 0) {
215
+ log?.info(`[pipeline] worktree dirty after pipeline; HEAD left at \`${current}\`. ` +
216
+ `To restore: \`git stash && git checkout ${originalHead}\``);
217
+ return;
218
+ }
219
+ await execFileAsync('git', ['checkout', originalHead], {
220
+ cwd: workDir,
221
+ env: cleanGitEnv(),
222
+ });
223
+ }
224
+ catch (err) {
225
+ log?.info(`[pipeline] failed to restore HEAD to \`${originalHead}\`: ${err.message}`);
226
+ }
227
+ }
228
+ /**
229
+ * Push a branch to origin, rebasing onto the remote tip if it has advanced.
230
+ * Pre-AISDLC-68-rerun the pipeline did a plain `git push` and rejected with
231
+ * non-fast-forward when the remote branch had drifted (e.g. a previous run's
232
+ * commits or a manual edit). The fix: try push; on non-fast-forward, fetch +
233
+ * rebase HEAD onto origin/<branch>, retry once. Rebase conflicts surface as
234
+ * the original error so the operator can resolve manually.
235
+ *
236
+ * @internal — exported for unit tests.
237
+ */
238
+ export async function pushBranchWithRebase(workDir, branchName, log) {
239
+ // cleanGitEnv() ensures these git calls bind to `workDir`'s own .git, not
240
+ // a leaked GIT_DIR from a parent process (AISDLC-72).
241
+ const env = cleanGitEnv();
242
+ try {
243
+ await execFileAsync('git', ['push', 'origin', branchName], { cwd: workDir, env });
244
+ return;
245
+ }
246
+ catch (err) {
247
+ const stderr = err.stderr ?? err.message;
248
+ if (!/non-fast-forward|rejected/i.test(stderr)) {
249
+ throw err;
250
+ }
251
+ log?.info(`[pipeline] push rejected (non-fast-forward); rebasing onto origin/${branchName}`);
252
+ }
253
+ // Fetch + rebase + retry. If the rebase fails, surface a clear error.
254
+ try {
255
+ await execFileAsync('git', ['fetch', 'origin', branchName], { cwd: workDir, env });
256
+ await execFileAsync('git', ['rebase', `origin/${branchName}`], { cwd: workDir, env });
257
+ }
258
+ catch (rebaseErr) {
259
+ // Abort the partial rebase so the worktree isn't left in a half-merged state.
260
+ try {
261
+ await execFileAsync('git', ['rebase', '--abort'], { cwd: workDir, env });
262
+ }
263
+ catch {
264
+ /* nothing to abort */
265
+ }
266
+ throw new Error(`Push rebase failed for branch ${branchName}: ${rebaseErr.message}. ` +
267
+ `Resolve conflicts manually and re-run the pipeline.`);
268
+ }
269
+ await execFileAsync('git', ['push', 'origin', branchName], { cwd: workDir, env });
125
270
  }
126
271
  async function executePipelineBody(issueId, issue, config, qualityGate, agentRole, autonomyPolicy, tracker, sc, auditLog, metricStore, options, log, workDir) {
127
272
  const issueNumber = issueIdToNumber(issueId);
@@ -262,11 +407,15 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
262
407
  const currentLevel = resolveAutonomyLevel(autonomyPolicy);
263
408
  log.stageEnd('validate-issue');
264
409
  // 7. Create branch and checkout locally (read pattern from pipeline config)
265
- const branchVars = { issueNumber: issueId, issueTitle: issue.title };
410
+ const branchVars = buildIssueTemplateVars(issueId, issue.title);
266
411
  const branchName = interpolateBranchPattern(config.pipeline?.spec.branching?.pattern, branchVars);
267
412
  await sc.createBranch({ name: branchName });
268
- await execFileAsync('git', ['fetch', 'origin', branchName], { cwd: workDir });
269
- await execFileAsync('git', ['checkout', branchName], { cwd: workDir });
413
+ // cleanGitEnv() prevents leaked GIT_DIR from corrupting these calls (AISDLC-72).
414
+ await execFileAsync('git', ['fetch', 'origin', branchName], {
415
+ cwd: workDir,
416
+ env: cleanGitEnv(),
417
+ });
418
+ await execFileAsync('git', ['checkout', branchName], { cwd: workDir, env: cleanGitEnv() });
270
419
  // 8. Resolve agent constraints
271
420
  const resolved = resolveConstraints(agentRole.spec.constraints, currentLevel);
272
421
  // 8b. Model selection: choose model based on complexity and budget pressure
@@ -423,9 +572,13 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
423
572
  `**${result.filesChanged.length} files changed.** Pushing branch...\n\n` +
424
573
  activitySection);
425
574
  await notifySlack(`:white_check_mark: Agent complete — ${result.filesChanged.length} files changed. Pushing...`);
426
- // 12. Push branch (before validation to preserve work even if validation fails)
575
+ // 12. Push branch (before validation to preserve work even if validation fails).
576
+ // Fetch first + rebase if the remote has diverged so push fast-forwards
577
+ // cleanly. Without this, re-runs against an existing remote branch (e.g. when
578
+ // the issue branch was updated by another pipeline run or by a hand-edit)
579
+ // reject with non-fast-forward and the agent's work is stranded locally.
427
580
  log.stage('push');
428
- await execFileAsync('git', ['push', 'origin', branchName], { cwd: workDir });
581
+ await pushBranchWithRebase(workDir, branchName, log);
429
582
  log.stageEnd('push');
430
583
  auditLog.record({
431
584
  actor: 'system',
@@ -453,14 +606,24 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
453
606
  return validationResult;
454
607
  });
455
608
  if (!validation.passed) {
456
- // Record validation failure in audit log
609
+ // One-line summary suitable for log lines + thrown error message.
610
+ const violationSummary = validation.violations.map((v) => `${v.rule}: ${v.message}`).join('; ');
611
+ // Record validation failure in audit log with full violation detail (rule
612
+ // + message), not just the rule names — the rule name alone doesn't tell
613
+ // the operator what to fix.
457
614
  auditLog.record({
458
615
  actor: 'system',
459
616
  action: 'check',
460
617
  resource: 'agent-output',
461
618
  decision: 'denied',
462
- details: { violations: validation.violations.map((v) => v.rule) },
619
+ details: {
620
+ violations: validation.violations.map((v) => ({ rule: v.rule, message: v.message })),
621
+ },
463
622
  });
623
+ // Surface the rejection detail to the structured log so the operator sees
624
+ // it in stderr immediately. Without this the only signal was the
625
+ // (detail-free) thrown error message.
626
+ log.error(`[validate-output] rejected: ${violationSummary}`);
464
627
  // Post comment explaining the violations
465
628
  const violationList = validation.violations
466
629
  .map((v) => `- **${v.rule}**: ${v.message}`)
@@ -468,8 +631,10 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
468
631
  await tracker.addComment(issueId, `## ${NOTIFICATION_TITLES.guardrailViolations}\n\n${violationList}\n\n` +
469
632
  `The branch \`${branchName}\` has been pushed with your changes. You can review the work, ` +
470
633
  `cherry-pick valid changes, or adjust the guardrails as needed.`);
471
- // Exit without creating PR
472
- throw new Error(`Agent output failed guardrail validation. Branch ${branchName} preserved for review.`);
634
+ // Exit without creating PR. Include the violation summary in the error
635
+ // message so cli-watch and downstream loggers see the actual rejection.
636
+ throw new Error(`Agent output failed guardrail validation: ${violationSummary}. ` +
637
+ `Branch ${branchName} preserved for review.`);
473
638
  }
474
639
  // Validation passed — record success
475
640
  auditLog.record({
@@ -483,6 +648,7 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
483
648
  try {
484
649
  const { stdout: headSha } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
485
650
  cwd: workDir,
651
+ env: cleanGitEnv(),
486
652
  });
487
653
  const gateResults = qualityGate.spec.gates.map((gate) => {
488
654
  const evalResult = evaluatePipelineGate(gate, {
@@ -519,6 +685,7 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
519
685
  try {
520
686
  const { stdout: diffStat } = await execFileAsync('git', ['diff', '--stat', 'HEAD~1'], {
521
687
  cwd: workDir,
688
+ env: cleanGitEnv(),
522
689
  });
523
690
  const insertMatch = diffStat.match(/(\d+) insertions?\(\+\)/);
524
691
  const deleteMatch = diffStat.match(/(\d+) deletions?\(-\)/);
@@ -578,7 +745,7 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
578
745
  }
579
746
  provenanceBlock = '\n\n' + provenanceText;
580
747
  }
581
- const prVars = { issueNumber: issueId, issueTitle: issue.title };
748
+ const prVars = buildIssueTemplateVars(issueId, issue.title);
582
749
  const prTitle = interpolatePRTitle(prConfig?.titleTemplate, prVars);
583
750
  const closeKeyword = prConfig?.closeKeyword ?? 'Closes';
584
751
  const targetBranch = config.pipeline?.spec.branching?.targetBranch ?? 'main';
@@ -878,11 +1045,17 @@ function ensureRuntimeGitignore(workDir) {
878
1045
  try {
879
1046
  const gitignorePath = join(workDir, '.gitignore');
880
1047
  const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf-8') : '';
881
- const missing = RUNTIME_GITIGNORE_PATHS.filter((entry) => !existing.includes(entry));
1048
+ const SENTINEL = '# ai-sdlc:runtime-gitignore';
1049
+ if (existing.includes(SENTINEL))
1050
+ return;
1051
+ const missing = RUNTIME_GITIGNORE_PATHS.filter((entry) => !existing.split('\n').some((line) => line.trim() === entry));
882
1052
  if (missing.length === 0)
883
1053
  return;
884
- const block = '\n# AI-SDLC runtime artifacts\n' + missing.join('\n') + '\n';
885
- appendFileSync(gitignorePath, block, 'utf-8');
1054
+ // Write atomically (writeFileSync, not appendFileSync) to avoid race conditions
1055
+ // when parallel test processes both read before either writes.
1056
+ const block = `${SENTINEL}\n` + missing.join('\n') + '\n';
1057
+ const newContent = existing.length > 0 ? existing.trimEnd() + '\n' + block : block;
1058
+ writeFileSync(gitignorePath, newContent, 'utf-8');
886
1059
  }
887
1060
  catch {
888
1061
  // Best-effort — workDir may not exist yet in tests or dry-run scenarios
package/dist/fix-ci.js CHANGED
@@ -14,6 +14,7 @@ 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
16
  import { createCycleDetectorFromConfig, checkAndHandleCycle } from './cycle-utils.js';
17
+ import { cleanGitEnv } from './runtime/git-env.js';
17
18
  export const MAX_FIX_ATTEMPTS = DEFAULT_MAX_FIX_ATTEMPTS;
18
19
  export const MAX_LOG_LINES = DEFAULT_MAX_LOG_LINES;
19
20
  export const RETRY_MARKER = '<!-- ai-sdlc-fix-ci-attempt -->';
@@ -180,8 +181,10 @@ export async function executeFixCI(prNumber, runId, options = {}) {
180
181
  const ciLogs = await fetchCILogs(runId, options._ciLogs, options.ciAdapter);
181
182
  log.stageEnd('fetch-logs');
182
183
  // 4. Determine branch and issue number
184
+ // cleanGitEnv() prevents leaked GIT_DIR from binding to a parent repo (AISDLC-72).
183
185
  const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], {
184
186
  cwd: workDir,
187
+ env: cleanGitEnv(),
185
188
  });
186
189
  const currentBranch = branchStdout.trim();
187
190
  const issueId = extractIssueId(currentBranch);
@@ -341,8 +344,12 @@ export async function executeFixCI(prNumber, runId, options = {}) {
341
344
  });
342
345
  });
343
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).
344
348
  log.stage('push');
345
- await execFileAsync('git', ['push', 'origin', currentBranch], { cwd: workDir });
349
+ await execFileAsync('git', ['push', 'origin', currentBranch], {
350
+ cwd: workDir,
351
+ env: cleanGitEnv(),
352
+ });
346
353
  log.stageEnd('push');
347
354
  auditLog.record({
348
355
  actor: 'system',
@@ -14,6 +14,7 @@ import { parseDuration } from './policy-evaluators.js';
14
14
  import { checkKillSwitch, issueAgentCredentials, revokeAgentCredentials, } from './security.js';
15
15
  import { DEFAULT_GH_CLI_TIMEOUT_MS, DEFAULT_CONFIG_DIR_NAME, defaultSandboxConstraints, NOTIFICATION_TITLES, } from './defaults.js';
16
16
  import { createCycleDetectorFromConfig, checkAndHandleCycle } from './cycle-utils.js';
17
+ import { cleanGitEnv } from './runtime/git-env.js';
17
18
  // Default max review-fix attempts (lower than CI-fix since reviews are more deterministic)
18
19
  export const MAX_REVIEW_FIX_ATTEMPTS = 2;
19
20
  export const RETRY_MARKER = '<!-- ai-sdlc-fix-review-attempt -->';
@@ -210,8 +211,10 @@ export async function executeFixReview(prNumber, options = {}) {
210
211
  return;
211
212
  }
212
213
  // 4. Determine branch and issue number
214
+ // cleanGitEnv() prevents leaked GIT_DIR from binding to a parent repo (AISDLC-72).
213
215
  const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], {
214
216
  cwd: workDir,
217
+ env: cleanGitEnv(),
215
218
  });
216
219
  const currentBranch = sanitizeBranchName(branchStdout.trim());
217
220
  const issueId = extractIssueId(currentBranch);
@@ -371,8 +374,12 @@ export async function executeFixReview(prNumber, options = {}) {
371
374
  });
372
375
  });
373
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).
374
378
  log.stage('push');
375
- await execFileAsync('git', ['push', 'origin', currentBranch], { cwd: workDir });
379
+ await execFileAsync('git', ['push', 'origin', currentBranch], {
380
+ cwd: workDir,
381
+ env: cleanGitEnv(),
382
+ });
376
383
  log.stageEnd('push');
377
384
  auditLog.record({
378
385
  actor: 'system',
@@ -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