@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.
Files changed (72) hide show
  1. package/dist/action-enforcement.d.ts +26 -0
  2. package/dist/action-enforcement.js +70 -0
  3. package/dist/adapters.d.ts +18 -3
  4. package/dist/adapters.js +92 -2
  5. package/dist/admission-score.d.ts +58 -0
  6. package/dist/admission-score.js +164 -0
  7. package/dist/cli/commands/init.js +4 -8
  8. package/dist/cli/commands/run.js +2 -2
  9. package/dist/config.d.ts +3 -0
  10. package/dist/config.js +14 -5
  11. package/dist/cycle-utils.d.ts +51 -0
  12. package/dist/cycle-utils.js +77 -0
  13. package/dist/defaults.d.ts +5 -0
  14. package/dist/defaults.js +5 -0
  15. package/dist/execute.d.ts +5 -2
  16. package/dist/execute.js +212 -62
  17. package/dist/fix-ci.js +45 -13
  18. package/dist/fix-review.d.ts +66 -0
  19. package/dist/fix-review.js +441 -0
  20. package/dist/index.d.ts +14 -4
  21. package/dist/index.js +18 -3
  22. package/dist/orchestrator.d.ts +1 -1
  23. package/dist/orchestrator.js +31 -9
  24. package/dist/pipeline-cycle-detector.d.ts +70 -0
  25. package/dist/pipeline-cycle-detector.js +111 -0
  26. package/dist/plugin.d.ts +9 -3
  27. package/dist/priority.d.ts +28 -0
  28. package/dist/priority.js +230 -0
  29. package/dist/review.d.ts +31 -0
  30. package/dist/review.js +74 -0
  31. package/dist/runners/claude-code.js +367 -35
  32. package/dist/runners/codex.js +15 -4
  33. package/dist/runners/copilot.js +15 -4
  34. package/dist/runners/cursor.js +15 -4
  35. package/dist/runners/generic-llm.js +1 -1
  36. package/dist/runners/index.d.ts +3 -1
  37. package/dist/runners/index.js +2 -0
  38. package/dist/runners/review-agent.d.ts +47 -0
  39. package/dist/runners/review-agent.js +220 -0
  40. package/dist/runners/security-triage.d.ts +43 -0
  41. package/dist/runners/security-triage.js +158 -0
  42. package/dist/runners/types.d.ts +24 -1
  43. package/dist/security.d.ts +8 -3
  44. package/dist/security.js +13 -2
  45. package/dist/shared.d.ts +17 -0
  46. package/dist/shared.js +27 -0
  47. package/dist/state/index.d.ts +1 -1
  48. package/dist/state/schema.d.ts +4 -1
  49. package/dist/state/schema.js +89 -1
  50. package/dist/state/store.d.ts +31 -1
  51. package/dist/state/store.js +208 -13
  52. package/dist/state/types.d.ts +52 -0
  53. package/dist/triage.d.ts +36 -0
  54. package/dist/triage.js +133 -0
  55. package/dist/types.d.ts +1 -1
  56. package/dist/watch.d.ts +6 -2
  57. package/dist/watch.js +34 -6
  58. package/dist/workflow-patterns/artifact-writer.d.ts +16 -0
  59. package/dist/workflow-patterns/artifact-writer.js +34 -0
  60. package/dist/workflow-patterns/classifiers.d.ts +10 -0
  61. package/dist/workflow-patterns/classifiers.js +72 -0
  62. package/dist/workflow-patterns/detector.d.ts +27 -0
  63. package/dist/workflow-patterns/detector.js +186 -0
  64. package/dist/workflow-patterns/index.d.ts +8 -0
  65. package/dist/workflow-patterns/index.js +7 -0
  66. package/dist/workflow-patterns/proposal-generator.d.ts +15 -0
  67. package/dist/workflow-patterns/proposal-generator.js +183 -0
  68. package/dist/workflow-patterns/telemetry-ingest.d.ts +27 -0
  69. package/dist/workflow-patterns/telemetry-ingest.js +103 -0
  70. package/dist/workflow-patterns/types.d.ts +61 -0
  71. package/dist/workflow-patterns/types.js +11 -0
  72. 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
@@ -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 number.
81
+ * Execute the full AI-SDLC pipeline for a given issue ID.
79
82
  */
80
- export declare function executePipeline(issueNumber: number, options?: ExecuteOptions): Promise<PipelineResult>;
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 { createGitHubIssueTracker, createGitHubSourceControl, routeByComplexity, evaluatePromotion, evaluateComplexity, selectModel, withSpan, getMeter, SPAN_NAMES, METRIC_NAMES, ATTRIBUTE_KEYS, } from '@ai-sdlc/reference';
8
+ import { createGitHubSourceControl, routeByComplexity, evaluatePromotion, evaluateComplexity, selectModel, withSpan, getMeter, SPAN_NAMES, METRIC_NAMES, ATTRIBUTE_KEYS, } from '@ai-sdlc/reference';
9
9
  import { loadConfigAsync } from './config.js';
10
10
  import { validateIssue, validateIssueWithExtensions, parseComplexity } from './validate-issue.js';
11
+ import { validateAgentOutput } from './validate-agent-output.js';
11
12
  import { createLogger } from './logger.js';
12
13
  import { createStructuredConsoleLogger, createStructuredBufferLogger, } from './structured-logger.js';
13
14
  import { ClaudeCodeRunner } from './runners/claude-code.js';
14
- import { execFileAsync, getGitHubConfig, resolveRepoRoot, createDefaultAuditLog, resolveAutonomyLevel, resolveConstraints, isAutonomousStrategy, recordMetric, validateAndAuditOutput, evaluatePipelineCompliance, authorizeFilesChanged, interpolateBranchPattern, interpolatePRTitle, } from './shared.js';
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 { defaultSandboxConstraints, DEFAULT_CONFIG_DIR_NAME, DEFAULT_PR_FOOTER, NOTIFICATION_TITLES, } from './defaults.js';
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 number.
37
+ * Execute the full AI-SDLC pipeline for a given issue ID.
35
38
  */
36
- export async function executePipeline(issueNumber, options = {}) {
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 ?? createGitHubIssueTracker(ghConfig);
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(String(issueNumber));
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
- number: issueNumber,
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(issueNumber, issue, config, qualityGate, agentRole, autonomyPolicy, tracker, sc, auditLog, metricStore, options, log, workDir);
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
- issueNumber,
115
+ issueId,
113
116
  outcome: 'failure',
114
117
  error: err instanceof Error ? err.message : String(err),
115
118
  },
116
- metadata: { summary: `Failed issue #${issueNumber}: ${issue.title}` },
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(issueNumber, issue, config, qualityGate, agentRole, autonomyPolicy, tracker, sc, auditLog, metricStore, options, log, workDir) {
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#${issueNumber}`,
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(String(issueNumber), `## ${gateFailTitle}\n\n${gateFailBody}`);
187
- throw new Error(`Issue #${issueNumber} failed quality gate validation`);
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#${issueNumber}`,
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#${issueNumber}`,
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(String(issueNumber), `## ${complexityComment.title}\n\n${complexityComment.body}`);
218
- throw new Error(`Issue #${issueNumber} complexity ${complexity} routed as "${strategy}"`);
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 #${issueNumber}`);
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 #${issueNumber} requires approval (tier: ${approval.tier}) — status is pending`);
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: String(issueNumber), issueTitle: issue.title };
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-${issueNumber}`, defaultSandboxConstraints(workDir, timeoutMs));
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#${issueNumber}`,
339
+ [ATTRIBUTE_KEYS.RESOURCE_NAME]: `issue#${issueId}`,
284
340
  }, () => runner.run({
285
- issueNumber,
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(String(issueNumber), `## ${agentFailComment.title}\n\n${agentFailComment.body}`);
323
- throw new Error(`Agent failed on issue #${issueNumber}: ${err}`);
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. Validate agent output against guardrails
355
- await withSpan(SPAN_NAMES.PIPELINE_STAGE, {
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
- await validateAndAuditOutput({
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
- // 11b. Evaluate quality gates and report as GitHub Check Runs
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#${issueNumber}`,
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
- // 11c. Post-agent complexity evaluation (non-blocking)
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#${issueNumber}`,
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
- // 12. Push branch
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
- // 13. Create PR (with optional provenance, reading config from pipeline)
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
- provenanceBlock = '\n\n' + attachProvenanceToPR(provenance);
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: String(issueNumber), issueTitle: issue.title };
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} #${issueNumber}`);
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, issueNumber },
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: String(issueNumber),
625
+ issueNumber: issueId,
518
626
  })
519
627
  : { title: NOTIFICATION_TITLES.prCreated, body: `Pull request created: ${pr.url}` };
520
- await tracker.addComment(String(issueNumber), `## ${prCreatedComment.title}\n\n${prCreatedComment.body}`);
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()}-${issueNumber}`,
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
- issueNumber,
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