@ai-sdlc/orchestrator 0.4.0 → 0.6.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/adapters.d.ts +18 -3
- package/dist/adapters.js +92 -2
- package/dist/admission-score.d.ts +58 -0
- package/dist/admission-score.js +164 -0
- package/dist/cli/commands/init.js +4 -8
- package/dist/cli/commands/run.js +2 -2
- package/dist/config.d.ts +3 -0
- package/dist/config.js +14 -5
- package/dist/cycle-utils.d.ts +51 -0
- package/dist/cycle-utils.js +77 -0
- package/dist/defaults.d.ts +5 -0
- package/dist/defaults.js +5 -0
- package/dist/execute.d.ts +5 -2
- package/dist/execute.js +212 -62
- package/dist/fix-ci.js +45 -13
- package/dist/fix-review.d.ts +66 -0
- package/dist/fix-review.js +441 -0
- package/dist/index.d.ts +14 -4
- package/dist/index.js +18 -3
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +31 -9
- package/dist/pipeline-cycle-detector.d.ts +70 -0
- package/dist/pipeline-cycle-detector.js +111 -0
- package/dist/plugin.d.ts +9 -3
- package/dist/priority.d.ts +28 -0
- package/dist/priority.js +230 -0
- package/dist/review.d.ts +31 -0
- package/dist/review.js +74 -0
- package/dist/runners/claude-code.js +367 -35
- package/dist/runners/codex.js +15 -4
- package/dist/runners/copilot.js +15 -4
- package/dist/runners/cursor.js +15 -4
- package/dist/runners/generic-llm.js +1 -1
- package/dist/runners/index.d.ts +3 -1
- package/dist/runners/index.js +2 -0
- package/dist/runners/review-agent.d.ts +47 -0
- package/dist/runners/review-agent.js +220 -0
- package/dist/runners/security-triage.d.ts +43 -0
- package/dist/runners/security-triage.js +158 -0
- package/dist/runners/types.d.ts +24 -1
- package/dist/security.d.ts +8 -3
- package/dist/security.js +13 -2
- package/dist/shared.d.ts +17 -0
- package/dist/shared.js +27 -0
- package/dist/state/index.d.ts +1 -1
- package/dist/state/schema.d.ts +4 -1
- package/dist/state/schema.js +89 -1
- package/dist/state/store.d.ts +31 -1
- package/dist/state/store.js +208 -13
- package/dist/state/types.d.ts +52 -0
- package/dist/triage.d.ts +36 -0
- package/dist/triage.js +133 -0
- package/dist/types.d.ts +1 -1
- package/dist/watch.d.ts +6 -2
- package/dist/watch.js +34 -6
- 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 +4 -2
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for cycle detection and handling.
|
|
3
|
+
*/
|
|
4
|
+
import { PipelineCycleDetector } from './pipeline-cycle-detector.js';
|
|
5
|
+
import { NOTIFICATION_TITLES } from './defaults.js';
|
|
6
|
+
/**
|
|
7
|
+
* Sanitize template content to prevent markdown/HTML injection.
|
|
8
|
+
* Strips HTML tags and limits length.
|
|
9
|
+
*/
|
|
10
|
+
function sanitizeTemplate(text) {
|
|
11
|
+
return text.replace(/<[^>]*>/g, '').slice(0, 2000);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Check for pipeline cycles and post notification if detected.
|
|
15
|
+
*
|
|
16
|
+
* The marker is generated upfront so the caller can append it to comments
|
|
17
|
+
* BEFORE executing the stage (records intent, prevents race conditions).
|
|
18
|
+
* Cycle detection accounts for the pending invocation (+1 to current count).
|
|
19
|
+
*/
|
|
20
|
+
export async function checkAndHandleCycle(options) {
|
|
21
|
+
const { issueOrPrId, stage, tracker, detector, notifySlack, cycleTemplate } = options;
|
|
22
|
+
// Detect cycles from existing comment markers (no +1 — marker is separate)
|
|
23
|
+
const cycleResult = await detector.detectCycle(tracker, issueOrPrId);
|
|
24
|
+
// Generate marker for the caller to append to comments
|
|
25
|
+
const marker = detector.recordInvocation(stage);
|
|
26
|
+
if (cycleResult.cycleDetected) {
|
|
27
|
+
const loopingSummary = cycleResult.loopingStages
|
|
28
|
+
.map((s) => `- **${s.stage}**: ${s.count}/${s.max} invocations`)
|
|
29
|
+
.join('\n');
|
|
30
|
+
const title = sanitizeTemplate(cycleTemplate?.title ?? NOTIFICATION_TITLES.pipelineCycleDetected);
|
|
31
|
+
const body = sanitizeTemplate(cycleTemplate?.body ??
|
|
32
|
+
`The pipeline has detected an infinite loop across the following stages:\n\n${loopingSummary}\n\n**Manual intervention is required** to resolve the cycle. Please review the issue and PR to determine the root cause.`);
|
|
33
|
+
const cycleMessage = `## ${title}\n\n${body}`;
|
|
34
|
+
// Post cycle detection comment
|
|
35
|
+
await tracker.addComment(issueOrPrId, cycleMessage);
|
|
36
|
+
// Send Slack notification if configured
|
|
37
|
+
if (notifySlack) {
|
|
38
|
+
const slackMessage = `:warning: *Pipeline Cycle Detected* for #${issueOrPrId}\n\nLooping stages:\n${cycleResult.loopingStages.map((s) => `• ${s.stage}: ${s.count}/${s.max}`).join('\n')}`;
|
|
39
|
+
await notifySlack(slackMessage).catch((err) => {
|
|
40
|
+
console.error('[cycle-detector] Slack notification failed:', err instanceof Error ? err.message : String(err));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return { cycleDetected: true, marker, cycleMessage };
|
|
44
|
+
}
|
|
45
|
+
return { cycleDetected: false, marker };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Create a cycle detector from pipeline config.
|
|
49
|
+
* Reads maxRetries from stage configurations.
|
|
50
|
+
*/
|
|
51
|
+
export function createCycleDetectorFromConfig(config) {
|
|
52
|
+
const detector = new PipelineCycleDetector();
|
|
53
|
+
if (config.stages) {
|
|
54
|
+
const overrides = {};
|
|
55
|
+
for (const stage of config.stages) {
|
|
56
|
+
const maxRetries = stage.onFailure?.maxRetries;
|
|
57
|
+
if (maxRetries !== undefined) {
|
|
58
|
+
if (stage.name === 'code') {
|
|
59
|
+
overrides['fix-ci'] = maxRetries;
|
|
60
|
+
}
|
|
61
|
+
else if (stage.name === 'review') {
|
|
62
|
+
overrides['fix-review'] = maxRetries;
|
|
63
|
+
}
|
|
64
|
+
else if (stage.name === 'admission' ||
|
|
65
|
+
stage.name === 'triage' ||
|
|
66
|
+
stage.name === 'agent') {
|
|
67
|
+
overrides[stage.name] = maxRetries;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (Object.keys(overrides).length > 0) {
|
|
72
|
+
detector.updateMaxInvocations(overrides);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return detector;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=cycle-utils.js.map
|
package/dist/defaults.d.ts
CHANGED
|
@@ -136,5 +136,10 @@ export declare const NOTIFICATION_TITLES: {
|
|
|
136
136
|
readonly fixCIAgentFailed: "AI-SDLC: Fix-CI Agent Failed";
|
|
137
137
|
readonly fixCIGuardrailViolations: "AI-SDLC: Fix-CI Guardrail Violations";
|
|
138
138
|
readonly fixCIApplied: "AI-SDLC: Fix-CI Applied";
|
|
139
|
+
readonly fixReviewRetryLimit: "AI-SDLC: Fix-Review Retry Limit Reached";
|
|
140
|
+
readonly fixReviewAgentFailed: "AI-SDLC: Fix-Review Agent Failed";
|
|
141
|
+
readonly fixReviewGuardrailViolations: "AI-SDLC: Fix-Review Guardrail Violations";
|
|
142
|
+
readonly fixReviewApplied: "AI-SDLC: Fix-Review Applied";
|
|
143
|
+
readonly pipelineCycleDetected: "AI-SDLC: Pipeline Cycle Detected";
|
|
139
144
|
};
|
|
140
145
|
//# sourceMappingURL=defaults.d.ts.map
|
package/dist/defaults.js
CHANGED
|
@@ -214,5 +214,10 @@ export const NOTIFICATION_TITLES = {
|
|
|
214
214
|
fixCIAgentFailed: 'AI-SDLC: Fix-CI Agent Failed',
|
|
215
215
|
fixCIGuardrailViolations: 'AI-SDLC: Fix-CI Guardrail Violations',
|
|
216
216
|
fixCIApplied: 'AI-SDLC: Fix-CI Applied',
|
|
217
|
+
fixReviewRetryLimit: 'AI-SDLC: Fix-Review Retry Limit Reached',
|
|
218
|
+
fixReviewAgentFailed: 'AI-SDLC: Fix-Review Agent Failed',
|
|
219
|
+
fixReviewGuardrailViolations: 'AI-SDLC: Fix-Review Guardrail Violations',
|
|
220
|
+
fixReviewApplied: 'AI-SDLC: Fix-Review Applied',
|
|
221
|
+
pipelineCycleDetected: 'AI-SDLC: Pipeline Cycle Detected',
|
|
217
222
|
};
|
|
218
223
|
//# sourceMappingURL=defaults.js.map
|
package/dist/execute.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { CodebaseContext } from './analysis/types.js';
|
|
|
14
14
|
import type { AutonomyTracker } from './autonomy-tracker.js';
|
|
15
15
|
import { CostTracker } from './cost-tracker.js';
|
|
16
16
|
import type { StateStore } from './state/store.js';
|
|
17
|
+
import type { PriorityScore } from './priority.js';
|
|
17
18
|
export interface PipelineResult {
|
|
18
19
|
prUrl: string;
|
|
19
20
|
filesChanged: string[];
|
|
@@ -73,9 +74,11 @@ export interface ExecuteOptions {
|
|
|
73
74
|
costTracker?: CostTracker;
|
|
74
75
|
/** StateStore for episodic context enrichment. */
|
|
75
76
|
stateStore?: StateStore;
|
|
77
|
+
/** Priority score from PPA scoring (set by watch loop or caller). */
|
|
78
|
+
priorityScore?: PriorityScore;
|
|
76
79
|
}
|
|
77
80
|
/**
|
|
78
|
-
* Execute the full AI-SDLC pipeline for a given issue
|
|
81
|
+
* Execute the full AI-SDLC pipeline for a given issue ID.
|
|
79
82
|
*/
|
|
80
|
-
export declare function executePipeline(
|
|
83
|
+
export declare function executePipeline(issueId: string, options?: ExecuteOptions): Promise<PipelineResult>;
|
|
81
84
|
//# sourceMappingURL=execute.d.ts.map
|
package/dist/execute.js
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
* load config -> fetch issue -> validate -> check autonomy ->
|
|
6
6
|
* create branch -> invoke agent -> authorize -> validate -> push -> create PR -> comment
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
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, } from './shared.js';
|
|
15
16
|
import { checkKillSwitch, issueAgentCredentials, revokeAgentCredentials, classifyAndSubmitApproval, createPipelineSecurity, } from './security.js';
|
|
16
17
|
import { createPipelineOrchestration, executePipelineOrchestration, validatePipelineHandoffs, } from './orchestration.js';
|
|
17
18
|
import { createPipelineProvenance, attachProvenanceToPR, validatePipelineProvenance, } from './provenance.js';
|
|
@@ -21,8 +22,10 @@ import { createPipelineExpressionEvaluator, createPipelineLLMEvaluator, createPi
|
|
|
21
22
|
import { verifyAuditIntegrity, createFileAuditLog, loadAuditEntries, computeAuditHash, } from './audit-extended.js';
|
|
22
23
|
import { renderTemplate } from './notifications.js';
|
|
23
24
|
import { admitIssueResource, createPipelineAdmission, } from './admission.js';
|
|
24
|
-
import { createPipelineAdapterRegistry, createPipelineWebhookBridge, resolveAdapterFromGit, scanPipelineAdapters, } from './adapters.js';
|
|
25
|
-
import {
|
|
25
|
+
import { createPipelineAdapterRegistry, createPipelineWebhookBridge, resolveAdapterFromGit, scanPipelineAdapters, resolveIssueTrackerFromConfig, } from './adapters.js';
|
|
26
|
+
import { existsSync, readFileSync, appendFileSync } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
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';
|
|
26
29
|
import { checkFrameworkCompliance, getControlCatalog, getFrameworkMappings, listSupportedFrameworks, } from './compliance-extended.js';
|
|
27
30
|
import { createSilentLogger, withPipelineSpanSync, getPipelineTracer, validateResourceSchema, } from './telemetry-extended.js';
|
|
28
31
|
import { createPipelineMemory } from './shared.js';
|
|
@@ -31,9 +34,9 @@ import { CostTracker } from './cost-tracker.js';
|
|
|
31
34
|
import { enrichAgentContext } from './context-enrichment.js';
|
|
32
35
|
import { reportGateCheckRuns } from './check-runs.js';
|
|
33
36
|
/**
|
|
34
|
-
* Execute the full AI-SDLC pipeline for a given issue
|
|
37
|
+
* Execute the full AI-SDLC pipeline for a given issue ID.
|
|
35
38
|
*/
|
|
36
|
-
export async function executePipeline(
|
|
39
|
+
export async function executePipeline(issueId, options = {}) {
|
|
37
40
|
const workDir = options.workDir ?? (await resolveRepoRoot());
|
|
38
41
|
const configDir = options.configDir ?? `${workDir}/${DEFAULT_CONFIG_DIR_NAME}`;
|
|
39
42
|
const log = options.logger ??
|
|
@@ -86,22 +89,22 @@ export async function executePipeline(issueNumber, options = {}) {
|
|
|
86
89
|
// 2. Create adapters (or use injected ones)
|
|
87
90
|
const { org, repo } = getGitHubConfig(options.secretStore);
|
|
88
91
|
const ghConfig = { org, repo, token: { secretRef: 'github-token' } };
|
|
89
|
-
const tracker = options.tracker ??
|
|
92
|
+
const tracker = options.tracker ?? resolveIssueTrackerFromConfig(config, ghConfig);
|
|
90
93
|
const sc = options.sourceControl ?? createGitHubSourceControl(ghConfig);
|
|
91
94
|
// 3. Fetch issue
|
|
92
95
|
log.stage('validate-issue');
|
|
93
|
-
const issue = await tracker.getIssue(
|
|
96
|
+
const issue = await tracker.getIssue(issueId);
|
|
94
97
|
// Store issue context in working memory if provided
|
|
95
98
|
if (options.memory) {
|
|
96
99
|
options.memory.working.set('currentIssue', {
|
|
97
|
-
|
|
100
|
+
issueId,
|
|
98
101
|
title: issue.title,
|
|
99
102
|
description: issue.description,
|
|
100
103
|
});
|
|
101
104
|
}
|
|
102
105
|
// Wrap pipeline body in try/catch to record failure episodes
|
|
103
106
|
try {
|
|
104
|
-
return await executePipelineBody(
|
|
107
|
+
return await executePipelineBody(issueId, issue, config, qualityGate, agentRole, autonomyPolicy, tracker, sc, auditLog, metricStore, options, log, workDir);
|
|
105
108
|
}
|
|
106
109
|
catch (err) {
|
|
107
110
|
// Record failure episode before rethrowing
|
|
@@ -109,19 +112,50 @@ export async function executePipeline(issueNumber, options = {}) {
|
|
|
109
112
|
options.memory.episodic.append({
|
|
110
113
|
key: 'pipeline-execution',
|
|
111
114
|
value: {
|
|
112
|
-
|
|
115
|
+
issueId,
|
|
113
116
|
outcome: 'failure',
|
|
114
117
|
error: err instanceof Error ? err.message : String(err),
|
|
115
118
|
},
|
|
116
|
-
metadata: { summary: `Failed issue
|
|
119
|
+
metadata: { summary: `Failed issue ${formatIssueRef(issueId)}: ${issue.title}` },
|
|
117
120
|
});
|
|
118
121
|
options.memory.working.clear();
|
|
119
122
|
}
|
|
120
123
|
throw err;
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
|
-
async function executePipelineBody(
|
|
126
|
+
async function executePipelineBody(issueId, issue, config, qualityGate, agentRole, autonomyPolicy, tracker, sc, auditLog, metricStore, options, log, workDir) {
|
|
127
|
+
const issueNumber = issueIdToNumber(issueId);
|
|
124
128
|
const meter = getMeter();
|
|
129
|
+
// Slack notifications (optional — activates when SLACK_BOT_TOKEN is set)
|
|
130
|
+
let slackThreadId;
|
|
131
|
+
const slackToken = process.env.SLACK_BOT_TOKEN;
|
|
132
|
+
const slackChannel = process.env.SLACK_CHANNEL;
|
|
133
|
+
const notifySlack = async (message) => {
|
|
134
|
+
if (!slackToken || !slackChannel)
|
|
135
|
+
return;
|
|
136
|
+
try {
|
|
137
|
+
const body = { channel: slackChannel, text: message };
|
|
138
|
+
if (slackThreadId)
|
|
139
|
+
body.thread_ts = slackThreadId;
|
|
140
|
+
const res = await fetch('https://slack.com/api/chat.postMessage', {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: {
|
|
143
|
+
'Content-Type': 'application/json',
|
|
144
|
+
Authorization: `Bearer ${slackToken}`,
|
|
145
|
+
},
|
|
146
|
+
body: JSON.stringify(body),
|
|
147
|
+
});
|
|
148
|
+
const data = (await res.json());
|
|
149
|
+
// Capture the first message's timestamp as thread ID
|
|
150
|
+
if (data.ok && !slackThreadId && data.ts) {
|
|
151
|
+
slackThreadId = data.ts;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Best-effort — don't fail the pipeline for Slack
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
await notifySlack(`:rocket: *Pipeline started* for <https://github.com/${process.env.GITHUB_REPOSITORY ?? ''}/issues/${issueId}|#${issueId}: ${issue.title}>`);
|
|
125
159
|
// Discovery: register agent and resolve by issue labels
|
|
126
160
|
const discovery = createPipelineDiscovery();
|
|
127
161
|
discovery.register(agentRole);
|
|
@@ -167,7 +201,7 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
167
201
|
auditLog.record({
|
|
168
202
|
actor: 'system',
|
|
169
203
|
action: 'evaluate',
|
|
170
|
-
resource: `issue#${
|
|
204
|
+
resource: `issue#${issueId}`,
|
|
171
205
|
policy: qualityGate.metadata.name,
|
|
172
206
|
decision: 'denied',
|
|
173
207
|
details: {
|
|
@@ -183,13 +217,13 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
183
217
|
const gateFailTitle = gateFailTpl
|
|
184
218
|
? renderTemplate(gateFailTpl, { details: failures }).title
|
|
185
219
|
: NOTIFICATION_TITLES.issueValidationFailed;
|
|
186
|
-
await tracker.addComment(
|
|
187
|
-
throw new Error(`Issue
|
|
220
|
+
await tracker.addComment(issueId, `## ${gateFailTitle}\n\n${gateFailBody}`);
|
|
221
|
+
throw new Error(`Issue ${formatIssueRef(issueId)} failed quality gate validation:\n${failures}`);
|
|
188
222
|
}
|
|
189
223
|
auditLog.record({
|
|
190
224
|
actor: 'system',
|
|
191
225
|
action: 'evaluate',
|
|
192
|
-
resource: `issue#${
|
|
226
|
+
resource: `issue#${issueId}`,
|
|
193
227
|
policy: qualityGate.metadata.name,
|
|
194
228
|
decision: 'allowed',
|
|
195
229
|
});
|
|
@@ -202,7 +236,7 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
202
236
|
auditLog.record({
|
|
203
237
|
actor: 'system',
|
|
204
238
|
action: 'route',
|
|
205
|
-
resource: `issue#${
|
|
239
|
+
resource: `issue#${issueId}`,
|
|
206
240
|
decision: isAutonomousStrategy(strategy) ? 'allowed' : 'denied',
|
|
207
241
|
details: { score: complexity, strategy },
|
|
208
242
|
});
|
|
@@ -214,21 +248,21 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
214
248
|
title: NOTIFICATION_TITLES.complexityTooHigh,
|
|
215
249
|
body: `Issue complexity (${complexity}) routed as "${strategy}" — requires human involvement.`,
|
|
216
250
|
};
|
|
217
|
-
await tracker.addComment(
|
|
218
|
-
throw new Error(`Issue
|
|
251
|
+
await tracker.addComment(issueId, `## ${complexityComment.title}\n\n${complexityComment.body}`);
|
|
252
|
+
throw new Error(`Issue ${formatIssueRef(issueId)} complexity ${complexity} routed as "${strategy}"`);
|
|
219
253
|
}
|
|
220
254
|
// Approval workflow check (after routing, before agent)
|
|
221
255
|
if (options.security) {
|
|
222
|
-
const approval = await classifyAndSubmitApproval(options.security, complexity, agentRole.metadata.name, `Execute pipeline for issue
|
|
256
|
+
const approval = await classifyAndSubmitApproval(options.security, complexity, agentRole.metadata.name, `Execute pipeline for issue ${formatIssueRef(issueId)}`);
|
|
223
257
|
if (approval.status === 'pending') {
|
|
224
|
-
throw new Error(`Issue
|
|
258
|
+
throw new Error(`Issue ${formatIssueRef(issueId)} requires approval (tier: ${approval.tier}) — status is pending`);
|
|
225
259
|
}
|
|
226
260
|
}
|
|
227
261
|
// 6. Check autonomy level allows coding
|
|
228
262
|
const currentLevel = resolveAutonomyLevel(autonomyPolicy);
|
|
229
263
|
log.stageEnd('validate-issue');
|
|
230
264
|
// 7. Create branch and checkout locally (read pattern from pipeline config)
|
|
231
|
-
const branchVars = { issueNumber:
|
|
265
|
+
const branchVars = { issueNumber: issueId, issueTitle: issue.title };
|
|
232
266
|
const branchName = interpolateBranchPattern(config.pipeline?.spec.branching?.pattern, branchVars);
|
|
233
267
|
await sc.createBranch({ name: branchName });
|
|
234
268
|
await execFileAsync('git', ['fetch', 'origin', branchName], { cwd: workDir });
|
|
@@ -248,9 +282,31 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
248
282
|
log.info(`Model selected: ${modelResult.model} (${modelResult.reason})`);
|
|
249
283
|
}
|
|
250
284
|
}
|
|
285
|
+
// 8b. Ensure .gitignore covers runtime artifacts so the agent doesn't
|
|
286
|
+
// re-add entries on every run.
|
|
287
|
+
ensureRuntimeGitignore(workDir);
|
|
251
288
|
// 9. Invoke agent (with sandbox + JIT credential lifecycle when security is provided)
|
|
252
289
|
log.stage('agent');
|
|
290
|
+
// Post progress comment so users can see the pipeline is working
|
|
291
|
+
await tracker.addComment(issueId, `## AI-SDLC: Agent Started\n\n` +
|
|
292
|
+
`The AI agent is now working on this issue on branch \`${branchName}\`.\n\n` +
|
|
293
|
+
`| Detail | Value |\n|---|---|\n` +
|
|
294
|
+
`| Model | ${selectedModel ?? 'default'} |\n` +
|
|
295
|
+
`| Complexity | ${complexity} |\n` +
|
|
296
|
+
`| Strategy | ${strategy} |\n`);
|
|
297
|
+
await notifySlack(`:hammer_and_wrench: Agent working on \`${branchName}\` (model: ${selectedModel ?? 'default'}, complexity: ${complexity})`);
|
|
253
298
|
const runner = options.runner ?? new ClaudeCodeRunner();
|
|
299
|
+
// Set up progress tracking — collects streaming events for the activity log
|
|
300
|
+
const progressLog = [];
|
|
301
|
+
const onProgress = (event) => {
|
|
302
|
+
const timestamp = new Date().toISOString().slice(11, 19);
|
|
303
|
+
if (event.type === 'tool_start' && event.tool) {
|
|
304
|
+
progressLog.push(`[${timestamp}] ${event.tool}${event.file ? `: ${event.file}` : ''}`);
|
|
305
|
+
}
|
|
306
|
+
else if (event.type === 'cost') {
|
|
307
|
+
progressLog.push(`[${timestamp}] Cost: $${event.costUsd?.toFixed(4)}`);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
254
310
|
// Sandbox isolation around agent execution
|
|
255
311
|
const codeStage = config.pipeline?.spec.stages.find((s) => s.name === 'code');
|
|
256
312
|
let sandboxId;
|
|
@@ -258,7 +314,7 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
258
314
|
try {
|
|
259
315
|
if (options.security) {
|
|
260
316
|
const timeoutMs = codeStage?.timeout ? parseDuration(codeStage.timeout) : undefined;
|
|
261
|
-
sandboxId = await options.security.sandbox.isolate(`issue-${
|
|
317
|
+
sandboxId = await options.security.sandbox.isolate(`issue-${issueId}`, defaultSandboxConstraints(workDir, timeoutMs));
|
|
262
318
|
}
|
|
263
319
|
// Issue JIT credentials before agent execution
|
|
264
320
|
const jitCred = options.security
|
|
@@ -272,7 +328,7 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
272
328
|
// Enrich agent context with episodic memory
|
|
273
329
|
const episodicContext = options.stateStore
|
|
274
330
|
? enrichAgentContext(options.stateStore, {
|
|
275
|
-
issueNumber,
|
|
331
|
+
issueNumber: issueNumber ?? undefined,
|
|
276
332
|
agentName: effectiveAgent.metadata.name,
|
|
277
333
|
files: result ? result.filesChanged : undefined,
|
|
278
334
|
})
|
|
@@ -280,9 +336,10 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
280
336
|
const orchestrationResult = await executePipelineOrchestration(plan, [effectiveAgent], async (agent) => {
|
|
281
337
|
return withSpan(SPAN_NAMES.AGENT_TASK, {
|
|
282
338
|
[ATTRIBUTE_KEYS.AGENT]: agent.metadata.name,
|
|
283
|
-
[ATTRIBUTE_KEYS.RESOURCE_NAME]: `issue#${
|
|
339
|
+
[ATTRIBUTE_KEYS.RESOURCE_NAME]: `issue#${issueId}`,
|
|
284
340
|
}, () => runner.run({
|
|
285
|
-
|
|
341
|
+
issueId,
|
|
342
|
+
issueNumber: issueNumber ?? undefined,
|
|
286
343
|
issueTitle: issue.title,
|
|
287
344
|
issueBody: issue.description ?? '',
|
|
288
345
|
workDir,
|
|
@@ -296,6 +353,13 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
296
353
|
memory: options.memory,
|
|
297
354
|
codebaseContext: options.codebaseContext,
|
|
298
355
|
episodicContext,
|
|
356
|
+
sandboxId,
|
|
357
|
+
lintCommand: DEFAULT_LINT_COMMAND,
|
|
358
|
+
formatCommand: DEFAULT_FORMAT_COMMAND,
|
|
359
|
+
typecheckCommand: process.env.AI_SDLC_TYPECHECK_COMMAND,
|
|
360
|
+
commitMessageTemplate: DEFAULT_COMMIT_MESSAGE_TEMPLATE,
|
|
361
|
+
commitCoAuthor: DEFAULT_COMMIT_CO_AUTHOR,
|
|
362
|
+
onProgress,
|
|
299
363
|
}));
|
|
300
364
|
});
|
|
301
365
|
// Extract the AgentResult from orchestration output
|
|
@@ -319,8 +383,8 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
319
383
|
const agentFailComment = agentFailTpl
|
|
320
384
|
? renderTemplate(agentFailTpl, { stageName: 'code', details: errorDetail })
|
|
321
385
|
: { title: NOTIFICATION_TITLES.agentFailed, body: errorDetail };
|
|
322
|
-
await tracker.addComment(
|
|
323
|
-
throw new Error(`Agent failed on issue
|
|
386
|
+
await tracker.addComment(issueId, `## ${agentFailComment.title}\n\n${agentFailComment.body}`);
|
|
387
|
+
throw new Error(`Agent failed on issue ${formatIssueRef(issueId)}: ${err}`);
|
|
324
388
|
}
|
|
325
389
|
result = stepOutput;
|
|
326
390
|
log.stageEnd('agent');
|
|
@@ -351,11 +415,31 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
351
415
|
if (currentLevel.permissions.write.length > 0) {
|
|
352
416
|
authorizeFilesChanged(result.filesChanged, currentLevel.permissions, agentRole.spec.constraints, auditLog, agentRole.metadata.name);
|
|
353
417
|
}
|
|
354
|
-
// 11.
|
|
355
|
-
|
|
418
|
+
// 11. Post activity log so users can see what the agent did
|
|
419
|
+
const activitySection = progressLog.length > 0
|
|
420
|
+
? `### Activity Log\n\n\`\`\`\n${progressLog.slice(-40).join('\n')}\n\`\`\``
|
|
421
|
+
: '';
|
|
422
|
+
await tracker.addComment(issueId, `## AI-SDLC: Agent Complete\n\n` +
|
|
423
|
+
`**${result.filesChanged.length} files changed.** Pushing branch...\n\n` +
|
|
424
|
+
activitySection);
|
|
425
|
+
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)
|
|
427
|
+
log.stage('push');
|
|
428
|
+
await execFileAsync('git', ['push', 'origin', branchName], { cwd: workDir });
|
|
429
|
+
log.stageEnd('push');
|
|
430
|
+
auditLog.record({
|
|
431
|
+
actor: 'system',
|
|
432
|
+
action: 'push',
|
|
433
|
+
resource: `branch/${branchName}`,
|
|
434
|
+
decision: 'allowed',
|
|
435
|
+
details: { filesChanged: result.filesChanged.length, issueId },
|
|
436
|
+
});
|
|
437
|
+
// 13. Validate agent output against guardrails (after push)
|
|
438
|
+
const validation = await withSpan(SPAN_NAMES.PIPELINE_STAGE, {
|
|
356
439
|
[ATTRIBUTE_KEYS.STAGE]: 'validate-output',
|
|
357
440
|
}, async () => {
|
|
358
|
-
|
|
441
|
+
log.stage('validate-output');
|
|
442
|
+
const validationResult = await validateAgentOutput({
|
|
359
443
|
filesChanged: result.filesChanged,
|
|
360
444
|
workDir,
|
|
361
445
|
constraints: {
|
|
@@ -364,14 +448,37 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
364
448
|
blockedPaths: resolved.blockedPaths,
|
|
365
449
|
},
|
|
366
450
|
guardrails: { maxLinesPerPR: currentLevel.guardrails.maxLinesPerPR },
|
|
367
|
-
auditLog,
|
|
368
|
-
log,
|
|
369
|
-
onViolation: async (violationList) => {
|
|
370
|
-
await tracker.addComment(String(issueNumber), `## ${NOTIFICATION_TITLES.guardrailViolations}\n\n${violationList}`);
|
|
371
|
-
},
|
|
372
451
|
});
|
|
452
|
+
log.stageEnd('validate-output');
|
|
453
|
+
return validationResult;
|
|
373
454
|
});
|
|
374
|
-
|
|
455
|
+
if (!validation.passed) {
|
|
456
|
+
// Record validation failure in audit log
|
|
457
|
+
auditLog.record({
|
|
458
|
+
actor: 'system',
|
|
459
|
+
action: 'check',
|
|
460
|
+
resource: 'agent-output',
|
|
461
|
+
decision: 'denied',
|
|
462
|
+
details: { violations: validation.violations.map((v) => v.rule) },
|
|
463
|
+
});
|
|
464
|
+
// Post comment explaining the violations
|
|
465
|
+
const violationList = validation.violations
|
|
466
|
+
.map((v) => `- **${v.rule}**: ${v.message}`)
|
|
467
|
+
.join('\n');
|
|
468
|
+
await tracker.addComment(issueId, `## ${NOTIFICATION_TITLES.guardrailViolations}\n\n${violationList}\n\n` +
|
|
469
|
+
`The branch \`${branchName}\` has been pushed with your changes. You can review the work, ` +
|
|
470
|
+
`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.`);
|
|
473
|
+
}
|
|
474
|
+
// Validation passed — record success
|
|
475
|
+
auditLog.record({
|
|
476
|
+
actor: 'system',
|
|
477
|
+
action: 'check',
|
|
478
|
+
resource: 'agent-output',
|
|
479
|
+
decision: 'allowed',
|
|
480
|
+
});
|
|
481
|
+
// 13b. Evaluate quality gates and report as GitHub Check Runs
|
|
375
482
|
if (qualityGate.spec.gates.length > 0) {
|
|
376
483
|
try {
|
|
377
484
|
const { stdout: headSha } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
|
|
@@ -396,7 +503,7 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
396
503
|
auditLog.record({
|
|
397
504
|
actor: 'system',
|
|
398
505
|
action: 'evaluate',
|
|
399
|
-
resource: `issue#${
|
|
506
|
+
resource: `issue#${issueId}`,
|
|
400
507
|
policy: 'post-agent-gates',
|
|
401
508
|
decision: gateResults.every((g) => g.verdict === 'pass') ? 'allowed' : 'denied',
|
|
402
509
|
details: {
|
|
@@ -408,7 +515,7 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
408
515
|
log.info('Post-agent gate evaluation skipped');
|
|
409
516
|
}
|
|
410
517
|
}
|
|
411
|
-
//
|
|
518
|
+
// 13c. Post-agent complexity evaluation (non-blocking)
|
|
412
519
|
try {
|
|
413
520
|
const { stdout: diffStat } = await execFileAsync('git', ['diff', '--stat', 'HEAD~1'], {
|
|
414
521
|
cwd: workDir,
|
|
@@ -423,7 +530,7 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
423
530
|
auditLog.record({
|
|
424
531
|
actor: 'system',
|
|
425
532
|
action: 'evaluate',
|
|
426
|
-
resource: `issue#${
|
|
533
|
+
resource: `issue#${issueId}`,
|
|
427
534
|
policy: 'post-agent-complexity',
|
|
428
535
|
decision: 'allowed',
|
|
429
536
|
details: {
|
|
@@ -437,11 +544,7 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
437
544
|
catch {
|
|
438
545
|
log.info('Post-agent complexity evaluation skipped');
|
|
439
546
|
}
|
|
440
|
-
//
|
|
441
|
-
log.stage('push');
|
|
442
|
-
await execFileAsync('git', ['push', 'origin', branchName], { cwd: workDir });
|
|
443
|
-
log.stageEnd('push');
|
|
444
|
-
// 12b. Compute cost receipt for provenance (before PR creation)
|
|
547
|
+
// 14. Compute cost receipt for provenance (before PR creation)
|
|
445
548
|
let costReceipt;
|
|
446
549
|
if (result.tokenUsage) {
|
|
447
550
|
const tu = result.tokenUsage;
|
|
@@ -459,7 +562,7 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
459
562
|
},
|
|
460
563
|
};
|
|
461
564
|
}
|
|
462
|
-
//
|
|
565
|
+
// 15. Create PR (with optional provenance, reading config from pipeline)
|
|
463
566
|
const prConfig = config.pipeline?.spec.pullRequest;
|
|
464
567
|
const shouldIncludeProvenance = options.includeProvenance ?? prConfig?.includeProvenance ?? options.security !== undefined;
|
|
465
568
|
let provenanceBlock = '';
|
|
@@ -468,9 +571,14 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
468
571
|
promptText: issue.description,
|
|
469
572
|
cost: costReceipt,
|
|
470
573
|
});
|
|
471
|
-
|
|
574
|
+
let provenanceText = attachProvenanceToPR(provenance);
|
|
575
|
+
// Include priority score in provenance section when available
|
|
576
|
+
if (options.priorityScore) {
|
|
577
|
+
provenanceText += `\n- **Priority Score**: ${options.priorityScore.composite.toFixed(4)} (confidence: ${options.priorityScore.confidence.toFixed(2)})`;
|
|
578
|
+
}
|
|
579
|
+
provenanceBlock = '\n\n' + provenanceText;
|
|
472
580
|
}
|
|
473
|
-
const prVars = { issueNumber:
|
|
581
|
+
const prVars = { issueNumber: issueId, issueTitle: issue.title };
|
|
474
582
|
const prTitle = interpolatePRTitle(prConfig?.titleTemplate, prVars);
|
|
475
583
|
const closeKeyword = prConfig?.closeKeyword ?? 'Closes';
|
|
476
584
|
const targetBranch = config.pipeline?.spec.branching?.targetBranch ?? 'main';
|
|
@@ -486,7 +594,7 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
486
594
|
if (section === 'changes')
|
|
487
595
|
parts.push('## Changes', '', result.filesChanged.map((f) => `- \`${f}\``).join('\n'));
|
|
488
596
|
if (section === 'closes')
|
|
489
|
-
parts.push('', `${closeKeyword}
|
|
597
|
+
parts.push('', `${closeKeyword} ${formatIssueRef(issueId)}`);
|
|
490
598
|
}
|
|
491
599
|
const footer = options.prFooter ?? DEFAULT_PR_FOOTER;
|
|
492
600
|
parts.push('', '---', footer);
|
|
@@ -504,7 +612,7 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
504
612
|
action: 'create',
|
|
505
613
|
resource: 'pull-request',
|
|
506
614
|
decision: 'allowed',
|
|
507
|
-
details: { prUrl: prResult.url,
|
|
615
|
+
details: { prUrl: prResult.url, issueId },
|
|
508
616
|
});
|
|
509
617
|
return prResult;
|
|
510
618
|
});
|
|
@@ -514,15 +622,16 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
514
622
|
const prCreatedComment = prCreatedTemplate
|
|
515
623
|
? renderTemplate(prCreatedTemplate, {
|
|
516
624
|
prUrl: pr.url,
|
|
517
|
-
issueNumber:
|
|
625
|
+
issueNumber: issueId,
|
|
518
626
|
})
|
|
519
627
|
: { title: NOTIFICATION_TITLES.prCreated, body: `Pull request created: ${pr.url}` };
|
|
520
|
-
await tracker.addComment(
|
|
628
|
+
await tracker.addComment(issueId, `## ${prCreatedComment.title}\n\n${prCreatedComment.body}`);
|
|
629
|
+
await notifySlack(`:pull_request: PR created: ${pr.url}`);
|
|
521
630
|
// 14b. Record cost from agent result
|
|
522
631
|
if (options.costTracker && result.tokenUsage) {
|
|
523
632
|
const tu = result.tokenUsage;
|
|
524
633
|
options.costTracker.recordCost({
|
|
525
|
-
runId: `run-${Date.now()}-${
|
|
634
|
+
runId: `run-${Date.now()}-${issueId}`,
|
|
526
635
|
agentName: agentRole.metadata.name,
|
|
527
636
|
pipelineType: 'execute',
|
|
528
637
|
model: tu.model,
|
|
@@ -530,7 +639,7 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
530
639
|
outputTokens: tu.outputTokens,
|
|
531
640
|
cacheReadTokens: tu.cacheReadTokens,
|
|
532
641
|
stageName: 'code',
|
|
533
|
-
issueNumber,
|
|
642
|
+
issueNumber: issueNumber ?? undefined,
|
|
534
643
|
});
|
|
535
644
|
}
|
|
536
645
|
// 14c. Record task outcome in autonomy tracker
|
|
@@ -617,18 +726,35 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
|
|
|
617
726
|
}
|
|
618
727
|
// 17. Record episodic memory (success)
|
|
619
728
|
if (options.memory) {
|
|
729
|
+
const episodicValue = {
|
|
730
|
+
issueId,
|
|
731
|
+
prUrl: pr.url,
|
|
732
|
+
filesChanged: result.filesChanged.length,
|
|
733
|
+
outcome: 'success',
|
|
734
|
+
};
|
|
735
|
+
if (options.priorityScore) {
|
|
736
|
+
episodicValue.priorityComposite = options.priorityScore.composite;
|
|
737
|
+
episodicValue.priorityConfidence = options.priorityScore.confidence;
|
|
738
|
+
}
|
|
620
739
|
options.memory.episodic.append({
|
|
621
740
|
key: 'pipeline-execution',
|
|
622
|
-
value:
|
|
623
|
-
|
|
624
|
-
prUrl: pr.url,
|
|
625
|
-
filesChanged: result.filesChanged.length,
|
|
626
|
-
outcome: 'success',
|
|
627
|
-
},
|
|
628
|
-
metadata: { summary: `Completed issue #${issueNumber}: ${issue.title}` },
|
|
741
|
+
value: episodicValue,
|
|
742
|
+
metadata: { summary: `Completed issue ${formatIssueRef(issueId)}: ${issue.title}` },
|
|
629
743
|
});
|
|
630
744
|
options.memory.working.clear();
|
|
631
745
|
}
|
|
746
|
+
// 17b. Record priority calibration sample for feedback loop
|
|
747
|
+
if (options.stateStore && options.priorityScore) {
|
|
748
|
+
options.stateStore.savePrioritySample({
|
|
749
|
+
issueId,
|
|
750
|
+
priorityComposite: options.priorityScore.composite,
|
|
751
|
+
priorityConfidence: options.priorityScore.confidence,
|
|
752
|
+
priorityDimensions: JSON.stringify(options.priorityScore.dimensions),
|
|
753
|
+
actualComplexity: complexity,
|
|
754
|
+
filesChanged: result.filesChanged.length,
|
|
755
|
+
outcome: 'success',
|
|
756
|
+
});
|
|
757
|
+
}
|
|
632
758
|
// 18. Audit integrity verification (non-blocking)
|
|
633
759
|
if (options.auditFilePath) {
|
|
634
760
|
try {
|
|
@@ -738,4 +864,28 @@ function runPipelineDiagnostics(input) {
|
|
|
738
864
|
// which would empty it before artifact upload can capture the contents.
|
|
739
865
|
}
|
|
740
866
|
}
|
|
867
|
+
// ── Gitignore helper ─────────────────────────────────────────────────
|
|
868
|
+
const RUNTIME_GITIGNORE_PATHS = ['.ai-sdlc/state.db', '.ai-sdlc/state/', '.ai-sdlc/audit.jsonl'];
|
|
869
|
+
/**
|
|
870
|
+
* Ensure .gitignore in the working directory covers AI-SDLC runtime artifacts.
|
|
871
|
+
* Without this the agent sees untracked runtime files and appends duplicate
|
|
872
|
+
* gitignore entries on every run.
|
|
873
|
+
*
|
|
874
|
+
* Only checks path entries (not the comment header) to avoid false mismatches.
|
|
875
|
+
* Writes the block once with any missing paths.
|
|
876
|
+
*/
|
|
877
|
+
function ensureRuntimeGitignore(workDir) {
|
|
878
|
+
try {
|
|
879
|
+
const gitignorePath = join(workDir, '.gitignore');
|
|
880
|
+
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf-8') : '';
|
|
881
|
+
const missing = RUNTIME_GITIGNORE_PATHS.filter((entry) => !existing.includes(entry));
|
|
882
|
+
if (missing.length === 0)
|
|
883
|
+
return;
|
|
884
|
+
const block = '\n# AI-SDLC runtime artifacts\n' + missing.join('\n') + '\n';
|
|
885
|
+
appendFileSync(gitignorePath, block, 'utf-8');
|
|
886
|
+
}
|
|
887
|
+
catch {
|
|
888
|
+
// Best-effort — workDir may not exist yet in tests or dry-run scenarios
|
|
889
|
+
}
|
|
890
|
+
}
|
|
741
891
|
//# sourceMappingURL=execute.js.map
|