@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.
- package/dist/action-enforcement.d.ts +26 -0
- package/dist/action-enforcement.js +70 -0
- package/dist/admission-composite.d.ts +67 -0
- package/dist/admission-composite.js +158 -0
- package/dist/admission-enrichment.d.ts +142 -0
- package/dist/admission-enrichment.js +331 -0
- package/dist/admission-hc.d.ts +62 -0
- package/dist/admission-hc.js +83 -0
- package/dist/admission-score.d.ts +148 -0
- package/dist/admission-score.js +237 -0
- package/dist/analysis/analyzer.js +3 -2
- package/dist/analysis/convention-detector.d.ts +85 -2
- package/dist/analysis/convention-detector.js +375 -70
- package/dist/analysis/diff-analyzer.d.ts +33 -0
- package/dist/analysis/diff-analyzer.js +122 -0
- package/dist/analysis/hotspot-analyzer.js +3 -1
- package/dist/analysis/index.d.ts +2 -1
- package/dist/analysis/index.js +2 -1
- package/dist/artifacts/index.d.ts +65 -0
- package/dist/artifacts/index.js +142 -0
- package/dist/backlog-adapter.d.ts +121 -0
- package/dist/backlog-adapter.js +438 -0
- package/dist/calibration.d.ts +43 -0
- package/dist/calibration.js +76 -0
- package/dist/cli/commands/agents.d.ts +24 -0
- package/dist/cli/commands/agents.js +66 -1
- package/dist/cli/commands/git-remote.d.ts +49 -0
- package/dist/cli/commands/git-remote.js +91 -0
- package/dist/cli/commands/health.d.ts +4 -0
- package/dist/cli/commands/health.js +13 -1
- package/dist/cli/commands/init.d.ts +26 -0
- package/dist/cli/commands/init.js +164 -22
- package/dist/cli/commands/mcp-setup.d.ts +31 -0
- package/dist/cli/commands/mcp-setup.js +78 -8
- package/dist/cli/formatters/table.js +15 -2
- package/dist/cli/index.d.ts +14 -1
- package/dist/cli/index.js +81 -20
- package/dist/cli/versions.d.ts +57 -0
- package/dist/cli/versions.js +128 -0
- package/dist/code-area-classifier.d.ts +21 -0
- package/dist/code-area-classifier.js +48 -0
- package/dist/config.d.ts +33 -1
- package/dist/config.js +78 -8
- package/dist/cycle-utils.d.ts +51 -0
- package/dist/cycle-utils.js +77 -0
- package/dist/database/adapters/external.d.ts +24 -0
- package/dist/database/adapters/external.js +80 -0
- package/dist/database/adapters/neon.d.ts +41 -0
- package/dist/database/adapters/neon.js +98 -0
- package/dist/database/adapters/pg-snapshot-restore.d.ts +28 -0
- package/dist/database/adapters/pg-snapshot-restore.js +68 -0
- package/dist/database/adapters/sqlite-copy.d.ts +32 -0
- package/dist/database/adapters/sqlite-copy.js +145 -0
- package/dist/database/connection-injection.d.ts +35 -0
- package/dist/database/connection-injection.js +93 -0
- package/dist/database/index.d.ts +12 -0
- package/dist/database/index.js +23 -0
- package/dist/database/registry.d.ts +13 -0
- package/dist/database/registry.js +27 -0
- package/dist/database/topology.d.ts +52 -0
- package/dist/database/topology.js +44 -0
- package/dist/database/types.d.ts +89 -0
- package/dist/database/types.js +26 -0
- package/dist/defaults.d.ts +5 -0
- package/dist/defaults.js +5 -0
- package/dist/design-authority.d.ts +40 -0
- package/dist/design-authority.js +71 -0
- package/dist/design-lookahead.d.ts +64 -0
- package/dist/design-lookahead.js +86 -0
- package/dist/design-quality-trend.d.ts +87 -0
- package/dist/design-quality-trend.js +190 -0
- package/dist/design-system-context.d.ts +46 -0
- package/dist/design-system-context.js +80 -0
- package/dist/design-system-correction-loop.d.ts +64 -0
- package/dist/design-system-correction-loop.js +128 -0
- package/dist/design-system-metrics.d.ts +61 -0
- package/dist/design-system-metrics.js +104 -0
- package/dist/design-system-stewardship.d.ts +22 -0
- package/dist/design-system-stewardship.js +85 -0
- package/dist/design-system-validation.d.ts +37 -0
- package/dist/design-system-validation.js +88 -0
- package/dist/dispatch/index.d.ts +4 -0
- package/dist/dispatch/index.js +4 -0
- package/dist/dispatch/merge-gate.d.ts +46 -0
- package/dist/dispatch/merge-gate.js +90 -0
- package/dist/dispatch/requeue.d.ts +57 -0
- package/dist/dispatch/requeue.js +131 -0
- package/dist/dispatch/worker-pool.d.ts +62 -0
- package/dist/dispatch/worker-pool.js +60 -0
- package/dist/execute.d.ts +40 -0
- package/dist/execute.js +295 -30
- package/dist/fix-ci.js +40 -3
- package/dist/fix-review.d.ts +66 -0
- package/dist/fix-review.js +448 -0
- package/dist/harness/adapters/claude-code.d.ts +29 -0
- package/dist/harness/adapters/claude-code.js +191 -0
- package/dist/harness/adapters/codex.d.ts +25 -0
- package/dist/harness/adapters/codex.js +61 -0
- package/dist/harness/independence.d.ts +51 -0
- package/dist/harness/independence.js +67 -0
- package/dist/harness/index.d.ts +14 -0
- package/dist/harness/index.js +20 -0
- package/dist/harness/registry.d.ts +17 -0
- package/dist/harness/registry.js +31 -0
- package/dist/harness/types.d.ts +123 -0
- package/dist/harness/types.js +8 -0
- package/dist/harness/version-probe.d.ts +14 -0
- package/dist/harness/version-probe.js +123 -0
- package/dist/index.d.ts +39 -5
- package/dist/index.js +48 -3
- package/dist/models/classifier.d.ts +76 -0
- package/dist/models/classifier.js +221 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.js +3 -0
- package/dist/models/registry.d.ts +97 -0
- package/dist/models/registry.js +173 -0
- package/dist/pillar-breakdown.d.ts +63 -0
- package/dist/pillar-breakdown.js +153 -0
- package/dist/pipeline-cycle-detector.d.ts +70 -0
- package/dist/pipeline-cycle-detector.js +111 -0
- package/dist/priority.d.ts +7 -76
- package/dist/priority.js +18 -5
- package/dist/review-meta.d.ts +65 -0
- package/dist/review-meta.js +149 -0
- package/dist/review.d.ts +35 -0
- package/dist/review.js +84 -0
- package/dist/runners/claude-code-sdk.d.ts +31 -0
- package/dist/runners/claude-code-sdk.js +238 -0
- package/dist/runners/claude-code.d.ts +3 -0
- package/dist/runners/claude-code.js +319 -73
- package/dist/runners/codex.js +4 -1
- package/dist/runners/copilot.js +4 -1
- package/dist/runners/cursor.js +4 -1
- package/dist/runners/git-utils.d.ts +81 -0
- package/dist/runners/git-utils.js +201 -0
- package/dist/runners/index.d.ts +5 -1
- package/dist/runners/index.js +4 -0
- package/dist/runners/review-agent.d.ts +67 -0
- package/dist/runners/review-agent.js +350 -0
- package/dist/runners/runner-registry.js +10 -0
- package/dist/runners/sdk-review-runner.d.ts +65 -0
- package/dist/runners/sdk-review-runner.js +185 -0
- package/dist/runners/security-triage.d.ts +20 -4
- package/dist/runners/security-triage.js +42 -14
- package/dist/runners/types.d.ts +25 -0
- package/dist/runtime/attestations.d.ts +274 -0
- package/dist/runtime/attestations.js +460 -0
- package/dist/runtime/git-env.d.ts +53 -0
- package/dist/runtime/git-env.js +60 -0
- package/dist/runtime/index.d.ts +7 -0
- package/dist/runtime/index.js +7 -0
- package/dist/runtime/parallelism-flag.d.ts +10 -0
- package/dist/runtime/parallelism-flag.js +18 -0
- package/dist/runtime/port-allocator.d.ts +21 -0
- package/dist/runtime/port-allocator.js +66 -0
- package/dist/runtime/worktree-pool.d.ts +86 -0
- package/dist/runtime/worktree-pool.js +204 -0
- package/dist/runtime/worktree.d.ts +25 -0
- package/dist/runtime/worktree.js +111 -0
- package/dist/sa-scoring/auto-calibrate.d.ts +69 -0
- package/dist/sa-scoring/auto-calibrate.js +107 -0
- package/dist/sa-scoring/c1-sa2-computable.d.ts +42 -0
- package/dist/sa-scoring/c1-sa2-computable.js +59 -0
- package/dist/sa-scoring/composite.d.ts +107 -0
- package/dist/sa-scoring/composite.js +139 -0
- package/dist/sa-scoring/depparse-client.d.ts +79 -0
- package/dist/sa-scoring/depparse-client.js +187 -0
- package/dist/sa-scoring/did-compiler.d.ts +122 -0
- package/dist/sa-scoring/did-compiler.js +286 -0
- package/dist/sa-scoring/drift-monitor.d.ts +84 -0
- package/dist/sa-scoring/drift-monitor.js +186 -0
- package/dist/sa-scoring/exemplar-bank.d.ts +78 -0
- package/dist/sa-scoring/exemplar-bank.js +154 -0
- package/dist/sa-scoring/feedback-store.d.ts +100 -0
- package/dist/sa-scoring/feedback-store.js +156 -0
- package/dist/sa-scoring/index.d.ts +71 -0
- package/dist/sa-scoring/index.js +158 -0
- package/dist/sa-scoring/layer1-deterministic.d.ts +115 -0
- package/dist/sa-scoring/layer1-deterministic.js +298 -0
- package/dist/sa-scoring/layer2-structural.d.ts +71 -0
- package/dist/sa-scoring/layer2-structural.js +151 -0
- package/dist/sa-scoring/layer3-llm.d.ts +86 -0
- package/dist/sa-scoring/layer3-llm.js +282 -0
- package/dist/sa-scoring/rescore-orchestrator.d.ts +52 -0
- package/dist/sa-scoring/rescore-orchestrator.js +47 -0
- package/dist/scheduling/burn-down.d.ts +27 -0
- package/dist/scheduling/burn-down.js +43 -0
- package/dist/scheduling/calibration.d.ts +42 -0
- package/dist/scheduling/calibration.js +150 -0
- package/dist/scheduling/index.d.ts +8 -0
- package/dist/scheduling/index.js +8 -0
- package/dist/scheduling/ledger.d.ts +59 -0
- package/dist/scheduling/ledger.js +216 -0
- package/dist/scheduling/off-peak.d.ts +27 -0
- package/dist/scheduling/off-peak.js +112 -0
- package/dist/scheduling/schedule-decision.d.ts +39 -0
- package/dist/scheduling/schedule-decision.js +89 -0
- package/dist/scheduling/tier-analysis.d.ts +47 -0
- package/dist/scheduling/tier-analysis.js +81 -0
- package/dist/scheduling/types.d.ts +140 -0
- package/dist/scheduling/types.js +11 -0
- package/dist/shared.d.ts +13 -0
- package/dist/shared.js +32 -0
- package/dist/state/index.d.ts +1 -1
- package/dist/state/schema.d.ts +6 -1
- package/dist/state/schema.js +276 -1
- package/dist/state/store.d.ts +71 -1
- package/dist/state/store.js +464 -7
- package/dist/state/types.d.ts +174 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate-agent-output.js +4 -1
- package/dist/watch.js +6 -0
- package/dist/workflow-patterns/artifact-writer.d.ts +16 -0
- package/dist/workflow-patterns/artifact-writer.js +34 -0
- package/dist/workflow-patterns/classifiers.d.ts +10 -0
- package/dist/workflow-patterns/classifiers.js +72 -0
- package/dist/workflow-patterns/detector.d.ts +27 -0
- package/dist/workflow-patterns/detector.js +186 -0
- package/dist/workflow-patterns/index.d.ts +8 -0
- package/dist/workflow-patterns/index.js +7 -0
- package/dist/workflow-patterns/proposal-generator.d.ts +15 -0
- package/dist/workflow-patterns/proposal-generator.js +183 -0
- package/dist/workflow-patterns/telemetry-ingest.d.ts +27 -0
- package/dist/workflow-patterns/telemetry-ingest.js +103 -0
- package/dist/workflow-patterns/types.d.ts +61 -0
- package/dist/workflow-patterns/types.js +11 -0
- 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,
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
238
|
-
await execFileAsync('git', ['
|
|
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.
|
|
363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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], {
|
|
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
|