@ai-sdlc/orchestrator 0.5.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/admission-score.d.ts +58 -0
- package/dist/admission-score.js +164 -0
- 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.js +121 -26
- package/dist/fix-ci.js +32 -2
- package/dist/fix-review.d.ts +66 -0
- package/dist/fix-review.js +441 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +13 -1
- package/dist/pipeline-cycle-detector.d.ts +70 -0
- package/dist/pipeline-cycle-detector.js +111 -0
- package/dist/priority.d.ts +2 -76
- package/dist/review.d.ts +31 -0
- package/dist/review.js +74 -0
- package/dist/runners/claude-code.js +314 -32
- package/dist/runners/index.d.ts +2 -1
- package/dist/runners/index.js +1 -0
- package/dist/runners/review-agent.d.ts +47 -0
- package/dist/runners/review-agent.js +220 -0
- package/dist/runners/security-triage.js +4 -0
- package/dist/runners/types.d.ts +19 -0
- package/dist/state/index.d.ts +1 -1
- package/dist/state/schema.d.ts +2 -1
- package/dist/state/schema.js +54 -1
- package/dist/state/store.d.ts +17 -1
- package/dist/state/store.js +122 -0
- package/dist/state/types.d.ts +35 -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,11 @@
|
|
|
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, } 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';
|
|
@@ -24,7 +25,7 @@ import { admitIssueResource, createPipelineAdmission, } from './admission.js';
|
|
|
24
25
|
import { createPipelineAdapterRegistry, createPipelineWebhookBridge, resolveAdapterFromGit, scanPipelineAdapters, resolveIssueTrackerFromConfig, } from './adapters.js';
|
|
25
26
|
import { existsSync, readFileSync, appendFileSync } from 'node:fs';
|
|
26
27
|
import { join } from 'node:path';
|
|
27
|
-
import { defaultSandboxConstraints, DEFAULT_CONFIG_DIR_NAME, DEFAULT_PR_FOOTER, NOTIFICATION_TITLES, } from './defaults.js';
|
|
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';
|
|
28
29
|
import { checkFrameworkCompliance, getControlCatalog, getFrameworkMappings, listSupportedFrameworks, } from './compliance-extended.js';
|
|
29
30
|
import { createSilentLogger, withPipelineSpanSync, getPipelineTracer, validateResourceSchema, } from './telemetry-extended.js';
|
|
30
31
|
import { createPipelineMemory } from './shared.js';
|
|
@@ -125,6 +126,36 @@ export async function executePipeline(issueId, options = {}) {
|
|
|
125
126
|
async function executePipelineBody(issueId, issue, config, qualityGate, agentRole, autonomyPolicy, tracker, sc, auditLog, metricStore, options, log, workDir) {
|
|
126
127
|
const issueNumber = issueIdToNumber(issueId);
|
|
127
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}>`);
|
|
128
159
|
// Discovery: register agent and resolve by issue labels
|
|
129
160
|
const discovery = createPipelineDiscovery();
|
|
130
161
|
discovery.register(agentRole);
|
|
@@ -256,7 +287,26 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
|
|
|
256
287
|
ensureRuntimeGitignore(workDir);
|
|
257
288
|
// 9. Invoke agent (with sandbox + JIT credential lifecycle when security is provided)
|
|
258
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})`);
|
|
259
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
|
+
};
|
|
260
310
|
// Sandbox isolation around agent execution
|
|
261
311
|
const codeStage = config.pipeline?.spec.stages.find((s) => s.name === 'code');
|
|
262
312
|
let sandboxId;
|
|
@@ -304,6 +354,12 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
|
|
|
304
354
|
codebaseContext: options.codebaseContext,
|
|
305
355
|
episodicContext,
|
|
306
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,
|
|
307
363
|
}));
|
|
308
364
|
});
|
|
309
365
|
// Extract the AgentResult from orchestration output
|
|
@@ -359,11 +415,31 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
|
|
|
359
415
|
if (currentLevel.permissions.write.length > 0) {
|
|
360
416
|
authorizeFilesChanged(result.filesChanged, currentLevel.permissions, agentRole.spec.constraints, auditLog, agentRole.metadata.name);
|
|
361
417
|
}
|
|
362
|
-
// 11.
|
|
363
|
-
|
|
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, {
|
|
364
439
|
[ATTRIBUTE_KEYS.STAGE]: 'validate-output',
|
|
365
440
|
}, async () => {
|
|
366
|
-
|
|
441
|
+
log.stage('validate-output');
|
|
442
|
+
const validationResult = await validateAgentOutput({
|
|
367
443
|
filesChanged: result.filesChanged,
|
|
368
444
|
workDir,
|
|
369
445
|
constraints: {
|
|
@@ -372,14 +448,37 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
|
|
|
372
448
|
blockedPaths: resolved.blockedPaths,
|
|
373
449
|
},
|
|
374
450
|
guardrails: { maxLinesPerPR: currentLevel.guardrails.maxLinesPerPR },
|
|
375
|
-
auditLog,
|
|
376
|
-
log,
|
|
377
|
-
onViolation: async (violationList) => {
|
|
378
|
-
await tracker.addComment(issueId, `## ${NOTIFICATION_TITLES.guardrailViolations}\n\n${violationList}`);
|
|
379
|
-
},
|
|
380
451
|
});
|
|
452
|
+
log.stageEnd('validate-output');
|
|
453
|
+
return validationResult;
|
|
454
|
+
});
|
|
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',
|
|
381
480
|
});
|
|
382
|
-
//
|
|
481
|
+
// 13b. Evaluate quality gates and report as GitHub Check Runs
|
|
383
482
|
if (qualityGate.spec.gates.length > 0) {
|
|
384
483
|
try {
|
|
385
484
|
const { stdout: headSha } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
|
|
@@ -416,7 +515,7 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
|
|
|
416
515
|
log.info('Post-agent gate evaluation skipped');
|
|
417
516
|
}
|
|
418
517
|
}
|
|
419
|
-
//
|
|
518
|
+
// 13c. Post-agent complexity evaluation (non-blocking)
|
|
420
519
|
try {
|
|
421
520
|
const { stdout: diffStat } = await execFileAsync('git', ['diff', '--stat', 'HEAD~1'], {
|
|
422
521
|
cwd: workDir,
|
|
@@ -445,11 +544,7 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
|
|
|
445
544
|
catch {
|
|
446
545
|
log.info('Post-agent complexity evaluation skipped');
|
|
447
546
|
}
|
|
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)
|
|
547
|
+
// 14. Compute cost receipt for provenance (before PR creation)
|
|
453
548
|
let costReceipt;
|
|
454
549
|
if (result.tokenUsage) {
|
|
455
550
|
const tu = result.tokenUsage;
|
|
@@ -467,7 +562,7 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
|
|
|
467
562
|
},
|
|
468
563
|
};
|
|
469
564
|
}
|
|
470
|
-
//
|
|
565
|
+
// 15. Create PR (with optional provenance, reading config from pipeline)
|
|
471
566
|
const prConfig = config.pipeline?.spec.pullRequest;
|
|
472
567
|
const shouldIncludeProvenance = options.includeProvenance ?? prConfig?.includeProvenance ?? options.security !== undefined;
|
|
473
568
|
let provenanceBlock = '';
|
|
@@ -531,6 +626,7 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
|
|
|
531
626
|
})
|
|
532
627
|
: { title: NOTIFICATION_TITLES.prCreated, body: `Pull request created: ${pr.url}` };
|
|
533
628
|
await tracker.addComment(issueId, `## ${prCreatedComment.title}\n\n${prCreatedComment.body}`);
|
|
629
|
+
await notifySlack(`:pull_request: PR created: ${pr.url}`);
|
|
534
630
|
// 14b. Record cost from agent result
|
|
535
631
|
if (options.costTracker && result.tokenUsage) {
|
|
536
632
|
const tu = result.tokenUsage;
|
|
@@ -769,25 +865,24 @@ function runPipelineDiagnostics(input) {
|
|
|
769
865
|
}
|
|
770
866
|
}
|
|
771
867
|
// ── Gitignore helper ─────────────────────────────────────────────────
|
|
772
|
-
const
|
|
773
|
-
'# AI-SDLC runtime artifacts',
|
|
774
|
-
'.ai-sdlc/state.db',
|
|
775
|
-
'.ai-sdlc/state/',
|
|
776
|
-
'.ai-sdlc/audit.jsonl',
|
|
777
|
-
];
|
|
868
|
+
const RUNTIME_GITIGNORE_PATHS = ['.ai-sdlc/state.db', '.ai-sdlc/state/', '.ai-sdlc/audit.jsonl'];
|
|
778
869
|
/**
|
|
779
870
|
* Ensure .gitignore in the working directory covers AI-SDLC runtime artifacts.
|
|
780
871
|
* Without this the agent sees untracked runtime files and appends duplicate
|
|
781
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.
|
|
782
876
|
*/
|
|
783
877
|
function ensureRuntimeGitignore(workDir) {
|
|
784
878
|
try {
|
|
785
879
|
const gitignorePath = join(workDir, '.gitignore');
|
|
786
880
|
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf-8') : '';
|
|
787
|
-
const missing =
|
|
881
|
+
const missing = RUNTIME_GITIGNORE_PATHS.filter((entry) => !existing.includes(entry));
|
|
788
882
|
if (missing.length === 0)
|
|
789
883
|
return;
|
|
790
|
-
|
|
884
|
+
const block = '\n# AI-SDLC runtime artifacts\n' + missing.join('\n') + '\n';
|
|
885
|
+
appendFileSync(gitignorePath, block, 'utf-8');
|
|
791
886
|
}
|
|
792
887
|
catch {
|
|
793
888
|
// Best-effort — workDir may not exist yet in tests or dry-run scenarios
|
package/dist/fix-ci.js
CHANGED
|
@@ -13,6 +13,7 @@ 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';
|
|
16
17
|
export const MAX_FIX_ATTEMPTS = DEFAULT_MAX_FIX_ATTEMPTS;
|
|
17
18
|
export const MAX_LOG_LINES = DEFAULT_MAX_LOG_LINES;
|
|
18
19
|
export const RETRY_MARKER = '<!-- ai-sdlc-fix-ci-attempt -->';
|
|
@@ -107,6 +108,8 @@ export async function executeFixCI(prNumber, runId, options = {}) {
|
|
|
107
108
|
}
|
|
108
109
|
// Tracker is available if injected directly or if we're not in test mode
|
|
109
110
|
const trackerAvailable = !!options.tracker || options._prComments === undefined;
|
|
111
|
+
// Create cycle detector (marker generated after all guard conditions pass)
|
|
112
|
+
const cycleDetector = createCycleDetectorFromConfig(config.pipeline?.spec ?? {});
|
|
110
113
|
// 2. Count retry attempts (via injected comments or IssueTracker)
|
|
111
114
|
log.stage('check-retries');
|
|
112
115
|
let comments;
|
|
@@ -126,6 +129,27 @@ export async function executeFixCI(prNumber, runId, options = {}) {
|
|
|
126
129
|
await getTracker().addComment(String(prNumber), body);
|
|
127
130
|
}
|
|
128
131
|
};
|
|
132
|
+
// Check for pipeline-level cycles AFTER retry counting
|
|
133
|
+
// (so legitimate retries under the limit aren't blocked)
|
|
134
|
+
if (trackerAvailable) {
|
|
135
|
+
const cycleCheck = await checkAndHandleCycle({
|
|
136
|
+
issueOrPrId: String(prNumber),
|
|
137
|
+
stage: 'fix-ci',
|
|
138
|
+
tracker: getTracker(),
|
|
139
|
+
detector: cycleDetector,
|
|
140
|
+
});
|
|
141
|
+
if (cycleCheck.cycleDetected) {
|
|
142
|
+
log.info('Pipeline cycle detected. Halting fix-ci execution.');
|
|
143
|
+
auditLog.record({
|
|
144
|
+
actor: 'system',
|
|
145
|
+
action: 'evaluate',
|
|
146
|
+
resource: `pr#${prNumber}`,
|
|
147
|
+
decision: 'denied',
|
|
148
|
+
details: { reason: 'pipeline-cycle-detected' },
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
129
153
|
if (attempts >= maxFixAttempts) {
|
|
130
154
|
log.info(`Fix-CI retry limit reached (${maxFixAttempts}). Commenting and stopping.`);
|
|
131
155
|
auditLog.record({
|
|
@@ -148,6 +172,9 @@ export async function executeFixCI(prNumber, runId, options = {}) {
|
|
|
148
172
|
await addComment(`## ${limitComment.title}\n\n${limitComment.body}`);
|
|
149
173
|
return;
|
|
150
174
|
}
|
|
175
|
+
// Generate cycle marker AFTER all guard conditions (retry limit + cycle detection)
|
|
176
|
+
// so it's only recorded when we actually proceed with execution
|
|
177
|
+
const cycleMarker = cycleDetector.recordInvocation('fix-ci');
|
|
151
178
|
// 3. Fetch CI logs
|
|
152
179
|
log.stage('fetch-logs');
|
|
153
180
|
const ciLogs = await fetchCILogs(runId, options._ciLogs, options.ciAdapter);
|
|
@@ -259,7 +286,8 @@ export async function executeFixCI(prNumber, runId, options = {}) {
|
|
|
259
286
|
const agentFailComment = agentFailTpl
|
|
260
287
|
? renderTemplate(agentFailTpl, { stageName: 'fix-ci', details: errorDetail })
|
|
261
288
|
: { title: NOTIFICATION_TITLES.fixCIAgentFailed, body: errorDetail };
|
|
262
|
-
|
|
289
|
+
// Use pre-created marker (one per execution, prevents double-counting)
|
|
290
|
+
await addComment(`## ${agentFailComment.title}\n\n${agentFailComment.body}\n\n${RETRY_MARKER}\n${cycleMarker}`);
|
|
263
291
|
throw new Error(`Fix-CI agent failed on PR #${prNumber}: ${r.error}`);
|
|
264
292
|
}
|
|
265
293
|
log.stageEnd('agent');
|
|
@@ -308,7 +336,7 @@ export async function executeFixCI(prNumber, runId, options = {}) {
|
|
|
308
336
|
auditLog,
|
|
309
337
|
log,
|
|
310
338
|
onViolation: async (violationList) => {
|
|
311
|
-
await addComment(`## ${NOTIFICATION_TITLES.fixCIGuardrailViolations}\n\n${violationList}\n\n${RETRY_MARKER}`);
|
|
339
|
+
await addComment(`## ${NOTIFICATION_TITLES.fixCIGuardrailViolations}\n\n${violationList}\n\n${RETRY_MARKER}\n${cycleMarker}`);
|
|
312
340
|
},
|
|
313
341
|
});
|
|
314
342
|
});
|
|
@@ -335,6 +363,7 @@ export async function executeFixCI(prNumber, runId, options = {}) {
|
|
|
335
363
|
title: NOTIFICATION_TITLES.fixCIApplied,
|
|
336
364
|
body: `Attempt ${attempts + 1} of ${maxFixAttempts} — pushed fixes to \`${currentBranch}\`.`,
|
|
337
365
|
};
|
|
366
|
+
// Use pre-created marker (one per execution, prevents double-counting)
|
|
338
367
|
await addComment([
|
|
339
368
|
`## ${successComment.title}`,
|
|
340
369
|
'',
|
|
@@ -344,6 +373,7 @@ export async function executeFixCI(prNumber, runId, options = {}) {
|
|
|
344
373
|
result.filesChanged.map((f) => `- \`${f}\``).join('\n'),
|
|
345
374
|
'',
|
|
346
375
|
RETRY_MARKER,
|
|
376
|
+
cycleMarker,
|
|
347
377
|
].join('\n'));
|
|
348
378
|
// 12. Record episodic memory (success)
|
|
349
379
|
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
|