Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a58aa1657 |
Generated
+5
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@continuous-intelligence/ci",
|
||||
"version": "0.4.0",
|
||||
"name": "@continuous-intelligence/ciagent",
|
||||
"version": "0.5.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@continuous-intelligence/ci",
|
||||
"version": "0.4.0",
|
||||
"name": "@continuous-intelligence/ciagent",
|
||||
"version": "0.5.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -14,7 +14,7 @@
|
||||
"zod": "^3.23.0"
|
||||
},
|
||||
"bin": {
|
||||
"ci": "dist/cli/index.js"
|
||||
"ciagent": "dist/cli/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.0",
|
||||
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@continuous-intelligence/ci",
|
||||
"name": "@continuous-intelligence/ciagent",
|
||||
"version": "0.5.0",
|
||||
"description": "Fully autonomous AI-driven software engineering harness - Continuous Intelligence",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"ci": "./dist/cli/index.js"
|
||||
"ciagent": "./dist/cli/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
@@ -22,7 +22,7 @@
|
||||
"prepublishOnly": "npm run build",
|
||||
"install-opencode": "node scripts/postinstall.js"
|
||||
},
|
||||
"keywords": ["ci", "autonomous", "ai", "software-engineering", "agent", "multi-project"],
|
||||
"keywords": ["ciagent", "autonomous", "ai", "software-engineering", "agent", "multi-project"],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@@ -17,6 +17,7 @@ export { ProjectResearcherAgent } from "./project-researcher.js";
|
||||
export { ResearchSynthesizerAgent } from "./research-synthesizer.js";
|
||||
export { SolutionWriterAgent } from "./solution-writer.js";
|
||||
export { PhaseResearcherAgent } from "./phase-researcher.js";
|
||||
export { TesterAgent } from "./tester.js";
|
||||
|
||||
import { AgentName } from "../types/config.js";
|
||||
import { BaseAgent as BaseAgentType } from "./base.js";
|
||||
@@ -38,6 +39,7 @@ import { ProjectResearcherAgent } from "./project-researcher.js";
|
||||
import { ResearchSynthesizerAgent } from "./research-synthesizer.js";
|
||||
import { SolutionWriterAgent } from "./solution-writer.js";
|
||||
import { PhaseResearcherAgent } from "./phase-researcher.js";
|
||||
import { TesterAgent } from "./tester.js";
|
||||
|
||||
const agentRegistry: Record<AgentName, () => BaseAgentType> = {
|
||||
orchestrator: () => new OrchestratorAgent(),
|
||||
@@ -58,6 +60,7 @@ const agentRegistry: Record<AgentName, () => BaseAgentType> = {
|
||||
"project-researcher": () => new ProjectResearcherAgent(),
|
||||
"research-synthesizer": () => new ResearchSynthesizerAgent(),
|
||||
"solution-writer": () => new SolutionWriterAgent(),
|
||||
tester: () => new TesterAgent(),
|
||||
};
|
||||
|
||||
export function getAgent(name: AgentName): BaseAgentType {
|
||||
|
||||
+15
-14
@@ -4,9 +4,9 @@ import { ClarifyPhase } from "../core/clarify.js";
|
||||
import { EscalationProtocol, EscalationInput } from "../core/escalation.js";
|
||||
import { GitContext, ProjectState } from "../core/git-context.js";
|
||||
import { GitBranch } from "../core/git-branch.js";
|
||||
import { CiFiles } from "../core/ci-files.js";
|
||||
import { CIAgentFiles } from "../core/ciagent-files.js";
|
||||
import { CommitBuilder } from "../core/commit-builder.js";
|
||||
import { CIConfig, AgentName } from "../types/config.js";
|
||||
import { CIAgentConfig, AgentName } from "../types/config.js";
|
||||
import {
|
||||
PipelineState,
|
||||
PipelineStage,
|
||||
@@ -16,29 +16,29 @@ import {
|
||||
STAGE_ORDER,
|
||||
} from "../types/pipeline.js";
|
||||
import { Specification, parseSpecification } from "../types/specification.js";
|
||||
import { loadConfig, saveConfig, isCIInitialized, initCI } from "../core/config.js";
|
||||
import { loadConfig, saveConfig, isCIAgentInitialized, initCIAgent } from "../core/config.js";
|
||||
import { getAgent } from "./index.js";
|
||||
import { IntelligenceBackend, BackendUnavailableError } from "../backends/types.js";
|
||||
|
||||
export interface GitAgentContext extends AgentContext {
|
||||
gitContext: GitContext;
|
||||
gitBranch: GitBranch;
|
||||
ciFiles: CiFiles;
|
||||
ciFiles: CIAgentFiles;
|
||||
milestone: string;
|
||||
}
|
||||
|
||||
export class OrchestratorAgent extends BaseAgent {
|
||||
readonly name: AgentName = "orchestrator";
|
||||
readonly description = "Top-level autonomous controller that coordinates the full CI pipeline";
|
||||
readonly description = "Top-level autonomous controller that coordinates the full CIAgent pipeline";
|
||||
readonly workflow = "run";
|
||||
|
||||
private config: CIConfig;
|
||||
private config: CIAgentConfig;
|
||||
private pipelineState: PipelineState | null = null;
|
||||
private decisionEngine: DecisionEngine | null = null;
|
||||
private escalationProtocol: EscalationProtocol | null = null;
|
||||
private gitContext: GitContext | null = null;
|
||||
private gitBranch: GitBranch | null = null;
|
||||
private ciFiles: CiFiles | null = null;
|
||||
private ciFiles: CIAgentFiles | null = null;
|
||||
private currentMilestone: string;
|
||||
private phaseResults: PhaseResult[] = [];
|
||||
|
||||
@@ -46,10 +46,11 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
research: "researcher",
|
||||
plan: "planner",
|
||||
execute: "executor",
|
||||
test: "tester",
|
||||
verify: "verifier",
|
||||
};
|
||||
|
||||
constructor(config?: CIConfig) {
|
||||
constructor(config?: CIAgentConfig) {
|
||||
super();
|
||||
this.config = config || loadConfig(process.cwd());
|
||||
this.currentMilestone = "v1.0";
|
||||
@@ -57,14 +58,14 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
const startTime = Date.now();
|
||||
this.log("Starting CI Orchestrator pipeline (git-native)");
|
||||
this.log("Starting CIAgent Orchestrator pipeline (git-native)");
|
||||
|
||||
try {
|
||||
this.config = loadConfig(context.project_path);
|
||||
|
||||
this.gitContext = new GitContext(context.project_path);
|
||||
this.gitBranch = new GitBranch(context.project_path);
|
||||
this.ciFiles = new CiFiles(context.project_path);
|
||||
this.ciFiles = new CIAgentFiles(context.project_path);
|
||||
this.ciFiles.ensureCIDir();
|
||||
|
||||
const projectState = this.gitContext.reconstructState();
|
||||
@@ -207,7 +208,7 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
});
|
||||
|
||||
this.log("Init commit prepared with specification in ---ci--- block");
|
||||
artifactsCreated.push(".ci/config.json");
|
||||
artifactsCreated.push(".ciagent/config.json");
|
||||
|
||||
if (this.config.git.auto_commit && this.gitContext!.isGitRepo()) {
|
||||
try {
|
||||
@@ -296,7 +297,7 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
1,
|
||||
this.currentMilestone,
|
||||
"initial domain research",
|
||||
["Research completed. Key findings in .ci/ARCHITECTURE.md and .ci/PROJECT.md updates."]
|
||||
["Research completed. Key findings in .ciagent/ARCHITECTURE.md and .ciagent/PROJECT.md updates."]
|
||||
);
|
||||
try {
|
||||
const { execSync } = await import("node:child_process");
|
||||
@@ -310,7 +311,7 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
}
|
||||
|
||||
this.pipelineState!.research_completed = true;
|
||||
artifactsCreated.push(".ci/ARCHITECTURE.md");
|
||||
artifactsCreated.push(".ciagent/ARCHITECTURE.md");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -425,7 +426,7 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
|
||||
private generateCompletionReport(): string {
|
||||
const lines: string[] = [
|
||||
"# CI Completion Report",
|
||||
"# CIAgent Completion Report",
|
||||
"",
|
||||
`✓ Pipeline completed successfully (git-native)`,
|
||||
"",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
|
||||
export class TesterAgent extends BaseAgent {
|
||||
readonly name = "tester";
|
||||
readonly description = "Runs automated tests and validates test coverage.";
|
||||
readonly workflow = "test";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
const start = Date.now();
|
||||
this.log("Running automated tests...");
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
`Run automated tests for: ${context.specification}`
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: "Testing requires an intelligence backend.",
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ describe("OllamaBaseBackend", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-ollama-base-test-"));
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-ollama-base-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -170,7 +170,7 @@ export abstract class OllamaBaseBackend implements IntelligenceBackend {
|
||||
return fs.readFileSync(candidate, "utf-8");
|
||||
}
|
||||
}
|
||||
return `You are the CI ${persona} agent. Execute the requested task thoroughly and autonomously.`;
|
||||
return `You are the CIAgent ${persona} agent. Execute the requested task thoroughly and autonomously.`;
|
||||
}
|
||||
|
||||
protected loadWorkflow(workflow: string): string {
|
||||
|
||||
@@ -8,7 +8,7 @@ describe("ToolRegistry Extended", () => {
|
||||
let registry: ToolRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-tool-registry-ext-"));
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-tool-registry-ext-"));
|
||||
registry = new ToolRegistry(tempDir);
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ describe("ToolRegistry", () => {
|
||||
let registry: ToolRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-tool-registry-test-"));
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-tool-registry-test-"));
|
||||
registry = new ToolRegistry(tempDir);
|
||||
});
|
||||
|
||||
|
||||
+49
-49
@@ -1,6 +1,6 @@
|
||||
import { Command } from "commander";
|
||||
import { CIConfig, AutonomyLevel } from "../types/config.js";
|
||||
import { initCI, loadConfig, isCIInitialized, saveConfig } from "../core/config.js";
|
||||
import { CIAgentConfig, AutonomyLevel } from "../types/config.js";
|
||||
import { initCIAgent, loadConfig, isCIAgentInitialized, saveConfig } from "../core/config.js";
|
||||
import { Specification, parseSpecification } from "../types/specification.js";
|
||||
import { saveSpecification } from "../core/clarify.js";
|
||||
import { OrchestratorAgent } from "../agents/orchestrator.js";
|
||||
@@ -21,7 +21,7 @@ import { execSync } from "node:child_process";
|
||||
|
||||
export function createInitCommand(): Command {
|
||||
return new Command("init")
|
||||
.description("Initialize a new CI project from a specification")
|
||||
.description("Initialize a new CIAgent project from a specification")
|
||||
.argument("[specification]", "Inline specification text")
|
||||
.option("-s, --spec <file>", "Specification file path")
|
||||
.option("-c, --clarify", "Start interactive clarify phase", false)
|
||||
@@ -36,9 +36,9 @@ export function createInitCommand(): Command {
|
||||
.action(async (specification, options) => {
|
||||
const projectPath = process.cwd();
|
||||
|
||||
if (isCIInitialized(projectPath)) {
|
||||
console.log("CI project already initialized in this directory.");
|
||||
console.log("Use 'ci run' to execute the pipeline or 'ci status' to check progress.");
|
||||
if (isCIAgentInitialized(projectPath)) {
|
||||
console.log("CIAgent project already initialized in this directory.");
|
||||
console.log("Use 'ciagent run' to execute the pipeline or 'ciagent status' to check progress.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export function createInitCommand(): Command {
|
||||
}
|
||||
|
||||
const autonomyLevel = options.autonomy as AutonomyLevel;
|
||||
const config: Partial<CIConfig> = {
|
||||
const config: Partial<CIAgentConfig> = {
|
||||
autonomy: {
|
||||
level: autonomyLevel,
|
||||
escalation_hooks: ["deploy", "delete_data", "merge_to_main"],
|
||||
@@ -86,8 +86,8 @@ export function createInitCommand(): Command {
|
||||
},
|
||||
};
|
||||
|
||||
const fullConfig = initCI(projectPath, config);
|
||||
console.log(`✓ CI project initialized (autonomy: ${autonomyLevel})`);
|
||||
const fullConfig = initCIAgent(projectPath, config);
|
||||
console.log(`✓ CIAgent project initialized (autonomy: ${autonomyLevel})`);
|
||||
console.log(` Backend: ${options.backend || "auto"}`);
|
||||
|
||||
if (specText) {
|
||||
@@ -115,15 +115,15 @@ export function createInitCommand(): Command {
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\nConfiguration saved to .ci/config.json");
|
||||
console.log("\nConfiguration saved to .ciagent/config.json");
|
||||
console.log("\nNext steps:");
|
||||
console.log(" ci run --all # Run full pipeline");
|
||||
console.log(" ci run research # Run specific phase");
|
||||
console.log(" ciagent run --all # Run full pipeline");
|
||||
console.log(" ciagent run research # Run specific phase");
|
||||
console.log(" ci status # Check project status");
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveBackendForCommand(config: CIConfig, overrideBackend?: string): Promise<{ backend: import("../backends/types.js").IntelligenceBackend | undefined; error?: string }> {
|
||||
async function resolveBackendForCommand(config: CIAgentConfig, overrideBackend?: string): Promise<{ backend: import("../backends/types.js").IntelligenceBackend | undefined; error?: string }> {
|
||||
const backendConfig = { ...config.backend };
|
||||
if (overrideBackend) {
|
||||
backendConfig.provider = overrideBackend as typeof backendConfig.provider;
|
||||
@@ -168,8 +168,8 @@ export function createRunCommand(): Command {
|
||||
.action(async (phase, options) => {
|
||||
const projectPath = process.cwd();
|
||||
|
||||
if (!isCIInitialized(projectPath)) {
|
||||
console.error("CI project not initialized. Run 'ci init' first.");
|
||||
if (!isCIAgentInitialized(projectPath)) {
|
||||
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ export function createRunCommand(): Command {
|
||||
phase: parseInt(options.phase) || 1,
|
||||
stage: phase || "all",
|
||||
specification: "",
|
||||
config_path: path.join(projectPath, ".ci", "config.json"),
|
||||
config_path: path.join(projectPath, ".ciagent", "config.json"),
|
||||
backend,
|
||||
};
|
||||
|
||||
@@ -196,7 +196,7 @@ export function createRunCommand(): Command {
|
||||
context.specification = spec.raw_content;
|
||||
}
|
||||
|
||||
console.log(`Running CI pipeline...`);
|
||||
console.log(`Running CIAgent pipeline...`);
|
||||
if (options.all) {
|
||||
console.log(" Mode: Full pipeline (all phases)");
|
||||
} else {
|
||||
@@ -226,16 +226,16 @@ export function createQuickCommand(): Command {
|
||||
const projectPath = process.cwd();
|
||||
console.log(`Quick task: ${description}`);
|
||||
|
||||
if (!isCIInitialized(projectPath)) {
|
||||
const config = initCI(projectPath);
|
||||
console.log("Initialized temporary CI project");
|
||||
if (!isCIAgentInitialized(projectPath)) {
|
||||
const config = initCIAgent(projectPath);
|
||||
console.log("Initialized temporary CIAgent project");
|
||||
}
|
||||
|
||||
const config = loadConfig(projectPath);
|
||||
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
|
||||
|
||||
if (!backend) {
|
||||
console.error(`\n✗ "ci quick" requires an intelligence backend.`);
|
||||
console.error(`\n✗ "ciagent quick" requires an intelligence backend.`);
|
||||
if (backendError) console.error(` ${backendError}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -249,7 +249,7 @@ export function createQuickCommand(): Command {
|
||||
phase: 0,
|
||||
stage: "all",
|
||||
specification: description,
|
||||
config_path: path.join(projectPath, ".ci", "config.json"),
|
||||
config_path: path.join(projectPath, ".ciagent", "config.json"),
|
||||
backend,
|
||||
};
|
||||
|
||||
@@ -274,8 +274,8 @@ export function createDebugCommand(): Command {
|
||||
.action(async (description, options) => {
|
||||
const projectPath = process.cwd();
|
||||
|
||||
if (!isCIInitialized(projectPath)) {
|
||||
console.error("CI project not initialized. Run 'ci init' first.");
|
||||
if (!isCIAgentInitialized(projectPath)) {
|
||||
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -283,7 +283,7 @@ export function createDebugCommand(): Command {
|
||||
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
|
||||
|
||||
if (!backend) {
|
||||
console.error(`\n✗ "ci debug" requires an intelligence backend.`);
|
||||
console.error(`\n✗ "ciagent debug" requires an intelligence backend.`);
|
||||
if (backendError) console.error(` ${backendError}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -300,7 +300,7 @@ export function createDebugCommand(): Command {
|
||||
phase: 0,
|
||||
stage: "debug",
|
||||
specification: description || "",
|
||||
config_path: path.join(projectPath, ".ci", "config.json"),
|
||||
config_path: path.join(projectPath, ".ciagent", "config.json"),
|
||||
backend,
|
||||
};
|
||||
|
||||
@@ -324,8 +324,8 @@ export function createVerifyCommand(): Command {
|
||||
.action(async (phase, options) => {
|
||||
const projectPath = process.cwd();
|
||||
|
||||
if (!isCIInitialized(projectPath)) {
|
||||
console.error("CI project not initialized. Run 'ci init' first.");
|
||||
if (!isCIAgentInitialized(projectPath)) {
|
||||
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -371,8 +371,8 @@ export function createReviewCommand(): Command {
|
||||
.action(async (phase, options) => {
|
||||
const projectPath = process.cwd();
|
||||
|
||||
if (!isCIInitialized(projectPath)) {
|
||||
console.error("CI project not initialized. Run 'ci init' first.");
|
||||
if (!isCIAgentInitialized(projectPath)) {
|
||||
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -380,7 +380,7 @@ export function createReviewCommand(): Command {
|
||||
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
|
||||
|
||||
if (!backend) {
|
||||
console.error(`\n✗ "ci review" requires an intelligence backend.`);
|
||||
console.error(`\n✗ "ciagent review" requires an intelligence backend.`);
|
||||
if (backendError) console.error(` ${backendError}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -394,7 +394,7 @@ export function createReviewCommand(): Command {
|
||||
phase: phaseNum,
|
||||
stage: "review",
|
||||
specification: "",
|
||||
config_path: path.join(projectPath, ".ci", "config.json"),
|
||||
config_path: path.join(projectPath, ".ciagent", "config.json"),
|
||||
backend,
|
||||
};
|
||||
|
||||
@@ -415,16 +415,16 @@ export function createStatusCommand(): Command {
|
||||
.action(() => {
|
||||
const projectPath = process.cwd();
|
||||
|
||||
if (!isCIInitialized(projectPath)) {
|
||||
console.log("CI project not initialized in this directory.");
|
||||
console.log("Run 'ci init' to get started.");
|
||||
if (!isCIAgentInitialized(projectPath)) {
|
||||
console.log("CIAgent project not initialized in this directory.");
|
||||
console.log("Run 'ciagent init' to get started.");
|
||||
return;
|
||||
}
|
||||
|
||||
const config = loadConfig(projectPath);
|
||||
const artifacts = new ArtifactManager(projectPath);
|
||||
|
||||
console.log("─── CI Project Status ───");
|
||||
console.log("─── CIAgent Project Status ───");
|
||||
console.log(`\nAutonomy: ${config.autonomy.level}`);
|
||||
console.log(`Model Profile: ${config.model_profile}`);
|
||||
console.log(`Backend: ${config.backend?.provider || "auto"}`);
|
||||
@@ -444,7 +444,7 @@ export function createStatusCommand(): Command {
|
||||
console.log(` ${icon} ${stage}`);
|
||||
}
|
||||
} else {
|
||||
console.log("\nNo pipeline state found. Run 'ci run --all' to start.");
|
||||
console.log("\nNo pipeline state found. Run 'ciagent run --all' to start.");
|
||||
}
|
||||
|
||||
const summary = getAuditSummary(projectPath);
|
||||
@@ -464,15 +464,15 @@ export function createAuditCommand(): Command {
|
||||
.action((options) => {
|
||||
const projectPath = process.cwd();
|
||||
|
||||
if (!isCIInitialized(projectPath)) {
|
||||
console.error("CI project not initialized. Run 'ci init' first.");
|
||||
if (!isCIAgentInitialized(projectPath)) {
|
||||
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const phase = options.phase ? parseInt(options.phase) : undefined;
|
||||
const summary = getAuditSummary(projectPath);
|
||||
|
||||
console.log("─── CI Audit Report ───");
|
||||
console.log("─── CIAgent Audit Report ───");
|
||||
console.log(`\nTotal Decisions: ${summary.total_decisions}`);
|
||||
console.log(`Total Escalations: ${summary.total_escalations}`);
|
||||
console.log(`Phases Audited: ${summary.phases.join(", ") || "none"}`);
|
||||
@@ -517,8 +517,8 @@ export function createClarifyCommand(): Command {
|
||||
.action(async (options) => {
|
||||
const projectPath = process.cwd();
|
||||
|
||||
if (!isCIInitialized(projectPath)) {
|
||||
console.error("CI project not initialized. Run 'ci init' first.");
|
||||
if (!isCIAgentInitialized(projectPath)) {
|
||||
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -526,7 +526,7 @@ export function createClarifyCommand(): Command {
|
||||
const spec = loadSpec(projectPath);
|
||||
|
||||
if (!spec) {
|
||||
console.error("No specification found. Run 'ci init' first.");
|
||||
console.error("No specification found. Run 'ciagent init' first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -557,8 +557,8 @@ export function createRollbackCommand(): Command {
|
||||
.action(async (target, options) => {
|
||||
const projectPath = process.cwd();
|
||||
|
||||
if (!isCIInitialized(projectPath)) {
|
||||
console.error("CI project not initialized. Run 'ci init' first.");
|
||||
if (!isCIAgentInitialized(projectPath)) {
|
||||
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -650,8 +650,8 @@ export function createShipCommand(): Command {
|
||||
.action(async (phase, options) => {
|
||||
const projectPath = process.cwd();
|
||||
|
||||
if (!isCIInitialized(projectPath)) {
|
||||
console.error("CI project not initialized. Run 'ci init' first.");
|
||||
if (!isCIAgentInitialized(projectPath)) {
|
||||
console.error("CIAgent project not initialized. Run 'ciagent init' first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -707,7 +707,7 @@ export function createShipCommand(): Command {
|
||||
cwd: projectPath,
|
||||
stdio: "pipe",
|
||||
});
|
||||
execSync(`git tag -a ${version.tag} -m "CI: Phase ${phaseNum} shipped"`, {
|
||||
execSync(`git tag -a ${version.tag} -m "CIAgent: Phase ${phaseNum} shipped"`, {
|
||||
cwd: projectPath,
|
||||
stdio: "pipe",
|
||||
});
|
||||
@@ -730,7 +730,7 @@ export function createShipCommand(): Command {
|
||||
function computeShipVersion(
|
||||
projectPath: string,
|
||||
phaseNum: number,
|
||||
config: CIConfig
|
||||
config: CIAgentConfig
|
||||
): { tag: string; milestoneType: "nfr" | "feature" | "schema-breaking" } {
|
||||
const tags = execSync("git tag -l", { cwd: projectPath, encoding: "utf-8" })
|
||||
.split("\n")
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@ import {
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name("ci")
|
||||
.description("CI — Continuous Intelligence: autonomous AI-driven software engineering harness")
|
||||
.name("ciagent")
|
||||
.description("CIAgent — Continuous Intelligence: autonomous AI-driven software engineering harness")
|
||||
.version(VERSION)
|
||||
.addCommand(createInitCommand())
|
||||
.addCommand(createRunCommand())
|
||||
|
||||
@@ -8,7 +8,7 @@ describe("ArtifactManager", () => {
|
||||
let manager: ArtifactManager;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-artifact-test-"));
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-artifact-test-"));
|
||||
manager = new ArtifactManager(tempDir);
|
||||
});
|
||||
|
||||
@@ -17,16 +17,16 @@ describe("ArtifactManager", () => {
|
||||
});
|
||||
|
||||
describe("ensureStructure", () => {
|
||||
it("creates .ci directory structure", () => {
|
||||
it("creates .ciagent directory structure", () => {
|
||||
manager.ensureStructure();
|
||||
expect(fs.existsSync(path.join(tempDir, ".ci"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(tempDir, ".ci", "audit"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(tempDir, ".ciagent"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(tempDir, ".ciagent", "audit"))).toBe(true);
|
||||
});
|
||||
|
||||
it("is idempotent", () => {
|
||||
manager.ensureStructure();
|
||||
manager.ensureStructure();
|
||||
expect(fs.existsSync(path.join(tempDir, ".ci"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(tempDir, ".ciagent"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("ArtifactManager", () => {
|
||||
|
||||
manager.writeProject(manifest);
|
||||
|
||||
const projectPath = path.join(tempDir, ".ci", "PROJECT.md");
|
||||
const projectPath = path.join(tempDir, ".ciagent", "PROJECT.md");
|
||||
expect(fs.existsSync(projectPath)).toBe(true);
|
||||
const content = fs.readFileSync(projectPath, "utf-8");
|
||||
expect(content).toContain("Test Project");
|
||||
@@ -131,7 +131,7 @@ describe("ArtifactManager", () => {
|
||||
],
|
||||
});
|
||||
|
||||
const decisionsPath = path.join(tempDir, ".ci", "DECISIONS.md");
|
||||
const decisionsPath = path.join(tempDir, ".ciagent", "DECISIONS.md");
|
||||
expect(fs.existsSync(decisionsPath)).toBe(true);
|
||||
const content = fs.readFileSync(decisionsPath, "utf-8");
|
||||
expect(content).toContain("D-001");
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { writeFile, readFile, ensureDir } from "../utils/file.js";
|
||||
|
||||
const CI_DIR = ".ci";
|
||||
const CI_DIR = ".ciagent";
|
||||
|
||||
export interface ProjectManifest {
|
||||
name: string;
|
||||
|
||||
@@ -9,8 +9,8 @@ describe("Audit", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-audit-test-"));
|
||||
fs.mkdirSync(path.join(tempDir, ".ci", "audit"), { recursive: true });
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-audit-test-"));
|
||||
fs.mkdirSync(path.join(tempDir, ".ciagent", "audit"), { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -40,7 +40,7 @@ describe("Audit", () => {
|
||||
],
|
||||
default_option_id: "A",
|
||||
resolution: "pending",
|
||||
audit_file: ".ci/audit/test.json",
|
||||
audit_file: ".ciagent/audit/test.json",
|
||||
};
|
||||
|
||||
describe("logDecision", () => {
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ export interface AuditEntry {
|
||||
const AUDIT_DIR = "audit";
|
||||
|
||||
function getAuditDir(projectPath: string): string {
|
||||
return path.join(projectPath, ".ci", AUDIT_DIR);
|
||||
return path.join(projectPath, ".ciagent", AUDIT_DIR);
|
||||
}
|
||||
|
||||
function getAuditFilePath(projectPath: string, phase: number): string {
|
||||
|
||||
+85
-85
@@ -1,17 +1,17 @@
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import * as fs from "node:fs";
|
||||
import { CiFiles, ProjectMd, RoadmapMd, RequirementsMd, ArchitectureMd } from "../core/ci-files.js";
|
||||
import { CIAgentFiles, ProjectMd, RoadmapMd, RequirementsMd, ArchitectureMd } from "../core/ciagent-files.js";
|
||||
|
||||
function createTempDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "ci-files-test-"));
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-files-test-"));
|
||||
}
|
||||
|
||||
function cleanup(dir: string): void {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
describe("CiFiles", () => {
|
||||
describe("CIAgentFiles", () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -22,41 +22,41 @@ describe("CiFiles", () => {
|
||||
cleanup(dir);
|
||||
});
|
||||
|
||||
describe("ensureCIDir", () => {
|
||||
it("creates .ci directory", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
describe("ensureCIAgentDir", () => {
|
||||
it("creates .ciagent directory", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
expect(fs.existsSync(path.join(dir, ".ci"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(dir, ".ciagent"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isInitialized", () => {
|
||||
it("returns false when no config.json exists", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
expect(ciFiles.isInitialized()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when config.json exists", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), "{}");
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), "{}");
|
||||
expect(ciFiles.isInitialized()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("projectSlug", () => {
|
||||
it("defaults to empty string", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
expect(ciFiles.getProjectSlug()).toBe("");
|
||||
});
|
||||
|
||||
it("uses provided project slug", () => {
|
||||
const ciFiles = new CiFiles(dir, "task-api");
|
||||
const ciFiles = new CIAgentFiles(dir, "task-api");
|
||||
expect(ciFiles.getProjectSlug()).toBe("task-api");
|
||||
});
|
||||
|
||||
it("setProjectSlug updates slug", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.setProjectSlug("auth-svc");
|
||||
expect(ciFiles.getProjectSlug()).toBe("auth-svc");
|
||||
});
|
||||
@@ -64,14 +64,14 @@ describe("CiFiles", () => {
|
||||
|
||||
describe("multi-project support", () => {
|
||||
it("isMultiProject returns false when not initialized", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
expect(ciFiles.isMultiProject()).toBe(false);
|
||||
});
|
||||
|
||||
it("isMultiProject returns false for single-project config", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "default", name: "Default" }],
|
||||
active_project: "default",
|
||||
}));
|
||||
@@ -79,59 +79,59 @@ describe("CiFiles", () => {
|
||||
});
|
||||
|
||||
it("isMultiProject returns false for config without projects array", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({}));
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({}));
|
||||
expect(ciFiles.isMultiProject()).toBe(false);
|
||||
});
|
||||
|
||||
it("addProject adds a project to config", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [],
|
||||
active_project: "",
|
||||
}));
|
||||
|
||||
ciFiles.addProject("task-api", "Task API", true);
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ci", "config.json"), "utf-8"));
|
||||
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ciagent", "config.json"), "utf-8"));
|
||||
expect(config.projects).toHaveLength(1);
|
||||
expect(config.projects[0].slug).toBe("task-api");
|
||||
expect(config.active_project).toBe("task-api");
|
||||
});
|
||||
|
||||
it("addProject does not duplicate existing project", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "task-api", name: "Task API" }],
|
||||
active_project: "task-api",
|
||||
}));
|
||||
|
||||
ciFiles.addProject("task-api", "Task API V2");
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ci", "config.json"), "utf-8"));
|
||||
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ciagent", "config.json"), "utf-8"));
|
||||
expect(config.projects).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("addProject creates project subdirectory", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [],
|
||||
active_project: "",
|
||||
}));
|
||||
|
||||
ciFiles.addProject("task-api", "Task API", true);
|
||||
|
||||
expect(fs.existsSync(path.join(dir, ".ci", "task-api"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(dir, ".ciagent", "task-api"))).toBe(true);
|
||||
});
|
||||
|
||||
it("getActiveProject returns from config", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "task-api", name: "Task API", default: true }],
|
||||
active_project: "task-api",
|
||||
}));
|
||||
@@ -140,9 +140,9 @@ describe("CiFiles", () => {
|
||||
});
|
||||
|
||||
it("setActiveProject updates config", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [
|
||||
{ slug: "task-api", name: "Task API" },
|
||||
{ slug: "auth-svc", name: "Auth Service" },
|
||||
@@ -152,14 +152,14 @@ describe("CiFiles", () => {
|
||||
|
||||
ciFiles.setActiveProject("auth-svc");
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ci", "config.json"), "utf-8"));
|
||||
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ciagent", "config.json"), "utf-8"));
|
||||
expect(config.active_project).toBe("auth-svc");
|
||||
});
|
||||
|
||||
it("listProjects returns projects from config", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [
|
||||
{ slug: "task-api", name: "Task API", default: true },
|
||||
{ slug: "auth-svc", name: "Auth Service" },
|
||||
@@ -176,71 +176,71 @@ describe("CiFiles", () => {
|
||||
|
||||
describe("needsMigration", () => {
|
||||
it("returns false when not initialized", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
expect(ciFiles.needsMigration()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when already multi-project", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "default", name: "Default" }],
|
||||
}));
|
||||
fs.writeFileSync(path.join(dir, ".ci", "PROJECT.md"), "# Test");
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "PROJECT.md"), "# Test");
|
||||
expect(ciFiles.needsMigration()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when flat files exist without subdirs or multi-project config", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({}));
|
||||
fs.writeFileSync(path.join(dir, ".ci", "PROJECT.md"), "# Test");
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({}));
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "PROJECT.md"), "# Test");
|
||||
expect(ciFiles.needsMigration()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when flat files exist but subdirs also exist", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({}));
|
||||
fs.writeFileSync(path.join(dir, ".ci", "PROJECT.md"), "# Test");
|
||||
fs.mkdirSync(path.join(dir, ".ci", "task-api"));
|
||||
fs.writeFileSync(path.join(dir, ".ci", "task-api", "PROJECT.md"), "# Task API");
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({}));
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "PROJECT.md"), "# Test");
|
||||
fs.mkdirSync(path.join(dir, ".ciagent", "task-api"));
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "task-api", "PROJECT.md"), "# Task API");
|
||||
expect(ciFiles.needsMigration()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("migrateFlatToProject", () => {
|
||||
it("moves flat files to project subdirectory", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({}));
|
||||
fs.writeFileSync(path.join(dir, ".ci", "PROJECT.md"), "# Test Project");
|
||||
fs.writeFileSync(path.join(dir, ".ci", "ARCHITECTURE.md"), "# Architecture");
|
||||
fs.writeFileSync(path.join(dir, ".ci", "ROADMAP.md"), "# Roadmap");
|
||||
fs.writeFileSync(path.join(dir, ".ci", "REQUIREMENTS.md"), "# Requirements");
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({}));
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "PROJECT.md"), "# Test Project");
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "ARCHITECTURE.md"), "# Architecture");
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "ROADMAP.md"), "# Roadmap");
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "REQUIREMENTS.md"), "# Requirements");
|
||||
|
||||
ciFiles.migrateFlatToProject("my-app");
|
||||
|
||||
expect(fs.existsSync(path.join(dir, ".ci", "my-app", "PROJECT.md"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(dir, ".ci", "my-app", "ARCHITECTURE.md"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(dir, ".ci", "my-app", "ROADMAP.md"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(dir, ".ci", "my-app", "REQUIREMENTS.md"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(dir, ".ciagent", "my-app", "PROJECT.md"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(dir, ".ciagent", "my-app", "ARCHITECTURE.md"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(dir, ".ciagent", "my-app", "ROADMAP.md"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(dir, ".ciagent", "my-app", "REQUIREMENTS.md"))).toBe(true);
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ci", "config.json"), "utf-8"));
|
||||
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ciagent", "config.json"), "utf-8"));
|
||||
expect(config.projects).toHaveLength(1);
|
||||
expect(config.active_project).toBe("my-app");
|
||||
});
|
||||
|
||||
it("does not migrate when not needed", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "existing", name: "Existing" }],
|
||||
}));
|
||||
|
||||
ciFiles.migrateFlatToProject("new-proj");
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ci", "config.json"), "utf-8"));
|
||||
const config = JSON.parse(fs.readFileSync(path.join(dir, ".ciagent", "config.json"), "utf-8"));
|
||||
expect(config.projects).toHaveLength(1);
|
||||
expect(config.projects[0].slug).toBe("existing");
|
||||
});
|
||||
@@ -248,14 +248,14 @@ describe("CiFiles", () => {
|
||||
|
||||
describe("isNfrMilestone", () => {
|
||||
it("returns true when no roadmap exists", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
expect(ciFiles.isNfrMilestone()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when phases are all NFR types", () => {
|
||||
const ciFiles = new CiFiles(dir, "nfr-proj");
|
||||
const ciFiles = new CIAgentFiles(dir, "nfr-proj");
|
||||
ciFiles.ensureProjectDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "nfr-proj", name: "NFR Project", default: true }],
|
||||
active_project: "nfr-proj",
|
||||
}));
|
||||
@@ -271,9 +271,9 @@ describe("CiFiles", () => {
|
||||
});
|
||||
|
||||
it("returns false when phases include feature work", () => {
|
||||
const ciFiles = new CiFiles(dir, "feat-proj");
|
||||
const ciFiles = new CIAgentFiles(dir, "feat-proj");
|
||||
ciFiles.ensureProjectDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "feat-proj", name: "Feature Project", default: true }],
|
||||
active_project: "feat-proj",
|
||||
}));
|
||||
@@ -291,14 +291,14 @@ describe("CiFiles", () => {
|
||||
|
||||
describe("getMilestoneType", () => {
|
||||
it("returns nfr when no roadmap exists", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
expect(ciFiles.getMilestoneType()).toBe("nfr");
|
||||
});
|
||||
|
||||
it("returns nfr when phases are all NFR types", () => {
|
||||
const ciFiles = new CiFiles(dir, "nfr-proj2");
|
||||
const ciFiles = new CIAgentFiles(dir, "nfr-proj2");
|
||||
ciFiles.ensureProjectDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "nfr-proj2", name: "NFR Project 2", default: true }],
|
||||
active_project: "nfr-proj2",
|
||||
}));
|
||||
@@ -313,9 +313,9 @@ describe("CiFiles", () => {
|
||||
});
|
||||
|
||||
it("returns feature when phases include feat work", () => {
|
||||
const ciFiles = new CiFiles(dir, "feat-proj2");
|
||||
const ciFiles = new CIAgentFiles(dir, "feat-proj2");
|
||||
ciFiles.ensureProjectDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "feat-proj2", name: "Feature Project 2", default: true }],
|
||||
active_project: "feat-proj2",
|
||||
}));
|
||||
@@ -330,9 +330,9 @@ describe("CiFiles", () => {
|
||||
});
|
||||
|
||||
it("returns schema-breaking when phases include refactor/rewrite/migrate", () => {
|
||||
const ciFiles = new CiFiles(dir, "schema-proj");
|
||||
const ciFiles = new CIAgentFiles(dir, "schema-proj");
|
||||
ciFiles.ensureProjectDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "schema-proj", name: "Schema Project", default: true }],
|
||||
active_project: "schema-proj",
|
||||
}));
|
||||
@@ -349,9 +349,9 @@ describe("CiFiles", () => {
|
||||
|
||||
describe("multi-project file paths", () => {
|
||||
it("writes PROJECT.md to project subdirectory when slug is set", () => {
|
||||
const ciFiles = new CiFiles(dir, "my-app");
|
||||
const ciFiles = new CIAgentFiles(dir, "my-app");
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({
|
||||
projects: [{ slug: "my-app", name: "My App", default: true }],
|
||||
active_project: "my-app",
|
||||
}));
|
||||
@@ -367,13 +367,13 @@ describe("CiFiles", () => {
|
||||
|
||||
ciFiles.writeProjectMd(project, "initial");
|
||||
|
||||
expect(fs.existsSync(path.join(dir, ".ci", "my-app", "PROJECT.md"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(dir, ".ciagent", "my-app", "PROJECT.md"))).toBe(true);
|
||||
});
|
||||
|
||||
it("writes PROJECT.md to .ci root when no slug is set", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
fs.writeFileSync(path.join(dir, ".ci", "config.json"), JSON.stringify({}));
|
||||
fs.writeFileSync(path.join(dir, ".ciagent", "config.json"), JSON.stringify({}));
|
||||
|
||||
const project: ProjectMd = {
|
||||
name: "Default App",
|
||||
@@ -386,7 +386,7 @@ describe("CiFiles", () => {
|
||||
|
||||
ciFiles.writeProjectMd(project, "initial");
|
||||
|
||||
expect(fs.existsSync(path.join(dir, ".ci", "PROJECT.md"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(dir, ".ciagent", "PROJECT.md"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -407,7 +407,7 @@ describe("CiFiles", () => {
|
||||
};
|
||||
|
||||
it("writes and reads PROJECT.md", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.writeProjectMd(project, "initial creation");
|
||||
|
||||
const read = ciFiles.readProjectMd();
|
||||
@@ -418,7 +418,7 @@ describe("CiFiles", () => {
|
||||
});
|
||||
|
||||
it("overwrites PROJECT.md on update", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.writeProjectMd(project, "initial");
|
||||
|
||||
const updated = { ...project, coreValue: "Updated description" };
|
||||
@@ -455,7 +455,7 @@ describe("CiFiles", () => {
|
||||
};
|
||||
|
||||
it("writes and reads ROADMAP.md", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.writeRoadmapMd(roadmap);
|
||||
|
||||
const read = ciFiles.readRoadmapMd();
|
||||
@@ -489,7 +489,7 @@ describe("CiFiles", () => {
|
||||
};
|
||||
|
||||
it("writes and reads REQUIREMENTS.md", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.writeRequirementsMd(requirements);
|
||||
|
||||
const read = ciFiles.readRequirementsMd();
|
||||
@@ -497,7 +497,7 @@ describe("CiFiles", () => {
|
||||
});
|
||||
|
||||
it("updates requirement status", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.writeRequirementsMd(requirements);
|
||||
|
||||
ciFiles.updateRequirementStatus("AUTH-01", "complete");
|
||||
@@ -523,7 +523,7 @@ describe("CiFiles", () => {
|
||||
};
|
||||
|
||||
it("writes and reads ARCHITECTURE.md", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.writeArchitectureMd(arch);
|
||||
|
||||
const read = ciFiles.readArchitectureMd();
|
||||
@@ -534,7 +534,7 @@ describe("CiFiles", () => {
|
||||
|
||||
describe("updatePhaseStatus", () => {
|
||||
it("updates phase status in roadmap", () => {
|
||||
const ciFiles = new CiFiles(dir);
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
const roadmap: RoadmapMd = {
|
||||
overview: "test",
|
||||
phases: [
|
||||
|
||||
@@ -4,7 +4,7 @@ import { writeFile, readFile, ensureDir, fileExists } from "../utils/file.js";
|
||||
import { PipelineStage } from "../types/pipeline.js";
|
||||
import { MilestoneType } from "../types/config.js";
|
||||
|
||||
const CI_DIR = ".ci";
|
||||
const CI_DIR = ".ciagent";
|
||||
|
||||
export interface ProjectMd {
|
||||
name: string;
|
||||
@@ -71,7 +71,7 @@ export interface ProjectEntry {
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
export class CiFiles {
|
||||
export class CIAgentFiles {
|
||||
private projectPath: string;
|
||||
private projectSlug: string;
|
||||
|
||||
+18
-18
@@ -2,15 +2,15 @@ import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { ClarifyPhase, saveSpecification, loadSpecification } from "../core/clarify.js";
|
||||
import { DEFAULT_CI_CONFIG } from "../types/config.js";
|
||||
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
|
||||
import { Specification, parseSpecification } from "../types/specification.js";
|
||||
|
||||
describe("ClarifyPhase", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-clarify-test-"));
|
||||
fs.mkdirSync(path.join(tempDir, ".ci"), { recursive: true });
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-clarify-test-"));
|
||||
fs.mkdirSync(path.join(tempDir, ".ciagent"), { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -41,7 +41,7 @@ describe("ClarifyPhase", () => {
|
||||
|
||||
describe("generateQuestions", () => {
|
||||
it("generates questions for missing requirements", () => {
|
||||
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
|
||||
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
|
||||
const questions = clarify.generateQuestions(specWithoutRequirements);
|
||||
expect(questions.length).toBeGreaterThan(0);
|
||||
const reqQuestion = questions.find((q) => q.category === "requirements");
|
||||
@@ -50,7 +50,7 @@ describe("ClarifyPhase", () => {
|
||||
});
|
||||
|
||||
it("generates questions for missing constraints", () => {
|
||||
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
|
||||
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
|
||||
const questions = clarify.generateQuestions(specWithoutRequirements);
|
||||
const constraintQuestion = questions.find((q) => q.category === "constraints");
|
||||
expect(constraintQuestion).toBeDefined();
|
||||
@@ -58,7 +58,7 @@ describe("ClarifyPhase", () => {
|
||||
});
|
||||
|
||||
it("generates deployment question when deploy is mentioned without deploy constraint", () => {
|
||||
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
|
||||
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
|
||||
const questions = clarify.generateQuestions(specWithRequirements);
|
||||
const deployQuestion = questions.find((q) => q.category === "deployment");
|
||||
expect(deployQuestion).toBeDefined();
|
||||
@@ -66,8 +66,8 @@ describe("ClarifyPhase", () => {
|
||||
|
||||
it("respects clarify_budget", () => {
|
||||
const limitedConfig = {
|
||||
...DEFAULT_CI_CONFIG,
|
||||
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, clarify_budget: 1 },
|
||||
...DEFAULT_CIAGENT_CONFIG,
|
||||
autonomy: { ...DEFAULT_CIAGENT_CONFIG.autonomy, clarify_budget: 1 },
|
||||
};
|
||||
const clarify = new ClarifyPhase(limitedConfig, tempDir);
|
||||
const questions = clarify.generateQuestions(specWithoutRequirements);
|
||||
@@ -75,7 +75,7 @@ describe("ClarifyPhase", () => {
|
||||
});
|
||||
|
||||
it("assigns sequential question IDs", () => {
|
||||
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
|
||||
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
|
||||
const questions = clarify.generateQuestions(specWithoutRequirements);
|
||||
for (let i = 0; i < questions.length; i++) {
|
||||
expect(questions[i].id).toBe(`Q-${String(i + 1).padStart(3, "0")}`);
|
||||
@@ -83,7 +83,7 @@ describe("ClarifyPhase", () => {
|
||||
});
|
||||
|
||||
it("sorts questions by impact priority", () => {
|
||||
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
|
||||
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
|
||||
const questions = clarify.generateQuestions(specWithoutRequirements);
|
||||
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
||||
for (let i = 1; i < questions.length; i++) {
|
||||
@@ -96,7 +96,7 @@ describe("ClarifyPhase", () => {
|
||||
|
||||
describe("answerQuestion", () => {
|
||||
it("records an answer to a question", () => {
|
||||
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
|
||||
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
|
||||
const questions = clarify.generateQuestions(specWithoutRequirements);
|
||||
expect(questions.length).toBeGreaterThan(0);
|
||||
|
||||
@@ -107,7 +107,7 @@ describe("ClarifyPhase", () => {
|
||||
});
|
||||
|
||||
it("returns null for unknown question ID", () => {
|
||||
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
|
||||
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
|
||||
const result = clarify.answerQuestion("Q-999", "answer");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
@@ -115,7 +115,7 @@ describe("ClarifyPhase", () => {
|
||||
|
||||
describe("acceptDefaults", () => {
|
||||
it("accepts defaults for all unanswered questions", () => {
|
||||
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
|
||||
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
|
||||
clarify.generateQuestions(specWithoutRequirements);
|
||||
const result = clarify.acceptDefaults();
|
||||
|
||||
@@ -125,7 +125,7 @@ describe("ClarifyPhase", () => {
|
||||
});
|
||||
|
||||
it("preserves manually answered questions", () => {
|
||||
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
|
||||
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
|
||||
const questions = clarify.generateQuestions(specWithoutRequirements);
|
||||
if (questions.length > 0) {
|
||||
clarify.answerQuestion(questions[0].id, "My answer");
|
||||
@@ -138,11 +138,11 @@ describe("ClarifyPhase", () => {
|
||||
});
|
||||
|
||||
it("saves clarify responses file", () => {
|
||||
const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir);
|
||||
const clarify = new ClarifyPhase(DEFAULT_CIAGENT_CONFIG, tempDir);
|
||||
clarify.generateQuestions(specWithoutRequirements);
|
||||
clarify.acceptDefaults();
|
||||
|
||||
const responsesPath = path.join(tempDir, ".ci", "clarify-responses.md");
|
||||
const responsesPath = path.join(tempDir, ".ciagent", "clarify-responses.md");
|
||||
expect(fs.existsSync(responsesPath)).toBe(true);
|
||||
const content = fs.readFileSync(responsesPath, "utf-8");
|
||||
expect(content).toContain("Clarify Phase Responses");
|
||||
@@ -154,8 +154,8 @@ describe("saveSpecification / loadSpecification", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-spec-test-"));
|
||||
fs.mkdirSync(path.join(tempDir, ".ci"), { recursive: true });
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-spec-test-"));
|
||||
fs.mkdirSync(path.join(tempDir, ".ciagent"), { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
+4
-4
@@ -2,22 +2,22 @@ import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { ClarifyQuestion, ClarifyResult } from "../types/clarify.js";
|
||||
import { Specification, parseSpecification } from "../types/specification.js";
|
||||
import { CIConfig } from "../types/config.js";
|
||||
import { CIAgentConfig } from "../types/config.js";
|
||||
|
||||
const CLARIFY_RESPONSES_FILE = "clarify-responses.md";
|
||||
const SPECIFICATION_FILE = "specification.md";
|
||||
|
||||
function getCIDir(projectPath: string): string {
|
||||
return path.join(projectPath, ".ci");
|
||||
return path.join(projectPath, ".ciagent");
|
||||
}
|
||||
|
||||
export class ClarifyPhase {
|
||||
private config: CIConfig;
|
||||
private config: CIAgentConfig;
|
||||
private projectPath: string;
|
||||
private questions: ClarifyQuestion[];
|
||||
private questionCounter: number;
|
||||
|
||||
constructor(config: CIConfig, projectPath: string) {
|
||||
constructor(config: CIAgentConfig, projectPath: string) {
|
||||
this.config = config;
|
||||
this.projectPath = projectPath;
|
||||
this.questions = [];
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { CommitBuilder } from "../core/commit-builder.js";
|
||||
import { extractCiBlock, parseCiBlock } from "../core/commit-parser.js";
|
||||
import { CiMetadata } from "../types/commit-meta.js";
|
||||
import { extractCIAgentBlock, parseCIAgentBlock } from "../core/commit-parser.js";
|
||||
import { CIAgentMetadata } from "../types/commit-meta.js";
|
||||
|
||||
describe("CommitBuilder", () => {
|
||||
describe("buildCiBlock", () => {
|
||||
it("builds minimal ci block", () => {
|
||||
const ci: CiMetadata = { phase: 1, milestone: "v1.0", status: "execute" };
|
||||
const ci: CIAgentMetadata = { phase: 1, milestone: "v1.0", status: "execute" };
|
||||
const block = CommitBuilder.buildCiBlock(ci);
|
||||
|
||||
expect(block).toContain("phase: 1");
|
||||
@@ -14,19 +14,19 @@ describe("CommitBuilder", () => {
|
||||
});
|
||||
|
||||
it("builds ci block with project", () => {
|
||||
const ci: CiMetadata = { phase: 1, milestone: "v1.0", status: "execute", project: "task-api" };
|
||||
const ci: CIAgentMetadata = { phase: 1, milestone: "v1.0", status: "execute", project: "task-api" };
|
||||
const block = CommitBuilder.buildCiBlock(ci);
|
||||
expect(block).toContain("project: task-api");
|
||||
});
|
||||
|
||||
it("builds ci block without project when not set", () => {
|
||||
const ci: CiMetadata = { phase: 1, milestone: "v1.0", status: "execute" };
|
||||
const ci: CIAgentMetadata = { phase: 1, milestone: "v1.0", status: "execute" };
|
||||
const block = CommitBuilder.buildCiBlock(ci);
|
||||
expect(block).not.toContain("project:");
|
||||
});
|
||||
|
||||
it("builds ci block with decisions", () => {
|
||||
const ci: CiMetadata = {
|
||||
const ci: CIAgentMetadata = {
|
||||
phase: 1,
|
||||
milestone: "v1.0",
|
||||
status: "execute",
|
||||
@@ -49,7 +49,7 @@ describe("CommitBuilder", () => {
|
||||
});
|
||||
|
||||
it("builds ci block with lessons", () => {
|
||||
const ci: CiMetadata = {
|
||||
const ci: CIAgentMetadata = {
|
||||
phase: 1,
|
||||
milestone: "v1.0",
|
||||
status: "complete",
|
||||
@@ -63,7 +63,7 @@ describe("CommitBuilder", () => {
|
||||
});
|
||||
|
||||
it("builds ci block with compound", () => {
|
||||
const ci: CiMetadata = {
|
||||
const ci: CIAgentMetadata = {
|
||||
phase: 1,
|
||||
milestone: "v1.0",
|
||||
status: "complete",
|
||||
@@ -82,7 +82,7 @@ describe("CommitBuilder", () => {
|
||||
});
|
||||
|
||||
it("builds ci block with escalations", () => {
|
||||
const ci: CiMetadata = {
|
||||
const ci: CIAgentMetadata = {
|
||||
phase: 3,
|
||||
milestone: "v1.0",
|
||||
status: "execute",
|
||||
@@ -103,7 +103,7 @@ describe("CommitBuilder", () => {
|
||||
});
|
||||
|
||||
it("builds ci block with requirements", () => {
|
||||
const ci: CiMetadata = {
|
||||
const ci: CIAgentMetadata = {
|
||||
phase: 1,
|
||||
milestone: "v1.0",
|
||||
status: "complete",
|
||||
@@ -122,12 +122,12 @@ describe("CommitBuilder", () => {
|
||||
|
||||
describe("round-trip: build then parse", () => {
|
||||
it("round-trips a simple ci block", () => {
|
||||
const ci: CiMetadata = { phase: 1, milestone: "v1.0", status: "execute" };
|
||||
const ci: CIAgentMetadata = { phase: 1, milestone: "v1.0", status: "execute" };
|
||||
const block = CommitBuilder.buildCiBlock(ci);
|
||||
|
||||
const fullMessage = `feat(P01): test\n\n---ci---\n${block}\n---/ci---\n\nBody text`;
|
||||
const extracted = extractCiBlock(fullMessage)!;
|
||||
const parsed = parseCiBlock(extracted)!;
|
||||
const extracted = extractCIAgentBlock(fullMessage)!;
|
||||
const parsed = parseCIAgentBlock(extracted)!;
|
||||
|
||||
expect(parsed.phase).toBe(1);
|
||||
expect(parsed.milestone).toBe("v1.0");
|
||||
@@ -135,7 +135,7 @@ describe("CommitBuilder", () => {
|
||||
});
|
||||
|
||||
it("round-trips decisions", () => {
|
||||
const ci: CiMetadata = {
|
||||
const ci: CIAgentMetadata = {
|
||||
phase: 1,
|
||||
milestone: "v1.0",
|
||||
status: "execute",
|
||||
@@ -152,8 +152,8 @@ describe("CommitBuilder", () => {
|
||||
|
||||
const block = CommitBuilder.buildCiBlock(ci);
|
||||
const fullMessage = `feat(P01): test\n\n---ci---\n${block}\n---/ci---`;
|
||||
const extracted = extractCiBlock(fullMessage)!;
|
||||
const parsed = parseCiBlock(extracted)!;
|
||||
const extracted = extractCIAgentBlock(fullMessage)!;
|
||||
const parsed = parseCIAgentBlock(extracted)!;
|
||||
|
||||
expect(parsed.decisions).toHaveLength(1);
|
||||
expect(parsed.decisions![0].id).toBe("D-001");
|
||||
@@ -163,7 +163,7 @@ describe("CommitBuilder", () => {
|
||||
});
|
||||
|
||||
it("round-trips compound with lessons", () => {
|
||||
const ci: CiMetadata = {
|
||||
const ci: CIAgentMetadata = {
|
||||
phase: 2,
|
||||
milestone: "v1.0",
|
||||
status: "complete",
|
||||
@@ -177,8 +177,8 @@ describe("CommitBuilder", () => {
|
||||
|
||||
const block = CommitBuilder.buildCiBlock(ci);
|
||||
const fullMessage = `compound(P02): test\n\n---ci---\n${block}\n---/ci---`;
|
||||
const extracted = extractCiBlock(fullMessage)!;
|
||||
const parsed = parseCiBlock(extracted)!;
|
||||
const extracted = extractCIAgentBlock(fullMessage)!;
|
||||
const parsed = parseCIAgentBlock(extracted)!;
|
||||
|
||||
expect(parsed.compound!.category).toBe("auth");
|
||||
expect(parsed.compound!.problem).toBe("Token replay attacks");
|
||||
@@ -186,11 +186,11 @@ describe("CommitBuilder", () => {
|
||||
});
|
||||
|
||||
it("round-trips project field", () => {
|
||||
const ci: CiMetadata = { phase: 1, milestone: "v1.0", status: "execute", project: "task-api" };
|
||||
const ci: CIAgentMetadata = { phase: 1, milestone: "v1.0", status: "execute", project: "task-api" };
|
||||
const block = CommitBuilder.buildCiBlock(ci);
|
||||
const fullMessage = `feat(task-api/P01): test\n\n---ci---\n${block}\n---/ci---`;
|
||||
const extracted = extractCiBlock(fullMessage)!;
|
||||
const parsed = parseCiBlock(extracted)!;
|
||||
const extracted = extractCIAgentBlock(fullMessage)!;
|
||||
const parsed = parseCIAgentBlock(extracted)!;
|
||||
|
||||
expect(parsed.project).toBe("task-api");
|
||||
});
|
||||
|
||||
+11
-11
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
CiMetadata,
|
||||
CIAgentMetadata,
|
||||
CommitType,
|
||||
CommitScope,
|
||||
CommitDecision,
|
||||
@@ -17,7 +17,7 @@ export interface CommitMessageInput {
|
||||
type: CommitType;
|
||||
scope: CommitScope;
|
||||
subject: string;
|
||||
ci: CiMetadata;
|
||||
ci: CIAgentMetadata;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export interface VerifyCommitInput {
|
||||
}
|
||||
|
||||
export class CommitBuilder {
|
||||
static buildCiBlock(ci: CiMetadata): string {
|
||||
static buildCiBlock(ci: CIAgentMetadata): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`phase: ${ci.phase}`);
|
||||
lines.push(`milestone: ${ci.milestone}`);
|
||||
@@ -162,7 +162,7 @@ export class CommitBuilder {
|
||||
}
|
||||
|
||||
static buildInitCommit(input: InitCommitInput): string {
|
||||
const ci: CiMetadata = {
|
||||
const ci: CIAgentMetadata = {
|
||||
phase: 0,
|
||||
milestone: input.milestone,
|
||||
project: input.project,
|
||||
@@ -194,7 +194,7 @@ export class CommitBuilder {
|
||||
}
|
||||
|
||||
static buildTaskCommit(input: TaskCommitInput): string {
|
||||
const ci: CiMetadata = {
|
||||
const ci: CIAgentMetadata = {
|
||||
phase: input.phase,
|
||||
milestone: input.milestone,
|
||||
project: input.project,
|
||||
@@ -224,7 +224,7 @@ export class CommitBuilder {
|
||||
}
|
||||
|
||||
static buildPhaseCompletionCommit(input: PhaseCompletionInput): string {
|
||||
const ci: CiMetadata = {
|
||||
const ci: CIAgentMetadata = {
|
||||
phase: input.phase,
|
||||
milestone: input.milestone,
|
||||
status: "complete",
|
||||
@@ -253,7 +253,7 @@ export class CommitBuilder {
|
||||
}
|
||||
|
||||
static buildDecisionCommit(input: DecisionCommitInput): string {
|
||||
const ci: CiMetadata = {
|
||||
const ci: CIAgentMetadata = {
|
||||
phase: input.phase,
|
||||
milestone: input.milestone,
|
||||
status: "plan",
|
||||
@@ -271,7 +271,7 @@ export class CommitBuilder {
|
||||
}
|
||||
|
||||
static buildEscalationCommit(input: EscalationCommitInput): string {
|
||||
const ci: CiMetadata = {
|
||||
const ci: CIAgentMetadata = {
|
||||
phase: input.phase,
|
||||
milestone: input.milestone,
|
||||
status: "execute",
|
||||
@@ -289,7 +289,7 @@ export class CommitBuilder {
|
||||
}
|
||||
|
||||
static buildCompoundCommit(input: CompoundCommitInput): string {
|
||||
const ci: CiMetadata = {
|
||||
const ci: CIAgentMetadata = {
|
||||
phase: input.phase,
|
||||
milestone: input.milestone,
|
||||
status: "complete",
|
||||
@@ -313,7 +313,7 @@ export class CommitBuilder {
|
||||
}
|
||||
|
||||
static buildVerifyCommit(input: VerifyCommitInput): string {
|
||||
const ci: CiMetadata = {
|
||||
const ci: CIAgentMetadata = {
|
||||
phase: input.phase,
|
||||
milestone: input.milestone,
|
||||
status: "verify",
|
||||
@@ -338,7 +338,7 @@ export class CommitBuilder {
|
||||
findings: string[],
|
||||
decisions?: CommitDecision[]
|
||||
): string {
|
||||
const ci: CiMetadata = {
|
||||
const ci: CIAgentMetadata = {
|
||||
phase,
|
||||
milestone,
|
||||
status: "research",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
CiMetadata,
|
||||
CIAgentMetadata,
|
||||
CommitDecision,
|
||||
CommitEscalation,
|
||||
CommitRequirements,
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
CommitScope,
|
||||
} from "../types/commit-meta.js";
|
||||
import {
|
||||
extractCiBlock,
|
||||
parseCiBlock,
|
||||
extractCIAgentBlock,
|
||||
parseCIAgentBlock,
|
||||
parseCommitMessage,
|
||||
} from "./commit-parser.js";
|
||||
|
||||
@@ -128,29 +128,29 @@ status: execute
|
||||
|
||||
Registration endpoint for task-api project.`;
|
||||
|
||||
describe("extractCiBlock", () => {
|
||||
describe("extractCIAgentBlock", () => {
|
||||
it("extracts ---ci--- block from commit message", () => {
|
||||
const block = extractCiBlock(SAMPLE_INIT_COMMIT);
|
||||
const block = extractCIAgentBlock(SAMPLE_INIT_COMMIT);
|
||||
expect(block).toBeTruthy();
|
||||
expect(block).toContain("phase: 0");
|
||||
expect(block).toContain("milestone: v1.0");
|
||||
});
|
||||
|
||||
it("returns null when no ---ci--- block exists", () => {
|
||||
const block = extractCiBlock("docs: some regular commit\n\nNo CI block here");
|
||||
const block = extractCIAgentBlock("docs: some regular commit\n\nNo CI block here");
|
||||
expect(block).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for unclosed ---ci--- block", () => {
|
||||
const block = extractCiBlock("docs: bad\n---ci---\nphase: 1\nno end marker");
|
||||
const block = extractCIAgentBlock("docs: bad\n---ci---\nphase: 1\nno end marker");
|
||||
expect(block).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseCiBlock", () => {
|
||||
describe("parseCIAgentBlock", () => {
|
||||
it("parses init commit ci block", () => {
|
||||
const block = extractCiBlock(SAMPLE_INIT_COMMIT)!;
|
||||
const meta = parseCiBlock(block)!;
|
||||
const block = extractCIAgentBlock(SAMPLE_INIT_COMMIT)!;
|
||||
const meta = parseCIAgentBlock(block)!;
|
||||
|
||||
expect(meta.phase).toBe(0);
|
||||
expect(meta.milestone).toBe("v1.0");
|
||||
@@ -163,8 +163,8 @@ describe("parseCiBlock", () => {
|
||||
});
|
||||
|
||||
it("parses task commit ci block", () => {
|
||||
const block = extractCiBlock(SAMPLE_TASK_COMMIT)!;
|
||||
const meta = parseCiBlock(block)!;
|
||||
const block = extractCIAgentBlock(SAMPLE_TASK_COMMIT)!;
|
||||
const meta = parseCIAgentBlock(block)!;
|
||||
|
||||
expect(meta.phase).toBe(1);
|
||||
expect(meta.plan).toBe("01-01");
|
||||
@@ -177,8 +177,8 @@ describe("parseCiBlock", () => {
|
||||
});
|
||||
|
||||
it("parses phase completion with lessons", () => {
|
||||
const block = extractCiBlock(SAMPLE_PHASE_COMPLETE_COMMIT)!;
|
||||
const meta = parseCiBlock(block)!;
|
||||
const block = extractCIAgentBlock(SAMPLE_PHASE_COMPLETE_COMMIT)!;
|
||||
const meta = parseCIAgentBlock(block)!;
|
||||
|
||||
expect(meta.phase).toBe(1);
|
||||
expect(meta.status).toBe("complete");
|
||||
@@ -188,8 +188,8 @@ describe("parseCiBlock", () => {
|
||||
});
|
||||
|
||||
it("parses compound commit", () => {
|
||||
const block = extractCiBlock(SAMPLE_COMPOUND_COMMIT)!;
|
||||
const meta = parseCiBlock(block)!;
|
||||
const block = extractCIAgentBlock(SAMPLE_COMPOUND_COMMIT)!;
|
||||
const meta = parseCIAgentBlock(block)!;
|
||||
|
||||
expect(meta.compound).toBeDefined();
|
||||
expect(meta.compound!.category).toBe("auth");
|
||||
@@ -199,8 +199,8 @@ describe("parseCiBlock", () => {
|
||||
});
|
||||
|
||||
it("parses escalation commit", () => {
|
||||
const block = extractCiBlock(SAMPLE_ESCALATION_COMMIT)!;
|
||||
const meta = parseCiBlock(block)!;
|
||||
const block = extractCIAgentBlock(SAMPLE_ESCALATION_COMMIT)!;
|
||||
const meta = parseCIAgentBlock(block)!;
|
||||
|
||||
expect(meta.escalations).toHaveLength(1);
|
||||
expect(meta.escalations![0].id).toBe("E-001");
|
||||
@@ -209,20 +209,20 @@ describe("parseCiBlock", () => {
|
||||
});
|
||||
|
||||
it("parses project field", () => {
|
||||
const block = extractCiBlock(SAMPLE_PROJECT_COMMIT)!;
|
||||
const meta = parseCiBlock(block)!;
|
||||
const block = extractCIAgentBlock(SAMPLE_PROJECT_COMMIT)!;
|
||||
const meta = parseCIAgentBlock(block)!;
|
||||
expect(meta.project).toBe("task-api");
|
||||
expect(meta.phase).toBe(1);
|
||||
expect(meta.plan).toBe("01-01");
|
||||
});
|
||||
|
||||
it("returns null for empty block", () => {
|
||||
const meta = parseCiBlock("");
|
||||
const meta = parseCIAgentBlock("");
|
||||
expect(meta).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for block missing required fields", () => {
|
||||
const meta = parseCiBlock("something: true\nother: false");
|
||||
const meta = parseCIAgentBlock("something: true\nother: false");
|
||||
expect(meta).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
+17
-17
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
CiMetadata,
|
||||
CIAgentMetadata,
|
||||
CommitType,
|
||||
CommitEscalation,
|
||||
ParsedCiCommit,
|
||||
ParsedCIAgentCommit,
|
||||
parseCommitType,
|
||||
parseCommitScope,
|
||||
} from "../types/commit-meta.js";
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
const CI_BLOCK_START = "---ci---";
|
||||
const CI_BLOCK_END = "---/ci---";
|
||||
|
||||
export function extractCiBlock(message: string): string | null {
|
||||
export function extractCIAgentBlock(message: string): string | null {
|
||||
const startIdx = message.indexOf(CI_BLOCK_START);
|
||||
if (startIdx < 0) return null;
|
||||
|
||||
@@ -20,10 +20,10 @@ export function extractCiBlock(message: string): string | null {
|
||||
return message.slice(startIdx + CI_BLOCK_START.length, endIdx).trim();
|
||||
}
|
||||
|
||||
export function parseCiBlock(yaml: string): CiMetadata | null {
|
||||
export function parseCIAgentBlock(yaml: string): CIAgentMetadata | null {
|
||||
if (!yaml) return null;
|
||||
|
||||
const result: Partial<CiMetadata> = {};
|
||||
const result: Partial<CIAgentMetadata> = {};
|
||||
|
||||
const phaseMatch = yaml.match(/^phase:\s*(.+)$/m);
|
||||
if (phaseMatch) result.phase = parseInt(phaseMatch[1], 10) || 0;
|
||||
@@ -38,7 +38,7 @@ export function parseCiBlock(yaml: string): CiMetadata | null {
|
||||
if (taskMatch) result.task = taskMatch[1].trim();
|
||||
|
||||
const statusMatch = yaml.match(/^status:\s*(.+)$/m);
|
||||
if (statusMatch) result.status = statusMatch[1].trim() as CiMetadata["status"];
|
||||
if (statusMatch) result.status = statusMatch[1].trim() as CIAgentMetadata["status"];
|
||||
|
||||
const projectMatch = yaml.match(/^project:\s*(.+)$/m);
|
||||
if (projectMatch) result.project = projectMatch[1].trim();
|
||||
@@ -50,14 +50,14 @@ export function parseCiBlock(yaml: string): CiMetadata | null {
|
||||
result.compound = parseCompoundFromYaml(yaml);
|
||||
|
||||
if (result.phase !== undefined && result.milestone !== undefined && result.status !== undefined) {
|
||||
return result as CiMetadata;
|
||||
return result as CIAgentMetadata;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseDecisionsFromYaml(yaml: string): CiMetadata["decisions"] {
|
||||
const decisions: NonNullable<CiMetadata["decisions"]> = [];
|
||||
function parseDecisionsFromYaml(yaml: string): CIAgentMetadata["decisions"] {
|
||||
const decisions: NonNullable<CIAgentMetadata["decisions"]> = [];
|
||||
const decisionRegex = /- id: (.+)\n\s+decision: (.+)\n\s+rationale: (.+)\n\s+confidence: (.+)\n\s+alternatives: \[([^\]]*)\]/g;
|
||||
let match;
|
||||
|
||||
@@ -74,8 +74,8 @@ function parseDecisionsFromYaml(yaml: string): CiMetadata["decisions"] {
|
||||
return decisions.length > 0 ? decisions : undefined;
|
||||
}
|
||||
|
||||
function parseEscalationsFromYaml(yaml: string): CiMetadata["escalations"] {
|
||||
const escalations: NonNullable<CiMetadata["escalations"]> = [];
|
||||
function parseEscalationsFromYaml(yaml: string): CIAgentMetadata["escalations"] {
|
||||
const escalations: NonNullable<CIAgentMetadata["escalations"]> = [];
|
||||
const escalationRegex = /- id: (.+)\n\s+type: (.+)\n\s+description: (.+)\n\s+resolution: (.+)/g;
|
||||
let match;
|
||||
|
||||
@@ -91,7 +91,7 @@ function parseEscalationsFromYaml(yaml: string): CiMetadata["escalations"] {
|
||||
return escalations.length > 0 ? escalations : undefined;
|
||||
}
|
||||
|
||||
function parseRequirementsFromYaml(yaml: string): CiMetadata["requirements"] {
|
||||
function parseRequirementsFromYaml(yaml: string): CIAgentMetadata["requirements"] {
|
||||
const coveredMatch = yaml.match(/^\s+covered: \[([^\]]*)\]/m);
|
||||
const partialMatch = yaml.match(/^\s+partial: \[([^\]]*)\]/m);
|
||||
|
||||
@@ -106,7 +106,7 @@ function parseRequirementsFromYaml(yaml: string): CiMetadata["requirements"] {
|
||||
return { covered, partial };
|
||||
}
|
||||
|
||||
function parseLessonsFromYaml(yaml: string): CiMetadata["lessons"] {
|
||||
function parseLessonsFromYaml(yaml: string): CIAgentMetadata["lessons"] {
|
||||
const lessonRegex = /^ - (.+)$/gm;
|
||||
const lessons: string[] = [];
|
||||
let inLessonsSection = false;
|
||||
@@ -126,7 +126,7 @@ function parseLessonsFromYaml(yaml: string): CiMetadata["lessons"] {
|
||||
return lessons.length > 0 ? lessons : undefined;
|
||||
}
|
||||
|
||||
function parseCompoundFromYaml(yaml: string): CiMetadata["compound"] {
|
||||
function parseCompoundFromYaml(yaml: string): CIAgentMetadata["compound"] {
|
||||
const categoryMatch = yaml.match(/^\s+category: (.+)$/m);
|
||||
const problemMatch = yaml.match(/^\s+problem: (.+)$/m);
|
||||
const solutionMatch = yaml.match(/^\s+solution: (.+)$/m);
|
||||
@@ -143,7 +143,7 @@ function parseCompoundFromYaml(yaml: string): CiMetadata["compound"] {
|
||||
export function parseCommitMessage(
|
||||
hash: string,
|
||||
message: string
|
||||
): ParsedCiCommit {
|
||||
): ParsedCIAgentCommit {
|
||||
const firstLine = message.split("\n")[0] || "";
|
||||
const subjectMatch = firstLine.match(/^(\w+)(?:\(([^)]+)\))?: (.+)$/);
|
||||
|
||||
@@ -157,8 +157,8 @@ export function parseCommitMessage(
|
||||
subject = subjectMatch[3] || firstLine;
|
||||
}
|
||||
|
||||
const ciBlock = extractCiBlock(message);
|
||||
const ci = ciBlock ? parseCiBlock(ciBlock) : null;
|
||||
const ciBlock = extractCIAgentBlock(message);
|
||||
const ci = ciBlock ? parseCIAgentBlock(ciBlock) : null;
|
||||
|
||||
const bodyStart = message.indexOf("\n");
|
||||
let body = bodyStart >= 0 ? message.slice(bodyStart + 1).trim() : "";
|
||||
|
||||
+37
-37
@@ -1,45 +1,45 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { initCI, loadConfig, saveConfig, isCIInitialized, ensureCIDir } from "../core/config.js";
|
||||
import { DEFAULT_CI_CONFIG } from "../types/config.js";
|
||||
import { initCIAgent, loadConfig, saveConfig, isCIAgentInitialized, ensureCIDir } from "../core/config.js";
|
||||
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
|
||||
|
||||
describe("CI Config", () => {
|
||||
describe("CIAgent Config", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-config-test-"));
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-config-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("initCI", () => {
|
||||
it("initializes a new CI project with default config", () => {
|
||||
const config = initCI(tempDir);
|
||||
describe("initCIAgent", () => {
|
||||
it("initializes a new CIAgent project with default config", () => {
|
||||
const config = initCIAgent(tempDir);
|
||||
expect(config.autonomy.level).toBe("full");
|
||||
expect(isCIInitialized(tempDir)).toBe(true);
|
||||
expect(isCIAgentInitialized(tempDir)).toBe(true);
|
||||
});
|
||||
|
||||
it("initializes with custom config merged on top of defaults", () => {
|
||||
const config = initCI(tempDir, {
|
||||
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, level: "guided" },
|
||||
const config = initCIAgent(tempDir, {
|
||||
autonomy: { ...DEFAULT_CIAGENT_CONFIG.autonomy, level: "guided" },
|
||||
});
|
||||
expect(config.autonomy.level).toBe("guided");
|
||||
expect(config.autonomy.clarify_budget).toBe(10);
|
||||
expect(config.model_profile).toBe("quality");
|
||||
});
|
||||
|
||||
it("creates .ci/ directory structure", () => {
|
||||
initCI(tempDir);
|
||||
expect(fs.existsSync(path.join(tempDir, ".ci"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(tempDir, ".ci", "config.json"))).toBe(true);
|
||||
it("creates .ciagent/ directory structure", () => {
|
||||
initCIAgent(tempDir);
|
||||
expect(fs.existsSync(path.join(tempDir, ".ciagent"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(tempDir, ".ciagent", "config.json"))).toBe(true);
|
||||
});
|
||||
|
||||
it("deep merges nested config", () => {
|
||||
const config = initCI(tempDir, {
|
||||
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, level: "supervised" },
|
||||
const config = initCIAgent(tempDir, {
|
||||
autonomy: { ...DEFAULT_CIAGENT_CONFIG.autonomy, level: "supervised" },
|
||||
});
|
||||
expect(config.autonomy.level).toBe("supervised");
|
||||
expect(config.autonomy.max_revision_iterations).toBe(3);
|
||||
@@ -47,7 +47,7 @@ describe("CI Config", () => {
|
||||
});
|
||||
|
||||
it("initializes with project slug", () => {
|
||||
const config = initCI(tempDir, undefined, "task-api", "Task API");
|
||||
const config = initCIAgent(tempDir, undefined, "task-api", "Task API");
|
||||
expect(config.projects).toHaveLength(1);
|
||||
expect(config.projects[0].slug).toBe("task-api");
|
||||
expect(config.projects[0].name).toBe("Task API");
|
||||
@@ -56,20 +56,20 @@ describe("CI Config", () => {
|
||||
});
|
||||
|
||||
it("does not re-add existing project slug", () => {
|
||||
initCI(tempDir, undefined, "task-api", "Task API");
|
||||
const config = initCI(tempDir, undefined, "task-api", "Task API V2");
|
||||
initCIAgent(tempDir, undefined, "task-api", "Task API");
|
||||
const config = initCIAgent(tempDir, undefined, "task-api", "Task API V2");
|
||||
expect(config.projects).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("defaults projects and active_project when no slug provided", () => {
|
||||
const config = initCI(tempDir);
|
||||
const config = initCIAgent(tempDir);
|
||||
expect(config.projects).toEqual([]);
|
||||
expect(config.active_project).toBe("");
|
||||
});
|
||||
|
||||
it("preserves existing projects when adding new one", () => {
|
||||
const config1 = initCI(tempDir, undefined, "task-api", "Task API");
|
||||
const config2 = initCI(tempDir, {
|
||||
const config1 = initCIAgent(tempDir, undefined, "task-api", "Task API");
|
||||
const config2 = initCIAgent(tempDir, {
|
||||
...config1,
|
||||
projects: [...config1.projects, { slug: "auth-svc", name: "Auth Service" }],
|
||||
}, "auth-svc", "Auth Service");
|
||||
@@ -81,11 +81,11 @@ describe("CI Config", () => {
|
||||
describe("loadConfig", () => {
|
||||
it("returns default config when no config file exists", () => {
|
||||
const config = loadConfig(tempDir);
|
||||
expect(config).toEqual(DEFAULT_CI_CONFIG);
|
||||
expect(config).toEqual(DEFAULT_CIAGENT_CONFIG);
|
||||
});
|
||||
|
||||
it("loads and deep merges config from file", () => {
|
||||
initCI(tempDir, { autonomy: { ...DEFAULT_CI_CONFIG.autonomy, decision_confidence_threshold: 0.85 } });
|
||||
initCIAgent(tempDir, { autonomy: { ...DEFAULT_CIAGENT_CONFIG.autonomy, decision_confidence_threshold: 0.85 } });
|
||||
const config = loadConfig(tempDir);
|
||||
expect(config.autonomy.decision_confidence_threshold).toBe(0.85);
|
||||
expect(config.autonomy.level).toBe("full");
|
||||
@@ -93,7 +93,7 @@ describe("CI Config", () => {
|
||||
});
|
||||
|
||||
it("preserves nested objects that are not overridden", () => {
|
||||
initCI(tempDir, { git: { ...DEFAULT_CI_CONFIG.git, auto_push: true } });
|
||||
initCIAgent(tempDir, { git: { ...DEFAULT_CIAGENT_CONFIG.git, auto_push: true } });
|
||||
const config = loadConfig(tempDir);
|
||||
expect(config.git.auto_push).toBe(true);
|
||||
expect(config.git.auto_commit).toBe(true);
|
||||
@@ -101,7 +101,7 @@ describe("CI Config", () => {
|
||||
});
|
||||
|
||||
it("loads projects array from config", () => {
|
||||
initCI(tempDir, undefined, "task-api", "Task API");
|
||||
initCIAgent(tempDir, undefined, "task-api", "Task API");
|
||||
const config = loadConfig(tempDir);
|
||||
expect(config.projects).toHaveLength(1);
|
||||
expect(config.active_project).toBe("task-api");
|
||||
@@ -112,8 +112,8 @@ describe("CI Config", () => {
|
||||
it("saves and reloads config correctly", () => {
|
||||
ensureCIDir(tempDir);
|
||||
const customConfig = {
|
||||
...DEFAULT_CI_CONFIG,
|
||||
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, level: "guided" as const },
|
||||
...DEFAULT_CIAGENT_CONFIG,
|
||||
autonomy: { ...DEFAULT_CIAGENT_CONFIG.autonomy, level: "guided" as const },
|
||||
};
|
||||
saveConfig(tempDir, customConfig);
|
||||
const loaded = loadConfig(tempDir);
|
||||
@@ -123,7 +123,7 @@ describe("CI Config", () => {
|
||||
it("saves and reloads config with projects", () => {
|
||||
ensureCIDir(tempDir);
|
||||
const config = {
|
||||
...DEFAULT_CI_CONFIG,
|
||||
...DEFAULT_CIAGENT_CONFIG,
|
||||
projects: [{ slug: "my-app", name: "My App", default: true }],
|
||||
active_project: "my-app",
|
||||
};
|
||||
@@ -134,27 +134,27 @@ describe("CI Config", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isCIInitialized", () => {
|
||||
describe("isCIAgentInitialized", () => {
|
||||
it("returns false for uninitialized directory", () => {
|
||||
expect(isCIInitialized(tempDir)).toBe(false);
|
||||
expect(isCIAgentInitialized(tempDir)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true after initCI", () => {
|
||||
initCI(tempDir);
|
||||
expect(isCIInitialized(tempDir)).toBe(true);
|
||||
it("returns true after initCIAgent", () => {
|
||||
initCIAgent(tempDir);
|
||||
expect(isCIAgentInitialized(tempDir)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensureCIDir", () => {
|
||||
it("creates .ci directory", () => {
|
||||
it("creates .ciagent directory", () => {
|
||||
ensureCIDir(tempDir);
|
||||
expect(fs.existsSync(path.join(tempDir, ".ci"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(tempDir, ".ciagent"))).toBe(true);
|
||||
});
|
||||
|
||||
it("is idempotent", () => {
|
||||
ensureCIDir(tempDir);
|
||||
ensureCIDir(tempDir);
|
||||
expect(fs.existsSync(path.join(tempDir, ".ci"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(tempDir, ".ciagent"))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
+25
-25
@@ -1,11 +1,11 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { CIConfig, DEFAULT_CI_CONFIG } from "../types/config.js";
|
||||
import { CIAgentConfig, DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
|
||||
|
||||
const CI_DIR = ".ci";
|
||||
const CI_DIR = ".ciagent";
|
||||
const CONFIG_FILE = "config.json";
|
||||
|
||||
export function getCIConfigPath(projectPath: string): string {
|
||||
export function getCIAgentConfigPath(projectPath: string): string {
|
||||
return path.join(projectPath, CI_DIR, CONFIG_FILE);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export function ensureCIDir(projectPath: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function deepMerge(base: CIConfig, override: Record<string, unknown>): CIConfig {
|
||||
function deepMerge(base: CIAgentConfig, override: Record<string, unknown>): CIAgentConfig {
|
||||
const result = { ...base } as Record<string, unknown>;
|
||||
for (const key of Object.keys(override)) {
|
||||
const baseVal = result[key];
|
||||
@@ -30,43 +30,43 @@ function deepMerge(base: CIConfig, override: Record<string, unknown>): CIConfig
|
||||
overrideVal && typeof overrideVal === "object" && !Array.isArray(overrideVal)
|
||||
) {
|
||||
result[key] = deepMerge(
|
||||
baseVal as unknown as CIConfig,
|
||||
baseVal as unknown as CIAgentConfig,
|
||||
overrideVal as Record<string, unknown>
|
||||
) as unknown;
|
||||
} else if (overrideVal !== undefined) {
|
||||
result[key] = overrideVal;
|
||||
}
|
||||
}
|
||||
return result as unknown as CIConfig;
|
||||
return result as unknown as CIAgentConfig;
|
||||
}
|
||||
|
||||
export function loadConfig(projectPath: string): CIConfig {
|
||||
const configPath = getCIConfigPath(projectPath);
|
||||
export function loadConfig(projectPath: string): CIAgentConfig {
|
||||
const configPath = getCIAgentConfigPath(projectPath);
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return { ...DEFAULT_CI_CONFIG };
|
||||
return { ...DEFAULT_CIAGENT_CONFIG };
|
||||
}
|
||||
const raw = fs.readFileSync(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw);
|
||||
return deepMerge(DEFAULT_CI_CONFIG, parsed);
|
||||
return deepMerge(DEFAULT_CIAGENT_CONFIG, parsed);
|
||||
}
|
||||
|
||||
export function saveConfig(projectPath: string, config: CIConfig): void {
|
||||
export function saveConfig(projectPath: string, config: CIAgentConfig): void {
|
||||
ensureCIDir(projectPath);
|
||||
const configPath = getCIConfigPath(projectPath);
|
||||
const configPath = getCIAgentConfigPath(projectPath);
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
export function isCIInitialized(projectPath: string): boolean {
|
||||
export function isCIAgentInitialized(projectPath: string): boolean {
|
||||
const ciDir = getCIDir(projectPath);
|
||||
const configPath = getCIConfigPath(projectPath);
|
||||
const configPath = getCIAgentConfigPath(projectPath);
|
||||
return fs.existsSync(ciDir) && fs.existsSync(configPath);
|
||||
}
|
||||
|
||||
export function initCI(projectPath: string, config?: Partial<CIConfig>, projectSlug?: string, projectName?: string): CIConfig {
|
||||
export function initCIAgent(projectPath: string, config?: Partial<CIAgentConfig>, projectSlug?: string, projectName?: string): CIAgentConfig {
|
||||
ensureCIDir(projectPath);
|
||||
|
||||
let projects = config?.projects || DEFAULT_CI_CONFIG.projects;
|
||||
let activeProject = config?.active_project || DEFAULT_CI_CONFIG.active_project;
|
||||
let projects = config?.projects || DEFAULT_CIAGENT_CONFIG.projects;
|
||||
let activeProject = config?.active_project || DEFAULT_CIAGENT_CONFIG.active_project;
|
||||
|
||||
if (projectSlug) {
|
||||
if (!projects.some((p) => p.slug === projectSlug)) {
|
||||
@@ -75,20 +75,20 @@ export function initCI(projectPath: string, config?: Partial<CIConfig>, projectS
|
||||
activeProject = projectSlug;
|
||||
}
|
||||
|
||||
const fullConfig: CIConfig = {
|
||||
...DEFAULT_CI_CONFIG,
|
||||
const fullConfig: CIAgentConfig = {
|
||||
...DEFAULT_CIAGENT_CONFIG,
|
||||
...config,
|
||||
projects,
|
||||
active_project: activeProject,
|
||||
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, ...config?.autonomy },
|
||||
autonomy: { ...DEFAULT_CIAGENT_CONFIG.autonomy, ...config?.autonomy },
|
||||
parallelization: {
|
||||
...DEFAULT_CI_CONFIG.parallelization,
|
||||
...DEFAULT_CIAGENT_CONFIG.parallelization,
|
||||
...config?.parallelization,
|
||||
},
|
||||
verification: { ...DEFAULT_CI_CONFIG.verification, ...config?.verification },
|
||||
security: { ...DEFAULT_CI_CONFIG.security, ...config?.security },
|
||||
git: { ...DEFAULT_CI_CONFIG.git, ...config?.git },
|
||||
backend: { ...DEFAULT_CI_CONFIG.backend, ...config?.backend },
|
||||
verification: { ...DEFAULT_CIAGENT_CONFIG.verification, ...config?.verification },
|
||||
security: { ...DEFAULT_CIAGENT_CONFIG.security, ...config?.security },
|
||||
git: { ...DEFAULT_CIAGENT_CONFIG.git, ...config?.git },
|
||||
backend: { ...DEFAULT_CIAGENT_CONFIG.backend, ...config?.backend },
|
||||
};
|
||||
saveConfig(projectPath, fullConfig);
|
||||
return fullConfig;
|
||||
|
||||
@@ -2,15 +2,15 @@ import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { DecisionEngine, DecisionInput } from "../core/decision-engine.js";
|
||||
import { DEFAULT_CI_CONFIG } from "../types/config.js";
|
||||
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
|
||||
|
||||
describe("DecisionEngine", () => {
|
||||
let tempDir: string;
|
||||
let engine: DecisionEngine;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-decision-test-"));
|
||||
engine = new DecisionEngine(DEFAULT_CI_CONFIG, tempDir);
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-decision-test-"));
|
||||
engine = new DecisionEngine(DEFAULT_CIAGENT_CONFIG, tempDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -106,8 +106,8 @@ describe("DecisionEngine", () => {
|
||||
|
||||
it("escalates if threshold is raised above 0.7", () => {
|
||||
const strictConfig = {
|
||||
...DEFAULT_CI_CONFIG,
|
||||
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, decision_confidence_threshold: 0.8 },
|
||||
...DEFAULT_CIAGENT_CONFIG,
|
||||
autonomy: { ...DEFAULT_CIAGENT_CONFIG.autonomy, decision_confidence_threshold: 0.8 },
|
||||
};
|
||||
const strictEngine = new DecisionEngine(strictConfig, tempDir);
|
||||
const result = strictEngine.makeMediumConfidenceDecision(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { Decision, DecisionCategory, Alternative, confidenceToLevel } from "../types/decisions.js";
|
||||
import { CIConfig } from "../types/config.js";
|
||||
import { CIAgentConfig } from "../types/config.js";
|
||||
import { CommitBuilder, DecisionCommitInput } from "./commit-builder.js";
|
||||
import { CommitDecision } from "../types/commit-meta.js";
|
||||
|
||||
@@ -22,13 +22,13 @@ export interface DecisionResult {
|
||||
}
|
||||
|
||||
export class DecisionEngine {
|
||||
private config: CIConfig;
|
||||
private config: CIAgentConfig;
|
||||
private projectPath: string;
|
||||
private currentPhase: number;
|
||||
private currentMilestone: string;
|
||||
private decisionCounter: number;
|
||||
|
||||
constructor(config: CIConfig, projectPath: string, milestone: string = "v1.0") {
|
||||
constructor(config: CIAgentConfig, projectPath: string, milestone: string = "v1.0") {
|
||||
this.config = config;
|
||||
this.projectPath = projectPath;
|
||||
this.currentPhase = 0;
|
||||
|
||||
@@ -2,16 +2,16 @@ import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { ErrorRecovery } from "../core/error-recovery.js";
|
||||
import { DEFAULT_CI_CONFIG } from "../types/config.js";
|
||||
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
|
||||
|
||||
describe("ErrorRecovery", () => {
|
||||
let tempDir: string;
|
||||
let recovery: ErrorRecovery;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-recovery-test-"));
|
||||
fs.mkdirSync(path.join(tempDir, ".ci"), { recursive: true });
|
||||
recovery = new ErrorRecovery(DEFAULT_CI_CONFIG, tempDir);
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-recovery-test-"));
|
||||
fs.mkdirSync(path.join(tempDir, ".ciagent"), { recursive: true });
|
||||
recovery = new ErrorRecovery(DEFAULT_CIAGENT_CONFIG, tempDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { CIConfig } from "../types/config.js";
|
||||
import { CIAgentConfig } from "../types/config.js";
|
||||
|
||||
export interface RetryConfig {
|
||||
max_retries: number;
|
||||
@@ -15,11 +15,11 @@ export interface RecoveryResult {
|
||||
}
|
||||
|
||||
export class ErrorRecovery {
|
||||
private config: CIConfig;
|
||||
private config: CIAgentConfig;
|
||||
private projectPath: string;
|
||||
private revisionCount: number;
|
||||
|
||||
constructor(config: CIConfig, projectPath: string) {
|
||||
constructor(config: CIAgentConfig, projectPath: string) {
|
||||
this.config = config;
|
||||
this.projectPath = projectPath;
|
||||
this.revisionCount = 0;
|
||||
|
||||
@@ -2,17 +2,17 @@ import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { EscalationProtocol, EscalationInput } from "../core/escalation.js";
|
||||
import { DEFAULT_CI_CONFIG } from "../types/config.js";
|
||||
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
|
||||
|
||||
describe("EscalationProtocol", () => {
|
||||
let tempDir: string;
|
||||
let protocol: EscalationProtocol;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-escalation-test-"));
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-escalation-test-"));
|
||||
const noAutoCommitConfig = {
|
||||
...DEFAULT_CI_CONFIG,
|
||||
git: { ...DEFAULT_CI_CONFIG.git, auto_commit: false },
|
||||
...DEFAULT_CIAGENT_CONFIG,
|
||||
git: { ...DEFAULT_CIAGENT_CONFIG.git, auto_commit: false },
|
||||
};
|
||||
protocol = new EscalationProtocol(noAutoCommitConfig, tempDir);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
EscalationResolution,
|
||||
ESCALATION_TYPES,
|
||||
} from "../types/escalation.js";
|
||||
import { CIConfig } from "../types/config.js";
|
||||
import { CIAgentConfig } from "../types/config.js";
|
||||
import { CommitBuilder, EscalationCommitInput } from "./commit-builder.js";
|
||||
import { CommitEscalation } from "../types/commit-meta.js";
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface EscalationInput {
|
||||
}
|
||||
|
||||
export class EscalationProtocol {
|
||||
private config: CIConfig;
|
||||
private config: CIAgentConfig;
|
||||
private projectPath: string;
|
||||
private currentMilestone: string;
|
||||
private counter: number;
|
||||
@@ -31,7 +31,7 @@ export class EscalationProtocol {
|
||||
private timers: NodeJS.Timeout[];
|
||||
|
||||
constructor(
|
||||
config: CIConfig,
|
||||
config: CIAgentConfig,
|
||||
projectPath: string,
|
||||
milestone: string = "v1.0",
|
||||
timeoutCallback: (escalation: Escalation, chosenOption: string) => void = () => {}
|
||||
@@ -64,7 +64,7 @@ export class EscalationProtocol {
|
||||
options: input.options,
|
||||
default_option_id: input.default_option_id,
|
||||
resolution: "pending",
|
||||
audit_file: `.ci/audit/deprecated`,
|
||||
audit_file: `.ciagent/audit/deprecated`,
|
||||
};
|
||||
|
||||
this.pendingEscalations.set(id, escalation);
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as fs from "node:fs";
|
||||
import { GitBranch } from "../core/git-branch.js";
|
||||
|
||||
function createTempRepo(): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-branch-test-"));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-branch-test-"));
|
||||
execSync("git init", { cwd: dir, stdio: "pipe" });
|
||||
execSync('git config user.email "test@test.com"', { cwd: dir, stdio: "pipe" });
|
||||
execSync('git config user.name "Test"', { cwd: dir, stdio: "pipe" });
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as fs from "node:fs";
|
||||
import { GitContext } from "../core/git-context.js";
|
||||
|
||||
function createTempRepo(): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-test-"));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-test-"));
|
||||
execSync("git init", { cwd: dir, stdio: "pipe" });
|
||||
execSync('git config user.email "test@test.com"', { cwd: dir, stdio: "pipe" });
|
||||
execSync('git config user.name "Test"', { cwd: dir, stdio: "pipe" });
|
||||
@@ -41,7 +41,7 @@ describe("GitContext", () => {
|
||||
});
|
||||
|
||||
it("returns false for non-git directory", () => {
|
||||
const nonGit = fs.mkdtempSync(path.join(os.tmpdir(), "ci-nongit-"));
|
||||
const nonGit = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-nongit-"));
|
||||
const ctx = new GitContext(nonGit);
|
||||
expect(ctx.isGitRepo()).toBe(false);
|
||||
cleanup(nonGit);
|
||||
|
||||
+10
-10
@@ -1,7 +1,7 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import {
|
||||
ParsedCiCommit,
|
||||
CiMetadata,
|
||||
ParsedCIAgentCommit,
|
||||
CIAgentMetadata,
|
||||
CommitDecision,
|
||||
} from "../types/commit-meta.js";
|
||||
import { parseCommitMessage } from "./commit-parser.js";
|
||||
@@ -16,7 +16,7 @@ export interface ProjectState {
|
||||
phasesCompleted: number[];
|
||||
phaseBranches: BranchInfo[];
|
||||
milestoneBranches: string[];
|
||||
lastCommit: ParsedCiCommit | null;
|
||||
lastCommit: ParsedCIAgentCommit | null;
|
||||
}
|
||||
|
||||
export interface BranchInfo {
|
||||
@@ -69,13 +69,13 @@ export class GitContext {
|
||||
return this.git("rev-parse --abbrev-ref HEAD");
|
||||
}
|
||||
|
||||
getRecentCommits(count: number = 20): ParsedCiCommit[] {
|
||||
getRecentCommits(count: number = 20): ParsedCIAgentCommit[] {
|
||||
const format = "%H%x00%s%x00%B%x01";
|
||||
const raw = this.git(`log --max-count=${count} --format="${format}"`);
|
||||
|
||||
if (!raw) return [];
|
||||
|
||||
const commits: ParsedCiCommit[] = [];
|
||||
const commits: ParsedCIAgentCommit[] = [];
|
||||
const entries = raw.split("\x01").filter(Boolean);
|
||||
|
||||
for (const entry of entries) {
|
||||
@@ -93,7 +93,7 @@ export class GitContext {
|
||||
return commits;
|
||||
}
|
||||
|
||||
getLatestCiCommit(): ParsedCiCommit | null {
|
||||
getLatestCiCommit(): ParsedCIAgentCommit | null {
|
||||
const commits = this.getRecentCommits(1);
|
||||
return commits.length > 0 ? commits[0] : null;
|
||||
}
|
||||
@@ -207,7 +207,7 @@ export class GitContext {
|
||||
return decisions;
|
||||
}
|
||||
|
||||
getDecisionsFromCommits(commits: ParsedCiCommit[], phase?: number): CommitDecision[] {
|
||||
getDecisionsFromCommits(commits: ParsedCIAgentCommit[], phase?: number): CommitDecision[] {
|
||||
const decisions: CommitDecision[] = [];
|
||||
for (const commit of commits) {
|
||||
if (commit.ci?.decisions) {
|
||||
@@ -300,20 +300,20 @@ export class GitContext {
|
||||
};
|
||||
}
|
||||
|
||||
getCommitsForPhase(phase: number): ParsedCiCommit[] {
|
||||
getCommitsForPhase(phase: number): ParsedCIAgentCommit[] {
|
||||
const commits = this.getRecentCommits(200);
|
||||
return commits.filter(
|
||||
(c) => c.scope === `P${String(phase).padStart(2, "0")}` || c.ci?.phase === phase
|
||||
);
|
||||
}
|
||||
|
||||
getCommitsForBranch(branch: string): ParsedCiCommit[] {
|
||||
getCommitsForBranch(branch: string): ParsedCIAgentCommit[] {
|
||||
const format = "%H%x00%s%x00%B%x01";
|
||||
const raw = this.git(`log ${branch} --max-count=100 --format="${format}"`);
|
||||
|
||||
if (!raw) return [];
|
||||
|
||||
const commits: ParsedCiCommit[] = [];
|
||||
const commits: ParsedCIAgentCommit[] = [];
|
||||
const entries = raw.split("\x01").filter(Boolean);
|
||||
|
||||
for (const entry of entries) {
|
||||
|
||||
+5
-5
@@ -1,12 +1,12 @@
|
||||
export { initCI, loadConfig, saveConfig, isCIInitialized, getCIConfigPath, getCIDir, ensureCIDir } from "./config.js";
|
||||
export { initCIAgent, loadConfig, saveConfig, isCIAgentInitialized, getCIAgentConfigPath, getCIDir, ensureCIDir } from "./config.js";
|
||||
export { DecisionEngine } from "./decision-engine.js";
|
||||
export { EscalationProtocol } from "./escalation.js";
|
||||
export { ClarifyPhase } from "./clarify.js";
|
||||
export { CiFiles } from "./ci-files.js";
|
||||
export { CIAgentFiles } from "./ciagent-files.js";
|
||||
export { ErrorRecovery } from "./error-recovery.js";
|
||||
export { GitContext } from "./git-context.js";
|
||||
export { GitBranch } from "./git-branch.js";
|
||||
export { CommitBuilder } from "./commit-builder.js";
|
||||
export { extractCiBlock, parseCiBlock, parseCommitMessage } from "./commit-parser.js";
|
||||
export type { CIConfig } from "../types/config.js";
|
||||
export { DEFAULT_CI_CONFIG } from "../types/config.js";
|
||||
export { extractCIAgentBlock, parseCIAgentBlock, parseCommitMessage } from "./commit-parser.js";
|
||||
export type { CIAgentConfig } from "../types/config.js";
|
||||
export { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
|
||||
+7
-7
@@ -2,20 +2,20 @@ export { OrchestratorAgent } from "./agents/orchestrator.js";
|
||||
export { DecisionEngine } from "./core/decision-engine.js";
|
||||
export { EscalationProtocol } from "./core/escalation.js";
|
||||
export { ClarifyPhase } from "./core/clarify.js";
|
||||
export { CiFiles } from "./core/ci-files.js";
|
||||
export { CIAgentFiles } from "./core/ciagent-files.js";
|
||||
export { ErrorRecovery } from "./core/error-recovery.js";
|
||||
export { GitContext } from "./core/git-context.js";
|
||||
export { GitBranch } from "./core/git-branch.js";
|
||||
export { CommitBuilder } from "./core/commit-builder.js";
|
||||
export { extractCiBlock, parseCiBlock, parseCommitMessage } from "./core/commit-parser.js";
|
||||
export { extractCIAgentBlock, parseCIAgentBlock, parseCommitMessage } from "./core/commit-parser.js";
|
||||
export { VerificationPipeline } from "./verification/index.js";
|
||||
export { StructuralVerification } from "./verification/structural.js";
|
||||
export { BehavioralVerification } from "./verification/behavioral.js";
|
||||
export { SecurityVerification } from "./verification/security.js";
|
||||
export { QualityVerification } from "./verification/quality.js";
|
||||
export { getAgent, getAvailableAgents } from "./agents/index.js";
|
||||
export { initCI, loadConfig, saveConfig, isCIInitialized } from "./core/config.js";
|
||||
export { DEFAULT_CI_CONFIG } from "./types/config.js";
|
||||
export { initCIAgent, loadConfig, saveConfig, isCIAgentInitialized } from "./core/config.js";
|
||||
export { DEFAULT_CIAGENT_CONFIG } from "./types/config.js";
|
||||
export { confidenceToLevel, shouldEscalate } from "./types/decisions.js";
|
||||
export { ESCALATION_TYPES } from "./types/escalation.js";
|
||||
export { createClarifyQuestion } from "./types/clarify.js";
|
||||
@@ -28,7 +28,7 @@ export { OllamaLocalBackend } from "./backends/ollama-local.js";
|
||||
export { OllamaCloudBackend } from "./backends/ollama-cloud.js";
|
||||
export { ToolRegistry } from "./backends/tool-registry.js";
|
||||
|
||||
export type { CIConfig, AutonomyLevel, ModelProfile } from "./types/config.js";
|
||||
export type { CIAgentConfig, AutonomyLevel, ModelProfile } from "./types/config.js";
|
||||
export type { Decision, DecisionCategory } from "./types/decisions.js";
|
||||
export type { Escalation, EscalationType } from "./types/escalation.js";
|
||||
export type { PipelineState, PhaseResult, OrchestratorResult } from "./types/pipeline.js";
|
||||
@@ -38,9 +38,9 @@ export type { AgentContext, AgentResult } from "./agents/base.js";
|
||||
export type { LayeredVerificationResult } from "./verification/index.js";
|
||||
export type { VerificationResult, VerificationCheck } from "./verification/types.js";
|
||||
export type { AgentName } from "./types/config.js";
|
||||
export type { CiMetadata, ParsedCiCommit, CommitType, CommitScope, CommitDecision, CommitEscalation, CommitRequirements, CommitCompoundMeta } from "./types/commit-meta.js";
|
||||
export type { CIAgentMetadata, ParsedCIAgentCommit, CommitType, CommitScope, CommitDecision, CommitEscalation, CommitRequirements, CommitCompoundMeta } from "./types/commit-meta.js";
|
||||
export type { ProjectState, BranchInfo } from "./core/git-context.js";
|
||||
export type { PhaseBranchInfo, MilestoneBranchInfo, BranchCreateResult, BranchMergeResult } from "./core/git-branch.js";
|
||||
export type { ProjectMd, RoadmapMd, RequirementsMd, ArchitectureMd } from "./core/ci-files.js";
|
||||
export type { ProjectMd, RoadmapMd, RequirementsMd, ArchitectureMd } from "./core/ciagent-files.js";
|
||||
export type { IntelligenceBackend, BackendRequest, BackendResult, BackendConfigSection, BackendUnavailableError, Artifact, TokenUsage } from "./backends/types.js";
|
||||
export type { ToolDefinition, ToolCall, ToolResult } from "./backends/tool-registry.js";
|
||||
@@ -51,7 +51,7 @@ export interface CommitCompoundMeta {
|
||||
solution: string;
|
||||
}
|
||||
|
||||
export interface CiMetadata {
|
||||
export interface CIAgentMetadata {
|
||||
phase: number;
|
||||
milestone: string;
|
||||
project?: string;
|
||||
@@ -65,12 +65,12 @@ export interface CiMetadata {
|
||||
compound?: CommitCompoundMeta;
|
||||
}
|
||||
|
||||
export interface ParsedCiCommit {
|
||||
export interface ParsedCIAgentCommit {
|
||||
hash: string;
|
||||
type: CommitType;
|
||||
scope: string;
|
||||
subject: string;
|
||||
ci: CiMetadata | null;
|
||||
ci: CIAgentMetadata | null;
|
||||
body: string;
|
||||
}
|
||||
|
||||
|
||||
+32
-32
@@ -1,33 +1,33 @@
|
||||
import { CIConfig, DEFAULT_CI_CONFIG, AutonomyLevel, ModelProfile, ProjectEntry } from "../types/config.js";
|
||||
import { CIAgentConfig, DEFAULT_CIAGENT_CONFIG, AutonomyLevel, ModelProfile, ProjectEntry } from "../types/config.js";
|
||||
|
||||
describe("CIConfig", () => {
|
||||
it("DEFAULT_CI_CONFIG has all required fields", () => {
|
||||
expect(DEFAULT_CI_CONFIG.autonomy.level).toBe("full");
|
||||
expect(DEFAULT_CI_CONFIG.autonomy.clarify_budget).toBe(10);
|
||||
expect(DEFAULT_CI_CONFIG.autonomy.decision_confidence_threshold).toBe(0.6);
|
||||
expect(DEFAULT_CI_CONFIG.autonomy.max_revision_iterations).toBe(3);
|
||||
expect(DEFAULT_CI_CONFIG.autonomy.max_verification_retries).toBe(2);
|
||||
expect(DEFAULT_CI_CONFIG.autonomy.escalation_timeout_ms).toBe(300000);
|
||||
expect(DEFAULT_CI_CONFIG.autonomy.escalation_hooks).toContain("deploy");
|
||||
expect(DEFAULT_CI_CONFIG.model_profile).toBe("quality");
|
||||
expect(DEFAULT_CI_CONFIG.parallelization.enabled).toBe(true);
|
||||
expect(DEFAULT_CI_CONFIG.verification.automated_only).toBe(true);
|
||||
expect(DEFAULT_CI_CONFIG.security.auto_accept_low_severity).toBe(true);
|
||||
expect(DEFAULT_CI_CONFIG.git.auto_commit).toBe(true);
|
||||
expect(DEFAULT_CI_CONFIG.git.auto_push).toBe(false);
|
||||
describe("CIAgentConfig", () => {
|
||||
it("DEFAULT_CIAGENT_CONFIG has all required fields", () => {
|
||||
expect(DEFAULT_CIAGENT_CONFIG.autonomy.level).toBe("full");
|
||||
expect(DEFAULT_CIAGENT_CONFIG.autonomy.clarify_budget).toBe(10);
|
||||
expect(DEFAULT_CIAGENT_CONFIG.autonomy.decision_confidence_threshold).toBe(0.6);
|
||||
expect(DEFAULT_CIAGENT_CONFIG.autonomy.max_revision_iterations).toBe(3);
|
||||
expect(DEFAULT_CIAGENT_CONFIG.autonomy.max_verification_retries).toBe(2);
|
||||
expect(DEFAULT_CIAGENT_CONFIG.autonomy.escalation_timeout_ms).toBe(300000);
|
||||
expect(DEFAULT_CIAGENT_CONFIG.autonomy.escalation_hooks).toContain("deploy");
|
||||
expect(DEFAULT_CIAGENT_CONFIG.model_profile).toBe("quality");
|
||||
expect(DEFAULT_CIAGENT_CONFIG.parallelization.enabled).toBe(true);
|
||||
expect(DEFAULT_CIAGENT_CONFIG.verification.automated_only).toBe(true);
|
||||
expect(DEFAULT_CIAGENT_CONFIG.security.auto_accept_low_severity).toBe(true);
|
||||
expect(DEFAULT_CIAGENT_CONFIG.git.auto_commit).toBe(true);
|
||||
expect(DEFAULT_CIAGENT_CONFIG.git.auto_push).toBe(false);
|
||||
});
|
||||
|
||||
it("DEFAULT_CI_CONFIG has multi-project fields", () => {
|
||||
expect(DEFAULT_CI_CONFIG.projects).toEqual([]);
|
||||
expect(DEFAULT_CI_CONFIG.active_project).toBe("");
|
||||
it("DEFAULT_CIAGENT_CONFIG has multi-project fields", () => {
|
||||
expect(DEFAULT_CIAGENT_CONFIG.projects).toEqual([]);
|
||||
expect(DEFAULT_CIAGENT_CONFIG.active_project).toBe("");
|
||||
});
|
||||
|
||||
it("AutonomyLevel accepts all valid levels", () => {
|
||||
const levels: AutonomyLevel[] = ["full", "supervised", "guided"];
|
||||
for (const level of levels) {
|
||||
const config: CIConfig = {
|
||||
...DEFAULT_CI_CONFIG,
|
||||
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, level },
|
||||
const config: CIAgentConfig = {
|
||||
...DEFAULT_CIAGENT_CONFIG,
|
||||
autonomy: { ...DEFAULT_CIAGENT_CONFIG.autonomy, level },
|
||||
};
|
||||
expect(config.autonomy.level).toBe(level);
|
||||
}
|
||||
@@ -36,8 +36,8 @@ describe("CIConfig", () => {
|
||||
it("ModelProfile accepts all valid profiles", () => {
|
||||
const profiles: ModelProfile[] = ["quality", "speed", "balanced"];
|
||||
for (const profile of profiles) {
|
||||
const config: CIConfig = {
|
||||
...DEFAULT_CI_CONFIG,
|
||||
const config: CIAgentConfig = {
|
||||
...DEFAULT_CIAGENT_CONFIG,
|
||||
model_profile: profile,
|
||||
};
|
||||
expect(config.model_profile).toBe(profile);
|
||||
@@ -45,7 +45,7 @@ describe("CIConfig", () => {
|
||||
});
|
||||
|
||||
it("escalation_hooks defaults include expected items", () => {
|
||||
expect(DEFAULT_CI_CONFIG.autonomy.escalation_hooks).toEqual([
|
||||
expect(DEFAULT_CIAGENT_CONFIG.autonomy.escalation_hooks).toEqual([
|
||||
"deploy",
|
||||
"delete_data",
|
||||
"merge_to_main",
|
||||
@@ -66,10 +66,10 @@ describe("CIConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("CIConfig with projects", () => {
|
||||
describe("CIAgentConfig with projects", () => {
|
||||
it("supports multiple projects", () => {
|
||||
const config: CIConfig = {
|
||||
...DEFAULT_CI_CONFIG,
|
||||
const config: CIAgentConfig = {
|
||||
...DEFAULT_CIAGENT_CONFIG,
|
||||
projects: [
|
||||
{ slug: "task-api", name: "Task API", default: true },
|
||||
{ slug: "auth-svc", name: "Auth Service" },
|
||||
@@ -82,8 +82,8 @@ describe("CIConfig", () => {
|
||||
});
|
||||
|
||||
it("supports single project", () => {
|
||||
const config: CIConfig = {
|
||||
...DEFAULT_CI_CONFIG,
|
||||
const config: CIAgentConfig = {
|
||||
...DEFAULT_CIAGENT_CONFIG,
|
||||
projects: [{ slug: "my-app", name: "My App", default: true }],
|
||||
active_project: "my-app",
|
||||
};
|
||||
@@ -92,8 +92,8 @@ describe("CIConfig", () => {
|
||||
});
|
||||
|
||||
it("defaults to empty projects array and empty active_project", () => {
|
||||
expect(DEFAULT_CI_CONFIG.projects).toEqual([]);
|
||||
expect(DEFAULT_CI_CONFIG.active_project).toBe("");
|
||||
expect(DEFAULT_CIAGENT_CONFIG.projects).toEqual([]);
|
||||
expect(DEFAULT_CIAGENT_CONFIG.active_project).toBe("");
|
||||
});
|
||||
});
|
||||
});
|
||||
+4
-3
@@ -28,7 +28,8 @@ export type AgentName =
|
||||
| "plan-checker"
|
||||
| "project-researcher"
|
||||
| "research-synthesizer"
|
||||
| "solution-writer";
|
||||
| "solution-writer"
|
||||
| "tester";
|
||||
|
||||
export interface AutonomyConfig {
|
||||
level: AutonomyLevel;
|
||||
@@ -71,7 +72,7 @@ export interface ProjectEntry {
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
export interface CIConfig {
|
||||
export interface CIAgentConfig {
|
||||
projects: ProjectEntry[];
|
||||
active_project: string;
|
||||
autonomy: AutonomyConfig;
|
||||
@@ -83,7 +84,7 @@ export interface CIConfig {
|
||||
backend: BackendConfigSection;
|
||||
}
|
||||
|
||||
export const DEFAULT_CI_CONFIG: CIConfig = {
|
||||
export const DEFAULT_CIAGENT_CONFIG: CIAgentConfig = {
|
||||
projects: [],
|
||||
active_project: "",
|
||||
autonomy: {
|
||||
|
||||
@@ -3,11 +3,11 @@ import { confidenceToLevel, shouldEscalate } from "../types/decisions.js";
|
||||
import { ESCALATION_TYPES } from "../types/escalation.js";
|
||||
import { parseSpecification } from "../types/specification.js";
|
||||
import { createClarifyQuestion } from "../types/clarify.js";
|
||||
import { DEFAULT_CI_CONFIG } from "../types/config.js";
|
||||
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
|
||||
|
||||
describe("Type exports", () => {
|
||||
it("pipeline types are importable and functional", () => {
|
||||
expect(STAGE_ORDER).toHaveLength(7);
|
||||
expect(STAGE_ORDER).toHaveLength(8);
|
||||
expect(getNextStage("specify")).toBe("clarify");
|
||||
const state = createInitialPipelineState("/tmp/test");
|
||||
expect(state.current_stage).toBe("specify");
|
||||
@@ -40,6 +40,6 @@ describe("Type exports", () => {
|
||||
});
|
||||
|
||||
it("config defaults are importable", () => {
|
||||
expect(DEFAULT_CI_CONFIG.autonomy.level).toBe("full");
|
||||
expect(DEFAULT_CIAGENT_CONFIG.autonomy.level).toBe("full");
|
||||
});
|
||||
});
|
||||
@@ -8,13 +8,14 @@ import {
|
||||
} from "../types/pipeline.js";
|
||||
|
||||
describe("STAGE_ORDER", () => {
|
||||
it("has 7 stages in correct order", () => {
|
||||
it("has 8 stages in correct order", () => {
|
||||
expect(STAGE_ORDER).toEqual([
|
||||
"specify",
|
||||
"clarify",
|
||||
"research",
|
||||
"plan",
|
||||
"execute",
|
||||
"test",
|
||||
"verify",
|
||||
"complete",
|
||||
]);
|
||||
@@ -27,7 +28,8 @@ describe("getNextStage", () => {
|
||||
expect(getNextStage("clarify")).toBe("research");
|
||||
expect(getNextStage("research")).toBe("plan");
|
||||
expect(getNextStage("plan")).toBe("execute");
|
||||
expect(getNextStage("execute")).toBe("verify");
|
||||
expect(getNextStage("execute")).toBe("test");
|
||||
expect(getNextStage("test")).toBe("verify");
|
||||
expect(getNextStage("verify")).toBe("complete");
|
||||
});
|
||||
|
||||
@@ -51,6 +53,7 @@ describe("createInitialPipelineState", () => {
|
||||
expect(state.research_completed).toBe(false);
|
||||
expect(state.plan_completed).toBe(false);
|
||||
expect(state.execute_completed).toBe(false);
|
||||
expect(state.test_completed).toBe(false);
|
||||
expect(state.verify_completed).toBe(false);
|
||||
expect(state.errors).toHaveLength(0);
|
||||
expect(state.started_at).toBeTruthy();
|
||||
|
||||
@@ -6,6 +6,7 @@ export type PipelineStage =
|
||||
| "research"
|
||||
| "plan"
|
||||
| "execute"
|
||||
| "test"
|
||||
| "verify"
|
||||
| "complete";
|
||||
|
||||
@@ -19,6 +20,7 @@ export interface PipelineState {
|
||||
research_completed: boolean;
|
||||
plan_completed: boolean;
|
||||
execute_completed: boolean;
|
||||
test_completed: boolean;
|
||||
verify_completed: boolean;
|
||||
errors: PipelineError[];
|
||||
started_at: string;
|
||||
@@ -61,6 +63,7 @@ export const STAGE_ORDER: PipelineStage[] = [
|
||||
"research",
|
||||
"plan",
|
||||
"execute",
|
||||
"test",
|
||||
"verify",
|
||||
"complete",
|
||||
];
|
||||
@@ -84,6 +87,7 @@ export function createInitialPipelineState(
|
||||
research_completed: false,
|
||||
plan_completed: false,
|
||||
execute_completed: false,
|
||||
test_completed: false,
|
||||
verify_completed: false,
|
||||
errors: [],
|
||||
started_at: new Date().toISOString(),
|
||||
|
||||
@@ -7,7 +7,7 @@ describe("file utilities", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-file-test-"));
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-file-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -115,8 +115,8 @@ describe("file utilities", () => {
|
||||
expect(getProjectRoot(path.join(tempDir, "subdir"))).toBe(tempDir);
|
||||
});
|
||||
|
||||
it("finds project root with .ci directory", () => {
|
||||
fs.mkdirSync(path.join(tempDir, ".ci"));
|
||||
it("finds project root with .ciagent directory", () => {
|
||||
fs.mkdirSync(path.join(tempDir, ".ciagent"));
|
||||
expect(getProjectRoot(path.join(tempDir, "nested", "dir"))).toBe(tempDir);
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ export function copyFile(src: string, dest: string): void {
|
||||
export function getProjectRoot(startPath?: string): string {
|
||||
let current = startPath || process.cwd();
|
||||
while (current !== path.dirname(current)) {
|
||||
if (fs.existsSync(path.join(current, ".ci"))) return current;
|
||||
if (fs.existsSync(path.join(current, ".ciagent"))) return current;
|
||||
if (fs.existsSync(path.join(current, ".git"))) return current;
|
||||
if (fs.existsSync(path.join(current, "package.json"))) return current;
|
||||
current = path.dirname(current);
|
||||
|
||||
@@ -7,7 +7,7 @@ describe("BehavioralVerification", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-behavioral-test-"));
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-behavioral-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -50,7 +50,7 @@ describe("BehavioralVerification", () => {
|
||||
});
|
||||
|
||||
it("passes with REQUIREMENTS.md", async () => {
|
||||
const ciDir = path.join(tempDir, ".ci");
|
||||
const ciDir = path.join(tempDir, ".ciagent");
|
||||
fs.mkdirSync(ciDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(ciDir, "REQUIREMENTS.md"), "# Requirements\n\n| REQ-ID | Requirement | Priority | Phase | Status |\n|--------|-------------|----------|-------|--------|\n| REQ-01 | Must have auth | P0 | 1 | pending |\n");
|
||||
|
||||
@@ -62,7 +62,7 @@ describe("BehavioralVerification", () => {
|
||||
});
|
||||
|
||||
it("skips when no REQUIREMENTS.md or PROJECT.md", async () => {
|
||||
const ciDir = path.join(tempDir, ".ci");
|
||||
const ciDir = path.join(tempDir, ".ciagent");
|
||||
fs.mkdirSync(ciDir, { recursive: true });
|
||||
|
||||
const verifier = new BehavioralVerification();
|
||||
@@ -73,7 +73,7 @@ describe("BehavioralVerification", () => {
|
||||
});
|
||||
|
||||
it("passes with PROJECT.md when no REQUIREMENTS.md", async () => {
|
||||
const ciDir = path.join(tempDir, ".ci");
|
||||
const ciDir = path.join(tempDir, ".ciagent");
|
||||
fs.mkdirSync(ciDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(ciDir, "PROJECT.md"), "# Test\n\n## What This Is\nBuild it\n\n## Requirements\n\n### Active\n\n- [ ] Must have auth\n- [ ] Shall support CRUD\n");
|
||||
|
||||
|
||||
@@ -108,8 +108,8 @@ export class BehavioralVerification extends VerificationLayer {
|
||||
}
|
||||
|
||||
private checkSpecificationRequirements(projectPath: string): VerificationCheck {
|
||||
const reqPath = path.join(projectPath, ".ci", "REQUIREMENTS.md");
|
||||
const projectPath_md = path.join(projectPath, ".ci", "PROJECT.md");
|
||||
const reqPath = path.join(projectPath, ".ciagent", "REQUIREMENTS.md");
|
||||
const projectPath_md = path.join(projectPath, ".ciagent", "PROJECT.md");
|
||||
|
||||
const specPath = reqPath;
|
||||
if (!fs.existsSync(specPath)) {
|
||||
@@ -189,7 +189,7 @@ export class BehavioralVerification extends VerificationLayer {
|
||||
private checkPlanMustHaves(projectPath: string, phase: number): VerificationCheck {
|
||||
const roadmapPath = path.join(
|
||||
projectPath,
|
||||
".ci",
|
||||
".ciagent",
|
||||
"ROADMAP.md"
|
||||
);
|
||||
|
||||
@@ -252,7 +252,7 @@ export class BehavioralVerification extends VerificationLayer {
|
||||
ciBlockRegex.lastIndex = 0;
|
||||
}
|
||||
|
||||
const reqPath = path.join(projectPath, ".ci", "REQUIREMENTS.md");
|
||||
const reqPath = path.join(projectPath, ".ciagent", "REQUIREMENTS.md");
|
||||
if (!fs.existsSync(reqPath)) {
|
||||
return this.check(
|
||||
"Requirement test coverage via git log",
|
||||
|
||||
@@ -7,8 +7,8 @@ describe("VerificationPipeline", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-pipeline-test-"));
|
||||
const ciDir = path.join(tempDir, ".ci");
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-pipeline-test-"));
|
||||
const ciDir = path.join(tempDir, ".ciagent");
|
||||
fs.mkdirSync(ciDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(ciDir, "config.json"), JSON.stringify({ autonomy: { level: "full" } }));
|
||||
fs.writeFileSync(path.join(ciDir, "specification.md"), "# Test\n## Objective\nBuild it\n\n## Requirements\n- Feature A\n");
|
||||
|
||||
@@ -7,7 +7,7 @@ describe("QualityVerification", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-quality-test-"));
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-quality-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -7,7 +7,7 @@ describe("SecurityVerification", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-security-test-"));
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-security-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -7,28 +7,28 @@ describe("StructuralVerification", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-structural-test-"));
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-structural-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function setupProjectStructure(hasCIDir = true, hasRoadmap = true, hasCIConfig = true, hasSpec = true) {
|
||||
function setupProjectStructure(hasCIDir = true, hasRoadmap = true, hasCIAgentConfig = true, hasSpec = true) {
|
||||
if (hasCIDir) {
|
||||
const ciDir = path.join(tempDir, ".ci");
|
||||
const ciDir = path.join(tempDir, ".ciagent");
|
||||
fs.mkdirSync(ciDir, { recursive: true });
|
||||
if (hasRoadmap) {
|
||||
fs.writeFileSync(path.join(ciDir, "ROADMAP.md"), "# Roadmap\n\n## Phases\n\n### Phase 1: Init\n**Goal**: Set up project\n**Status**: not_started\n");
|
||||
}
|
||||
}
|
||||
if (hasCIConfig) {
|
||||
const ciDir = path.join(tempDir, ".ci");
|
||||
if (hasCIAgentConfig) {
|
||||
const ciDir = path.join(tempDir, ".ciagent");
|
||||
fs.mkdirSync(ciDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(ciDir, "config.json"), JSON.stringify({ autonomy: { level: "full" } }, null, 2));
|
||||
}
|
||||
if (hasSpec) {
|
||||
const specDir = path.join(tempDir, ".ci");
|
||||
const specDir = path.join(tempDir, ".ciagent");
|
||||
if (!fs.existsSync(specDir)) fs.mkdirSync(specDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(specDir, "specification.md"), "# Project\n## Objective\nBuild a REST API for task management\n\n## Requirements\n- User auth\n- CRUD\n");
|
||||
}
|
||||
@@ -43,13 +43,13 @@ describe("StructuralVerification", () => {
|
||||
expect(result.name).toBe("Structural");
|
||||
expect(result.checks.length).toBeGreaterThan(0);
|
||||
|
||||
const phaseDirCheck = result.checks.find((c) => c.name === ".ci directory exists");
|
||||
const phaseDirCheck = result.checks.find((c) => c.name === ".ciagent directory exists");
|
||||
expect(phaseDirCheck?.status).toBe("pass");
|
||||
|
||||
const planCheck = result.checks.find((c) => c.name === "ROADMAP.md exists");
|
||||
expect(planCheck?.status).toBe("pass");
|
||||
|
||||
const configCheck = result.checks.find((c) => c.name === "CI config valid");
|
||||
const configCheck = result.checks.find((c) => c.name === "CIAgent config valid");
|
||||
expect(configCheck?.status).toBe("pass");
|
||||
|
||||
const specCheck = result.checks.find((c) => c.name === "Specification exists");
|
||||
@@ -61,7 +61,7 @@ describe("StructuralVerification", () => {
|
||||
const verifier = new StructuralVerification();
|
||||
const result = await verifier.verify(tempDir, 1);
|
||||
|
||||
const phaseDirCheck = result.checks.find((c) => c.name === ".ci directory exists");
|
||||
const phaseDirCheck = result.checks.find((c) => c.name === ".ciagent directory exists");
|
||||
expect(phaseDirCheck?.status).toBe("fail");
|
||||
});
|
||||
|
||||
@@ -75,15 +75,15 @@ describe("StructuralVerification", () => {
|
||||
});
|
||||
|
||||
it("fails when CI config has invalid JSON", async () => {
|
||||
const ciDir = path.join(tempDir, ".ci");
|
||||
const ciDir = path.join(tempDir, ".ciagent");
|
||||
fs.mkdirSync(ciDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(ciDir, "config.json"), "not valid json{{{");
|
||||
fs.mkdirSync(path.join(tempDir, ".ci"), { recursive: true });
|
||||
fs.mkdirSync(path.join(tempDir, ".ciagent"), { recursive: true });
|
||||
|
||||
const verifier = new StructuralVerification();
|
||||
const result = await verifier.verify(tempDir, 1);
|
||||
|
||||
const configCheck = result.checks.find((c) => c.name === "CI config valid");
|
||||
const configCheck = result.checks.find((c) => c.name === "CIAgent config valid");
|
||||
expect(configCheck?.status).toBe("fail");
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ describe("StructuralVerification", () => {
|
||||
const srcDir = path.join(tempDir, "src");
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, "app.ts"), "export function main() { /* TODO: implement */ }");
|
||||
const ciDir = path.join(tempDir, ".ci");
|
||||
const ciDir = path.join(tempDir, ".ciagent");
|
||||
fs.mkdirSync(ciDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(ciDir, "config.json"), "{}");
|
||||
fs.writeFileSync(path.join(ciDir, "specification.md"), "# Test\n## Objective\nBuild it");
|
||||
|
||||
@@ -23,7 +23,7 @@ export class StructuralVerification extends VerificationLayer {
|
||||
|
||||
checks.push(this.checkPhaseDir(projectPath, phase));
|
||||
checks.push(this.checkPlanExists(projectPath, phase));
|
||||
checks.push(this.checkCIConfig(projectPath));
|
||||
checks.push(this.checkCIAgentConfig(projectPath));
|
||||
checks.push(this.checkSpecification(projectPath));
|
||||
checks.push(this.checkNoStubs(projectPath));
|
||||
checks.push(this.checkImportsWired(projectPath));
|
||||
@@ -41,47 +41,47 @@ export class StructuralVerification extends VerificationLayer {
|
||||
}
|
||||
|
||||
private checkPhaseDir(projectPath: string, phase: number) {
|
||||
const ciDir = path.join(projectPath, ".ci");
|
||||
const ciDir = path.join(projectPath, ".ciagent");
|
||||
const exists = fs.existsSync(ciDir);
|
||||
return this.check(
|
||||
".ci directory exists",
|
||||
".ciagent directory exists",
|
||||
exists ? "pass" : "fail",
|
||||
exists ? ".ci directory found" : ".ci directory not found",
|
||||
exists ? ".ciagent directory found" : ".ciagent directory not found",
|
||||
ciDir
|
||||
);
|
||||
}
|
||||
|
||||
private checkPlanExists(projectPath: string, phase: number) {
|
||||
const roadmapPath = path.join(projectPath, ".ci", "ROADMAP.md");
|
||||
const roadmapPath = path.join(projectPath, ".ciagent", "ROADMAP.md");
|
||||
const exists = fs.existsSync(roadmapPath);
|
||||
return this.check(
|
||||
"ROADMAP.md exists",
|
||||
exists ? "pass" : "warning",
|
||||
exists ? "ROADMAP.md found" : "ROADMAP.md not found (run 'ci init' first)",
|
||||
exists ? "ROADMAP.md found" : "ROADMAP.md not found (run 'ciagent init' first)",
|
||||
roadmapPath
|
||||
);
|
||||
}
|
||||
|
||||
private checkCIConfig(projectPath: string) {
|
||||
const configPath = path.join(projectPath, ".ci", "config.json");
|
||||
private checkCIAgentConfig(projectPath: string) {
|
||||
const configPath = path.join(projectPath, ".ciagent", "config.json");
|
||||
const exists = fs.existsSync(configPath);
|
||||
if (!exists) {
|
||||
return this.check("CI config exists", "fail", ".ci/config.json not found", configPath);
|
||||
return this.check("CIAgent config exists", "fail", ".ciagent/config.json not found", configPath);
|
||||
}
|
||||
try {
|
||||
const content = fs.readFileSync(configPath, "utf-8");
|
||||
JSON.parse(content);
|
||||
return this.check("CI config valid", "pass", ".ci/config.json is valid JSON");
|
||||
return this.check("CIAgent config valid", "pass", ".ciagent/config.json is valid JSON");
|
||||
} catch (e) {
|
||||
return this.check("CI config valid", "fail", `.ci/config.json has invalid JSON: ${(e as Error).message}`);
|
||||
return this.check("CIAgent config valid", "fail", `.ciagent/config.json has invalid JSON: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private checkSpecification(projectPath: string) {
|
||||
const specPath = path.join(projectPath, ".ci", "specification.md");
|
||||
const specPath = path.join(projectPath, ".ciagent", "specification.md");
|
||||
const exists = fs.existsSync(specPath);
|
||||
if (!exists) {
|
||||
return this.check("Specification exists", "warning", ".ci/specification.md not found — specification may not be loaded yet");
|
||||
return this.check("Specification exists", "warning", ".ciagent/specification.md not found — specification may not be loaded yet");
|
||||
}
|
||||
const content = fs.readFileSync(specPath, "utf-8");
|
||||
if (content.trim().length < 10) {
|
||||
|
||||
Reference in New Issue
Block a user