@ai-sdlc/orchestrator 0.6.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 (203) hide show
  1. package/dist/admission-composite.d.ts +67 -0
  2. package/dist/admission-composite.js +158 -0
  3. package/dist/admission-enrichment.d.ts +142 -0
  4. package/dist/admission-enrichment.js +331 -0
  5. package/dist/admission-hc.d.ts +62 -0
  6. package/dist/admission-hc.js +83 -0
  7. package/dist/admission-score.d.ts +95 -5
  8. package/dist/admission-score.js +84 -11
  9. package/dist/analysis/analyzer.js +3 -2
  10. package/dist/analysis/convention-detector.d.ts +85 -2
  11. package/dist/analysis/convention-detector.js +375 -70
  12. package/dist/analysis/diff-analyzer.d.ts +33 -0
  13. package/dist/analysis/diff-analyzer.js +122 -0
  14. package/dist/analysis/hotspot-analyzer.js +3 -1
  15. package/dist/analysis/index.d.ts +2 -1
  16. package/dist/analysis/index.js +2 -1
  17. package/dist/artifacts/index.d.ts +65 -0
  18. package/dist/artifacts/index.js +142 -0
  19. package/dist/backlog-adapter.d.ts +121 -0
  20. package/dist/backlog-adapter.js +438 -0
  21. package/dist/calibration.d.ts +43 -0
  22. package/dist/calibration.js +76 -0
  23. package/dist/cli/commands/agents.d.ts +24 -0
  24. package/dist/cli/commands/agents.js +66 -1
  25. package/dist/cli/commands/git-remote.d.ts +49 -0
  26. package/dist/cli/commands/git-remote.js +91 -0
  27. package/dist/cli/commands/health.d.ts +4 -0
  28. package/dist/cli/commands/health.js +13 -1
  29. package/dist/cli/commands/init.d.ts +26 -0
  30. package/dist/cli/commands/init.js +164 -22
  31. package/dist/cli/commands/mcp-setup.d.ts +31 -0
  32. package/dist/cli/commands/mcp-setup.js +78 -8
  33. package/dist/cli/formatters/table.js +15 -2
  34. package/dist/cli/index.d.ts +14 -1
  35. package/dist/cli/index.js +81 -20
  36. package/dist/cli/versions.d.ts +57 -0
  37. package/dist/cli/versions.js +128 -0
  38. package/dist/code-area-classifier.d.ts +21 -0
  39. package/dist/code-area-classifier.js +48 -0
  40. package/dist/config.d.ts +33 -1
  41. package/dist/config.js +78 -8
  42. package/dist/database/adapters/external.d.ts +24 -0
  43. package/dist/database/adapters/external.js +80 -0
  44. package/dist/database/adapters/neon.d.ts +41 -0
  45. package/dist/database/adapters/neon.js +98 -0
  46. package/dist/database/adapters/pg-snapshot-restore.d.ts +28 -0
  47. package/dist/database/adapters/pg-snapshot-restore.js +68 -0
  48. package/dist/database/adapters/sqlite-copy.d.ts +32 -0
  49. package/dist/database/adapters/sqlite-copy.js +145 -0
  50. package/dist/database/connection-injection.d.ts +35 -0
  51. package/dist/database/connection-injection.js +93 -0
  52. package/dist/database/index.d.ts +12 -0
  53. package/dist/database/index.js +23 -0
  54. package/dist/database/registry.d.ts +13 -0
  55. package/dist/database/registry.js +27 -0
  56. package/dist/database/topology.d.ts +52 -0
  57. package/dist/database/topology.js +44 -0
  58. package/dist/database/types.d.ts +89 -0
  59. package/dist/database/types.js +26 -0
  60. package/dist/design-authority.d.ts +40 -0
  61. package/dist/design-authority.js +71 -0
  62. package/dist/design-lookahead.d.ts +64 -0
  63. package/dist/design-lookahead.js +86 -0
  64. package/dist/design-quality-trend.d.ts +87 -0
  65. package/dist/design-quality-trend.js +190 -0
  66. package/dist/design-system-context.d.ts +46 -0
  67. package/dist/design-system-context.js +80 -0
  68. package/dist/design-system-correction-loop.d.ts +64 -0
  69. package/dist/design-system-correction-loop.js +128 -0
  70. package/dist/design-system-metrics.d.ts +61 -0
  71. package/dist/design-system-metrics.js +104 -0
  72. package/dist/design-system-stewardship.d.ts +22 -0
  73. package/dist/design-system-stewardship.js +85 -0
  74. package/dist/design-system-validation.d.ts +37 -0
  75. package/dist/design-system-validation.js +88 -0
  76. package/dist/dispatch/index.d.ts +4 -0
  77. package/dist/dispatch/index.js +4 -0
  78. package/dist/dispatch/merge-gate.d.ts +46 -0
  79. package/dist/dispatch/merge-gate.js +90 -0
  80. package/dist/dispatch/requeue.d.ts +57 -0
  81. package/dist/dispatch/requeue.js +131 -0
  82. package/dist/dispatch/worker-pool.d.ts +62 -0
  83. package/dist/dispatch/worker-pool.js +60 -0
  84. package/dist/execute.d.ts +40 -0
  85. package/dist/execute.js +185 -15
  86. package/dist/fix-ci.js +8 -1
  87. package/dist/fix-review.js +8 -1
  88. package/dist/harness/adapters/claude-code.d.ts +29 -0
  89. package/dist/harness/adapters/claude-code.js +191 -0
  90. package/dist/harness/adapters/codex.d.ts +25 -0
  91. package/dist/harness/adapters/codex.js +61 -0
  92. package/dist/harness/independence.d.ts +51 -0
  93. package/dist/harness/independence.js +67 -0
  94. package/dist/harness/index.d.ts +14 -0
  95. package/dist/harness/index.js +20 -0
  96. package/dist/harness/registry.d.ts +17 -0
  97. package/dist/harness/registry.js +31 -0
  98. package/dist/harness/types.d.ts +123 -0
  99. package/dist/harness/types.js +8 -0
  100. package/dist/harness/version-probe.d.ts +14 -0
  101. package/dist/harness/version-probe.js +123 -0
  102. package/dist/index.d.ts +30 -4
  103. package/dist/index.js +35 -2
  104. package/dist/models/classifier.d.ts +76 -0
  105. package/dist/models/classifier.js +221 -0
  106. package/dist/models/index.d.ts +3 -0
  107. package/dist/models/index.js +3 -0
  108. package/dist/models/registry.d.ts +97 -0
  109. package/dist/models/registry.js +173 -0
  110. package/dist/pillar-breakdown.d.ts +63 -0
  111. package/dist/pillar-breakdown.js +153 -0
  112. package/dist/priority.d.ts +5 -0
  113. package/dist/priority.js +18 -5
  114. package/dist/review-meta.d.ts +65 -0
  115. package/dist/review-meta.js +149 -0
  116. package/dist/review.d.ts +4 -0
  117. package/dist/review.js +12 -2
  118. package/dist/runners/claude-code-sdk.d.ts +31 -0
  119. package/dist/runners/claude-code-sdk.js +238 -0
  120. package/dist/runners/claude-code.d.ts +3 -0
  121. package/dist/runners/claude-code.js +30 -66
  122. package/dist/runners/codex.js +4 -1
  123. package/dist/runners/copilot.js +4 -1
  124. package/dist/runners/cursor.js +4 -1
  125. package/dist/runners/git-utils.d.ts +81 -0
  126. package/dist/runners/git-utils.js +201 -0
  127. package/dist/runners/index.d.ts +3 -0
  128. package/dist/runners/index.js +3 -0
  129. package/dist/runners/review-agent.d.ts +20 -0
  130. package/dist/runners/review-agent.js +171 -41
  131. package/dist/runners/runner-registry.js +10 -0
  132. package/dist/runners/sdk-review-runner.d.ts +65 -0
  133. package/dist/runners/sdk-review-runner.js +185 -0
  134. package/dist/runners/security-triage.d.ts +20 -4
  135. package/dist/runners/security-triage.js +39 -15
  136. package/dist/runners/types.d.ts +6 -0
  137. package/dist/runtime/attestations.d.ts +274 -0
  138. package/dist/runtime/attestations.js +460 -0
  139. package/dist/runtime/git-env.d.ts +53 -0
  140. package/dist/runtime/git-env.js +60 -0
  141. package/dist/runtime/index.d.ts +7 -0
  142. package/dist/runtime/index.js +7 -0
  143. package/dist/runtime/parallelism-flag.d.ts +10 -0
  144. package/dist/runtime/parallelism-flag.js +18 -0
  145. package/dist/runtime/port-allocator.d.ts +21 -0
  146. package/dist/runtime/port-allocator.js +66 -0
  147. package/dist/runtime/worktree-pool.d.ts +86 -0
  148. package/dist/runtime/worktree-pool.js +204 -0
  149. package/dist/runtime/worktree.d.ts +25 -0
  150. package/dist/runtime/worktree.js +111 -0
  151. package/dist/sa-scoring/auto-calibrate.d.ts +69 -0
  152. package/dist/sa-scoring/auto-calibrate.js +107 -0
  153. package/dist/sa-scoring/c1-sa2-computable.d.ts +42 -0
  154. package/dist/sa-scoring/c1-sa2-computable.js +59 -0
  155. package/dist/sa-scoring/composite.d.ts +107 -0
  156. package/dist/sa-scoring/composite.js +139 -0
  157. package/dist/sa-scoring/depparse-client.d.ts +79 -0
  158. package/dist/sa-scoring/depparse-client.js +187 -0
  159. package/dist/sa-scoring/did-compiler.d.ts +122 -0
  160. package/dist/sa-scoring/did-compiler.js +286 -0
  161. package/dist/sa-scoring/drift-monitor.d.ts +84 -0
  162. package/dist/sa-scoring/drift-monitor.js +186 -0
  163. package/dist/sa-scoring/exemplar-bank.d.ts +78 -0
  164. package/dist/sa-scoring/exemplar-bank.js +154 -0
  165. package/dist/sa-scoring/feedback-store.d.ts +100 -0
  166. package/dist/sa-scoring/feedback-store.js +156 -0
  167. package/dist/sa-scoring/index.d.ts +71 -0
  168. package/dist/sa-scoring/index.js +158 -0
  169. package/dist/sa-scoring/layer1-deterministic.d.ts +115 -0
  170. package/dist/sa-scoring/layer1-deterministic.js +298 -0
  171. package/dist/sa-scoring/layer2-structural.d.ts +71 -0
  172. package/dist/sa-scoring/layer2-structural.js +151 -0
  173. package/dist/sa-scoring/layer3-llm.d.ts +86 -0
  174. package/dist/sa-scoring/layer3-llm.js +282 -0
  175. package/dist/sa-scoring/rescore-orchestrator.d.ts +52 -0
  176. package/dist/sa-scoring/rescore-orchestrator.js +47 -0
  177. package/dist/scheduling/burn-down.d.ts +27 -0
  178. package/dist/scheduling/burn-down.js +43 -0
  179. package/dist/scheduling/calibration.d.ts +42 -0
  180. package/dist/scheduling/calibration.js +150 -0
  181. package/dist/scheduling/index.d.ts +8 -0
  182. package/dist/scheduling/index.js +8 -0
  183. package/dist/scheduling/ledger.d.ts +59 -0
  184. package/dist/scheduling/ledger.js +216 -0
  185. package/dist/scheduling/off-peak.d.ts +27 -0
  186. package/dist/scheduling/off-peak.js +112 -0
  187. package/dist/scheduling/schedule-decision.d.ts +39 -0
  188. package/dist/scheduling/schedule-decision.js +89 -0
  189. package/dist/scheduling/tier-analysis.d.ts +47 -0
  190. package/dist/scheduling/tier-analysis.js +81 -0
  191. package/dist/scheduling/types.d.ts +140 -0
  192. package/dist/scheduling/types.js +11 -0
  193. package/dist/shared.d.ts +13 -0
  194. package/dist/shared.js +32 -0
  195. package/dist/state/schema.d.ts +5 -1
  196. package/dist/state/schema.js +223 -1
  197. package/dist/state/store.d.ts +55 -1
  198. package/dist/state/store.js +342 -7
  199. package/dist/state/types.d.ts +139 -0
  200. package/dist/types.d.ts +1 -1
  201. package/dist/validate-agent-output.js +4 -1
  202. package/dist/watch.js +6 -0
  203. package/package.json +2 -2
@@ -0,0 +1,62 @@
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
+ export interface WorkItem<T> {
7
+ /** Unique id (typically issue id). */
8
+ id: string;
9
+ /** PPA composite score; higher = higher priority. */
10
+ ppaScore: number;
11
+ /** The actual work to perform when admitted. */
12
+ payload: T;
13
+ }
14
+ export interface WorkerPoolDeps<T, R> {
15
+ /** Number of in-flight items allowed at once. */
16
+ maxConcurrent: number;
17
+ /** Execute a single work item; called by the pool with concurrency-bounded parallelism. */
18
+ execute: (item: WorkItem<T>) => Promise<R>;
19
+ /** Optional admission gate; return false to defer the item back into the queue. */
20
+ admit?: (item: WorkItem<T>) => Promise<boolean>;
21
+ /** Optional structured event sink. */
22
+ onEvent?: (event: WorkerPoolEvent<T>) => void;
23
+ }
24
+ export type WorkerPoolEvent<T> = {
25
+ type: 'queued';
26
+ item: WorkItem<T>;
27
+ queueSize: number;
28
+ } | {
29
+ type: 'admission-deferred';
30
+ item: WorkItem<T>;
31
+ } | {
32
+ type: 'started';
33
+ item: WorkItem<T>;
34
+ inFlight: number;
35
+ } | {
36
+ type: 'completed';
37
+ item: WorkItem<T>;
38
+ durationMs: number;
39
+ } | {
40
+ type: 'failed';
41
+ item: WorkItem<T>;
42
+ error: Error;
43
+ };
44
+ export interface WorkerPoolResult<T, R> {
45
+ succeeded: Array<{
46
+ item: WorkItem<T>;
47
+ result: R;
48
+ }>;
49
+ failed: Array<{
50
+ item: WorkItem<T>;
51
+ error: Error;
52
+ }>;
53
+ deferred: Array<WorkItem<T>>;
54
+ }
55
+ /**
56
+ * Process all items with bounded concurrency. Items are dispatched in PPA score order
57
+ * (descending). Items that the admission gate rejects are returned in `deferred` for
58
+ * the caller to requeue or escalate. Failed executions are returned in `failed`; the
59
+ * pool does NOT auto-retry — that's the caller's responsibility (see RFC §9.4).
60
+ */
61
+ export declare function runWorkerPool<T, R>(items: WorkItem<T>[], deps: WorkerPoolDeps<T, R>): Promise<WorkerPoolResult<T, R>>;
62
+ //# sourceMappingURL=worker-pool.d.ts.map
@@ -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,29 @@ 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 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
46
60
  // 1. Load .ai-sdlc/ config (async: includes adapter scanning + manifest distribution)
47
61
  log.stage('load-config');
48
62
  const config = await loadConfigAsync(configDir);
49
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
+ }
50
71
  // Kill switch check (before any work)
51
72
  if (options.security) {
52
73
  await checkKillSwitch(options.security);
@@ -102,6 +123,12 @@ export async function executePipeline(issueId, options = {}) {
102
123
  description: issue.description,
103
124
  });
104
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);
105
132
  // Wrap pipeline body in try/catch to record failure episodes
106
133
  try {
107
134
  return await executePipelineBody(issueId, issue, config, qualityGate, agentRole, autonomyPolicy, tracker, sc, auditLog, metricStore, options, log, workDir);
@@ -122,6 +149,121 @@ export async function executePipeline(issueId, options = {}) {
122
149
  }
123
150
  throw err;
124
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 });
125
267
  }
126
268
  async function executePipelineBody(issueId, issue, config, qualityGate, agentRole, autonomyPolicy, tracker, sc, auditLog, metricStore, options, log, workDir) {
127
269
  const issueNumber = issueIdToNumber(issueId);
@@ -262,11 +404,15 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
262
404
  const currentLevel = resolveAutonomyLevel(autonomyPolicy);
263
405
  log.stageEnd('validate-issue');
264
406
  // 7. Create branch and checkout locally (read pattern from pipeline config)
265
- const branchVars = { issueNumber: issueId, issueTitle: issue.title };
407
+ const branchVars = buildIssueTemplateVars(issueId, issue.title);
266
408
  const branchName = interpolateBranchPattern(config.pipeline?.spec.branching?.pattern, branchVars);
267
409
  await sc.createBranch({ name: branchName });
268
- await execFileAsync('git', ['fetch', 'origin', branchName], { cwd: workDir });
269
- 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() });
270
416
  // 8. Resolve agent constraints
271
417
  const resolved = resolveConstraints(agentRole.spec.constraints, currentLevel);
272
418
  // 8b. Model selection: choose model based on complexity and budget pressure
@@ -423,9 +569,13 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
423
569
  `**${result.filesChanged.length} files changed.** Pushing branch...\n\n` +
424
570
  activitySection);
425
571
  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)
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.
427
577
  log.stage('push');
428
- await execFileAsync('git', ['push', 'origin', branchName], { cwd: workDir });
578
+ await pushBranchWithRebase(workDir, branchName, log);
429
579
  log.stageEnd('push');
430
580
  auditLog.record({
431
581
  actor: 'system',
@@ -453,14 +603,24 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
453
603
  return validationResult;
454
604
  });
455
605
  if (!validation.passed) {
456
- // Record validation failure in audit log
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.
457
611
  auditLog.record({
458
612
  actor: 'system',
459
613
  action: 'check',
460
614
  resource: 'agent-output',
461
615
  decision: 'denied',
462
- details: { violations: validation.violations.map((v) => v.rule) },
616
+ details: {
617
+ violations: validation.violations.map((v) => ({ rule: v.rule, message: v.message })),
618
+ },
463
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}`);
464
624
  // Post comment explaining the violations
465
625
  const violationList = validation.violations
466
626
  .map((v) => `- **${v.rule}**: ${v.message}`)
@@ -468,8 +628,10 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
468
628
  await tracker.addComment(issueId, `## ${NOTIFICATION_TITLES.guardrailViolations}\n\n${violationList}\n\n` +
469
629
  `The branch \`${branchName}\` has been pushed with your changes. You can review the work, ` +
470
630
  `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.`);
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.`);
473
635
  }
474
636
  // Validation passed — record success
475
637
  auditLog.record({
@@ -483,6 +645,7 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
483
645
  try {
484
646
  const { stdout: headSha } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
485
647
  cwd: workDir,
648
+ env: cleanGitEnv(),
486
649
  });
487
650
  const gateResults = qualityGate.spec.gates.map((gate) => {
488
651
  const evalResult = evaluatePipelineGate(gate, {
@@ -519,6 +682,7 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
519
682
  try {
520
683
  const { stdout: diffStat } = await execFileAsync('git', ['diff', '--stat', 'HEAD~1'], {
521
684
  cwd: workDir,
685
+ env: cleanGitEnv(),
522
686
  });
523
687
  const insertMatch = diffStat.match(/(\d+) insertions?\(\+\)/);
524
688
  const deleteMatch = diffStat.match(/(\d+) deletions?\(-\)/);
@@ -578,7 +742,7 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
578
742
  }
579
743
  provenanceBlock = '\n\n' + provenanceText;
580
744
  }
581
- const prVars = { issueNumber: issueId, issueTitle: issue.title };
745
+ const prVars = buildIssueTemplateVars(issueId, issue.title);
582
746
  const prTitle = interpolatePRTitle(prConfig?.titleTemplate, prVars);
583
747
  const closeKeyword = prConfig?.closeKeyword ?? 'Closes';
584
748
  const targetBranch = config.pipeline?.spec.branching?.targetBranch ?? 'main';
@@ -878,11 +1042,17 @@ function ensureRuntimeGitignore(workDir) {
878
1042
  try {
879
1043
  const gitignorePath = join(workDir, '.gitignore');
880
1044
  const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf-8') : '';
881
- const missing = RUNTIME_GITIGNORE_PATHS.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));
882
1049
  if (missing.length === 0)
883
1050
  return;
884
- const block = '\n# AI-SDLC runtime artifacts\n' + missing.join('\n') + '\n';
885
- appendFileSync(gitignorePath, block, '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');
886
1056
  }
887
1057
  catch {
888
1058
  // 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