refactor(rebrand): rename & rebrand CI → CIAgent across all source and test files

- Type renames: CIConfig → CIAgentConfig, DEFAULT_CI_CONFIG → DEFAULT_CIAGENT_CONFIG
- Type renames: CiMetadata → CIAgentMetadata, ParsedCiCommit → ParsedCIAgentCommit
- Function renames: initCI → initCIAgent, isCIInitialized → isCIAgentInitialized
- Function renames: extractCiBlock → extractCIAgentBlock, parseCiBlock → parseCIAgentBlock
- Class renames: CiFiles → CIAgentFiles
- Import paths: ci-files.js → ciagent-files.js
- Directory paths: .ci/ → .ciagent/ across all source and test files
- Check names: ".ci directory exists" → ".ciagent directory exists"
- Check names: "CI config valid" → "CIAgent config valid"
- Temp dir names: ci-*-test- → ciagent-*-test-
- CLI examples: "ci init" → "ciagent init"
- Fix deepMerge infinite recursion bug in config.ts
- ---ci---/---/ci--- block markers preserved unchanged
- All 31 test suites, 370 tests passing

---ci---
phase: 1
milestone: v0.5
plan: 07
task: 07-01-01
status: execute
---/ci---
This commit is contained in:
Jon Chery
2026-05-29 18:01:13 +00:00
parent e31afe3b59
commit 4a58aa1657
51 changed files with 505 additions and 465 deletions
+7 -7
View File
@@ -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");
+1 -1
View File
@@ -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;
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 = [];
+22 -22
View File
@@ -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
View File
@@ -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",
+22 -22
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+5 -5
View File
@@ -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(
+3 -3
View File
@@ -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;
+4 -4
View File
@@ -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(() => {
+3 -3
View File
@@ -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;
+4 -4
View File
@@ -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);
});
+4 -4
View File
@@ -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);
+1 -1
View File
@@ -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" });
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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";