@ai-sdlc/orchestrator 0.4.0 → 0.5.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.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Security triage pipeline — lightweight entry point that fetches an issue,
3
+ * runs the SecurityTriageRunner, posts findings as a comment, and applies
4
+ * a `rejected` or `triage-passed` label.
5
+ *
6
+ * **Asymmetric by design**: the triage agent can auto-reject issues above the
7
+ * risk threshold, but NEVER auto-approves (no `ai-ready` label). A human
8
+ * must review the triage analysis and manually apply `ai-ready`.
9
+ */
10
+ import type { IssueTracker } from '@ai-sdlc/reference';
11
+ import { type SecurityTriageConfig, type TriageVerdict } from './runners/security-triage.js';
12
+ import { type Logger } from './logger.js';
13
+ export interface TriageOptions {
14
+ /** Override the issue tracker (skips config-driven resolution). */
15
+ tracker?: IssueTracker;
16
+ /** SecurityTriageRunner configuration overrides. */
17
+ triageConfig?: SecurityTriageConfig;
18
+ /** Custom logger. */
19
+ logger?: Logger;
20
+ /** Working directory for config loading. Defaults to cwd. */
21
+ workDir?: string;
22
+ /** If true, skip posting a comment to the issue. */
23
+ dryRun?: boolean;
24
+ }
25
+ export interface TriageResult {
26
+ issueId: string;
27
+ verdict: TriageVerdict;
28
+ /** Whether the issue was auto-rejected (riskScore >= threshold). */
29
+ rejected: boolean;
30
+ /** The label applied to the issue, if any. */
31
+ labelApplied?: string;
32
+ /** Error message if the triage pipeline failed. */
33
+ error?: string;
34
+ }
35
+ export declare function executeTriage(issueId: string, options?: TriageOptions): Promise<TriageResult>;
36
+ //# sourceMappingURL=triage.d.ts.map
package/dist/triage.js ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Security triage pipeline — lightweight entry point that fetches an issue,
3
+ * runs the SecurityTriageRunner, posts findings as a comment, and applies
4
+ * a `rejected` or `triage-passed` label.
5
+ *
6
+ * **Asymmetric by design**: the triage agent can auto-reject issues above the
7
+ * risk threshold, but NEVER auto-approves (no `ai-ready` label). A human
8
+ * must review the triage analysis and manually apply `ai-ready`.
9
+ */
10
+ import { join } from 'node:path';
11
+ import { loadConfigAsync } from './config.js';
12
+ import { resolveIssueTrackerFromConfig } from './adapters.js';
13
+ import { getGitHubConfig } from './shared.js';
14
+ import { DEFAULT_CONFIG_DIR_NAME } from './defaults.js';
15
+ import { SecurityTriageRunner, } from './runners/security-triage.js';
16
+ import { createLogger } from './logger.js';
17
+ // ── Labels ──────────────────────────────────────────────────────────
18
+ const LABEL_REJECTED = 'security-rejected';
19
+ const LABEL_TRIAGE_PASSED = 'triage-passed';
20
+ // ── Comment formatting ──────────────────────────────────────────────
21
+ function formatTriageComment(verdict, rejected) {
22
+ const icon = rejected ? '🚨' : verdict.riskScore >= 3 ? '⚠️' : '✅';
23
+ const status = rejected ? 'REJECTED' : 'PASSED';
24
+ const lines = [
25
+ `## ${icon} Security Triage: ${status}`,
26
+ '',
27
+ `**Risk Score:** ${verdict.riskScore}/10`,
28
+ `**Safe:** ${verdict.safe}`,
29
+ '',
30
+ ];
31
+ if (verdict.findings.length > 0) {
32
+ lines.push('### Findings', '');
33
+ for (const finding of verdict.findings) {
34
+ lines.push(`- ${finding}`);
35
+ }
36
+ lines.push('');
37
+ }
38
+ lines.push('### Rationale', '', verdict.rationale, '');
39
+ if (rejected) {
40
+ lines.push('---', '> This issue has been automatically rejected due to a high risk score.', '> A maintainer may override this by removing the `security-rejected` label', '> and manually applying `ai-ready` after review.');
41
+ }
42
+ else {
43
+ lines.push('---', '> This issue passed automated security triage.', '> A maintainer must still manually apply the `ai-ready` label to enable AI processing.');
44
+ }
45
+ lines.push('', '*Analyzed by [AI-SDLC Security Triage](https://github.com/ai-sdlc-framework/ai-sdlc)*');
46
+ return lines.join('\n');
47
+ }
48
+ // ── Pipeline ────────────────────────────────────────────────────────
49
+ export async function executeTriage(issueId, options = {}) {
50
+ const workDir = options.workDir ?? process.cwd();
51
+ const log = options.logger ?? createLogger();
52
+ log.info(`[triage] Starting security triage for issue ${issueId}`);
53
+ // ── Resolve issue tracker ───────────────────────────────────────
54
+ let tracker;
55
+ if (options.tracker) {
56
+ tracker = options.tracker;
57
+ }
58
+ else {
59
+ const configDir = join(workDir, DEFAULT_CONFIG_DIR_NAME);
60
+ const config = await loadConfigAsync(configDir);
61
+ const ghConfig = getGitHubConfig();
62
+ tracker = resolveIssueTrackerFromConfig(config, {
63
+ org: ghConfig.org,
64
+ repo: ghConfig.repo,
65
+ token: { secretRef: 'GITHUB_TOKEN' },
66
+ });
67
+ }
68
+ // ── Fetch issue ─────────────────────────────────────────────────
69
+ const issue = await tracker.getIssue(issueId);
70
+ log.info(`[triage] Fetched issue: "${issue.title}"`);
71
+ // ── Run triage ──────────────────────────────────────────────────
72
+ const runner = new SecurityTriageRunner(options.triageConfig);
73
+ const agentResult = await runner.run({
74
+ issueId,
75
+ issueTitle: issue.title,
76
+ issueBody: issue.description ?? '',
77
+ workDir,
78
+ branch: 'main',
79
+ constraints: {
80
+ maxFilesPerChange: 0,
81
+ requireTests: false,
82
+ blockedPaths: ['**/*'],
83
+ },
84
+ });
85
+ if (!agentResult.success) {
86
+ log.error(`[triage] Triage failed: ${agentResult.error}`);
87
+ return {
88
+ issueId,
89
+ verdict: {
90
+ safe: false,
91
+ riskScore: 7,
92
+ findings: ['Triage pipeline error — treating as suspicious'],
93
+ sanitizedDescription: '',
94
+ rationale: agentResult.error ?? 'Unknown error',
95
+ },
96
+ rejected: true,
97
+ error: agentResult.error,
98
+ };
99
+ }
100
+ // ── Parse verdict ───────────────────────────────────────────────
101
+ const verdict = JSON.parse(agentResult.summary);
102
+ const rejected = verdict.riskScore >= runner.rejectThreshold;
103
+ log.info(`[triage] Verdict: riskScore=${verdict.riskScore}, safe=${verdict.safe}, rejected=${rejected}`);
104
+ // ── Post comment & apply label ──────────────────────────────────
105
+ if (!options.dryRun) {
106
+ const comment = formatTriageComment(verdict, rejected);
107
+ try {
108
+ await tracker.addComment(issueId, comment);
109
+ log.info(`[triage] Posted triage comment on issue ${issueId}`);
110
+ }
111
+ catch (err) {
112
+ log.error(`[triage] Failed to post comment: ${err}`);
113
+ }
114
+ const label = rejected ? LABEL_REJECTED : LABEL_TRIAGE_PASSED;
115
+ try {
116
+ const existingLabels = issue.labels ?? [];
117
+ await tracker.updateIssue(issueId, {
118
+ labels: [
119
+ ...existingLabels.filter((l) => l !== LABEL_REJECTED && l !== LABEL_TRIAGE_PASSED),
120
+ label,
121
+ ],
122
+ });
123
+ log.info(`[triage] Applied label "${label}" to issue ${issueId}`);
124
+ return { issueId, verdict, rejected, labelApplied: label };
125
+ }
126
+ catch (err) {
127
+ log.error(`[triage] Failed to apply label: ${err}`);
128
+ return { issueId, verdict, rejected, error: `Label application failed: ${err}` };
129
+ }
130
+ }
131
+ return { issueId, verdict, rejected };
132
+ }
133
+ //# sourceMappingURL=triage.js.map
package/dist/types.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * Ensures all reference types are accessible through the orchestrator package
4
4
  * for downstream consumers without direct reference dependency.
5
5
  */
6
- 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, } from '@ai-sdlc/reference';
6
+ 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, PriorityPolicy, PriorityDimensionConfig, PriorityDimensionsConfig, PriorityCalibrationConfig, PriorityAdaptersConfig, ValidationError, } from '@ai-sdlc/reference';
7
7
  export type { GateVerdict, DemotionResult, ComplexityFactor, AuthorizationContext, AuthorizationResult, AuthIdentity, AuthenticationResult, Authenticator, MutatingGateContext, ExpressionEvaluator, ExpressionVerdict, LLMEvaluationDimension, LLMEvaluationResult, LLMGateVerdict, } from '@ai-sdlc/reference';
8
8
  export type { AgentExecutionState, HandoffValidationError, SchemaResolver, SchemaValidationError, MemoryTier, MemoryEntry, WorkingMemory, ShortTermMemory, LongTermMemory, SharedMemory, EpisodicMemory, } from '@ai-sdlc/reference';
9
9
  //# sourceMappingURL=types.d.ts.map
package/dist/watch.d.ts CHANGED
@@ -6,6 +6,8 @@
6
6
  */
7
7
  import { type ReconcilerConfig, type ReconcileResult, type Pipeline, type QualityGate, type AutonomyPolicy, type AgentRole, type MetricStore } from '@ai-sdlc/reference';
8
8
  import { type ExecuteOptions } from './execute.js';
9
+ import { type PriorityInput } from './priority.js';
10
+ import type { PriorityPolicy } from '@ai-sdlc/reference';
9
11
  export interface WatchOptions {
10
12
  /** Override the reconciler config (poll interval, concurrency, backoff). */
11
13
  reconcilerConfig?: Partial<ReconcilerConfig>;
@@ -21,10 +23,12 @@ export interface WatchOptions {
21
23
  qualityGates?: QualityGate[];
22
24
  /** Optional autonomy policies for autonomy reconciler evaluation. */
23
25
  autonomyPolicies?: AutonomyPolicy[];
26
+ /** Priority scoring policy configuration. */
27
+ priorityPolicy?: PriorityPolicy;
24
28
  }
25
29
  export interface WatchHandle {
26
- /** Enqueue a pipeline resource for reconciliation. */
27
- enqueue(pipeline: Pipeline, issueNumber: number): void;
30
+ /** Enqueue a pipeline resource for reconciliation. Optionally provide PriorityInput for scoring. */
31
+ enqueue(pipeline: Pipeline, issueId: string, priorityInput?: PriorityInput): void;
28
32
  /** Enqueue a quality gate resource for reconciliation. */
29
33
  enqueueGate(gate: QualityGate): void;
30
34
  /** Enqueue an autonomy policy resource for reconciliation. */
package/dist/watch.js CHANGED
@@ -7,6 +7,7 @@
7
7
  import { ReconcilerLoop, createResourceCache, instrumentReconciler, } from '@ai-sdlc/reference';
8
8
  import { executePipeline } from './execute.js';
9
9
  import { createPipelineReconciler, createGateReconciler, createAutonomyReconciler, } from './reconcilers.js';
10
+ import { computePriority } from './priority.js';
10
11
  /**
11
12
  * Start a reconciler watch loop that continuously processes pipeline resources.
12
13
  */
@@ -30,15 +31,15 @@ export function startWatch(options = {}) {
30
31
  switch (resource.kind) {
31
32
  case 'Pipeline': {
32
33
  const pipeline = resource;
33
- const issueNumber = issueMap.get(pipeline.metadata.name);
34
- if (!issueNumber) {
34
+ const issueId = issueMap.get(pipeline.metadata.name);
35
+ if (!issueId) {
35
36
  return {
36
37
  type: 'error',
37
- error: new Error(`No issue number for pipeline ${pipeline.metadata.name}`),
38
+ error: new Error(`No issue ID for pipeline ${pipeline.metadata.name}`),
38
39
  };
39
40
  }
40
41
  try {
41
- await executePipeline(issueNumber, {
42
+ await executePipeline(issueId, {
42
43
  ...options.executeOptions,
43
44
  });
44
45
  const result = { type: 'success' };
@@ -68,9 +69,36 @@ export function startWatch(options = {}) {
68
69
  }
69
70
  const loop = new ReconcilerLoop(reconcileFn, options.reconcilerConfig);
70
71
  loop.start();
72
+ // Map to hold priority scores for pipelines
73
+ const priorityScores = new Map();
71
74
  return {
72
- enqueue(pipeline, issueNumber) {
73
- issueMap.set(pipeline.metadata.name, issueNumber);
75
+ enqueue(pipeline, issueId, priorityInput) {
76
+ issueMap.set(pipeline.metadata.name, issueId);
77
+ // Priority scoring when policy is enabled
78
+ if (options.priorityPolicy?.enabled && priorityInput) {
79
+ const config = options.priorityPolicy.dimensions?.humanCurveWeights
80
+ ? { humanCurveWeights: options.priorityPolicy.dimensions.humanCurveWeights }
81
+ : undefined;
82
+ const score = computePriority(priorityInput, config);
83
+ // Skip items below minimum score
84
+ if (options.priorityPolicy.minimumScore !== undefined &&
85
+ score.composite < options.priorityPolicy.minimumScore) {
86
+ return;
87
+ }
88
+ // Flag items below minimum confidence (still enqueue, but mark)
89
+ if (options.priorityPolicy.minimumConfidence !== undefined &&
90
+ score.confidence < options.priorityPolicy.minimumConfidence) {
91
+ // Store the score with a low-confidence flag for downstream consumers
92
+ priorityScores.set(pipeline.metadata.name, score);
93
+ }
94
+ else {
95
+ priorityScores.set(pipeline.metadata.name, score);
96
+ }
97
+ if (cache.shouldReconcile(pipeline)) {
98
+ loop.enqueue(pipeline, score.composite);
99
+ }
100
+ return;
101
+ }
74
102
  if (cache.shouldReconcile(pipeline)) {
75
103
  loop.enqueue(pipeline);
76
104
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdlc/orchestrator",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "AI-SDLC Orchestrator — long-running runtime that drives issues through the complete SDLC with AI agents",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -45,11 +45,12 @@
45
45
  "better-sqlite3": "^11.0.0",
46
46
  "commander": "^12.0.0",
47
47
  "yaml": "^2.7.0",
48
- "@ai-sdlc/reference": "0.4.0"
48
+ "@ai-sdlc/reference": "0.5.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/better-sqlite3": "^7.6.0",
52
52
  "@types/node": "^22.0.0",
53
+ "@vitest/coverage-v8": "^3.2.4",
53
54
  "tsx": "^4.19.0",
54
55
  "typescript": "^5.7.0",
55
56
  "vitest": "^3.0.0"
@@ -58,6 +59,7 @@
58
59
  "build": "tsc",
59
60
  "clean": "rm -rf dist *.tsbuildinfo",
60
61
  "test": "vitest run",
62
+ "test:coverage": "vitest run --coverage",
61
63
  "test:watch": "vitest"
62
64
  }
63
65
  }