Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a416413c7d |
@@ -53,7 +53,7 @@ src/
|
||||
security.ts # Layer 3: regex-based threat pattern scanning (no STRIDE analysis yet)
|
||||
quality.ts # Layer 4: regex-based code quality checks (no multi-persona review yet)
|
||||
index.ts # Public API exports
|
||||
version.ts # VERSION = "0.6.0"
|
||||
version.ts # VERSION = "0.7.0"
|
||||
templates/ # Template files (config.json, DECISIONS.md, specification.md)
|
||||
```
|
||||
|
||||
@@ -82,10 +82,10 @@ templates/ # Template files (config.json, DECISIONS.md, specification.md
|
||||
## Pipeline Flow
|
||||
|
||||
```
|
||||
SPECIFY → CLARIFY → RESEARCH → PLAN → EXECUTE → VERIFY → COMPLETE
|
||||
SPECIFY → CLARIFY → RESEARCH → PLAN → EXECUTE → TEST → VERIFY → COMPLETE
|
||||
```
|
||||
|
||||
Each stage is executed by `OrchestratorAgent.executeStage()`. The orchestrator delegates intelligent stages (research, plan, execute, verify) to specialized agents via `context.backend` when available, falling back to mechanical execution when no backend is configured. Mechanical stages (specify, clarify, complete) are always handled by the orchestrator directly.
|
||||
Each stage is executed by `OrchestratorAgent.executeStage()`. The orchestrator delegates intelligent stages (research, plan, execute, test, verify) to specialized agents via `context.backend` when available, falling back to mechanical execution when no backend is configured. Mechanical stages (specify, clarify, complete) are always handled by the orchestrator directly.
|
||||
|
||||
## Intelligence Backend Architecture
|
||||
|
||||
@@ -191,7 +191,7 @@ IntelligenceBackend (unified interface)
|
||||
|
||||
## Current State
|
||||
|
||||
- **v0.6.0**: Backends module (OllamaLocal, OllamaCloud, Opencode), learnship references removed, verification layers migrated from .planning/ to .ciagent/
|
||||
- **v0.7.0**: Backends module (OllamaLocal, OllamaCloud, Opencode), learnship references removed, verification layers migrated from .planning/ to .ciagent/
|
||||
- **New modules**: commit-parser (`---ci---` YAML block extraction/parsing), commit-builder (structured commit message generation), git-context (project state reconstruction from git log + branches), git-branch (phase/milestone branch lifecycle), ciagent-files (`.ciagent/` long-lived reference file management)
|
||||
- **Commit schema**: Every CIAgent-generated commit contains a `---ci---` YAML block with phase, milestone, status, decisions, escalations, requirements, lessons, and compound metadata
|
||||
- **Branch strategy**: `phase/NN-slug` and `milestone/vX.X-slug` branches encode project structure; merged = complete, active = in progress
|
||||
|
||||
@@ -211,9 +211,9 @@ CIAgent uses `.ciagent/config.json` for project configuration:
|
||||
### Pipeline
|
||||
|
||||
```
|
||||
SPECIFY → CLARIFY → RESEARCH → PLAN → EXECUTE → VERIFY → COMPLETE
|
||||
↕ ↕ ↕ ↕
|
||||
(questions) (auto-decide) (auto-run) (auto-verify)
|
||||
SPECIFY → CLARIFY → RESEARCH → PLAN → EXECUTE → TEST → VERIFY → COMPLETE
|
||||
↕ ↕ ↕ ↕ ↕
|
||||
(questions) (auto-decide) (auto-run) (auto-test) (auto-verify)
|
||||
```
|
||||
|
||||
### Git-Native Core Modules
|
||||
@@ -235,7 +235,7 @@ Every autonomous decision is classified by confidence:
|
||||
|
||||
Decisions are committed to git as `decision` type commits. The audit trail is `git log --grep="decisions:"`.
|
||||
|
||||
### 18 Agents
|
||||
### 19 Agents
|
||||
|
||||
| Agent | Role | CIAgent Modification |
|
||||
|-------|------|----------------|
|
||||
@@ -244,17 +244,18 @@ Decisions are committed to git as `decision` type commits. The audit trail is `g
|
||||
| executor | Task execution | Never pauses for checkpoints |
|
||||
| verifier | Output verification | Generates automated tests, not human UAT |
|
||||
| researcher | Domain research | Logs assumptions, never flags for human |
|
||||
| tester | Integration/e2e tests | Detects and runs existing test files, never writes tests |
|
||||
| challenger | Plan stress-testing | Binding verdicts, only escalates <0.60 |
|
||||
| security-auditor | Security audit | Auto-dispositions threats |
|
||||
| debugger | Bug fixing | Auto-fixes when confidence > threshold |
|
||||
| Others | Various | Retained from Learnship |
|
||||
| Others | Various | Delegates to active intelligence backend |
|
||||
|
||||
### Verification Layers
|
||||
|
||||
1. **Structural**: File existence, import/export wiring, no stubs
|
||||
2. **Behavioral**: Generated automated tests for must-haves
|
||||
3. **Security**: STRIDE analysis with auto-disposition
|
||||
4. **Code Quality**: Multi-persona review with P0 auto-fix
|
||||
2. **Behavioral**: Test infrastructure and requirement traceability (partially implemented — static analysis, no test generation yet)
|
||||
3. **Security**: Regex-based threat pattern scanning with auto-disposition (partially implemented — no STRIDE analysis yet)
|
||||
4. **Code Quality**: Regex-based code quality checks (partially implemented — no multi-persona review yet)
|
||||
|
||||
## Specification Format
|
||||
|
||||
@@ -292,9 +293,9 @@ Each escalation is committed as an `escalation` type commit. Resolved escalation
|
||||
|
||||
## Current Limitations
|
||||
|
||||
- **Agent implementations are stubs**: All 18 agents return success immediately. Real LLM-based agent implementations are needed for research, planning, execution, and verification.
|
||||
- **Agent implementations**: 5 core agents have intrinsic logic (planner, executor, verifier, researcher, tester); 13 agents delegate to backends. Full LLM-powered agent behavior requires an intelligence backend.
|
||||
- **Package not published to npm**: Install from source only until a publishing pipeline is configured.
|
||||
- **Behavioral/Security/Quality verification layers**: Structural verification is fully implemented; behavioral, security, and quality layers are partially stubbed.
|
||||
- **Behavioral/Security/Quality verification layers**: Partially implemented — structural verification is complete; behavioral does static analysis; security does regex-based threat scanning; quality does regex-based code quality checks.
|
||||
|
||||
## Differences from Learnship
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
0.5.0
|
||||
0.7.0
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@continuous-intelligence/ciagent",
|
||||
"version": "0.5.0",
|
||||
"version": "0.7.0",
|
||||
"description": "Fully autonomous AI-driven software engineering harness - Continuous Intelligence",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
|
||||
export class DocWriterAgent extends BaseAgent {
|
||||
readonly name = "doc-writer";
|
||||
readonly description = "Autonomous documentation writer. No behavioral changes from Learnship.";
|
||||
readonly description = "Autonomous documentation writer.";
|
||||
readonly workflow = "execute";
|
||||
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { ExecutorAgent } from "../agents/executor.js";
|
||||
import { AgentContext } from "../agents/base.js";
|
||||
import { IntelligenceBackend, BackendRequest, BackendResult } from "../backends/types.js";
|
||||
import { emptyTokenUsage } from "../backends/types.js";
|
||||
|
||||
class MockBackend implements IntelligenceBackend {
|
||||
readonly name = "mock";
|
||||
readonly type = "llm" as const;
|
||||
async isAvailable(): Promise<boolean> { return true; }
|
||||
async execute(request: BackendRequest): Promise<BackendResult> {
|
||||
return {
|
||||
success: true,
|
||||
output: `Mock backend executed: ${request.task.slice(0, 50)}`,
|
||||
artifacts: [],
|
||||
decisions: [],
|
||||
escalations: [],
|
||||
usage: emptyTokenUsage(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createTempDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-executor-test-"));
|
||||
}
|
||||
|
||||
function cleanup(dir: string): void {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function makeContext(dir: string, backend?: IntelligenceBackend): AgentContext {
|
||||
return {
|
||||
project_path: dir,
|
||||
phase: 1,
|
||||
stage: "execute",
|
||||
specification: "Build a REST API for task management",
|
||||
config_path: path.join(dir, ".ciagent", "config.json"),
|
||||
backend,
|
||||
};
|
||||
}
|
||||
|
||||
describe("ExecutorAgent", () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = createTempDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(dir);
|
||||
});
|
||||
|
||||
it("returns honest failure without backend", async () => {
|
||||
const executor = new ExecutorAgent();
|
||||
const result = await executor.execute(makeContext(dir));
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain("intelligence backend");
|
||||
});
|
||||
|
||||
it("delegates to backend when available", async () => {
|
||||
const mockBackend = new MockBackend();
|
||||
const executor = new ExecutorAgent();
|
||||
const result = await executor.execute(makeContext(dir, mockBackend));
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain("Mock backend executed");
|
||||
});
|
||||
|
||||
it("has correct agent name", () => {
|
||||
const executor = new ExecutorAgent();
|
||||
expect(executor.name).toBe("executor");
|
||||
});
|
||||
|
||||
it("has correct workflow", () => {
|
||||
const executor = new ExecutorAgent();
|
||||
expect(executor.workflow).toBe("execute");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { PlannerAgent } from "../agents/planner.js";
|
||||
import { AgentContext } from "../agents/base.js";
|
||||
import { IntelligenceBackend, BackendRequest, BackendResult } from "../backends/types.js";
|
||||
import { Decision } from "../types/decisions.js";
|
||||
import { Escalation } from "../types/escalation.js";
|
||||
import { emptyTokenUsage } from "../backends/types.js";
|
||||
|
||||
class MockBackend implements IntelligenceBackend {
|
||||
readonly name = "mock";
|
||||
readonly type = "llm" as const;
|
||||
async isAvailable(): Promise<boolean> { return true; }
|
||||
async execute(request: BackendRequest): Promise<BackendResult> {
|
||||
return {
|
||||
success: true,
|
||||
output: `Mock backend executed: ${request.task.slice(0, 50)}`,
|
||||
artifacts: [],
|
||||
decisions: [],
|
||||
escalations: [],
|
||||
usage: emptyTokenUsage(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createTempDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-planner-test-"));
|
||||
}
|
||||
|
||||
function cleanup(dir: string): void {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function makeContext(dir: string, backend?: IntelligenceBackend): AgentContext {
|
||||
return {
|
||||
project_path: dir,
|
||||
phase: 1,
|
||||
stage: "plan",
|
||||
specification: "Build a REST API for task management",
|
||||
config_path: path.join(dir, ".ciagent", "config.json"),
|
||||
backend,
|
||||
};
|
||||
}
|
||||
|
||||
function setupCIAgentDir(dir: string): void {
|
||||
const ciDir = path.join(dir, ".ciagent");
|
||||
fs.mkdirSync(ciDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(ciDir, "config.json"), "{}");
|
||||
}
|
||||
|
||||
function writeRequirementsMd(dir: string): void {
|
||||
const ciDir = path.join(dir, ".ciagent");
|
||||
const content = [
|
||||
"# Requirements",
|
||||
"",
|
||||
"## v1 Requirements",
|
||||
"",
|
||||
"### Core",
|
||||
"",
|
||||
"- [ ] **REQ-01**: User authentication",
|
||||
"- [ ] **REQ-02**: Task CRUD operations",
|
||||
"- [ ] **REQ-03**: Real-time notifications",
|
||||
"",
|
||||
"## Traceability",
|
||||
"",
|
||||
"| Requirement | Phase | Status |",
|
||||
"|-------------|-------|--------|",
|
||||
"| REQ-01 | Phase 1 | in_progress |",
|
||||
"| REQ-02 | Phase 1 | pending |",
|
||||
"| REQ-03 | Phase 1 | blocked |",
|
||||
].join("\n");
|
||||
fs.writeFileSync(path.join(ciDir, "REQUIREMENTS.md"), content);
|
||||
}
|
||||
|
||||
function writeRoadmapMd(dir: string): void {
|
||||
const ciDir = path.join(dir, ".ciagent");
|
||||
const content = [
|
||||
"# Roadmap",
|
||||
"",
|
||||
"## Overview",
|
||||
"",
|
||||
"Task management API roadmap",
|
||||
"",
|
||||
"## Phases",
|
||||
"",
|
||||
"- [ ] **Phase 1: Authentication** - Implement auth",
|
||||
"",
|
||||
"## Phase Details",
|
||||
"",
|
||||
"### Phase 1: Authentication",
|
||||
"**Goal**: Implement user authentication",
|
||||
"**Depends on**: Nothing",
|
||||
"**Requirements**: REQ-01, REQ-02",
|
||||
"**Success Criteria**:",
|
||||
"1. .ciagent/REQUIREMENTS.md exists",
|
||||
"**Status**: in_progress",
|
||||
].join("\n");
|
||||
fs.writeFileSync(path.join(ciDir, "ROADMAP.md"), content);
|
||||
}
|
||||
|
||||
describe("PlannerAgent", () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = createTempDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(dir);
|
||||
});
|
||||
|
||||
it("returns honest failure without backend when no requirements or roadmap", async () => {
|
||||
setupCIAgentDir(dir);
|
||||
const planner = new PlannerAgent();
|
||||
const result = await planner.execute(makeContext(dir));
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain("No requirements or roadmap");
|
||||
});
|
||||
|
||||
it("creates PLAN.md from REQUIREMENTS.md without backend", async () => {
|
||||
setupCIAgentDir(dir);
|
||||
writeRequirementsMd(dir);
|
||||
writeRoadmapMd(dir);
|
||||
|
||||
const planner = new PlannerAgent();
|
||||
const result = await planner.execute(makeContext(dir));
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain("plan");
|
||||
expect(fs.existsSync(path.join(dir, ".ciagent", "PLAN.md"))).toBe(true);
|
||||
});
|
||||
|
||||
it("PLAN.md contains phase goal and tasks", async () => {
|
||||
setupCIAgentDir(dir);
|
||||
writeRequirementsMd(dir);
|
||||
writeRoadmapMd(dir);
|
||||
|
||||
const planner = new PlannerAgent();
|
||||
await planner.execute(makeContext(dir));
|
||||
|
||||
const planContent = fs.readFileSync(path.join(dir, ".ciagent", "PLAN.md"), "utf-8");
|
||||
expect(planContent).toContain("Phase 1 Plan");
|
||||
expect(planContent).toContain("Phase Goal");
|
||||
expect(planContent).toContain("Tasks");
|
||||
});
|
||||
|
||||
it("delegates to backend when available", async () => {
|
||||
setupCIAgentDir(dir);
|
||||
const mockBackend = new MockBackend();
|
||||
const planner = new PlannerAgent();
|
||||
const result = await planner.execute(makeContext(dir, mockBackend));
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain("Mock backend executed");
|
||||
});
|
||||
|
||||
it("has correct agent name", () => {
|
||||
const planner = new PlannerAgent();
|
||||
expect(planner.name).toBe("planner");
|
||||
});
|
||||
|
||||
it("has correct workflow", () => {
|
||||
const planner = new PlannerAgent();
|
||||
expect(planner.workflow).toBe("plan");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,208 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { ResearcherAgent } from "../agents/researcher.js";
|
||||
import { AgentContext } from "../agents/base.js";
|
||||
import { IntelligenceBackend, BackendRequest, BackendResult } from "../backends/types.js";
|
||||
import { emptyTokenUsage } from "../backends/types.js";
|
||||
|
||||
class MockBackend implements IntelligenceBackend {
|
||||
readonly name = "mock";
|
||||
readonly type = "llm" as const;
|
||||
async isAvailable(): Promise<boolean> { return true; }
|
||||
async execute(request: BackendRequest): Promise<BackendResult> {
|
||||
return {
|
||||
success: true,
|
||||
output: `Mock backend executed: ${request.task.slice(0, 50)}`,
|
||||
artifacts: [],
|
||||
decisions: [],
|
||||
escalations: [],
|
||||
usage: emptyTokenUsage(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createTempDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-researcher-test-"));
|
||||
}
|
||||
|
||||
function cleanup(dir: string): void {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function makeContext(dir: string, backend?: IntelligenceBackend): AgentContext {
|
||||
return {
|
||||
project_path: dir,
|
||||
phase: 1,
|
||||
stage: "research",
|
||||
specification: "Build a REST API for task management",
|
||||
config_path: path.join(dir, ".ciagent", "config.json"),
|
||||
backend,
|
||||
};
|
||||
}
|
||||
|
||||
function setupCIAgentDir(dir: string): void {
|
||||
const ciDir = path.join(dir, ".ciagent");
|
||||
fs.mkdirSync(ciDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(ciDir, "config.json"), '{"projects":[],"active_project":""}');
|
||||
}
|
||||
|
||||
function writeProjectMd(dir: string): void {
|
||||
const ciDir = path.join(dir, ".ciagent");
|
||||
const content = [
|
||||
"# Task API",
|
||||
"",
|
||||
"## What This Is",
|
||||
"",
|
||||
"A REST API for managing tasks",
|
||||
"",
|
||||
"## Requirements",
|
||||
"",
|
||||
"### Validated",
|
||||
"",
|
||||
"- ✓ User authentication",
|
||||
"",
|
||||
"### Active",
|
||||
"",
|
||||
"- [ ] Task CRUD",
|
||||
"",
|
||||
"### Out of Scope",
|
||||
"",
|
||||
"- Admin dashboard",
|
||||
"",
|
||||
"## Context",
|
||||
"",
|
||||
"Node.js project",
|
||||
"",
|
||||
"## Constraints",
|
||||
"",
|
||||
"- Must use Node.js",
|
||||
"",
|
||||
"## Key Decisions",
|
||||
"",
|
||||
"| Decision | Rationale | Outcome |",
|
||||
"|----------|-----------|---------|",
|
||||
].join("\n");
|
||||
fs.writeFileSync(path.join(ciDir, "PROJECT.md"), content);
|
||||
}
|
||||
|
||||
function writeArchitectureMd(dir: string): void {
|
||||
const ciDir = path.join(dir, ".ciagent");
|
||||
const content = [
|
||||
"# Architecture",
|
||||
"",
|
||||
"## Overview",
|
||||
"",
|
||||
"Task management system architecture",
|
||||
"",
|
||||
"## Components",
|
||||
"",
|
||||
"### Core",
|
||||
"- **Description**: Core module",
|
||||
"- **Boundaries**: src/core/ — internal module",
|
||||
"- **Depends on**: None",
|
||||
"",
|
||||
"## Data Flow",
|
||||
"",
|
||||
"Request → Handler → Service → Database",
|
||||
"",
|
||||
"## Build Order",
|
||||
"",
|
||||
"1. Build core module",
|
||||
].join("\n");
|
||||
fs.writeFileSync(path.join(ciDir, "ARCHITECTURE.md"), content);
|
||||
}
|
||||
|
||||
function setupSourceDir(dir: string): void {
|
||||
const srcDir = path.join(dir, "src");
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
fs.mkdirSync(path.join(srcDir, "core"), { recursive: true });
|
||||
fs.mkdirSync(path.join(srcDir, "agents"), { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, "core", "index.ts"), "export {};\n");
|
||||
fs.writeFileSync(path.join(srcDir, "agents", "base.ts"), "export {};\n");
|
||||
}
|
||||
|
||||
describe("ResearcherAgent", () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = createTempDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(dir);
|
||||
});
|
||||
|
||||
it("reads .ciagent/ files without backend", async () => {
|
||||
setupCIAgentDir(dir);
|
||||
writeProjectMd(dir);
|
||||
writeArchitectureMd(dir);
|
||||
|
||||
const researcher = new ResearcherAgent();
|
||||
const result = await researcher.execute(makeContext(dir));
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain("findingsCount");
|
||||
});
|
||||
|
||||
it("only modifies .ciagent/ files", async () => {
|
||||
setupCIAgentDir(dir);
|
||||
writeProjectMd(dir);
|
||||
writeArchitectureMd(dir);
|
||||
setupSourceDir(dir);
|
||||
|
||||
const srcDir = path.join(dir, "src");
|
||||
const filesBefore = new Set<string>();
|
||||
function collectFiles(d: string): void {
|
||||
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
||||
const full = path.join(d, entry.name);
|
||||
if (entry.isDirectory() && entry.name !== "node_modules") {
|
||||
collectFiles(full);
|
||||
} else {
|
||||
filesBefore.add(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
collectFiles(srcDir);
|
||||
|
||||
const researcher = new ResearcherAgent();
|
||||
await researcher.execute(makeContext(dir));
|
||||
|
||||
collectFiles(srcDir);
|
||||
for (const f of filesBefore) {
|
||||
expect(fs.existsSync(f)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("updates ARCHITECTURE.md from source scan", async () => {
|
||||
setupCIAgentDir(dir);
|
||||
writeProjectMd(dir);
|
||||
setupSourceDir(dir);
|
||||
|
||||
const researcher = new ResearcherAgent();
|
||||
const result = await researcher.execute(makeContext(dir));
|
||||
|
||||
if (result.success) {
|
||||
const parsed = JSON.parse(result.output);
|
||||
expect(parsed.filesUpdated).toContain(".ciagent/ARCHITECTURE.md");
|
||||
}
|
||||
});
|
||||
|
||||
it("delegates to backend when available", async () => {
|
||||
setupCIAgentDir(dir);
|
||||
const mockBackend = new MockBackend();
|
||||
const researcher = new ResearcherAgent();
|
||||
const result = await researcher.execute(makeContext(dir, mockBackend));
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain("Mock backend executed");
|
||||
});
|
||||
|
||||
it("has correct agent name", () => {
|
||||
const researcher = new ResearcherAgent();
|
||||
expect(researcher.name).toBe("researcher");
|
||||
});
|
||||
|
||||
it("has correct workflow", () => {
|
||||
const researcher = new ResearcherAgent();
|
||||
expect(researcher.workflow).toBe("research");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { TesterAgent } from "../agents/tester.js";
|
||||
import { AgentContext } from "../agents/base.js";
|
||||
import { IntelligenceBackend, BackendRequest, BackendResult } from "../backends/types.js";
|
||||
import { emptyTokenUsage } from "../backends/types.js";
|
||||
|
||||
class MockBackend implements IntelligenceBackend {
|
||||
readonly name = "mock";
|
||||
readonly type = "llm" as const;
|
||||
async isAvailable(): Promise<boolean> { return true; }
|
||||
async execute(request: BackendRequest): Promise<BackendResult> {
|
||||
return {
|
||||
success: true,
|
||||
output: `Mock backend executed: ${request.task.slice(0, 50)}`,
|
||||
artifacts: [],
|
||||
decisions: [],
|
||||
escalations: [],
|
||||
usage: emptyTokenUsage(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createTempDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-tester-test-"));
|
||||
}
|
||||
|
||||
function cleanup(dir: string): void {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function makeContext(dir: string, backend?: IntelligenceBackend): AgentContext {
|
||||
return {
|
||||
project_path: dir,
|
||||
phase: 1,
|
||||
stage: "test",
|
||||
specification: "Build a REST API for task management",
|
||||
config_path: path.join(dir, ".ciagent", "config.json"),
|
||||
backend,
|
||||
};
|
||||
}
|
||||
|
||||
describe("TesterAgent", () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = createTempDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(dir);
|
||||
});
|
||||
|
||||
it("detects test files when src directory exists", async () => {
|
||||
const srcDir = path.join(dir, "src");
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, "app.integration.test.ts"), "test('integration', () => {});\n");
|
||||
|
||||
const tester = new TesterAgent();
|
||||
const result = await tester.execute(makeContext(dir));
|
||||
expect(result.success).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not write test files", async () => {
|
||||
const srcDir = path.join(dir, "src");
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, "app.test.ts"), "test('unit', () => {});\n");
|
||||
|
||||
const testFilesBefore = fs.readdirSync(srcDir).filter(f => f.endsWith(".test.ts"));
|
||||
const tester = new TesterAgent();
|
||||
await tester.execute(makeContext(dir));
|
||||
const testFilesAfter = fs.readdirSync(srcDir).filter(f => f.endsWith(".test.ts"));
|
||||
expect(testFilesAfter.length).toBe(testFilesBefore.length);
|
||||
});
|
||||
|
||||
it("delegates to backend when available", async () => {
|
||||
const mockBackend = new MockBackend();
|
||||
const tester = new TesterAgent();
|
||||
const result = await tester.execute(makeContext(dir, mockBackend));
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain("Mock backend executed");
|
||||
});
|
||||
|
||||
it("has correct agent name", () => {
|
||||
const tester = new TesterAgent();
|
||||
expect(tester.name).toBe("tester");
|
||||
});
|
||||
|
||||
it("has correct workflow", () => {
|
||||
const tester = new TesterAgent();
|
||||
expect(tester.workflow).toBe("test");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { VerifierAgent } from "../agents/verifier.js";
|
||||
import { AgentContext } from "../agents/base.js";
|
||||
import { IntelligenceBackend, BackendRequest, BackendResult } from "../backends/types.js";
|
||||
import { emptyTokenUsage } from "../backends/types.js";
|
||||
|
||||
class MockBackend implements IntelligenceBackend {
|
||||
readonly name = "mock";
|
||||
readonly type = "llm" as const;
|
||||
async isAvailable(): Promise<boolean> { return true; }
|
||||
async execute(request: BackendRequest): Promise<BackendResult> {
|
||||
return {
|
||||
success: true,
|
||||
output: `Mock backend executed: ${request.task.slice(0, 50)}`,
|
||||
artifacts: [],
|
||||
decisions: [],
|
||||
escalations: [],
|
||||
usage: emptyTokenUsage(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createTempDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-verifier-test-"));
|
||||
}
|
||||
|
||||
function cleanup(dir: string): void {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function makeContext(dir: string, backend?: IntelligenceBackend): AgentContext {
|
||||
return {
|
||||
project_path: dir,
|
||||
phase: 1,
|
||||
stage: "verify",
|
||||
specification: "Build a REST API for task management",
|
||||
config_path: path.join(dir, ".ciagent", "config.json"),
|
||||
backend,
|
||||
};
|
||||
}
|
||||
|
||||
function setupBasicProject(dir: string): void {
|
||||
const ciDir = path.join(dir, ".ciagent");
|
||||
fs.mkdirSync(ciDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(ciDir, "config.json"), "{}");
|
||||
|
||||
const srcDir = path.join(dir, "src");
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, "index.ts"), 'export const VERSION = "0.7.0";\n');
|
||||
}
|
||||
|
||||
describe("VerifierAgent", () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = createTempDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(dir);
|
||||
});
|
||||
|
||||
it("runs mechanical verification without backend", async () => {
|
||||
setupBasicProject(dir);
|
||||
const verifier = new VerifierAgent();
|
||||
const result = await verifier.execute(makeContext(dir));
|
||||
expect(result.output).toBeDefined();
|
||||
});
|
||||
|
||||
it("is read-only — does not create new source files", async () => {
|
||||
setupBasicProject(dir);
|
||||
const srcDir = path.join(dir, "src");
|
||||
const filesBefore = fs.readdirSync(srcDir);
|
||||
const verifier = new VerifierAgent();
|
||||
await verifier.execute(makeContext(dir));
|
||||
const filesAfter = fs.readdirSync(srcDir);
|
||||
expect(filesAfter.length).toBe(filesBefore.length);
|
||||
});
|
||||
|
||||
it("delegates to backend when available", async () => {
|
||||
setupBasicProject(dir);
|
||||
const mockBackend = new MockBackend();
|
||||
const verifier = new VerifierAgent();
|
||||
const result = await verifier.execute(makeContext(dir, mockBackend));
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain("Mock backend executed");
|
||||
});
|
||||
|
||||
it("has correct agent name", () => {
|
||||
const verifier = new VerifierAgent();
|
||||
expect(verifier.name).toBe("verifier");
|
||||
});
|
||||
|
||||
it("has correct workflow", () => {
|
||||
const verifier = new VerifierAgent();
|
||||
expect(verifier.workflow).toBe("verify");
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -121,7 +121,7 @@ export function createInitCommand(): Command {
|
||||
console.log("\nNext steps:");
|
||||
console.log(" ciagent run --all # Run full pipeline");
|
||||
console.log(" ciagent run research # Run specific phase");
|
||||
console.log(" ci status # Check project status");
|
||||
console.log(" ciagent status # Check project status");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { GiteaClient, generateReleaseNotes, GiteaReleaseConfig } from "../core/gitea.js";
|
||||
|
||||
const defaultConfig: GiteaReleaseConfig = {
|
||||
baseUrl: "https://git.example.com",
|
||||
token: "test-token-123",
|
||||
owner: "testorg",
|
||||
repo: "testrepo",
|
||||
};
|
||||
|
||||
function makeReleaseResponse(overrides: Partial<{
|
||||
id: number;
|
||||
tag_name: string;
|
||||
name: string;
|
||||
body: string;
|
||||
url: string;
|
||||
html_url: string;
|
||||
draft: boolean;
|
||||
prerelease: boolean;
|
||||
}> = {}): Record<string, unknown> {
|
||||
return {
|
||||
id: overrides.id ?? 1,
|
||||
tag_name: overrides.tag_name ?? "v1.0.0",
|
||||
name: overrides.name ?? "v1.0.0",
|
||||
body: overrides.body ?? "Release notes",
|
||||
url: overrides.url ?? "https://git.example.com/api/v1/repos/testorg/testrepo/releases/1",
|
||||
html_url: overrides.html_url ?? "https://git.example.com/testorg/testrepo/releases/tag/v1.0.0",
|
||||
draft: overrides.draft ?? false,
|
||||
prerelease: overrides.prerelease ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
describe("GiteaClient", () => {
|
||||
let originalFetch: typeof globalThis.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
originalFetch = globalThis.fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
describe("createRelease", () => {
|
||||
it("creates a release via POST", async () => {
|
||||
const client = new GiteaClient(defaultConfig);
|
||||
globalThis.fetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => makeReleaseResponse({ tag_name: "v1.0.0", name: "v1.0.0" }),
|
||||
});
|
||||
|
||||
const release = await client.createRelease({
|
||||
tag_name: "v1.0.0",
|
||||
name: "v1.0.0",
|
||||
body: "Initial release",
|
||||
});
|
||||
|
||||
expect(release.tag_name).toBe("v1.0.0");
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
||||
const call = (globalThis.fetch as jest.Mock).mock.calls[0];
|
||||
expect(call[0]).toContain("/releases");
|
||||
expect(call[1].method).toBe("POST");
|
||||
expect(call[1].headers.Authorization).toBe("token test-token-123");
|
||||
});
|
||||
|
||||
it("throws on non-ok response", async () => {
|
||||
const client = new GiteaClient(defaultConfig);
|
||||
globalThis.fetch = jest.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 409,
|
||||
text: async () => "Conflict: tag already exists",
|
||||
});
|
||||
|
||||
await expect(client.createRelease({
|
||||
tag_name: "v1.0.0",
|
||||
name: "v1.0.0",
|
||||
body: "",
|
||||
})).rejects.toThrow("Gitea API error: 409");
|
||||
});
|
||||
});
|
||||
|
||||
describe("listReleases", () => {
|
||||
it("lists releases via GET", async () => {
|
||||
const client = new GiteaClient(defaultConfig);
|
||||
globalThis.fetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => [
|
||||
makeReleaseResponse({ id: 1, tag_name: "v1.0.0" }),
|
||||
makeReleaseResponse({ id: 2, tag_name: "v1.1.0" }),
|
||||
],
|
||||
});
|
||||
|
||||
const releases = await client.listReleases();
|
||||
expect(releases).toHaveLength(2);
|
||||
expect(releases[0].tag_name).toBe("v1.0.0");
|
||||
expect(releases[1].tag_name).toBe("v1.1.0");
|
||||
});
|
||||
|
||||
it("throws on non-ok response", async () => {
|
||||
const client = new GiteaClient(defaultConfig);
|
||||
globalThis.fetch = jest.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
});
|
||||
|
||||
await expect(client.listReleases()).rejects.toThrow("Gitea API error: 500");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getReleaseByTag", () => {
|
||||
it("returns release when found", async () => {
|
||||
const client = new GiteaClient(defaultConfig);
|
||||
globalThis.fetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => makeReleaseResponse({ tag_name: "v1.0.0" }),
|
||||
});
|
||||
|
||||
const release = await client.getReleaseByTag("v1.0.0");
|
||||
expect(release).not.toBeNull();
|
||||
expect(release!.tag_name).toBe("v1.0.0");
|
||||
});
|
||||
|
||||
it("returns null on 404", async () => {
|
||||
const client = new GiteaClient(defaultConfig);
|
||||
globalThis.fetch = jest.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
});
|
||||
|
||||
const release = await client.getReleaseByTag("v0.0.0");
|
||||
expect(release).toBeNull();
|
||||
});
|
||||
|
||||
it("throws on other non-ok status", async () => {
|
||||
const client = new GiteaClient(defaultConfig);
|
||||
globalThis.fetch = jest.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
});
|
||||
|
||||
await expect(client.getReleaseByTag("v1.0.0")).rejects.toThrow("Gitea API error: 500");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateReleaseNotes", () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-gitea-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("parses git log into categorized sections", () => {
|
||||
const gitDir = path.join(dir, "repo");
|
||||
fs.mkdirSync(gitDir, { recursive: true });
|
||||
|
||||
const { execSync } = require("node:child_process");
|
||||
execSync("git init", { cwd: gitDir, stdio: "pipe" });
|
||||
execSync('git config user.email "test@test.com"', { cwd: gitDir, stdio: "pipe" });
|
||||
execSync('git config user.name "Test"', { cwd: gitDir, stdio: "pipe" });
|
||||
|
||||
fs.writeFileSync(path.join(gitDir, "file1.txt"), "hello");
|
||||
execSync("git add -A", { cwd: gitDir, stdio: "pipe" });
|
||||
execSync('git commit -m "feat: add authentication"', { cwd: gitDir, stdio: "pipe" });
|
||||
|
||||
fs.writeFileSync(path.join(gitDir, "file2.txt"), "world");
|
||||
execSync("git add -A", { cwd: gitDir, stdio: "pipe" });
|
||||
execSync('git commit -m "fix: resolve login bug"', { cwd: gitDir, stdio: "pipe" });
|
||||
|
||||
execSync("git tag v1.0.0", { cwd: gitDir, stdio: "pipe" });
|
||||
|
||||
const notes = generateReleaseNotes(gitDir, null, "v1.0.0");
|
||||
expect(notes).toContain("New Features");
|
||||
expect(notes).toContain("add authentication");
|
||||
expect(notes).toContain("Bug Fixes");
|
||||
expect(notes).toContain("resolve login bug");
|
||||
});
|
||||
|
||||
it("returns no-commits message when no commits found", () => {
|
||||
const nonExistent = path.join(dir, "nonexistent");
|
||||
const notes = generateReleaseNotes(nonExistent, null, "v0.0.0");
|
||||
expect(notes).toContain("No commits found");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,171 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { CIAgentFiles, ProjectEntry } from "../core/ciagent-files.js";
|
||||
import { initCIAgent, loadConfig, saveConfig } from "../core/config.js";
|
||||
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
|
||||
|
||||
function createTempDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-multiproject-test-"));
|
||||
}
|
||||
|
||||
function cleanup(dir: string): void {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
describe("Multi-project CIAgentFiles operations", () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = createTempDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(dir);
|
||||
});
|
||||
|
||||
describe("--project flag behavior via CIAgentFiles", () => {
|
||||
it("sets active project via setActiveProject", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
ciFiles.addProject("task-api", "Task API");
|
||||
ciFiles.addProject("auth-svc", "Auth Service");
|
||||
|
||||
ciFiles.setActiveProject("auth-svc");
|
||||
expect(ciFiles.getActiveProject()).toBe("auth-svc");
|
||||
});
|
||||
|
||||
it("lists all added projects", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
ciFiles.addProject("task-api", "Task API");
|
||||
ciFiles.addProject("auth-svc", "Auth Service");
|
||||
|
||||
const projects = ciFiles.listProjects();
|
||||
expect(projects.length).toBeGreaterThanOrEqual(2);
|
||||
const slugs = projects.map(p => p.slug);
|
||||
expect(slugs).toContain("task-api");
|
||||
expect(slugs).toContain("auth-svc");
|
||||
});
|
||||
|
||||
it("addProject does not duplicate existing slug", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
ciFiles.addProject("task-api", "Task API");
|
||||
ciFiles.addProject("task-api", "Task API V2");
|
||||
|
||||
const projects = ciFiles.listProjects();
|
||||
const taskApiProjects = projects.filter(p => p.slug === "task-api");
|
||||
expect(taskApiProjects.length).toBe(1);
|
||||
});
|
||||
|
||||
it("defaults to empty string when no active project set", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
expect(ciFiles.getActiveProject()).toBe("");
|
||||
});
|
||||
|
||||
it("isMultiProject returns false for single or no projects", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
expect(ciFiles.isMultiProject()).toBe(false);
|
||||
});
|
||||
|
||||
it("isMultiProject returns true when projects exist in config", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
ciFiles.addProject("task-api", "Task API");
|
||||
ciFiles.addProject("auth-svc", "Auth Service");
|
||||
expect(ciFiles.isMultiProject()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("config-level project operations", () => {
|
||||
it("initCIAgent with slug adds project to config", () => {
|
||||
const config = initCIAgent(dir, undefined, "task-api", "Task API");
|
||||
expect(config.projects).toHaveLength(1);
|
||||
expect(config.active_project).toBe("task-api");
|
||||
});
|
||||
|
||||
it("--project override sets active_project in config", () => {
|
||||
initCIAgent(dir, undefined, "task-api", "Task API");
|
||||
const config = loadConfig(dir);
|
||||
config.active_project = "task-api";
|
||||
config.projects = [
|
||||
{ slug: "task-api", name: "Task API", default: true },
|
||||
{ slug: "auth-svc", name: "Auth Service" },
|
||||
];
|
||||
saveConfig(dir, config);
|
||||
|
||||
const loaded = loadConfig(dir);
|
||||
expect(loaded.active_project).toBe("task-api");
|
||||
expect(loaded.projects).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("setActiveProject persists to config", () => {
|
||||
initCIAgent(dir, undefined, "task-api", "Task API");
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.addProject("auth-svc", "Auth Service");
|
||||
ciFiles.setActiveProject("auth-svc");
|
||||
|
||||
const config = loadConfig(dir);
|
||||
expect(config.active_project).toBe("auth-svc");
|
||||
});
|
||||
});
|
||||
|
||||
describe("project slug and directory structure", () => {
|
||||
it("multi-project mode uses .ciagent/<slug>/ subdirectory", () => {
|
||||
const ciFiles = new CIAgentFiles(dir, "task-api");
|
||||
ciFiles.ensureCIDir();
|
||||
ciFiles.ensureProjectDir();
|
||||
|
||||
const projectDir = path.join(dir, ".ciagent", "task-api");
|
||||
expect(fs.existsSync(projectDir)).toBe(true);
|
||||
});
|
||||
|
||||
it("single-project mode uses .ciagent/ directly", () => {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
ciFiles.ensureProjectDir();
|
||||
|
||||
expect(fs.existsSync(path.join(dir, ".ciagent"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(dir, ".ciagent", "task-api"))).toBe(false);
|
||||
});
|
||||
|
||||
it("writeProjectMd writes to project subdirectory in multi-project", () => {
|
||||
const ciFiles = new CIAgentFiles(dir, "task-api");
|
||||
ciFiles.ensureCIDir();
|
||||
ciFiles.ensureProjectDir();
|
||||
|
||||
ciFiles.writeProjectMd({
|
||||
name: "Task API",
|
||||
coreValue: "Manage tasks",
|
||||
requirements: { validated: [], active: ["Task CRUD"], outOfScope: [] },
|
||||
constraints: ["Node.js"],
|
||||
context: "REST API",
|
||||
keyDecisions: [],
|
||||
}, "test write");
|
||||
|
||||
expect(fs.existsSync(path.join(dir, ".ciagent", "task-api", "PROJECT.md"))).toBe(true);
|
||||
});
|
||||
|
||||
it("readProjectMd reads from project subdirectory in multi-project", () => {
|
||||
const ciFiles = new CIAgentFiles(dir, "task-api");
|
||||
ciFiles.ensureCIDir();
|
||||
ciFiles.ensureProjectDir();
|
||||
|
||||
ciFiles.writeProjectMd({
|
||||
name: "Task API",
|
||||
coreValue: "Manage tasks",
|
||||
requirements: { validated: [], active: [], outOfScope: [] },
|
||||
constraints: [],
|
||||
context: "",
|
||||
keyDecisions: [],
|
||||
}, "test write");
|
||||
|
||||
const projectMd = ciFiles.readProjectMd();
|
||||
expect(projectMd).not.toBeNull();
|
||||
expect(projectMd!.name).toBe("Task API");
|
||||
});
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const VERSION = "0.5.0";
|
||||
export const VERSION = "0.7.0";
|
||||
@@ -4,14 +4,13 @@
|
||||
|
||||
## Decision Log
|
||||
|
||||
Decisions are automatically logged to `.ciagent/audit/` with:
|
||||
Decisions are automatically logged to git commits via `---ci---` YAML blocks with:
|
||||
- Timestamp
|
||||
- Decision ID
|
||||
- What was decided
|
||||
- Why (reasoning chain)
|
||||
- Confidence level
|
||||
- What alternatives were considered
|
||||
- What the human would have been asked in Learnship mode
|
||||
|
||||
## Reviewing Decisions
|
||||
|
||||
|
||||
Reference in New Issue
Block a user