release(v0.4.0): purge learnship, migrate .planning→.ci, fix backends, add test coverage

- Remove all learnship references: Decision.learnship_equivalent field,
  agent persona prompts, opencode.json permissions, test fixtures
- Migrate verification layers from .planning/ to .ci/: structural
  checks .ci/ dir + ROADMAP.md, behavioral checks ROADMAP.md
- Fix ollama-local: remove sync require+curl blocking, use async
  fetchAvailableModels() in callModel
- Fix opencode.json: use __OPENCODE_DIR__ template tokens, remove
  legacy learnship permission entries
- Remove duplicate install script from package.json (keep postinstall)
- Fix quality any-regex false positives (target type annotations only)
- Add backends test coverage: backends.test.ts, tool-registry.test.ts
- Version bump 0.3.0 → 0.4.0
- Artifacts module: rename .planning→.ci internal paths
- Remove dead TODO_PATTERN/FIXME_PATTERN constants

---ci---
phase: 3
milestone: v0.4
status: complete
requirements:
  covered: [REQ-09, REQ-10, REQ-11, REQ-13, REQ-14, REQ-17]
  partial: []
decisions:
  - id: D-001
    decision: purge all learnship references from codebase
    rationale: project is CI-only, learnship is no longer a dependency
    confidence: 0.99
    category: scope
    alternatives: [keep for historical reference]
  - id: D-002
    decision: migrate verification from .planning/ to .ci/ paths
    rationale: .planning/ is removed schema, all current state lives in .ci/
    confidence: 0.95
    category: architecture
    alternatives: [keep dual-path support]
  - id: D-003
    decision: use __OPENCODE_DIR__ template tokens in opencode.json
    rationale: hardcoded ~ paths fail in containers and non-standard homes
    confidence: 0.90
    category: implementation_approach
    alternatives: [keep tilde expansion]
---/ci---
This commit is contained in:
CI
2026-05-29 16:18:30 +00:00
parent 7a20784c87
commit fb3f1df13e
32 changed files with 364 additions and 136 deletions
+11 -11
View File
@@ -26,7 +26,7 @@ src/
index.ts # Backend registry + auto-detection index.ts # Backend registry + auto-detection
cli/ # Commander.js CLI (commands.ts, index.ts) cli/ # Commander.js CLI (commands.ts, index.ts)
core/ # Core engine components core/ # Core engine components
artifacts.ts # Legacy .planning/ artifact management (retained for backward compat) artifacts.ts # Legacy .ci/ artifact management (retained for backward compat)
audit.ts # Legacy audit trail in .ci/audit/ (retained for backward compat) audit.ts # Legacy audit trail in .ci/audit/ (retained for backward compat)
ci-files.ts # .ci/ long-lived reference file management (PROJECT.md, ROADMAP.md, etc.) ci-files.ts # .ci/ long-lived reference file management (PROJECT.md, ROADMAP.md, etc.)
clarify.ts # Clarify phase: question generation, default acceptance clarify.ts # Clarify phase: question generation, default acceptance
@@ -49,11 +49,11 @@ src/
utils/ # File utilities (readFile, writeFile, ensureDir, readJSON, writeJSON) utils/ # File utilities (readFile, writeFile, ensureDir, readJSON, writeJSON)
verification/ # 4-layer verification pipeline verification/ # 4-layer verification pipeline
structural.ts # Layer 1: file existence, imports wired, no stubs structural.ts # Layer 1: file existence, imports wired, no stubs
behavioral.ts # Layer 2: test generation and execution (stub) behavioral.ts # Layer 2: test infrastructure checks (static analysis, no test generation yet)
security.ts # Layer 3: STRIDE threat analysis (stub) security.ts # Layer 3: regex-based threat pattern scanning (no STRIDE analysis yet)
quality.ts # Layer 4: multi-persona code review (stub) quality.ts # Layer 4: regex-based code quality checks (no multi-persona review yet)
index.ts # Public API exports index.ts # Public API exports
version.ts # VERSION = "0.3.0" version.ts # VERSION = "0.4.0"
templates/ # Template files (config.json, DECISIONS.md, specification.md) templates/ # Template files (config.json, DECISIONS.md, specification.md)
``` ```
@@ -62,7 +62,7 @@ templates/ # Template files (config.json, DECISIONS.md, specification.md
- **Autonomy levels**: `full` (no HITL after clarify), `supervised` (escalate on gates + verification failures), `guided` (escalate on every decision gate) - **Autonomy levels**: `full` (no HITL after clarify), `supervised` (escalate on gates + verification failures), `guided` (escalate on every decision gate)
- **Decision confidence thresholds**: High (>0.85) auto-decide and log; Medium (0.600.85) auto-decide with assumption logging; Low (<0.60) escalate to human - **Decision confidence thresholds**: High (>0.85) auto-decide and log; Medium (0.600.85) auto-decide with assumption logging; Low (<0.60) escalate to human
- **Escalation timeout**: Default 5 minutes, then auto-proceeds with recommended option. Set to `0` to require human, `-1` to always auto-proceed - **Escalation timeout**: Default 5 minutes, then auto-proceeds with recommended option. Set to `0` to require human, `-1` to always auto-proceed
- **18 agents** inherited from Learnship, all re-prompted for autonomous operation. OrchestratorAgent is CI-specific - **18 agents** purpose-built for CI, all configured for autonomous operation. OrchestratorAgent is CI-specific
- **Git-native context**: The git log IS the project memory. Agent's first impulse to gather context is `git log` + `git branch`, not file reads. Dynamic state (decisions, escalations, lessons, compounding) lives in `---ci---` YAML blocks in commit messages. `.ci/` holds only long-lived reference docs (PROJECT.md, ARCHITECTURE.md, ROADMAP.md, REQUIREMENTS.md, config.json). - **Git-native context**: The git log IS the project memory. Agent's first impulse to gather context is `git log` + `git branch`, not file reads. Dynamic state (decisions, escalations, lessons, compounding) lives in `---ci---` YAML blocks in commit messages. `.ci/` holds only long-lived reference docs (PROJECT.md, ARCHITECTURE.md, ROADMAP.md, REQUIREMENTS.md, config.json).
- **Artifact compatibility**: CI no longer writes `.planning/` schema. Dynamic state is derived from git history. `.ci/` files follow a CI-native schema. - **Artifact compatibility**: CI no longer writes `.planning/` schema. Dynamic state is derived from git history. `.ci/` files follow a CI-native schema.
@@ -122,9 +122,9 @@ IntelligenceBackend (unified interface)
## Verification Layers ## Verification Layers
1. **Structural**: Files exist, imports wired, no stubs/TODOs 1. **Structural**: Files exist, imports wired, no stubs/TODOs
2. **Behavioral**: Generate and run automated tests for must-haves (currently stub) 2. **Behavioral**: Check test infrastructure and requirement traceability (static analysis — test generation not yet implemented)
3. **Security**: STRIDE analysis with auto-disposition (currently stub) 3. **Security**: Regex-based threat pattern scanning with auto-disposition (STRIDE analysis not yet implemented)
4. **Code Quality**: Multi-persona review with P0 auto-fix (currently stub) 4. **Code Quality**: Regex-based code quality checks (multi-persona review not yet implemented)
## Testing ## Testing
@@ -191,7 +191,7 @@ IntelligenceBackend (unified interface)
## Current State ## Current State
- **v0.2.0**: Git-native architecture — project memory lives in git log, not `.planning/` files - **v0.4.0**: Backends module (OllamaLocal, OllamaCloud, Opencode), learnship references removed, verification layers migrated from .planning/ to .ci/
- **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), ci-files (`.ci/` long-lived reference file management) - **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), ci-files (`.ci/` long-lived reference file management)
- **Commit schema**: Every CI-generated commit contains a `---ci---` YAML block with phase, milestone, status, decisions, escalations, requirements, lessons, and compound metadata - **Commit schema**: Every CI-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 - **Branch strategy**: `phase/NN-slug` and `milestone/vX.X-slug` branches encode project structure; merged = complete, active = in progress
@@ -203,4 +203,4 @@ IntelligenceBackend (unified interface)
- **CLI**: All 11 commands wired up (`init`, `run`, `quick`, `debug`, `verify`, `review`, `status`, `audit`, `clarify`, `rollback`, `ship`) - **CLI**: All 11 commands wired up (`init`, `run`, `quick`, `debug`, `verify`, `review`, `status`, `audit`, `clarify`, `rollback`, `ship`)
- **Agent implementations**: Persona loaders that delegate to active backend. Fail honestly when no backend is available (no more fake success). - **Agent implementations**: Persona loaders that delegate to active backend. Fail honestly when no backend is available (no more fake success).
- **Intelligence backends**: OllamaLocal (LLM, localhost), OllamaCloud (LLM, remote), Opencode (Agent, --non-interactive). Auto-detection: opencode → ollama-local → ollama-cloud. - **Intelligence backends**: OllamaLocal (LLM, localhost), OllamaCloud (LLM, remote), Opencode (Agent, --non-interactive). Auto-detection: opencode → ollama-local → ollama-cloud.
- **Tests**: 25 test suites, 218 tests covering types, config, decision-engine, escalation, clarify, commit-parser, commit-builder, git-context, git-branch, ci-files, all 4 verification layers, file utils - **Tests**: 27 test suites covering types, config, decision-engine, escalation, clarify, commit-parser, commit-builder, git-context, git-branch, ci-files, all 4 verification layers, file utils, backends, tool-registry
+1 -1
View File
@@ -11,7 +11,7 @@ tools:
<role> <role>
You are a CI challenger. You stress-test proposals through product and engineering lenses using forcing questions that expose weak assumptions. You are a CI challenger. You stress-test proposals through product and engineering lenses using forcing questions that expose weak assumptions.
Unlike learnship, CI challengers produce binding verdicts. Only escalate when confidence < 0.60. If confident the proposal is sound, it proceeds. If confident it needs rework, it is sent back. CI challengers produce binding verdicts. Only escalate when confidence < 0.60. If confident the proposal is sound, it proceeds. If confident it needs rework, it is sent back.
**CRITICAL: Mandatory Initial Read** **CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions. If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
+1 -1
View File
@@ -12,7 +12,7 @@ tools:
<role> <role>
You are a CI code reviewer. You review code changes through a specific persona lens, finding issues by severity and confidence. You are a CI code reviewer. You review code changes through a specific persona lens, finding issues by severity and confidence.
Unlike learnship, CI code reviewers auto-apply P0 fixes. P1+ issues are flagged for post-hoc review via `git log --grep="review"`. CI code reviewers auto-apply P0 fixes. P1+ issues are flagged for post-hoc review via `git log --grep="review"`.
**CRITICAL: Mandatory Initial Read** **CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions. If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
+1 -1
View File
@@ -13,7 +13,7 @@ tools:
<role> <role>
You are a CI debugger. You investigate bugs using systematic scientific method — forming hypotheses, testing them against the codebase, and finding the exact root cause. You are a CI debugger. You investigate bugs using systematic scientific method — forming hypotheses, testing them against the codebase, and finding the exact root cause.
Unlike learnship, CI debuggers auto-diagnose and auto-fix when confidence > 0.60. Only low-confidence root causes are escalated to human. CI debuggers auto-diagnose and auto-fix when confidence > 0.60. Only low-confidence root causes are escalated to human.
**CRITICAL: Mandatory Initial Read** **CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions. If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
+1 -1
View File
@@ -13,7 +13,7 @@ tools:
<role> <role>
You are a CI executor. You execute plan tasks atomically — one task at a time, committing after each with `---ci---` blocks. You are a CI executor. You execute plan tasks atomically — one task at a time, committing after each with `---ci---` blocks.
Unlike learnship, CI executors NEVER pause for checkpoints. Every task is autonomous. Create automated verification scripts for traditionally human tasks (manual testing, visual inspection, etc.). CI executors NEVER pause for checkpoints. Every task is autonomous. Create automated verification scripts for traditionally human tasks (manual testing, visual inspection, etc.).
**CRITICAL: Mandatory Initial Read** **CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions. If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
+1 -1
View File
@@ -13,7 +13,7 @@ tools:
<role> <role>
You are the CI orchestrator. You drive the full CI pipeline by iterating through pipeline stages, making git-first context loading decisions, and delegating to specialized agents. You are the CI orchestrator. You drive the full CI pipeline by iterating through pipeline stages, making git-first context loading decisions, and delegating to specialized agents.
Unlike learnship, CI operates autonomously after the clarify phase. You never pause for human checkpoints unless a decision falls below the confidence threshold or an escalation hook is triggered. CI operates autonomously after the clarify phase. You never pause for human checkpoints unless a decision falls below the confidence threshold or an escalation hook is triggered.
Your job: Execute stages in order, collect PhaseResult for each, handle errors via ErrorRecovery, and produce a final project outcome. Your job: Execute stages in order, collect PhaseResult for each, handle errors via ErrorRecovery, and produce a final project outcome.
+1 -1
View File
@@ -12,7 +12,7 @@ tools:
<role> <role>
You are a CI planner. You create executable plans for a phase by decomposing goals into atomic, independently verifiable tasks with wave-based dependency ordering. You are a CI planner. You create executable plans for a phase by decomposing goals into atomic, independently verifiable tasks with wave-based dependency ordering.
Unlike learnship, CI plans NEVER have `autonomous: false`. Every task is autonomous by default. Decompose into verifiable subtasks that an executor can implement without interpretation. CI plans NEVER have `autonomous: false`. Every task is autonomous by default. Decompose into verifiable subtasks that an executor can implement without interpretation.
**CRITICAL: Mandatory Initial Read** **CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions. If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
+1 -1
View File
@@ -11,7 +11,7 @@ tools:
<role> <role>
You are a CI researcher. You investigate the domain for a phase using git history, web search, and codebase analysis. You are a CI researcher. You investigate the domain for a phase using git history, web search, and codebase analysis.
Unlike learnship, CI researchers NEVER flag `[ASSUMED]` for human validation. Instead, log assumptions to DecisionEngine with confidence scores. Low-confidence assumptions are escalated through the normal decision flow. CI researchers NEVER flag `[ASSUMED]` for human validation. Instead, log assumptions to DecisionEngine with confidence scores. Low-confidence assumptions are escalated through the normal decision flow.
**CRITICAL: Mandatory Initial Read** **CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions. If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
+1 -1
View File
@@ -11,7 +11,7 @@ tools:
<role> <role>
You are a CI security auditor. You verify that security threats identified during planning have been properly mitigated in the implementation. You are a CI security auditor. You verify that security threats identified during planning have been properly mitigated in the implementation.
Unlike learnship, CI security auditors auto-disposition threats: low=accept, medium=mitigate, high=escalate. Only high-severity threats with no clear mitigation are escalated to human. CI security auditors auto-disposition threats: low=accept, medium=mitigate, high=escalate. Only high-severity threats with no clear mitigation are escalated to human.
You are READ-ONLY. Do not modify source code. You are READ-ONLY. Do not modify source code.
+1 -1
View File
@@ -11,7 +11,7 @@ tools:
<role> <role>
You are a CI verifier. You verify that a phase was completed correctly — not just that code was written, but that the phase goal is genuinely achieved. You are a CI verifier. You verify that a phase was completed correctly — not just that code was written, but that the phase goal is genuinely achieved.
Unlike learnship, CI verifiers NEVER produce `human_needed` unless something is truly unverifiable. Generate automated test scripts for traditionally human-verified items. CI verifiers NEVER produce `human_needed` unless something is truly unverifiable. Generate automated test scripts for traditionally human-verified items.
**CRITICAL: Mandatory Initial Read** **CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions. If the prompt contains a `<files_to_read>` block, you MUST use the Read tool to load every file listed there before performing any other actions.
+1 -1
View File
@@ -1 +1 @@
0.3.0 0.4.0
+3 -5
View File
@@ -2,12 +2,10 @@
"$schema": "https://opencode.ai/config.json", "$schema": "https://opencode.ai/config.json",
"permission": { "permission": {
"read": { "read": {
"~/.config/opencode/learnship/*": "allow", "__OPENCODE_DIR__/ci/*": "allow"
"~/.config/opencode/ci/*": "allow"
}, },
"external_directory": { "external_directory": {
"~/.config/opencode/learnship/*": "allow", "__OPENCODE_DIR__/ci/*": "allow"
"~/.config/opencode/ci/*": "allow"
} }
} }
} }
+2 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@continuous-intelligence/ci", "name": "@continuous-intelligence/ci",
"version": "0.3.0", "version": "0.4.0",
"description": "Fully autonomous AI-driven software engineering harness - Continuous Intelligence", "description": "Fully autonomous AI-driven software engineering harness - Continuous Intelligence",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@@ -21,8 +21,7 @@
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"test": "jest", "test": "jest",
"prepublishOnly": "npm run build", "prepublishOnly": "npm run build",
"postinstall": "node scripts/postinstall.js", "postinstall": "node scripts/postinstall.js"
"install": "bash scripts/install.sh"
}, },
"keywords": ["ci", "autonomous", "ai", "software-engineering", "agent", "multi-project"], "keywords": ["ci", "autonomous", "ai", "software-engineering", "agent", "multi-project"],
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -2,7 +2,7 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js";
export class SolutionWriterAgent extends BaseAgent { export class SolutionWriterAgent extends BaseAgent {
readonly name = "solution-writer"; readonly name = "solution-writer";
readonly description = "Produces structured solution documents for .planning/solutions/."; readonly description = "Produces structured solution documents.";
readonly workflow = "execute"; readonly workflow = "execute";
async execute(context: AgentContext): Promise<AgentResult> { async execute(context: AgentContext): Promise<AgentResult> {
+129
View File
@@ -0,0 +1,129 @@
import { BackendUnavailableError, emptyTokenUsage, emptyBackendResult, DEFAULT_BACKEND_CONFIG } from "../backends/types.js";
import { OllamaLocalBackend } from "../backends/ollama-local.js";
import { OllamaCloudBackend } from "../backends/ollama-cloud.js";
import { OpencodeBackend } from "../backends/opencode.js";
describe("BackendUnavailableError", () => {
it("includes backend name in message", () => {
const err = new BackendUnavailableError("ollama-local");
expect(err.message).toContain("ollama-local");
expect(err.backendName).toBe("ollama-local");
});
it("includes agent name when provided", () => {
const err = new BackendUnavailableError("opencode", "executor");
expect(err.agentName).toBe("executor");
expect(err.message).toContain("executor");
});
});
describe("emptyTokenUsage", () => {
it("returns zeroed usage", () => {
const usage = emptyTokenUsage();
expect(usage.input_tokens).toBe(0);
expect(usage.output_tokens).toBe(0);
expect(usage.total_tokens).toBe(0);
expect(usage.estimated_cost_usd).toBe(0);
});
});
describe("emptyBackendResult", () => {
it("returns failed result with no artifacts", () => {
const result = emptyBackendResult("something failed");
expect(result.success).toBe(false);
expect(result.error).toBe("something failed");
expect(result.artifacts).toEqual([]);
expect(result.decisions).toEqual([]);
expect(result.escalations).toEqual([]);
});
it("returns result without error when no message provided", () => {
const result = emptyBackendResult();
expect(result.success).toBe(false);
expect(result.error).toBeUndefined();
});
});
describe("DEFAULT_BACKEND_CONFIG", () => {
it("has auto provider by default", () => {
expect(DEFAULT_BACKEND_CONFIG.provider).toBe("auto");
});
it("has opencode agent backend enabled", () => {
expect(DEFAULT_BACKEND_CONFIG.agent_backends.opencode?.enabled).toBe(true);
});
it("has ollama-local and ollama-cloud llm backends", () => {
expect(DEFAULT_BACKEND_CONFIG.llm_backends["ollama-local"]).toBeDefined();
expect(DEFAULT_BACKEND_CONFIG.llm_backends["ollama-cloud"]).toBeDefined();
});
});
describe("OllamaLocalBackend", () => {
it("has correct name and type", () => {
const backend = new OllamaLocalBackend();
expect(backend.name).toBe("ollama-local");
expect(backend.type).toBe("llm");
});
it("returns false when local Ollama is not available", async () => {
const backend = new OllamaLocalBackend({
base_url: "http://localhost:1",
model_profile: "balanced",
});
const available = await backend.isAvailable();
expect(available).toBe(false);
});
it("uses default config when none provided", () => {
const backend = new OllamaLocalBackend();
expect(backend).toBeDefined();
});
});
describe("OllamaCloudBackend", () => {
it("has correct name and type", () => {
const backend = new OllamaCloudBackend();
expect(backend.name).toBe("ollama-cloud");
expect(backend.type).toBe("llm");
});
it("returns false when no base_url configured", async () => {
const backend = new OllamaCloudBackend({
base_url: "",
api_key_env: "NONEXISTENT_KEY",
model_profile: "quality",
timeout_ms: 5000,
});
const available = await backend.isAvailable();
expect(available).toBe(false);
});
it("returns false when no API key available", async () => {
const backend = new OllamaCloudBackend({
base_url: "https://example.com",
api_key_env: "NONEXISTENT_CI_KEY_12345",
model_profile: "quality",
timeout_ms: 5000,
});
const available = await backend.isAvailable();
expect(available).toBe(false);
});
});
describe("OpencodeBackend", () => {
it("has correct name and type", () => {
const backend = new OpencodeBackend();
expect(backend.name).toBe("opencode");
expect(backend.type).toBe("agent");
});
it("returns false when opencode is not installed", async () => {
const backend = new OpencodeBackend({
enabled: true,
executable: "nonexistent-opencode-binary-xyz",
});
const available = await backend.isAvailable();
expect(available).toBe(false);
});
});
+1 -2
View File
@@ -153,7 +153,7 @@ export abstract class OllamaBaseBackend implements IntelligenceBackend {
' "success": true,', ' "success": true,',
' "output": "Summary of what was accomplished",', ' "output": "Summary of what was accomplished",',
' "artifacts": [{"path": "file/path", "content": "...", "operation": "create"}],', ' "artifacts": [{"path": "file/path", "content": "...", "operation": "create"}],',
' "decisions": [{"id": "D-NNN", "decision": "what", "rationale": "why", "confidence": 0.85, "category": "general", "alternatives_considered": [], "learnship_equivalent": "", "human_override": null, "timestamp": ""}],', ' "decisions": [{"id": "D-NNN", "decision": "what", "rationale": "why", "confidence": 0.85, "category": "general", "alternatives_considered": [], "human_override": null, "timestamp": ""}],',
' "escalations": []', ' "escalations": []',
'}', '}',
"```" "```"
@@ -241,7 +241,6 @@ export abstract class OllamaBaseBackend implements IntelligenceBackend {
: (a as { option: string; rejected_reason: string }) : (a as { option: string; rejected_reason: string })
) )
: [], : [],
learnship_equivalent: String(d.learnship_equivalent || ""),
human_override: d.human_override ? String(d.human_override) : null, human_override: d.human_override ? String(d.human_override) : null,
timestamp: String(d.timestamp || new Date().toISOString()), timestamp: String(d.timestamp || new Date().toISOString()),
})); }));
+7 -16
View File
@@ -25,8 +25,7 @@ export class OllamaLocalBackend extends OllamaBaseBackend {
protected resolveModel(): string { protected resolveModel(): string {
if (this.localConfig.model) return this.localConfig.model; if (this.localConfig.model) return this.localConfig.model;
const models = this.fetchAvailableModelsSync(); return this.modelProfileToModel(this.localConfig.model_profile, []);
return this.modelProfileToModel(this.localConfig.model_profile, models);
} }
protected async callModel( protected async callModel(
@@ -34,10 +33,15 @@ export class OllamaLocalBackend extends OllamaBaseBackend {
model: string, model: string,
toolRegistry: ToolRegistry toolRegistry: ToolRegistry
): Promise<OllamaChatResponse> { ): Promise<OllamaChatResponse> {
let resolvedModel = model;
if (!this.localConfig.model) {
const models = await this.fetchAvailableModels();
resolvedModel = this.modelProfileToModel(this.localConfig.model_profile, models);
}
const url = `${this.localConfig.base_url}/v1/chat/completions`; const url = `${this.localConfig.base_url}/v1/chat/completions`;
const body: Record<string, unknown> = { const body: Record<string, unknown> = {
model, model: resolvedModel,
messages: messages.map((m) => { messages: messages.map((m) => {
const msg: Record<string, unknown> = { role: m.role, content: m.content }; const msg: Record<string, unknown> = { role: m.role, content: m.content };
if (m.name) msg.name = m.name; if (m.name) msg.name = m.name;
@@ -65,17 +69,4 @@ export class OllamaLocalBackend extends OllamaBaseBackend {
return (await response.json()) as OllamaChatResponse; return (await response.json()) as OllamaChatResponse;
} }
private fetchAvailableModelsSync(): string[] {
try {
const { execSync } = require("node:child_process");
const result = execSync(`curl -s ${this.localConfig.base_url}/api/tags`, {
encoding: "utf-8",
timeout: 5000,
});
const data = JSON.parse(result) as { models?: Array<{ name: string }> };
return (data.models || []).map((m) => m.name);
} catch {
return [];
}
}
} }
-1
View File
@@ -141,7 +141,6 @@ export class OpencodeBackend implements IntelligenceBackend {
: (a as { option: string; rejected_reason: string }) : (a as { option: string; rejected_reason: string })
) )
: [], : [],
learnship_equivalent: String(d.learnship_equivalent || ""),
human_override: d.human_override ? String(d.human_override) : null, human_override: d.human_override ? String(d.human_override) : null,
timestamp: String(d.timestamp || new Date().toISOString()), timestamp: String(d.timestamp || new Date().toISOString()),
})) }))
+139
View File
@@ -0,0 +1,139 @@
import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { ToolRegistry, TOOL_DEFINITIONS } from "../backends/tool-registry.js";
describe("ToolRegistry", () => {
let tempDir: string;
let registry: ToolRegistry;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-tool-registry-test-"));
registry = new ToolRegistry(tempDir);
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe("definitions", () => {
it("provides 6 tool definitions", () => {
expect(TOOL_DEFINITIONS).toHaveLength(6);
const names = TOOL_DEFINITIONS.map((d) => d.name);
expect(names).toContain("readFile");
expect(names).toContain("writeFile");
expect(names).toContain("editFile");
expect(names).toContain("runBash");
expect(names).toContain("glob");
expect(names).toContain("grep");
});
it("getOpenAIToolSchema returns function-type schema", () => {
const schema = registry.getOpenAIToolSchema();
expect(schema.length).toBe(6);
expect(schema[0].type).toBe("function");
expect((schema[0].function as Record<string, unknown>).name).toBeDefined();
expect((schema[0].function as Record<string, unknown>).parameters).toBeDefined();
});
});
describe("readFile", () => {
it("reads an existing file", () => {
const filePath = path.join(tempDir, "test.txt");
fs.writeFileSync(filePath, "hello world");
const result = registry.execute({ name: "readFile", arguments: { path: filePath } });
expect(result.name).toBe("readFile");
expect(result.content).toBe("hello world");
expect(result.isError).toBeFalsy();
});
it("returns error for missing file", () => {
const result = registry.execute({ name: "readFile", arguments: { path: "/nonexistent/file.txt" } });
expect(result.isError).toBe(true);
expect(result.content).toContain("not found");
});
it("returns error for files exceeding max size", () => {
const bigRegistry = new ToolRegistry(tempDir, 10);
const filePath = path.join(tempDir, "big.txt");
fs.writeFileSync(filePath, "x".repeat(100));
const result = bigRegistry.execute({ name: "readFile", arguments: { path: filePath } });
expect(result.isError).toBe(true);
expect(result.content).toContain("too large");
});
});
describe("writeFile", () => {
it("writes a file creating parent directories", () => {
const filePath = path.join(tempDir, "sub", "dir", "test.txt");
const result = registry.execute({ name: "writeFile", arguments: { path: filePath, content: "written" } });
expect(result.isError).toBeFalsy();
expect(fs.readFileSync(filePath, "utf-8")).toBe("written");
});
});
describe("editFile", () => {
it("replaces an exact string in a file", () => {
const filePath = path.join(tempDir, "edit.txt");
fs.writeFileSync(filePath, "hello world");
const result = registry.execute({ name: "editFile", arguments: { path: filePath, old: "hello", new: "goodbye" } });
expect(result.isError).toBeFalsy();
expect(fs.readFileSync(filePath, "utf-8")).toBe("goodbye world");
});
it("returns error when old string not found", () => {
const filePath = path.join(tempDir, "edit.txt");
fs.writeFileSync(filePath, "hello world");
const result = registry.execute({ name: "editFile", arguments: { path: filePath, old: "missing", new: "replacement" } });
expect(result.isError).toBe(true);
});
it("returns error for missing file", () => {
const result = registry.execute({ name: "editFile", arguments: { path: "/nonexistent", old: "a", new: "b" } });
expect(result.isError).toBe(true);
});
});
describe("runBash", () => {
it("executes a command and returns stdout", () => {
const result = registry.execute({ name: "runBash", arguments: { command: "echo hello" } });
expect(result.content).toContain("hello");
expect(result.isError).toBeFalsy();
});
it("returns error with stderr for failing commands", () => {
const result = registry.execute({ name: "runBash", arguments: { command: "false" } });
expect(result.isError).toBe(true);
expect(result.content).toContain("Exit code");
});
});
describe("glob", () => {
it("finds files matching a pattern", () => {
fs.writeFileSync(path.join(tempDir, "app.ts"), "");
fs.writeFileSync(path.join(tempDir, "app.test.ts"), "");
fs.writeFileSync(path.join(tempDir, "README.md"), "");
const result = registry.execute({ name: "glob", arguments: { pattern: "*.ts" } });
const matches = JSON.parse(result.content);
expect(matches.length).toBeGreaterThanOrEqual(2);
});
});
describe("grep", () => {
it("finds matching lines", () => {
fs.writeFileSync(path.join(tempDir, "app.ts"), "export function main() {}\nconst x = 1;\n");
const result = registry.execute({ name: "grep", arguments: { pattern: "export", include: "*.ts" } });
const matches = JSON.parse(result.content);
expect(matches.length).toBe(1);
expect(matches[0].content).toContain("export");
});
});
describe("unknown tool", () => {
it("returns error for unknown tool name", () => {
const result = registry.execute({ name: "unknownTool", arguments: {} });
expect(result.isError).toBe(true);
expect(result.content).toContain("Unknown tool");
});
});
});
+5 -6
View File
@@ -17,17 +17,16 @@ describe("ArtifactManager", () => {
}); });
describe("ensureStructure", () => { describe("ensureStructure", () => {
it("creates .planning directory structure", () => { it("creates .ci directory structure", () => {
manager.ensureStructure(); manager.ensureStructure();
expect(fs.existsSync(path.join(tempDir, ".planning"))).toBe(true); expect(fs.existsSync(path.join(tempDir, ".ci"))).toBe(true);
expect(fs.existsSync(path.join(tempDir, ".planning", "phases"))).toBe(true);
expect(fs.existsSync(path.join(tempDir, ".ci", "audit"))).toBe(true); expect(fs.existsSync(path.join(tempDir, ".ci", "audit"))).toBe(true);
}); });
it("is idempotent", () => { it("is idempotent", () => {
manager.ensureStructure(); manager.ensureStructure();
manager.ensureStructure(); manager.ensureStructure();
expect(fs.existsSync(path.join(tempDir, ".planning"))).toBe(true); expect(fs.existsSync(path.join(tempDir, ".ci"))).toBe(true);
}); });
}); });
@@ -68,7 +67,7 @@ describe("ArtifactManager", () => {
manager.writeProject(manifest); manager.writeProject(manifest);
const projectPath = path.join(tempDir, ".planning", "PROJECT.md"); const projectPath = path.join(tempDir, ".ci", "PROJECT.md");
expect(fs.existsSync(projectPath)).toBe(true); expect(fs.existsSync(projectPath)).toBe(true);
const content = fs.readFileSync(projectPath, "utf-8"); const content = fs.readFileSync(projectPath, "utf-8");
expect(content).toContain("Test Project"); expect(content).toContain("Test Project");
@@ -132,7 +131,7 @@ describe("ArtifactManager", () => {
], ],
}); });
const decisionsPath = path.join(tempDir, ".planning", "DECISIONS.md"); const decisionsPath = path.join(tempDir, ".ci", "DECISIONS.md");
expect(fs.existsSync(decisionsPath)).toBe(true); expect(fs.existsSync(decisionsPath)).toBe(true);
const content = fs.readFileSync(decisionsPath, "utf-8"); const content = fs.readFileSync(decisionsPath, "utf-8");
expect(content).toContain("D-001"); expect(content).toContain("D-001");
+14 -14
View File
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
import * as path from "node:path"; import * as path from "node:path";
import { writeFile, readFile, ensureDir } from "../utils/file.js"; import { writeFile, readFile, ensureDir } from "../utils/file.js";
const PLANNING_DIR = ".planning"; const CI_DIR = ".ci";
export interface ProjectManifest { export interface ProjectManifest {
name: string; name: string;
@@ -48,18 +48,18 @@ export class ArtifactManager {
this.projectPath = projectPath; this.projectPath = projectPath;
} }
private get planningDir(): string { private get ciDir(): string {
return path.join(this.projectPath, PLANNING_DIR); return path.join(this.projectPath, CI_DIR);
} }
ensureStructure(): void { ensureStructure(): void {
ensureDir(this.planningDir); ensureDir(this.ciDir);
ensureDir(path.join(this.planningDir, "phases")); ensureDir(path.join(this.ciDir, "phases"));
ensureDir(path.join(this.projectPath, ".ci", "audit")); ensureDir(path.join(this.ciDir, "audit"));
} }
isInitialized(): boolean { isInitialized(): boolean {
return fs.existsSync(path.join(this.planningDir, "PROJECT.md")); return fs.existsSync(path.join(this.ciDir, "PROJECT.md"));
} }
writeProject(manifest: ProjectManifest): void { writeProject(manifest: ProjectManifest): void {
@@ -81,7 +81,7 @@ export class ArtifactManager {
} }
lines.push(""); lines.push("");
writeFile(path.join(this.planningDir, "PROJECT.md"), lines.join("\n")); writeFile(path.join(this.ciDir, "PROJECT.md"), lines.join("\n"));
} }
writeDecisions(decisions: DecisionsManifest): void { writeDecisions(decisions: DecisionsManifest): void {
@@ -99,11 +99,11 @@ export class ArtifactManager {
lines.push(`- **Timestamp**: ${d.timestamp}`); lines.push(`- **Timestamp**: ${d.timestamp}`);
lines.push(""); lines.push("");
} }
writeFile(path.join(this.planningDir, "DECISIONS.md"), lines.join("\n")); writeFile(path.join(this.ciDir, "DECISIONS.md"), lines.join("\n"));
} }
writeState(state: StateManifest): void { writeState(state: StateManifest): void {
writeJSON(path.join(this.planningDir, "STATE.md.json"), state); writeJSON(path.join(this.ciDir, "STATE.md.json"), state);
const lines = [ const lines = [
"# Project State", "# Project State",
@@ -124,11 +124,11 @@ export class ArtifactManager {
} }
lines.push(""); lines.push("");
writeFile(path.join(this.planningDir, "STATE.md"), lines.join("\n")); writeFile(path.join(this.ciDir, "STATE.md"), lines.join("\n"));
} }
readState(): StateManifest | null { readState(): StateManifest | null {
const filePath = path.join(this.planningDir, "STATE.md.json"); const filePath = path.join(this.ciDir, "STATE.md.json");
if (!fs.existsSync(filePath)) return null; if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, "utf-8")); return JSON.parse(fs.readFileSync(filePath, "utf-8"));
} }
@@ -150,7 +150,7 @@ export class ArtifactManager {
artifactName: string, artifactName: string,
content: string content: string
): void { ): void {
const phaseDir = path.join(this.planningDir, "phases", `phase-${phase}`); const phaseDir = path.join(this.ciDir, "phases", `phase-${phase}`);
ensureDir(phaseDir); ensureDir(phaseDir);
writeFile(path.join(phaseDir, artifactName), content); writeFile(path.join(phaseDir, artifactName), content);
} }
@@ -160,7 +160,7 @@ export class ArtifactManager {
artifactName: string artifactName: string
): string | null { ): string | null {
const filePath = path.join( const filePath = path.join(
this.planningDir, this.ciDir,
"phases", "phases",
`phase-${phase}`, `phase-${phase}`,
artifactName artifactName
-1
View File
@@ -25,7 +25,6 @@ describe("Audit", () => {
confidence: 0.92, confidence: 0.92,
category: "technology_choice", category: "technology_choice",
alternatives_considered: [{ option: "MongoDB", rejected_reason: "No ACID" }], alternatives_considered: [{ option: "MongoDB", rejected_reason: "No ACID" }],
learnship_equivalent: "discuss-phase would ask: What database?",
human_override: null, human_override: null,
}; };
-1
View File
@@ -26,7 +26,6 @@ describe("DecisionEngine", () => {
{ option: "MongoDB", rejected_reason: "No ACID transactions" }, { option: "MongoDB", rejected_reason: "No ACID transactions" },
{ option: "SQLite", rejected_reason: "No concurrent writes" }, { option: "SQLite", rejected_reason: "No concurrent writes" },
], ],
learnship_equivalent: "discuss-phase would ask: What database? Options: A) PostgreSQL B) MongoDB",
}; };
describe("makeDecision", () => { describe("makeDecision", () => {
+2 -8
View File
@@ -10,7 +10,6 @@ export interface DecisionInput {
confidence: number; confidence: number;
category: DecisionCategory; category: DecisionCategory;
alternatives_considered: Alternative[]; alternatives_considered: Alternative[];
learnship_equivalent: string;
phase?: string; phase?: string;
task?: string; task?: string;
} }
@@ -57,7 +56,6 @@ export class DecisionEngine {
confidence: input.confidence, confidence: input.confidence,
category: input.category, category: input.category,
alternatives_considered: input.alternatives_considered, alternatives_considered: input.alternatives_considered,
learnship_equivalent: input.learnship_equivalent,
human_override: null, human_override: null,
phase: input.phase, phase: input.phase,
task: input.task, task: input.task,
@@ -101,8 +99,7 @@ export class DecisionEngine {
decision: string, decision: string,
rationale: string, rationale: string,
category: DecisionCategory, category: DecisionCategory,
alternatives: Alternative[] = [], alternatives: Alternative[] = []
learnship_equivalent: string = ""
): DecisionResult { ): DecisionResult {
return this.makeDecision({ return this.makeDecision({
decision, decision,
@@ -110,7 +107,6 @@ export class DecisionEngine {
confidence: 0.95, confidence: 0.95,
category, category,
alternatives_considered: alternatives, alternatives_considered: alternatives,
learnship_equivalent,
}); });
} }
@@ -118,8 +114,7 @@ export class DecisionEngine {
decision: string, decision: string,
rationale: string, rationale: string,
category: DecisionCategory, category: DecisionCategory,
alternatives: Alternative[] = [], alternatives: Alternative[] = []
learnship_equivalent: string = ""
): DecisionResult { ): DecisionResult {
return this.makeDecision({ return this.makeDecision({
decision, decision,
@@ -127,7 +122,6 @@ export class DecisionEngine {
confidence: 0.7, confidence: 0.7,
category, category,
alternatives_considered: alternatives, alternatives_considered: alternatives,
learnship_equivalent,
}); });
} }
+1 -2
View File
@@ -10,8 +10,7 @@ describe("ErrorRecovery", () => {
beforeEach(() => { beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-recovery-test-")); tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-recovery-test-"));
fs.mkdirSync(path.join(tempDir, ".planning", "phases"), { recursive: true }); fs.mkdirSync(path.join(tempDir, ".ci"), { recursive: true });
fs.mkdirSync(path.join(tempDir, ".ci", "audit"), { recursive: true });
recovery = new ErrorRecovery(DEFAULT_CI_CONFIG, tempDir); recovery = new ErrorRecovery(DEFAULT_CI_CONFIG, tempDir);
}); });
-1
View File
@@ -18,7 +18,6 @@ export interface Decision {
confidence: number; confidence: number;
category: DecisionCategory; category: DecisionCategory;
alternatives_considered: Alternative[]; alternatives_considered: Alternative[];
learnship_equivalent: string;
human_override: string | null; human_override: string | null;
phase?: string; phase?: string;
task?: string; task?: string;
+10 -12
View File
@@ -141,38 +141,36 @@ export class BehavioralVerification extends VerificationLayer {
} }
private checkPlanMustHaves(projectPath: string, phase: number): VerificationCheck { private checkPlanMustHaves(projectPath: string, phase: number): VerificationCheck {
const planPath = path.join( const roadmapPath = path.join(
projectPath, projectPath,
".planning", ".ci",
"phases", "ROADMAP.md"
`phase-${phase}`,
"PLAN.md"
); );
if (!fs.existsSync(planPath)) { if (!fs.existsSync(roadmapPath)) {
return this.check( return this.check(
"Plan must-haves covered", "Plan must-haves covered",
"skipped", "skipped",
`No PLAN.md found for phase ${phase}` "No ROADMAP.md found — run 'ci init' first"
); );
} }
const content = fs.readFileSync(planPath, "utf-8"); const content = fs.readFileSync(roadmapPath, "utf-8");
const hasMustHaves = content.toLowerCase().includes("must"); const hasMustHaves = content.toLowerCase().includes("must");
const hasTasks = content.includes("- [") || content.includes("* ["); const hasPhases = content.includes("Phase") || content.includes("phase");
if (!hasTasks && !hasMustHaves) { if (!hasPhases && !hasMustHaves) {
return this.check( return this.check(
"Plan must-haves covered", "Plan must-haves covered",
"warning", "warning",
"PLAN.md has no tasks or must-have items" "ROADMAP.md has no phases or must-have items"
); );
} }
return this.check( return this.check(
"Plan must-haves covered", "Plan must-haves covered",
"pass", "pass",
"PLAN.md contains task definitions" "ROADMAP.md contains phase definitions"
); );
} }
+1 -3
View File
@@ -8,13 +8,11 @@ describe("VerificationPipeline", () => {
beforeEach(() => { beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-pipeline-test-")); tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-pipeline-test-"));
const phaseDir = path.join(tempDir, ".planning", "phases", "phase-1");
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, "PLAN.md"), "# Plan\n\n- [ ] Task 1\n- [ ] Task 2\n");
const ciDir = path.join(tempDir, ".ci"); const ciDir = path.join(tempDir, ".ci");
fs.mkdirSync(ciDir, { recursive: true }); fs.mkdirSync(ciDir, { recursive: true });
fs.writeFileSync(path.join(ciDir, "config.json"), JSON.stringify({ autonomy: { level: "full" } })); fs.writeFileSync(path.join(ciDir, "config.json"), JSON.stringify({ autonomy: { level: "full" } }));
fs.writeFileSync(path.join(ciDir, "specification.md"), "# Test\n## Objective\nBuild it\n\n## Requirements\n- Feature A\n"); fs.writeFileSync(path.join(ciDir, "specification.md"), "# Test\n## Objective\nBuild it\n\n## Requirements\n- Feature A\n");
fs.writeFileSync(path.join(ciDir, "ROADMAP.md"), "# Roadmap\n\n## Phases\n\n### Phase 1: Init\n**Goal**: Set up project\n**Status**: not_started\n");
}); });
afterEach(() => { afterEach(() => {
+1 -1
View File
@@ -28,7 +28,7 @@ const CODE_QUALITY_PATTERNS: Array<{
message: "Direct console.log usage — consider structured logging", message: "Direct console.log usage — consider structured logging",
}, },
{ {
pattern: /any\b/g, pattern: /(?:as\s+any\b|:\s*any\b|<any>|any\[\s*\])/g,
severity: "P1", severity: "P1",
category: "type_safety", category: "type_safety",
message: "Use of 'any' type — loses type safety", message: "Use of 'any' type — loses type safety",
+15 -17
View File
@@ -14,12 +14,12 @@ describe("StructuralVerification", () => {
fs.rmSync(tempDir, { recursive: true, force: true }); fs.rmSync(tempDir, { recursive: true, force: true });
}); });
function setupProjectStructure(hasPhaseDir = true, hasPlan = true, hasCIConfig = true, hasSpec = true) { function setupProjectStructure(hasCIDir = true, hasRoadmap = true, hasCIConfig = true, hasSpec = true) {
if (hasPhaseDir) { if (hasCIDir) {
const phaseDir = path.join(tempDir, ".planning", "phases", "phase-1"); const ciDir = path.join(tempDir, ".ci");
fs.mkdirSync(phaseDir, { recursive: true }); fs.mkdirSync(ciDir, { recursive: true });
if (hasPlan) { if (hasRoadmap) {
fs.writeFileSync(path.join(phaseDir, "PLAN.md"), "# Plan\n\nTasks:\n- [ ] Task 1\n- [ ] Task 2\n"); fs.writeFileSync(path.join(ciDir, "ROADMAP.md"), "# Roadmap\n\n## Phases\n\n### Phase 1: Init\n**Goal**: Set up project\n**Status**: not_started\n");
} }
} }
if (hasCIConfig) { if (hasCIConfig) {
@@ -43,10 +43,10 @@ describe("StructuralVerification", () => {
expect(result.name).toBe("Structural"); expect(result.name).toBe("Structural");
expect(result.checks.length).toBeGreaterThan(0); expect(result.checks.length).toBeGreaterThan(0);
const phaseDirCheck = result.checks.find((c) => c.name === "Phase directory exists"); const phaseDirCheck = result.checks.find((c) => c.name === ".ci directory exists");
expect(phaseDirCheck?.status).toBe("pass"); expect(phaseDirCheck?.status).toBe("pass");
const planCheck = result.checks.find((c) => c.name === "PLAN.md exists"); const planCheck = result.checks.find((c) => c.name === "ROADMAP.md exists");
expect(planCheck?.status).toBe("pass"); expect(planCheck?.status).toBe("pass");
const configCheck = result.checks.find((c) => c.name === "CI config valid"); const configCheck = result.checks.find((c) => c.name === "CI config valid");
@@ -56,29 +56,29 @@ describe("StructuralVerification", () => {
expect(specCheck?.status).toBe("pass"); expect(specCheck?.status).toBe("pass");
}); });
it("fails when phase directory is missing", async () => { it("fails when .ci directory is missing", async () => {
setupProjectStructure(false, false, true, true); setupProjectStructure(false, false, false, false);
const verifier = new StructuralVerification(); const verifier = new StructuralVerification();
const result = await verifier.verify(tempDir, 1); const result = await verifier.verify(tempDir, 1);
const phaseDirCheck = result.checks.find((c) => c.name === "Phase directory exists"); const phaseDirCheck = result.checks.find((c) => c.name === ".ci directory exists");
expect(phaseDirCheck?.status).toBe("fail"); expect(phaseDirCheck?.status).toBe("fail");
}); });
it("fails when PLAN.md is missing", async () => { it("warns when ROADMAP.md is missing", async () => {
setupProjectStructure(true, false, true, true); setupProjectStructure(true, false, true, true);
const verifier = new StructuralVerification(); const verifier = new StructuralVerification();
const result = await verifier.verify(tempDir, 1); const result = await verifier.verify(tempDir, 1);
const planCheck = result.checks.find((c) => c.name === "PLAN.md exists"); const planCheck = result.checks.find((c) => c.name === "ROADMAP.md exists");
expect(planCheck?.status).toBe("fail"); expect(planCheck?.status).toBe("warning");
}); });
it("fails when CI config has invalid JSON", async () => { it("fails when CI config has invalid JSON", async () => {
const ciDir = path.join(tempDir, ".ci"); const ciDir = path.join(tempDir, ".ci");
fs.mkdirSync(ciDir, { recursive: true }); fs.mkdirSync(ciDir, { recursive: true });
fs.writeFileSync(path.join(ciDir, "config.json"), "not valid json{{{"); fs.writeFileSync(path.join(ciDir, "config.json"), "not valid json{{{");
fs.mkdirSync(path.join(tempDir, ".planning", "phases", "phase-1"), { recursive: true }); fs.mkdirSync(path.join(tempDir, ".ci"), { recursive: true });
const verifier = new StructuralVerification(); const verifier = new StructuralVerification();
const result = await verifier.verify(tempDir, 1); const result = await verifier.verify(tempDir, 1);
@@ -91,8 +91,6 @@ describe("StructuralVerification", () => {
const srcDir = path.join(tempDir, "src"); const srcDir = path.join(tempDir, "src");
fs.mkdirSync(srcDir, { recursive: true }); fs.mkdirSync(srcDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, "app.ts"), "export function main() { /* TODO: implement */ }"); fs.writeFileSync(path.join(srcDir, "app.ts"), "export function main() { /* TODO: implement */ }");
fs.mkdirSync(path.join(tempDir, ".planning", "phases", "phase-1"), { recursive: true });
fs.writeFileSync(path.join(tempDir, ".planning", "phases", "phase-1", "PLAN.md"), "# Plan");
const ciDir = path.join(tempDir, ".ci"); const ciDir = path.join(tempDir, ".ci");
fs.mkdirSync(ciDir, { recursive: true }); fs.mkdirSync(ciDir, { recursive: true });
fs.writeFileSync(path.join(ciDir, "config.json"), "{}"); fs.writeFileSync(path.join(ciDir, "config.json"), "{}");
+11 -20
View File
@@ -13,9 +13,6 @@ const STUB_PATTERNS = [
/not\s+implemented/i, /not\s+implemented/i,
]; ];
const TODO_PATTERN = /\bTODO\b/gi;
const FIXME_PATTERN = /\bFIXME\b/gi;
export class StructuralVerification extends VerificationLayer { export class StructuralVerification extends VerificationLayer {
readonly layer = 1; readonly layer = 1;
readonly name = "Structural"; readonly name = "Structural";
@@ -44,30 +41,24 @@ export class StructuralVerification extends VerificationLayer {
} }
private checkPhaseDir(projectPath: string, phase: number) { private checkPhaseDir(projectPath: string, phase: number) {
const phaseDir = path.join(projectPath, ".planning", "phases", `phase-${phase}`); const ciDir = path.join(projectPath, ".ci");
const exists = fs.existsSync(phaseDir); const exists = fs.existsSync(ciDir);
return this.check( return this.check(
"Phase directory exists", ".ci directory exists",
exists ? "pass" : "fail", exists ? "pass" : "fail",
exists ? `Phase ${phase} directory found` : `Phase ${phase} directory not found`, exists ? ".ci directory found" : ".ci directory not found",
phaseDir ciDir
); );
} }
private checkPlanExists(projectPath: string, phase: number) { private checkPlanExists(projectPath: string, phase: number) {
const planPath = path.join( const roadmapPath = path.join(projectPath, ".ci", "ROADMAP.md");
projectPath, const exists = fs.existsSync(roadmapPath);
".planning",
"phases",
`phase-${phase}`,
"PLAN.md"
);
const exists = fs.existsSync(planPath);
return this.check( return this.check(
"PLAN.md exists", "ROADMAP.md exists",
exists ? "pass" : "fail", exists ? "pass" : "warning",
exists ? "PLAN.md found" : "PLAN.md not found", exists ? "ROADMAP.md found" : "ROADMAP.md not found (run 'ci init' first)",
planPath roadmapPath
); );
} }
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "0.3.0"; export const VERSION = "0.4.0";