@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.
Files changed (47) hide show
  1. package/dist/action-enforcement.d.ts +26 -0
  2. package/dist/action-enforcement.js +70 -0
  3. package/dist/admission-score.d.ts +58 -0
  4. package/dist/admission-score.js +164 -0
  5. package/dist/cycle-utils.d.ts +51 -0
  6. package/dist/cycle-utils.js +77 -0
  7. package/dist/defaults.d.ts +5 -0
  8. package/dist/defaults.js +5 -0
  9. package/dist/execute.js +121 -26
  10. package/dist/fix-ci.js +32 -2
  11. package/dist/fix-review.d.ts +66 -0
  12. package/dist/fix-review.js +441 -0
  13. package/dist/index.d.ts +10 -2
  14. package/dist/index.js +13 -1
  15. package/dist/pipeline-cycle-detector.d.ts +70 -0
  16. package/dist/pipeline-cycle-detector.js +111 -0
  17. package/dist/priority.d.ts +2 -76
  18. package/dist/review.d.ts +31 -0
  19. package/dist/review.js +74 -0
  20. package/dist/runners/claude-code.js +314 -32
  21. package/dist/runners/index.d.ts +2 -1
  22. package/dist/runners/index.js +1 -0
  23. package/dist/runners/review-agent.d.ts +47 -0
  24. package/dist/runners/review-agent.js +220 -0
  25. package/dist/runners/security-triage.js +4 -0
  26. package/dist/runners/types.d.ts +19 -0
  27. package/dist/state/index.d.ts +1 -1
  28. package/dist/state/schema.d.ts +2 -1
  29. package/dist/state/schema.js +54 -1
  30. package/dist/state/store.d.ts +17 -1
  31. package/dist/state/store.js +122 -0
  32. package/dist/state/types.d.ts +35 -0
  33. package/dist/workflow-patterns/artifact-writer.d.ts +16 -0
  34. package/dist/workflow-patterns/artifact-writer.js +34 -0
  35. package/dist/workflow-patterns/classifiers.d.ts +10 -0
  36. package/dist/workflow-patterns/classifiers.js +72 -0
  37. package/dist/workflow-patterns/detector.d.ts +27 -0
  38. package/dist/workflow-patterns/detector.js +186 -0
  39. package/dist/workflow-patterns/index.d.ts +8 -0
  40. package/dist/workflow-patterns/index.js +7 -0
  41. package/dist/workflow-patterns/proposal-generator.d.ts +15 -0
  42. package/dist/workflow-patterns/proposal-generator.js +183 -0
  43. package/dist/workflow-patterns/telemetry-ingest.d.ts +27 -0
  44. package/dist/workflow-patterns/telemetry-ingest.js +103 -0
  45. package/dist/workflow-patterns/types.d.ts +61 -0
  46. package/dist/workflow-patterns/types.js +11 -0
  47. package/package.json +2 -2
@@ -0,0 +1,441 @@
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 { createGitHubIssueTracker, evaluateDemotion, withSpan, getMeter, SPAN_NAMES, METRIC_NAMES, ATTRIBUTE_KEYS, } from '@ai-sdlc/reference';
7
+ import { loadConfig } from './config.js';
8
+ import { createLogger } from './logger.js';
9
+ import { createStructuredConsoleLogger } from './structured-logger.js';
10
+ import { ClaudeCodeRunner } from './runners/claude-code.js';
11
+ import { execFileAsync, getGitHubConfig, extractIssueId, resolveRepoRoot, createDefaultAuditLog, resolveAutonomyLevel, resolveConstraints, recordMetric, validateAndAuditOutput, authorizeFilesChanged, issueIdToNumber, } from './shared.js';
12
+ import { renderTemplate } from './notifications.js';
13
+ import { parseDuration } from './policy-evaluators.js';
14
+ import { checkKillSwitch, issueAgentCredentials, revokeAgentCredentials, } from './security.js';
15
+ import { DEFAULT_GH_CLI_TIMEOUT_MS, DEFAULT_CONFIG_DIR_NAME, defaultSandboxConstraints, NOTIFICATION_TITLES, } from './defaults.js';
16
+ import { createCycleDetectorFromConfig, checkAndHandleCycle } from './cycle-utils.js';
17
+ // Default max review-fix attempts (lower than CI-fix since reviews are more deterministic)
18
+ export const MAX_REVIEW_FIX_ATTEMPTS = 2;
19
+ export const RETRY_MARKER = '<!-- ai-sdlc-fix-review-attempt -->';
20
+ /**
21
+ * Count how many fix-review retry attempts have been made on a PR
22
+ * by scanning comments for the hidden retry marker.
23
+ */
24
+ export function countRetryAttempts(comments) {
25
+ let count = 0;
26
+ for (const body of comments) {
27
+ const matches = body.match(new RegExp(RETRY_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'));
28
+ if (matches) {
29
+ count += matches.length;
30
+ }
31
+ }
32
+ return count;
33
+ }
34
+ /**
35
+ * Validate that a PR number is a positive integer.
36
+ */
37
+ export function validatePrNumber(prNumber) {
38
+ if (!Number.isInteger(prNumber) || prNumber <= 0) {
39
+ throw new Error(`Invalid PR number: ${prNumber} (must be a positive integer)`);
40
+ }
41
+ }
42
+ /**
43
+ * Sanitize a git branch name to prevent command injection.
44
+ * Allows only alphanumeric characters, slashes, dashes, underscores, and dots.
45
+ */
46
+ export function sanitizeBranchName(branch) {
47
+ if (!/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
48
+ throw new Error(`Invalid branch name: "${branch}" (only alphanumeric, /, -, _, . allowed)`);
49
+ }
50
+ return branch;
51
+ }
52
+ /**
53
+ * Fetch review findings from PR reviews that requested changes.
54
+ * Returns a formatted string with all findings from review agents.
55
+ */
56
+ export async function fetchReviewFindings(prNumber, injectedFindings, _secretStore) {
57
+ if (injectedFindings !== undefined) {
58
+ return injectedFindings;
59
+ }
60
+ // Validate PR number to prevent command injection
61
+ validatePrNumber(prNumber);
62
+ // Fetch reviews using gh CLI
63
+ const { stdout } = await execFileAsync('gh', ['pr', 'review', String(prNumber), '--json', 'state,body,author'], { timeout: DEFAULT_GH_CLI_TIMEOUT_MS });
64
+ let reviews;
65
+ try {
66
+ reviews = JSON.parse(stdout);
67
+ }
68
+ catch (err) {
69
+ throw new Error(`Failed to parse review data from gh CLI: ${err instanceof Error ? err.message : String(err)}`);
70
+ }
71
+ const changesRequestedReviews = reviews.filter((r) => r.state === 'CHANGES_REQUESTED');
72
+ if (changesRequestedReviews.length === 0) {
73
+ return 'No review findings (all reviews approved or pending)';
74
+ }
75
+ // Format findings from all reviews
76
+ const findings = changesRequestedReviews
77
+ .map((r) => `### Review by ${r.author.login}\n\n${r.body}\n\n---\n`)
78
+ .join('\n');
79
+ return findings;
80
+ }
81
+ /**
82
+ * Execute the fix-review pipeline for a PR with review findings.
83
+ *
84
+ * Returns gracefully (no throw) when the retry limit is reached.
85
+ * Throws on agent failure or guardrail violations.
86
+ */
87
+ export async function executeFixReview(prNumber, options = {}) {
88
+ const workDir = options.workDir ?? (await resolveRepoRoot());
89
+ const configDir = options.configDir ?? `${workDir}/${DEFAULT_CONFIG_DIR_NAME}`;
90
+ const log = options.logger ??
91
+ (options.useStructuredLogger ? createStructuredConsoleLogger() : createLogger());
92
+ const auditLog = options.auditLog ?? createDefaultAuditLog(workDir);
93
+ const metricStore = options.metricStore;
94
+ // 1. Load config
95
+ log.stage('load-config');
96
+ const config = loadConfig(configDir);
97
+ log.stageEnd('load-config');
98
+ // Kill switch check (before any work)
99
+ if (options.security) {
100
+ await checkKillSwitch(options.security);
101
+ }
102
+ if (!config.agentRole) {
103
+ throw new Error('No AgentRole resource found in .ai-sdlc/');
104
+ }
105
+ if (!config.autonomyPolicy) {
106
+ throw new Error('No AutonomyPolicy resource found in .ai-sdlc/');
107
+ }
108
+ const agentRole = config.agentRole;
109
+ const autonomyPolicy = config.autonomyPolicy;
110
+ // Derive max fix attempts from pipeline config (review stage onFailure.maxRetries)
111
+ const reviewStage = config.pipeline?.spec.stages.find((s) => s.name === 'review');
112
+ const maxFixAttempts = reviewStage?.onFailure?.maxRetries ?? MAX_REVIEW_FIX_ATTEMPTS;
113
+ // Notification templates
114
+ const notifTemplates = config.pipeline?.spec.notifications?.templates;
115
+ // Create default tracker when needed (lazy to avoid resolving secrets in test environments).
116
+ // In production (no _prComments injected), the tracker is always available.
117
+ let _tracker = options.tracker;
118
+ function getTracker() {
119
+ if (!_tracker) {
120
+ const { org, repo } = getGitHubConfig(options.secretStore);
121
+ const ghConfig = { org, repo, token: { secretRef: 'github-token' } };
122
+ _tracker = createGitHubIssueTracker(ghConfig);
123
+ }
124
+ return _tracker;
125
+ }
126
+ // Tracker is available if injected directly or if we're not in test mode
127
+ const trackerAvailable = !!options.tracker || options._prComments === undefined;
128
+ // Create cycle detector (marker generated after all guard conditions pass)
129
+ const cycleDetector = createCycleDetectorFromConfig(config.pipeline?.spec ?? {});
130
+ // 2. Count retry attempts (via injected comments or IssueTracker)
131
+ log.stage('check-retries');
132
+ let comments;
133
+ if (options._prComments !== undefined) {
134
+ comments = options._prComments;
135
+ }
136
+ else {
137
+ const issueComments = await getTracker().getComments(String(prNumber));
138
+ comments = issueComments.map((c) => c.body);
139
+ }
140
+ const attempts = countRetryAttempts(comments);
141
+ log.info(`Fix-review attempt ${attempts + 1} of ${maxFixAttempts}`);
142
+ log.stageEnd('check-retries');
143
+ // Helper to add a comment via tracker (uses default tracker in production)
144
+ const addComment = async (body) => {
145
+ if (trackerAvailable) {
146
+ await getTracker().addComment(String(prNumber), body);
147
+ }
148
+ };
149
+ if (attempts >= maxFixAttempts) {
150
+ log.info(`Fix-review retry limit reached (${maxFixAttempts}). Commenting and stopping.`);
151
+ auditLog.record({
152
+ actor: 'system',
153
+ action: 'evaluate',
154
+ resource: `pr#${prNumber}`,
155
+ decision: 'denied',
156
+ details: { reason: 'retry-limit-reached', attempts, max: maxFixAttempts },
157
+ });
158
+ const limitTpl = notifTemplates?.['fix-review-limit'];
159
+ const limitComment = limitTpl
160
+ ? renderTemplate(limitTpl, {
161
+ attempts: String(attempts),
162
+ max: String(maxFixAttempts),
163
+ })
164
+ : {
165
+ title: NOTIFICATION_TITLES.fixReviewRetryLimit,
166
+ body: `This PR has reached the maximum number of automated review-fix attempts (${maxFixAttempts}). Manual intervention is needed.`,
167
+ };
168
+ await addComment(`## ${limitComment.title}\n\n${limitComment.body}`);
169
+ return;
170
+ }
171
+ // Check for pipeline-level cycles AFTER retry counting
172
+ if (trackerAvailable) {
173
+ const cycleCheck = await checkAndHandleCycle({
174
+ issueOrPrId: String(prNumber),
175
+ stage: 'fix-review',
176
+ tracker: getTracker(),
177
+ detector: cycleDetector,
178
+ });
179
+ if (cycleCheck.cycleDetected) {
180
+ log.info('Pipeline cycle detected. Halting fix-review execution.');
181
+ auditLog.record({
182
+ actor: 'system',
183
+ action: 'evaluate',
184
+ resource: `pr#${prNumber}`,
185
+ decision: 'denied',
186
+ details: { reason: 'pipeline-cycle-detected' },
187
+ });
188
+ return;
189
+ }
190
+ }
191
+ // Generate cycle marker AFTER all guard conditions pass
192
+ const cycleMarker = cycleDetector.recordInvocation('fix-review');
193
+ // 3. Fetch review findings
194
+ log.stage('fetch-findings');
195
+ const reviewFindings = await fetchReviewFindings(prNumber, options._reviewFindings, options.secretStore);
196
+ log.stageEnd('fetch-findings');
197
+ // Check if review findings are actionable (not a generic "no findings" message)
198
+ const isActionable = reviewFindings &&
199
+ !reviewFindings.startsWith('No review findings') &&
200
+ reviewFindings.trim().length > 0;
201
+ if (!isActionable) {
202
+ log.info('No actionable review findings. Skipping fix-review execution.');
203
+ auditLog.record({
204
+ actor: 'system',
205
+ action: 'evaluate',
206
+ resource: `pr#${prNumber}`,
207
+ decision: 'allowed',
208
+ details: { reason: 'no-actionable-findings', reviewFindings },
209
+ });
210
+ return;
211
+ }
212
+ // 4. Determine branch and issue number
213
+ const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], {
214
+ cwd: workDir,
215
+ });
216
+ const currentBranch = sanitizeBranchName(branchStdout.trim());
217
+ const issueId = extractIssueId(currentBranch);
218
+ if (issueId === null) {
219
+ throw new Error(`Branch "${currentBranch}" does not match ai-sdlc/issue-<id> pattern`);
220
+ }
221
+ const issueNumber = issueIdToNumber(issueId);
222
+ // 5. Resolve autonomy level and constraints
223
+ const currentLevel = resolveAutonomyLevel(autonomyPolicy);
224
+ const resolved = resolveConstraints(agentRole.spec.constraints, currentLevel);
225
+ // 6. Fetch issue data (via tracker when available)
226
+ let issueTitle = `Issue ${issueId}`;
227
+ let issueBody = '';
228
+ if (trackerAvailable) {
229
+ const issueData = await getTracker().getIssue(issueId);
230
+ issueTitle = issueData.title;
231
+ issueBody = issueData.description ?? '';
232
+ }
233
+ // Store issue context in working memory
234
+ if (options.memory) {
235
+ options.memory.working.set('currentIssue', { prNumber, issueId, currentBranch });
236
+ }
237
+ // Query episodic memory for previous fix-review attempts
238
+ if (options.memory) {
239
+ const previousAttempts = options.memory.episodic.search('fix-review-execution');
240
+ if (previousAttempts.length > 0) {
241
+ log.info(`Found ${previousAttempts.length} previous fix-review episodes in memory`);
242
+ }
243
+ }
244
+ const meter = getMeter();
245
+ // Wrap agent+validation+push in try/catch for failure episodes
246
+ try {
247
+ // 7. Invoke agent with review findings (with sandbox + JIT credential lifecycle)
248
+ log.stage('agent');
249
+ const runner = options.runner ?? new ClaudeCodeRunner();
250
+ // Sandbox isolation around agent execution
251
+ let sandboxId;
252
+ let result;
253
+ try {
254
+ if (options.security) {
255
+ const timeoutMs = reviewStage?.timeout ? parseDuration(reviewStage.timeout) : undefined;
256
+ sandboxId = await options.security.sandbox.isolate(`issue-${issueId}`, defaultSandboxConstraints(workDir, timeoutMs));
257
+ }
258
+ // Issue JIT credentials before agent execution
259
+ const jitCred = options.security
260
+ ? await issueAgentCredentials(options.security, agentRole.metadata.name)
261
+ : undefined;
262
+ try {
263
+ result = await withSpan(SPAN_NAMES.AGENT_TASK, {
264
+ [ATTRIBUTE_KEYS.AGENT]: agentRole.metadata.name,
265
+ [ATTRIBUTE_KEYS.RESOURCE_NAME]: `pr#${prNumber}`,
266
+ }, async () => {
267
+ const r = await runner.run({
268
+ issueId,
269
+ issueNumber: issueNumber ?? undefined,
270
+ issueTitle,
271
+ issueBody,
272
+ workDir,
273
+ branch: currentBranch,
274
+ constraints: {
275
+ maxFilesPerChange: resolved.maxFiles,
276
+ requireTests: resolved.requireTests,
277
+ blockedPaths: resolved.blockedPaths,
278
+ },
279
+ reviewFindings,
280
+ });
281
+ if (!r.success) {
282
+ log.stageEnd('agent');
283
+ auditLog.record({
284
+ actor: 'system',
285
+ action: 'execute',
286
+ resource: `agent/${agentRole.metadata.name}`,
287
+ decision: 'denied',
288
+ details: { error: r.error },
289
+ });
290
+ meter.createCounter(METRIC_NAMES.TASK_FAILURE_TOTAL).add(1);
291
+ recordMetric(metricStore, METRIC_NAMES.TASK_FAILURE_TOTAL, 1);
292
+ // Evaluate demotion on agent failure
293
+ const agentMetrics = {
294
+ name: agentRole.metadata.name,
295
+ currentLevel: currentLevel.level,
296
+ totalTasksCompleted: 0,
297
+ metrics: {},
298
+ approvals: [],
299
+ };
300
+ const demotion = evaluateDemotion(autonomyPolicy, agentMetrics, 'failed-test');
301
+ log.info(`Demotion evaluation: ${demotion.demoted ? `demoted from ${demotion.fromLevel} to ${demotion.toLevel}` : 'no demotion'}`);
302
+ auditLog.record({
303
+ actor: 'system',
304
+ action: 'evaluate',
305
+ resource: `agent/${agentRole.metadata.name}`,
306
+ policy: 'demotion',
307
+ decision: demotion.demoted ? 'denied' : 'allowed',
308
+ details: {
309
+ trigger: demotion.trigger,
310
+ fromLevel: demotion.fromLevel,
311
+ toLevel: demotion.toLevel,
312
+ },
313
+ });
314
+ const agentFailTpl = notifTemplates?.['agent-failure'];
315
+ const errorDetail = r.error ?? 'Unknown error';
316
+ const agentFailComment = agentFailTpl
317
+ ? renderTemplate(agentFailTpl, { stageName: 'fix-review', details: errorDetail })
318
+ : { title: NOTIFICATION_TITLES.fixReviewAgentFailed, body: errorDetail };
319
+ // Use pre-created marker (one per execution, prevents double-counting)
320
+ await addComment(`## ${agentFailComment.title}\n\n${agentFailComment.body}\n\n${RETRY_MARKER}\n${cycleMarker}`);
321
+ throw new Error(`Fix-review agent failed on PR #${prNumber}: ${r.error}`);
322
+ }
323
+ log.stageEnd('agent');
324
+ auditLog.record({
325
+ actor: 'system',
326
+ action: 'execute',
327
+ resource: `agent/${agentRole.metadata.name}`,
328
+ decision: 'allowed',
329
+ details: { filesChanged: r.filesChanged.length },
330
+ });
331
+ meter.createCounter(METRIC_NAMES.TASK_SUCCESS_TOTAL).add(1);
332
+ recordMetric(metricStore, METRIC_NAMES.TASK_SUCCESS_TOTAL, 1);
333
+ return r;
334
+ });
335
+ }
336
+ finally {
337
+ // Revoke JIT credentials after agent execution (success or failure)
338
+ if (jitCred && options.security) {
339
+ await revokeAgentCredentials(options.security, jitCred.id);
340
+ }
341
+ }
342
+ }
343
+ finally {
344
+ // Destroy sandbox after agent execution
345
+ if (sandboxId && options.security) {
346
+ await options.security.sandbox.destroy(sandboxId);
347
+ }
348
+ }
349
+ // 8. ABAC authorization check (if write permissions are defined)
350
+ if (currentLevel.permissions.write.length > 0) {
351
+ authorizeFilesChanged(result.filesChanged, currentLevel.permissions, agentRole.spec.constraints, auditLog, agentRole.metadata.name);
352
+ }
353
+ // 9. Validate agent output against guardrails
354
+ await withSpan(SPAN_NAMES.PIPELINE_STAGE, {
355
+ [ATTRIBUTE_KEYS.STAGE]: 'validate-output',
356
+ }, async () => {
357
+ await validateAndAuditOutput({
358
+ filesChanged: result.filesChanged,
359
+ workDir,
360
+ constraints: {
361
+ maxFilesPerChange: resolved.maxFiles,
362
+ requireTests: resolved.requireTests,
363
+ blockedPaths: resolved.blockedPaths,
364
+ },
365
+ guardrails: { maxLinesPerPR: currentLevel.guardrails.maxLinesPerPR },
366
+ auditLog,
367
+ log,
368
+ onViolation: async (violationList) => {
369
+ await addComment(`## ${NOTIFICATION_TITLES.fixReviewGuardrailViolations}\n\n${violationList}\n\n${RETRY_MARKER}\n${cycleMarker}`);
370
+ },
371
+ });
372
+ });
373
+ // 10. Push to the same branch (review agents re-run automatically via pull_request.synchronize)
374
+ log.stage('push');
375
+ await execFileAsync('git', ['push', 'origin', currentBranch], { cwd: workDir });
376
+ log.stageEnd('push');
377
+ auditLog.record({
378
+ actor: 'system',
379
+ action: 'create',
380
+ resource: `push/${currentBranch}`,
381
+ decision: 'allowed',
382
+ details: { prNumber, attempt: attempts + 1 },
383
+ });
384
+ // 11. Comment on PR with success details
385
+ const successTpl = notifTemplates?.['fix-review-success'];
386
+ const successComment = successTpl
387
+ ? renderTemplate(successTpl, {
388
+ attempt: String(attempts + 1),
389
+ max: String(maxFixAttempts),
390
+ branch: currentBranch,
391
+ })
392
+ : {
393
+ title: NOTIFICATION_TITLES.fixReviewApplied,
394
+ body: `Attempt ${attempts + 1} of ${maxFixAttempts} — pushed review fixes to \`${currentBranch}\`.`,
395
+ };
396
+ await addComment([
397
+ `## ${successComment.title}`,
398
+ '',
399
+ successComment.body,
400
+ '',
401
+ '### Changes',
402
+ result.filesChanged.map((f) => `- \`${f}\``).join('\n'),
403
+ '',
404
+ RETRY_MARKER,
405
+ cycleMarker,
406
+ ].join('\n'));
407
+ // 12. Record episodic memory (success)
408
+ if (options.memory) {
409
+ options.memory.episodic.append({
410
+ key: 'fix-review-execution',
411
+ value: {
412
+ prNumber,
413
+ issueId,
414
+ filesChanged: result.filesChanged.length,
415
+ outcome: 'success',
416
+ },
417
+ metadata: { summary: `Fix-review for PR #${prNumber} (attempt ${attempts + 1})` },
418
+ });
419
+ options.memory.working.clear();
420
+ }
421
+ }
422
+ catch (err) {
423
+ // Record failure episode before rethrowing
424
+ if (options.memory) {
425
+ options.memory.episodic.append({
426
+ key: 'fix-review-execution',
427
+ value: {
428
+ prNumber,
429
+ issueId,
430
+ outcome: 'failure',
431
+ error: err instanceof Error ? err.message : String(err),
432
+ },
433
+ metadata: { summary: `Failed fix-review for PR #${prNumber}` },
434
+ });
435
+ options.memory.working.clear();
436
+ }
437
+ throw err;
438
+ }
439
+ log.summary();
440
+ }
441
+ //# sourceMappingURL=fix-review.js.map
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export { validateAgentOutput, type ValidationContext, type ValidationResult, typ
5
5
  export { createLogger, type Logger } from './logger.js';
6
6
  export { validateConfigFiles, type FileValidationResult } from './validate-config.js';
7
7
  export { executeFixCI, countRetryAttempts, fetchCILogs, type FixCIOptions } from './fix-ci.js';
8
+ export { executeFixReview, countRetryAttempts as countReviewRetryAttempts, fetchReviewFindings, type FixReviewOptions, } from './fix-review.js';
8
9
  export { executeTriage, type TriageOptions, type TriageResult } from './triage.js';
9
10
  export { getGitHubConfig, resolveRepoRoot, createDefaultAuditLog, resolveAutonomyLevel, resolveConstraints, mergeBlockedPaths, isAutonomousStrategy, recordMetric, validateAndAuditOutput, createPipelineMemory, evaluatePipelineCompliance, authorizeFilesChanged, extractIssueNumber, extractIssueId, issueIdToNumber, formatIssueRef, BRANCH_PATTERN, createAbacPermissionHook, createBlockedPathsHook, createAuditLoggingHook, createPipelineAuthorizationChain, type GitHubEnvConfig, type ValidateAndAuditParams, } from './shared.js';
10
11
  export { DEFAULT_MODEL, DEFAULT_GITHUB_ORG, DEFAULT_GITHUB_REPO, DEFAULT_GITHUB_REPOSITORY, DEFAULT_CONFIG_DIR_NAME, DEFAULT_SANDBOX_MEMORY_MB, DEFAULT_SANDBOX_CPU_PERCENT, DEFAULT_SANDBOX_NETWORK_POLICY, DEFAULT_SANDBOX_TIMEOUT_MS, defaultSandboxConstraints, DEFAULT_RUNNER_TIMEOUT_MS, DEFAULT_ALLOWED_TOOLS, DEFAULT_MAX_FILES_PER_CHANGE, DEFAULT_REQUIRE_TESTS, DEFAULT_BLOCKED_PATHS, DEFAULT_MAX_FIX_ATTEMPTS, DEFAULT_MAX_LOG_LINES, DEFAULT_GH_CLI_TIMEOUT_MS, DEFAULT_JIT_TTL_MS, DEFAULT_JIT_SCOPE, DEFAULT_BRANCH_TEMPLATE, DEFAULT_BRANCH_PATTERN, DEFAULT_PR_TITLE_TEMPLATE, DEFAULT_PR_FOOTER, DEFAULT_COMPLEXITY_THRESHOLDS, DEFAULT_MAX_LINES_PER_PR, DEFAULT_ANALYSIS_INCLUDE, DEFAULT_ANALYSIS_EXCLUDE, DEFAULT_GIT_HISTORY_DAYS, DEFAULT_HOTSPOT_THRESHOLD, NOTIFICATION_TITLES, DEFAULT_MODEL_COSTS, DEFAULT_COST_BUDGET_USD, DEFAULT_DASHBOARD_REFRESH_MS, PROGRESSIVE_GATE_PROFILES, DEFAULT_LINT_COMMAND, DEFAULT_FORMAT_COMMAND, DEFAULT_COMMIT_MESSAGE_TEMPLATE, DEFAULT_COMMIT_CO_AUTHOR, DEFAULT_OPENAI_API_URL, DEFAULT_OPENAI_MODEL, DEFAULT_ANTHROPIC_API_URL, DEFAULT_ANTHROPIC_MODEL, DEFAULT_GENERIC_LLM_MODEL, DEFAULT_LLM_TIMEOUT_MS, DEFAULT_LLM_MAX_TOKENS, DEFAULT_LLM_SYSTEM_PROMPT, DEFAULT_DOCKER_IMAGE, DEFAULT_WORKFLOW_FILE, DEFAULT_LABEL_TO_SKILL_MAP, DEFAULT_ANALYSIS_CACHE_TTL_MS, } from './defaults.js';
@@ -18,6 +19,9 @@ export { createPipelineDiscovery, findMatchingAgent, resolveAgentForIssue, match
18
19
  export { createStructuredConsoleLogger, createStructuredBufferLogger, } from './structured-logger.js';
19
20
  export { startWatch, type WatchOptions, type WatchHandle } from './watch.js';
20
21
  export { computePriority, rankWorkItems, type PriorityScore, type PriorityInput, type PriorityConfig, } from './priority.js';
22
+ export { checkAction, enforceAction, DEFAULT_BLOCKED_ACTIONS, type ActionEnforcementResult, } from './action-enforcement.js';
23
+ export { scoreIssueForAdmission, mapIssueToPriorityInput, type AdmissionInput, type AdmissionThresholds, type IssueAdmissionResult, type AuthorAssociation, } from './admission-score.js';
24
+ export { executeReview, type ReviewContext, type ReviewOptions } from './review.js';
21
25
  export { createPipelineOrchestration, executePipelineOrchestration, validatePipelineHandoffs, sequential, parallel, hybrid, hierarchical, swarm, validateHandoff, simpleSchemaValidate, } from './orchestration.js';
22
26
  export { createPipelineRegoEvaluator, createPipelineCELEvaluator, createPipelineABACHook, createPipelineExpressionEvaluator, createPipelineLLMEvaluator, evaluatePipelineGate, scorePipelineComplexity, evaluatePipelineComplexityRouting, } from './policy-evaluators.js';
23
27
  export { createPipelineAdapterRegistry, createPipelineWebhookBridge, resolveAdapterFromGit, resolveInfrastructure, resolveIssueTrackerFromConfig, scanPipelineAdapters, } from './adapters.js';
@@ -25,12 +29,14 @@ export { createPipelineReconciler, createGateReconciler, createAutonomyReconcile
25
29
  export { createFileAuditLog, verifyAuditIntegrity, loadAuditEntries, rotateAuditLog, computeAuditHash, } from './audit-extended.js';
26
30
  export { checkFrameworkCompliance, getControlCatalog, getFrameworkMappings, listSupportedFrameworks, } from './compliance-extended.js';
27
31
  export { createSilentLogger, withPipelineSpanSync, getPipelineTracer, validateResourceSchema, } from './telemetry-extended.js';
28
- export { ClaudeCodeRunner, ClaudeCodeRunner as GitHubActionsRunner, GenericLLMRunner, CopilotRunner, CursorRunner, CodexRunner, RunnerRegistry, createRunnerRegistry, SecurityTriageRunner, type AgentRunner, type AgentContext, type AgentResult, type GenericLLMConfig, type RegisteredRunner, type SecurityTriageConfig, type TriageVerdict, } from './runners/index.js';
32
+ export { ClaudeCodeRunner, ClaudeCodeRunner as GitHubActionsRunner, GenericLLMRunner, CopilotRunner, CursorRunner, CodexRunner, RunnerRegistry, createRunnerRegistry, SecurityTriageRunner, ReviewAgentRunner, REVIEW_PROMPTS, type AgentRunner, type AgentContext, type AgentResult, type AgentProgressEvent, type GenericLLMConfig, type RegisteredRunner, type SecurityTriageConfig, type TriageVerdict, type ReviewAgentConfig, type ReviewType, type ReviewFinding, type ReviewVerdict, } from './runners/index.js';
29
33
  export type { TokenUsage } from './runners/index.js';
30
34
  export { SlackMessenger, TeamsMessenger, NotificationRouter } from './notifications/index.js';
31
35
  export type { SlackConfig, TeamsConfig, PipelineEvent, PipelineEventType, NotificationRoute, NotificationTemplate, } from './notifications/index.js';
32
36
  export { StateStore } from './state/index.js';
33
- export type { HotspotRecord, RoutingDecision, CostLedgerEntry, GateThresholdOverride, AutonomyEvent, AutonomyEventType, HandoffEvent, DeploymentRecord, DeploymentRecordState, RolloutStepRecord, AuditEntryRecord, PriorityCalibrationSample, } from './state/index.js';
37
+ export type { HotspotRecord, RoutingDecision, CostLedgerEntry, GateThresholdOverride, AutonomyEvent, AutonomyEventType, HandoffEvent, DeploymentRecord, DeploymentRecordState, RolloutStepRecord, AuditEntryRecord, PriorityCalibrationSample, ToolSequenceEvent, WorkflowPattern, PatternProposal, } from './state/index.js';
38
+ export { readToolSequenceJSONL, readSessionMetaFiles, sessionMetaToEvents, categorizeAction, DEFAULT_DETECTION_OPTIONS, } from './workflow-patterns/index.js';
39
+ export type { CanonicalStep, NGram, DetectedPattern, DetectionOptions, RawToolSequenceEntry, SessionMeta, } from './workflow-patterns/index.js';
34
40
  export { createKubernetesTarget, createVercelTarget, createFlyioTarget, createHttpMetricsCollector, createStubMetricsCollector, RolloutController, } from './deploy/index.js';
35
41
  export type { DeploymentTargetConfig, HealthCheckConfig, DeploymentState, DeploymentResult, DeploymentTarget, ExecFn, FetchFn, KubernetesConfig, VercelConfig, FlyioConfig, CanaryStep, CanaryConfig, BlueGreenConfig, RollingConfig, RolloutStrategy, RolloutPhase, RolloutStatus, RolloutMetrics, MetricsSource, RolloutControllerConfig, HttpMetricsConfig, } from './deploy/index.js';
36
42
  export { getComplexityBand, getGateProfile, adjustEnforcement, adjustGateForComplexity, adjustGatesForComplexity, computeGateAdjustments, } from './progressive-gates.js';
@@ -69,5 +75,7 @@ export type { ArchiveManifest, ArchivalOptions } from './audit-archival.js';
69
75
  export { Orchestrator, type OrchestratorConfig, type WebhookConfig } from './orchestrator.js';
70
76
  export type { OrchestratorPlugin, PluginContext, BeforeRunEvent, AfterRunEvent, RunErrorEvent, } from './plugin.js';
71
77
  export { CostGovernancePlugin } from './cost-governance.js';
78
+ export { PipelineCycleDetector, createStageMarker, parseStageInvocations, DEFAULT_CYCLE_LIMITS, type PipelineStage, type CycleConfig, type CycleDetectionResult, } from './pipeline-cycle-detector.js';
79
+ export { checkAndHandleCycle, createCycleDetectorFromConfig, type CycleHandlerOptions, type CycleCheckResult, } from './cycle-utils.js';
72
80
  export type { ApiVersion, Metadata, Condition, SecretRef, MetricCondition, Duration, Resource, TriggerFilter, Trigger, Provider, RoutingStrategy, ComplexityThreshold, Routing, Stage, PipelineSpec, PipelinePhase, PipelineStatus, Pipeline, AgentConstraints, HandoffContractRef, Handoff, SkillExample, Skill, AgentCard, AgentRoleSpec, AgentRoleStatus, AgentRole, GateScope, MetricRule, ToolRule, ReviewerRule, DocumentationRule, ProvenanceRule, ExpressionRule, GateRule, EnforcementLevel, Override, RetryPolicy, Evaluation, Gate, QualityGateSpec, QualityGateStatus, QualityGate, Permissions, ApprovalRequirement, Guardrails, MonitoringLevel, AutonomyLevel, PromotionCriteria, DemotionTrigger, AgentAutonomyStatus, AutonomyPolicySpec, AutonomyPolicyStatus, AutonomyPolicy, AdapterInterface, HealthCheck, AdapterBindingSpec, AdapterBindingStatus, AdapterBinding, AnyResource, ResourceKind, ValidationError, GateVerdict, DemotionResult, ComplexityFactor, AuthorizationContext, AuthorizationResult, AuthIdentity, AuthenticationResult, Authenticator, MutatingGateContext, ExpressionEvaluator, ExpressionVerdict, LLMEvaluationDimension, LLMEvaluationResult, LLMGateVerdict, AgentExecutionState, HandoffValidationError, SchemaResolver, SchemaValidationError, MemoryTier, MemoryEntry, WorkingMemory, ShortTermMemory, LongTermMemory, SharedMemory, EpisodicMemory, } from './types.js';
73
81
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export { validateAgentOutput, } from './validate-agent-output.js';
6
6
  export { createLogger } from './logger.js';
7
7
  export { validateConfigFiles } from './validate-config.js';
8
8
  export { executeFixCI, countRetryAttempts, fetchCILogs } from './fix-ci.js';
9
+ export { executeFixReview, countRetryAttempts as countReviewRetryAttempts, fetchReviewFindings, } from './fix-review.js';
9
10
  export { executeTriage } from './triage.js';
10
11
  // Shared utilities
11
12
  export { getGitHubConfig, resolveRepoRoot, createDefaultAuditLog, resolveAutonomyLevel, resolveConstraints, mergeBlockedPaths, isAutonomousStrategy, recordMetric, validateAndAuditOutput, createPipelineMemory, evaluatePipelineCompliance, authorizeFilesChanged, extractIssueNumber, extractIssueId, issueIdToNumber, formatIssueRef, BRANCH_PATTERN, createAbacPermissionHook, createBlockedPathsHook, createAuditLoggingHook, createPipelineAuthorizationChain, } from './shared.js';
@@ -29,6 +30,12 @@ export { createStructuredConsoleLogger, createStructuredBufferLogger, } from './
29
30
  export { startWatch } from './watch.js';
30
31
  // Priority scoring (PPA)
31
32
  export { computePriority, rankWorkItems, } from './priority.js';
33
+ // Action enforcement
34
+ export { checkAction, enforceAction, DEFAULT_BLOCKED_ACTIONS, } from './action-enforcement.js';
35
+ // Issue admission scoring
36
+ export { scoreIssueForAdmission, mapIssueToPriorityInput, } from './admission-score.js';
37
+ // PR review orchestration
38
+ export { executeReview } from './review.js';
32
39
  // Agent orchestration
33
40
  export { createPipelineOrchestration, executePipelineOrchestration, validatePipelineHandoffs, sequential, parallel, hybrid, hierarchical, swarm, validateHandoff, simpleSchemaValidate, } from './orchestration.js';
34
41
  // Policy evaluators
@@ -44,11 +51,13 @@ export { checkFrameworkCompliance, getControlCatalog, getFrameworkMappings, list
44
51
  // Extended telemetry
45
52
  export { createSilentLogger, withPipelineSpanSync, getPipelineTracer, validateResourceSchema, } from './telemetry-extended.js';
46
53
  // Runners
47
- export { ClaudeCodeRunner, ClaudeCodeRunner as GitHubActionsRunner, GenericLLMRunner, CopilotRunner, CursorRunner, CodexRunner, RunnerRegistry, createRunnerRegistry, SecurityTriageRunner, } from './runners/index.js';
54
+ export { ClaudeCodeRunner, ClaudeCodeRunner as GitHubActionsRunner, GenericLLMRunner, CopilotRunner, CursorRunner, CodexRunner, RunnerRegistry, createRunnerRegistry, SecurityTriageRunner, ReviewAgentRunner, REVIEW_PROMPTS, } from './runners/index.js';
48
55
  // Notifications
49
56
  export { SlackMessenger, TeamsMessenger, NotificationRouter } from './notifications/index.js';
50
57
  // State store
51
58
  export { StateStore } from './state/index.js';
59
+ // Workflow pattern detection
60
+ export { readToolSequenceJSONL, readSessionMetaFiles, sessionMetaToEvents, categorizeAction, DEFAULT_DETECTION_OPTIONS, } from './workflow-patterns/index.js';
52
61
  // Deployment targets
53
62
  export { createKubernetesTarget, createVercelTarget, createFlyioTarget, createHttpMetricsCollector, createStubMetricsCollector, RolloutController, } from './deploy/index.js';
54
63
  // Progressive gates
@@ -88,4 +97,7 @@ export { archiveEntries, loadArchivedEntries, verifyArchiveContinuity } from './
88
97
  export { Orchestrator } from './orchestrator.js';
89
98
  // Cost governance plugin
90
99
  export { CostGovernancePlugin } from './cost-governance.js';
100
+ // Pipeline cycle detection
101
+ export { PipelineCycleDetector, createStageMarker, parseStageInvocations, DEFAULT_CYCLE_LIMITS, } from './pipeline-cycle-detector.js';
102
+ export { checkAndHandleCycle, createCycleDetectorFromConfig, } from './cycle-utils.js';
91
103
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,70 @@
1
+ /**
2
+ * PipelineCycleDetector — tracks stage invocations per issue/PR across
3
+ * all workflow runs to detect infinite orchestration loops.
4
+ *
5
+ * Uses GitHub issue/PR comments as shared state (hidden HTML markers)
6
+ * to work across workflow boundaries.
7
+ */
8
+ import type { IssueTracker } from '@ai-sdlc/reference';
9
+ export type PipelineStage = 'admission' | 'triage' | 'agent' | 'review' | 'fix-ci' | 'fix-review';
10
+ export interface CycleConfig {
11
+ /** Max invocations per stage (default: 3 for agent stages, 2 for fix stages). */
12
+ maxInvocations: Record<PipelineStage, number>;
13
+ }
14
+ export interface CycleDetectionResult {
15
+ cycleDetected: boolean;
16
+ loopingStages: Array<{
17
+ stage: PipelineStage;
18
+ count: number;
19
+ max: number;
20
+ }>;
21
+ totalInvocations: number;
22
+ }
23
+ /**
24
+ * Default max invocations per stage.
25
+ * These must be HIGHER than the per-stage retry limits (which are the
26
+ * primary guard). The cycle detector is a safety net for cross-stage
27
+ * loops, not a replacement for retry counting.
28
+ *
29
+ * Per-stage retry limits: fix-ci=2, fix-review=2
30
+ * Cycle limits: set to retry_limit + 2 to allow retries + headroom
31
+ */
32
+ export declare const DEFAULT_CYCLE_LIMITS: Record<PipelineStage, number>;
33
+ /**
34
+ * Generate HTML comment marker for a stage invocation.
35
+ * Format: <!-- ai-sdlc-cycle:{stage}:{timestamp} -->
36
+ */
37
+ export declare function createStageMarker(stage: PipelineStage): string;
38
+ /**
39
+ * Parse stage invocation markers from comment bodies.
40
+ * Returns a map of stage -> invocation count.
41
+ */
42
+ export declare function parseStageInvocations(comments: string[]): Map<PipelineStage, number>;
43
+ export declare class PipelineCycleDetector {
44
+ private config;
45
+ constructor(config?: Partial<CycleConfig>);
46
+ /**
47
+ * Check if a cycle exists for the given issue/PR by analyzing comment history.
48
+ * @param pendingStage — if set, adds +1 to this stage's count to account for the upcoming invocation
49
+ */
50
+ detectCycle(tracker: IssueTracker, issueOrPrId: string, pendingStage?: PipelineStage): Promise<CycleDetectionResult>;
51
+ /**
52
+ * Detect cycle from comment bodies (for testing without IssueTracker).
53
+ * @param pendingStage — if set, adds +1 to this stage's count for the pending invocation
54
+ */
55
+ detectCycleFromComments(comments: string[], pendingStage?: PipelineStage): CycleDetectionResult;
56
+ /**
57
+ * Record a stage invocation by creating a marker.
58
+ * The caller should append this to their comment.
59
+ */
60
+ recordInvocation(stage: PipelineStage): string;
61
+ /**
62
+ * Get the max invocation limit for a stage.
63
+ */
64
+ getMaxInvocations(stage: PipelineStage): number;
65
+ /**
66
+ * Update max invocations for specific stages.
67
+ */
68
+ updateMaxInvocations(overrides: Partial<Record<PipelineStage, number>>): void;
69
+ }
70
+ //# sourceMappingURL=pipeline-cycle-detector.d.ts.map