diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index eadad97..7ebc030 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,6 +12,10 @@ - [ ] `bun src/index.ts audit ../..` score maintained or improved - [ ] No new external dependencies added +## Merge Readiness + +- [ ] All PR conversations are resolved (GitHub review threads, CodeRabbit threads, and agent comments when applicable) + ## Audit Impact diff --git a/AGENTS.md b/AGENTS.md index 34a7e54..a0c8ff0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,15 @@ reins — Open-source CLI that operationalizes harness engineering. Scaffold, audit, evolve, and doctor any project's agent-readiness. Zero dependencies, Bun-powered. +## Product Model (Must Keep Clear) + +- `cli/reins` is the product engine. It is the only source of truth for readiness scoring and JSON outputs. +- `skill/Reins` is the control-plane wrapper for coding agents. It decides when/how to call CLI commands and how to parse results. +- Humans steer outcomes. Agents execute the loop using Reins outputs. +- Reins exists to transfer proven harness patterns into any repo so agents can become more autonomous and consistent over time. + +When updating docs or workflows, preserve this separation explicitly. Do not duplicate scoring logic in the skill layer. + ## Architecture See ARCHITECTURE.md for domain map, module structure, and dependency rules. @@ -14,6 +23,9 @@ See ARCHITECTURE.md for domain map, module structure, and dependency rules. |-------|----------|--------| | Architecture | ARCHITECTURE.md | Current | | CLI Source | cli/reins/src/index.ts | Current | +| CLI Modules | cli/reins/src/lib/ | Current | +| CLI Commands | cli/reins/src/lib/commands/ | Current | +| Audit Engine | cli/reins/src/lib/audit/ | Current | | CLI Tests | cli/reins/src/index.test.ts | Current | | Claude Skill | skill/Reins/SKILL.md | Current | | Harness Methodology | skill/Reins/HarnessMethodology.md | Current | @@ -35,20 +47,22 @@ See ARCHITECTURE.md for domain map, module structure, and dependency rules. 1. Receive task via prompt 2. Read this file, then follow pointers to relevant docs -3. All CLI logic lives in `cli/reins/src/index.ts` (single-file design) +3. Keep CLI command routing in `cli/reins/src/index.ts`; put reusable internals in `cli/reins/src/lib/` (especially `lib/commands/` and `lib/audit/`) 4. Run tests: `cd cli/reins && bun test` 5. Self-audit: `cd cli/reins && bun src/index.ts audit ../..` 6. Self-review changes for correctness and style 7. Open PR with concise summary +8. Resolve all PR conversations/comments before merge (GitHub review threads, CodeRabbit threads, and agent comments when applicable) ## Key Constraints - Zero external runtime dependencies — stdlib only (fs, path) -- Single-file CLI at `cli/reins/src/index.ts` +- Single entrypoint CLI at `cli/reins/src/index.ts` with shared internals in `cli/reins/src/lib/` - All commands output deterministic JSON - Tests are co-located (`index.test.ts` next to `index.ts`) - All knowledge lives in-repo, not in external tools - Bun is the runtime; Node.js/tsx fallback via `bin/reins.cjs` +- Merge discipline: treat unresolved PR conversations as a hard block when branch protection requires conversation resolution ## Golden Principles diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2bb50e1..22ade65 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -10,29 +10,40 @@ ## Module Structure -reins is a single-file CLI tool. The architecture is function-based, not layered. +reins uses a single CLI entrypoint with modular internals. ``` -cli/reins/src/index.ts - | - +-- Types (interfaces: AuditScore, AuditResult, InitOptions, EvolutionStep, EvolutionPath) - +-- Templates (agentsMdTemplate, architectureMdTemplate, goldenPrinciplesTemplate, ...) - +-- Commands (init, runAudit, audit, doctor, evolve) - +-- CLI Router (main, printHelp, flag parsing) +cli/reins/src/ + index.ts # CLI router + command orchestration + index.test.ts # End-to-end command tests + lib/ + commands/ + init.ts # init command handler + audit.ts # audit scoring + command output + doctor.ts # doctor checks + command output + evolve.ts # evolve command handler + audit/ + context.ts # audit runtime context and repo signal collection + scoring.ts # scoring functions and maturity resolution + types.ts # Shared CLI domain types + templates.ts # Scaffold/templates for docs/scripts/workflows + filesystem.ts # Safe file walking and discovery helpers + detection.ts # Workflow/CLI/monorepo signal detection + automation-pack.ts # Pack normalization/recommendation/scaffolding + scoring-utils.ts # Shared scoring helpers ``` ### Dependency Direction -Forward-only within the file: +Forward-only between modules: ``` -Types → Templates → Commands → CLI Router +lib/helpers + lib/commands → index.ts router → CLI output ``` -- Types are pure interfaces, no imports -- Templates depend on nothing (return strings) -- Commands import Types, call Templates, use fs/path -- CLI Router calls Commands based on argv +- `lib/*` modules are reusable helpers with narrow responsibilities +- `lib/commands/*` own command semantics and JSON response contracts +- `index.ts` focuses on argument parsing and routing only ### Skill Structure diff --git a/CLAUDE.md b/CLAUDE.md index 6008628..f895cd2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,29 +1,5 @@ # CLAUDE.md -## Project +Canonical project instructions live in `AGENTS.md`. -reins — Harness Engineering CLI. Scaffold, audit, evolve, and doctor agent-readiness for any project. - -## Quick Start - -```bash -cd cli/reins -bun install -bun test # Run tests -bun src/index.ts audit ../.. # Self-audit -``` - -## Key Files - -- `cli/reins/src/index.ts` — All CLI logic (single file) -- `cli/reins/src/index.test.ts` — Test suite (36 tests) -- `AGENTS.md` — Repository map (start here) -- `ARCHITECTURE.md` — Domain map and structure - -## Rules - -- Zero external runtime dependencies -- All commands output deterministic JSON -- Tests co-located with source -- Run `bun test` before committing -- Self-audit with `bun src/index.ts audit ../..` to verify score +If `CLAUDE.md` and `AGENTS.md` ever differ, follow `AGENTS.md`. diff --git a/README.md b/README.md index e521835..6a90768 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,22 @@ The open-source toolkit for [Harness Engineering](https://openai.com/index/harne OpenAI published the methodology. We built the tooling. +## Why Reins exists + +Reins came from real project pressure: as the harness improved, coding agents became more autonomous, consistent, and reliable. The problem was portability. Those gains were trapped in one repo. + +Reins packages the same approach so you can apply it to any project: scaffold the harness, score readiness, diagnose gaps, and iteratively evolve toward stronger agent autonomy. + +## The model (for humans and agents) + +| Layer | Role | Source of truth | +|------|------|-----------------| +| **Skill** | Control plane. Teaches the agent *when* to run Reins and *how* to interpret output. | `skill/Reins/SKILL.md` | +| **CLI** | Execution plane. Produces deterministic JSON for `init`, `audit`, `doctor`, `evolve`. | `cli/reins/src/lib/commands/` (+ routed via `cli/reins/src/index.ts`) | +| **Human** | Steering plane. Sets goals, accepts tradeoffs, and decides product/taste direction. | Prompts + repo decisions | + +If this split is unclear, agents drift: they either skip Reins or use it incorrectly. Reins is designed so agents can repeatedly improve repo quality with explicit, machine-readable feedback loops. + ## Quick start **1. Install the skill** so your agent knows how to use Reins: @@ -36,7 +52,8 @@ Agent: runs audit, identifies current level, executes the evolution path **2. Or run the CLI directly** for a quick score without the skill: ```bash -npx reins-cli audit . +# "." means "current directory" +npx reins-cli@latest audit . ``` ```json @@ -52,6 +69,26 @@ npx reins-cli audit . } ``` +## Keep Reins fresh + +```bash +# Check whether your installed skills are outdated +npx skills check + +# Update installed skills (including Reins) when updates are available +npx skills update +``` + +If you run Reins directly (without the skill), prefer `npx reins-cli@latest ...` so agents always use the latest published CLI. + +## The steering loop + +```text +Install/refresh skill -> Audit -> Doctor/Evolve -> Apply changes -> Re-audit +``` + +That loop is the product: repeatedly steering agents toward a better repository state. + ## Why teams adopt Reins Most agent rollouts fail for one boring reason: agents can edit code, but the repository doesn't teach them how to reason safely. @@ -118,6 +155,8 @@ graph LR ```bash reins init . # Scaffold the full structure +reins init . --pack auto # Adaptive pack selection from project signals +reins init . --pack agent-factory # Optional advanced automation pack reins audit . # Score against harness principles (0-18) reins evolve . # Roadmap to next maturity level reins doctor . # Health check with prescriptive fixes @@ -170,6 +209,25 @@ docs/ generated/ # Auto-generated docs (schema, API specs) ``` +Optional pack: + +```bash +reins init . --pack auto +reins init . --pack agent-factory +``` + +`--pack auto` keeps base scaffold for unknown stacks and selects `agent-factory` when the repo looks Node/JS compatible. + +`--pack agent-factory` adds an advanced automation layer: +- `scripts/lint-structure.mjs` (hard structural gate) +- `scripts/doc-gardener.mjs` + `scripts/check-changed-doc-freshness.mjs` (docs freshness loop) +- `scripts/pr-review.mjs` (soft golden-principles reviewer) +- `.github/workflows/risk-policy-gate.yml` (risk-tier + docs drift checks) +- `.github/workflows/pr-review-bot.yml` (PR feedback loop) +- `.github/workflows/structural-lint.yml` (CI enforcement gate) + +`reins evolve` now includes pack recommendations and `reins evolve . --apply` can scaffold compatible pack automation into an existing repo. + ## The six audit dimensions Each scored 0-3, totaling 0-18: @@ -230,7 +288,9 @@ For CLI repositories, Reins treats strong diagnosability signals (for example `d ``` reins/ cli/reins/ # The CLI tool (Bun + TypeScript, zero deps) - src/index.ts # Single-file CLI + src/index.ts # Thin CLI router + src/lib/commands/ # Command handlers (init/audit/doctor/evolve) + src/lib/audit/ # Audit runtime context + scoring internals package.json skill/ # Agent skill (Claude Code) Reins/ diff --git a/cli/reins/README.md b/cli/reins/README.md index 2a5176d..bc95f42 100644 --- a/cli/reins/README.md +++ b/cli/reins/README.md @@ -2,6 +2,18 @@ Scaffold, audit, and evolve projects using the [Harness Engineering](https://openai.com/index/harness-engineering/) methodology. +## Relationship to the Reins skill + +- `reins-cli` is the execution engine (deterministic JSON commands). +- The Reins skill is the control plane that teaches coding agents when/how to call this CLI. +- Human operators steer intent; agents execute with Reins command outputs. + +For end-user agent workflows, install the skill first: + +```bash +npx skills add WellDunDun/reins +``` + ## What is Harness Engineering? A development methodology where **humans steer and agents execute**. All code — application logic, tests, CI, docs, tooling — is written by AI agents. Humans design environments, specify intent, and build feedback loops. @@ -11,7 +23,7 @@ A development methodology where **humans steer and agents execute**. All code ```bash # From npm # "." means "current directory" -npx reins-cli audit . +npx reins-cli@latest audit . # Or clone and link git clone https://github.com/WellDunDun/reins.git @@ -32,6 +44,8 @@ Scaffold the full harness engineering structure in a directory: reins init . reins init ./my-project --name "My Project" reins init . --force # Overwrite existing files +reins init . --pack auto # Adaptive pack selection from project signals +reins init . --pack agent-factory # Optional advanced automation pack ``` Creates: @@ -45,6 +59,12 @@ Creates: - `docs/references/` — External LLM-friendly reference docs - `docs/generated/` — Auto-generated documentation +Pack modes: +- `--pack auto` selects a compatible pack when stack signals are clear, otherwise keeps base scaffold. +- `--pack agent-factory` explicitly scaffolds advanced automation: +- `scripts/lint-structure.mjs`, `scripts/doc-gardener.mjs`, `scripts/check-changed-doc-freshness.mjs`, `scripts/pr-review.mjs` +- `.github/workflows/risk-policy-gate.yml`, `.github/workflows/pr-review-bot.yml`, `.github/workflows/structural-lint.yml` + ### `reins audit ` Score a project against harness engineering principles (0-18): diff --git a/cli/reins/package.json b/cli/reins/package.json index 3bd37b8..faa1b96 100644 --- a/cli/reins/package.json +++ b/cli/reins/package.json @@ -1,6 +1,6 @@ { "name": "reins-cli", - "version": "0.1.1", + "version": "0.1.2", "description": "Scaffold, audit, and evolve projects using the Harness Engineering methodology", "type": "module", "license": "MIT", diff --git a/cli/reins/src/index.test.ts b/cli/reins/src/index.test.ts index 8a3a5f9..2395d47 100644 --- a/cli/reins/src/index.test.ts +++ b/cli/reins/src/index.test.ts @@ -5,6 +5,7 @@ import { $ } from "bun"; const CLI = join(import.meta.dir, "index.ts"); const TMP = join(import.meta.dir, "..", ".test-fixtures"); +const REPO_ROOT = join(import.meta.dir, "..", "..", ".."); function tmpDir(name: string): string { const dir = join(TMP, name); @@ -107,6 +108,69 @@ describe("reins init", () => { expect(exitCode).toBe(1); expect(stderr).toContain("does not exist"); }); + + test("scaffolds agent-factory automation pack when requested", async () => { + const dir = tmpDir("init-agent-factory-pack"); + const { stdout, exitCode } = await runCli(`init ${dir} --pack agent-factory`); + expect(exitCode).toBe(0); + + const result = JSON.parse(stdout); + expect(result.automation_pack).toBe("agent-factory"); + expect(result.created).toContain("scripts/lint-structure.mjs"); + expect(result.created).toContain("scripts/doc-gardener.mjs"); + expect(result.created).toContain("scripts/check-changed-doc-freshness.mjs"); + expect(result.created).toContain("scripts/pr-review.mjs"); + expect(result.created).toContain(".github/workflows/risk-policy-gate.yml"); + expect(result.created).toContain(".github/workflows/pr-review-bot.yml"); + expect(result.created).toContain(".github/workflows/structural-lint.yml"); + + expect(existsSync(join(dir, "scripts", "lint-structure.mjs"))).toBe(true); + expect(existsSync(join(dir, "scripts", "doc-gardener.mjs"))).toBe(true); + expect(existsSync(join(dir, ".github", "workflows", "risk-policy-gate.yml"))).toBe(true); + + const riskPolicy = JSON.parse(readFileSync(join(dir, "risk-policy.json"), "utf-8")); + expect(riskPolicy).toHaveProperty("riskTierRules"); + expect(riskPolicy).toHaveProperty("mergePolicy"); + expect(riskPolicy.riskTierRules.high).toEqual( + expect.arrayContaining(["src/security", "src/auth", ".github/workflows"]), + ); + expect(riskPolicy.docsDriftRules.watchPaths).toEqual( + expect.arrayContaining(["src", "scripts", ".github/workflows"]), + ); + }); + + test("auto pack stays base scaffold when stack signals are insufficient", async () => { + const dir = tmpDir("init-auto-pack-none"); + const { stdout, exitCode } = await runCli(`init ${dir} --pack auto`); + expect(exitCode).toBe(0); + + const result = JSON.parse(stdout); + expect(result.requested_automation_pack).toBe("auto"); + expect(result.automation_pack).toBeNull(); + expect(result.automation_pack_reason).toContain("Insufficient stack signals"); + expect(existsSync(join(dir, "scripts", "lint-structure.mjs"))).toBe(false); + }); + + test("auto pack selects agent-factory for Node/JS projects", async () => { + const dir = tmpDir("init-auto-pack-node"); + writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "sample", scripts: { dev: "node index.js" } })); + + const { stdout, exitCode } = await runCli(`init ${dir} --pack auto`); + expect(exitCode).toBe(0); + + const result = JSON.parse(stdout); + expect(result.requested_automation_pack).toBe("auto"); + expect(result.automation_pack).toBe("agent-factory"); + expect(result.automation_pack_reason).toContain("Detected package.json"); + expect(existsSync(join(dir, "scripts", "lint-structure.mjs"))).toBe(true); + }); + + test("rejects unknown automation packs", async () => { + const dir = tmpDir("init-unknown-pack"); + const { stderr, exitCode } = await runCli(`init ${dir} --pack unknown-pack`); + expect(exitCode).toBe(1); + expect(stderr).toContain("Unknown automation pack"); + }); }); // ─── Audit Command ────────────────────────────────────────────────────────── @@ -298,6 +362,77 @@ describe("reins evolve", () => { expect(result.weakest_dimensions[0]).toHaveProperty("score"); expect(result.weakest_dimensions[0]).toHaveProperty("findings"); }); + + test("recommends agent-factory pack when project is compatible and pack is missing", async () => { + const dir = tmpDir("evolve-pack-recommendation"); + writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "sample", scripts: { dev: "node index.js" } })); + await runCli(`init ${dir}`); + + const { stdout } = await runCli(`evolve ${dir}`); + const result = JSON.parse(stdout); + + expect(result.pack_recommendation?.recommended).toBe("agent-factory"); + expect(result.steps).toEqual( + expect.arrayContaining([ + expect.objectContaining({ action: expect.stringContaining("agent-factory automation pack") }), + ]), + ); + }); + + test("applies recommended agent-factory pack during evolve --apply", async () => { + const dir = tmpDir("evolve-pack-apply"); + writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "sample", scripts: { dev: "node index.js" } })); + await runCli(`init ${dir}`); + + const { stdout, exitCode } = await runCli(`evolve ${dir} --apply`); + expect(exitCode).toBe(0); + + const result = JSON.parse(stdout); + expect(result.pack_recommendation?.recommended).toBe("agent-factory"); + expect(existsSync(join(dir, "scripts", "lint-structure.mjs"))).toBe(true); + expect(existsSync(join(dir, ".github", "workflows", "risk-policy-gate.yml"))).toBe(true); + }); + + test("applies base scaffold during evolve --apply even when AGENTS.md already exists", async () => { + const dir = tmpDir("evolve-apply-existing-agents"); + writeFileSync(join(dir, "AGENTS.md"), "# AGENTS.md\n\nExisting hand-written agent guide."); + + const { stdout, exitCode } = await runCli(`evolve ${dir} --apply`); + expect(exitCode).toBe(0); + + const evolveJsonStart = stdout.lastIndexOf('{\n "command": "evolve"'); + expect(evolveJsonStart).toBeGreaterThanOrEqual(0); + const result = JSON.parse(stdout.slice(evolveJsonStart)); + expect(result.applied).toEqual(expect.arrayContaining([expect.stringContaining("Ran 'reins init'")])); + expect(existsSync(join(dir, "ARCHITECTURE.md"))).toBe(true); + expect(existsSync(join(dir, "docs", "design-docs", "index.md"))).toBe(true); + expect(existsSync(join(dir, "docs", "exec-plans", "tech-debt-tracker.md"))).toBe(true); + }); + + test("returns JSON error when base scaffolding fails during evolve --apply", async () => { + const dir = tmpDir("evolve-apply-init-failure"); + writeFileSync(join(dir, "AGENTS.md"), "# AGENTS.md\n\nExisting hand-written agent guide."); + writeFileSync(join(dir, "docs"), "this blocks docs/ directory creation"); + + const { stderr, exitCode } = await runCli(`evolve ${dir} --apply`); + expect(exitCode).toBe(1); + + const error = JSON.parse(stderr.trim()); + expect(error.error).toContain("Scaffolding failed"); + }); + + test("returns JSON error when pack scaffolding fails during evolve --apply", async () => { + const dir = tmpDir("evolve-apply-pack-failure"); + writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "sample", scripts: { dev: "node index.js" } })); + await runCli(`init ${dir}`); + writeFileSync(join(dir, ".github"), "this blocks .github/workflows directory creation"); + + const { stderr, exitCode } = await runCli(`evolve ${dir} --apply`); + expect(exitCode).toBe(1); + + const error = JSON.parse(stderr.trim()); + expect(error.error).toContain("Pack scaffolding failed"); + }); }); // ─── New Audit Checks ─────────────────────────────────────────────────────── @@ -883,3 +1018,41 @@ describe("reins help", () => { expect(stderr).toContain("Unknown command"); }); }); + +// ─── Docs Contract ────────────────────────────────────────────────────────── + +describe("docs contract — skill/cli/human model clarity", () => { + test("README keeps canonical model and steering loop", () => { + const readme = readFileSync(join(REPO_ROOT, "README.md"), "utf-8"); + expect(readme).toContain("## The model (for humans and agents)"); + expect(readme).toContain("| **Skill** |"); + expect(readme).toContain("| **CLI** |"); + expect(readme).toContain("| **Human** |"); + expect(readme).toContain("## The steering loop"); + expect(readme).toContain("Install/refresh skill -> Audit -> Doctor/Evolve -> Apply changes -> Re-audit"); + }); + + test("AGENTS.md keeps product split explicit", () => { + const agents = readFileSync(join(REPO_ROOT, "AGENTS.md"), "utf-8"); + expect(agents).toContain("## Product Model (Must Keep Clear)"); + expect(agents).toContain("`cli/reins` is the product engine"); + expect(agents).toContain("`skill/Reins` is the control-plane wrapper"); + expect(agents).toContain("Humans steer outcomes"); + }); + + test("skill instructions keep execution model contract", () => { + const skill = readFileSync(join(REPO_ROOT, "skill", "Reins", "SKILL.md"), "utf-8"); + expect(skill).toContain("## Execution Model (Critical)"); + expect(skill).toContain("The CLI is the execution engine and scoring source of truth."); + expect(skill).toContain("This skill is the control plane for agent behavior"); + expect(skill).toContain("Always run commands and parse JSON outputs."); + }); + + test("CLI README explains skill relationship for end-user agents", () => { + const cliReadme = readFileSync(join(REPO_ROOT, "cli", "reins", "README.md"), "utf-8"); + expect(cliReadme).toContain("## Relationship to the Reins skill"); + expect(cliReadme).toContain("`reins-cli` is the execution engine"); + expect(cliReadme).toContain("The Reins skill is the control plane"); + expect(cliReadme).toContain("npx skills add WellDunDun/reins"); + }); +}); diff --git a/cli/reins/src/index.ts b/cli/reins/src/index.ts index 8b99c65..cc8e93e 100644 --- a/cli/reins/src/index.ts +++ b/cli/reins/src/index.ts @@ -1,1690 +1,9 @@ #!/usr/bin/env bun -import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; -import { basename, join, resolve } from "node:path"; - -// ─── Types ────────────────────────────────────────────────────────────────── - -interface AuditScore { - score: number; - max: number; - findings: string[]; -} - -interface AuditResult { - project: string; - timestamp: string; - scores: { - repository_knowledge: AuditScore; - architecture_enforcement: AuditScore; - agent_legibility: AuditScore; - golden_principles: AuditScore; - agent_workflow: AuditScore; - garbage_collection: AuditScore; - }; - total_score: number; - max_score: 18; - maturity_level: string; - recommendations: string[]; -} - -interface InitOptions { - path: string; - name: string; - force: boolean; -} - -type DoctorStatus = "pass" | "fail" | "warn"; - -interface DoctorCheck { - check: string; - status: DoctorStatus; - fix: string; -} - -// ─── Templates ────────────────────────────────────────────────────────────── - -function agentsMdTemplate(projectName: string): string { - return `# AGENTS.md - -## Repository Overview - -${projectName} — [Brief description of the project]. - -## Architecture - -See ARCHITECTURE.md for domain map, package layering, and dependency rules. - -## Documentation Map - -| Topic | Location | Status | -|-------|----------|--------| -| Architecture | ARCHITECTURE.md | Current | -| Design Docs | docs/design-docs/index.md | Current | -| Core Beliefs | docs/design-docs/core-beliefs.md | Current | -| Product Specs | docs/product-specs/index.md | Current | -| Active Plans | docs/exec-plans/active/ | Current | -| Completed Plans | docs/exec-plans/completed/ | Current | -| Technical Debt | docs/exec-plans/tech-debt-tracker.md | Current | -| Risk Policy | risk-policy.json | Current | -| Golden Principles | docs/golden-principles.md | Current | -| References | docs/references/ | Current | - -## Development Workflow - -1. Receive task via prompt -2. Read this file, then follow pointers to relevant docs -3. Implement changes following ARCHITECTURE.md layer rules -4. Run linters and structural tests (\`bun run lint && bun run test\`) -5. Self-review changes for correctness and style -6. Request agent review if available -7. Iterate until all reviewers satisfied -8. Open PR with concise summary - -## Key Constraints - -- Dependencies flow forward only: Types > Config > Repo > Service > Runtime > UI -- Cross-cutting concerns enter ONLY through Providers -- Validate data at boundaries — never probe shapes without validation -- Prefer shared utilities over hand-rolled helpers -- All knowledge lives in-repo, not in external tools - -## Golden Principles - -See docs/golden-principles.md for the full set of mechanical taste rules. -`; -} - -function architectureMdTemplate(projectName: string): string { - return `# Architecture — ${projectName} - -## Domain Map - - - -| Domain | Description | Quality Grade | -|--------|-------------|---------------| -| Core | Core business logic | — | -| Auth | Authentication and authorization | — | -| UI | User interface components | — | - -## Layered Architecture - -Each domain follows a strict layer ordering. Dependencies flow forward only. - -\`\`\` -Utils - | - v -Business Domain - +-- Types --> Config --> Repo --> Service --> Runtime --> UI - | - +-- Providers (cross-cutting: auth, connectors, telemetry, feature flags) - | - v - App Wiring + UI -\`\`\` - -### Layer Definitions - -| Layer | Responsibility | May Import From | -|-------|---------------|-----------------| -| Types | Data shapes, enums, interfaces | Utils | -| Config | Configuration loading, validation | Types, Utils | -| Repo | Data access, storage | Config, Types, Utils | -| Service | Business logic orchestration | Repo, Config, Types, Utils | -| Runtime | Process lifecycle, scheduling | Service, Config, Types, Utils | -| UI | User-facing presentation | Runtime, Service, Types, Utils | -| Providers | Cross-cutting adapters | Any layer (explicit interface) | - -### Enforcement - -These rules are enforced mechanically: -- [ ] Custom linter for import direction (TODO: implement) -- [ ] Structural tests for layer violations (TODO: implement) -- [ ] CI gate that fails on violations (TODO: implement) - -## Package Structure - -\`\`\` -src/ - domains/ - [domain-name]/ - types/ - config/ - repo/ - service/ - runtime/ - ui/ - providers/ - utils/ - shared/ -\`\`\` -`; -} - -function goldenPrinciplesTemplate(): string { - return `# Golden Principles - -Opinionated mechanical rules that encode human taste. These go beyond standard linters and are enforced in CI. - -## Structural Rules - -1. **Shared utilities over hand-rolled helpers** - Centralize invariants in shared packages. Never duplicate utility logic across domains. - -2. **Validate at boundaries, never YOLO** - Parse and validate all external data at system boundaries. Use typed SDKs. Never access unvalidated shapes. - -3. **Boring technology preferred** - Choose composable, stable, well-documented dependencies. Prefer libraries well-represented in LLM training data. Reimplement simple utilities rather than pulling opaque dependencies. - -4. **Single source of truth** - Every piece of knowledge has exactly one canonical location. If it's duplicated, one copy is a reference to the other. - -## Naming Conventions - -- Files: kebab-case (\`user-service.ts\`) -- Types/Interfaces: PascalCase (\`UserProfile\`) -- Functions/Variables: camelCase (\`getUserProfile\`) -- Constants: SCREAMING_SNAKE_CASE (\`MAX_RETRY_COUNT\`) -- Domains: kebab-case directories (\`app-settings/\`) - -## Code Style - -- Prefer explicit over implicit -- No magic strings — use enums or constants -- Error messages must be actionable (what happened, what to do) -- Functions do one thing -- No nested ternaries -- Prefer early returns over deep nesting - -## Testing Rules - -- Every public function has at least one test -- Tests are co-located with source (\`*.test.ts\` next to \`*.ts\`) -- Test names describe the expected state, not the action -- No test interdependence — each test is isolated - -## Documentation Rules - -- Every design decision is documented with rationale -- Docs are verified against code on a recurring cadence -- Stale docs are worse than no docs — delete or update - -## Review Rules - -- Agent reviews check: layer violations, golden principle adherence, test coverage -- Human reviews focus on: intent alignment, architectural fit, user impact -- Nit-level feedback is captured as golden principle updates, not blocking comments -`; -} - -function coreBeliefsTemplate(): string { - return `# Core Beliefs - -Agent-first operating principles that guide all development decisions. - -## 1. Repository is the Single Source of Truth - -If it's not in the repo, it doesn't exist to the agent. Slack discussions, meeting notes, and tribal knowledge must be captured in versioned markdown. - -## 2. Agents Are First-Class Team Members - -Design docs, architecture guides, and workflows are written for agent consumption first. Human readability is a bonus, not the goal. - -## 3. Constraints Enable Speed - -Strict architectural rules, enforced mechanically, allow agents to ship fast without creating drift. Freedom within boundaries. - -## 4. Corrections Are Cheap - -In a high-throughput agent environment, fixing forward is usually cheaper than blocking. Optimize for flow, not perfection at merge time. - -## 5. Taste Is Captured Once, Enforced Continuously - -Human engineering judgment is encoded into golden principles and tooling, then applied to every line of code automatically. Taste doesn't scale through review — it scales through automation. - -## 6. Technical Debt Is a High-Interest Loan - -Pay it down continuously in small increments. Background agents handle cleanup. Never let it compound. - -## 7. Progressive Disclosure Over Information Dumps - -Give agents a map (short AGENTS.md) and teach them where to look. Don't overwhelm context with everything at once. -`; -} - -function techDebtTrackerTemplate(): string { - return `# Technical Debt Tracker - -Track known technical debt with priority and ownership. - -| ID | Description | Domain | Priority | Status | Created | Updated | -|----|-------------|--------|----------|--------|---------|---------| -| TD-001 | Example: implement dependency linter | Core | High | Open | ${new Date().toISOString().split("T")[0]} | ${new Date().toISOString().split("T")[0]} | - -## Priority Definitions - -- **Critical**: Actively causing bugs or blocking features -- **High**: Will cause problems soon, should address this sprint -- **Medium**: Noticeable drag on velocity, schedule for cleanup -- **Low**: Minor annoyance, address opportunistically - -## Process - -1. New debt discovered → add row here -2. Background agents scan weekly for new debt -3. Cleanup PRs opened targeting highest priority items -4. Resolved debt marked as "Closed" with resolution date -`; -} - -function riskPolicyTemplate(): string { - return `{ - "version": 1, - "tiers": ["low", "medium", "high"], - "watchPaths": ["src/", "docs/", "skill/"], - "docsDriftRules": [ - { - "watch": "src/", - "docs": ["ARCHITECTURE.md", "docs/design-docs/index.md", "docs/golden-principles.md"] - }, - { - "watch": "skill/", - "docs": ["AGENTS.md", "skill/Reins/HarnessMethodology.md"] - } - ] -} -`; -} - -function designDocsIndexTemplate(): string { - return `# Design Documents Index - -Registry of all design documents with verification status. - -| Document | Status | Last Verified | Owner | -|----------|--------|---------------|-------| -| core-beliefs.md | Current | ${new Date().toISOString().split("T")[0]} | Team | - -## Verification Schedule - -Design docs are verified against the actual codebase on a recurring cadence: -- **Weekly**: Active design docs for in-progress features -- **Monthly**: All design docs -- **On change**: When related code is significantly modified - -## Status Definitions - -- **Current**: Verified to match actual implementation -- **Stale**: Known to be out of date, needs update -- **Draft**: In progress, not yet finalized -- **Archived**: No longer relevant, kept for historical reference -`; -} - -function productSpecsIndexTemplate(): string { - return `# Product Specifications Index - -Registry of all product specifications. - -| Spec | Status | Priority | Owner | -|------|--------|----------|-------| -| — | — | — | — | - -## Adding a New Spec - -1. Create a new markdown file in this directory -2. Add it to the table above -3. Include: problem statement, proposed solution, acceptance criteria, out of scope -4. Link related design docs and execution plans -`; -} - -// ─── Helpers ──────────────────────────────────────────────────────────────── - -const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", ".expo"]); - -function safeReadDir(dir: string): string[] { - try { - return readdirSync(dir); - } catch { - return []; - } -} - -function safeStat(path: string): ReturnType | null { - try { - return statSync(path); - } catch { - return null; - } -} - -function collectFileMatch( - current: string, - entry: string, - depth: number, - maxDepth: number, - pattern: RegExp, - results: string[], - walk: (next: string, nextDepth: number) => void, -): void { - if (IGNORED_DIRS.has(entry)) return; - - const fullPath = join(current, entry); - const stat = safeStat(fullPath); - if (!stat) return; - - if (stat.isDirectory()) { - walk(fullPath, depth + 1); - return; - } - - if (depth <= maxDepth && pattern.test(entry)) { - results.push(fullPath); - } -} - -function findFiles(dir: string, pattern: RegExp, maxDepth = 3): string[] { - const results: string[] = []; - - function walk(current: string, depth: number): void { - if (depth > maxDepth) return; - - for (const entry of safeReadDir(current)) { - collectFileMatch(current, entry, depth, maxDepth, pattern, results, walk); - } - } - - walk(dir, 0); - return results; -} - -function countGoldenPrinciples(content: string): number { - const headings = content.match(/^##\s+/gm)?.length || 0; - const numbered = content.match(/^\d+\.\s+/gm)?.length || 0; - return Math.max(headings, numbered); -} - -function scanWorkflowsForEnforcement(workflowDir: string): string[] { - const steps: Set = new Set(); - const keywordPatterns: Array<{ step: string; pattern: RegExp }> = [ - { step: "lint", pattern: /\b(lint|eslint|biome\s+check)\b/ }, - { step: "test", pattern: /\b(test|vitest|jest)\b/ }, - { step: "typecheck", pattern: /\b(typecheck|type-check|tsc\s+--no-?emit)\b/ }, - { step: "build", pattern: /\bbuild\b/ }, - { step: "audit", pattern: /\baudit\b/ }, - { step: "prettier", pattern: /\bprettier\b/ }, - { step: "format", pattern: /\bformat\b/ }, - ]; - try { - const files = readdirSync(workflowDir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml")); - for (const file of files) { - const content = readFileSync(join(workflowDir, file), "utf-8").toLowerCase(); - for (const { step, pattern } of keywordPatterns) { - if (pattern.test(content)) steps.add(step); - } - } - } catch { - /* no workflows */ - } - return [...steps]; -} - -function detectMonorepoWorkspaces(pkgJsonPath: string): string[] { - try { - const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8")); - const workspaces = pkg.workspaces?.packages || pkg.workspaces; - if (Array.isArray(workspaces)) return workspaces; - } catch {} - return []; -} - -function isCliPackage(pkg: Record): boolean { - const hasBin = - typeof pkg.bin === "string" || - (typeof pkg.bin === "object" && pkg.bin !== null && Object.keys(pkg.bin as Record).length > 0); - const hasCliName = typeof pkg.name === "string" && /(^|[-_])cli($|[-_])/.test(pkg.name); - const hasCliKeywords = - Array.isArray(pkg.keywords) && - pkg.keywords.some((k) => typeof k === "string" && /(cli|command-?line|terminal)/i.test(k)); - return hasBin || hasCliName || hasCliKeywords; -} - -function detectCliProject(targetDir: string, rootPkgJsonPath: string): boolean { - if (existsSync(rootPkgJsonPath)) { - try { - const rootPkg = JSON.parse(readFileSync(rootPkgJsonPath, "utf-8")); - if (isCliPackage(rootPkg)) return true; - } catch { - // ignore parse errors - } - } - - const pkgJsonFiles = findFiles(targetDir, /^package\.json$/, 4).filter( - (f) => f !== rootPkgJsonPath && !f.includes("node_modules"), - ); - for (const pkgPath of pkgJsonFiles) { - try { - const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); - if (isCliPackage(pkg)) return true; - } catch { - // ignore parse errors - } - } - return false; -} - -function detectReadmeSignals(targetDir: string): string[] { - const readmePath = join(targetDir, "README.md"); - if (!existsSync(readmePath)) return []; - - try { - const readmeContent = readFileSync(readmePath, "utf-8"); - return /\bdoctor\b|\bhealth check\b/i.test(readmeContent) ? ["doctor docs"] : []; - } catch { - return []; - } -} - -function detectWorkflowSignals(targetDir: string): string[] { - const workflowDir = join(targetDir, ".github", "workflows"); - if (!existsSync(workflowDir)) return []; - - try { - const files = readdirSync(workflowDir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml")); - for (const file of files) { - const workflowContent = readFileSync(join(workflowDir, file), "utf-8"); - if (/\baudit\b|\bdoctor\b/i.test(workflowContent)) return ["ci diagnostic checks"]; - } - } catch { - return []; - } - - return []; -} - -function detectSourceSignals(targetDir: string): string[] { - const sourceFiles = findFiles(targetDir, /^index\.(ts|js|mjs|cjs)$/, 5).filter((f) => !f.includes("node_modules")); - for (const file of sourceFiles) { - try { - const sourceContent = readFileSync(file, "utf-8"); - if (/function\s+doctor\s*\(|--help|Unknown command|printHelp/i.test(sourceContent)) { - return ["cli diagnostic command surface"]; - } - } catch { - // ignore read errors - } - } - - return []; -} - -function detectTestSignals(targetDir: string): string[] { - const testFiles = findFiles(targetDir, /\.(test|spec)\.(ts|js|mjs|cjs)$/, 5).filter( - (f) => !f.includes("node_modules"), - ); - for (const file of testFiles) { - try { - const testContent = readFileSync(file, "utf-8"); - if (/--help|Unknown command|doctor/i.test(testContent)) return ["cli diagnostic tests"]; - } catch { - // ignore read errors - } - } - - return []; -} - -function detectCliDiagnosabilitySignals(targetDir: string): string[] { - const signals = new Set(); - const probes = [ - detectReadmeSignals(targetDir), - detectWorkflowSignals(targetDir), - detectSourceSignals(targetDir), - detectTestSignals(targetDir), - ]; - - for (const probeSignals of probes) { - for (const signal of probeSignals) { - signals.add(signal); - } - } - - return [...signals]; -} - -// ─── Commands ─────────────────────────────────────────────────────────────── - -function init(options: InitOptions): void { - const targetDir = resolve(options.path); - const projectName = options.name || basename(targetDir); - - if (!existsSync(targetDir)) { - console.error(JSON.stringify({ error: `Directory does not exist: ${targetDir}` })); - process.exit(1); - } - - const agentsMdPath = join(targetDir, "AGENTS.md"); - if (existsSync(agentsMdPath) && !options.force) { - console.error( - JSON.stringify({ - error: "AGENTS.md already exists. Use --force to overwrite.", - hint: "Run 'reins audit' to assess your current setup instead.", - }), - ); - process.exit(1); - } - - const created: string[] = []; - - // Create directories - const dirs = [ - "docs/design-docs", - "docs/exec-plans/active", - "docs/exec-plans/completed", - "docs/generated", - "docs/product-specs", - "docs/references", - ]; - - for (const dir of dirs) { - const fullPath = join(targetDir, dir); - if (!existsSync(fullPath)) { - mkdirSync(fullPath, { recursive: true }); - created.push(`${dir}/`); - } - } - - // Create files - const files: Array<{ path: string; content: string }> = [ - { path: "AGENTS.md", content: agentsMdTemplate(projectName) }, - { path: "ARCHITECTURE.md", content: architectureMdTemplate(projectName) }, - { path: "risk-policy.json", content: riskPolicyTemplate() }, - { path: "docs/golden-principles.md", content: goldenPrinciplesTemplate() }, - { path: "docs/design-docs/index.md", content: designDocsIndexTemplate() }, - { path: "docs/design-docs/core-beliefs.md", content: coreBeliefsTemplate() }, - { path: "docs/product-specs/index.md", content: productSpecsIndexTemplate() }, - { path: "docs/exec-plans/tech-debt-tracker.md", content: techDebtTrackerTemplate() }, - ]; - - for (const file of files) { - const fullPath = join(targetDir, file.path); - if (!existsSync(fullPath) || options.force) { - writeFileSync(fullPath, file.content); - created.push(file.path); - } - } - - console.log( - JSON.stringify( - { - command: "init", - project: projectName, - target: targetDir, - created, - next_steps: [ - "Edit AGENTS.md — fill in the project description", - "Edit ARCHITECTURE.md — define your business domains", - "Review risk-policy.json — set tiers and docs drift rules for your repo", - "Edit docs/golden-principles.md — customize rules for your project", - "Run 'reins audit .' to see your starting score", - ], - }, - null, - 2, - ), - ); -} - -interface AuditRuntimeContext { - targetDir: string; - pkgJsonPath: string; - docsDir: string; - execPlansDir: string; - archMdPath: string; - workflowDir: string; - goldenPath: string; - hasRiskPolicy: boolean; - hasEslint: boolean; - hasBiome: boolean; - hasStructuralLintScript: boolean; - ciEnforcementSteps: string[]; - monorepoWorkspaces: string[]; - isMonorepo: boolean; - isCliRepo: boolean; - verifiedDocs: string[]; - hasCleanupDocs: boolean; -} - -function createAuditResult(projectName: string): AuditResult { - return { - project: projectName, - timestamp: new Date().toISOString(), - scores: { - repository_knowledge: { score: 0, max: 3, findings: [] }, - architecture_enforcement: { score: 0, max: 3, findings: [] }, - agent_legibility: { score: 0, max: 3, findings: [] }, - golden_principles: { score: 0, max: 3, findings: [] }, - agent_workflow: { score: 0, max: 3, findings: [] }, - garbage_collection: { score: 0, max: 3, findings: [] }, - }, - total_score: 0, - max_score: 18, - maturity_level: "L0", - recommendations: [], - }; -} - -function readVerifiedDocs(targetDir: string): string[] { - const allDocFiles = findFiles(targetDir, /\.(md|markdown)$/); - return allDocFiles.filter((file) => { - try { - return readFileSync(file, "utf-8").includes(" headers to key docs for freshness tracking", - }, - ]; -} - -function collectDoctorHierarchicalAgentsCheck(targetDir: string): DoctorCheck[] { - const agentsMdFiles = findFiles(targetDir, /^AGENTS\.md$/, 3); - if (agentsMdFiles.length >= 2) { - return [{ check: `Hierarchical AGENTS.md (${agentsMdFiles.length} files)`, status: "pass", fix: "" }]; - } - - return []; -} - -function collectDoctorStructuralLintChecks(targetDir: string): DoctorCheck[] { - if (!existsSync(join(targetDir, "scripts"))) return []; - - const structuralScripts = findFiles(join(targetDir, "scripts"), /lint|structure/i, 1); - if (structuralScripts.length > 0) { - return [{ check: "Structural lint scripts found", status: "pass", fix: "" }]; - } - - return [ - { - check: "No structural lint scripts", - status: "warn", - fix: "Add scripts/structural-lint.ts to enforce layer and dependency rules", - }, - ]; -} - -function doctor(targetPath: string): void { - const targetDir = resolve(targetPath); - if (!existsSync(targetDir)) { - console.error(JSON.stringify({ error: `Directory does not exist: ${targetDir}` })); - process.exit(1); - } - - const checks: DoctorCheck[] = [ - ...collectDoctorAgentsCheck(targetDir), - ...collectDoctorArchitectureCheck(targetDir), - ...collectDoctorRequiredDocChecks(targetDir), - ...collectDoctorLinterCheck(targetDir), - ...collectDoctorCiChecks(targetDir), - ...collectDoctorRiskPolicyCheck(targetDir), - ...collectDoctorVerificationChecks(targetDir), - ...collectDoctorHierarchicalAgentsCheck(targetDir), - ...collectDoctorStructuralLintChecks(targetDir), - ]; - - const passed = checks.filter((check) => check.status === "pass").length; - const failed = checks.filter((check) => check.status === "fail").length; - const warnings = checks.filter((check) => check.status === "warn").length; - - console.log( - JSON.stringify( - { - command: "doctor", - project: basename(targetDir), - target: targetDir, - summary: { passed, failed, warnings, total: checks.length }, - checks, - }, - null, - 2, - ), - ); -} - -// ─── Evolution Paths ──────────────────────────────────────────────────────── - -interface EvolutionStep { - step: number; - action: string; - description: string; - automated: boolean; -} - -interface EvolutionPath { - from: string; - to: string; - goal: string; - steps: EvolutionStep[]; - success_criteria: string; -} - -const EVOLUTION_PATHS: Record = { - L0: { - from: "L0: Manual", - to: "L1: Assisted", - goal: "Get agents into the development loop", - steps: [ - { - step: 1, - action: "Create AGENTS.md", - description: "Concise map (~100 lines) pointing agents to deeper docs. Run 'reins init .' to generate.", - automated: true, - }, - { - step: 2, - action: "Create docs/ structure", - description: "Design docs, product specs, references, execution plans — all versioned in-repo.", - automated: true, - }, - { - step: 3, - action: "Document architecture", - description: "ARCHITECTURE.md with domain map, layer ordering, and dependency direction rules.", - automated: true, - }, - { - step: 4, - action: "Set up agent-friendly CI", - description: "Fast feedback, clear error messages, deterministic output. Agents need to parse CI results.", - automated: false, - }, - { - step: 5, - action: "First agent PR", - description: "Have an agent open its first PR from a prompt. Validates the full loop works end-to-end.", - automated: false, - }, - ], - success_criteria: "Agent can read AGENTS.md, follow pointers, and open a useful PR.", - }, - L1: { - from: "L1: Assisted", - to: "L2: Steered", - goal: "Shift from human-writes-code to human-steers-agent", - steps: [ - { - step: 1, - action: "Write golden principles", - description: "Mechanical taste rules in docs/golden-principles.md, enforced in CI — not just documented.", - automated: true, - }, - { - step: 2, - action: "Add structural linters", - description: "Custom lint rules for dependency direction, layer violations, naming conventions.", - automated: false, - }, - { - step: 3, - action: "Enable worktree isolation", - description: "App bootable per git worktree — one instance per in-flight change.", - automated: false, - }, - { - step: 4, - action: "Create exec-plan templates", - description: "Versioned execution plans in docs/exec-plans/ — active, completed, and tech debt tracked.", - automated: true, - }, - { - step: 5, - action: "Adopt prompt-first workflow", - description: "Describe tasks in natural language. Agents write all code, tests, and docs.", - automated: false, - }, - ], - success_criteria: "Most new code is written by agents, not humans.", - }, - L2: { - from: "L2: Steered", - to: "L3: Autonomous", - goal: "Agent handles full PR lifecycle end-to-end", - steps: [ - { - step: 1, - action: "Establish risk tiers and policy-as-code", - description: "Create risk-policy.json defining risk tiers, docs-drift rules, and watch paths for enforcement.", - automated: false, - }, - { - step: 2, - action: "Enforce golden principles mechanically", - description: - "Add structural lint scripts and CI gates that enforce golden principles — not just document them.", - automated: false, - }, - { - step: 3, - action: "Enable self-validation", - description: "Agent drives the app, takes screenshots, checks behavior against expectations.", - automated: false, - }, - { - step: 4, - action: "Add doc-gardening automation", - description: "Add verification headers (), freshness scripts, and recurring doc review.", - automated: false, - }, - { - step: 5, - action: "Build escalation paths", - description: "Clear criteria for when to involve humans vs. when agents can proceed autonomously.", - automated: false, - }, - ], - success_criteria: "Agent can end-to-end ship a feature from prompt to merge.", - }, - L3: { - from: "L3: Autonomous", - to: "L4: Self-Correcting", - goal: "System maintains and improves itself without human intervention", - steps: [ - { - step: 1, - action: "Implement active doc-gardening with drift detection", - description: "Automated drift detection between docs and code, with auto-repair capabilities.", - automated: false, - }, - { - step: 2, - action: "Add quality grades", - description: "Per-domain, per-layer scoring tracked in ARCHITECTURE.md.", - automated: false, - }, - { - step: 3, - action: "Automate enforcement ratio tracking", - description: "Track >80% of golden principles enforced in CI — measure and improve coverage.", - automated: false, - }, - { - step: 4, - action: "Track tech debt continuously", - description: "In-repo tracker with recurring review — debt paid down in small increments.", - automated: true, - }, - { - step: 5, - action: "Establish docs-drift rules", - description: "Link code changes to required doc updates via risk-policy.json watchPaths and docsDriftRules.", - automated: false, - }, - ], - success_criteria: "Codebase improves in quality without human intervention.", - }, -}; - -function evolve(targetPath: string, runInit: boolean): void { - let auditResult: AuditResult; - try { - auditResult = runAudit(targetPath); - } catch (e: unknown) { - const message = e instanceof Error ? e.message : String(e); - console.error(JSON.stringify({ error: message })); - process.exit(1); - } - - // Determine current level key - let currentKey: string; - if (auditResult.total_score <= 4) currentKey = "L0"; - else if (auditResult.total_score <= 8) currentKey = "L1"; - else if (auditResult.total_score <= 13) currentKey = "L2"; - else if (auditResult.total_score <= 16) currentKey = "L3"; - else currentKey = "L4"; - - if (currentKey === "L4") { - console.log( - JSON.stringify( - { - command: "evolve", - project: auditResult.project, - current_level: auditResult.maturity_level, - current_score: auditResult.total_score, - message: "Already at L4: Self-Correcting. Focus on maintaining quality grades and continuous improvement.", - }, - null, - 2, - ), - ); - return; - } - - const path = EVOLUTION_PATHS[currentKey]; - const automatedSteps = path.steps.filter((s) => s.automated); - const manualSteps = path.steps.filter((s) => !s.automated); - - // If --apply flag is set, run automated steps - const applied: string[] = []; - if (runInit && automatedSteps.some((s) => s.action.includes("AGENTS.md") || s.action.includes("docs/"))) { - const targetDir = resolve(targetPath); - const agentsMd = join(targetDir, "AGENTS.md"); - if (!existsSync(agentsMd)) { - init({ path: targetPath, name: "", force: false }); - applied.push("Ran 'reins init' to scaffold missing structure"); - } - } - - // Find weakest dimensions for targeted advice - const weakest = Object.entries(auditResult.scores) - .sort(([, a], [, b]) => a.score - b.score) - .slice(0, 3) - .map(([dim, score]) => ({ dimension: dim, score: score.score, max: score.max, findings: score.findings })); - - console.log( - JSON.stringify( - { - command: "evolve", - project: auditResult.project, - current_level: auditResult.maturity_level, - current_score: auditResult.total_score, - next_level: path.to, - goal: path.goal, - steps: path.steps, - success_criteria: path.success_criteria, - weakest_dimensions: weakest, - applied, - recommendations: auditResult.recommendations, - }, - null, - 2, - ), - ); -} - -// ─── CLI Router ───────────────────────────────────────────────────────────── +import { runAudit, runAuditCommand } from "./lib/commands/audit"; +import { runDoctor } from "./lib/commands/doctor"; +import { runEvolve } from "./lib/commands/evolve"; +import { runInit as runInitCommand } from "./lib/commands/init"; function printHelp(): void { const help = `reins — Harness Engineering CLI @@ -1702,12 +21,15 @@ COMMANDS: OPTIONS: --name Project name (default: directory name) --force Overwrite existing files + --pack Optional automation pack (auto, agent-factory) --apply Auto-run scaffolding steps during evolve --json Force JSON output (default) EXAMPLES: reins init . # Scaffold in current directory reins init ./my-project --name "My Project" + reins init . --pack auto # Adaptive pack selection by stack signals + reins init . --pack agent-factory reins audit . # Score current project reins evolve . # Get evolution roadmap reins evolve . --apply # Evolve with auto-scaffolding @@ -1732,37 +54,36 @@ function main(): void { process.exit(0); } - // Parse flags - const flagIndex = (flag: string) => args.indexOf(flag); const hasFlag = (flag: string) => args.includes(flag); const getFlagValue = (flag: string): string | undefined => { - const idx = flagIndex(flag); + const idx = args.indexOf(flag); return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined; }; switch (command) { case "init": { const path = args[1] || "."; - init({ + runInitCommand({ path, name: getFlagValue("--name") || "", force: hasFlag("--force"), + pack: getFlagValue("--pack") || "none", }); break; } case "audit": { const path = args[1] || "."; - audit(path); + runAuditCommand(path); break; } case "evolve": { const path = args[1] || "."; - evolve(path, hasFlag("--apply")); + runEvolve(path, hasFlag("--apply"), { runAudit, runInit: runInitCommand }); break; } case "doctor": { const path = args[1] || "."; - doctor(path); + runDoctor(path); break; } default: diff --git a/cli/reins/src/lib/audit/context.ts b/cli/reins/src/lib/audit/context.ts new file mode 100644 index 0000000..8da3fec --- /dev/null +++ b/cli/reins/src/lib/audit/context.ts @@ -0,0 +1,101 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { detectCliProject, detectMonorepoWorkspaces, scanWorkflowsForEnforcement } from "../detection"; +import { findFiles } from "../filesystem"; +import type { AuditResult } from "../types"; + +export interface AuditRuntimeContext { + targetDir: string; + pkgJsonPath: string; + docsDir: string; + execPlansDir: string; + archMdPath: string; + workflowDir: string; + goldenPath: string; + hasRiskPolicy: boolean; + hasEslint: boolean; + hasBiome: boolean; + hasStructuralLintScript: boolean; + ciEnforcementSteps: string[]; + monorepoWorkspaces: string[]; + isMonorepo: boolean; + isCliRepo: boolean; + verifiedDocs: string[]; + hasCleanupDocs: boolean; +} + +export function createAuditResult(projectName: string): AuditResult { + return { + project: projectName, + timestamp: new Date().toISOString(), + scores: { + repository_knowledge: { score: 0, max: 3, findings: [] }, + architecture_enforcement: { score: 0, max: 3, findings: [] }, + agent_legibility: { score: 0, max: 3, findings: [] }, + golden_principles: { score: 0, max: 3, findings: [] }, + agent_workflow: { score: 0, max: 3, findings: [] }, + garbage_collection: { score: 0, max: 3, findings: [] }, + }, + total_score: 0, + max_score: 18, + maturity_level: "L0", + recommendations: [], + }; +} + +export function readVerifiedDocs(targetDir: string): string[] { + const allDocFiles = findFiles(targetDir, /\.(md|markdown)$/); + return allDocFiles.filter((file) => { + try { + return readFileSync(file, "utf-8").includes(" headers to key docs for freshness tracking", + }, + ]; +} + +function collectDoctorHierarchicalAgentsCheck(targetDir: string): DoctorCheck[] { + const agentsMdFiles = findFiles(targetDir, /^AGENTS\.md$/, 3); + if (agentsMdFiles.length >= 2) { + return [{ check: `Hierarchical AGENTS.md (${agentsMdFiles.length} files)`, status: "pass", fix: "" }]; + } + + return []; +} + +function collectDoctorStructuralLintChecks(targetDir: string): DoctorCheck[] { + if (!existsSync(join(targetDir, "scripts"))) return []; + + const structuralScripts = findFiles(join(targetDir, "scripts"), /lint|structure/i, 1); + if (structuralScripts.length > 0) { + return [{ check: "Structural lint scripts found", status: "pass", fix: "" }]; + } + + return [ + { + check: "No structural lint scripts", + status: "warn", + fix: "Add scripts/structural-lint.ts to enforce layer and dependency rules", + }, + ]; +} + +export function runDoctor(targetPath: string): void { + const targetDir = resolve(targetPath); + if (!existsSync(targetDir)) { + console.error(JSON.stringify({ error: `Directory does not exist: ${targetDir}` })); + process.exit(1); + } + + const checks: DoctorCheck[] = [ + ...collectDoctorAgentsCheck(targetDir), + ...collectDoctorArchitectureCheck(targetDir), + ...collectDoctorRequiredDocChecks(targetDir), + ...collectDoctorLinterCheck(targetDir), + ...collectDoctorCiChecks(targetDir), + ...collectDoctorRiskPolicyCheck(targetDir), + ...collectDoctorVerificationChecks(targetDir), + ...collectDoctorHierarchicalAgentsCheck(targetDir), + ...collectDoctorStructuralLintChecks(targetDir), + ]; + + const passed = checks.filter((check) => check.status === "pass").length; + const failed = checks.filter((check) => check.status === "fail").length; + const warnings = checks.filter((check) => check.status === "warn").length; + + console.log( + JSON.stringify( + { + command: "doctor", + project: basename(targetDir), + target: targetDir, + summary: { passed, failed, warnings, total: checks.length }, + checks, + }, + null, + 2, + ), + ); +} diff --git a/cli/reins/src/lib/commands/evolve.test.ts b/cli/reins/src/lib/commands/evolve.test.ts new file mode 100644 index 0000000..9d256ce --- /dev/null +++ b/cli/reins/src/lib/commands/evolve.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from "bun:test"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { AuditResult } from "../types"; +import { runEvolve } from "./evolve"; + +describe("runEvolve unit behavior", () => { + test("suppresses pack recommendation reason when maturity gating defers agent-factory", () => { + const dir = mkdtempSync(join(tmpdir(), "reins-evolve-unit-")); + writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "sample", scripts: { dev: "node index.js" } })); + + const mockAuditResult: AuditResult = { + project: "sample", + timestamp: new Date().toISOString(), + scores: { + repository_knowledge: { score: 3, max: 3, findings: [] }, + architecture_enforcement: { score: 3, max: 3, findings: [] }, + agent_legibility: { score: 3, max: 3, findings: [] }, + golden_principles: { score: 3, max: 3, findings: [] }, + agent_workflow: { score: 1, max: 3, findings: [] }, + garbage_collection: { score: 1, max: 3, findings: [] }, + }, + total_score: 14, + max_score: 18, + maturity_level: "L3: Autonomous", + recommendations: [], + }; + + let output = ""; + const originalLog = console.log; + console.log = (value?: unknown) => { + output = String(value ?? ""); + }; + + try { + runEvolve(dir, false, { + runAudit: () => mockAuditResult, + runInit: () => {}, + }); + } finally { + console.log = originalLog; + rmSync(dir, { recursive: true, force: true }); + } + + const result = JSON.parse(output); + expect(result.pack_recommendation?.recommended).toBeNull(); + expect(result.pack_recommendation?.reason).toContain("suppressed by maturity level"); + }); +}); diff --git a/cli/reins/src/lib/commands/evolve.ts b/cli/reins/src/lib/commands/evolve.ts new file mode 100644 index 0000000..afe9821 --- /dev/null +++ b/cli/reins/src/lib/commands/evolve.ts @@ -0,0 +1,399 @@ +import { existsSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { + type AutomationPack, + hasAgentFactoryPack, + recommendAutomationPack, + scaffoldAutomationPack, +} from "../automation-pack"; +import type { AuditResult, EvolutionPath, EvolutionStep, InitOptions } from "../types"; + +type LevelKey = "L0" | "L1" | "L2" | "L3" | "L4"; +type EvolvePathKey = Exclude; + +interface EvolvePackState { + packRecommendation: ReturnType; + hasFactoryPack: boolean; + shouldRecommendFactoryPack: boolean; +} + +interface EvolveDeps { + runAudit: (targetPath: string) => AuditResult; + runInit: (options: InitOptions) => void; +} + +const EVOLUTION_PATHS: Record = { + L0: { + from: "L0: Manual", + to: "L1: Assisted", + goal: "Get agents into the development loop", + steps: [ + { + step: 1, + action: "Create AGENTS.md", + description: "Concise map (~100 lines) pointing agents to deeper docs. Run 'reins init .' to generate.", + automated: true, + }, + { + step: 2, + action: "Create docs/ structure", + description: "Design docs, product specs, references, execution plans — all versioned in-repo.", + automated: true, + }, + { + step: 3, + action: "Document architecture", + description: "ARCHITECTURE.md with domain map, layer ordering, and dependency direction rules.", + automated: true, + }, + { + step: 4, + action: "Set up agent-friendly CI", + description: "Fast feedback, clear error messages, deterministic output. Agents need to parse CI results.", + automated: false, + }, + { + step: 5, + action: "First agent PR", + description: "Have an agent open its first PR from a prompt. Validates the full loop works end-to-end.", + automated: false, + }, + ], + success_criteria: "Agent can read AGENTS.md, follow pointers, and open a useful PR.", + }, + L1: { + from: "L1: Assisted", + to: "L2: Steered", + goal: "Shift from human-writes-code to human-steers-agent", + steps: [ + { + step: 1, + action: "Write golden principles", + description: "Mechanical taste rules in docs/golden-principles.md, enforced in CI — not just documented.", + automated: true, + }, + { + step: 2, + action: "Add structural linters", + description: "Custom lint rules for dependency direction, layer violations, naming conventions.", + automated: false, + }, + { + step: 3, + action: "Enable worktree isolation", + description: "App bootable per git worktree — one instance per in-flight change.", + automated: false, + }, + { + step: 4, + action: "Create exec-plan templates", + description: "Versioned execution plans in docs/exec-plans/ — active, completed, and tech debt tracked.", + automated: true, + }, + { + step: 5, + action: "Adopt prompt-first workflow", + description: "Describe tasks in natural language. Agents write all code, tests, and docs.", + automated: false, + }, + ], + success_criteria: "Most new code is written by agents, not humans.", + }, + L2: { + from: "L2: Steered", + to: "L3: Autonomous", + goal: "Agent handles full PR lifecycle end-to-end", + steps: [ + { + step: 1, + action: "Establish risk tiers and policy-as-code", + description: "Create risk-policy.json defining risk tiers, docs-drift rules, and watch paths for enforcement.", + automated: false, + }, + { + step: 2, + action: "Enforce golden principles mechanically", + description: + "Add structural lint scripts and CI gates that enforce golden principles — not just document them.", + automated: false, + }, + { + step: 3, + action: "Enable self-validation", + description: "Agent drives the app, takes screenshots, checks behavior against expectations.", + automated: false, + }, + { + step: 4, + action: "Add doc-gardening automation", + description: "Add verification headers (), freshness scripts, and recurring doc review.", + automated: false, + }, + { + step: 5, + action: "Build escalation paths", + description: "Clear criteria for when to involve humans vs. when agents can proceed autonomously.", + automated: false, + }, + ], + success_criteria: "Agent can end-to-end ship a feature from prompt to merge.", + }, + L3: { + from: "L3: Autonomous", + to: "L4: Self-Correcting", + goal: "System maintains and improves itself without human intervention", + steps: [ + { + step: 1, + action: "Implement active doc-gardening with drift detection", + description: "Automated drift detection between docs and code, with auto-repair capabilities.", + automated: false, + }, + { + step: 2, + action: "Add quality grades", + description: "Per-domain, per-layer scoring tracked in ARCHITECTURE.md.", + automated: false, + }, + { + step: 3, + action: "Automate enforcement ratio tracking", + description: "Track >80% of golden principles enforced in CI — measure and improve coverage.", + automated: false, + }, + { + step: 4, + action: "Track tech debt continuously", + description: "In-repo tracker with recurring review — debt paid down in small increments.", + automated: true, + }, + { + step: 5, + action: "Establish docs-drift rules", + description: "Link code changes to required doc updates via risk-policy.json watchPaths and docsDriftRules.", + automated: false, + }, + ], + success_criteria: "Codebase improves in quality without human intervention.", + }, +}; + +function resolveCurrentLevelKey(totalScore: number): LevelKey { + if (totalScore <= 4) return "L0"; + if (totalScore <= 8) return "L1"; + if (totalScore <= 13) return "L2"; + if (totalScore <= 16) return "L3"; + return "L4"; +} + +function isFactoryPackRecommended( + currentKey: LevelKey, + selectedPack: ReturnType["selected"], + hasFactoryPack: boolean, +): boolean { + if (selectedPack !== "agent-factory" || hasFactoryPack) return false; + return currentKey === "L0" || currentKey === "L1" || currentKey === "L2"; +} + +function buildPackState(targetDir: string, currentKey: LevelKey): EvolvePackState { + const packRecommendation = recommendAutomationPack(targetDir); + const hasFactoryPack = hasAgentFactoryPack(targetDir); + const shouldRecommendFactoryPack = isFactoryPackRecommended(currentKey, packRecommendation.selected, hasFactoryPack); + return { packRecommendation, hasFactoryPack, shouldRecommendFactoryPack }; +} + +function buildEvolutionSteps(path: EvolutionPath, shouldRecommendFactoryPack: boolean): EvolutionStep[] { + const steps = [...path.steps]; + if (!shouldRecommendFactoryPack) return steps; + + steps.push({ + step: steps.length + 1, + action: "Adopt agent-factory automation pack", + description: + "Scaffold structural lint, docs freshness, and PR review automation with 'reins init . --pack agent-factory'.", + automated: true, + }); + return steps; +} + +function hasMissingBaseScaffold(targetDir: string): boolean { + const requiredArtifacts = [ + "AGENTS.md", + "ARCHITECTURE.md", + "risk-policy.json", + "docs/golden-principles.md", + "docs/design-docs/index.md", + "docs/design-docs/core-beliefs.md", + "docs/product-specs/index.md", + "docs/exec-plans/tech-debt-tracker.md", + "docs/exec-plans/active", + "docs/exec-plans/completed", + "docs/generated", + "docs/references", + ]; + + return requiredArtifacts.some((artifact) => !existsSync(join(targetDir, artifact))); +} + +function runInitScaffoldOrExit(targetPath: string, initPack: AutomationPack, runInit: EvolveDeps["runInit"]): void { + try { + runInit({ path: targetPath, name: "", force: false, pack: initPack, allowExistingAgents: true }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + printJsonErrorAndExit(`Scaffolding failed: ${message}`); + } +} + +function scaffoldPackOrExit(targetDir: string): string[] { + try { + return scaffoldAutomationPack(targetDir, "agent-factory", false); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + printJsonErrorAndExit(`Pack scaffolding failed: ${message}`); + } +} + +function applyEvolveScaffolding( + targetDir: string, + targetPath: string, + runApply: boolean, + shouldRecommendFactoryPack: boolean, + runInit: EvolveDeps["runInit"], +): string[] { + const applied: string[] = []; + if (!runApply) return applied; + + const missingBaseScaffold = hasMissingBaseScaffold(targetDir); + if (missingBaseScaffold) { + const initPack: AutomationPack = shouldRecommendFactoryPack ? "agent-factory" : "none"; + runInitScaffoldOrExit(targetPath, initPack, runInit); + applied.push( + initPack === "agent-factory" + ? "Ran 'reins init' with agent-factory pack to scaffold missing structure and automation" + : "Ran 'reins init' to scaffold missing structure", + ); + return applied; + } + + if (!shouldRecommendFactoryPack) return applied; + + const packArtifacts = scaffoldPackOrExit(targetDir); + if (packArtifacts.length > 0) { + applied.push(`Applied 'agent-factory' automation pack scaffolding (${packArtifacts.length} artifact(s))`); + } + + return applied; +} + +function buildWeakestDimensions( + result: AuditResult, +): Array<{ dimension: string; score: number; max: number; findings: string[] }> { + return Object.entries(result.scores) + .sort(([, a], [, b]) => a.score - b.score) + .slice(0, 3) + .map(([dimension, score]) => ({ dimension, score: score.score, max: score.max, findings: score.findings })); +} + +function buildPackRecommendationOutput(packState: EvolvePackState): { recommended: string | null; reason: string } { + if (packState.shouldRecommendFactoryPack) { + return { + recommended: "agent-factory", + reason: packState.packRecommendation.reason, + }; + } + + if (packState.hasFactoryPack) { + return { + recommended: null, + reason: "Agent-factory automation pack already present.", + }; + } + + if (packState.packRecommendation.selected === "agent-factory") { + return { + recommended: null, + reason: "Agent-factory recommendation suppressed by maturity level.", + }; + } + + return { + recommended: null, + reason: packState.packRecommendation.reason, + }; +} + +function printL4Response(auditResult: AuditResult): void { + console.log( + JSON.stringify( + { + command: "evolve", + project: auditResult.project, + current_level: auditResult.maturity_level, + current_score: auditResult.total_score, + message: "Already at L4: Self-Correcting. Focus on maintaining quality grades and continuous improvement.", + pack_recommendation: { + recommended: null, + reason: "Already at L4. Keep existing automation healthy and continuously verified.", + }, + }, + null, + 2, + ), + ); +} + +function printJsonErrorAndExit(message: string): never { + console.error(JSON.stringify({ error: message })); + process.exit(1); +} + +function getAuditResultOrExit(targetPath: string, runAudit: EvolveDeps["runAudit"]): AuditResult { + try { + return runAudit(targetPath); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + printJsonErrorAndExit(message); + } +} + +export function runEvolve(targetPath: string, runApply: boolean, deps: EvolveDeps): void { + const auditResult = getAuditResultOrExit(targetPath, deps.runAudit); + const currentKey = resolveCurrentLevelKey(auditResult.total_score); + if (currentKey === "L4") { + printL4Response(auditResult); + return; + } + + const targetDir = resolve(targetPath); + const path = EVOLUTION_PATHS[currentKey]; + const packState = buildPackState(targetDir, currentKey); + const steps = buildEvolutionSteps(path, packState.shouldRecommendFactoryPack); + const applied = applyEvolveScaffolding( + targetDir, + targetPath, + runApply, + packState.shouldRecommendFactoryPack, + deps.runInit, + ); + + console.log( + JSON.stringify( + { + command: "evolve", + project: auditResult.project, + current_level: auditResult.maturity_level, + current_score: auditResult.total_score, + next_level: path.to, + goal: path.goal, + steps, + success_criteria: path.success_criteria, + weakest_dimensions: buildWeakestDimensions(auditResult), + pack_recommendation: buildPackRecommendationOutput(packState), + applied, + recommendations: auditResult.recommendations, + }, + null, + 2, + ), + ); +} diff --git a/cli/reins/src/lib/commands/init.ts b/cli/reins/src/lib/commands/init.ts new file mode 100644 index 0000000..a016a42 --- /dev/null +++ b/cli/reins/src/lib/commands/init.ts @@ -0,0 +1,172 @@ +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { basename, join, resolve } from "node:path"; +import type { ResolvedAutomationPack } from "../automation-pack"; +import { + type AutomationPack, + normalizeAutomationPack, + resolveAutomationPack, + scaffoldAutomationPack, +} from "../automation-pack"; +import { + agentsMdTemplate, + architectureMdTemplate, + coreBeliefsTemplate, + designDocsIndexTemplate, + goldenPrinciplesTemplate, + productSpecsIndexTemplate, + riskPolicyTemplate, + techDebtTrackerTemplate, +} from "../templates"; +import type { InitOptions } from "../types"; + +interface InitContext { + targetDir: string; + projectName: string; + requestedPack: AutomationPack; +} + +function parseRequestedPack(rawPack: string): AutomationPack { + const requestedPack = normalizeAutomationPack(rawPack || "none"); + if (requestedPack) return requestedPack; + + console.error( + JSON.stringify({ + error: `Unknown automation pack: ${rawPack}`, + allowed: ["none", "auto", "agent-factory"], + hint: "Use '--pack auto' for adaptive selection or '--pack agent-factory' for explicit scaffolding.", + }), + ); + process.exit(1); +} + +function ensureInitTarget(targetDir: string, force: boolean, allowExistingAgents = false): void { + if (!existsSync(targetDir)) { + console.error(JSON.stringify({ error: `Directory does not exist: ${targetDir}` })); + process.exit(1); + } + + const agentsMdPath = join(targetDir, "AGENTS.md"); + if (!existsSync(agentsMdPath) || force || allowExistingAgents) return; + + console.error( + JSON.stringify({ + error: "AGENTS.md already exists. Use --force to overwrite.", + hint: "Run 'reins audit' to assess your current setup instead.", + }), + ); + process.exit(1); +} + +function createBaseDirectories(targetDir: string, created: string[]): void { + const dirs = [ + "docs/design-docs", + "docs/exec-plans/active", + "docs/exec-plans/completed", + "docs/generated", + "docs/product-specs", + "docs/references", + ]; + + for (const dir of dirs) { + const fullPath = join(targetDir, dir); + if (existsSync(fullPath)) continue; + + mkdirSync(fullPath, { recursive: true }); + created.push(`${dir}/`); + } +} + +function createBaseFiles( + targetDir: string, + projectName: string, + selectedPack: ResolvedAutomationPack, + force: boolean, + created: string[], +): void { + const files: Array<{ path: string; content: string }> = [ + { path: "AGENTS.md", content: agentsMdTemplate(projectName) }, + { path: "ARCHITECTURE.md", content: architectureMdTemplate(projectName) }, + { path: "risk-policy.json", content: riskPolicyTemplate(selectedPack) }, + { path: "docs/golden-principles.md", content: goldenPrinciplesTemplate() }, + { path: "docs/design-docs/index.md", content: designDocsIndexTemplate() }, + { path: "docs/design-docs/core-beliefs.md", content: coreBeliefsTemplate() }, + { path: "docs/product-specs/index.md", content: productSpecsIndexTemplate() }, + { path: "docs/exec-plans/tech-debt-tracker.md", content: techDebtTrackerTemplate() }, + ]; + + for (const file of files) { + const fullPath = join(targetDir, file.path); + if (existsSync(fullPath) && !force) continue; + + writeFileSync(fullPath, file.content); + created.push(file.path); + } +} + +function buildNextSteps( + selectedPack: ResolvedAutomationPack, + requestedPack: AutomationPack, + packReason: string, +): string[] { + const steps = [ + "Edit AGENTS.md — fill in the project description", + "Edit ARCHITECTURE.md — define your business domains", + "Review risk-policy.json — set tiers and docs drift rules for your repo", + "Edit docs/golden-principles.md — customize rules for your project", + "Run 'reins audit .' to see your starting score", + ]; + + if (selectedPack === "agent-factory") { + steps.push( + "Review generated scripts in scripts/ and tune checks for your stack and taste", + "Enable or adapt new workflows in .github/workflows/ to match your branch protections", + "Run 'node scripts/lint-structure.mjs' locally to baseline structural checks", + ); + return steps; + } + + if (requestedPack === "auto") { + steps.push(`Auto-pack selection: ${packReason}`); + } + + return steps; +} + +function buildInitContext(options: InitOptions): InitContext { + const targetDir = resolve(options.path); + return { + targetDir, + projectName: options.name || basename(targetDir), + requestedPack: parseRequestedPack(options.pack || "none"), + }; +} + +export function runInit(options: InitOptions): void { + const { targetDir, projectName, requestedPack } = buildInitContext(options); + ensureInitTarget(targetDir, options.force, options.allowExistingAgents ?? false); + + const packResolution = resolveAutomationPack(targetDir, requestedPack); + const selectedPack = packResolution.selected; + const created: string[] = []; + + createBaseDirectories(targetDir, created); + createBaseFiles(targetDir, projectName, selectedPack, options.force, created); + created.push(...scaffoldAutomationPack(targetDir, selectedPack, options.force)); + + console.log( + JSON.stringify( + { + command: "init", + project: projectName, + target: targetDir, + requested_automation_pack: requestedPack === "none" ? null : requestedPack, + automation_pack: selectedPack === "none" ? null : selectedPack, + automation_pack_reason: packResolution.reason, + created, + next_steps: buildNextSteps(selectedPack, requestedPack, packResolution.reason), + }, + null, + 2, + ), + ); +} diff --git a/cli/reins/src/lib/detection.ts b/cli/reins/src/lib/detection.ts new file mode 100644 index 0000000..a316997 --- /dev/null +++ b/cli/reins/src/lib/detection.ts @@ -0,0 +1,151 @@ +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { join } from "node:path"; +import { findFiles } from "./filesystem"; + +export function scanWorkflowsForEnforcement(workflowDir: string): string[] { + const steps: Set = new Set(); + const keywordPatterns: Array<{ step: string; pattern: RegExp }> = [ + { step: "lint", pattern: /\b(lint|eslint|biome\s+check)\b/ }, + { step: "test", pattern: /\b(test|vitest|jest)\b/ }, + { step: "typecheck", pattern: /\b(typecheck|type-check|tsc\s+--no-?emit)\b/ }, + { step: "build", pattern: /\bbuild\b/ }, + { step: "audit", pattern: /\baudit\b/ }, + { step: "prettier", pattern: /\bprettier\b/ }, + { step: "format", pattern: /\bformat\b/ }, + ]; + try { + const files = readdirSync(workflowDir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml")); + for (const file of files) { + const content = readFileSync(join(workflowDir, file), "utf-8").toLowerCase(); + for (const { step, pattern } of keywordPatterns) { + if (pattern.test(content)) steps.add(step); + } + } + } catch { + // no workflows + } + return [...steps]; +} + +export function detectMonorepoWorkspaces(pkgJsonPath: string): string[] { + try { + const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8")); + const workspaces = pkg.workspaces?.packages || pkg.workspaces; + if (Array.isArray(workspaces)) return workspaces; + } catch {} + return []; +} + +function isCliPackage(pkg: Record): boolean { + const hasBin = + typeof pkg.bin === "string" || + (typeof pkg.bin === "object" && pkg.bin !== null && Object.keys(pkg.bin as Record).length > 0); + const hasCliName = typeof pkg.name === "string" && /(^|[-_])cli($|[-_])/.test(pkg.name); + const hasCliKeywords = + Array.isArray(pkg.keywords) && + pkg.keywords.some((k) => typeof k === "string" && /(cli|command-?line|terminal)/i.test(k)); + return hasBin || hasCliName || hasCliKeywords; +} + +export function detectCliProject(targetDir: string, rootPkgJsonPath: string): boolean { + if (existsSync(rootPkgJsonPath)) { + try { + const rootPkg = JSON.parse(readFileSync(rootPkgJsonPath, "utf-8")); + if (isCliPackage(rootPkg)) return true; + } catch { + // ignore parse errors + } + } + + const pkgJsonFiles = findFiles(targetDir, /^package\.json$/, 4).filter( + (f) => f !== rootPkgJsonPath && !f.includes("node_modules"), + ); + for (const pkgPath of pkgJsonFiles) { + try { + const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); + if (isCliPackage(pkg)) return true; + } catch { + // ignore parse errors + } + } + return false; +} + +function detectReadmeSignals(targetDir: string): string[] { + const readmePath = join(targetDir, "README.md"); + if (!existsSync(readmePath)) return []; + + try { + const readmeContent = readFileSync(readmePath, "utf-8"); + return /\bdoctor\b|\bhealth check\b/i.test(readmeContent) ? ["doctor docs"] : []; + } catch { + return []; + } +} + +function detectWorkflowSignals(targetDir: string): string[] { + const workflowDir = join(targetDir, ".github", "workflows"); + if (!existsSync(workflowDir)) return []; + + try { + const files = readdirSync(workflowDir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml")); + for (const file of files) { + const workflowContent = readFileSync(join(workflowDir, file), "utf-8"); + if (/\baudit\b|\bdoctor\b/i.test(workflowContent)) return ["ci diagnostic checks"]; + } + } catch { + return []; + } + + return []; +} + +function detectSourceSignals(targetDir: string): string[] { + const sourceFiles = findFiles(targetDir, /^index\.(ts|js|mjs|cjs)$/, 5).filter((f) => !f.includes("node_modules")); + for (const file of sourceFiles) { + try { + const sourceContent = readFileSync(file, "utf-8"); + if (/function\s+doctor\s*\(|--help|Unknown command|printHelp/i.test(sourceContent)) { + return ["cli diagnostic command surface"]; + } + } catch { + // ignore read errors + } + } + + return []; +} + +function detectTestSignals(targetDir: string): string[] { + const testFiles = findFiles(targetDir, /\.(test|spec)\.(ts|js|mjs|cjs)$/, 5).filter( + (f) => !f.includes("node_modules"), + ); + for (const file of testFiles) { + try { + const testContent = readFileSync(file, "utf-8"); + if (/--help|Unknown command|doctor/i.test(testContent)) return ["cli diagnostic tests"]; + } catch { + // ignore read errors + } + } + + return []; +} + +export function detectCliDiagnosabilitySignals(targetDir: string): string[] { + const signals = new Set(); + const probes = [ + detectReadmeSignals(targetDir), + detectWorkflowSignals(targetDir), + detectSourceSignals(targetDir), + detectTestSignals(targetDir), + ]; + + for (const probeSignals of probes) { + for (const signal of probeSignals) { + signals.add(signal); + } + } + + return [...signals]; +} diff --git a/cli/reins/src/lib/filesystem.ts b/cli/reins/src/lib/filesystem.ts new file mode 100644 index 0000000..b80400c --- /dev/null +++ b/cli/reins/src/lib/filesystem.ts @@ -0,0 +1,60 @@ +import { readdirSync, statSync } from "node:fs"; +import { join } from "node:path"; + +const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", ".expo"]); + +export function safeReadDir(dir: string): string[] { + try { + return readdirSync(dir); + } catch { + return []; + } +} + +function safeStat(path: string): ReturnType | null { + try { + return statSync(path); + } catch { + return null; + } +} + +function collectFileMatch( + current: string, + entry: string, + depth: number, + maxDepth: number, + pattern: RegExp, + results: string[], + walk: (next: string, nextDepth: number) => void, +): void { + if (IGNORED_DIRS.has(entry)) return; + + const fullPath = join(current, entry); + const stat = safeStat(fullPath); + if (!stat) return; + + if (stat.isDirectory()) { + walk(fullPath, depth + 1); + return; + } + + if (depth <= maxDepth && pattern.test(entry)) { + results.push(fullPath); + } +} + +export function findFiles(dir: string, pattern: RegExp, maxDepth = 3): string[] { + const results: string[] = []; + + function walk(current: string, depth: number): void { + if (depth > maxDepth) return; + + for (const entry of safeReadDir(current)) { + collectFileMatch(current, entry, depth, maxDepth, pattern, results, walk); + } + } + + walk(dir, 0); + return results; +} diff --git a/cli/reins/src/lib/scoring-utils.ts b/cli/reins/src/lib/scoring-utils.ts new file mode 100644 index 0000000..6d57053 --- /dev/null +++ b/cli/reins/src/lib/scoring-utils.ts @@ -0,0 +1,5 @@ +export function countGoldenPrinciples(content: string): number { + const headings = content.match(/^##\s+/gm)?.length || 0; + const numbered = content.match(/^\d+\.\s+/gm)?.length || 0; + return Math.max(headings, numbered); +} diff --git a/cli/reins/src/lib/templates.ts b/cli/reins/src/lib/templates.ts new file mode 100644 index 0000000..c1bb501 --- /dev/null +++ b/cli/reins/src/lib/templates.ts @@ -0,0 +1,893 @@ +export type AutomationPack = "none" | "auto" | "agent-factory"; + +export interface AgentFactoryPackFile { + path: string; + content: string; +} + +export function agentsMdTemplate(projectName: string): string { + return `# AGENTS.md + +## Repository Overview + +${projectName} — [Brief description of the project]. + +## Architecture + +See ARCHITECTURE.md for domain map, package layering, and dependency rules. + +## Documentation Map + +| Topic | Location | Status | +|-------|----------|--------| +| Architecture | ARCHITECTURE.md | Current | +| Design Docs | docs/design-docs/index.md | Current | +| Core Beliefs | docs/design-docs/core-beliefs.md | Current | +| Product Specs | docs/product-specs/index.md | Current | +| Active Plans | docs/exec-plans/active/ | Current | +| Completed Plans | docs/exec-plans/completed/ | Current | +| Technical Debt | docs/exec-plans/tech-debt-tracker.md | Current | +| Risk Policy | risk-policy.json | Current | +| Golden Principles | docs/golden-principles.md | Current | +| References | docs/references/ | Current | + +## Development Workflow + +1. Receive task via prompt +2. Read this file, then follow pointers to relevant docs +3. Implement changes following ARCHITECTURE.md layer rules +4. Run linters and structural tests (\`bun run lint && bun run test\`) +5. Self-review changes for correctness and style +6. Request agent review if available +7. Iterate until all reviewers satisfied +8. Open PR with concise summary + +## Key Constraints + +- Dependencies flow forward only: Types > Config > Repo > Service > Runtime > UI +- Cross-cutting concerns enter ONLY through Providers +- Validate data at boundaries — never probe shapes without validation +- Prefer shared utilities over hand-rolled helpers +- All knowledge lives in-repo, not in external tools + +## Golden Principles + +See docs/golden-principles.md for the full set of mechanical taste rules. +`; +} + +export function architectureMdTemplate(projectName: string): string { + return `# Architecture — ${projectName} + +## Domain Map + + + +| Domain | Description | Quality Grade | +|--------|-------------|---------------| +| Core | Core business logic | — | +| Auth | Authentication and authorization | — | +| UI | User interface components | — | + +## Layered Architecture + +Each domain follows a strict layer ordering. Dependencies flow forward only. + +\`\`\` +Utils + | + v +Business Domain + +-- Types --> Config --> Repo --> Service --> Runtime --> UI + | + +-- Providers (cross-cutting: auth, connectors, telemetry, feature flags) + | + v + App Wiring + UI +\`\`\` + +### Layer Definitions + +| Layer | Responsibility | May Import From | +|-------|---------------|-----------------| +| Types | Data shapes, enums, interfaces | Utils | +| Config | Configuration loading, validation | Types, Utils | +| Repo | Data access, storage | Config, Types, Utils | +| Service | Business logic orchestration | Repo, Config, Types, Utils | +| Runtime | Process lifecycle, scheduling | Service, Config, Types, Utils | +| UI | User-facing presentation | Runtime, Service, Types, Utils | +| Providers | Cross-cutting adapters | Any layer (explicit interface) | + +### Enforcement + +These rules are enforced mechanically: +- [ ] Custom linter for import direction (TODO: implement) +- [ ] Structural tests for layer violations (TODO: implement) +- [ ] CI gate that fails on violations (TODO: implement) + +## Package Structure + +\`\`\` +src/ + domains/ + [domain-name]/ + types/ + config/ + repo/ + service/ + runtime/ + ui/ + providers/ + utils/ + shared/ +\`\`\` +`; +} + +export function goldenPrinciplesTemplate(): string { + return `# Golden Principles + +Opinionated mechanical rules that encode human taste. These go beyond standard linters and are enforced in CI. + +## Structural Rules + +1. **Shared utilities over hand-rolled helpers** + Centralize invariants in shared packages. Never duplicate utility logic across domains. + +2. **Validate at boundaries, never YOLO** + Parse and validate all external data at system boundaries. Use typed SDKs. Never access unvalidated shapes. + +3. **Boring technology preferred** + Choose composable, stable, well-documented dependencies. Prefer libraries well-represented in LLM training data. Reimplement simple utilities rather than pulling opaque dependencies. + +4. **Single source of truth** + Every piece of knowledge has exactly one canonical location. If it's duplicated, one copy is a reference to the other. + +## Naming Conventions + +- Files: kebab-case (\`user-service.ts\`) +- Types/Interfaces: PascalCase (\`UserProfile\`) +- Functions/Variables: camelCase (\`getUserProfile\`) +- Constants: SCREAMING_SNAKE_CASE (\`MAX_RETRY_COUNT\`) +- Domains: kebab-case directories (\`app-settings/\`) + +## Code Style + +- Prefer explicit over implicit +- No magic strings — use enums or constants +- Error messages must be actionable (what happened, what to do) +- Functions do one thing +- No nested ternaries +- Prefer early returns over deep nesting + +## Testing Rules + +- Every public function has at least one test +- Tests are co-located with source (\`*.test.ts\` next to \`*.ts\`) +- Test names describe the expected state, not the action +- No test interdependence — each test is isolated + +## Documentation Rules + +- Every design decision is documented with rationale +- Docs are verified against code on a recurring cadence +- Stale docs are worse than no docs — delete or update + +## Review Rules + +- Agent reviews check: layer violations, golden principle adherence, test coverage +- Human reviews focus on: intent alignment, architectural fit, user impact +- Nit-level feedback is captured as golden principle updates, not blocking comments +`; +} + +export function coreBeliefsTemplate(): string { + return `# Core Beliefs + +Agent-first operating principles that guide all development decisions. + +## 1. Repository is the Single Source of Truth + +If it's not in the repo, it doesn't exist to the agent. Slack discussions, meeting notes, and tribal knowledge must be captured in versioned markdown. + +## 2. Agents Are First-Class Team Members + +Design docs, architecture guides, and workflows are written for agent consumption first. Human readability is a bonus, not the goal. + +## 3. Constraints Enable Speed + +Strict architectural rules, enforced mechanically, allow agents to ship fast without creating drift. Freedom within boundaries. + +## 4. Corrections Are Cheap + +In a high-throughput agent environment, fixing forward is usually cheaper than blocking. Optimize for flow, not perfection at merge time. + +## 5. Taste Is Captured Once, Enforced Continuously + +Human engineering judgment is encoded into golden principles and tooling, then applied to every line of code automatically. Taste doesn't scale through review — it scales through automation. + +## 6. Technical Debt Is a High-Interest Loan + +Pay it down continuously in small increments. Background agents handle cleanup. Never let it compound. + +## 7. Progressive Disclosure Over Information Dumps + +Give agents a map (short AGENTS.md) and teach them where to look. Don't overwhelm context with everything at once. +`; +} + +export function techDebtTrackerTemplate(): string { + return `# Technical Debt Tracker + +Track known technical debt with priority and ownership. + +| ID | Description | Domain | Priority | Status | Created | Updated | +|----|-------------|--------|----------|--------|---------|---------| +| TD-001 | Example: implement dependency linter | Core | High | Open | ${new Date().toISOString().split("T")[0]} | ${new Date().toISOString().split("T")[0]} | + +## Priority Definitions + +- **Critical**: Actively causing bugs or blocking features +- **High**: Will cause problems soon, should address this sprint +- **Medium**: Noticeable drag on velocity, schedule for cleanup +- **Low**: Minor annoyance, address opportunistically + +## Process + +1. New debt discovered → add row here +2. Background agents scan weekly for new debt +3. Cleanup PRs opened targeting highest priority items +4. Resolved debt marked as "Closed" with resolution date +`; +} + +export function riskPolicyTemplate(pack: AutomationPack = "none"): string { + if (pack === "agent-factory") { + return `{ + "version": 1, + "description": "Agent-factory policy template for high-autonomy repositories. High-risk changes require human review and stronger CI gates.", + "tiers": ["low", "medium", "high"], + "riskTierRules": { + "high": [ + "src/security", + "src/auth", + ".github/workflows", + "risk-policy.json", + "AGENTS.md", + "ARCHITECTURE.md" + ], + "low": ["**"] + }, + "mergePolicy": { + "high": { + "requiredChecks": ["ci", "structural-lint", "risk-policy-gate"], + "requiresHumanReview": true, + "minApprovals": 1 + }, + "low": { + "requiredChecks": ["ci"], + "requiresHumanReview": false + } + }, + "docsDriftRules": { + "watchPaths": ["src", "scripts", ".github/workflows"], + "mustUpdate": ["AGENTS.md", "ARCHITECTURE.md", "docs/golden-principles.md"] + } +} +`; + } + + return `{ + "version": 1, + "tiers": ["low", "medium", "high"], + "watchPaths": ["src", "docs", "skill"], + "docsDriftRules": [ + { + "watch": "src", + "docs": ["ARCHITECTURE.md", "docs/design-docs/index.md", "docs/golden-principles.md"] + }, + { + "watch": "skill", + "docs": ["AGENTS.md", "skill/Reins/HarnessMethodology.md"] + } + ] +} +`; +} + +function agentFactoryLintStructureScriptTemplate(): string { + return `#!/usr/bin/env node +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { join, relative } from "node:path"; + +const ROOT = process.cwd(); +const TARGET_DIRS = ["src", "apps", "packages"]; +const CODE_FILE_PATTERN = /\\.(ts|tsx|js|jsx|mjs|cjs)$/; + +const checks = [ + { + id: "no-window-confirm", + pattern: /window\\\\.confirm\\\\(/, + message: "Use a controlled confirmation flow instead of window.confirm().", + }, + { + id: "no-raw-img", + pattern: /\\\\/]/, + message: "Use framework image components instead of raw where possible.", + }, + { + id: "no-nested-ternary", + pattern: /\\\\?.*:\\\\s*.*\\\\?.*:/, + message: "Avoid nested ternaries. Prefer explicit control flow.", + }, +]; + +function collectFiles(dir) { + const files = []; + let entries = []; + try { + entries = readdirSync(dir); + } catch { + return files; + } + + for (const entry of entries) { + const fullPath = join(dir, entry); + let stat = null; + try { + stat = statSync(fullPath); + } catch { + continue; + } + if (!stat) continue; + + if (stat.isDirectory()) { + if (["node_modules", ".git", "dist", "build", ".next"].includes(entry)) continue; + files.push(...collectFiles(fullPath)); + continue; + } + + if (CODE_FILE_PATTERN.test(entry)) files.push(fullPath); + } + + return files; +} + +const files = []; +for (const dir of TARGET_DIRS) { + files.push(...collectFiles(join(ROOT, dir))); +} + +const violations = []; +for (const file of files) { + let content = ""; + try { + content = readFileSync(file, "utf-8"); + } catch { + continue; + } + + const lines = content.split("\\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + for (const check of checks) { + if (!check.pattern.test(line)) continue; + violations.push({ + file: relative(ROOT, file), + line: i + 1, + rule: check.id, + message: check.message, + }); + } + } +} + +if (violations.length > 0) { + console.error("Structural lint violations found:"); + for (const violation of violations) { + console.error( + "- " + + violation.file + + ":" + + violation.line + + " [" + + violation.rule + + "] " + + violation.message, + ); + } + process.exit(1); +} + +console.log("Structural lint passed."); +`; +} + +function agentFactoryDocGardenerScriptTemplate(): string { + return `#!/usr/bin/env node +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { join, relative } from "node:path"; + +const ROOT = process.cwd(); +const DOCS_DIR = join(ROOT, "docs"); +const STALE_DAYS = 90; +const VERIFIED_PATTERN = //; + +function collectMarkdown(dir) { + const files = []; + let entries = []; + try { + entries = readdirSync(dir); + } catch { + return files; + } + + for (const entry of entries) { + const fullPath = join(dir, entry); + let stat = null; + try { + stat = statSync(fullPath); + } catch { + continue; + } + if (!stat) continue; + + if (stat.isDirectory()) { + files.push(...collectMarkdown(fullPath)); + continue; + } + + if (entry.endsWith(".md")) files.push(fullPath); + } + return files; +} + +function ageInDays(dateStr) { + const now = new Date(); + const verified = new Date(dateStr + "T00:00:00Z"); + const diffMs = now.getTime() - verified.getTime(); + return Math.floor(diffMs / (1000 * 60 * 60 * 24)); +} + +const files = collectMarkdown(DOCS_DIR); +const stale = []; +const unverified = []; + +for (const file of files) { + let content = ""; + try { + content = readFileSync(file, "utf-8"); + } catch { + continue; + } + const head = content.split("\\n").slice(0, 8).join("\\n"); + const match = VERIFIED_PATTERN.exec(head); + if (!match) { + unverified.push(relative(ROOT, file)); + continue; + } + + const days = ageInDays(match[1]); + if (days > STALE_DAYS) { + stale.push(relative(ROOT, file) + " (" + days + " days)"); + } +} + +if (stale.length > 0) { + console.error("Stale docs detected:"); + for (const entry of stale) console.error("- " + entry); + process.exit(1); +} + +console.log("Doc-gardener passed. Unverified docs: " + unverified.length); +`; +} + +function agentFactoryChangedDocFreshnessScriptTemplate(): string { + return `#!/usr/bin/env node +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +const ROOT = process.cwd(); +const VERIFIED_PATTERN = //; + +function getArg(name) { + const idx = process.argv.indexOf(name); + if (idx === -1 || idx + 1 >= process.argv.length) return null; + return process.argv[idx + 1]; +} + +function isDocPath(filePath) { + if (!filePath.endsWith(".md")) return false; + return ( + filePath.startsWith("docs/") || + filePath === "AGENTS.md" || + filePath === "ARCHITECTURE.md" || + filePath === "docs/golden-principles.md" + ); +} + +const changedPath = getArg("--changed-file-list"); +if (!changedPath) { + console.error("Missing required argument: --changed-file-list "); + process.exit(2); +} + +const changedFileList = existsSync(changedPath) ? changedPath : join(ROOT, changedPath); +if (!existsSync(changedFileList)) { + console.error("Changed file list not found: " + changedFileList); + process.exit(2); +} + +const changedDocs = readFileSync(changedFileList, "utf-8") + .split("\\n") + .map((line) => line.trim()) + .filter(Boolean) + .filter(isDocPath); + +if (changedDocs.length === 0) { + console.log("No changed docs detected. Skipping changed-doc freshness gate."); + process.exit(0); +} + +const invalidDocs = []; +for (const relPath of changedDocs) { + const fullPath = join(ROOT, relPath); + if (!existsSync(fullPath)) continue; + + const content = readFileSync(fullPath, "utf-8"); + const head = content.split("\\n").slice(0, 8).join("\\n"); + if (!VERIFIED_PATTERN.test(head)) { + invalidDocs.push(relPath + " (missing Verified header)"); + } +} + +if (invalidDocs.length > 0) { + console.error("Changed-doc freshness check failed:"); + for (const entry of invalidDocs) console.error("- " + entry); + process.exit(1); +} + +console.log("Changed-doc freshness check passed for " + changedDocs.length + " doc(s)."); +`; +} + +function agentFactoryPrReviewScriptTemplate(): string { + return `#!/usr/bin/env node +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +const ROOT = process.cwd(); + +function getArg(name) { + const idx = process.argv.indexOf(name); + if (idx === -1 || idx + 1 >= process.argv.length) return null; + return process.argv[idx + 1]; +} + +const changedPathArg = getArg("--changed-file-list"); +if (!changedPathArg) { + console.error("Missing required argument: --changed-file-list "); + process.exit(2); +} + +const changedPath = existsSync(changedPathArg) ? changedPathArg : join(ROOT, changedPathArg); +if (!existsSync(changedPath)) { + console.error("Changed file list not found: " + changedPath); + process.exit(2); +} + +const checks = [ + { + id: "no-window-confirm", + pattern: /window\\\\.confirm\\\\(/, + message: "Use a controlled confirmation flow instead of window.confirm().", + }, + { + id: "no-raw-img", + pattern: /\\\\/]/, + message: "Prefer framework image components over raw .", + }, + { + id: "no-nested-ternary", + pattern: /\\\\?.*:\\\\s*.*\\\\?.*:/, + message: "Avoid nested ternaries. Prefer explicit control flow.", + }, +]; + +const changedFiles = readFileSync(changedPath, "utf-8") + .split("\\n") + .map((line) => line.trim()) + .filter(Boolean); + +const comments = []; + +for (const relPath of changedFiles) { + const fullPath = join(ROOT, relPath); + if (!existsSync(fullPath)) continue; + if (!/\\\\.(ts|tsx|js|jsx|mjs|cjs)$/.test(relPath)) continue; + + const content = readFileSync(fullPath, "utf-8"); + const lines = content.split("\\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + for (const check of checks) { + if (!check.pattern.test(line)) continue; + comments.push({ + path: relPath, + line: i + 1, + body: "Golden principle (" + check.id + "): " + check.message, + }); + } + } +} + +const output = { comments }; +const json = JSON.stringify(output, null, 2); +console.log(json); +writeFileSync("/tmp/reins-review-comments.json", json, "utf-8"); +process.exit(0); +`; +} + +function agentFactoryRiskPolicyWorkflowTemplate(): string { + return [ + "name: Risk Policy Gate", + "", + "on:", + " pull_request:", + " types: [opened, synchronize, reopened]", + "", + "permissions:", + " contents: read", + " pull-requests: write", + "", + "jobs:", + " risk-gate:", + " runs-on: ubuntu-latest", + " timeout-minutes: 8", + " steps:", + " - name: Checkout", + " uses: actions/checkout@v4", + "", + " - name: Get changed files", + " run: |", + " git fetch --no-tags --depth=1 origin ${{ github.event.pull_request.base.sha }}", + ' BASE_SHA="$(git rev-parse FETCH_HEAD)"', + ' git diff --name-only "$BASE_SHA" HEAD > changed_files.txt', + " cat changed_files.txt", + "", + " - name: Evaluate risk tier", + " id: evaluate", + " run: |", + " node - <<'NODE'", + " const fs = require('node:fs');", + " const policy = JSON.parse(fs.readFileSync('risk-policy.json', 'utf8'));", + " const changed = fs.readFileSync('changed_files.txt', 'utf8').split('\\n').map((x) => x.trim()).filter(Boolean);", + " const highPaths = (policy.riskTierRules && policy.riskTierRules.high) || [];", + " const highMatches = changed.filter((file) =>", + " highPaths.some((pattern) => file === pattern || file.startsWith(pattern + '/'))", + " );", + " const riskTier = highMatches.length > 0 ? 'HIGH' : 'LOW';", + " fs.appendFileSync(process.env.GITHUB_OUTPUT, `risk_tier=${riskTier}\\n`);", + " fs.writeFileSync('risk-summary.txt', highMatches.join('\\n'), 'utf8');", + " NODE", + "", + " - name: Post risk summary comment", + " uses: actions/github-script@v7", + " with:", + " script: |", + " const marker = '';", + " const riskTier = '${{ steps.evaluate.outputs.risk_tier }}';", + " const fs = require('node:fs');", + " const highMatches = fs.readFileSync('risk-summary.txt', 'utf8').split('\\n').map((x) => x.trim()).filter(Boolean);", + " let body = marker + '\\n## Risk Policy Gate\\n\\n';", + " body += '**Risk Tier:** ' + riskTier + '\\n';", + " if (highMatches.length > 0) {", + " body += '\\n### High-Risk Files Changed\\n';", + " for (const file of highMatches) body += '- `' + file + '`\\n';", + " }", + " const { data: comments } = await github.rest.issues.listComments({", + " owner: context.repo.owner,", + " repo: context.repo.repo,", + " issue_number: context.issue.number,", + " });", + " const existing = comments.find((c) => c.body && c.body.includes(marker));", + " if (existing) {", + " await github.rest.issues.updateComment({", + " owner: context.repo.owner,", + " repo: context.repo.repo,", + " comment_id: existing.id,", + " body,", + " });", + " } else {", + " await github.rest.issues.createComment({", + " owner: context.repo.owner,", + " repo: context.repo.repo,", + " issue_number: context.issue.number,", + " body,", + " });", + " }", + "", + " docs-freshness:", + " runs-on: ubuntu-latest", + " timeout-minutes: 8", + " continue-on-error: true", + " steps:", + " - name: Checkout", + " uses: actions/checkout@v4", + " - name: Doc gardener", + " run: node scripts/doc-gardener.mjs", + "", + " changed-doc-freshness:", + " runs-on: ubuntu-latest", + " timeout-minutes: 8", + " steps:", + " - name: Checkout", + " uses: actions/checkout@v4", + " - name: Get changed files", + " run: |", + " git fetch --no-tags --depth=1 origin ${{ github.event.pull_request.base.sha }}", + ' BASE_SHA="$(git rev-parse FETCH_HEAD)"', + ' git diff --name-only "$BASE_SHA" HEAD > changed_files.txt', + " cat changed_files.txt", + " - name: Check changed-doc freshness", + " run: node scripts/check-changed-doc-freshness.mjs --changed-file-list changed_files.txt", + "", + ].join("\\n"); +} + +function agentFactoryPrReviewWorkflowTemplate(): string { + return [ + "name: PR Review Bot — Golden Principles", + "", + "on:", + " pull_request:", + " types: [opened, synchronize]", + "", + "permissions:", + " contents: read", + " pull-requests: write", + "", + "concurrency:", + " group: pr-review-bot-${{ github.event.pull_request.number }}", + " cancel-in-progress: true", + "", + "jobs:", + " review-changed-files:", + " runs-on: ubuntu-latest", + " timeout-minutes: 10", + " steps:", + " - name: Checkout", + " uses: actions/checkout@v4", + " with:", + " fetch-depth: 0", + "", + " - name: Get changed files", + " run: |", + " git fetch --no-tags --depth=1 origin ${{ github.event.pull_request.base.sha }}", + ' BASE_SHA="$(git rev-parse FETCH_HEAD)"', + ' git diff --name-only "$BASE_SHA" HEAD > changed_files.txt', + " cat changed_files.txt", + "", + " - name: Run review script", + " run: node scripts/pr-review.mjs --changed-file-list changed_files.txt > /tmp/reins-review-output.json", + "", + " - name: Post review summary comment", + " uses: actions/github-script@v7", + " with:", + " script: |", + " const marker = '';", + " const fs = require('node:fs');", + " const report = JSON.parse(fs.readFileSync('/tmp/reins-review-output.json', 'utf8'));", + " const comments = report.comments || [];", + " let body = marker + '\\n## Reins Golden Principles Review\\n\\n';", + " if (comments.length === 0) {", + " body += 'No golden principle violations found in changed files.';", + " } else {", + " body += 'Found **' + comments.length + '** potential violations.\\n\\n';", + " for (const item of comments.slice(0, 20)) {", + " body += '- `' + item.path + ':' + item.line + '` ' + item.body + '\\n';", + " }", + " if (comments.length > 20) body += '\\n...and more. See workflow logs for full output.\\n';", + " }", + " const { data: commentsList } = await github.rest.issues.listComments({", + " owner: context.repo.owner,", + " repo: context.repo.repo,", + " issue_number: context.issue.number,", + " });", + " const existing = commentsList.find((c) => c.body && c.body.includes(marker));", + " if (existing) {", + " await github.rest.issues.updateComment({", + " owner: context.repo.owner,", + " repo: context.repo.repo,", + " comment_id: existing.id,", + " body,", + " });", + " } else {", + " await github.rest.issues.createComment({", + " owner: context.repo.owner,", + " repo: context.repo.repo,", + " issue_number: context.issue.number,", + " body,", + " });", + " }", + "", + ].join("\\n"); +} + +function agentFactoryStructuralLintWorkflowTemplate(): string { + return [ + "name: Structural Lint", + "", + "on:", + " pull_request:", + " types: [opened, synchronize, reopened]", + "", + "jobs:", + " structural-lint:", + " runs-on: ubuntu-latest", + " timeout-minutes: 8", + " steps:", + " - name: Checkout", + " uses: actions/checkout@v4", + " - name: Run structural lint", + " run: node scripts/lint-structure.mjs", + "", + ].join("\\n"); +} + +export function getAgentFactoryPackFiles(): AgentFactoryPackFile[] { + return [ + { path: "scripts/lint-structure.mjs", content: agentFactoryLintStructureScriptTemplate() }, + { path: "scripts/doc-gardener.mjs", content: agentFactoryDocGardenerScriptTemplate() }, + { path: "scripts/check-changed-doc-freshness.mjs", content: agentFactoryChangedDocFreshnessScriptTemplate() }, + { path: "scripts/pr-review.mjs", content: agentFactoryPrReviewScriptTemplate() }, + { path: ".github/workflows/risk-policy-gate.yml", content: agentFactoryRiskPolicyWorkflowTemplate() }, + { path: ".github/workflows/pr-review-bot.yml", content: agentFactoryPrReviewWorkflowTemplate() }, + { path: ".github/workflows/structural-lint.yml", content: agentFactoryStructuralLintWorkflowTemplate() }, + ]; +} + +export function designDocsIndexTemplate(): string { + return `# Design Documents Index + +Registry of all design documents with verification status. + +| Document | Status | Last Verified | Owner | +|----------|--------|---------------|-------| +| core-beliefs.md | Current | ${new Date().toISOString().split("T")[0]} | Team | + +## Verification Schedule + +Design docs are verified against the actual codebase on a recurring cadence: +- **Weekly**: Active design docs for in-progress features +- **Monthly**: All design docs +- **On change**: When related code is significantly modified + +## Status Definitions + +- **Current**: Verified to match actual implementation +- **Stale**: Known to be out of date, needs update +- **Draft**: In progress, not yet finalized +- **Archived**: No longer relevant, kept for historical reference +`; +} + +export function productSpecsIndexTemplate(): string { + return `# Product Specifications Index + +Registry of all product specifications. + +| Spec | Status | Priority | Owner | +|------|--------|----------|-------| +| — | — | — | — | + +## Adding a New Spec + +1. Create a new markdown file in this directory +2. Add it to the table above +3. Include: problem statement, proposed solution, acceptance criteria, out of scope +4. Link related design docs and execution plans +`; +} diff --git a/cli/reins/src/lib/types.ts b/cli/reins/src/lib/types.ts new file mode 100644 index 0000000..5fc14b7 --- /dev/null +++ b/cli/reins/src/lib/types.ts @@ -0,0 +1,53 @@ +export interface AuditScore { + score: number; + max: number; + findings: string[]; +} + +export interface AuditResult { + project: string; + timestamp: string; + scores: { + repository_knowledge: AuditScore; + architecture_enforcement: AuditScore; + agent_legibility: AuditScore; + golden_principles: AuditScore; + agent_workflow: AuditScore; + garbage_collection: AuditScore; + }; + total_score: number; + max_score: 18; + maturity_level: string; + recommendations: string[]; +} + +export interface InitOptions { + path: string; + name: string; + force: boolean; + pack: string; + allowExistingAgents?: boolean; +} + +export type DoctorStatus = "pass" | "fail" | "warn"; + +export interface DoctorCheck { + check: string; + status: DoctorStatus; + fix: string; +} + +export interface EvolutionStep { + step: number; + action: string; + description: string; + automated: boolean; +} + +export interface EvolutionPath { + from: string; + to: string; + goal: string; + steps: EvolutionStep[]; + success_criteria: string; +} diff --git a/docs/design-docs/ci-enforcement-and-risk-policy.md b/docs/design-docs/ci-enforcement-and-risk-policy.md index 6958ccd..0366779 100644 --- a/docs/design-docs/ci-enforcement-and-risk-policy.md +++ b/docs/design-docs/ci-enforcement-and-risk-policy.md @@ -1,6 +1,6 @@ # CI Enforcement and Risk Policy - + This document captures how `reins` currently enforces quality gates and docs-drift policy on itself. @@ -39,6 +39,10 @@ It now uses explicit regex patterns for gates like `lint`, `test`, and `typechec - Commits and pushes the bump to the PR branch for same-repo PRs - Skips push behavior for fork PRs +6. Conversation resolution is a merge gate. +When GitHub branch protection enables "Require conversation resolution before merging", unresolved review threads block merge even if CI is green. +Operationally, use Conductor's `Checks` view and `Todos` to track unresolved review feedback before attempting merge. + ## Rationale - Policy-as-code provides deterministic governance signals for audits and doctor checks. @@ -55,3 +59,4 @@ It now uses explicit regex patterns for gates like `lint`, `test`, and `typechec - Merges to master only publish when the merged PR already includes a new package version. - Requires npm Trusted Publisher configuration for this GitHub repository/workflow (OIDC), not a long-lived `NPM_TOKEN`. - Fork PRs may still require maintainer follow-up for version bump commits. +- "Green CI" is not sufficient for merge readiness when conversation resolution is enforced; unresolved comment threads must be closed. diff --git a/docs/design-docs/ecosystem-positioning.md b/docs/design-docs/ecosystem-positioning.md index 400b944..4ba7bb1 100644 --- a/docs/design-docs/ecosystem-positioning.md +++ b/docs/design-docs/ecosystem-positioning.md @@ -36,7 +36,7 @@ npx skills add WellDunDun/reins Once installed, the agent can discover the Reins workflows and run the CLI directly: - local source mode (inside this repo): `cd cli/reins && bun src/index.ts ../..` -- package mode (any repo): `npx reins-cli ` +- package mode (any repo): `npx reins-cli@latest ` **Core operations:** - `reins init` — scaffold the full harness engineering structure diff --git a/docs/design-docs/index.md b/docs/design-docs/index.md index 09e526f..213d34a 100644 --- a/docs/design-docs/index.md +++ b/docs/design-docs/index.md @@ -7,7 +7,7 @@ Registry of all design documents with verification status. | core-beliefs.md | Current | 2026-02-22 | Team | | ci-enforcement-and-risk-policy.md | Current | 2026-02-22 | Team | | ecosystem-positioning.md | Current | 2026-02-22 | Team | -| skill-evals-and-shell-boundaries.md | Current | 2026-02-22 | Team | +| skill-evals-and-shell-boundaries.md | Current | 2026-02-23 | Team | ## Verification Schedule diff --git a/docs/design-docs/skill-evals-and-shell-boundaries.md b/docs/design-docs/skill-evals-and-shell-boundaries.md index f99df69..69f34df 100644 --- a/docs/design-docs/skill-evals-and-shell-boundaries.md +++ b/docs/design-docs/skill-evals-and-shell-boundaries.md @@ -39,7 +39,7 @@ Reins remains local-first by default. Hosted shell guidance is documented as opt Local-first default: 1. Run local source inside this repo when available. -2. Otherwise use `npx reins-cli ...`. +2. Otherwise use `npx reins-cli@latest ...`. 3. Prefer deterministic JSON parsing for all command outputs. Hosted-shell optional guidance: @@ -62,16 +62,33 @@ Rationale: 1. Better trigger precision and fewer false positives. 2. Cleaner separation from adjacent general-purpose coding tasks. +## Decision 4: Preserve a Strict JSON Error Contract for CLI Commands + +Commands that emit JSON on success should emit structured JSON on operational failure as well. + +Implementation requirement: +1. Use `stderr` JSON objects for recoverable command failures (for example: scaffolding/apply failures). +2. Exit nonzero (`exit 1`) without emitting unstructured stack traces. +3. Keep message shape stable (`{ "error": "..." }`) so skill parsers can rely on it. +4. Skill parsers and CI consumers must capture `stderr` independently from `stdout` to observe structured failure payloads. + +Rationale: +1. Skills parse CLI output programmatically; unstructured exceptions break routing and remediation logic. +2. Deterministic failure payloads keep retry/escalation behavior testable. +3. Contract consistency reduces hidden coupling between skill prompts and CLI internals. + ## Consequences Positive: 1. Skill behavior becomes testable and trackable over time. 2. Failures become actionable via structured traces and rubric scores. 3. Security posture is explicit where shell/network integration exists. +4. `evolve --apply` now fails with structured JSON if init/pack scaffolding throws. Trade-offs: 1. Evals require fixture maintenance as prompts evolve. 2. CI integration must stay lightweight to avoid contributor friction. +3. Error message wording changes must preserve machine-readable shape to avoid parser drift. ## Related Plan diff --git a/docs/exec-plans/tech-debt-tracker.md b/docs/exec-plans/tech-debt-tracker.md index 6070b20..08ef909 100644 --- a/docs/exec-plans/tech-debt-tracker.md +++ b/docs/exec-plans/tech-debt-tracker.md @@ -4,7 +4,7 @@ Track known technical debt with priority and ownership. | ID | Description | Domain | Priority | Status | Created | Updated | |----|-------------|--------|----------|--------|---------|---------| -| TD-001 | Single-file CLI (1767 lines) could be modularized | CLI | Medium | Open | 2026-02-22 | 2026-02-22 | +| TD-001 | CLI modularization completed: router in `src/index.ts`, command handlers in `src/lib/commands/`, audit internals in `src/lib/audit/` | CLI | Medium | Closed | 2026-02-22 | 2026-02-23 | | TD-002 | Audit heuristics are file-existence only, no AST analysis | CLI | Medium | Open | 2026-02-22 | 2026-02-22 | | TD-003 | No language support beyond JS/TS ecosystems | CLI | High | Open | 2026-02-22 | 2026-02-22 | | TD-004 | Observability check not meaningful for CLI tools (resolved via CLI diagnosability scoring) | CLI | Low | Closed | 2026-02-22 | 2026-02-22 | diff --git a/package.json b/package.json index d7e6259..fae7e92 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "reins", - "version": "0.1.1", + "version": "0.1.2", "description": "Harness Engineering CLI — scaffold, audit, evolve, and doctor agent-readiness", "scripts": { "dev": "cd cli/reins && bun --watch src/index.ts", diff --git a/skill/Reins/SKILL.md b/skill/Reins/SKILL.md index ccc3879..7e004e5 100644 --- a/skill/Reins/SKILL.md +++ b/skill/Reins/SKILL.md @@ -7,6 +7,14 @@ description: Reins CLI skill for scaffold/audit/doctor/evolve workflows. Use whe Use the Reins CLI to operationalize harness engineering in any repository. +## Execution Model (Critical) + +1. The CLI is the execution engine and scoring source of truth. +2. This skill is the control plane for agent behavior (routing, command order, JSON parsing discipline). +3. Humans steer goals and tradeoffs; agents execute the loop. + +Do not re-implement CLI logic in skill instructions. Always run commands and parse JSON outputs. + ## Use When Use this skill when the user asks to: @@ -29,10 +37,14 @@ Do not use this skill for: Use this order when running commands: -1. If working inside the Reins repository itself: +1. In user repositories, check if installed skills are stale: +`npx skills check` +If updates are available, refresh before running workflow commands: +`npx skills update` +2. If working inside the Reins repository itself: `cd cli/reins && bun src/index.ts ../..` -2. Otherwise (or if local source is unavailable): -`npx reins-cli ` +3. Otherwise (or if local source is unavailable): +`npx reins-cli@latest ` All Reins commands output deterministic JSON. **Always parse JSON output** — never text-match against findings strings. @@ -42,7 +54,7 @@ All Reins commands output deterministic JSON. **Always parse JSON output** — n ```bash # Scaffold harness engineering structure -reins init [--name ] [--force] +reins init [--name ] [--force] [--pack ] # Score maturity across 6 dimensions (0-18) reins audit diff --git a/skill/Reins/Workflows/Audit.md b/skill/Reins/Workflows/Audit.md index 20edca1..2a2115c 100644 --- a/skill/Reins/Workflows/Audit.md +++ b/skill/Reins/Workflows/Audit.md @@ -6,7 +6,7 @@ Score an existing project against harness engineering principles. Produces a str Run the CLI first: - Local source: `cd cli/reins && bun src/index.ts audit ` -- Package mode: `npx reins-cli audit ` +- Package mode: `npx reins-cli@latest audit ` For remediation detail, pair with doctor: - `reins doctor ` diff --git a/skill/Reins/Workflows/Doctor.md b/skill/Reins/Workflows/Doctor.md index f12c43e..1058bbe 100644 --- a/skill/Reins/Workflows/Doctor.md +++ b/skill/Reins/Workflows/Doctor.md @@ -6,7 +6,7 @@ Diagnose readiness gaps with pass/fail/warn checks and prescriptive fixes. Run the CLI first: - Local source: `cd cli/reins && bun src/index.ts doctor ` -- Package mode: `npx reins-cli doctor ` +- Package mode: `npx reins-cli@latest doctor ` For scoring context, pair with audit: - `reins audit ` diff --git a/skill/Reins/Workflows/Evolve.md b/skill/Reins/Workflows/Evolve.md index 8c21c7f..ed0e720 100644 --- a/skill/Reins/Workflows/Evolve.md +++ b/skill/Reins/Workflows/Evolve.md @@ -6,7 +6,7 @@ Upgrade a project to the next Reins maturity level. Run: - Local source: `cd cli/reins && bun src/index.ts evolve ` -- Package mode: `npx reins-cli evolve ` +- Package mode: `npx reins-cli@latest evolve ` Optional flag: - `--apply` (limited auto-apply support) diff --git a/skill/Reins/Workflows/Scaffold.md b/skill/Reins/Workflows/Scaffold.md index 1ddb8c6..6e0b05a 100644 --- a/skill/Reins/Workflows/Scaffold.md +++ b/skill/Reins/Workflows/Scaffold.md @@ -6,7 +6,7 @@ Set up a repository with Reins harness-engineering structure. Use Reins before manual scaffolding: - Local source: `cd cli/reins && bun src/index.ts init ` -- Package mode: `npx reins-cli init ` +- Package mode: `npx reins-cli@latest init ` ## Output Format @@ -15,6 +15,9 @@ Use Reins before manual scaffolding: "command": "init", "project": "project-name", "target": "/abs/path/to/project", + "requested_automation_pack": null, + "automation_pack": null, + "automation_pack_reason": "No optional automation pack selected.", "created": [ "docs/design-docs/", "docs/exec-plans/active/", @@ -44,6 +47,7 @@ Use Reins before manual scaffolding: Notes: - `created` includes both directories (with trailing `/`) and files. - Existing scaffolding is refused unless `--force` is used. +- `automation_pack` is `null` by default, `"agent-factory"` when explicitly requested, or selected adaptively when `--pack auto` is used. ## Flags @@ -51,6 +55,7 @@ Notes: |------|---------|---------| | `--name ` | Set project name (default: directory name) | `reins init . --name MyProject` | | `--force` | Overwrite existing files | `reins init . --force` | +| `--pack ` | Optional automation templates (`auto`, `agent-factory`) | `reins init . --pack auto` | ## Steps