@ai-sdlc/orchestrator 0.1.1 → 0.1.2

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,6 @@
1
+ /**
2
+ * ai-sdlc validate — validate config files without running the full health check.
3
+ */
4
+ import { Command } from 'commander';
5
+ export declare const validateCommand: Command;
6
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * ai-sdlc validate — validate config files without running the full health check.
3
+ */
4
+ import { resolve } from 'node:path';
5
+ import { Command } from 'commander';
6
+ import { formatOutput } from '../formatters/index.js';
7
+ import { validateConfigFiles } from '../../validate-config.js';
8
+ export const validateCommand = new Command('validate')
9
+ .description('Validate AI-SDLC config files')
10
+ .option('--file <name>', 'Validate a specific YAML file only')
11
+ .action(async (opts, cmd) => {
12
+ const globalOpts = cmd.parent?.opts() ?? {};
13
+ const format = globalOpts.format ?? 'table';
14
+ const configDir = resolve(globalOpts.config ?? '.ai-sdlc');
15
+ const results = validateConfigFiles(configDir, opts.file);
16
+ console.log(formatOutput(format, { type: 'validate', results, configDir }));
17
+ if (results.some((r) => !r.valid)) {
18
+ process.exitCode = 1;
19
+ }
20
+ });
21
+ //# sourceMappingURL=validate.js.map
@@ -172,6 +172,29 @@ export function formatTable(data) {
172
172
  }
173
173
  break;
174
174
  }
175
+ case 'validate': {
176
+ const configDir = data.configDir;
177
+ lines.push(`Validation Results (${configDir})`);
178
+ lines.push('─'.repeat(50));
179
+ const results = data.results;
180
+ if (results.length === 0) {
181
+ lines.push('No YAML files found.');
182
+ }
183
+ else {
184
+ for (const r of results) {
185
+ const status = r.valid ? 'VALID' : 'INVALID';
186
+ const kindStr = r.kind ? String(r.kind).padEnd(16) : '(unknown)'.padEnd(16);
187
+ lines.push(` ${String(r.file).padEnd(28)} ${kindStr} ${status}`);
188
+ if (!r.valid) {
189
+ const errors = r.errors;
190
+ for (const e of errors) {
191
+ lines.push(` ${e.path}: ${e.message}`);
192
+ }
193
+ }
194
+ }
195
+ }
196
+ break;
197
+ }
175
198
  default: {
176
199
  // Generic key-value output
177
200
  for (const [key, value] of Object.entries(data)) {
package/dist/cli/index.js CHANGED
@@ -13,6 +13,7 @@ import { routingCommand } from './commands/routing.js';
13
13
  import { complexityCommand } from './commands/complexity.js';
14
14
  import { costCommand } from './commands/cost.js';
15
15
  import { dashboardCommand } from './commands/dashboard.js';
16
+ import { validateCommand } from './commands/validate.js';
16
17
  const program = new Command();
17
18
  program
18
19
  .name('ai-sdlc')
@@ -31,5 +32,6 @@ program.addCommand(routingCommand);
31
32
  program.addCommand(complexityCommand);
32
33
  program.addCommand(costCommand);
33
34
  program.addCommand(dashboardCommand);
35
+ program.addCommand(validateCommand);
34
36
  program.parse();
35
37
  //# sourceMappingURL=index.js.map
package/dist/execute.js CHANGED
@@ -29,6 +29,7 @@ import { createPipelineMemory } from './shared.js';
29
29
  import { hasResourceChanged, fingerprintResource } from './reconcilers.js';
30
30
  import { CostTracker } from './cost-tracker.js';
31
31
  import { enrichAgentContext } from './context-enrichment.js';
32
+ import { reportGateCheckRuns } from './check-runs.js';
32
33
  /**
33
34
  * Execute the full AI-SDLC pipeline for a given issue number.
34
35
  */
@@ -370,7 +371,44 @@ async function executePipelineBody(issueNumber, issue, config, qualityGate, agen
370
371
  },
371
372
  });
372
373
  });
373
- // 11b. Post-agent complexity evaluation (non-blocking)
374
+ // 11b. Evaluate quality gates and report as GitHub Check Runs
375
+ if (qualityGate.spec.gates.length > 0) {
376
+ try {
377
+ const { stdout: headSha } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
378
+ cwd: workDir,
379
+ });
380
+ const gateResults = qualityGate.spec.gates.map((gate) => {
381
+ const evalResult = evaluatePipelineGate(gate, {
382
+ authorType: 'ai-agent',
383
+ repository: '',
384
+ metrics: costMetrics,
385
+ });
386
+ return {
387
+ gate: evalResult.gate,
388
+ verdict: evalResult.verdict === 'override' ? 'pass' : evalResult.verdict,
389
+ message: evalResult.message,
390
+ };
391
+ });
392
+ // Report to GitHub Check Runs (best-effort, non-blocking)
393
+ await reportGateCheckRuns(headSha.trim(), gateResults).catch(() => {
394
+ log.info('Failed to report gate check runs to GitHub (non-blocking)');
395
+ });
396
+ auditLog.record({
397
+ actor: 'system',
398
+ action: 'evaluate',
399
+ resource: `issue#${issueNumber}`,
400
+ policy: 'post-agent-gates',
401
+ decision: gateResults.every((g) => g.verdict === 'pass') ? 'allowed' : 'denied',
402
+ details: {
403
+ gates: gateResults.map((g) => ({ gate: g.gate, verdict: g.verdict })),
404
+ },
405
+ });
406
+ }
407
+ catch {
408
+ log.info('Post-agent gate evaluation skipped');
409
+ }
410
+ }
411
+ // 11c. Post-agent complexity evaluation (non-blocking)
374
412
  try {
375
413
  const { stdout: diffStat } = await execFileAsync('git', ['diff', '--stat', 'HEAD~1'], {
376
414
  cwd: workDir,
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { validateIssue, validateIssueWithExtensions, parseComplexity } from './v
3
3
  export { executePipeline, type ExecuteOptions, type PipelineResult, type PromotionResult, } from './execute.js';
4
4
  export { validateAgentOutput, type ValidationContext, type ValidationResult, type ValidationViolation, } from './validate-agent-output.js';
5
5
  export { createLogger, type Logger } from './logger.js';
6
+ export { validateConfigFiles, type FileValidationResult } from './validate-config.js';
6
7
  export { executeFixCI, countRetryAttempts, fetchCILogs, type FixCIOptions } from './fix-ci.js';
7
8
  export { getGitHubConfig, resolveRepoRoot, createDefaultAuditLog, resolveAutonomyLevel, resolveConstraints, mergeBlockedPaths, isAutonomousStrategy, recordMetric, validateAndAuditOutput, createPipelineMemory, evaluatePipelineCompliance, authorizeFilesChanged, extractIssueNumber, BRANCH_PATTERN, createAbacPermissionHook, createBlockedPathsHook, createAuditLoggingHook, createPipelineAuthorizationChain, type GitHubEnvConfig, type ValidateAndAuditParams, } from './shared.js';
8
9
  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';
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export { validateIssue, validateIssueWithExtensions, parseComplexity } from './v
4
4
  export { executePipeline, } from './execute.js';
5
5
  export { validateAgentOutput, } from './validate-agent-output.js';
6
6
  export { createLogger } from './logger.js';
7
+ export { validateConfigFiles } from './validate-config.js';
7
8
  export { executeFixCI, countRetryAttempts, fetchCILogs } from './fix-ci.js';
8
9
  // Shared utilities
9
10
  export { getGitHubConfig, resolveRepoRoot, createDefaultAuditLog, resolveAutonomyLevel, resolveConstraints, mergeBlockedPaths, isAutonomousStrategy, recordMetric, validateAndAuditOutput, createPipelineMemory, evaluatePipelineCompliance, authorizeFilesChanged, extractIssueNumber, BRANCH_PATTERN, createAbacPermissionHook, createBlockedPathsHook, createAuditLoggingHook, createPipelineAuthorizationChain, } from './shared.js';
@@ -32,7 +32,7 @@ export function buildPrompt(ctx) {
32
32
  else if (fmtCmd) {
33
33
  lines.push(`${++step}. After making ANY code changes, always run \`${fmtCmd}\` to catch issues before committing.`);
34
34
  }
35
- lines.push(`${++step}. Write or update tests if needed to cover your fix.`, `${++step}. Do NOT modify files matching the blocked paths below.`, `${++step}. Keep your changes to at most ${ctx.constraints.maxFilesPerChange} files.`);
35
+ lines.push(`${++step}. Write or update tests if needed to cover your fix.`, `${++step}. NEVER modify files matching the blocked paths below — violations will be automatically detected and the change will be rejected.`, `${++step}. Keep your changes to at most ${ctx.constraints.maxFilesPerChange} files.`);
36
36
  }
37
37
  else {
38
38
  let step = 0;
@@ -46,9 +46,9 @@ export function buildPrompt(ctx) {
46
46
  else if (fmtCmd) {
47
47
  lines.push(`${++step}. After making code changes, run \`${fmtCmd}\` to ensure CI will pass.`);
48
48
  }
49
- lines.push(`${++step}. Do NOT modify files matching the blocked paths below.`, `${++step}. Keep your changes to at most ${ctx.constraints.maxFilesPerChange} files.`);
49
+ lines.push(`${++step}. NEVER modify files matching the blocked paths below — violations will be automatically detected and the change will be rejected.`, `${++step}. Keep your changes to at most ${ctx.constraints.maxFilesPerChange} files.`);
50
50
  }
51
- lines.push('', '## Constraints', `- Maximum files to change: ${ctx.constraints.maxFilesPerChange}`, `- Tests required: ${ctx.constraints.requireTests}`, `- Blocked paths (do NOT modify): ${ctx.constraints.blockedPaths.join(', ') || 'none'}`);
51
+ lines.push('', '## Constraints (enforced — violations will be automatically rejected)', `- Maximum files to change: ${ctx.constraints.maxFilesPerChange}`, `- Tests required: ${ctx.constraints.requireTests}`, `- Blocked paths (NEVER modify — changes will be rejected): ${ctx.constraints.blockedPaths.join(', ') || 'none'}`);
52
52
  // Append relevant episodic memory if available
53
53
  if (ctx.memory) {
54
54
  const episodes = ctx.memory.episodic.search(`issue-${ctx.issueNumber}`);
@@ -18,6 +18,7 @@ export interface ValidationContext {
18
18
  export interface ValidationViolation {
19
19
  rule: string;
20
20
  message: string;
21
+ severity: 'error' | 'warning';
21
22
  }
22
23
  export interface ValidationResult {
23
24
  passed: boolean;
@@ -36,7 +36,8 @@ export async function validateAgentOutput(ctx) {
36
36
  if (matchesBlockedPath(file, pattern)) {
37
37
  violations.push({
38
38
  rule: 'blocked-path',
39
- message: `File \`${file}\` matches blocked path \`${pattern}\``,
39
+ message: `File \`${file}\` matches blocked path \`${pattern}\` — this change will be rejected. Remove modifications to this file.`,
40
+ severity: 'error',
40
41
  });
41
42
  }
42
43
  }
@@ -45,7 +46,8 @@ export async function validateAgentOutput(ctx) {
45
46
  if (ctx.filesChanged.length > ctx.constraints.maxFilesPerChange) {
46
47
  violations.push({
47
48
  rule: 'max-files',
48
- message: `Changed ${ctx.filesChanged.length} files (max ${ctx.constraints.maxFilesPerChange})`,
49
+ message: `Changed ${ctx.filesChanged.length} files (max ${ctx.constraints.maxFilesPerChange}). Split this change into smaller PRs.`,
50
+ severity: 'warning',
49
51
  });
50
52
  }
51
53
  // 3. Max lines per PR
@@ -62,7 +64,8 @@ export async function validateAgentOutput(ctx) {
62
64
  if (totalLines > ctx.guardrails.maxLinesPerPR) {
63
65
  violations.push({
64
66
  rule: 'max-lines',
65
- message: `Changed ${totalLines} lines (max ${ctx.guardrails.maxLinesPerPR})`,
67
+ message: `Changed ${totalLines} lines (max ${ctx.guardrails.maxLinesPerPR}). Consider breaking this into smaller incremental changes.`,
68
+ severity: 'warning',
66
69
  });
67
70
  }
68
71
  }
@@ -72,7 +75,8 @@ export async function validateAgentOutput(ctx) {
72
75
  if (!hasTestFile) {
73
76
  violations.push({
74
77
  rule: 'require-tests',
75
- message: 'No test file found in changed files',
78
+ message: 'No test file found in changed files. Add or update tests to cover your changes.',
79
+ severity: 'error',
76
80
  });
77
81
  }
78
82
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Per-file config validation — validates YAML files individually and
3
+ * returns structured results instead of throwing on first error.
4
+ */
5
+ export interface FileValidationResult {
6
+ file: string;
7
+ kind: string | null;
8
+ valid: boolean;
9
+ errors: Array<{
10
+ path: string;
11
+ message: string;
12
+ }>;
13
+ }
14
+ /**
15
+ * Validate config files in the given directory.
16
+ * Returns one result per file instead of failing on the first error.
17
+ */
18
+ export declare function validateConfigFiles(configDir: string, fileFilter?: string): FileValidationResult[];
19
+ //# sourceMappingURL=validate-config.d.ts.map
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Per-file config validation — validates YAML files individually and
3
+ * returns structured results instead of throwing on first error.
4
+ */
5
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
6
+ import { resolve } from 'node:path';
7
+ import { parse as parseYaml } from 'yaml';
8
+ import { validateResource } from '@ai-sdlc/reference';
9
+ /**
10
+ * Validate config files in the given directory.
11
+ * Returns one result per file instead of failing on the first error.
12
+ */
13
+ export function validateConfigFiles(configDir, fileFilter) {
14
+ const dir = resolve(configDir);
15
+ const results = [];
16
+ if (!existsSync(dir)) {
17
+ results.push({
18
+ file: configDir,
19
+ kind: null,
20
+ valid: false,
21
+ errors: [{ path: '/', message: `Config directory not found: ${dir}` }],
22
+ });
23
+ return results;
24
+ }
25
+ let files = readdirSync(dir).filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'));
26
+ // Skip non-resource YAML files
27
+ files = files.filter((f) => f !== 'manifest.yaml');
28
+ if (fileFilter) {
29
+ files = files.filter((f) => f === fileFilter);
30
+ if (files.length === 0) {
31
+ results.push({
32
+ file: fileFilter,
33
+ kind: null,
34
+ valid: false,
35
+ errors: [{ path: '/', message: `File not found in config directory: ${fileFilter}` }],
36
+ });
37
+ return results;
38
+ }
39
+ }
40
+ for (const file of files) {
41
+ try {
42
+ const raw = readFileSync(resolve(dir, file), 'utf-8');
43
+ const doc = parseYaml(raw);
44
+ const result = validateResource(doc);
45
+ const kind = typeof doc === 'object' && doc !== null && 'kind' in doc
46
+ ? doc.kind
47
+ : null;
48
+ if (result.valid) {
49
+ results.push({ file, kind, valid: true, errors: [] });
50
+ }
51
+ else {
52
+ results.push({
53
+ file,
54
+ kind,
55
+ valid: false,
56
+ errors: (result.errors ?? []).map((e) => ({ path: e.path, message: e.message })),
57
+ });
58
+ }
59
+ }
60
+ catch (err) {
61
+ const message = err instanceof Error ? err.message : String(err);
62
+ results.push({
63
+ file,
64
+ kind: null,
65
+ valid: false,
66
+ errors: [{ path: '/', message }],
67
+ });
68
+ }
69
+ }
70
+ return results;
71
+ }
72
+ //# sourceMappingURL=validate-config.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdlc/orchestrator",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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,7 +45,7 @@
45
45
  "better-sqlite3": "^11.0.0",
46
46
  "commander": "^12.0.0",
47
47
  "yaml": "^2.7.0",
48
- "@ai-sdlc/reference": "0.1.1"
48
+ "@ai-sdlc/reference": "0.1.2"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/better-sqlite3": "^7.6.0",
@@ -1,9 +0,0 @@
1
- /**
2
- * Stub runner for GitHub Copilot — placeholder for future implementation.
3
- */
4
- import type { AgentRunner, AgentContext, AgentResult } from './types.js';
5
- export declare class CopilotStubRunner implements AgentRunner {
6
- readonly name = "copilot";
7
- run(_ctx: AgentContext): Promise<AgentResult>;
8
- }
9
- //# sourceMappingURL=copilot-stub.d.ts.map
@@ -1,15 +0,0 @@
1
- /**
2
- * Stub runner for GitHub Copilot — placeholder for future implementation.
3
- */
4
- export class CopilotStubRunner {
5
- name = 'copilot';
6
- async run(_ctx) {
7
- return {
8
- success: false,
9
- filesChanged: [],
10
- summary: 'GitHub Copilot runner not yet implemented',
11
- error: 'STUB: GitHub Copilot integration requires Copilot Workspace API access',
12
- };
13
- }
14
- }
15
- //# sourceMappingURL=copilot-stub.js.map
@@ -1,9 +0,0 @@
1
- /**
2
- * Stub runner for Cursor — placeholder for future implementation.
3
- */
4
- import type { AgentRunner, AgentContext, AgentResult } from './types.js';
5
- export declare class CursorStubRunner implements AgentRunner {
6
- readonly name = "cursor";
7
- run(_ctx: AgentContext): Promise<AgentResult>;
8
- }
9
- //# sourceMappingURL=cursor-stub.d.ts.map
@@ -1,15 +0,0 @@
1
- /**
2
- * Stub runner for Cursor — placeholder for future implementation.
3
- */
4
- export class CursorStubRunner {
5
- name = 'cursor';
6
- async run(_ctx) {
7
- return {
8
- success: false,
9
- filesChanged: [],
10
- summary: 'Cursor runner not yet implemented',
11
- error: 'STUB: Cursor integration requires Cursor Agent API access',
12
- };
13
- }
14
- }
15
- //# sourceMappingURL=cursor-stub.js.map
@@ -1,9 +0,0 @@
1
- /**
2
- * Stub runner for Devin — placeholder for future implementation.
3
- */
4
- import type { AgentRunner, AgentContext, AgentResult } from './types.js';
5
- export declare class DevinStubRunner implements AgentRunner {
6
- readonly name = "devin";
7
- run(_ctx: AgentContext): Promise<AgentResult>;
8
- }
9
- //# sourceMappingURL=devin-stub.d.ts.map
@@ -1,15 +0,0 @@
1
- /**
2
- * Stub runner for Devin — placeholder for future implementation.
3
- */
4
- export class DevinStubRunner {
5
- name = 'devin';
6
- async run(_ctx) {
7
- return {
8
- success: false,
9
- filesChanged: [],
10
- summary: 'Devin runner not yet implemented',
11
- error: 'STUB: Devin integration requires Devin API access',
12
- };
13
- }
14
- }
15
- //# sourceMappingURL=devin-stub.js.map