diff --git a/.gitignore b/.gitignore index fdedff0..9bfdfcd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,8 @@ __pycache__/ # Harness worktrees (per implement-mainspec parallel mode) .claude/worktrees/ + +# Per-machine harness runtime (managed by the context-specs CLI) +/state/ +/environments.toml +node_modules/ diff --git a/README.md b/README.md index 0cdacab..2df84b2 100644 --- a/README.md +++ b/README.md @@ -1,290 +1,98 @@ # Context Specs -**Context engineering for agent-first development.** +**Applied harness engineering.** -The biggest lever you have when building with a coding agent is what goes into -its context window — and what stays out. Context Specs is that lever, pulled at -three layers that build on each other. Pull all three and the work inverts: you -describe a feature and pin down what "done" means, then the project plans it, -implements it, verifies it, and hands you a finished pull request. What's left -for you is the judgment a model can't supply — understanding the problem, -deciding what's worth building, and telling whether what came back is right. +*Harness engineering is building a system — defined inputs, defined outputs — out of +deterministic code and probabilistic code, where the deterministic code invokes the +probabilistic code and controls the flow all the way to a guaranteed, well-defined +output.* -It ships as composable [Agent Skills](#installation). Install once, invoke via -slash commands, extend with your organization's own expertise. +Context Specs is that system for coding — **autonomous** and **goal-based**. You +express intent (your goal); the harness runs on its own; you always get back a pull +request (your guaranteed output) — either **ready to merge**, or **STUCK with a +diagnosis** of what blocked it. -> **New here? Read [the full story](./docs/README.md)** — six short chapters that -> take you from the idea to the mindset shift, in order. +## The point isn't one PR — it's the system that produces them ---- +You work at the level of the **system that builds features**, not the individual +feature. That distinction is the whole idea: -## The idea: the right context at the right time +- **Fix a piece of code** and you ship that one feature. Nothing else changes; the + next feature starts where the last one did. +- **Fix a context gap** — a thin definition of done, a missing piece of long-term + memory, an environment the agent couldn't navigate — and *every future feature* + gets better. You fixed a class of failure, not an instance. -A coding agent is only ever as good as the context it's reasoning over — the -actual tokens in its window at the moment it decides. That window is finite, and -it degrades: older instructions lose influence, autonomously-retrieved files -crowd out relevant ones, and compaction silently drops things when it fills. +You do both, but context-gap fixes are the main lever, because they compound. As you +pull it, more PRs come back ready-to-merge and fewer come back stuck. -Context engineering is the discipline of getting the right context into that -window at the right time, and keeping everything else out — the agent reading -what it needs, when it needs it, from files it can navigate on its own. The three -layers below are that same idea at a widening scope: first one feature, then a -whole project that builds itself, then the way you work day to day. +So the craft shifts. Instead of typing features, you do **context engineering**: set +up environments agents can work in, pin goals to a real definition of done, and grow +the project's long-term memory — the accumulated knowledge that tells the agent how +to plan every feature. Getting that right is what turns AI slop into PRs you can +actually merge. You operate on the harness; the harness operates on the code. -→ [Chapter 1 — Context engineering](./docs/1-context-engineering.md) +## One harness, many projects ---- - -## Layer 1 — Spec-Driven Development - -*Context engineering, applied to building one feature. Usable on its own.* - -![Experts](./experts.png) - -You create **domain experts** from your own documentation with -[`/expert-sdd-creator`](./skills/sdd/expert-sdd-creator/SKILL.md) — -define the knowledge once, and it flows automatically through every phase: - -1. **Spec Planning** ([`/spec-planning`](./skills/sdd/spec-planning/SKILL.md)) — - research the codebase, pull in matching experts, and write the plan to disk as - a **mainspec** plus temporally-ordered **slices**. Planning lives *outside* the - context window, so it can't decay or compact away; slicing feeds the - implementer only the piece it's working on. -2. **Spec Validation** ([`/spec-validate`](./skills/sdd/spec-validate/SKILL.md)) — - 3+ independent Opus reviewers plus expert review, with consensus scoring - (3/3 = very high, 2/3 = high) turning agreement into a confidence signal. - Impactful findings are applied in place. -3. **Implementation** ([`/implement-mainspec`](./skills/sdd/implement-mainspec/SKILL.md)) — - slices implemented in dependency order, sequential or auto-parallelized across - git worktrees, each gated by **Signal** (the runtime feedback loop, below). - -**Composable, not hardcoded.** Multiple experts activate for one feature — a -React expert and a DynamoDB expert both contribute on a full-stack change. Add or -remove experts without touching any existing skill; organizations layer in -private experts for internal libraries the same way. - -![Signal](./signal.png) - -**Signal is half of what an expert is.** Where the expert curates context -*before* implementation, Signal validates behavior *during* it: run the tests, -hit the endpoint, check the build, iterate until it passes. Unit tests are the -default; experts can define richer ones. - -→ [Chapter 2 — Spec-Driven Development](./docs/2-spec-driven-development.md) - ---- - -## Layer 2 — The agent harness - -*Your project runs the Layer 1 loop for you — and gets better every merge.* - -The whole promise, in one sequence: - -> Describe a feature to [`/intent`](./skills/human-loop/intent/SKILL.md) and confirm -> it. Walk away. The project plans the feature, validates the plan, implements -> it, runs its checks, opens a pull request, answers the reviewer — and hands you -> back a PR that's ready to merge. +You maintain **one** harness repo and point it at any number of project repos. Improve +the harness once and every project inherits it. ```mermaid flowchart LR - Intent["/intent\n(you, once)"] --> Plan["/spec-planning"] - Plan --> Validate["/spec-validate"] - Validate --> Implement["/implement-mainspec"] - Implement --> Checks["local-checks"] - Checks --> PR[Open PR] - PR --> Review[Reviewer ⇄ /address-feedback] - Review --> Ready[Ready for your review] - Ready --> You([You merge]) + H["Your harness\n(one repo you tune)"] + H --> A[project A] + H --> B[project B] + H --> C[project C] ``` -You can trust a machine to run this unattended because it keeps **no hidden -state**. The complete state of every feature is observable from the files on disk -and the branches in git. A small, deterministic dispatcher — **no LLM in the loop** -— reads that state each tick and shells out to a fresh `claude -p` process per -step, so no step inherits another's polluted window and crash recovery is free. -Four layers of verification (pre-commit, slice signals, `local-checks`, and a -runnable `run-prd-test.sh` definition of done) can't be talked past — and when a -step genuinely can't make progress, the harness stops and hands you a -**diagnosis-first** report rather than faking success. - -**It also remembers.** When code lands on `main`, the harness updates its own -long-term memory: [`/learn`](./skills/harness/learn/SKILL.md) reads the -merged diff and routes what's worth keeping into the Expert, the `AGENTS.md` map, -or a custom lint the agent can't ship past — so the next feature starts knowing -what the last one taught. One write path, ground truth only, behind a human -merge. - -Setting all this up is itself a guided skill: -[`/harness-init`](./skills/harness/harness-init/SKILL.md). - -→ [Chapter 3 — The agent harness](./docs/3-the-agent-harness.md) · -[Chapter 4 — Continuous improvement](./docs/4-continuous-improvement.md) - ---- - -## Layer 3 — The human loop - -*Once the machine does the typing, what's left is the part only you can do.* - -The governing line is Karpathy's: *you can outsource your thinking, but you can't -outsource your understanding.* The harness took the verifiable work — syntax, API -recall, mechanics. The human loop is you, working the unverifiable: whether this -is the right thing to build, whether the abstraction is sound, whether it feels -right. - -``` -Understanding ──▶ Intent ──▶ [ the harness builds ] ──▶ Evaluate - /wiki-init /intent /evaluate-pr - /evaluate-sessions -``` - -- **Understanding** ([`/wiki-init`](./skills/human-loop/wiki-init/SKILL.md)) — - a Karpathy "LLM Wiki" that builds *your* model of the problem space, so you - arrive at Intent with sharper questions. -- **Intent** ([`/intent`](./skills/human-loop/intent/SKILL.md)) — turn that - understanding into a PRD plus a runnable definition of done. -- **Evaluate** ([`/evaluate-pr`](./skills/human-loop/evaluate-pr/SKILL.md) + - [`/evaluate-sessions`](./skills/human-loop/evaluate-sessions/SKILL.md)) — - walk the change to build real understanding, *and* read the build trail to find - where the project's context served the agents or failed them. The flywheel: - *observe a trace → capture it as an eval → fix the context → it persists as a - regression test.* - -Your loop's output is the harness's input; the harness's output is your loop's -input. When you evaluate, you don't just approve work — you improve the thing -that produced it. - -→ [Chapter 5 — The human loop](./docs/5-the-human-loop.md) - ---- - -## The shift +How the two tiers divide, how the deterministic dispatcher drives each project, and +why it's safe to leave running are all in the docs — this README stays deliberately +short. -Put the three layers together and your project stops being a codebase you type -into and becomes a **harness you tune**. The code is an output of the system; your -attention moves from syntax to context. Three things stay yours: deep -understanding of the problem space, theorizing and expressing intent, and -evaluating outcomes to improve the context so every future feature benefits. +## Quickstart -A codebase you type into is a constant cost. A harness you tune is an -appreciating asset — each feature leaves it a little more capable of building the -next one. - -→ [Chapter 6 — The mindset shift](./docs/6-the-mindset-shift.md) - ---- +```bash +# 1 · Create your harness (one repo that drives all your projects) +npx context-specs init my-harness +cd my-harness -## Getting started +# 2 · Register a project as an environment +context-specs add ~/code/myapp -**Set it up once.** [Install the skills](#installation), then run -[`/harness-init`](./skills/harness/harness-init/SKILL.md) — a guided setup that -builds your project's custom harness and provisions its worktrees. It runs as -**two independent loops, each in its own long-lived session.** Start them: +# 3 · Generate the project-specific pieces, then merge the PR it opens +cd ~/code/myapp && claude +> /env-init # AGENTS.md, /intent, the Expert, checks, bootstrap -```bash -# session 1 — the build loop (features) -/loop 5m /poll-and-dispatch - -# session 2 — the memory loop (/learn), in a SEPARATE session -/loop 10m /learn-loop +# 4 · Describe a feature, then walk away +> /intent # idea → PRD + runnable definition of done +# then, back in your harness repo: +context-specs start ``` -*(Optionally run [`/wiki-init`](./skills/human-loop/wiki-init/SKILL.md) first to -build your own understanding of the problem space before you write intent.)* - -With both loops running, you live in a three-beat cycle: you express intent, the -harness builds, and you evaluate what comes back. +From there you live in a three-beat cycle — **express intent, the harness builds, you +evaluate**: ```mermaid flowchart LR - Intent["/intent\nyou express intent"] --> Build["build loop\nplans → builds the PR"] - Build --> Eval["/evaluate-pr · /evaluate-sessions\nyou understand it"] - Eval --> Merge["merge"] - Merge --> Learn["learn loop\nupdates memory → PR"] + Intent["/intent\nyou express intent"] --> Build["the harness builds\nplans → PR"] + Build --> Eval["/evaluate-pr\nyou understand & merge"] + Eval --> Learn["the harness learns\nupdates memory"] Learn --> Intent ``` -**1 · You express intent (human loop, before).** `/intent` a feature — a PRD plus -a runnable definition of done. That PRD is the harness's input. - -**2 · The harness builds (two loops).** -- **The build loop** (`/loop 5m /poll-and-dispatch`) picks up your intent and runs - the SDD chain — plan, validate, implement, verify — all the way to an open PR. -- **The memory loop** (`/loop 10m /learn-loop`) watches `main`. Every merge is a - chance to add memory, or remove a memory a change has made stale. It raises a - *separate* PR for those memory updates, so long-term learning never blocks the - build loop. The two coordinate only through git. - -**3 · You evaluate (human loop, after).** -- **`/evaluate-pr`** — the goal here is *understanding* the change, not reaching a - verdict. Approving, requesting changes, or closing are byproducts of that - understanding; the understanding itself is what lets you write sharper intent - next time. -- **`/evaluate-sessions`** — for when the outcome was off, whether the harness got - **STUCK** or the PR was wrong on review. This is usually a context issue: with - the right context, the agent could have done the task. Evaluate-sessions reads every session - that led to the PR — intent, spec-planning, spec-validate, implementation — to - locate the **context gap**. Fixing the context matters more than fixing - the code: the code fix repairs this one feature, while the context fix keeps the same mistake out of every future feature. - -Your intent is the harness's input; the harness's PR is your evaluation's input; evaluating -improves the context that feeds the next round. That's the cycle you live in. - -*(Only want the spec workflow? Layer 1 stands on its own — drive `/spec-planning` -→ `/spec-validate` → `/implement-mainspec` by hand, no harness needed.)* - ---- - -## Installation - -Context Specs has two kinds of installable content — **skills** and -**subagents** — each with its own step. - -### Skills - -```bash -npx skills add https://github.com/capitalone/context-specs -``` - -Installs all skills into your project's `.claude/skills/` directory. - -### Subagents - -```bash -curl -fsSL https://raw.githubusercontent.com/capitalone/context-specs/main/install-agents.sh | bash -``` - -Run from your target project directory. Copies agent definitions (e.g. -`slice-implementer.md`) into `.claude/agents/`. Safe to re-run. - ---- - -## The skills - -| Layer | Skill | Does | -|---|---|---| -| **1 · SDD** | `/expert-sdd-creator` | Create a domain expert from your docs | -| | `/spec-planning` | Idea → mainspec + temporal slices | -| | `/spec-validate` | Multi-agent consensus + expert review | -| | `/implement-slice`, `/implement-mainspec` | Implement with Signal feedback, sequential or parallel | -| **2 · Harness** | `/harness-init` | Guided setup of the local harness | -| | `/fix-local-checks` | Honest fixes for a failing pre-PR gate | -| | `/address-feedback` | Triage and answer reviewer findings | -| | `/learn` | Post-merge long-term memory update | -| **3 · Human loop** | `/wiki-init` | Stand up a Karpathy LLM-wiki (Understanding) | -| | `/intent` | Idea → PRD + runnable definition of done | -| | `/evaluate-pr` | Evaluate the change; build understanding | -| | `/evaluate-sessions` | Evaluate the build trail; capture evals | - ---- +`context-specs status` shows every project's features and phases; `run` does one +foreground pass; `logs -f` follows the loop; `doctor` checks the wiring. ## Documentation -- **[The full story](./docs/README.md)** — the six chapters, read in order. -- **[Design invariants](./docs/invariants.md)** — the properties that make the - harness safe to leave running. - ---- +- **[How Context Specs works](./docs/overview.md)** — the narrative tour, start to + finish. +- **[Core concepts](./docs/README.md#core-concepts)** — harness engineering, context + engineering, the dispatcher, memory, continuous improvement, the human loop. +- **[How-to guides](./docs/README.md#how-to-guides)** — set up, run, and improve the + harness. +- **[Design invariants](./docs/reference/invariants.md)** — the proof it's safe to + leave running. ## License diff --git a/bin/context-specs b/bin/context-specs new file mode 100755 index 0000000..c0b5ee1 --- /dev/null +++ b/bin/context-specs @@ -0,0 +1,66 @@ +#!/usr/bin/env node +'use strict'; +// context-specs — the deterministic half of the harness. +// +// Tier 1 (harness repo) owned. Everything here is plain code: registering +// environments, symlinking the canonical skills into them, and supervising the +// dispatcher loops. The probabilistic half (project-specific setup) lives in +// the /env-init skill, which this CLI hands off to after `add`. + +const HELP = `context-specs — set up and run the coding harness across your environments + +Usage: context-specs [args] + +Setup + init [name] Create (or adopt) a harness repo from the context-specs template + add [--name n] Register an environment repo: symlink skills, write .gitignore + entries, add it to environments.toml. Then run /env-init inside it. + remove Unregister an environment (leaves its files and state in place) + link (Re-)create the skill/agent symlinks in a repo or worktree. + Idempotent; called automatically by bootstrap-worktree.sh. + +Run + start [name|--all] Start the background supervisor (build loop + memory loop) + stop [name|--all] Stop the supervisor + run [name] [--learn] One foreground tick, drained to idle. No daemon. + status [--fetch] One-shot table: every environment's features and phases + logs [-f] [--learn|--supervisor] Show (or follow) an environment's logs + doctor Check the harness, the registry, and every environment + +The dispatcher itself is deterministic bash (scripts/poll-and-dispatch.sh); +this CLI only schedules it. Exit 10 from a tick means "work advanced" and the +supervisor re-invokes immediately until the environment is idle (exit 0). +`; + +const commands = { + init: () => require('../lib/init'), + add: () => require('../lib/add'), + remove: () => require('../lib/remove'), + link: () => require('../lib/link').cli, + start: () => require('../lib/supervisor').start, + stop: () => require('../lib/supervisor').stop, + run: () => require('../lib/supervisor').runOnce, + __supervise: () => require('../lib/supervisor').supervise, + status: () => require('../lib/status'), + logs: () => require('../lib/logs'), + doctor: () => require('../lib/doctor'), +}; + +async function main() { + const [cmd, ...args] = process.argv.slice(2); + if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { + process.stdout.write(HELP); + process.exit(cmd ? 0 : 1); + } + const load = commands[cmd]; + if (!load) { + console.error(`context-specs: unknown command '${cmd}'. Run 'context-specs --help'.`); + process.exit(1); + } + await load()(args); +} + +main().catch((err) => { + console.error(`context-specs: ${err.message}`); + process.exit(1); +}); diff --git a/docs/1-context-engineering.md b/docs/1-context-engineering.md deleted file mode 100644 index 38121f6..0000000 --- a/docs/1-context-engineering.md +++ /dev/null @@ -1,92 +0,0 @@ -# Chapter 1 — Context engineering - -> The single biggest lever you have when you build with a coding agent is what -> goes **into** — and what stays **out of** — its context window. - -It helps to start with one observation, because everything else in this library -follows from it: an agent is only ever as good as the context it's reasoning -over — the actual set of tokens in its window at the moment it decides. Less the -model in the abstract than the material in front of it right now. - -Everything Context Specs does is in service of getting the right context into -that window at the right time, and keeping everything else out. - -## The window is the scarce resource - -A coding agent doesn't "know" your codebase. At any instant it knows exactly -what's in its context window: the conversation so far, the files it has opened, -the tool output it has seen. That window is finite, and it degrades in ways that -are easy to forget: - -- **Context decay.** In a long session, older messages lose influence. The agent - pays the most attention to what's recent and quietly discounts what came - before — so the careful plan you laid out twenty tool calls ago is no longer - really steering anything. -- **Context pollution.** Left to roam, an agent retrieves context on its own: - it greps, opens files, follows imports. A lot of what it pulls in is - irrelevant, and every irrelevant token crowds out a relevant one. -- **Compaction loss.** When the window fills, the session is summarized to make - room. You don't get to choose what's dropped. Critical details disappear and - the agent "forgets" — not because it's careless, but because the bytes are - gone. - -None of these are quirks of a particular model; they're consequences of working -inside a finite window. And they intensify as a task grows — just when holding a -coherent picture matters most. - -## Context engineering is choosing, not dumping - -The intuitive move is to load everything up front: here's the whole architecture, -every convention, all the relevant files, now go. But that runs straight into the -three problems above — it front-loads the pollution and makes compaction a -near-certainty before the agent has done much of anything. - -**Context engineering is the opposite discipline: the agent dynamically pulls -the context it needs, when it needs it, and the substrate it pulls from is the -filesystem.** Two techniques do most of the work: - -- **Externalize the durable stuff.** Anything that must survive a long session — - the plan, the conventions, the domain knowledge — lives in files on disk, not - in the conversation. Files don't decay and don't get compacted. The agent - re-reads them precisely when they're relevant. -- **Progressive disclosure.** You don't hand the agent dense detail; you hand it - a *pointer* — a short, high-level description with a path. The agent reads the - description first, decides whether it needs more, and only then opens the dense - source. The window stays small and focused, holding just what the current step - requires. - -The filesystem is what makes this possible. A file is a unit of context the -agent can choose to load or ignore. A directory of well-named files is a menu it -can navigate just-in-time. The skill of context engineering is largely the skill -of *organizing that menu* — so the right thing is one obvious read away, and the -wrong thing is never accidentally in the window at all. - -## Why this is the foundation - -This lens carries through the whole library — the same idea, applied at larger -and larger scope: - -- A **spec** is the plan, externalized so it can't decay or compact — and sliced - so the agent only ever loads the piece it's working on. *(Chapter 2.)* -- An **Expert** is curated domain knowledge, pulled on demand instead of - re-explained every session. *(Chapter 2.)* -- The **harness** runs a fresh context window per step, so no step ever inherits - another's pollution — and grows a long-term memory so hard-won context - survives across features. *(Chapters 3 and 4.)* -- The **human loop** is you, doing context engineering deliberately: deciding - what the agents should have known, and putting it where they'll find it. - *(Chapter 5.)* - -It's one lever, pulled at every scale. Each layer is answering the same question: -*at this scope, what is the right context, and how does it reach the window at the -right moment?* - ---- - -The first place that question gets a concrete answer is the oldest and most -familiar unit of work: a single feature. How do you give an agent exactly the -context it needs to build one thing well — and nothing it doesn't? - -That's **Spec-Driven Development**, and it's where we go next. - -→ [Chapter 2 — Spec-Driven Development](./2-spec-driven-development.md) diff --git a/docs/2-spec-driven-development.md b/docs/2-spec-driven-development.md deleted file mode 100644 index 6a0be42..0000000 --- a/docs/2-spec-driven-development.md +++ /dev/null @@ -1,185 +0,0 @@ -# Chapter 2 — Spec-Driven Development - -*Layer 1 — context engineering, applied to building one feature.* - -The smallest complete unit of work is a feature: take an idea, turn it into code -that works. Spec-Driven Development (SDD) is what happens when you treat that as -a context-engineering problem rather than a typing problem. - -The question from Chapter 1 — *what is the right context, and how does it reach -the window at the right moment?* — has a concrete answer here, and Context Specs -ships it as a handful of Agent Skills. **This layer needs nothing else in this -book.** You can `npx skills add` it today and use it by hand on any project, with -or without the harness in later chapters. - -## The problem SDD solves - -Give an agent "add search to the app" and it does something reasonable: it -greps, opens files to infer your conventions, makes a guess at how you test -things. On a small change that's fine. On a large feature it accumulates — -halfway through, the window is full of half-relevant code the agent retrieved on -its own, the original intent has decayed, and the next compaction is poised to -drop the part it actually needed. - -SDD fixes this by doing the thinking **outside** the context window first, and -persisting it as structured files the agent reads progressively. Two artifacts -carry that externalized thinking — and one mechanism carries the knowledge that -shapes them. - -## The Expert — define knowledge once, use it everywhere - -![Experts](../experts.png) - -Most of what makes a feature "right" isn't in the feature request — it's in your -project's accumulated knowledge: how this codebase is layered, how it tests new -routes, which patterns to reach for and which to avoid. Re-explaining that every -session is exactly the context decay Chapter 1 warned about. - -An **Expert** externalizes it. You create one from your own documentation — -framework docs, an internal library guide, architecture notes — with -[`/expert-sdd-creator`](../skills/sdd/expert-sdd-creator/SKILL.md), and -it generates a complete, progressively-disclosed knowledge module: - -``` -expert-{name}/ -├── SKILL.md # high-level pointer (Expert Mode + Signal Mode) -├── references/ # dense knowledge, read only when relevant -│ ├── {topic}.md -│ └── signal-workflow.md -└── scripts/ - └── run_signal.sh -``` - -The Expert is the menu from Chapter 1 made concrete: a short `SKILL.md` the agent -sees first, pointing into dense `references/` it loads only on demand. Define the -knowledge **once**, and it flows automatically through every phase below — you -never paste it into a prompt again. - -Two properties make Experts more than a docs folder: - -- **Composable, not hardcoded.** Multiple Experts activate for one feature. A - React Expert and a DynamoDB Expert both contribute when you build a full-stack - feature, each curating its slice of the context. Add or remove Experts without - touching any existing skill; organizations layer in private Experts for - internal libraries the same way. -- **Two modes — and Signal is half of what an Expert *is*.** Expert Mode curates - context *before* you write code. Signal Mode validates behavior *during* - implementation. The same module that knows your patterns also knows how to tell - whether the code honoring them actually works. - -## Specs — planning lifted out of the window - -When you run [`/spec-planning`](../skills/sdd/spec-planning/SKILL.md), -the planner researches the actual codebase (grounding the plan in reality, not -guesswork), pulls in any Experts whose triggers match, and writes the plan to -disk as two kinds of file: - -- a **mainspec** (`specs//mainspec.md`) — the complete end state, the - north star you work backward from; -- ordered **slices** (`specs//slices/`) — temporal chunks of intent, - each a coherent unit of work focused on *what* and *why*. - -This is the answer to context decay and compaction loss: the plan lives in files, -so it can't be forgotten or summarized away. After a compaction the agent simply -re-reads the spec and "remembers." - -What goes *into* a spec is itself disciplined context engineering — these are the -practices that make a spec something an agent can execute without guessing: - -- **BEFORE/AFTER with precise file paths** — exact current state vs. desired - state, so there's no ambiguity to resolve by greping. -- **Type contracts first** — define interfaces and schemas up front; sometimes an - entire slice is nothing but types. -- **DO/DON'T counterexamples** — one good example, one bad, with *why* the bad one - fails. -- **Narrative temporal flows** — Mermaid sequence diagrams and flowcharts showing - causality across layers. -- **Forward-looking requirements** — each slice notes what future slices will - need, preventing rework. - -## Temporal slicing — progressive disclosure, structurally - -Why slice by *intent* (what needs to happen) rather than by *component* -(frontend/backend)? Because features have a natural dependency order, and slicing -by intent preserves it. Each slice depends on prior slices and declares contracts -for future ones. - -That ordering does two jobs at once. For the implementer, it's progressive -disclosure made structural: the agent is fed **only the current slice**, never -the whole feature, so its window stays small and focused. For the orchestrator, -the dependencies form a DAG — and a DAG can be parallelized. Every mainspec -carries a **Slice Dependency Map** (a table plus a Mermaid graph), and -[`compute_tiers.py`](../skills/sdd/implement-mainspec/scripts/compute_tiers.py) -topologically sorts it to find which slices can run concurrently. - -## Validation — consensus before code - -A spec is a plan, and plans have blind spots. [`/spec-validate`](../skills/sdd/spec-validate/SKILL.md) -hardens it before a line of code is written, in three phases: - -1. **Multi-agent consensus.** Spawn 3+ Opus subagents that independently review - the spec. Agreement is signal — and it's graded, not binary: - - | Consensus | Confidence | Reading | - |-----------|------------|---------| - | 3/3 found | Very high | Real issue — fix it | - | 2/3 found | High | Likely real — should fix | - | 1/3 found | Medium | Could be a false positive — judgment call | - -2. **Expert review.** The relevant Experts validate the spec for domain-specific - anti-patterns, library misuse, and gaps a general reviewer would miss. -3. **Consolidation.** Findings are deduplicated and grouped by consensus; the - impactful ones are applied **in place** to the spec files. Validation sharpens - the plan; it doesn't bounce it back to start over. - -A single reviewer has blind spots; independent reviewers have *different* blind -spots, and consensus scoring turns that into a confidence signal instead of a -coin flip. - -## Implementation — a feedback loop, not a single pass - -![Signal](../signal.png) - -[`/implement-slice`](../skills/sdd/implement-slice/SKILL.md) implements -one slice as a tight loop: implement the code, then run the slice's **Signal** to -check it behaves, iterating until the signal validates. Signal is the runtime -counterpart to the Expert's up-front curation — where the Expert shapes context -*before*, Signal feeds focused feedback *during*. Unit tests are the default -signal, but an Expert can define richer ones: hit an endpoint and check the -response, run browser automation and screenshot it, deploy to a lower -environment and read the logs. - -[`/implement-mainspec`](../skills/sdd/implement-mainspec/SKILL.md) -orchestrates the whole feature and auto-detects how to run it: - -- **Sequential** (≤3 slices) — one slice at a time, committed directly to the - branch. Simple and linear. -- **Parallel** (>3 slices) — the DAG from `compute_tiers.py` drives tiered - execution: Tier 0 (foundation) first, then later tiers fan out across git - worktrees with a focused `slice-implementer` subagent per slice (up to 7 - concurrent), gating each tier before the next. - -Either way, the feedback loop runs **per slice**, not just at the end — the agent -knows whether it's on track as it goes, not after. - -## What you have at the end of Layer 1 - -A repeatable way to take one feature from idea to working code while keeping the -agent's window full of exactly the right context and nothing else: knowledge -defined once and pulled on demand, planning externalized and disclosed -progressively, validation by consensus, implementation gated by signal. All of -it framework-agnostic, all of it usable on its own. - ---- - -But notice what you're still doing: you invoke `/spec-planning`, then you invoke -`/spec-validate`, then you invoke `/implement-mainspec`. You're driving every -step, one feature at a time, sitting at the keyboard the whole way through. - -The loop is good. What if the *project itself* ran it for you — picked up a -feature request, planned it, validated it, implemented it, and handed you back a -finished pull request while you did something else? - -That's the leap from a skill library to a **harness**. - -→ [Chapter 3 — The agent harness](./3-the-agent-harness.md) diff --git a/docs/3-the-agent-harness.md b/docs/3-the-agent-harness.md deleted file mode 100644 index d6cfd38..0000000 --- a/docs/3-the-agent-harness.md +++ /dev/null @@ -1,199 +0,0 @@ -# Chapter 3 — The agent harness - -*Layer 2 — the project runs the SDD loop for you.* - -Here is the whole promise of this chapter in one sequence: - -> You describe a feature to [`/intent`](../skills/human-loop/intent/SKILL.md) and -> confirm it. Then you walk away. The project plans the feature, validates the -> plan, implements it, runs its checks, opens a pull request, answers the -> reviewer's comments — and hands you back a PR that's ready to merge. You come -> back to finished work you never typed. - -That's the harness. It takes the Layer 1 loop — `/spec-planning` → -`/spec-validate` → `/implement-mainspec` — that you used to drive by hand, and -**runs it autonomously**, end to end, while you do something else. Everything in -this chapter is in service of that one capability, and of the second question it -immediately raises: *why would you trust a machine to do this unattended?* - -## The autonomous loop - -A feature flows through the harness as a sequence of states. Each state is -advanced by a skill; the next skill fires when the previous one's output appears -on disk: - -```mermaid -flowchart TD - Intent["/intent (you, once)\nPRD + runnable definition of done"] --> Claim[Harness claims the work] - Claim --> Plan["/spec-planning"] - Plan --> Validate["/spec-validate"] - Validate --> Implement["/implement-mainspec\n(signal loop per slice)"] - Implement --> Checks["local-checks.sh\n(lint, types, fast tests)"] - Checks --> PR[Open pull request] - PR --> Review[Reviewer posts findings] - Review -->|findings| Respond["/address-feedback"] - Respond --> Review - Review -->|no findings left| Ready[Ready for your review] - Ready --> You([You merge]) -``` - -The only human touch in that diagram is the first box and the last. In between, -the project builds the feature itself. The reviewer/responder exchange is bounded -so it can't argue forever, and if any step genuinely can't make progress, the -harness stops and hands you the problem rather than faking success — more on both -below. - -### `/intent` — the one place a human starts - -The loop begins with the single human-attentive skill in the chain. -[`/intent`](../skills/human-loop/intent/SKILL.md) turns an open-ended idea into two -coupled artifacts: - -- `prds//prd.md` — the prose: **why** this exists and **what** "done" - means; -- `prds//run-prd-test.sh` — the executable: **how you'll know** it's - done. It exits 0 when the feature is built. - -The runner is the load-bearing idea. Prose drifts and "done" becomes an -argument; an exit code doesn't. Before `/intent` commits anything, it runs the -script against today's unbuilt code and confirms it fails **for the right -reason** — because the behavior is genuinely absent, not because of a typo or a -missing dependency. That's the empirical proof that the test actually -corresponds to the intent. (`/intent` is also where the human loop in Chapter 5 -plugs in — but you can use it perfectly well on its own right now.) - -Once confirmed, the PRD lands on a branch and the harness takes over. - -## Why you can trust it: artifacts are the state - -The reason it's safe to leave this running is almost boring, and that's the -point. **The harness keeps no hidden state.** There is no daemon holding "I'm -currently on step 3" in memory, no queue, no coordination service. The complete -state of every feature is observable from two things: the files on disk and the -branches in git. - -A small bash script — the **dispatcher** -([`poll-and-dispatch.sh`](../skills/harness/harness-init/assets/poll-and-dispatch.sh)) -— wakes up periodically, reads that state fresh, and decides the single next step -for each active feature. Its core is an `if/elif` chain: *planning output absent? -run planning. Present but validation absent? run validate.* The chain **is** the -state machine; the artifacts on disk **are** the state. - -Three consequences fall out of that design, and together they're why the machine -is trustworthy: - -- **Crash recovery is free.** Kill the dispatcher mid-step — `kill -9`, a dead - laptop, a closed lid. The next tick re-reads disk, sees the half-finished - state, and resumes. Nothing to clean up, because nothing was ever held in - memory to lose. -- **Every step runs in a fresh context window.** The dispatcher doesn't *do* the - work; it shells out to a brand-new `claude -p` process per skill. Each skill - reads what it needs from disk, does its job, writes its output, and exits. No - step inherits another's polluted window — the Chapter 1 problem, solved by - construction. (This is also why the loop can run for hours without the context - rot a single long session would accumulate.) -- **The dispatcher itself contains no LLM.** The decision of *what runs next* is - pure, deterministic bash. The intelligence is in the skills; the routing is - mechanical. That's what makes "what will it do next?" answerable just by - looking at disk — no model in the loop to second-guess. - -These properties have names and precise statements. When you want the rigorous -version — what holds under concurrency, what each layer guarantees, how recovery -composes — read the **[design invariants](./invariants.md)**. This chapter gives -you the intuition; that document gives you the proof. - -## Verification the agent cannot talk past - -Autonomy is only safe if "done" can't be faked. So the harness leans on checks -that run **regardless of what the agent decides**, fastest to slowest: - -1. **Pre-commit hooks** — linters, formatters, type checks on changed files. -2. **Slice signals** — each slice's Signal section, run during implementation. -3. **`local-checks.sh`** — the cheapest gate before a PR: lint, typecheck, the - fast unit suite, skip-detection, and any custom project lints. It proves - **correctness, not coverage** — it blocks on real defects and merely warns on - style. -4. **The PRD runner** — `./prds//run-prd-test.sh` must exit 0. This is - the definition of done, and because it can mix deterministic shell checks with - an LLM-as-judge, "runnable" doesn't force everything into unit-test shape. - -The agent doesn't get to decide whether these run — they run, and the git/CI -layer enforces them independently. Crucially, **silencing a check counts as -bypassing it**: an agent may never add a suppression directive, weaken a config, -or skip a test to get to green. A check it can't pass honestly is one it lets -fail. - -## When it can't make progress: STUCK - -Sometimes a feature genuinely can't get through a step — a plan is subtly wrong, -a check won't pass honestly. The harness doesn't loop forever, and it won't fake -success. -Every step has a bounded retry budget; when a step hits its cap, the dispatcher -declares the feature **STUCK**, posts a diagnostic to the PR, and halts that -feature until you step in. - -What it hands you is deliberate. Not just "it failed" — a session log of every -`claude -p` invocation across the chain (so you can open any trace and see what -the agent saw), a tail of the failing output, and a **diagnosis-first -checklist**. The checklist's first item isn't "fix the code." It's *identify -which piece of context misled the agent* — a stale Expert note, a thin spec, a -PRD that left something out — and correct that first. Because if the context that -caused the failure stays wrong, the same class of failure comes back the next -time a feature touches that area. - -STUCK is the harness being honest about the boundary of what it can do alone — -and routing the rest to the one place judgment lives. - -## The branch namespace is the work queue - -There's no separate database of "what work exists." The branches *are* the -registry: - -``` -prd// ← /intent output. The waiting queue. An atomic rename - to feature/ is how the harness claims it. -feature/ ← active implementation lane. -main ← humans merge here. Never pushed to directly. -learn/ ← auto-generated memory update (Chapter 4). -``` - -Claiming a feature is a single atomic git operation — renaming `prd//` -to `feature/`. If two harnesses ever race for the same work, whichever pushes -the rename first wins and the other simply moves on. No lock service, no -coordination daemon; git refs are the lock. By default each developer's harness -watches only their own `prd//*` queue, so your harness is your personal -assistant, not a shared teammate. - -## It runs wherever you do - -The skills, the artifacts, and the branch conventions are portable. The only -thing that's environment-specific is the **trigger** — what wakes the dispatcher. -That's a one-line choice, and changing it changes nothing else: - -``` -/loop → CronCreate → Agent SDK → GitHub Actions / server -single dev, single dev, programmatic team-scale, runs without -visible background glue anyone's laptop on -``` - -The recommended start is the simplest thing that exists: open a Claude Code -session and run `/loop 5m /poll-and-dispatch`. That's it — the outer loop is a -session you already have open. When a team outgrows local laptops, they move the -*trigger* to a server and keep everything else. It's an upgrade path, not a -rewrite. (Setting all of this up is itself a guided skill — -[`/harness-init`](../skills/harness/harness-init/SKILL.md) — which stands -up the dispatcher, the config, the verification gate, and the worktree -provisioning, explaining each piece as it goes.) - ---- - -So the harness builds features on its own, and refuses to fake the ones it can't. -That alone would be worth having. But there's a second thing it does, and it's -the one that makes the project genuinely *yours* over time. - -Every feature that merges teaches the project something — a new pattern, a -constraint nobody had written down, a rule worth enforcing forever. A harness -that only *built* features would relearn those lessons every time. This one -doesn't. It remembers. - -→ [Chapter 4 — Continuous improvement](./4-continuous-improvement.md) diff --git a/docs/4-continuous-improvement.md b/docs/4-continuous-improvement.md deleted file mode 100644 index 45f282c..0000000 --- a/docs/4-continuous-improvement.md +++ /dev/null @@ -1,137 +0,0 @@ -# Chapter 4 — Continuous improvement - -*Layer 2 — the harness gets better every merge.* - -A harness that only built features would be a fast worker with no memory. Each -feature would start from the same blank slate; the same hard-won lesson — *this -codebase puts query-param pages behind SSR*, *never import `service` from -`repo`* — would have to be rediscovered, or re-explained by you, every single -time. That's the context decay of Chapter 1, but across features instead of -within a session. - -The harness closes that gap. **When code lands on `main`, the project updates its -own long-term memory.** The next feature starts knowing what the last one taught. - -```mermaid -flowchart LR - Merge([Merge to main]) --> Learn["/learn\nreads the merged diff"] - Learn --> Vote{Consensus:\nanything worth\nremembering?} - Vote -->|no — common| Noop[No-op. Most merges\nteach nothing durable.] - Vote -->|yes| Route[Route each fact to\nexactly one destination] - Route --> PR["learn/ PR\nyou review and merge"] -``` - -The skill that does this is [`/learn`](../skills/harness/learn/SKILL.md), -and a few principles make it trustworthy rather than a source of drift. - -## Ground truth only — one write path - -The most important rule: **memory reflects what is committed to `main`, never -what is planned.** PRDs and specs describe intent; code is reality. So `/learn` -only ever writes from a *merged diff* — never from a branch in flight, never from -a feature that might still be abandoned. - -That gives the whole system a single, auditable **write path** for memory: a -human merges a feature; `/learn` observes the result; the change to memory lands -on its own `learn/` pull request that a human reviews and merges. Memory is -never auto-written by a skill mid-task, never updated speculatively. There is -exactly one door, and it opens only on ground truth. - -This is also how a STUCK resolution (Chapter 3) compounds: when you correct the -misleading context on a feature branch and merge it, that correction is *in the -diff* `/learn` reads. The fix you made by hand becomes permanent project memory, -automatically. - -## Two memory shapes, opposite costs - -The harness keeps memory in two places, and the difference between them is the -whole discipline — it isn't an implementation detail: - -- **The Expert** *(from Chapter 2)* is **lazy memory** — pulled on demand. It - enters a context window only when a skill deliberately consults it. You pay - tokens for it *only when it earns them*. This is the default home for real - knowledge: architecture, patterns, how things get verified here. -- **`AGENTS.md`** is **eager memory** — loaded automatically into every agent - that touches the folder, before anyone knows whether it's relevant. You pay - for every line on *every* session. So the bar for putting something here is - much higher. - -Because eager memory is a standing tax and lazy memory is paid only when -consulted, the two compose as **map and territory**: `AGENTS.md` is a short table -of contents that *points into* the Expert; it never duplicates the dense -knowledge. A monolithic `AGENTS.md` rots, crowds out the task, and turns -"everything important" into "nothing important." Keep it a map; keep the density -in the Expert. (Using `AGENTS.md` — the vendor-neutral, open file every harness -reads — rather than a tool-specific one is also what keeps your project's memory -*yours*, portable across whatever agent you point at it.) - -## The four destinations — where a learned fact goes - -`/learn` is disciplined about *where* a remembered fact lands. Every fact worth -keeping routes to **exactly one** place: - -| Destination | Use when | What it costs | -|---|---|---| -| **A lint** (`scripts/lints/*`) | The rule is mechanically checkable — pass/fail needs no judgment | Enforced on every PR forever; the error message doubles as a fix prompt | -| **Eager prose** (`AGENTS.md`) | It must be known *before* an agent would think to consult the Expert, and clears a strict five-part bar | Paid every session — the high bar | -| **Lazy prose** (an Expert shard) | It's useful when *deliberately reasoning* about an area | Paid only when consulted — the usual home | -| **Nowhere** | It's inferable from the code, taste-only, or transient | — | - -Most facts go to the last two. A 0/3 consensus — "this merge teaches nothing -durable" — is a **common and correct** outcome, not a failure: vuln fixes, -refactors that don't change shape, and routine bug fixes usually change no memory -at all. The system improves by accumulating signal, not noise, and *preferring -nothing over noise* is what keeps the Expert worth reading. - -## Lints — the memory an agent cannot ship past - -The top row of that table deserves its own spotlight, because it's the -highest-value thing the harness can learn. A **lint** is just a short script: -glob some files, check a fact, exit non-zero with a remediation message. It beats -prose for two reasons: - -1. **The error message is a prompt.** It doesn't say "violation" — it says *how - to fix it*, feeding remediation straight into the agent's window so it - self-corrects. A prose rule is advice the agent *might* follow; a lint is a - rule it *cannot ship past*, plus the fix. -2. **It scales the way agents scale.** Once written, it applies to every file and - every future PR at once. - -So the most durable thing your project owns is the set of custom lints it grows -over time — layer-dependency direction, no raw SQL interpolation, -structured-logging-only, naming rules, file-size caps. Promotion is -conservative: a discovered invariant lives as prose first and is promoted to a -lint only on recurrence, and a drafted lint **must pass against the just-merged -code** before it's wired in (the inverse of `/intent`'s right-reason check — a -lint that reddens the merge it was born from is wrong). - -## Consensus gates the write - -Before anything changes, `/learn` spawns a few cheap reviewers that read the -merged diff against current memory and vote, per surface, on whether anything -*must* change. Only changes that clear the threshold survive. It's the same -consensus pattern `/spec-validate` uses in Chapter 2, pointed at a different -question — and it's cheap insurance against corrupting long-term memory on every -commit. Everything that does survive lands on a reviewable, revertible PR. You -stay the final gate. - -## What you have at the end of Layer 2 - -A project that not only builds features unattended, but gets measurably easier to -build the next feature in — because every merge that taught something durable put -that lesson where the next agent will find it, at the right cost, behind a human -merge. The harness compounds. - ---- - -Step back and notice what's happened to your job. The machine plans. It -implements. It verifies. It remembers. The typing — the part that used to be most -of the work — is gone. - -That doesn't make you redundant. It makes the *remaining* part the whole job, and -the remaining part is the one a machine structurally cannot do: deciding what's -worth building, judging whether what got built is right, and improving the -context so the harness keeps getting better. That's not a side activity anymore. -It's the loop you live in. - -→ [Chapter 5 — The human loop](./5-the-human-loop.md) diff --git a/docs/5-the-human-loop.md b/docs/5-the-human-loop.md deleted file mode 100644 index bc1e217..0000000 --- a/docs/5-the-human-loop.md +++ /dev/null @@ -1,185 +0,0 @@ -# Chapter 5 — The human loop - -*Layer 3 — freed from typing, you improve the harness.* - -Chapters 3 and 4 built a machine that builds and remembers. This chapter is about -what you do now that it does. The honest answer is: **the most important part** — -the part the machine structurally cannot do. - -The governing line comes from Andrej Karpathy: *"you can outsource your thinking -but you can't outsource your understanding."* It draws a sharp boundary: - -- **Verifiable work → outsource it freely.** Syntax, API recall, implementation - mechanics. The model is superhuman here. Spending your attention on it is - waste. -- **Unverifiable work → you must own it.** Whether this is the right thing to - build, whether the abstraction is sound, whether the edge cases are handled for - good reasons, whether the UX feels right. Errors here are subtle and don't - announce themselves — exactly where your understanding has to be load-bearing. - -The harness took the first category. The human loop is you, deliberately working -the second. It has three phases, and they form a closed cycle: - -``` - Understanding ──▶ Intent ──▶ [ the harness builds ] ──▶ Evaluate - build your model express Chapters 3 & 4 walk the result, - of the problem what to run it, judge it, - space build improve the context - ▲ │ - └──────────── evaluating deepens understanding ──────────┘ -``` - -## The two loops interlock - -The human loop and the machine loop aren't parallel tracks — they feed each -other, and that coupling is the design: - -> **Your loop's output is the harness's input. The harness's output is your -> loop's input.** - -You produce intent (a PRD); the harness consumes it and produces a pull request; -you consume that PR by evaluating it; what you learn sharpens the next intent. -Neither loop is complete without the other. The machine exists to serve your -loop — and naming your loop explicitly is what keeps *your* job legible as the -machine takes over more of the typing. - -Each phase is a built skill. - -## Understanding — `/wiki-init` - -Before you can express good intent, you need a real model of the problem space: -the domain, the prior art, the constraints, the trade-offs. The Understanding -phase builds *your* model, not the agent's. - -[`/wiki-init`](../skills/human-loop/wiki-init/SKILL.md) stands up a -standalone, LLM-maintained knowledge base in the shape of **Karpathy's "LLM -Wiki"**: rather than retrieving and re-synthesizing on every question, you -synthesize **once, at ingest time**, into durable, cross-linked pages that -compound as sources accumulate. You curate sources and ask questions; the LLM -does the bookkeeping humans abandon wikis over — updating cross-references, -reconciling pages, keeping the map current. - -This wiki is deliberately **not** a project artifact, and that's the bright line -worth holding: - -- The **wiki** is cross-project **domain and architecture** knowledge, written to - be read **by you**. It lives in its own repo, outside any codebase. -- The **Expert** (Chapters 2 & 4) is per-project **code** memory, written to be - read **by agents**. - -The payoff is direct: a richer model of the domain means a sharper `/intent`. You -arrive at the next phase already understanding the concepts, the constraints, and -the trade-offs — so the feature you ask for is the right one. - -## Intent — `/intent` - -This is the front bookend, and you met it in Chapter 3 as the harness's entry -point. From the human loop's side, [`/intent`](../skills/human-loop/intent/SKILL.md) -is where understanding becomes a buildable thing. Its discipline is worth -restating because it's where your thinking does its work: - -- **Elicit outcomes, not solutions.** People arrive describing a solution ("add a - `/api/search` endpoint"); the job is to surface the *need* underneath ("readers - can't find a post by title") and the observable outcome that would satisfy it. -- **"How would we know that's true?"** is the throughline. Asked of every desired - outcome, it converts a wish into both a sharp prose criterion *and* a concrete - check — the two coupled artifacts (`prd.md` + `run-prd-test.sh`) born together. -- **You don't write the runner; the Expert drafts it, you review.** This keeps - verification grounded in how the project actually works, not your instinct in - the moment. - -`/intent` is a *coordinator, not a knowledge holder* — the domain reasoning comes -from the Expert. Your contribution is the understanding from the previous phase -and the judgment about what's worth building. Confirm the PRD, and the machine -takes it from there. - -## Evaluate — `/evaluate-pr` and `/evaluate-sessions` - -The back bookend, and the mirror of `/intent`. The harness hands you a finished -PR; evaluation is where you do the unverifiable work the machine couldn't. There -are two skills, because there are two things to evaluate. - -### `/evaluate-pr` — evaluate *what was built* - -[`/evaluate-pr`](../skills/human-loop/evaluate-pr/SKILL.md) produces two -outcomes. The tangible one: merge, fix-and-push, or close. The intangible one — -**the one that matters more** — is *you* understanding the change deeply enough -to defend every scenario and design decision in it. - -That intangible output is the literal mechanism that closes the loop back to -Understanding and sharpens the next Intent. So the skill is a teacher and a taste -partner, not a second linter: the bot reviewer already caught the mechanical -defects, so it *ingests* those findings and sets them aside, then spends your -attention on what a bot structurally can't judge — is this simpler than it could -be, is the abstraction sound, does the UX feel right? It runs the system with -you, walks the definition-of-done scenarios live, and probes Socratically rather -than lecturing. The understanding gate is soft but real: it ends on "do you feel -you understand this change?" and skipping the walk-through is an explicit -opt-out, never a silent rubber-stamp. - -If the walk surfaces something to change, **you fix it here and push** — you -never hand work back to the loop. You are the last mile. - -### `/evaluate-sessions` — evaluate *how it was built* - -[`/evaluate-sessions`](../skills/human-loop/evaluate-sessions/SKILL.md) is -where the human loop reaches back and improves the harness itself. The harness -posts the full `claude -p` build trail on every PR — the sessions the agents ran. -This skill reads that trail *with you* to find where the project's context served -the agents and where it failed them: where an agent re-derived something the -Expert should have told it, followed a stale `AGENTS.md` pointer, or guessed -because a spec was thin. - -What you find turns into two durable outcomes — the **flywheel**: - -> *Observe a trace → capture it as an eval → fix the context → it persists as a -> regression test.* - -- **Evals** — when a session shows a skill behaved well or badly *given the - context it had*, freeze that as a runnable check under `evals/`. An eval is a - regression test over the harness's **own skills and context** (the analog of - testing a prompt), distinct from the PRD runner, which tests the product. -- **Context fixes** — when a piece of context misled an agent, fix the Expert - shard, the `AGENTS.md` pointer, or the skill. - -This is the moment the framing of the whole book becomes literal: every PR the -harness builds is a graded trial of your project's context, and every eval you -capture makes the project a little better at building itself next time. You're -not auditing one PR — you're **tuning the harness.** - -## Memory still has one write path - -Notice what evaluation does *not* do: it doesn't write memory directly. Whatever -you learn here isn't ground truth yet, because the change isn't merged yet. So -the insight either becomes a pushed fix that rides into `main` — where `/learn` -(Chapter 4) picks it up as ground truth to extend, not second-guess — or it lives -in your head and sharpens the next Intent. Evals and context fixes land on a -branch and reach memory the same way: through a merge. The single write path of -Chapter 4 holds. You may now *seed* memory deliberately; you still never bypass -the door. - -## The three places you steer - -Across the whole system, you touch the machine at exactly three points — and -every one of them is a phase of this loop: - -| You... | Human-loop phase | What it does to the machine | -|---|---|---| -| Confirm a PRD | end of **Intent** | starts the build | -| Evaluate and merge a PR | **Evaluate** | ends the build; triggers `/learn` | -| Unstick a STUCK feature | a forced detour into **Evaluate** | corrects the context, then merges | - -Three touchpoints. Everything between them is the machine. Everything *at* them -is judgment — yours. - ---- - -So here's where we've arrived. The project plans, implements, verifies, and -remembers on its own. You spend your time understanding the problem, expressing -intent, and evaluating outcomes — and when you evaluate, you don't just approve -work, you improve the thing that produced it. - -That's not a new workflow bolted onto coding. It's a different relationship to -your own project. Worth saying plainly, because it's the whole point. - -→ [Chapter 6 — The mindset shift](./6-the-mindset-shift.md) diff --git a/docs/6-the-mindset-shift.md b/docs/6-the-mindset-shift.md deleted file mode 100644 index 30fb652..0000000 --- a/docs/6-the-mindset-shift.md +++ /dev/null @@ -1,85 +0,0 @@ -# Chapter 6 — The mindset shift - -*The payoff.* - -Read the first five chapters and a single line falls out of them: - -> **Your project is not a codebase you type into. It's a harness you tune.** - -Read it literally: it describes what the previous chapters actually built. The -harness plans features, implements them, -verifies them, and remembers what it learns. The code is an *output* of the -system now — something the harness produces — not the thing you spend your hours -authoring. - -The shift this asks of you is the hard part, because it's a shift in where you -put your attention. - -## From syntax to context - -The old craft was syntax: knowing the API, writing the loop, getting the types to -line up, remembering how this codebase does things. That craft is exactly the -**verifiable** work from Chapter 5 — the work the model is superhuman at. Keep -spending your attention there and you're racing a machine at the one game it -always wins. - -The new craft is **context engineering** — the lever from Chapter 1, now wielded -deliberately and at every level: - -- You shape the context the harness reasons over (the Expert, `AGENTS.md`, the - specs). -- You decide what becomes a lint the agent can't ship past, and what stays - flexible prose. -- You read the build trail to find where the context failed an agent, and you fix - it so the next feature benefits. - -When a feature comes out wrong, the question is no longer "let me go fix that -code." It's "what did the agent not know, and where should that knowledge live so -it never bites us again?" Fixing the code fixes one feature. Fixing the context -fixes the *class*. - -## The three things that stay yours - -Strip away the typing and what's left is the work that was always the most -valuable, now unobscured. Three things, and they are not going anywhere: - -1. **Deep understanding of the problem space.** The harness can build anything you - can specify; it cannot tell you what's worth specifying. That comes from your - model of the domain — which is why the Understanding phase exists, and why it - compounds (Chapter 5). -2. **Theorizing and expressing intent.** Deciding what to build, and pinning - "done" to something executable. This is judgment under uncertainty — the part - no exit code can hand you. -3. **Evaluating outcomes and improving the context.** Judging whether what got - built is *right* in the ways a machine can't verify — and turning that judgment - into better context, so every future feature inherits it. - -Understand the problem. Express the intent. Evaluate the result and improve the -harness. That's the loop. The machine does the rest. - -## Why it compounds - -The reason this is worth the shift, and not just a rearrangement of chores: **the -system gets better the more you use it, and it gets better in a direction you -choose.** - -Every merge can teach the Expert something. Every STUCK you resolve corrects a -piece of context for good. Every eval you capture freezes a behavior so a future -change can't silently regress it. A codebase you type into is a constant cost — -it decays, it needs maintenance, every feature starts roughly where the last one -did. A harness you tune is an *appreciating asset* — each feature leaves the -project a little more capable of building the next one. - -That's the whole bet of Context Specs, stated plainly: - -> Stop spending yourself on the work a model does better than you. Spend yourself -> on understanding, intent, and context — and let the project you've turned into -> a harness compound everything you put in. - ---- - -That's the story. To go deeper on any layer, the chapters point into the actual -skills under [`skills/`](../skills/); to understand *why* the -harness is safe to leave running, read the **[design invariants](./invariants.md)**. - -← [Back to the start](./README.md) diff --git a/docs/README.md b/docs/README.md index 1364be5..0e0f388 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,71 +1,69 @@ -# Context Specs — the full story +# Context Specs documentation -This is the long-form documentation for Context Specs. The top-level -[README](../README.md) is the elevator pitch; this is the book. +Context Specs is [harness engineering](./concepts/harness-engineering.md) applied to +building software: you express intent, and a deterministic harness drives a coding +model all the way to a pull request that is either **ready to merge** or **STUCK +with a diagnosis**. Over time you improve the *system* — its context, its memory, +its checks — so more features come back ready and fewer come back stuck. -It's written to be read **in order**. Each chapter ends where the next one -begins — by the last page you should see why a coding project, run well with -agents, stops looking like a codebase you type into and starts looking like a -**harness you tune**. +New here? The top-level [README](../README.md) is the elevator pitch. For the full +narrative, read the [overview](./overview.md). Otherwise, jump straight to a concept +or a task below. -## The through-line +## Overview -One idea runs under everything here: **the right context at the right time.** -What enters an agent's context window is the biggest lever you have. Context -Specs is that lever, pulled at three levels — each one building on the one -before it: +- **[How Context Specs works](./overview.md)** — the narrative tour that connects + every concept in one reading. -``` -Context engineering the idea: an agent choosing what enters its window - │ - ▼ -Spec-Driven Development Layer 1 — the idea, applied to building one feature - │ - ▼ -The agent harness Layer 2 — the project runs that loop for you, and - │ gets better every merge - ▼ -The human loop Layer 3 — freed from typing, you improve the harness - │ - ▼ -A mindset shift your project has become a harness; your job is context -``` +## Core concepts -Each layer is usable without the ones above it. Spec-Driven Development needs no -harness. The harness needs no human-loop discipline to run. The value compounds -as you climb — but you can stop on any rung. +Standalone explanations of each idea. Read in any order; they cross-link. -## The chapters +- **[Harness engineering](./concepts/harness-engineering.md)** — what a harness is + (Agent = Model + Harness), and the discipline the whole system rests on. +- **[Context engineering](./concepts/context-engineering.md)** — the core lever: + the right context in the window at the right time. +- **[The two-tier architecture](./concepts/two-tier-architecture.md)** — one harness + repo driving N environments; the Software 3.0 dividing line. +- **[The dispatcher](./concepts/the-dispatcher.md)** — the deterministic engine; + artifacts as state; a fresh window per step. +- **[Spec-Driven Development](./concepts/spec-driven-development.md)** — short-term + memory: context engineering for one feature. +- **[Long-term memory](./concepts/long-term-memory.md)** — the Expert, `AGENTS.md`, + and lints; how the harness remembers. +- **[The output contract](./concepts/output-contract.md)** — the runnable definition + of done; ready-to-merge vs. STUCK. +- **[Continuous improvement](./concepts/continuous-improvement.md)** — operating at + the system level; the ready-to-merge ratio; the flywheel. +- **[The human loop](./concepts/the-human-loop.md)** — understand → intent → + evaluate; the part only you can do. -1. **[Context engineering](./1-context-engineering.md)** — what it actually is, - and why the context window is the scarce resource everything else is fighting - over. -2. **[Spec-Driven Development](./2-spec-driven-development.md)** *(Layer 1)* — how - Context Specs applies context engineering to a single feature: experts, specs, - temporal slicing, signal, consensus validation. The foundation, usable on its - own. -3. **[The agent harness](./3-the-agent-harness.md)** *(Layer 2)* — file a PRD, - walk away, come back to a finished PR. The autonomous loop, and why you can - trust a machine to run it unattended. -4. **[Continuous improvement](./4-continuous-improvement.md)** *(Layer 2)* — how - the harness gets better every merge: long-term memory, the four destinations - for a learned fact, and lints the agent cannot ship past. -5. **[The human loop](./5-the-human-loop.md)** *(Layer 3)* — once the machine does - the typing, what's left is the part only you can do: Understanding → Intent → - Evaluate. -6. **[The mindset shift](./6-the-mindset-shift.md)** — the payoff. Your project is - a harness now. Here's how the way you work changes. +## How-to guides -### Reference +Task-oriented steps for getting things done. -- **[Design invariants](./invariants.md)** — the properties the harness holds no - matter what crashes, races, or restarts. Read this when you want to understand - *why* the machine is safe to leave running. (Optional; you can also hand it to - an agent to give it a deeper model of the harness.) +- **[Create a harness](./how-to/create-a-harness.md)** — `init` your one harness + repo. +- **[Add an environment](./how-to/add-an-environment.md)** — register a project. +- **[Initialize a project](./how-to/initialize-a-project.md)** — generate the + Software 3.0 half with `/env-init`. +- **[Express intent](./how-to/express-intent.md)** — turn an idea into a PRD + + runnable definition of done. +- **[Run the harness](./how-to/run-the-harness.md)** — start, observe, and stop the + loops. +- **[Unstick a feature](./how-to/unstick-a-feature.md)** — resolve a STUCK by fixing + the context first. +- **[Evaluate a PR](./how-to/evaluate-a-pr.md)** — evaluate *what* was built. +- **[Improve from build traces](./how-to/improve-from-traces.md)** — evaluate *how* + it was built; capture evals and context fixes. -## Where the code lives +## Reference -Everything described here ships as Agent Skills under -[`skills/`](../skills/). The chapters point at the specific -skill, script, or reference file that implements each idea, so you can read the -story and then go read the source. +- **[The `context-specs` CLI](./reference/cli.md)** — every command, flag, and exit + code. +- **[The pieces](./reference/skills.md)** — the full catalog of CLI commands, + dispatchers, and skills. +- **[State and branches](./reference/state-and-branches.md)** — the branch namespace + and on-disk layout. +- **[Design invariants](./reference/invariants.md)** — the properties that make the + harness safe to leave running. diff --git a/docs/concepts/context-engineering.md b/docs/concepts/context-engineering.md new file mode 100644 index 0000000..f190fae --- /dev/null +++ b/docs/concepts/context-engineering.md @@ -0,0 +1,67 @@ +# Context engineering + +Context engineering is the practice of controlling what goes **into** — and what +stays **out of** — a coding agent's context window. It is the single biggest +lever you have when you build software with agents, and most other concepts in +Context Specs are an application of it at a larger scope. + +## Why the window is the scarce resource + +A coding agent does not "know" your codebase. At any instant it knows exactly +what is in its context window: the conversation so far, the files it has opened, +the tool output it has seen. That window is finite, and it degrades in three ways +that are easy to forget: + +- **Context decay** — in a long session, older messages lose influence. The agent + attends most to what is recent and quietly discounts what came before, so a plan + laid out twenty tool calls ago is no longer really steering the work. +- **Context pollution** — left to roam, an agent retrieves context on its own: it + greps, opens files, follows imports. Much of what it pulls in is irrelevant, and + every irrelevant token crowds out a relevant one. +- **Compaction loss** — when the window fills, the session is summarized to make + room, and you do not get to choose what is dropped. Critical details disappear + and the agent "forgets" — not from carelessness, but because the bytes are gone. + +None of these are quirks of a particular model. They are consequences of working +inside a finite window, and they intensify as a task grows — exactly when holding +a coherent picture matters most. + +## The discipline: choose, don't dump + +The intuitive move is to load everything up front — the whole architecture, every +convention, all the relevant files — and then start. That front-loads pollution +and makes compaction near-certain before the agent has done anything. + +Context engineering is the opposite discipline: **the agent dynamically pulls the +context it needs, when it needs it, and the substrate it pulls from is the +filesystem.** Two techniques do most of the work: + +- **Externalize the durable stuff.** Anything that must survive a long session — + the plan, the conventions, the domain knowledge — lives in files on disk, not in + the conversation. Files do not decay and are not compacted; the agent re-reads + them precisely when they are relevant. +- **Progressive disclosure.** You hand the agent a *pointer* — a short, high-level + description with a path — not dense detail. The agent reads the description + first, decides whether it needs more, and only then opens the dense source. The + window stays small and focused, holding just what the current step requires. + +The filesystem is what makes this possible. A file is a unit of context the agent +can choose to load or ignore; a directory of well-named files is a menu it can +navigate just-in-time. Much of the skill of context engineering is the skill of +*organizing that menu* — so the right thing is one obvious read away and the wrong +thing is never accidentally in the window at all. + +## Where it shows up in Context Specs + +The same lever is pulled at every scale of the system: + +| Scope | Application | +|---|---| +| One feature | A [spec](./spec-driven-development.md) externalizes the plan so it cannot decay or compact, and slices it so the agent only loads the piece it is working on. | +| Project knowledge | An [Expert](./long-term-memory.md) is curated knowledge pulled on demand instead of re-explained every session. | +| The whole run | The [dispatcher](./the-dispatcher.md) runs a fresh window per step, so no step inherits another's pollution. | +| Across features | [Long-term memory](./long-term-memory.md) keeps hard-won context alive so future features start from it. | +| You | The [human loop](./the-human-loop.md) is deliberate context engineering — deciding what the agents should have known and putting it where they will find it. | + +Every layer answers the same question at its own scope: *what is the right context +here, and how does it reach the window at the right moment?* diff --git a/docs/concepts/continuous-improvement.md b/docs/concepts/continuous-improvement.md new file mode 100644 index 0000000..3b7b0e5 --- /dev/null +++ b/docs/concepts/continuous-improvement.md @@ -0,0 +1,84 @@ +# Continuous improvement: operating at the system level + +This is the idea at the center of Context Specs: **you work on the system that +produces features, not on the features themselves.** A harness that only built +features would be a fast worker with no memory. This one gets measurably better at +building *your* project every time it builds — and it improves in a direction you +choose. + +## Two ways to fix a problem, very different leverage + +When a feature comes out wrong, you can fix it at two levels, and the difference is +the whole point: + +- **Fix the code.** You correct this one feature and ship it. Nothing else changes. + The next feature starts from the same place the last one did. This is real work, + and sometimes it is all a situation needs — but its leverage is exactly one + feature. +- **Fix the context gap.** You find *why* the agent got it wrong — a thin + definition of done, a missing piece of [long-term memory](./long-term-memory.md), + an environment the agent could not navigate, a rule nobody wrote down — and you + correct *that*. Now every future feature that touches the same area benefits. You + fixed a *class* of failure, not an instance. + +You do both. But **context-gap fixes are the main thing**, because they compound +and code fixes do not. Fixing the code fixes one feature; fixing the context fixes +the class. + +## The metric: the ready-to-merge ratio + +The harness's [output contract](./output-contract.md) gives you a clean signal to +optimize. Every feature ends **ready-to-merge** or **STUCK**, and the ratio between +them is your metric. It is not a vanity number — it is directly actionable, because +every STUCK *names the context to improve*. The job of continuous improvement is to +drive that ratio up: fewer stuck features, more that come back ready, as the +project's context gets sharper. + +A codebase you type into is a constant cost — it decays, it needs maintenance, +every feature starts roughly where the last one did. A harness you tune is an +**appreciating asset** — each feature leaves the project a little more capable of +building the next one. + +## The flywheel has several engines + +Improvement enters the system through more than one path, and they reinforce each +other: + +- **`/learn` (the machine loop).** After every merge, the harness reads the merged + diff and updates [long-term memory](./long-term-memory.md) — routing durable + lessons into the Expert, `AGENTS.md`, or a lint. Automatic, behind a human merge. +- **Lints (the memory an agent cannot ship past).** The highest-leverage + improvement: a recurring lesson promoted to a short script whose error message is + itself a fix prompt. Once written, it protects every future PR. See + [long-term memory](./long-term-memory.md#lints-the-memory-an-agent-cannot-ship-past). +- **Evals (the human loop).** Reading the build trail with + [`/evaluate-sessions`](../how-to/improve-from-traces.md) surfaces where the + project's context served or failed the agents. What you find is frozen as a + runnable eval — a regression test over the harness's *own skills and context* — + and captured as a context fix. +- **Unsticking (the forced detour).** Every STUCK you resolve by correcting the + misleading context, then merging, feeds that correction into `/learn` as ground + truth. See [Unstick a feature](../how-to/unstick-a-feature.md). + +The common shape across all four: + +> *Observe a trace → capture it as an eval or a lint → fix the context → it +> persists.* + +## Why this is the job now + +Strip away the typing — the part a model does better than you — and what is left is +not "less work." It is *higher-leverage* work: shaping the context the harness +reasons over, deciding what becomes a lint versus flexible prose, and reading the +build trail to fix what failed an agent. Fixing the code fixes one feature. Fixing +the context fixes the class. That shift, from operating on features to operating on +the system, is what lets one engineer do far more — and it is the whole bet of +Context Specs. + +## Related + +- [Long-term memory](./long-term-memory.md) — the durable store the flywheel writes + to. +- [The human loop](./the-human-loop.md) — the phases where *you* drive improvement. +- [The output contract](./output-contract.md) — where the ready-to-merge/STUCK + signal comes from. diff --git a/docs/concepts/harness-engineering.md b/docs/concepts/harness-engineering.md new file mode 100644 index 0000000..e21c932 --- /dev/null +++ b/docs/concepts/harness-engineering.md @@ -0,0 +1,76 @@ +# Harness engineering + +Harness engineering is the discipline Context Specs is built on. Understanding it +is the fastest way to understand everything else, because every other concept is +an instance of it. + +## What a harness is + +Start with the equation: + +> **Agent = Model + Harness** + +The **model** is the probabilistic part: given a context window, it produces a +plausible next step. On its own it is a suggestion engine — capable, but with no +guarantees. It can drift, hallucinate, declare success it did not achieve, or +quietly lose the thread in a long session. + +The **harness** is everything else wrapped around the model: the tools +it can call, the skills and prompts that shape each step, the control flow that +decides what runs next, and the checks that decide whether a step actually +succeeded. The harness makes the model's intelligence useful. + +An agent is the two together. When people say "the agent did X," the interesting +engineering is almost always in the harness — the model is a component inside it. + +## The discipline + +> **Harness engineering is building a system — defined inputs, defined outputs — +> out of deterministic code and probabilistic code, where the deterministic code +> invokes the probabilistic code and controls the flow all the way to a +> guaranteed, well-defined output.** + +Read that carefully, because each clause is load-bearing: + +- **Defined inputs, defined outputs.** A harness is a function with a contract, + not an open-ended chat. You know the shape of what goes in and the shape of what + comes out. +- **Deterministic code and probabilistic code.** Both are first-class. The model + supplies intelligence; the deterministic code supplies control, verification, + and repeatability. +- **The deterministic code invokes the probabilistic code.** The control flow is + in the reliable half. The model is called *by* the system; it does not steer + the system. There is no LLM in the decision path deciding what happens next. +- **All the way to a guaranteed, well-defined output.** The system does not stop + at "the model responded." It drives the process until it reaches one of a small + set of defined terminal states — and it always reaches one. + +## The guaranteed output: ready-to-merge or STUCK + +In Context Specs, the input is a PRD with a runnable definition of done, and the +guaranteed output is **a pull request in one of exactly two finished states**: + +- **Ready to merge** — the feature is built and every check passed. +- **STUCK** — the system could not finish honestly, and it hands you a diagnosis + of what blocked it instead of faking success. + +Both are *finished* states. The system never returns "maybe," never returns a +half-built branch with no verdict, and never claims done when it is not. That +guarantee is the whole point of engineering a harness rather than prompting a +model and hoping. The mechanics of how "done" is proven, and what a STUCK hands +you, are covered in [the output contract](./output-contract.md). + +## How the rest of the system follows from this + +Everything in Context Specs is harness engineering applied at a particular scope: + +- The reliability of each step comes from [context engineering](./context-engineering.md) — + the lever that decides what the model reasons over. +- The system's shape is [one harness driving N environments](./two-tier-architecture.md). +- The deterministic control flow is [the dispatcher](./the-dispatcher.md): pure + code, no model in the decision path. +- The guaranteed output is defined by [the output contract](./output-contract.md). +- The harness gets better over time through [continuous improvement](./continuous-improvement.md) — + you operate on the *system*, not the individual feature. +- And [the human loop](./the-human-loop.md) is the part of the job a harness + cannot take: deciding what to build and judging whether what came back is right. diff --git a/docs/concepts/long-term-memory.md b/docs/concepts/long-term-memory.md new file mode 100644 index 0000000..348ffc2 --- /dev/null +++ b/docs/concepts/long-term-memory.md @@ -0,0 +1,131 @@ +# Long-term memory + +Long-term memory is what makes the harness get *better* at building your project, +not just fast at it. Where a [spec](./spec-driven-development.md) is short-term +memory for one feature, long-term memory is durable, cross-feature knowledge: +architecture, patterns, constraints, and the rules the project has learned to +enforce. Improve one shard of it and every future feature plans better. + +## Two memory shapes, opposite costs + +The harness keeps long-term memory in two places, and the difference between them +is the whole discipline — not an implementation detail: + +- **The Expert** is **lazy memory** — pulled on demand. It enters a context window + only when a skill deliberately consults it, so you pay tokens for it *only when + it earns them*. This is the default home for real knowledge: architecture, + patterns, how things get verified here. +- **`AGENTS.md`** is **eager memory** — loaded automatically into every agent that + touches the folder, before anyone knows whether it is relevant. You pay for every + line on *every* session, so the bar for putting something here is much higher. + +Because eager memory is a standing tax and lazy memory is paid only when consulted, +the two compose as **map and territory**: `AGENTS.md` is a short table of contents +that *points into* the Expert; it never duplicates the dense knowledge. A +monolithic `AGENTS.md` rots, crowds out the task, and turns "everything important" +into "nothing important." Keep it a map; keep the density in the Expert. Using +`AGENTS.md` — the vendor-neutral file every harness reads — rather than a +tool-specific one also keeps your project's memory *yours*, portable across +whatever agent you point at it. + +## The Expert: knowledge defined once, pulled on demand + +An **Expert** is a progressively-disclosed knowledge module: + +``` +expert/ # (or expert-{name}/ for an outside domain) +├── SKILL.md # high-level index + routing table, read first +└── references/ # dense knowledge, one topic per file, read on demand + └── {prefix}-{topic}.md +``` + +It is the [context-engineering](./context-engineering.md) menu made concrete: a +short `SKILL.md` the agent sees first, pointing into dense `references/` it loads +only when relevant. Define the knowledge once and it flows automatically through +every phase — you never paste it into a prompt again. Experts are **composable**: +a React Expert and a DynamoDB Expert both contribute to a full-stack feature, and +organizations can layer in private Experts for internal libraries without touching +any existing skill. + +The Expert is one of the two [developer-owned levers](./two-tier-architecture.md). +It is seeded as a skeleton by `/env-init` and grows from there. + +## One write path: ground truth only + +The most important rule keeps memory honest: + +> **Memory reflects what is committed to `main`, never what is planned.** + +PRDs and specs describe intent; code is reality. So the memory loop only ever +writes from a *merged diff* — never from a branch in flight, never from a feature +that might still be abandoned. That gives the whole system a single, auditable +**write path**: a human merges a feature; +[`/learn`](../../skills/harness/learn/SKILL.md) observes the result; the change to +memory lands on its own `learn/` pull request that a human reviews and merges. +There is exactly one door, and it opens only on ground truth. + +```mermaid +flowchart LR + Merge([Merge to main]) --> Learn["/learn\nreads the merged diff"] + Learn --> Reconcile{Reconcile against\ncurrent memory:\nadd, edit, or delete?} + Reconcile -->|no — common| Noop[Nothing to learn.\nMost merges teach\nnothing durable.] + Reconcile -->|yes| Route[Route each change to\nexactly one destination] + Route --> PR["learn/ PR\nyou review and merge"] +``` + +## The four destinations of a learned fact + +`/learn` is disciplined about *where* a fact lands. Every fact worth keeping routes +to **exactly one** place: + +| Destination | Use when | What it costs | +|---|---|---| +| **A lint** (`scripts/lints/*`) | The rule is mechanically checkable — pass/fail needs no judgment | Enforced on every PR forever; the error message doubles as a fix prompt | +| **Eager prose** (`AGENTS.md`) | It must be known *before* an agent would consult the Expert, and clears a strict bar | Paid every session — the high bar | +| **Lazy prose** (an Expert reference) | It is useful when *deliberately reasoning* about an area | Paid only when consulted — the usual home | +| **Nowhere** | It is inferable from the code, taste-only, or transient | — | + +Most facts go to the last two. A **nothing-to-learn** outcome is **common and +correct**, not a failure: vuln fixes, refactors that do not change shape, and +routine bug fixes usually change no memory at all. Preferring nothing over noise is +what keeps the Expert worth reading. + +## Lints: the memory an agent cannot ship past + +The top row of that table is the highest-value thing the harness can learn. A +**lint** is just a short script: glob some files, check a fact, exit non-zero with a +remediation message. It beats prose for two reasons: + +1. **The error message is a prompt.** It does not say "violation" — it says *how to + fix it*, feeding remediation straight into the agent's window so it + self-corrects. A prose rule is advice the agent *might* follow; a lint is a rule + it *cannot ship past*, plus the fix. +2. **It scales the way agents scale.** Once written, it applies to every file and + every future PR at once. + +So the most durable thing your project owns is the set of custom lints it grows +over time — layer-dependency direction, no raw SQL interpolation, +structured-logging-only, naming rules, file-size caps. Promotion is conservative: a +discovered invariant lives as prose first and is promoted to a lint only on +recurrence, and a drafted lint **must pass against the just-merged code** before it +is wired in. + +## Reconcile, don't accumulate + +Memory is a *current model of `main`*, not an append-only log. On every merge +`/learn` runs a three-pass reconcile against the merged diff: it **deletes** +references whose anchor code is gone, resolves **contradictions** between files that +now disagree, and **edits** claims the diff has invalidated. Adds, edits, and +deletes all ride the same `learn/` PR, each with a one-line justification +citing the diff hunk that motivated it — if it cannot be justified from the diff, it +is dropped. Inside the Expert, files are kept small and cross-linked with +`[[wikilinks]]`, so consulting memory never means loading all of it. + +## Related + +- [Continuous improvement](./continuous-improvement.md) — long-term memory is one + engine of the flywheel; this is why the harness compounds. +- [Spec-Driven Development](./spec-driven-development.md) — the short-term + counterpart, and where Reflect feeds long-term memory. +- [The human loop](./the-human-loop.md) — you are the third write path: you seed + memory deliberately, always through a merge. diff --git a/docs/concepts/output-contract.md b/docs/concepts/output-contract.md new file mode 100644 index 0000000..40a8219 --- /dev/null +++ b/docs/concepts/output-contract.md @@ -0,0 +1,90 @@ +# The output contract: ready-to-merge or STUCK + +The harness makes one promise: for every feature you start, you get back a pull +request in one of exactly two finished states — **ready to merge** or **STUCK with +a diagnosis**. This page covers how "done" is defined, how it is proven, and what +each terminal state means. It is the mechanics behind the guarantee introduced in +[harness engineering](./harness-engineering.md). + +## The definition of done is runnable + +The input to the harness is not just a prose description; it is a PRD paired with an +**executable definition of done**. `/intent` produces two coupled artifacts: + +- `prds//prd.md` — the prose: **why** this exists and **what** "done" + means; +- `prds//run-prd-test.sh` — the executable: **how you will know** it is + done. It exits 0 when the feature is built. + +The runner is the load-bearing idea, and it is what makes the harness +**goal-based**: the runner *is* the goal, and the loop keeps invoking the model +until the goal is met or the retry caps call it honestly unreachable. Prose drifts +and "done" becomes an argument; an exit code does not. + +Before `/intent` commits anything, it runs the script against today's unbuilt code +and confirms it fails **for the right reason** — because the behavior is genuinely +absent, not because of a typo or a missing dependency. That is the empirical proof +that the test actually corresponds to the intent. See +[Express intent](../how-to/express-intent.md) for how to write one. + +## Verification the agent cannot talk past + +Autonomy is only safe if "done" cannot be faked, so the harness leans on checks +that run **regardless of what the agent decides**, fastest to slowest: + +1. **Pre-commit hooks** — linters, formatters, type checks on changed files. +2. **Slice unit tests** — each slice's tests, run during implementation. +3. **`local-checks.sh`** — the cheapest gate before a PR: lint, typecheck, the fast + unit suite, skip-detection, and any custom project lints. It proves + **correctness, not coverage** — it blocks on real defects and merely warns on + style. +4. **The PRD runner** — `./prds//run-prd-test.sh` must exit 0. This is the + definition of done, and because it can mix deterministic shell checks with an + LLM-as-judge, "runnable" does not force everything into unit-test shape. + +The agent does not get to decide whether these run — they run, and the git/CI layer +enforces them independently. Crucially, **silencing a check counts as bypassing +it**: an agent may never add a suppression directive, weaken a config, or skip a +test to reach green. A check it cannot pass honestly is one it lets fail. + +## Ready to merge + +The first terminal state: the feature is built, every check passed, the PR is open +and the reviewer has no findings left. What is waiting for you is finished work you +never typed — ready for your judgment, not your typing. Evaluating it is the +[human loop](./the-human-loop.md)'s job, not the harness's. + +## STUCK + +Sometimes a feature genuinely cannot get through a step — a plan is subtly wrong, a +check will not pass honestly. The harness does not loop forever and will not fake +success. Every step has a bounded retry budget; when a step hits its cap, the +dispatcher declares the feature **STUCK**, posts a diagnostic to the PR, and halts +that feature until you step in. + +What it hands you is deliberate. Not just "it failed" — a session log of every +`claude -p` invocation across the chain (so you can open any trace and see what the +agent saw), a tail of the failing output, and a **diagnosis-first checklist**. The +checklist's first item is not "fix the code." It is *identify which piece of context +misled the agent* — a stale Expert note, a thin spec, a PRD that left something out +— and correct that first. Because if the context that caused the failure stays +wrong, the same class of failure comes back the next time a feature touches that +area. See [Unstick a feature](../how-to/unstick-a-feature.md). + +## Both states are finished — and the ratio is your metric + +Ready-to-merge and STUCK are **both finished states**: the system completed its run +and told you the truth about which one you got. It never returns "maybe." + +The ratio between them is the number to watch. Every STUCK names a piece of context +to improve, and every improvement raises the odds the next feature comes back +ready. Driving that ratio up over time is [continuous improvement](./continuous-improvement.md) +— the reason the harness is worth tuning rather than just running. + +## Related + +- [Harness engineering](./harness-engineering.md) — where the guarantee comes from. +- [The dispatcher](./the-dispatcher.md) — the engine that drives every feature to + one of these two states. +- [Design invariants](../reference/invariants.md) — the proof the verification + layers cannot be talked past. diff --git a/docs/concepts/spec-driven-development.md b/docs/concepts/spec-driven-development.md new file mode 100644 index 0000000..43f1fe4 --- /dev/null +++ b/docs/concepts/spec-driven-development.md @@ -0,0 +1,114 @@ +# Spec-Driven Development: the harness's short-term memory + +Spec-Driven Development (SDD) is how the harness builds one feature well. It is +best understood as **[context engineering](./context-engineering.md) applied to a +single feature** — and as the harness's **short-term memory**: a point-in-time +plan for one feature, not a durable store of project knowledge. + +## SDD is a context-engineering technique + +Give an agent "add search to the app" and it does something reasonable: it greps, +opens files to infer conventions, guesses how you test. On a small change that is +fine. On a large feature it accumulates — halfway through, the window is full of +half-relevant code the agent retrieved on its own, the original intent has decayed, +and the next compaction is poised to drop the part it actually needed. + +SDD fixes this by doing the thinking **outside** the context window first and +persisting it as structured files on disk. The plan does not sit in the +conversation where it decays; it sits in files the agent pulls from. That is the +core context-engineering move: **the right context reaches the window at the right +time because the agent dynamically pulls the slice it needs, rather than carrying +the whole feature in-window the entire way.** The plan cannot decay or compact +away, because it was never in the conversation to begin with — after a compaction +the agent simply re-reads the spec and continues. + +## Short-term memory, not long-term memory + +This is the distinction that keeps the two memory systems from blurring: + +> A spec is **short-term memory**: it exists to build **one feature**, and it is +> a **point-in-time** artifact. It is not where the project's durable knowledge +> lives. + +While the feature is being built, the spec is the agent's working memory — +focused entirely on this one feature, disclosed a slice at a time. Once the +feature is complete, the spec **stops steering anything**. It does not roll up +into a growing knowledge base. Its remaining value is as a *record*: + +- **Reviewing the code** — the spec says what was intended, so a reviewer can + check the diff against it. +- **Evals** — a completed spec and its build trail are raw material for capturing + regression checks over the harness's own behavior. +- **Understanding the agent's decisions** — when you want to know *why* the agent + built it this way, the spec is the reasoning it worked from. + +Durable, cross-feature knowledge — architecture, patterns, hard-won constraints — +does not belong in a spec. That is [long-term memory](./long-term-memory.md), and +it lives in the Expert. Keeping short-term and long-term memory separate is what +lets each do its job: the spec stays lean and disposable; the Expert stays curated +and permanent. + +## The artifacts SDD produces + +When [`/spec-planning`](../../skills/sdd/spec-planning/SKILL.md) runs, the planner +researches the actual codebase (grounding the plan in reality, not guesswork), +pulls in any Experts whose triggers match, and writes the plan to disk as two +kinds of file: + +- a **mainspec** (`specs//mainspec.md`) — the complete end state, the + north star you work backward from; +- ordered **slices** (`specs//slices/`) — temporal chunks of intent, each + a coherent unit of work focused on *what* and *why*. + +What goes *into* a spec is itself disciplined context engineering — precise +BEFORE/AFTER file paths, type contracts first, DO/DON'T counterexamples, narrative +temporal flows, and forward-looking requirements — so the agent can execute +without guessing. + +## Temporal slicing: progressive disclosure, structurally + +Slices are ordered by *intent* (what needs to happen), not by *component* +(frontend/backend), because features have a natural dependency order and slicing +by intent preserves it. Each slice depends on prior slices and declares contracts +for future ones. + +That ordering does two jobs at once. For the implementer it is progressive +disclosure made structural: the agent is fed **only the current slice**, never the +whole feature, so its window stays small and focused. For the orchestrator, the +dependencies define execution order — every mainspec carries a **Slice Dependency +Map** that says which slice to implement next. + +## Validation: consensus before code + +A plan has blind spots, so +[`/spec-validate`](../../skills/sdd/spec-validate/SKILL.md) hardens it before a +line of code is written: multiple independent Opus subagents review the spec, and +agreement is graded (3/3 = very high confidence, down to 1/3 = a judgment call). +Relevant Experts add domain-specific review. Findings are consolidated and the +impactful ones applied **in place** — validation sharpens the plan, it does not +bounce it back to start over. Independent reviewers have *different* blind spots; +consensus scoring turns that into a confidence signal instead of a coin flip. + +## Implementation: write, verify, reflect + +[`/implement-slice`](../../skills/sdd/implement-slice/SKILL.md) implements one +slice in three beats: write the code, verify it with unit tests, then **Reflect**. +Reflect is the one that reaches *out* of short-term memory: once the code is green, +the agent compares what it just learned against the project's long-term memory and +— only when there is a real, durable lesson — writes it back into the Expert. This +is the one bridge from a feature's short-term memory to the project's long-term +memory, and the bar is deliberately high; most slices reflect nothing. + +[`/implement-mainspec`](../../skills/sdd/implement-mainspec/SKILL.md) orchestrates +the whole feature: it reads the Slice Dependency Map and delegates each slice, in +dependency order, to a focused subagent — committing each to the feature branch as +it lands. One slice at a time keeps the orchestrator's window lean and every +commit reviewable. + +## Related + +- [Context engineering](./context-engineering.md) — the lever SDD applies. +- [Long-term memory](./long-term-memory.md) — the durable counterpart; where + Reflect writes. +- [The dispatcher](./the-dispatcher.md) — runs the SDD loop autonomously, one step + per fresh window. diff --git a/docs/concepts/the-dispatcher.md b/docs/concepts/the-dispatcher.md new file mode 100644 index 0000000..0789715 --- /dev/null +++ b/docs/concepts/the-dispatcher.md @@ -0,0 +1,105 @@ +# The dispatcher: the deterministic engine + +The dispatcher is the deterministic control flow at the center of the harness. It +is the piece that makes a run repeatable, crash-safe, and trustworthy to leave +unattended — because it holds **no LLM in the decision path** and **no hidden +state**. + +## Artifacts are the state + +The reason it is safe to leave the harness running is almost boring, and that is +the point. **The harness keeps no hidden state.** There is no daemon holding "I am +currently on step 3" in memory, no queue, no coordination service. The complete +state of every feature is observable from two things: the files on disk and the +branches in git. + +A small bash script — the dispatcher +([`scripts/poll-and-dispatch.sh`](../../scripts/poll-and-dispatch.sh)) — wakes up +periodically, takes an environment as its argument, reads that environment's state +fresh, and decides the single next step for each active feature. Its core is an +`if/elif` chain: *planning output absent? run planning. Present but validation +absent? run validate.* The chain **is** the state machine; the artifacts on disk +**are** the state. + +A feature advances as a sequence of states, each produced by one skill and +detected by the presence of its output on disk: + +```mermaid +flowchart TD + Intent["/intent (you, once)\nPRD + runnable definition of done"] --> Claim[Harness claims the work] + Claim --> Plan["/spec-planning"] + Plan --> Validate["/spec-validate"] + Validate --> Implement["/implement-mainspec"] + Implement --> Checks["local-checks.sh"] + Checks --> PR[Open pull request] + PR --> Review[Reviewer posts findings] + Review -->|findings| Respond["/address-feedback"] + Respond --> Review + Review -->|no findings left| Ready[Ready for your review] + Ready --> You([You merge]) +``` + +## Three properties that make it trustworthy + +Three consequences fall out of "artifacts are the state," and together they are +why the machine is safe to run: + +- **Crash recovery is free.** Kill the dispatcher mid-step — `kill -9`, a dead + laptop, a closed lid. The next tick re-reads disk, sees the half-finished state, + and resumes. Nothing to clean up, because nothing was ever held in memory to + lose. +- **Every step runs in a fresh context window.** The dispatcher does not *do* the + work; it shells out to a brand-new `claude -p` process per skill. Each skill + reads what it needs from disk, does its job, writes its output, and exits. No + step inherits another's polluted window — the + [context-engineering](./context-engineering.md) problem, solved by construction. + This is also why the loop can run for hours without the context rot a single + long session accumulates. +- **The dispatcher itself contains no LLM.** The decision of *what runs next* is + pure, deterministic bash. The intelligence is in the skills; the routing is + mechanical. That is what makes "what will it do next?" answerable just by looking + at disk — no model in the loop to second-guess. + +## It never touches your checkout + +The property developers ask about first: **the harness never touches your working +tree.** It operates on your clone only through git *refs* — fetching, pushing, +claiming branches — and does all its work in *sibling worktrees* +(`-harness-`) it creates and tears down itself. Your uncommitted +work is structurally out of its reach. + +## Scheduling is one exit code + +The dispatcher does not schedule itself; a supervisor (or cron, or a server +runner) invokes it, and the dispatcher reports what happened through its **exit +code**: + +- **0 — idle.** No work advanced. Sleep the interval (default 5 minutes) and tick + again. +- **10 — work advanced.** Re-invoke immediately, so a claimed PRD marches through + planning → validate → implement at machine speed instead of one step per nap. +- **non-zero error** — back off exponentially. + +A STUCK feature reports idle, so a stuck environment never hot-loops. This one +exit code is the entire scheduling protocol; see [Run the harness](../how-to/run-the-harness.md) +for the CLI that drives it. + +## The branch namespace is the work queue + +There is no separate database of "what work exists." The branches *are* the +registry, and claiming work is a single atomic git rename — no lock service, no +coordination daemon. The full namespace and on-disk state layout are documented in +[Reference: state and branches](../reference/state-and-branches.md). + +## The rigorous version + +This page gives you the intuition. For the precise properties the harness holds +under concurrency, crashes, and restarts — and the proof it is safe to leave +running — see the [design invariants](../reference/invariants.md). + +## Related + +- [Harness engineering](./harness-engineering.md) — the discipline this engine + realizes. +- [The output contract](./output-contract.md) — the guaranteed output the + dispatcher drives toward, and the verification that backs it. diff --git a/docs/concepts/the-human-loop.md b/docs/concepts/the-human-loop.md new file mode 100644 index 0000000..f07b912 --- /dev/null +++ b/docs/concepts/the-human-loop.md @@ -0,0 +1,128 @@ +# The human loop + +Once the machine does the typing, what is left is the part only you can do — and it +is the most important part, not the leftover. The human loop is you, deliberately +working the judgment a harness structurally cannot supply. + +## Outsource your thinking, not your understanding + +The governing line comes from Andrej Karpathy: *"you can outsource your thinking but +you can't outsource your understanding."* It draws a sharp boundary: + +- **Verifiable work → outsource it freely.** Syntax, API recall, implementation + mechanics. The model is superhuman here; spending your attention on it is waste. +- **Unverifiable work → you must own it.** Whether this is the right thing to + build, whether the abstraction is sound, whether the edge cases are handled for + good reasons, whether the UX feels right. Errors here are subtle and do not + announce themselves — exactly where your understanding has to be load-bearing. + +The harness took the first category. The human loop is you, deliberately working +the second. + +## Three phases, one cycle + +``` + Understanding ──▶ Intent ──▶ [ the harness builds ] ──▶ Evaluate + build your model express the machine loop walk the result, + of the problem what to run it, judge it, + space build improve the context + ▲ │ + └──────────── evaluating deepens understanding ──────────┘ +``` + +The human loop and the machine loop are not parallel tracks — they feed each other: + +> **Your loop's output is the harness's input. The harness's output is your loop's +> input.** + +You produce intent (a PRD); the harness consumes it and produces a pull request; +you consume that PR by evaluating it; what you learn sharpens the next intent. +Naming your loop explicitly is what keeps *your* job legible as the machine takes +over more of the typing. + +## Understanding — `/wiki-init` + +Before you can express good intent, you need a real model of the problem space: the +domain, the prior art, the constraints, the trade-offs. +[`/wiki-init`](../../skills/human-loop/wiki-init/SKILL.md) stands up a standalone, +LLM-maintained knowledge base in the shape of Karpathy's "LLM Wiki": you synthesize +**once, at ingest time**, into durable, cross-linked pages that compound as sources +accumulate. You curate sources and ask questions; the LLM does the bookkeeping +humans abandon wikis over. + +This wiki is deliberately **not** a project artifact — hold this bright line: + +- The **wiki** is cross-project **domain and architecture** knowledge, written to + be read **by you**. It lives in its own repo, outside any codebase. +- The **Expert** ([long-term memory](./long-term-memory.md)) is per-project **code** + memory, written to be read **by agents**. + +A richer model of the domain means a sharper `/intent`. + +## Intent — `/intent` + +[`/intent`](../../skills/human-loop/intent/SKILL.md) is where understanding becomes +a buildable thing. Its discipline: + +- **Elicit outcomes, not solutions.** People arrive describing a solution ("add a + `/api/search` endpoint"); the job is to surface the *need* underneath ("readers + can't find a post by title") and the observable outcome that would satisfy it. +- **"How would we know that's true?"** is the throughline. Asked of every desired + outcome, it converts a wish into both a sharp prose criterion *and* a concrete + check — the coupled `prd.md` + `run-prd-test.sh` born together (see the + [output contract](./output-contract.md)). +- **You don't write the runner; the Expert drafts it, you review.** This keeps + verification grounded in how the project actually works. + +`/intent` is a coordinator, not a knowledge holder — the domain reasoning comes from +the Expert; your contribution is understanding and the judgment about what is worth +building. See [Express intent](../how-to/express-intent.md). + +## Evaluate — `/evaluate-pr` and `/evaluate-sessions` + +The back bookend, and the mirror of `/intent`. There are two skills, because there +are two things to evaluate. + +- **[`/evaluate-pr`](../how-to/evaluate-a-pr.md)** — evaluate *what was built*. The + tangible output is merge, fix-and-push, or close; the output that matters more is + *you* understanding the change deeply enough to defend every decision in it. The + bot reviewer already caught the mechanical defects, so this skill sets those aside + and spends your attention on what a bot cannot judge — is this simpler than it + could be, is the abstraction sound, does the UX feel right? If the walk surfaces + something to change, **you fix it here and push** — you never hand work back to + the loop. You are the last mile. +- **[`/evaluate-sessions`](../how-to/improve-from-traces.md)** — evaluate *how it + was built*. This is where the human loop reaches back and improves the harness + itself, reading the `claude -p` build trail to find where context served the + agents or failed them, and freezing what you find as evals and context fixes. This + is the heart of [continuous improvement](./continuous-improvement.md). + +## Memory still has one write path + +Evaluation does *not* write memory directly. Whatever you learn here is not ground +truth yet, because the change is not merged yet. So the insight either becomes a +pushed fix that rides into `main` — where `/learn` picks it up — or it lives in your +head and sharpens the next Intent. Evals and context fixes land on a branch and +reach memory the same way: through a merge. You may now *seed* memory deliberately; +you still never bypass the door. See [long-term memory](./long-term-memory.md#one-write-path-ground-truth-only). + +## The three places you steer + +Across the whole system you touch the machine at exactly three points — and every +one is a phase of this loop: + +| You... | Human-loop phase | What it does to the machine | +|---|---|---| +| Confirm a PRD | end of **Intent** | starts the build | +| Evaluate and merge a PR | **Evaluate** | ends the build; triggers `/learn` | +| Unstick a STUCK feature | a forced detour into **Evaluate** | corrects the context, then merges | + +Three touchpoints. Everything between them is the machine. Everything *at* them is +judgment — yours. + +## Related + +- [Continuous improvement](./continuous-improvement.md) — evaluation is how you + drive the flywheel. +- [Harness engineering](./harness-engineering.md) — what the machine half + guarantees, so you can spend your attention here. diff --git a/docs/concepts/two-tier-architecture.md b/docs/concepts/two-tier-architecture.md new file mode 100644 index 0000000..f15a514 --- /dev/null +++ b/docs/concepts/two-tier-architecture.md @@ -0,0 +1,77 @@ +# The two-tier architecture: one harness, N environments + +Context Specs is not something you install into a project. It is its own repo — +**the harness repo** — that operates on any number of project repos, called +**environments**. This two-tier split is the structural heart of the system. + +``` +HARNESS REPO (tier 1 — yours, one) ENVIRONMENT REPOS (tier 2 — your projects, many) + bin/context-specs the CLI AGENTS.md the eager contract + scripts/ the dispatchers .claude/skills/intent/ project-owned /intent + skills/ canonical skills .claude/skills/expert/ long-term memory + state// runtime state scripts/ bootstrap + local checks + .harness/env this env's dials +``` + +You maintain **one** harness repo. It drives **many** environments — so working on +several projects at once is the default, not a special mode. Improve something in +the harness repo and every environment inherits it at once. + +## The dividing line: could you write it without reading the project's code? + +Every artifact in the system lands on one side of a single test: + +> **Could you write it without reading the project's code?** + +- **Yes → it belongs in the harness repo.** The CLI, the dispatchers, the + canonical skills. These are project-agnostic; improving one improves every + environment. +- **No → it is Software 3.0.** It has to be generated by a model reading *that* + project, committed to *that* project, and evolved with it. An `AGENTS.md` that + describes this codebase's conventions, an `/intent` skill tuned to how this team + expresses features, the project's Expert — none can be written in the abstract. + +The phrase **Software 3.0** names this second category: code and context that are +authored by a model reading a specific project rather than typed by a human or +shipped by a framework. The two-tier split is what keeps the shared, reusable half +cleanly separated from the per-project, generated half. + +## How the tiers meet: symlinks and generation + +Two mechanisms connect the tiers: + +- **`context-specs add `** registers a project as an environment and + **symlinks** the canonical skills from the harness repo into the environment's + `.claude/skills/` (gitignored — they are the harness's, not the project's). This + is the deterministic half of setup. See [Add an environment](../how-to/add-an-environment.md). +- **`/env-init`** runs *inside* the environment and generates the Software 3.0 + half — the `AGENTS.md`, the project-owned `/intent`, the Expert skeleton, the + checks, the bootstrap. See [Initialize a project](../how-to/initialize-a-project.md). + +Setup itself demonstrates the discipline: the CLI does the deterministic, +project-agnostic part, then a skill reads the project and generates the part that +can only come from reading it. + +## The two developer-owned levers + +Most canonical skills are symlinked in and left alone. Two artifacts are +deliberately **project-owned** rather than shared, because they are the two levers +that most determine how well the harness serves a given project: + +- **`/intent`** — the input side. How well a project turns an open-ended idea into + a PRD plus a runnable definition of done is one of the biggest levers it has. + It ships as a committed copy you tune to your project, not a symlink. See + [Express intent](../how-to/express-intent.md). +- **The Expert** — the memory side. The project's [long-term memory](./long-term-memory.md), + which informs every future plan. The loops file lessons into it; you shape it. + +These are the two places your judgment lives in the environment. Everything else +the harness brings with it. + +## Related + +- [Harness engineering](./harness-engineering.md) — why the system has this shape. +- [The dispatcher](./the-dispatcher.md) — how one harness drives an environment, + step by step. +- [Reference: state and branches](../reference/state-and-branches.md) — the exact + on-disk layout of both tiers. diff --git a/docs/how-to/add-an-environment.md b/docs/how-to/add-an-environment.md new file mode 100644 index 0000000..fbdcbed --- /dev/null +++ b/docs/how-to/add-an-environment.md @@ -0,0 +1,55 @@ +# Add an environment + +Registering a project as an **environment** connects it to your harness. This is +the deterministic half of setup — the CLI does it without reading your project's +code. + +## Prerequisites + +- A [harness repo](./create-a-harness.md). +- A target project that is a git repository. + +## Steps + +From inside your harness repo: + +```bash +context-specs add ~/code/myapp # or: add ~/code/myapp --name myapp +``` + +`add` does three project-agnostic things: + +1. **Symlinks the canonical skills** from the harness repo into the project's + `.claude/skills/` (gitignored — they belong to the harness, not the project). +2. **Writes `.gitignore` entries** so those symlinks and the harness's runtime + artifacts stay out of the project's history. +3. **Registers the project** in the harness's `environments.toml`. + +Two skills are deliberately *not* symlinked, because they are the project's own +[developer-owned levers](../concepts/two-tier-architecture.md#the-two-developer-owned-levers): +`/intent` and the Expert. Those are generated into the project in the next step. + +## Verify + +```bash +context-specs doctor +``` + +The project should now appear as a registered environment. + +## Re-linking + +The skill symlinks live in worktrees the harness creates and tears down, so they +are re-created automatically by `bootstrap-worktree.sh`. If you ever need to +recreate them by hand (e.g. in a fresh clone), run: + +```bash +context-specs link ~/code/myapp +``` + +`link` is idempotent. + +## Next + +- [Initialize a project](./initialize-a-project.md) — generate the Software 3.0 + half (`AGENTS.md`, `/intent`, the Expert, checks). diff --git a/docs/how-to/create-a-harness.md b/docs/how-to/create-a-harness.md new file mode 100644 index 0000000..9975061 --- /dev/null +++ b/docs/how-to/create-a-harness.md @@ -0,0 +1,37 @@ +# Create a harness + +The harness repo is the one repo you maintain that drives all your projects. You +create it once. + +## Prerequisites + +- Node.js (for `npx`). +- `git`, and the `claude` CLI on your `PATH`. + +## Steps + +Create (or adopt) a harness repo from the template: + +```bash +npx context-specs init my-harness +cd my-harness +``` + +`init` scaffolds the [two-tier layout](../concepts/two-tier-architecture.md): the +`context-specs` CLI under `bin/`, the dispatchers under `scripts/`, the canonical +skills under `skills/`, and an empty `state/` for per-environment runtime data. If +you point `init` at an existing repo, it *adopts* it — adding the harness pieces +without disturbing what is already there. + +## Verify + +```bash +context-specs doctor +``` + +`doctor` checks the harness itself, the registry, and every registered environment. +On a fresh harness it should report a healthy harness with no environments yet. + +## Next + +- [Add an environment](./add-an-environment.md) — register your first project. diff --git a/docs/how-to/evaluate-a-pr.md b/docs/how-to/evaluate-a-pr.md new file mode 100644 index 0000000..25f2b60 --- /dev/null +++ b/docs/how-to/evaluate-a-pr.md @@ -0,0 +1,57 @@ +# Evaluate a PR + +When the harness hands back a **ready-to-merge** PR, evaluating it is your job — the +[unverifiable work](../concepts/the-human-loop.md#outsource-your-thinking-not-your-understanding) +the machine could not do. `/evaluate-pr` is a teacher and a taste partner, not a +second linter. + +## Prerequisites + +- A ready PR from the harness. +- You are running `claude` inside the project. + +## Steps + +```bash +cd ~/code/myapp +claude +> /evaluate-pr +``` + +[`/evaluate-pr`](../../skills/human-loop/evaluate-pr/SKILL.md) produces two outcomes: + +- **The tangible one:** merge, fix-and-push, or close. +- **The one that matters more:** *you* understanding the change deeply enough to + defend every scenario and design decision in it. + +Because the bot reviewer already caught the mechanical defects, the skill *ingests* +those findings and sets them aside, then spends your attention on what a bot +structurally cannot judge: + +- Is this simpler than it could be? +- Is the abstraction sound? +- Does the UX feel right? + +It runs the system with you, walks the definition-of-done scenarios live, and probes +Socratically rather than lecturing. The understanding gate is soft but real: it ends +on "do you feel you understand this change?" and skipping the walk-through is an +explicit opt-out, never a silent rubber-stamp. + +## If you find something to change + +**Fix it here and push.** You never hand work back to the loop — you are the last +mile. Whatever you fix rides into `main` on the merge, where +[`/learn`](../concepts/long-term-memory.md) picks it up as ground truth. + +## Why this closes the loop + +The understanding you build here is the literal mechanism that sharpens your next +[intent](./express-intent.md) — evaluating deepens understanding, which produces +better intent, which the harness builds. When you evaluate, you do not just approve +work; you improve the thing that produced it. + +## Related + +- [Improve from build traces](./improve-from-traces.md) — evaluate *how* it was + built, not just what. +- [The human loop](../concepts/the-human-loop.md) — where this phase sits. diff --git a/docs/how-to/express-intent.md b/docs/how-to/express-intent.md new file mode 100644 index 0000000..66a2061 --- /dev/null +++ b/docs/how-to/express-intent.md @@ -0,0 +1,58 @@ +# Express intent + +`/intent` is how you turn an idea into work the harness can build. It produces a +PRD paired with a **runnable definition of done** — the harness's input and its +goal. This is one of the three places [you steer the machine](../concepts/the-human-loop.md#the-three-places-you-steer). + +## Prerequisites + +- The project is [initialized](./initialize-a-project.md) (`/intent` and the Expert + exist). +- You are running `claude` inside the project. + +## Steps + +```bash +cd ~/code/myapp +claude +> /intent +``` + +Then describe what you want. [`/intent`](../../skills/human-loop/intent/SKILL.md) +will: + +1. **Elicit the outcome, not the solution.** If you describe an implementation + ("add a `/api/search` endpoint"), it surfaces the need underneath ("readers + can't find a post by title") and the observable outcome that would satisfy it. +2. **Ask "how would we know that's true?"** of each outcome, converting a wish into + a sharp prose criterion *and* a concrete check. +3. **Draft the runner for you.** You do not write `run-prd-test.sh` — the Expert + drafts it so verification is grounded in how the project actually works, and you + review it. +4. **Confirm the test fails for the right reason.** Before committing, `/intent` + runs the script against today's unbuilt code and checks that it fails because the + behavior is *genuinely absent* — not because of a typo or missing dependency. + +## What you get + +Two coupled artifacts on a `prd//` branch: + +- `prds//prd.md` — why it exists and what "done" means. +- `prds//run-prd-test.sh` — the executable definition of done; exits 0 when + the feature is built. + +The moment you confirm the PRD, it lands on the queue branch and the harness can +claim it. See [the output contract](../concepts/output-contract.md) for how that +runner becomes the goal the loop drives toward. + +## Tips + +- **Spend your judgment here.** How well a project turns an idea into a good PRD is + one of the biggest levers it has. A thin definition of done is the most common + cause of a STUCK later. +- **Tune the skill.** `/intent` is project-owned; adjust it to how your team + actually expresses features. + +## Next + +- [Run the harness](./run-the-harness.md) — let the loop build it. diff --git a/docs/how-to/improve-from-traces.md b/docs/how-to/improve-from-traces.md new file mode 100644 index 0000000..cdbeb23 --- /dev/null +++ b/docs/how-to/improve-from-traces.md @@ -0,0 +1,62 @@ +# Improve the harness from build traces + +`/evaluate-sessions` is where you evaluate *how* a feature was built, not just what +was built — and turn what you find into durable improvements to the harness itself. +This is the engine of [continuous improvement](../concepts/continuous-improvement.md). + +## Prerequisites + +- A PR the harness built (its `claude -p` build trail is posted on the PR). +- You are running `claude` inside the project. + +## Steps + +```bash +cd ~/code/myapp +claude +> /evaluate-sessions +``` + +[`/evaluate-sessions`](../../skills/human-loop/evaluate-sessions/SKILL.md) reads the +build trail *with you* to find where the project's context served the agents and +where it failed them — where an agent re-derived something the Expert should have +told it, followed a stale `AGENTS.md` pointer, or guessed because a spec was thin. + +## The two durable outcomes + +What you find turns into two things, both of which persist: + +- **Evals** — when a session shows a skill behaved well or badly *given the context + it had*, freeze that as a runnable check under `evals/`. An eval is a regression + test over the harness's **own skills and context** (the analog of testing a + prompt), distinct from the PRD runner, which tests the product. +- **Context fixes** — when a piece of context misled an agent, fix the Expert + reference, the `AGENTS.md` pointer, or the skill. + +The shape is always the same: + +> *Observe a trace → capture it as an eval → fix the context → it persists as a +> regression test.* + +## How it reaches memory + +Evals and context fixes are not ground truth until merged, so they land on a branch +and reach memory the same way everything else does — through a merge, where +[`/learn`](../concepts/long-term-memory.md#one-write-path-ground-truth-only) picks +them up. You may *seed* memory deliberately here; you still never bypass the single +write path. + +## Why it matters + +Every PR the harness builds is a graded trial of your project's context. You are not +auditing one PR — you are **tuning the harness**, and every eval you capture makes +the project a little better at building itself next time. That is what drives the +[ready-to-merge ratio](../concepts/continuous-improvement.md#the-metric-the-ready-to-merge-ratio) +up over time. + +## Related + +- [Continuous improvement](../concepts/continuous-improvement.md) — the flywheel + this feeds. +- [Evaluate a PR](./evaluate-a-pr.md) — the companion skill: evaluate *what* was + built. diff --git a/docs/how-to/initialize-a-project.md b/docs/how-to/initialize-a-project.md new file mode 100644 index 0000000..d39f241 --- /dev/null +++ b/docs/how-to/initialize-a-project.md @@ -0,0 +1,41 @@ +# Initialize a project + +After a project is [registered](./add-an-environment.md), it still needs the +**Software 3.0** half — the pieces that can only be written by a model reading +*this* project. `/env-init` generates them. + +## Prerequisites + +- The project is registered as an environment. +- You are running `claude` inside the project directory. + +## Steps + +From inside the project: + +```bash +cd ~/code/myapp +claude +> /env-init +``` + +[`/env-init`](../../skills/harness/env-init/SKILL.md) reads the codebase and +generates the project-owned artifacts as a single PR: + +- **`AGENTS.md`** — the eager contract: a short map that points into the Expert + (see [long-term memory](../concepts/long-term-memory.md)). +- **`/intent`** — a committed, project-tuned copy of the intent skill. +- **The Expert skeleton** — the project's [long-term memory](../concepts/long-term-memory.md), + seeded and ready to grow. +- **`scripts/`** — the bootstrap and `local-checks.sh` gate. + +## Review and merge + +`/env-init` opens its work as a pull request. **Review and merge it** before +starting the harness — this is generated project code, and it is the foundation +every future feature builds on. Treat it like any other PR. + +## Next + +- [Express intent](./express-intent.md) — describe your first feature. +- [Run the harness](./run-the-harness.md) — start the loop and walk away. diff --git a/docs/how-to/run-the-harness.md b/docs/how-to/run-the-harness.md new file mode 100644 index 0000000..40b62d8 --- /dev/null +++ b/docs/how-to/run-the-harness.md @@ -0,0 +1,64 @@ +# Run the harness + +Once a project is [initialized](./initialize-a-project.md) and has at least one +[PRD](./express-intent.md) on the queue, the harness can build unattended. This +guide covers the CLI that schedules [the dispatcher](../concepts/the-dispatcher.md). + +## Start the loop and walk away + +```bash +context-specs start # this environment +context-specs start --all # every registered environment +``` + +`start` runs one background supervisor per environment — a **build loop** and a +**memory loop**. Each environment gets its own supervisor, lock, and state, so +running several projects at once is the default, not a mode. + +The scheduling protocol is just the dispatcher's exit code: **0** means idle (sleep +the interval, default 5 minutes), **10** means "work advanced" (re-invoke +immediately, so a claimed PRD marches through planning → validate → implement at +machine speed). Errors back off exponentially, and a STUCK feature reports idle so a +stuck environment never hot-loops. + +Stop them with: + +```bash +context-specs stop # or: stop --all +``` + +## Run one pass in the foreground + +When you would rather watch than daemonize: + +```bash +context-specs run # one tick, drained to idle, no daemon +context-specs run --learn # run the memory loop instead of the build loop +``` + +## Observe + +```bash +context-specs status # one table: every environment's features and phases +context-specs status --fetch # fetch refs first, then show +context-specs logs myapp -f # follow the build loop +context-specs logs myapp --learn # the memory loop's logs +context-specs doctor # check the harness, registry, and every environment +``` + +`status` applies the dispatcher's liveness gate — it shows active features, not +ones already merged or closed. + +## What comes back + +Every feature ends in one of two states — **ready to merge** or **STUCK** — per +[the output contract](../concepts/output-contract.md). From here: + +- A ready PR → [evaluate it](./evaluate-a-pr.md). +- A STUCK feature → [unstick it](./unstick-a-feature.md). + +## Scaling beyond a laptop + +The dispatcher is the portable part: the same script a laptop supervisor invokes +can be driven by cron or a server runner. When a team outgrows local laptops, they +move the *scheduler* and keep everything else — an upgrade path, not a rewrite. diff --git a/docs/how-to/unstick-a-feature.md b/docs/how-to/unstick-a-feature.md new file mode 100644 index 0000000..96c840a --- /dev/null +++ b/docs/how-to/unstick-a-feature.md @@ -0,0 +1,49 @@ +# Unstick a feature + +When a feature cannot get through a step honestly, the harness declares it +**STUCK**, posts a diagnostic to the PR, and halts that feature. STUCK is a +*finished* state, not a crash — the system told you the truth instead of faking +success. Unsticking it is a forced detour into the [Evaluate phase](../concepts/the-human-loop.md), +and it is one of the three places you steer the machine. + +## What the harness hands you + +A STUCK PR comes with everything you need to diagnose it: + +- A **session log** of every `claude -p` invocation across the chain — open any + trace and see exactly what the agent saw. +- A **tail of the failing output**. +- A **diagnosis-first checklist**. + +## Steps + +Work the checklist in order — and note that its first item is *not* "fix the code": + +1. **Identify which piece of context misled the agent.** A stale Expert note, a + thin spec, a PRD that left something out, an environment the agent could not + navigate. Read the trace to find where it went wrong. +2. **Fix that context first.** Correct the Expert reference, sharpen the + definition of done, add the missing detail. This is the + [context-gap fix](../concepts/continuous-improvement.md#two-ways-to-fix-a-problem-very-different-leverage) + that stops the *class* of failure, not just this instance. +3. **Then make the feature pass** on its branch, honestly — never by silencing a + check. +4. **Merge.** + +## Why the order matters + +If the context that caused the failure stays wrong, the same class of failure comes +back the next time a feature touches that area. Fixing the code fixes one feature; +fixing the context fixes the class. + +Because you correct the context *on the feature branch* and merge it, that +correction is in the diff [`/learn`](../concepts/long-term-memory.md) reads — so your +hand fix becomes permanent project memory automatically. Every STUCK you resolve +this way raises the [ready-to-merge ratio](../concepts/continuous-improvement.md#the-metric-the-ready-to-merge-ratio). + +## Related + +- [The output contract](../concepts/output-contract.md) — what STUCK means and why + the harness refuses to fake success. +- [Continuous improvement](../concepts/continuous-improvement.md) — why the + context-first order compounds. diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 0000000..1525923 --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,93 @@ +# How Context Specs works + +This is the narrative tour — the through-line that connects the +[core concepts](./README.md#core-concepts) in one reading. If you prefer to jump +straight to a concept or a task, use the [documentation map](./README.md). If you +want the whole idea in one sitting, read on. + +## The one idea + +Context Specs is [harness engineering](./concepts/harness-engineering.md) applied to +building software. An **agent is a model plus a harness** — the model supplies +probabilistic intelligence, the harness supplies deterministic control. Harness +engineering is building a system with defined inputs and defined outputs, where the +deterministic code invokes the probabilistic code and drives the flow all the way to +a guaranteed output. + +Here the input is a PRD with a runnable definition of done. The output is **a pull +request that is either ready to merge or STUCK with a diagnosis** — never a guess. +That output is also the system's feedback signal, and improving the ratio between +the two is the whole game. + +## The lever underneath everything + +Every part of the system is an application of one lever: +[context engineering](./concepts/context-engineering.md) — controlling what enters an +agent's context window, and what stays out. The window is finite and it degrades +(decay, pollution, compaction), so the discipline is to externalize durable +knowledge to the filesystem and let the agent pull just what it needs, just in time. +Watch that same lever get pulled at larger and larger scope as the story goes on. + +## Building one feature: short-term memory + +The smallest unit of work is a feature. +[Spec-Driven Development](./concepts/spec-driven-development.md) is context +engineering applied to one feature: think outside the window first, and persist the +plan as a **mainspec** plus ordered **slices** on disk. The agent is fed one slice +at a time, so its window stays small and the plan can't decay or compact away. A +spec is the harness's **short-term memory** — a point-in-time plan for one feature, +disposable once the feature ships. It is *not* where durable knowledge lives. + +## Running it unattended: the machine + +[The dispatcher](./concepts/the-dispatcher.md) takes that feature loop and runs it +autonomously. It can be trusted unattended because it keeps **no hidden state**: +every feature's state is observable from files on disk and branches in git. A small +deterministic bash script reads that state each tick and shells out to a fresh +`claude -p` process per step — so no step inherits another's polluted window, crash +recovery is free, and there is **no LLM in the decision path**. It never touches your +checkout; it works your repo's refs and its own sibling worktrees. + +What makes "done" trustworthy is [the output contract](./concepts/output-contract.md): +a runnable definition of done the agent cannot argue with, backed by verification +layers it cannot talk past. When a feature can't finish honestly, the harness stops +and hands you a diagnosis — **STUCK** — instead of faking success. This all runs +across [one harness driving N environments](./concepts/two-tier-architecture.md), +split cleanly between the project-agnostic harness repo and the Software 3.0 pieces +generated per project. + +## Getting better every merge: the flywheel + +A harness that only built features would relearn the same lessons forever. This one +remembers. [Long-term memory](./concepts/long-term-memory.md) — the Expert (lazy, +pulled on demand), `AGENTS.md` (eager, a map into the Expert), and lints (the memory +an agent cannot ship past) — is updated on every merge through a single, auditable +write path. + +That is the start of [continuous improvement](./concepts/continuous-improvement.md), +the idea at the center of the whole system: **you operate on the system that +produces features, not on the features themselves.** Fix a piece of code and you +ship one feature. Fix a context gap and every future feature benefits. You do both, +but context-gap fixes are the main thing, because they compound — and the +ready-to-merge ratio is the metric you drive up. + +## Your job: the human loop + +Strip away the typing and what is left is not less work — it is higher-leverage +work. [The human loop](./concepts/the-human-loop.md) is the part a harness cannot +do: **understand** the problem (`/wiki-init`), express **intent** with a real +definition of done (`/intent`), and **evaluate** what comes back (`/evaluate-pr`, +`/evaluate-sessions`). When you evaluate, you don't just approve work — you improve +the thing that produced it. + +Put it together and your project stops being a codebase you type into and becomes a +harness you tune. A codebase you type into is a constant cost. A harness you tune is +an appreciating asset — each feature leaves it a little more capable of building the +next one. + +## Keep reading + +- Jump into any [core concept](./README.md#core-concepts). +- Follow a [how-to guide](./README.md#how-to-guides) to do something specific. +- Read the [design invariants](./reference/invariants.md) for the proof the machine + is safe to leave running. diff --git a/docs/reference/cli.md b/docs/reference/cli.md new file mode 100644 index 0000000..f27673c --- /dev/null +++ b/docs/reference/cli.md @@ -0,0 +1,55 @@ +# Reference: the `context-specs` CLI + +`context-specs` is the deterministic half of the harness — the CLI that registers +environments, symlinks the canonical skills into them, and supervises the +dispatcher loops. It contains no LLM; it only schedules +[the dispatcher](../concepts/the-dispatcher.md) (`scripts/poll-and-dispatch.sh`). + +``` +Usage: context-specs [args] +``` + +Run `context-specs --help` for the built-in summary. + +## Setup + +| Command | Description | +|---|---| +| `init [name]` | Create (or adopt) a harness repo from the context-specs template. See [Create a harness](../how-to/create-a-harness.md). | +| `add [--name n]` | Register an environment repo: symlink skills, write `.gitignore` entries, add it to `environments.toml`. Then run `/env-init` inside it. See [Add an environment](../how-to/add-an-environment.md). | +| `remove ` | Unregister an environment (leaves its files and state in place). | +| `link ` | (Re-)create the skill/agent symlinks in a repo or worktree. Idempotent; called automatically by `bootstrap-worktree.sh`. | + +## Run + +| Command | Description | +|---|---| +| `start [name\|--all]` | Start the background supervisor (build loop + memory loop). | +| `stop [name\|--all]` | Stop the supervisor. | +| `run [name] [--learn]` | One foreground tick, drained to idle. No daemon. `--learn` runs the memory loop instead of the build loop. | +| `status [--fetch]` | One-shot table: every environment's features and phases. `--fetch` updates refs first. Applies the dispatcher's liveness gate (no merged/closed features). | +| `logs [-f] [--learn\|--supervisor]` | Show (or follow) an environment's logs. | +| `doctor` | Check the harness, the registry, and every environment. | + +See [Run the harness](../how-to/run-the-harness.md) for usage. + +## The scheduling protocol: exit codes + +The CLI schedules the dispatcher and reacts to its exit code — that is the entire +protocol: + +| Exit code | Meaning | Supervisor reaction | +|---|---|---| +| `0` | Idle — no work advanced | Sleep the interval (default 5 min), then tick again | +| `10` | Work advanced | Re-invoke immediately, until the environment is idle | +| non-zero (other) | Error | Back off exponentially | + +A STUCK feature reports idle, so a stuck environment never hot-loops. + +## Related + +- [The dispatcher](../concepts/the-dispatcher.md) — the deterministic engine this + CLI drives. +- [Reference: state and branches](./state-and-branches.md) — where the CLI's state + lives. +- [Reference: skills](./skills.md) — the probabilistic half the CLI hands off to. diff --git a/docs/invariants.md b/docs/reference/invariants.md similarity index 75% rename from docs/invariants.md rename to docs/reference/invariants.md index 367ec52..409e650 100644 --- a/docs/invariants.md +++ b/docs/reference/invariants.md @@ -1,7 +1,7 @@ # Design invariants -This is the reference companion to the [documentation story](./README.md) — -specifically [Chapter 3, The agent harness](./3-the-agent-harness.md), which +This is the reference companion to the [documentation overview](../overview.md) — +specifically [The dispatcher](../concepts/the-dispatcher.md), which gives you the intuition for why the harness is safe to leave running. This document gives you the *proof*: it enumerates the properties the harness holds no matter what crashes, races, or restarts. @@ -24,7 +24,7 @@ break. 3. [Worktree ↔ branch is 1:1 per node](#invariant-3--worktree--branch-is-11-per-node) 4. [Skill idempotency via write-then-touch](#invariant-4--skill-idempotency-via-write-then-touch) 5. [Dispatcher discipline](#invariant-5--dispatcher-discipline) - 6. [Human's working tree is sandboxed](#invariant-6--humans-working-tree-is-sandboxed) + 6. [The developer's working tree is sandboxed](#invariant-6--the-developers-working-tree-is-sandboxed) 7. [Cross-node safety](#invariant-7--cross-node-safety) 8. [Verification is non-bypassable](#invariant-8--verification-is-non-bypassable) 9. [Forward-only state machine](#invariant-9--forward-only-state-machine) @@ -68,9 +68,10 @@ An **invariant** here is a property the system must maintain regardless of crash **How it's enforced.** -- Counters live in `.harness/` files. -- Sentinels are committed to the branch. -- PR state is observed via `gh pr view`. +- Retry counters and runtime sentinels live in the harness repo's + `state//` files (per-environment, gitignored, safely wipeable). +- Pipeline sentinels are committed to the feature branch. +- PR state is observed via `gh pr view` (run with the environment as cwd). - The dispatcher reads everything fresh at the start of every tick. ### Invariant 2 — Branch namespace IS the work registry @@ -97,7 +98,8 @@ An **invariant** here is a property the system must maintain regardless of crash **How it's enforced.** -- Worktree paths are per-feature: `${WORKTREE_BASE}-${feature}`. +- Worktree paths are per-feature siblings of the environment's clone: + `-harness-` (derived from the environment's path). - The advance loop verifies `git rev-parse --abbrev-ref HEAD` matches the iterated branch before acting; skip if not. - The `MAX_WORKTREES` env var caps concurrent worktrees; >1 = parallelism opt-in. @@ -119,22 +121,24 @@ An **invariant** here is a property the system must maintain regardless of crash ### Invariant 5 — Dispatcher discipline -**Statement.** The dispatcher contains zero LLM calls. It executes at most one skill step per branch per tick. Skill invocations (`claude -p`) within a tick are synchronous and sequential. +**Statement.** The dispatcher contains zero LLM calls. It executes at most one skill step per branch per tick. Skill invocations (`claude -p`) within a tick are synchronous and sequential. Its entire scheduling interface is its exit code: `0` = idle, `10` = advanced (the supervisor re-invokes immediately — a *drain*, not a second step within the tick), anything else = error. -**Why it matters.** A dispatcher with LLM in the decision loop is non-reproducible and inflates context cost. Multi-step-per-tick makes "what will the next action be?" unanswerable from disk state. +**Why it matters.** A dispatcher with LLM in the decision loop is non-reproducible and inflates context cost. Multi-step-per-tick makes "what will the next action be?" unanswerable from disk state. The exit-code protocol keeps the supervisor equally deterministic — it holds no opinion about the work, only about *when* to ask again. -**What breaks if violated.** Embedding `claude -p` in the dispatcher's decision logic makes the dispatcher itself a cost surface. Allowing skill chaining within a tick makes the state machine harder to debug. +**What breaks if violated.** Embedding `claude -p` in the dispatcher's decision logic makes the dispatcher itself a cost surface. Allowing skill chaining within a tick makes the state machine harder to debug. Exiting `10` from an error path (or from a STUCK-only tick) makes the supervisor hot-loop a broken environment. **How it's enforced.** - The dispatcher script is pure bash + `git` + `gh`. No `claude -p` outside the skill-invocation positions. - The if/elif chain in the advance loop fires at most one branch per iteration. - `claude -p` invocations are blocking; no `&` backgrounding. -- `flock -n` at the top of the script prevents overlapping dispatcher runs on the same node. +- `flock -n` on the per-environment lock (`state//tick.lock`) prevents overlapping ticks for the same environment. The lock is per-env, not on the script — the script is shared by every environment, and environments must not serialize against each other. +- A `TRANSITIONS` counter (bumped by `run_claude`, the PRD claim, the PR open, the auto-fix commit, and the `.prd-passed` sentinel) drives the final exit; STUCK paths never bump it, so a stuck environment reports idle. +- The build loop and the memory loop (`learn-dispatch.sh`) are two independent loops with separate locks and separate worktrees; they coordinate only through git. -### Invariant 6 — Human's working tree is sandboxed +### Invariant 6 — The developer's working tree is sandboxed -**Statement.** The harness never touches the human's checkout. Worktrees live at separate filesystem paths. `/intent` is the sole carve-out, justified by being a synchronous conversational skill where the human is attentive. +**Statement.** The harness never touches the developer's checked-out files. It operates on the environment's clone only through *ref* operations (`git -C fetch/push/for-each-ref/cat-file`) and through *sibling worktrees* it creates and tears down itself. `/intent` is the sole carve-out, justified by being a synchronous conversational skill the developer runs themselves, attentively, in their own checkout. **Why it matters.** This is the trust invariant. If the harness ever stomps WIP, social trust in the design collapses regardless of how clean the technical model is. @@ -142,11 +146,13 @@ An **invariant** here is a property the system must maintain regardless of crash **How it's enforced.** -- Worktree paths are `${WORKTREE_BASE}-${feature}`, separate from the human's `.git`-containing checkout. -- The dispatcher only runs in worktrees, never in the human's checkout. -- `/intent` is invoked manually in the human's checkout by the human; no automation triggers it. +- The dispatcher lives in the **harness repo**, not the environment — there is no harness code inside the project to run "in place," and its cwd is never the developer's checkout. +- Every environment operation is either a ref op (fetch, push, cat-file, for-each-ref — none touch a working tree) or scoped to a `-harness-*` sibling worktree (`git -C "$wt"`, `cd "$wt"`). The wipe/reset/clean commands run only inside per-feature worktrees. +- The one filesystem write into the environment's checkout is the gitignored `.claude/` symlinks — created by `context-specs add`/`link`, which the developer runs themselves. +- `bootstrap-worktree.sh` copies secrets FROM the developer's checkout INTO worktrees — never the reverse, never deletes. +- Worktree creation does add refs to the shared `.git` (a local `feature/` branch appears in the developer's `git branch` output, and is deleted again at cleanup) — ref noise, by design never working-tree changes. -In server mode this invariant becomes trivial (no human-checkout on the server). It remains stated because the invariants are mode-independent. +In server mode this invariant becomes trivial (no developer checkout on the server — the "clone" is the harness's own). It remains stated because the invariants are mode-independent. ### Invariant 7 — Cross-node safety @@ -199,12 +205,12 @@ The invariants above are stated mode-independently. The OS-level enforcement mec | Invariant | Local enforcement | Server enforcement | Notes | |---|---|---|---| -| 1 — State on disk | `.harness/` files + committed sentinels | Same, on the runner's checkout | Identical | +| 1 — State on disk | harness repo `state//` + committed sentinels | Same, on the runner's checkout | Identical | | 2 — Branch as registry | Atomic git ref ops on origin | Same | Same primitive; lower contention server-side | | 3 — Worktree ↔ branch 1:1 | `git worktree add` + HEAD guard | Each runner job = fresh checkout; implicit 1:1 | Server gets it "for free" via job ephemerality | | 4 — Skill idempotency | Write-then-touch in skill code | Same | Identical | -| 5 — Dispatcher discipline | Pure bash + `flock -n` | GH Actions workflow + `concurrency:` key per branch | `flock` and `concurrency:` serve the same role | -| 6 — Human sandbox | Worktree at separate path | N/A — no human on server | Becomes trivial server-side; remains stated for portability | +| 5 — Dispatcher discipline | Pure bash + per-env `flock -n` + exit-code protocol | GH Actions workflow + `concurrency:` key per branch | `flock` and `concurrency:` serve the same role | +| 6 — Developer sandbox | Ref-only ops on the clone + sibling worktrees | N/A — no developer checkout on server | Becomes trivial server-side; remains stated for portability | | 7 — Cross-node safety | Atomic ops + ls-remote checks | Same; usually N=1 on server | Same primitives, lower contention | | 8 — Verification non-bypass | Dispatcher invokes runner | Workflow step invokes runner | Identical | | 9 — Forward-only state | Skills don't un-touch | Same | Identical | @@ -212,7 +218,7 @@ The invariants above are stated mode-independently. The OS-level enforcement mec A few mode-specific notes worth carrying with you: - **`/intent` always runs locally.** It's conversational; needs a human at a terminal. Server picks up after the `prd/` branch is pushed. Universal, not a mode difference. -- **Counter location.** Per-node `.harness/` files. On server (N=1) this is effectively per-branch since one server owns each branch's lifetime. On local with multi-node handoff, counters reset on the new machine — see § 4. +- **Counter location.** Per-harness `state//` files. On server (N=1) this is effectively per-branch since one server owns each branch's lifetime. On local with multi-node handoff, counters reset on the new machine — see § 4. - **`flock` ↔ `concurrency:`.** Same role: serialize ticks against the same work unit. - **Wipe at tick start ↔ fresh checkout per run.** Same semantics; uncommitted state has zero lifetime across runs in either mode. - **Cross-node primitives cost nothing when N=1.** Leave them in regardless of mode — they're the upgrade path. @@ -247,18 +253,18 @@ Five decisions in the design are choices, not invariants. Each can be flipped pe **When to flip.** Team projects with multiple concurrent features and surplus disk + CI capacity. -### Counter location — per-node `.harness/` (chosen) vs per-branch committed +### Counter location — per-harness `state//` (chosen) vs per-branch committed **Alternative.** Counters committed to the feature branch itself (one commit per round). -**Why per-node.** Works for both pure-local single-dev and pure-server N=1. The handoff case (laptop dies mid-PR-debate, resume on desktop) is the only place per-node hurts, and the failure mode is "extra LLM rounds," not "wrong behavior." +**Why per-harness.** Works for both pure-local single-dev and pure-server N=1. The handoff case (laptop dies mid-PR-debate, resume on desktop) is the only place per-node hurts, and the failure mode is "extra LLM rounds," not "wrong behavior." **When to flip.** Shared-pool concurrency (multiple harnesses cooperating on the same branch) or scenarios where machine handoff is common and the cost of extra rounds matters. ### `feature/*` ownership contract — PRD-file-committed (chosen) vs loose match **Alternative (loose).** Pick up any `feature/*` on origin. -**Alternative (local file).** Record claims in `.harness/claimed`. +**Alternative (local file).** Record claims in a local state file. **Why PRD-file-committed.** Stateless, cross-node-safe, no per-node tracking. Also gives a clean opt-in mechanism for non-`/intent` work — commit a PRD stub on a branch and the harness picks it up. @@ -298,19 +304,21 @@ Each walkthrough names the failure, traces what happens tick by tick, and points ### Two ticks fire concurrently -**Setup.** `/loop 5m /poll-and-dispatch` and `CronCreate` both configured by mistake; both fire at the same minute. +**Setup.** The `context-specs start` supervisor fires a tick at the same moment a human runs `context-specs run` by hand for the same environment. **Trace.** -1. Tick A acquires flock first. -2. Tick B's `flock -n` fails; script exits 0. -3. Tick A proceeds normally. +1. Tick A acquires the per-env `flock` on `state//tick.lock` first. +2. Tick B's `flock -n` fails; script exits 0 (idle). +3. Tick A proceeds normally. (A tick for a *different* environment holds a + different lock file and is unaffected — environments never serialize + against each other.) **Invariants composed.** 5. ### Two harnesses race on the same PRD -**Setup.** Alice runs `/loop` on her laptop and also has an open Claude Code session on her desktop. Both see `prd/alice/foo`. +**Setup.** Alice runs `context-specs start` on her laptop and forgot a running supervisor on her desktop. Both see `prd/alice/foo`. **Trace.** @@ -349,12 +357,12 @@ Each walkthrough names the failure, traces what happens tick by tick, and points ### Human laptop dies mid-PR-review -**Setup.** Alice's laptop is at `feedback-rounds-foo=3` (two away from STUCK cap of 5). Laptop is bricked. Alice clones fresh on her desktop and runs `/loop`. +**Setup.** Alice's laptop is at `feedback-rounds-foo=3` (two away from STUCK cap of 5). Laptop is bricked. Alice clones her harness repo and the project fresh on her desktop, runs `context-specs add` + `start`. **Tick fires on desktop.** 1. Re-attach: `feature/foo` exists, has PRD, no local worktree — `git worktree add` creates it. -2. Advance: `.harness/feedback-rounds-foo` doesn't exist on the new machine; effective counter is 0. +2. Advance: `state//feedback-rounds-foo` doesn't exist on the new machine; effective counter is 0. 3. If a reviewer finding is unresolved, `/address-feedback` is invoked with counter incremented to 1 (not 4). **Result.** Alice gets the full 5 rounds again; extra LLM rounds, never broken correctness. This is the per-node counter limitation called out in § 4. Mitigation if it matters: commit counters to the branch (per-branch alternative). @@ -365,6 +373,6 @@ Each walkthrough names the failure, traces what happens tick by tick, and points ## See also -- [The documentation story](./README.md) — the narrative these properties underpin, starting from context engineering. -- [Chapter 3 — The agent harness](./3-the-agent-harness.md) — the intuition for why the harness is safe; this document is its proof. -- [`poll-and-dispatch.sh`](../skills/harness/harness-init/assets/poll-and-dispatch.sh) — the dispatcher: the canonical realization of these invariants in code. +- [Documentation overview](../overview.md) — the narrative these properties underpin, starting from context engineering. +- [The dispatcher](../concepts/the-dispatcher.md) — the intuition for why the harness is safe; this document is its proof. +- [`scripts/poll-and-dispatch.sh`](../../scripts/poll-and-dispatch.sh) — the dispatcher: the canonical realization of these invariants in code. diff --git a/docs/reference/skills.md b/docs/reference/skills.md new file mode 100644 index 0000000..3d117f5 --- /dev/null +++ b/docs/reference/skills.md @@ -0,0 +1,59 @@ +# Reference: the pieces + +Context Specs is a deterministic CLI and dispatcher plus a set of Agent Skills. This +is the catalog. The CLI and dispatchers are the harness's deterministic half; the +skills are its probabilistic half. + +## Deterministic half + +| Where | Piece | Does | +|---|---|---| +| **CLI** (`bin/context-specs`) | `init` / `add` / `link` / `remove` | Create the harness repo; register environments; symlink skills | +| | `start` / `stop` / `run` | Supervise the loops (drain-on-advance) / one foreground pass | +| | `status` / `logs` / `doctor` | Observe every environment; check the wiring | +| **Dispatchers** (`scripts/`) | `poll-and-dispatch.sh` | The build loop: PRD → PR, one deterministic step per tick | +| | `learn-dispatch.sh` | The memory loop: merged diff → `learn/` PR | + +See [Reference: the CLI](./cli.md) for full command detail. + +## Probabilistic half — Agent Skills + +### SDD skills (`skills/sdd/`) — short-term memory + +| Skill | Does | +|---|---| +| [`/spec-planning`](../../skills/sdd/spec-planning/SKILL.md) | PRD → mainspec + temporal slices | +| [`/spec-validate`](../../skills/sdd/spec-validate/SKILL.md) | Multi-agent consensus review of the plan | +| [`/implement-mainspec`](../../skills/sdd/implement-mainspec/SKILL.md) | Orchestrate slices in dependency order | +| [`/implement-slice`](../../skills/sdd/implement-slice/SKILL.md) | Implement one slice: write, unit-test, Reflect | + +See [Spec-Driven Development](../concepts/spec-driven-development.md). + +### Harness skills (`skills/harness/`) + +| Skill | Does | +|---|---| +| [`/env-init`](../../skills/harness/env-init/SKILL.md) | Guided setup of a project as an environment (the Software 3.0 half) | +| [`/fix-local-checks`](../../skills/harness/fix-local-checks/SKILL.md) | Honest fixes for a failing pre-PR gate | +| [`/address-feedback`](../../skills/harness/address-feedback/SKILL.md) | Triage and answer reviewer findings | +| [`/learn`](../../skills/harness/learn/SKILL.md) | Post-merge long-term memory update | + +See [Long-term memory](../concepts/long-term-memory.md). + +### Human-loop skills (`skills/human-loop/`) + +| Skill | Does | +|---|---| +| [`/wiki-init`](../../skills/human-loop/wiki-init/SKILL.md) | Stand up a Karpathy LLM-wiki (Understanding) | +| [`/intent`](../../skills/human-loop/intent/SKILL.md) | Idea → PRD + runnable definition of done (project-owned) | +| [`/evaluate-pr`](../../skills/human-loop/evaluate-pr/SKILL.md) | Evaluate the change; build understanding | +| [`/evaluate-sessions`](../../skills/human-loop/evaluate-sessions/SKILL.md) | Evaluate the build trail; capture evals | + +See [The human loop](../concepts/the-human-loop.md). + +## Project-owned vs. harness-owned + +Most skills are symlinked from the harness repo into each environment (gitignored). +Two are deliberately project-owned — committed to the environment and tuned per +project: **`/intent`** and the **Expert**. See +[the two-tier architecture](../concepts/two-tier-architecture.md#the-two-developer-owned-levers). diff --git a/docs/reference/state-and-branches.md b/docs/reference/state-and-branches.md new file mode 100644 index 0000000..8d50c49 --- /dev/null +++ b/docs/reference/state-and-branches.md @@ -0,0 +1,70 @@ +# Reference: state and branches + +The harness keeps **no hidden state**. Everything it knows is observable from two +places: the branches in git and the files on disk. This page documents both. For +why this design makes the harness safe to leave running, see +[the dispatcher](../concepts/the-dispatcher.md) and the +[design invariants](./invariants.md). + +## The branch namespace is the work queue + +There is no separate database of "what work exists." The branches *are* the +registry: + +| Branch | Role | +|---|---| +| `prd//` | `/intent` output — the waiting queue. An atomic rename to `feature/` is how the harness claims it. | +| `feature/` | The active implementation lane. | +| `main` | Humans merge here. Never pushed to directly. | +| `learn/` | Auto-generated memory update from the memory loop. | + +Claiming a feature is a single atomic git operation — renaming +`prd//` to `feature/`. If two harnesses ever race for the +same work, whichever pushes the rename first wins and the other moves on. No lock +service, no coordination daemon — git refs are the lock. + +By default each developer's harness watches only their own `prd//*` queue, +so your harness is your personal assistant, not a shared teammate. + +## Worktrees + +The harness never touches your checkout. It does all its work in **sibling +worktrees** it creates and tears down itself, named `-harness-`. Your +uncommitted work is structurally out of its reach. The worktree is set up by +`scripts/bootstrap-worktree.sh`, which also re-links the skill symlinks (they are +wiped with the worktree on each tick). + +## On-disk layout + +Following the [two-tier architecture](../concepts/two-tier-architecture.md): + +### Harness repo (tier 1) + +``` +bin/context-specs the CLI +scripts/ the dispatchers (poll-and-dispatch.sh, learn-dispatch.sh, harness-lib.sh) +skills/ canonical skills (symlinked into environments) +state// per-environment runtime state (logs, locks, supervisor bookkeeping) +environments.toml the registry of registered environments +``` + +### Environment repo (tier 2) + +``` +AGENTS.md the eager contract (a map into the Expert) +.claude/skills/intent/ project-owned /intent (committed) +.claude/skills/expert/ long-term memory (committed) +.claude/skills/* canonical skills (symlinked, gitignored) +scripts/ bootstrap-worktree.sh, local-checks.sh +prds// prd.md + run-prd-test.sh +specs// mainspec.md + slices/ (short-term memory) +evals/ captured regression checks over skills and context +.harness/env this environment's dials +``` + +## Related + +- [The dispatcher](../concepts/the-dispatcher.md) — reads this state fresh each + tick. +- [Design invariants](./invariants.md) — the properties this layout guarantees. +- [Reference: the CLI](./cli.md) — the commands that operate on this state. diff --git a/environments.example.toml b/environments.example.toml new file mode 100644 index 0000000..f8fb3a3 --- /dev/null +++ b/environments.example.toml @@ -0,0 +1,10 @@ +# environments.toml — the per-machine registry of environments this harness +# operates on. Your real environments.toml is GITIGNORED (paths differ per +# developer); this example documents the shape. You never edit it by hand: +# `context-specs add ` and `context-specs remove ` manage it. +# +# [[environment]] +# name = "myapp" # unique; defaults to the directory basename +# path = "/home/you/code/myapp" # absolute path to your clone +# enabled = true # false = skipped by start --all and status +# interval = 300 # optional per-env build-tick interval (seconds) diff --git a/install-agents.sh b/install-agents.sh deleted file mode 100755 index 2f31868..0000000 --- a/install-agents.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -REPO_URL="https://github.com/capitalone/context-specs.git" -TEMP_DIR="" - -cleanup() { - if [ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ]; then - rm -rf "$TEMP_DIR" - fi -} -trap cleanup EXIT - -echo "Installing context-specs agents..." - -# Clone into a temp directory -TEMP_DIR="$(mktemp -d)" -git clone --depth 1 --quiet "$REPO_URL" "$TEMP_DIR" - -# Verify source agents exist -SOURCE_DIR="$TEMP_DIR/subagents" -if [ ! -d "$SOURCE_DIR" ]; then - echo "Error: No subagents/ directory found in the repository." >&2 - exit 1 -fi - -# Create target directory -mkdir -p .claude/agents - -# Copy agent definitions -INSTALLED=() -for file in "$SOURCE_DIR"/*.md; do - [ -f "$file" ] || continue - cp "$file" ".claude/agents/$(basename "$file")" - INSTALLED+=("$(basename "$file")") -done - -if [ ${#INSTALLED[@]} -eq 0 ]; then - echo "No agent definitions found to install." - exit 0 -fi - -echo "" -echo "Installed ${#INSTALLED[@]} agent(s) into .claude/agents/:" -for name in "${INSTALLED[@]}"; do - echo " - $name" -done -echo "" -echo "Done." diff --git a/lib/add.js b/lib/add.js new file mode 100644 index 0000000..00cf15f --- /dev/null +++ b/lib/add.js @@ -0,0 +1,56 @@ +'use strict'; +// `context-specs add ` — the deterministic half of environment setup. +// Registers the repo, symlinks the canonical skills/agents into it, and writes +// the managed .gitignore block. The probabilistic half — AGENTS.md, +// bootstrap-worktree.sh, local-checks, the Expert skeleton, a project-owned +// /intent — is /env-init's job, run by the developer in a Claude session +// inside the environment afterward. + +const fs = require('fs'); +const path = require('path'); +const registry = require('./registry'); +const { linkEnv, ensureGitignore } = require('./link'); +const { logsDir } = require('./paths'); +const { git } = require('./util'); + +module.exports = async function add(args) { + const positional = args.filter((a) => !a.startsWith('--')); + const nameFlag = args.includes('--name') ? args[args.indexOf('--name') + 1] : null; + const target = positional[0]; + if (!target) throw new Error('usage: context-specs add [--name ]'); + + const envPath = path.resolve(target); + if (!fs.existsSync(envPath)) throw new Error(`no such directory: ${envPath}`); + if (git(envPath, ['rev-parse', '--git-dir']) === null) { + throw new Error(`${envPath} is not a git repository`); + } + if (git(envPath, ['remote', 'get-url', 'origin']) === null) { + console.warn('warn: no `origin` remote — the harness claims PRDs and opens PRs through origin; add one before starting the loops.'); + } + + const name = nameFlag || path.basename(envPath); + const envs = registry.load(); + const existing = envs.find((e) => e.name === name); + if (existing && existing.path !== envPath) { + throw new Error(`name '${name}' is already registered for ${existing.path} — pass --name to disambiguate`); + } + + const { linked, agents, ejected } = linkEnv(envPath); + ensureGitignore(envPath); + if (!existing) { + envs.push({ name, path: envPath, enabled: true }); + registry.save(envs); + } + fs.mkdirSync(logsDir(name), { recursive: true }); + + console.log(`Registered environment '${name}' → ${envPath}`); + console.log(` linked ${linked.length} skills + ${agents.length} agents (gitignored symlinks)`); + if (ejected.length) console.log(` ejected (project-owned): ${ejected.join(', ')}`); + console.log(''); + console.log('Next: the Software 3.0 half. In the environment, run the setup skill:'); + console.log(` cd ${envPath}`); + console.log(' claude'); + console.log(' /env-init'); + console.log(''); + console.log("Once its PR is merged: 'context-specs start' (or 'run' for one foreground pass)."); +}; diff --git a/lib/doctor.js b/lib/doctor.js new file mode 100644 index 0000000..1598993 --- /dev/null +++ b/lib/doctor.js @@ -0,0 +1,109 @@ +'use strict'; +// Health checks for the harness repo, the registry, and every registered +// environment. Absorbs the old harness-init preflight.sh, plus the failure +// modes the two-tier split introduces (broken symlinks after a move, leftover +// single-repo-era artifacts). Report-only: prints [ok]/[warn]/[MISS], never +// mutates. Exits 1 if any [MISS]. + +const fs = require('fs'); +const path = require('path'); +const registry = require('./registry'); +const { HOME, scriptsDir, stateRoot, registryPath } = require('./paths'); +const { tryRun, git } = require('./util'); +const { canonicalSkills, canonicalAgents, GITIGNORE_START } = require('./link'); + +let missed = false; +const ok = (m) => console.log(` [ok] ${m}`); +const warn = (m) => console.log(` [warn] ${m}`); +const miss = (m) => { console.log(` [MISS] ${m}`); missed = true; }; + +function checkHarness() { + console.log(`Harness repo: ${HOME}`); + const major = parseInt(process.versions.node.split('.')[0], 10); + major >= 18 ? ok(`node ${process.versions.node}`) : miss(`node >=18 required (found ${process.versions.node})`); + + for (const [cmd, why, fatal] of [ + ['bash', 'runs the dispatcher', true], + ['git', 'everything', true], + ['gh', 'PR creation, review polling, STUCK signals', true], + ['claude', "the dispatcher shells out to 'claude -p'", true], + ['flock', 'tick serialization (second line of defense behind the supervisor)', false], + ['uuidgen', 'session ids (falls back to /proc or a pseudo-id)', false], + ]) { + if (tryRun('sh', ['-c', `command -v ${cmd}`]) !== null) ok(`${cmd} on PATH`); + else (fatal ? miss : warn)(`${cmd} not on PATH — ${why}`); + } + if (tryRun('gh', ['auth', 'status']) !== null) ok('gh authenticated'); + else warn('gh not authenticated (gh auth login) — PR operations will fail'); + + for (const s of ['poll-and-dispatch.sh', 'learn-dispatch.sh']) { + const f = path.join(scriptsDir, s); + if (!fs.existsSync(f)) miss(`${s} missing from scripts/`); + else if (!(fs.statSync(f).mode & 0o100)) miss(`${s} not executable (chmod +x)`); + else ok(`scripts/${s} executable`); + } + fs.existsSync(path.join(scriptsDir, 'harness-lib.sh')) ? ok('scripts/harness-lib.sh present') : miss('scripts/harness-lib.sh missing'); + + try { fs.mkdirSync(stateRoot, { recursive: true }); fs.accessSync(stateRoot, fs.constants.W_OK); ok('state/ writable'); } + catch { miss('state/ not writable'); } + + if (!fs.existsSync(registryPath)) warn("no environments.toml yet — run 'context-specs add '"); + else { try { registry.load(); ok('environments.toml parses'); } catch (e) { miss(`environments.toml unreadable: ${e.message}`); } } +} + +function checkEnv(env) { + console.log(`\nEnvironment '${env.name}': ${env.path}`); + if (!fs.existsSync(env.path)) { miss('path does not exist'); return; } + if (git(env.path, ['rev-parse', '--git-dir']) === null) { miss('not a git repository'); return; } + git(env.path, ['remote', 'get-url', 'origin']) !== null ? ok('origin remote') : miss('no origin remote — the harness works entirely through origin'); + + // Tier-2 artifacts (created by /env-init). + fs.existsSync(path.join(env.path, '.harness', 'env')) ? ok('.harness/env config') : warn('.harness/env missing — run /env-init in this environment'); + fs.existsSync(path.join(env.path, 'AGENTS.md')) ? ok('AGENTS.md') : warn('AGENTS.md missing — run /env-init'); + const bootstrap = path.join(env.path, 'scripts', 'bootstrap-worktree.sh'); + if (!fs.existsSync(bootstrap)) warn('scripts/bootstrap-worktree.sh missing — fresh worktrees will not be provisioned or re-linked'); + else if (!fs.readFileSync(bootstrap, 'utf8').includes('context-specs')) warn('bootstrap-worktree.sh lacks the `context-specs link` header — fresh worktrees will miss the tier-1 skills'); + else ok('bootstrap-worktree.sh with link header'); + fs.existsSync(path.join(env.path, '.claude', 'skills', 'intent', 'SKILL.md')) ? ok('project-owned /intent') : warn('/intent not installed — run /env-init'); + fs.existsSync(path.join(env.path, '.claude', 'skills', 'expert', 'SKILL.md')) ? ok('Expert (long-term memory)') : warn('Expert skeleton not seeded — run /env-init'); + + // Symlink integrity for every canonical skill/agent. + let ejected = []; + for (const { name, dir } of canonicalSkills()) { + const lp = path.join(env.path, '.claude', 'skills', name); + let st = null; + try { st = fs.lstatSync(lp); } catch {} + if (!st) miss(`skill '${name}' not linked — run 'context-specs link ${env.path}'`); + else if (!st.isSymbolicLink()) ejected.push(name); + else if (fs.realpathSync(lp) !== fs.realpathSync(dir)) warn(`skill '${name}' links outside this harness (${fs.readlinkSync(lp)})`); + } + for (const { name, file } of canonicalAgents()) { + const lp = path.join(env.path, '.claude', 'agents', name); + let st = null; + try { st = fs.lstatSync(lp); } catch {} + if (!st) miss(`agent '${name}' not linked — run 'context-specs link ${env.path}'`); + else if (st.isSymbolicLink() && fs.realpathSync(lp) !== fs.realpathSync(file)) warn(`agent '${name}' links outside this harness`); + } + if (ejected.length) ok(`ejected (project-owned forks): ${ejected.join(', ')}`); + const gi = path.join(env.path, '.gitignore'); + fs.existsSync(gi) && fs.readFileSync(gi, 'utf8').includes(GITIGNORE_START) + ? ok('managed .gitignore block') + : warn("managed .gitignore block missing — re-run 'context-specs add'"); + + // Leftovers from the single-repo era (pre-two-tier installs). + const legacy = ['scripts/poll-and-dispatch.sh', 'scripts/harness-tick.sh', 'scripts/harness-lib.sh', 'scripts/learn-tick.sh', + '.claude/skills/poll-and-dispatch', '.claude/skills/learn-loop'] + .filter((f) => fs.existsSync(path.join(env.path, f))); + if (legacy.length) { + warn(`legacy single-repo harness artifacts found (two dispatchers would confuse, though claims stay atomic): ${legacy.join(', ')}`); + warn(' remove them in the environment: git rm ' + legacy.join(' ')); + } +} + +module.exports = async function doctor() { + checkHarness(); + for (const env of registry.load()) checkEnv(env); + console.log(''); + if (missed) { console.log('doctor: problems found (see [MISS] above)'); process.exit(1); } + console.log('doctor: healthy'); +}; diff --git a/lib/init.js b/lib/init.js new file mode 100644 index 0000000..50323d8 --- /dev/null +++ b/lib/init.js @@ -0,0 +1,66 @@ +'use strict'; +// `context-specs init [name]` — create (or adopt) a HARNESS repo. +// +// The context-specs repo doubles as the template: init clones it, renames +// origin → upstream (so `git pull upstream main` is the update path and origin +// stays free for the user's own remote), and prepares the per-machine pieces +// (registry, state/). Run with no args inside an existing clone, it "adopts" +// that clone as a harness instead. + +const fs = require('fs'); +const path = require('path'); +const { execFileSync } = require('child_process'); +const { HOME, registryPath, registryExamplePath } = require('./paths'); +const { tryRun, git, confirm } = require('./util'); + +const TEMPLATE_URL = process.env.CONTEXT_SPECS_TEMPLATE || 'https://github.com/capitalone/context-specs.git'; + +async function adopt(root) { + // Point `upstream` at the template so `context-specs`/git updates have a home. + const remotes = (git(root, ['remote']) || '').split('\n').filter(Boolean); + if (!remotes.includes('upstream')) { + const originUrl = git(root, ['remote', 'get-url', 'origin']); + if (originUrl && originUrl.replace(/\.git$/, '') === TEMPLATE_URL.replace(/\.git$/, '')) { + git(root, ['remote', 'rename', 'origin', 'upstream']); + console.log('renamed origin → upstream (the template; your own remote can take origin)'); + } else { + git(root, ['remote', 'add', 'upstream', TEMPLATE_URL]); + console.log(`added upstream → ${TEMPLATE_URL}`); + } + } + if (!fs.existsSync(path.join(root, 'environments.toml'))) { + const example = path.join(root, 'environments.example.toml'); + fs.writeFileSync(path.join(root, 'environments.toml'), + fs.existsSync(example) ? fs.readFileSync(example, 'utf8') : '# managed by `context-specs add`\n'); + } + fs.mkdirSync(path.join(root, 'state'), { recursive: true }); + console.log(`harness ready at ${root}`); + + if (git(root, ['remote', 'get-url', 'origin']) === null && tryRun('sh', ['-c', 'command -v gh']) !== null) { + if (await confirm('Create a private GitHub repo for this harness (gh repo create)?')) { + try { + execFileSync('gh', ['repo', 'create', path.basename(root), '--private', '--source', root, '--push'], { stdio: 'inherit' }); + } catch { + console.log('gh repo create failed — you can create and push a remote later.'); + } + } + } + console.log("\nNext: register your first environment — 'context-specs add '"); +} + +module.exports = async function init(args) { + const name = args[0]; + // Adopt mode: we ARE running from inside a harness clone (the CLI's own HOME). + if (!name) { + await adopt(HOME); + return; + } + const dest = path.resolve(name); + if (fs.existsSync(dest)) throw new Error(`${dest} already exists`); + console.log(`cloning ${TEMPLATE_URL} → ${dest}`); + execFileSync('git', ['clone', '--quiet', TEMPLATE_URL, dest], { stdio: 'inherit' }); + git(dest, ['remote', 'rename', 'origin', 'upstream']); + // Hand off to the CLONE's own CLI so paths resolve inside it. + execFileSync(process.execPath, [path.join(dest, 'bin', 'context-specs'), 'init'], { stdio: 'inherit' }); + console.log(`\ncd ${name}`); +}; diff --git a/lib/link.js b/lib/link.js new file mode 100644 index 0000000..eeb05f8 --- /dev/null +++ b/lib/link.js @@ -0,0 +1,115 @@ +'use strict'; +// The skill-delivery engine. Tier-1 skills reach an environment as SYMLINKS in +// its .claude/skills/ (gitignored — Claude Code follows skill-directory +// symlinks). Two callers: +// - `context-specs add ` at registration time, and +// - the deterministic header of the environment's bootstrap-worktree.sh for +// every fresh worktree (gitignored symlinks do not materialize in new git +// worktrees, so each one must be re-linked). +// Idempotent and fast by construction. +// +// Eject-by-shadowing: if the target already has a REAL directory (or file) of +// the same name, it is the project's own committed fork — skip it and report +// it as "ejected". Deleting the committed copy re-adopts the canonical skill on +// the next link. + +const fs = require('fs'); +const path = require('path'); +const { skillsDir, subagentsDir } = require('./paths'); + +// /intent is NOT symlinked: it is a Software 3.0 artifact — /env-init copies it +// into the environment (verbatim or customized) as a committed, project-owned +// skill. It is one of the two developer-owned levers (with the Expert). +const EXCLUDED_SKILLS = new Set(['intent']); + +// Enumerate canonical skills: skills///SKILL.md → flat . +function canonicalSkills() { + const out = []; + for (const group of fs.readdirSync(skillsDir)) { + const groupDir = path.join(skillsDir, group); + if (!fs.statSync(groupDir).isDirectory()) continue; + for (const name of fs.readdirSync(groupDir)) { + const dir = path.join(groupDir, name); + if (!EXCLUDED_SKILLS.has(name) && fs.existsSync(path.join(dir, 'SKILL.md'))) { + out.push({ name, dir }); + } + } + } + return out.sort((a, b) => a.name.localeCompare(b.name)); +} + +function canonicalAgents() { + if (!fs.existsSync(subagentsDir)) return []; + return fs.readdirSync(subagentsDir) + .filter((f) => f.endsWith('.md')) + .map((f) => ({ name: f, file: path.join(subagentsDir, f) })); +} + +// Create/refresh one symlink; returns 'linked' | 'ejected'. +function ensureLink(target, linkPath) { + let st = null; + try { st = fs.lstatSync(linkPath); } catch {} + if (st && !st.isSymbolicLink()) return 'ejected'; // real dir/file = project-owned fork + if (st) fs.unlinkSync(linkPath); // stale/moved symlink: heal it + fs.symlinkSync(target, linkPath); + return 'linked'; +} + +function linkEnv(envPath) { + const root = path.resolve(envPath); + if (!fs.existsSync(root)) throw new Error(`no such directory: ${root}`); + const skillsTarget = path.join(root, '.claude', 'skills'); + const agentsTarget = path.join(root, '.claude', 'agents'); + fs.mkdirSync(skillsTarget, { recursive: true }); + fs.mkdirSync(agentsTarget, { recursive: true }); + + const linked = []; + const ejected = []; + for (const { name, dir } of canonicalSkills()) { + const result = ensureLink(dir, path.join(skillsTarget, name)); + (result === 'linked' ? linked : ejected).push(name); + } + const agents = []; + for (const { name, file } of canonicalAgents()) { + const result = ensureLink(file, path.join(agentsTarget, name)); + (result === 'linked' ? agents : ejected).push(name); + } + return { linked, agents, ejected }; +} + +// The env-repo .gitignore entries for everything link creates. The block is +// marker-delimited so add can rewrite it idempotently. +const GITIGNORE_START = '# >>> context-specs (managed by `context-specs add` — do not edit inside markers) >>>'; +const GITIGNORE_END = '# <<< context-specs <<<'; + +function gitignoreBlock() { + const lines = [GITIGNORE_START]; + for (const { name } of canonicalSkills()) lines.push(`.claude/skills/${name}`); + for (const { name } of canonicalAgents()) lines.push(`.claude/agents/${name}`); + lines.push(GITIGNORE_END); + return lines.join('\n'); +} + +function ensureGitignore(envPath) { + const file = path.join(envPath, '.gitignore'); + const block = gitignoreBlock(); + let text = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''; + const start = text.indexOf(GITIGNORE_START); + const end = text.indexOf(GITIGNORE_END); + if (start !== -1 && end !== -1) { + text = text.slice(0, start) + block + text.slice(end + GITIGNORE_END.length); + } else { + text = text.replace(/\n*$/, text ? '\n\n' : '') + block + '\n'; + } + fs.writeFileSync(file, text); +} + +async function cli(args) { + const target = args[0]; + if (!target) throw new Error('usage: context-specs link '); + const { linked, agents, ejected } = linkEnv(target); + console.log(`linked ${linked.length} skills + ${agents.length} agents into ${path.resolve(target)}/.claude/`); + if (ejected.length) console.log(`ejected (project-owned, left alone): ${ejected.join(', ')}`); +} + +module.exports = { linkEnv, canonicalSkills, canonicalAgents, ensureGitignore, gitignoreBlock, cli, GITIGNORE_START, GITIGNORE_END }; diff --git a/lib/logs.js b/lib/logs.js new file mode 100644 index 0000000..313f59b --- /dev/null +++ b/lib/logs.js @@ -0,0 +1,20 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); +const { spawn } = require('child_process'); +const registry = require('./registry'); +const { logsDir } = require('./paths'); + +module.exports = async function logs(args) { + const follow = args.includes('-f') || args.includes('--follow'); + const which = args.includes('--learn') ? 'learn.log' + : args.includes('--supervisor') ? 'supervisor.log' : 'build.log'; + const env = registry.resolveOne(args.find((a) => !a.startsWith('-'))); + const file = path.join(logsDir(env.name), which); + if (!fs.existsSync(file)) { console.log(`no ${which} yet for '${env.name}' (${file})`); return; } + if (follow) { + spawn('tail', ['-n', '50', '-f', file], { stdio: 'inherit' }); + return new Promise(() => {}); // tail owns the terminal until Ctrl-C + } + process.stdout.write(fs.readFileSync(file, 'utf8')); +}; diff --git a/lib/paths.js b/lib/paths.js new file mode 100644 index 0000000..4b04f3b --- /dev/null +++ b/lib/paths.js @@ -0,0 +1,20 @@ +'use strict'; +const path = require('path'); + +// The harness repo root. The CLI always lives at /bin + /lib, +// so its own location IS the harness location — no lookup, no env var needed. +const HOME = path.resolve(__dirname, '..'); + +module.exports = { + HOME, + scriptsDir: path.join(HOME, 'scripts'), + skillsDir: path.join(HOME, 'skills'), + subagentsDir: path.join(HOME, 'subagents'), + registryPath: path.join(HOME, 'environments.toml'), + registryExamplePath: path.join(HOME, 'environments.example.toml'), + stateRoot: path.join(HOME, 'state'), + stateDir: (name) => path.join(HOME, 'state', name), + logsDir: (name) => path.join(HOME, 'state', name, 'logs'), + pidFile: (name) => path.join(HOME, 'state', name, 'supervisor.pid'), + binPath: path.join(HOME, 'bin', 'context-specs'), +}; diff --git a/lib/registry.js b/lib/registry.js new file mode 100644 index 0000000..2e15df4 --- /dev/null +++ b/lib/registry.js @@ -0,0 +1,75 @@ +'use strict'; +// environments.toml — the per-machine registry of environments this harness +// operates on. Gitignored (paths differ per developer); written only by +// `add`/`remove`, never hand-edited in normal use. Deliberately a tiny TOML +// subset so the CLI stays zero-dependency: +// +// [[environment]] +// name = "myapp" +// path = "/abs/path/to/myapp" +// enabled = true + +const fs = require('fs'); +const { registryPath } = require('./paths'); + +function parse(text) { + const envs = []; + let cur = null; + for (const raw of text.split('\n')) { + const line = raw.trim(); + if (!line || line.startsWith('#')) continue; + if (line === '[[environment]]') { cur = { enabled: true }; envs.push(cur); continue; } + const m = line.match(/^(\w+)\s*=\s*(.+)$/); + if (!m || !cur) continue; + let [, key, value] = m; + value = value.trim(); + if (value === 'true') value = true; + else if (value === 'false') value = false; + else value = value.replace(/^"(.*)"$/, '$1'); + cur[key] = value; + } + return envs; +} + +function serialize(envs) { + const lines = ['# Environments registered with this harness (managed by `context-specs add`).', '']; + for (const e of envs) { + lines.push('[[environment]]'); + lines.push(`name = "${e.name}"`); + lines.push(`path = "${e.path}"`); + lines.push(`enabled = ${e.enabled !== false}`); + if (e.interval) lines.push(`interval = ${e.interval}`); + lines.push(''); + } + return lines.join('\n'); +} + +function load() { + if (!fs.existsSync(registryPath)) return []; + return parse(fs.readFileSync(registryPath, 'utf8')); +} + +function save(envs) { + fs.writeFileSync(registryPath, serialize(envs)); +} + +function find(name) { + return load().find((e) => e.name === name) || null; +} + +// Resolve [name] the run/start/stop/logs commands take: explicit name, or the +// sole registered environment, or an error listing the choices. +function resolveOne(name) { + const envs = load(); + if (name) { + const env = envs.find((e) => e.name === name); + if (!env) throw new Error(`no environment named '${name}' (registered: ${envs.map((e) => e.name).join(', ') || 'none'})`); + return env; + } + const enabled = envs.filter((e) => e.enabled !== false); + if (enabled.length === 1) return enabled[0]; + if (!enabled.length) throw new Error("no environments registered — run 'context-specs add ' first"); + throw new Error(`multiple environments registered — name one of: ${enabled.map((e) => e.name).join(', ')}`); +} + +module.exports = { load, save, find, resolveOne, parse, serialize }; diff --git a/lib/remove.js b/lib/remove.js new file mode 100644 index 0000000..8823996 --- /dev/null +++ b/lib/remove.js @@ -0,0 +1,28 @@ +'use strict'; +// Unregister an environment. Deliberately conservative: only the registry entry +// is removed. The environment's files (symlinks, .gitignore block, committed +// artifacts) and its state dir are left in place and printed for manual cleanup +// — deleting things in a repo we no longer manage is not this command's call. + +const fs = require('fs'); +const registry = require('./registry'); +const { stateDir, pidFile } = require('./paths'); +const { isPidAlive } = require('./util'); + +module.exports = async function remove(args) { + const name = args[0]; + if (!name) throw new Error('usage: context-specs remove '); + const envs = registry.load(); + const env = envs.find((e) => e.name === name); + if (!env) throw new Error(`no environment named '${name}'`); + + const pf = pidFile(name); + if (fs.existsSync(pf) && isPidAlive(parseInt(fs.readFileSync(pf, 'utf8'), 10))) { + throw new Error(`supervisor for '${name}' is running — 'context-specs stop ${name}' first`); + } + + registry.save(envs.filter((e) => e.name !== name)); + console.log(`Unregistered '${name}'. Left in place (delete manually if you want them gone):`); + console.log(` ${env.path}/.claude/ symlinks + the managed .gitignore block`); + console.log(` ${stateDir(name)} runtime state and logs`); +}; diff --git a/lib/status.js b/lib/status.js new file mode 100644 index 0000000..cf4cfc1 --- /dev/null +++ b/lib/status.js @@ -0,0 +1,86 @@ +'use strict'; +// One-shot, deterministic render of the whole harness's state. Everything is +// derived from artifacts — committed sentinels on the feature branches, counter +// and sentinel files in state//, the pidfile — exactly the "state lives on +// disk + branches" invariant, read instead of written. Remote-tracking refs are +// as of the last fetch (a tick fetches every interval); pass --fetch to update +// them first. +// +// One gh call per feature is unavoidable: branch existence is NOT liveness. A +// merged feature keeps its origin branch and its committed PRD forever, so +// without the same MERGED/CLOSED gate the dispatcher's step 1 applies, status +// would resurrect every finished feature. Fail-safe mirrors the dispatcher's: +// no PR yet (pre-PR phases) or a transient gh failure → treat as live — never +// silently hide live work. + +const fs = require('fs'); +const path = require('path'); +const registry = require('./registry'); +const { stateDir, pidFile } = require('./paths'); +const { git, tryRun, renderTable, isPidAlive } = require('./util'); + +// The dispatcher's liveness gate, verbatim semantics. gh resolves the repo +// from cwd, so run it in the environment (the ghe() equivalent). +function isLive(envPath, feature) { + const state = tryRun('gh', ['pr', 'view', `feature/${feature}`, '--json', 'state', '--jq', '.state'], { cwd: envPath }); + return state !== 'MERGED' && state !== 'CLOSED'; +} + +// Phase from committed sentinels, mirroring the dispatcher's if/elif chain. +function phaseFor(envPath, feature) { + const has = (f) => git(envPath, ['cat-file', '-e', `origin/feature/${feature}:specs/${feature}/${f}`]) !== null; + if (!has('.planning-done')) return 'spec-planning'; + if (!has('.validated')) return 'spec-validate'; + if (!has('.prd-passed')) return 'implement'; + return 'checks/PR'; +} + +function counterFor(name, feature, phase) { + const files = { + 'spec-planning': `planning-attempts-${feature}`, + 'spec-validate': `validate-attempts-${feature}`, + implement: `implement-attempts-${feature}`, + 'checks/PR': `feedback-rounds-${feature}`, + }; + const alt = phase === 'checks/PR' ? `local-check-attempts-${feature}` : null; + let n = 0; + for (const f of [files[phase], alt].filter(Boolean)) { + try { n = Math.max(n, parseInt(fs.readFileSync(path.join(stateDir(name), f), 'utf8'), 10) || 0); } catch {} + } + return n || ''; +} + +module.exports = async function status(args) { + const envs = registry.load(); + if (!envs.length) { console.log("no environments registered — 'context-specs add '"); return; } + + const rows = [['env', 'feature', 'phase', 'attempts', 'flag', 'supervisor']]; + for (const env of envs) { + if (args.includes('--fetch')) git(env.path, ['fetch', '--quiet', 'origin']); + let sup = 'stopped'; + try { + const pid = parseInt(fs.readFileSync(pidFile(env.name), 'utf8'), 10); + if (isPidAlive(pid)) sup = `running (${pid})`; + } catch {} + if (env.enabled === false) sup = 'disabled'; + + const refs = git(env.path, ['for-each-ref', '--format=%(refname:lstrip=4)', 'refs/remotes/origin/feature/']) || ''; + const features = refs.split('\n').filter(Boolean) + .filter((f) => git(env.path, ['cat-file', '-e', `origin/feature/${f}:prds/${f}/prd.md`]) !== null) + .filter((f) => isLive(env.path, f)); + + if (!features.length) { + rows.push([env.name, '—', 'idle', '', '', sup]); + continue; + } + for (const f of features) { + const sd = stateDir(env.name); + const flag = fs.existsSync(path.join(sd, `stuck-${f}`)) ? 'STUCK' + : fs.existsSync(path.join(sd, `human-review-${f}`)) ? 'HUMAN_REVIEW' : ''; + const phase = phaseFor(env.path, f); + rows.push([env.name, f, phase, counterFor(env.name, f, phase), flag, sup]); + } + } + console.log(renderTable(rows)); + if (!args.includes('--fetch')) console.log('\n(branch state as of each environment’s last fetch — pass --fetch to refresh)'); +}; diff --git a/lib/supervisor.js b/lib/supervisor.js new file mode 100644 index 0000000..68c17b7 --- /dev/null +++ b/lib/supervisor.js @@ -0,0 +1,138 @@ +'use strict'; +// The scheduler around the deterministic dispatcher. One detached supervisor +// process per environment, each running two independent loops: +// +// build loop — scripts/poll-and-dispatch.sh every INTERVAL (default 5m). +// Exit 10 ("advanced") → re-invoke IMMEDIATELY: real work drains +// at machine speed instead of waiting out the poll interval. +// Exit 0 ("idle") → sleep the interval. Anything else → error; +// exponential backoff, capped at 1h. +// learn loop — scripts/learn-dispatch.sh every LEARN_INTERVAL (default 10m). +// Never drains: after a run, the open learn/ PR pauses it. +// +// The supervisor contains no decisions about WHAT to run — the dispatcher's +// exit code is the entire protocol. Intervals come from env vars INTERVAL / +// LEARN_INTERVAL (seconds) or a per-env `interval` in environments.toml. + +const fs = require('fs'); +const path = require('path'); +const { spawn, spawnSync } = require('child_process'); +const registry = require('./registry'); +const { scriptsDir, logsDir, pidFile, binPath } = require('./paths'); +const { isPidAlive } = require('./util'); + +const BUILD_SCRIPT = path.join(scriptsDir, 'poll-and-dispatch.sh'); +const LEARN_SCRIPT = path.join(scriptsDir, 'learn-dispatch.sh'); +const BACKOFF_CAP_S = 3600; + +function targets(args) { + if (args.includes('--all')) { + const envs = registry.load().filter((e) => e.enabled !== false); + if (!envs.length) throw new Error('no environments registered'); + return envs; + } + return [registry.resolveOne(args.find((a) => !a.startsWith('--')))]; +} + +function readPid(name) { + try { return parseInt(fs.readFileSync(pidFile(name), 'utf8'), 10); } catch { return null; } +} + +async function start(args) { + for (const env of targets(args)) { + const pid = readPid(env.name); + if (pid && isPidAlive(pid)) { + console.log(`${env.name}: supervisor already running (pid ${pid})`); + continue; + } + fs.mkdirSync(logsDir(env.name), { recursive: true }); + const out = fs.openSync(path.join(logsDir(env.name), 'supervisor.log'), 'a'); + const child = spawn(process.execPath, [binPath, '__supervise', env.name], { + detached: true, + stdio: ['ignore', out, out], + }); + child.unref(); + fs.writeFileSync(pidFile(env.name), String(child.pid)); + console.log(`${env.name}: supervisor started (pid ${child.pid}) — logs: context-specs logs ${env.name} -f`); + } +} + +async function stop(args) { + for (const env of targets(args)) { + const pid = readPid(env.name); + if (!pid || !isPidAlive(pid)) { + console.log(`${env.name}: not running`); + try { fs.unlinkSync(pidFile(env.name)); } catch {} + continue; + } + process.kill(pid, 'SIGTERM'); + const deadline = Date.now() + 10_000; + while (isPidAlive(pid) && Date.now() < deadline) await sleep(200); + if (isPidAlive(pid)) process.kill(pid, 'SIGKILL'); + try { fs.unlinkSync(pidFile(env.name)); } catch {} + console.log(`${env.name}: stopped`); + } +} + +// `run` — one FOREGROUND pass, drained to idle, then exit. No daemon: keeps +// re-invoking the tick while it reports "advanced" (10), stops at idle (0), +// propagates errors. The interactive way to watch the harness work. +async function runOnce(args) { + const learn = args.includes('--learn'); + const env = registry.resolveOne(args.find((a) => !a.startsWith('--'))); + const script = learn ? LEARN_SCRIPT : BUILD_SCRIPT; + for (;;) { + const { status } = spawnSync('bash', [script, env.path, env.name], { stdio: 'inherit' }); + if (status === 10) continue; // advanced — drain + process.exit(status ?? 1); + } +} + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +function tick(script, env, logFile) { + return new Promise((resolve) => { + const out = fs.openSync(logFile, 'a'); + fs.writeSync(out, `\n--- ${new Date().toISOString()} tick ---\n`); + const child = spawn('bash', [script, env.path, env.name], { stdio: ['ignore', out, out] }); + child.on('exit', (code) => { fs.closeSync(out); resolve(code ?? 1); }); + child.on('error', () => { fs.closeSync(out); resolve(1); }); + }); +} + +// Hidden command the detached child runs. Two setTimeout-chain loops; SIGTERM +// exits cleanly (in-flight bash tick is left to finish its own flock-guarded +// work — the next supervisor simply takes over the schedule). +async function supervise(args) { + const env = registry.resolveOne(args[0]); + const interval = (parseInt(process.env.INTERVAL, 10) || parseInt(env.interval, 10) || 300) * 1000; + const learnInterval = (parseInt(process.env.LEARN_INTERVAL, 10) || 600) * 1000; + const logs = logsDir(env.name); + fs.mkdirSync(logs, { recursive: true }); + console.log(`[supervisor] ${env.name}: build every ${interval / 1000}s (drain on advance), learn every ${learnInterval / 1000}s`); + + let running = true; + process.on('SIGTERM', () => { running = false; process.exit(0); }); + process.on('SIGINT', () => { running = false; process.exit(0); }); + + const loop = async (script, logName, base, drain) => { + let backoff = base; + while (running) { + const code = await tick(script, env, path.join(logs, logName)); + if (code === 10 && drain) { backoff = base; continue; } + if (code === 0) backoff = base; + else { + backoff = Math.min(backoff * 2, BACKOFF_CAP_S * 1000); + console.log(`[supervisor] ${env.name}: ${logName} tick exited ${code}; backing off ${backoff / 1000}s`); + } + await sleep(backoff); + } + }; + + await Promise.all([ + loop(BUILD_SCRIPT, 'build.log', interval, true), + loop(LEARN_SCRIPT, 'learn.log', learnInterval, false), + ]); +} + +module.exports = { start, stop, runOnce, supervise }; diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..de8eb8b --- /dev/null +++ b/lib/util.js @@ -0,0 +1,42 @@ +'use strict'; +const { execFileSync } = require('child_process'); +const readline = require('readline'); + +// Run a command, return trimmed stdout, or null on any failure. The callers +// that need the distinction between "empty output" and "failed" test for null. +function tryRun(cmd, args, opts = {}) { + try { + return execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], ...opts }).trim(); + } catch { + return null; + } +} + +// git against a specific repo without changing cwd. +function git(repo, args) { + return tryRun('git', ['-C', repo, ...args]); +} + +function ask(question) { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + return new Promise((resolve) => rl.question(question, (a) => { rl.close(); resolve(a.trim()); })); +} + +async function confirm(question) { + const a = await ask(`${question} [y/N] `); + return /^y(es)?$/i.test(a); +} + +// Render rows as a padded text table. rows = array of arrays; first row = header. +function renderTable(rows) { + if (!rows.length) return ''; + const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => String(r[i] ?? '').length))); + const line = (r) => r.map((c, i) => String(c ?? '').padEnd(widths[i])).join(' ').trimEnd(); + return [line(rows[0]), widths.map((w) => '-'.repeat(w)).join(' '), ...rows.slice(1).map(line)].join('\n'); +} + +function isPidAlive(pid) { + try { process.kill(pid, 0); return true; } catch { return false; } +} + +module.exports = { tryRun, git, ask, confirm, renderTable, isPidAlive }; diff --git a/package.json b/package.json new file mode 100644 index 0000000..5a11228 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "context-specs", + "version": "0.1.0", + "description": "Harness engineering in practice: a deterministic CLI + dispatcher that drives probabilistic coding agents from PRD to a guaranteed output — a PR that is ready to merge or STUCK with a diagnosis.", + "license": "Apache-2.0", + "bin": { + "context-specs": "bin/context-specs" + }, + "engines": { + "node": ">=18" + }, + "repository": { + "type": "git", + "url": "https://github.com/capitalone/context-specs.git" + } +} diff --git a/skills/harness/harness-init/assets/harness-lib.sh b/scripts/harness-lib.sh similarity index 52% rename from skills/harness/harness-init/assets/harness-lib.sh rename to scripts/harness-lib.sh index d08d2b0..308ade8 100644 --- a/skills/harness/harness-init/assets/harness-lib.sh +++ b/scripts/harness-lib.sh @@ -2,49 +2,61 @@ # harness-lib.sh — shared helpers for the harness scripts. # # Sourced by BOTH scripts/poll-and-dispatch.sh (the feature/build loop) and -# scripts/learn-tick.sh (the memory loop). These are pure functions: they read -# globals the sourcing script defines (CLAUDE_PERM_ARGS, WORKTREE_BASE), and bash -# resolves those at call time — so the only requirement is that the caller sets -# its config block before it calls a helper, not before it sources this file. +# scripts/learn-dispatch.sh (the memory loop). These are pure functions: they +# read globals the sourcing script defines (ENV_PATH, STATE_DIR, +# CLAUDE_PERM_ARGS, WORKTREE_BASE), and bash resolves those at call time — so +# the only requirement is that the caller sets its config block before it calls +# a helper, not before it sources this file. # -# Generated by /harness-init. Keep this minimal — it exists only to avoid +# Tier 1 (harness repo) owned. Keep this minimal — it exists only to avoid # duplicating run_claude (and its session-id discipline) across two scripts. # Map a name (a feature, or the literal "learn") to its worktree path. Both loops -# create sibling worktrees off the same WORKTREE_BASE, derived from the MAIN -# worktree by the sourcing script: feature paths are '-harness-', -# the memory loop's is '-harness-learn'. (Inv 3) +# create sibling worktrees off the same WORKTREE_BASE, derived from the +# environment's path by the sourcing script: feature paths are +# '-harness-', the memory loop's is '-harness-learn'. (Inv 3) worktree_for() { echo "${WORKTREE_BASE}-$1"; } -# LOCAL ADDITION (not in the canonical design-doc dispatcher): make a freshly -# created worktree runnable. A bare worktree has no gitignored files -# (node_modules, .env, generated code), so signals, the PRD runner, and /learn's -# drafted-lint check would fail for reasons unrelated to the work. If the project -# has no bootstrap script, this is a no-op. +# Run gh in the ENVIRONMENT repo. gh resolves "which GitHub repo" from its cwd; +# the dispatcher's cwd is the harness repo, which is the WRONG repo — every gh +# call that skipped this wrapper would silently act on (or fail against) the +# harness repo, and the 2>/dev/null guards would hide it. +ghe() { (cd "$ENV_PATH" && gh "$@"); } + +# Make a freshly created worktree runnable. A bare worktree has no gitignored +# files (node_modules, .env, generated code — and the tier-1 skill symlinks), +# so signals, the PRD runner, and /learn's drafted-lint check would fail for +# reasons unrelated to the work. The script is the environment's own +# (project-owned, generated by /env-init); its deterministic header re-links the +# tier-1 skills via `context-specs link` before the project-specific +# provisioning. If the environment has no bootstrap script, this is a no-op. bootstrap_worktree() { local wt="$1" - if [[ -x scripts/bootstrap-worktree.sh ]]; then - ./scripts/bootstrap-worktree.sh "$wt" || \ + if [[ -x "$ENV_PATH/scripts/bootstrap-worktree.sh" ]]; then + "$ENV_PATH/scripts/bootstrap-worktree.sh" "$wt" || \ echo "WARN: bootstrap-worktree.sh failed for ${wt}" >&2 fi } # Wrap every claude -p call. Generates a session UUID, runs claude headless with # --session-id so the trace at ~/.claude/projects//.jsonl is -# locatable later, and appends a row to .harness/sessions-.tsv. The TSV -# is the per-feature (or per-learn-sha) audit trail the STUCK / learn-review -# signals point the human into. +# locatable later, and appends a row to $STATE_DIR/sessions-.tsv. The +# TSV is the per-feature (or per-learn-sha) audit trail the STUCK / learn-review +# signals point the human into. Every invocation counts as a transition for the +# build dispatcher's exit-10 drain protocol (guarded default — the memory loop +# sources this lib without a counter). run_claude() { local step="$1" feature="$2" attempt="$3" wt="$4"; shift 4 + TRANSITIONS=$(( ${TRANSITIONS:-0} + 1 )) local sid; sid="$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "noid-$$-$RANDOM")" - local log=".harness/sessions-${feature}.tsv" + local log="$STATE_DIR/sessions-${feature}.tsv" [[ -s "$log" ]] || printf '#timestamp\tstep\tattempt\tsession_id\texit\tduration_s\n' > "$log" local t0; t0="$(date +%s)" # No print-mode --cwd flag exists; `cd` is the only way to set the working # directory, and it's also what gives the skill its .claude/ command + AGENTS.md # / CLAUDE.md discovery inside the worktree. (Inv 3 + 6: the skill operates on - # its own worktree, never the loop's checkout.) The build loop passes the - # per-feature worktree; the memory loop passes the '-harness-learn' worktree. + # its own worktree, never the developer's checkout.) The build loop passes the + # per-feature worktree; the memory loop passes the '-harness-learn' worktree. # The `|| exit=$?` keeps a non-zero skill exit from tripping `set -e` so the row # below always gets written (the STUCK / learn-review dossier needs it), and # `return 0` keeps a failed step from aborting the rest of the tick — the absent @@ -64,9 +76,9 @@ run_claude() { # are surfaced on the PR in every terminal state. render_sessions_table() { local feature="$1" - [[ -f ".harness/sessions-${feature}.tsv" ]] || return 0 + [[ -f "$STATE_DIR/sessions-${feature}.tsv" ]] || return 0 echo '| Time | Step | Attempt | Session ID | Exit |' echo '|------|------|---------|------------|------|' - grep -v '^#' ".harness/sessions-${feature}.tsv" 2>/dev/null | tail -n 20 | \ + grep -v '^#' "$STATE_DIR/sessions-${feature}.tsv" 2>/dev/null | tail -n 20 | \ awk -F'\t' '{printf "| %s | `%s` | %s | `%s` | %s |\n", $1, $2, $3, $4, $5}' } diff --git a/skills/harness/harness-init/assets/learn-tick.sh b/scripts/learn-dispatch.sh old mode 100644 new mode 100755 similarity index 55% rename from skills/harness/harness-init/assets/learn-tick.sh rename to scripts/learn-dispatch.sh index a2c72bb..4c680d7 --- a/skills/harness/harness-init/assets/learn-tick.sh +++ b/scripts/learn-dispatch.sh @@ -1,46 +1,55 @@ #!/usr/bin/env bash -# learn-tick.sh — one tick of the MEMORY loop. +# learn-dispatch.sh — one tick of the MEMORY loop for ONE environment. # -# The post-merge memory update (/learn) used to be step 5 of the dispatcher, -# running in the host worktree at cwd "." under the build loop's flock. That made a -# multi-minute memory rebuild block every feature step (and a live /learn froze the -# whole tick). This script makes the memory update its OWN loop: its own /loop -# session, its own flock, its own dedicated worktree. The two loops never block each -# other and coordinate only through git (Inv 1 + 7): +# The post-merge memory update (/learn) used to be a dispatcher step, which made +# a multi-minute memory rebuild block every feature step. This script makes the +# memory update its OWN loop: its own supervisor schedule, its own lock, its own +# dedicated worktree. The two loops never block each other and coordinate only +# through git (Inv 1 + 7): # - refs/harness/last-learned — a single moving "watermark" ref recording how far # into main's history /learn has already digested. It lives in its own ref # namespace, so it survives learn/ branch deletion and is shared across # nodes. We advance it with an atomic compare-and-swap (--force-with-lease). # - learn/ + `git ls-remote` — per-merge idempotency, exactly as before. # -# Run it on an interval from the harness HOST worktree, alongside the build loop: -# /loop 10m /learn-loop +# Tier 1 (harness repo) owned. Invoked by the context-specs CLI supervisor on its +# own interval (default 10m): # -# Generated by /harness-init. Safe to edit — see references/dispatcher-explained.md -# ("The memory loop") for the design and what is safe to change. - -# Serialize against concurrent learn ticks on this node (a from-scratch Expert -# bootstrap can outlast the tick interval). Independent of the dispatcher's flock — -# different file — so the two loops never wait on each other. (Inv 5) -exec {lockfd}<"$0"; flock -n $lockfd || exit 0 +# scripts/learn-dispatch.sh [env-name] +# +# Exit contract: 0 = no-op/paused/done, nonzero = error (supervisor backs off). +# There is no exit-10 drain here — after a learn run, the open learn/ PR +# pauses the loop until the human merges, so the next tick is a no-op anyway. +# +# See references/dispatcher-explained.md ("The memory loop") in +# skills/harness/env-init for the design and what is safe to change. set -euo pipefail -cd "$(dirname "${BASH_SOURCE[0]}")/.." # repo root of the host worktree (where /loop runs) -# Config the shared helpers need. Mirrors the dispatcher's block: derive WORKTREE_BASE -# from the MAIN worktree (always first in `git worktree list`), NOT $PWD, so the learn -# worktree path is '-harness-learn' even though we run from '-harness'. +CONTEXT_SPECS_HOME="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +export CONTEXT_SPECS_HOME +ENV_PATH="$(cd "${1:?usage: learn-dispatch.sh [env-name]}" && pwd)" +ENV_NAME="${2:-$(basename "$ENV_PATH")}" +STATE_DIR="${STATE_DIR:-$CONTEXT_SPECS_HOME/state/$ENV_NAME}" +mkdir -p "$STATE_DIR" + +# Serialize against concurrent learn ticks for THIS environment (a from-scratch +# Expert bootstrap can outlast the tick interval). Independent of the build +# loop's tick.lock — different file — so the two loops never wait on each +# other. (Inv 5) +exec {lockfd}>"$STATE_DIR/learn.lock"; flock -n "$lockfd" || exit 0 + +# Config the shared helpers need. Mirrors the dispatcher's block. # CLAUDE_PERM_ARGS must be set ABOVE the .harness/env source so env can replace it. -MAIN_WT="$(git worktree list --porcelain | sed -n '1s/^worktree //p')" -WORKTREE_BASE="../$(basename "${MAIN_WT:-$PWD}")-harness" +WORKTREE_BASE="$(dirname "$ENV_PATH")/$(basename "$ENV_PATH")-harness" CLAUDE_PERM_ARGS=( --permission-mode auto ) -[[ -f .harness/env ]] && source .harness/env -mkdir -p .harness +[[ -f "$ENV_PATH/.harness/env" ]] && source "$ENV_PATH/.harness/env" -# Shared helpers: worktree_for, bootstrap_worktree, run_claude, render_sessions_table. +# Shared helpers: worktree_for, bootstrap_worktree, run_claude, +# render_sessions_table, ghe. source "$(dirname "${BASH_SOURCE[0]}")/harness-lib.sh" -LEARN_WT="$(worktree_for learn)" # ../-harness-learn (persistent, reused) +LEARN_WT="$(worktree_for learn)" # -harness-learn sibling (persistent, reused) WATERMARK_REF="refs/harness/last-learned" # Post the headless session trail on the learn/ PR so the human evaluating the @@ -50,14 +59,14 @@ WATERMARK_REF="refs/harness/last-learned" # so the table shows exactly this run; $branch is learn/. signal_learn_review() { local feature="$1" branch="$2" - local body=".harness/learn-review-body-${feature}.md" + local body="$STATE_DIR/learn-review-body-${feature}.md" { echo "## /learn session — why memory changed" echo - echo "These memory edits (Expert shards, invariants, AGENTS.md pointers," + echo "These memory edits (Expert reference files, invariants, AGENTS.md pointers," echo "candidate lints) were written by a headless \`/learn\` over the merged" echo "diff. If a routing call here looks wrong — a fact in the wrong surface," - echo "an overfit lint, an Expert shard that says too much — open the trace" + echo "an overfit lint, an Expert reference file that says too much — open the trace" echo "below to see exactly what \`/learn\` read and how it decided, before you" echo "accept or revise it." echo @@ -66,7 +75,7 @@ signal_learn_review() { echo render_sessions_table "$feature" } > "$body" - gh pr comment "$branch" --body-file "$body" 2>/dev/null || true + ghe pr comment "$branch" --body-file "$body" 2>/dev/null || true } # Atomic compare-and-swap on the watermark: move it to $TO only if the remote is @@ -76,29 +85,29 @@ signal_learn_review() { # dispatcher uses to claim PRDs. (Inv 7) advance_watermark() { local expect="${1:-0000000000000000000000000000000000000000}" - git push --quiet --force-with-lease="${WATERMARK_REF}:${expect}" \ + git -C "$ENV_PATH" push --quiet --force-with-lease="${WATERMARK_REF}:${expect}" \ origin "${TO}:${WATERMARK_REF}" 2>/dev/null \ - || echo "learn-tick: watermark not advanced (another node won the race, or push failed)" >&2 + || echo "learn-dispatch: watermark not advanced (another node won the race, or push failed)" >&2 } -# Fetch main + the watermark ref namespace. Safe from the host cwd — fetch only -# touches refs/objects, never the host *working tree* (that's the build loop's, synced -# by harness-tick.sh). Explicit refspecs because refs/harness/* isn't in the default. -git fetch --quiet origin \ +# Fetch main + the watermark ref namespace. Fetch only touches refs/objects, +# never the environment's working tree. Explicit refspecs because refs/harness/* +# isn't in the default. +git -C "$ENV_PATH" fetch --quiet origin \ '+refs/heads/*:refs/remotes/origin/*' \ '+refs/harness/*:refs/harness/*' || true # No remote / no main yet (e.g. a fresh-repo dry-run before origin exists) → nothing # to do. Guard before anything reads origin/main. -git rev-parse --verify --quiet origin/main >/dev/null 2>&1 \ - || { echo "learn-tick: no origin/main yet — nothing to do." >&2; exit 0; } +git -C "$ENV_PATH" rev-parse --verify --quiet origin/main >/dev/null 2>&1 \ + || { echo "learn-dispatch: no origin/main yet — nothing to do." >&2; exit 0; } # PAUSE while a learn PR is open: serialize memory edits behind human review so they # stay linear and conflict-free. Nothing is queued and nothing is dropped — the # backlog is implicit in (watermark, origin/main) and recomputed every tick. When the # human merges the open learn PR, the next tick batches every merge that landed since. # Checked early so a paused tick never even touches the learn worktree. -if gh pr list --state open --json headRefName \ +if ghe pr list --state open --json headRefName \ --jq '.[] | select(.headRefName | startswith("learn/")) | .headRefName' \ 2>/dev/null | grep -q .; then exit 0 @@ -107,21 +116,21 @@ fi # The diff range: from the durable watermark (or a bounded look-back on the very first # run — a full from-scratch Expert is /learn --rebuild, a human action, not this loop) # up to current main. -WATERMARK_OLD="$(git rev-parse --verify --quiet "${WATERMARK_REF}" 2>/dev/null || echo "")" +WATERMARK_OLD="$(git -C "$ENV_PATH" rev-parse --verify --quiet "${WATERMARK_REF}" 2>/dev/null || echo "")" if [[ -n "$WATERMARK_OLD" ]]; then SINCE="$WATERMARK_OLD" else - SINCE="$(git rev-parse --verify --quiet 'origin/main~50^{commit}' 2>/dev/null \ - || git rev-list --max-parents=0 origin/main | tail -n1)" + SINCE="$(git -C "$ENV_PATH" rev-parse --verify --quiet 'origin/main~50^{commit}' 2>/dev/null \ + || git -C "$ENV_PATH" rev-list --max-parents=0 origin/main | tail -n1)" fi -TO="$(git rev-parse origin/main)" +TO="$(git -C "$ENV_PATH" rev-parse origin/main)" # Nothing new on main → no-op (don't even touch the watermark or the worktree). [[ "$SINCE" == "$TO" ]] && exit 0 # Cross-node idempotency: if another node already pushed learn/, don't duplicate # it — just catch our watermark up and exit. (Inv 7) -if git ls-remote --exit-code origin "learn/${TO}" >/dev/null 2>&1; then +if git -C "$ENV_PATH" ls-remote --exit-code origin "learn/${TO}" >/dev/null 2>&1; then advance_watermark "$WATERMARK_OLD" exit 0 fi @@ -130,12 +139,15 @@ fi # is a clean, current main. Created once (with bootstrap, so a drafted lint and # check-agents-md.sh can actually run) and reused every run — there is exactly one, so # it's never torn down. `git worktree add` only writes the NEW path; it never touches -# the host working tree, so it can't race the build loop's host sync. +# the developer's working tree. if [[ ! -d "$LEARN_WT" ]]; then - git worktree add --quiet --detach "$LEARN_WT" origin/main \ - || { echo "learn-tick: could not create $LEARN_WT" >&2; exit 1; } + git -C "$ENV_PATH" worktree add --quiet --detach "$LEARN_WT" origin/main \ + || { echo "learn-dispatch: could not create $LEARN_WT" >&2; exit 1; } bootstrap_worktree "$LEARN_WT" fi +# The worktree is persistent but the tier-1 skill set is not frozen: re-link on +# every run so /learn always sees the current skills (idempotent, fast). +"$CONTEXT_SPECS_HOME/bin/context-specs" link "$LEARN_WT" >/dev/null 2>&1 || true # Per-run wipe (-fd keeps node_modules & bootstrapped deps, like the dispatcher's # per-feature wipe), then put it on the learn/ branch off the clean main. git -C "$LEARN_WT" checkout --quiet -f --detach origin/main @@ -148,16 +160,15 @@ run_claude learn "learn-${TO}" 1 "$LEARN_WT" "/learn --since ${SINCE} --sha ${TO # Surface this run's session on the learn PR (only if /learn actually opened one — a # 0/3 no-op merge teaches nothing and opens no PR). -if gh pr view "learn/${TO}" >/dev/null 2>&1; then +if ghe pr view "learn/${TO}" >/dev/null 2>&1; then signal_learn_review "learn-${TO}" "learn/${TO}" fi # Advance the watermark to TO whether or not a PR opened: a no-op merge is still -# "learned" and must not be re-examined every tick. (Same semantics as the old step-5 -# `last-main-sha` advance.) A deterministic /learn failure is not retried here — the -# recovery path is a human `/learn --rebuild`. +# "learned" and must not be re-examined every tick. A deterministic /learn failure +# is not retried here — the recovery path is a human `/learn --rebuild`. advance_watermark "$WATERMARK_OLD" # One-shot cleanup of this run's transient files — the session id now lives in the PR # comment, and the watermark + ls-remote prevent a re-run. -rm -f ".harness/sessions-learn-${TO}.tsv" ".harness/learn-review-body-learn-${TO}.md" +rm -f "$STATE_DIR/sessions-learn-${TO}.tsv" "$STATE_DIR/learn-review-body-learn-${TO}.md" diff --git a/skills/harness/harness-init/assets/poll-and-dispatch.sh b/scripts/poll-and-dispatch.sh similarity index 69% rename from skills/harness/harness-init/assets/poll-and-dispatch.sh rename to scripts/poll-and-dispatch.sh index 9f713cd..c103f1f 100755 --- a/skills/harness/harness-init/assets/poll-and-dispatch.sh +++ b/scripts/poll-and-dispatch.sh @@ -6,25 +6,46 @@ # Every section maps to one of the harness's load-bearing invariants — when this # script and the prose disagree, the invariants win. # -# Generated by /harness-init. Safe to edit — see the dispatcher-explained.md -# reference for what each section does and what is safe to change. - -# Serialize against concurrent ticks on this node (cron + /loop, etc.). (Inv 5) -exec {lockfd}<"$0"; flock -n $lockfd || exit 0 +# Tier 1 (harness repo) owned. Invoked by the context-specs CLI (`start`/`run`) +# with the target environment as an argument: +# +# scripts/poll-and-dispatch.sh [env-name] +# +# It operates on the environment repo via `git -C` (refs, fetches, pushes, and +# sibling worktrees — never the developer's working tree) and keeps all runtime +# state under /state//. Exit code contract: +# 0 — idle: no transitions this tick (includes STUCK-only ticks) +# 10 — advanced: at least one transition; the supervisor re-invokes immediately +# * — error: the supervisor backs off +# +# See the dispatcher-explained.md reference in skills/harness/env-init for what +# each section does and what is safe to change. set -euo pipefail -SLUG="$(git config user.email | cut -d@ -f1)" +CONTEXT_SPECS_HOME="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +export CONTEXT_SPECS_HOME # bootstrap-worktree.sh's link header consumes this +ENV_PATH="$(cd "${1:?usage: poll-and-dispatch.sh [env-name]}" && pwd)" +ENV_NAME="${2:-$(basename "$ENV_PATH")}" +STATE_DIR="${STATE_DIR:-$CONTEXT_SPECS_HOME/state/$ENV_NAME}" +mkdir -p "$STATE_DIR" +TRANSITIONS=0 + +# Serialize against concurrent ticks for THIS environment only. Per-env lock +# file, not $0 — the script is shared by every environment now. (Inv 5) +exec {lockfd}>"$STATE_DIR/tick.lock"; flock -n "$lockfd" || exit 0 + +# Reserve exit code 10 for the explicit "advanced" exit at the bottom: if a +# failing command happens to exit 10, report a plain error (1) so the supervisor +# backs off instead of hot-draining. +trap '[[ $? -eq 10 ]] && exit 1' ERR + +SLUG="$(git -C "$ENV_PATH" config user.email | cut -d@ -f1)" WATCH="${WATCH_PATTERN:-prd/${SLUG}/*}" -# Derive the repo name from the MAIN worktree (always listed first by -# `git worktree list`), NOT from $PWD: the dispatcher runs from the detached -# '-harness' HOST worktree, so basename "$PWD" would double the suffix to -# '-harness-harness'. Per Inv 3 each in-flight feature gets its own -# ephemeral worktree at '-harness-'; the host worktree stays put, -# detached at origin/main (re-synced each tick by scripts/harness-tick.sh). -# Overridable from .harness/env (sourced below). -MAIN_WT="$(git worktree list --porcelain | sed -n '1s/^worktree //p')" -WORKTREE_BASE="../$(basename "${MAIN_WT:-$PWD}")-harness" +# Per Inv 3 each in-flight feature gets its own ephemeral sibling worktree at +# '-harness-', hanging off the developer's own clone. Overridable +# from the env's committed .harness/env (sourced below). +WORKTREE_BASE="$(dirname "$ENV_PATH")/$(basename "$ENV_PATH")-harness" MAX_WORKTREES="${MAX_WORKTREES:-1}" PLANNING_CAP="${PLANNING_CAP:-2}" # /spec-planning retries before STUCK VALIDATE_CAP="${VALIDATE_CAP:-2}" # /spec-validate retries before STUCK @@ -39,7 +60,6 @@ FEEDBACK_CAP="${FEEDBACK_CAP:-5}" # reviewer feedback rounds before STUC # PR the human merges) — acceptable degradation, no special case. Set the marker # to EMPTY in .harness/env when there is no reviewer (converge at PR-open instead). REVIEW_CLEAN_MARKER="${REVIEW_CLEAN_MARKER:-HARNESS_REVIEW_CLEAN}" -mkdir -p .harness # Headless permission posture for `claude -p`. A headless session cannot show a # permission prompt — without a posture, tool calls are denied, the skill can't @@ -53,21 +73,21 @@ mkdir -p .harness # can replace the array. CLAUDE_PERM_ARGS=( --permission-mode auto ) -# Load project-local config if present (MAX_WORKTREES, WATCH_PATTERN, -# CLAUDE_PERM_ARGS, ...). -[[ -f .harness/env ]] && source .harness/env +# Load the environment's committed config if present (MAX_WORKTREES, +# WATCH_PATTERN, CLAUDE_PERM_ARGS, ...). +[[ -f "$ENV_PATH/.harness/env" ]] && source "$ENV_PATH/.harness/env" # Shared helpers (worktree_for, bootstrap_worktree, run_claude, -# render_sessions_table) live in harness-lib.sh — the memory loop (learn-tick.sh) -# uses the same ones, so they're factored out to avoid drift. They read the config -# globals set above (CLAUDE_PERM_ARGS) and below (WORKTREE_BASE); bash resolves -# those at call time. +# render_sessions_table, ghe) live in harness-lib.sh — the memory loop +# (learn-dispatch.sh) uses the same ones, so they're factored out to avoid drift. +# They read the config globals set above (ENV_PATH, STATE_DIR, CLAUDE_PERM_ARGS, +# WORKTREE_BASE); bash resolves those at call time. source "$(dirname "${BASH_SOURCE[0]}")/harness-lib.sh" # Invariant 2: a feature/ is harness-owned iff prds//prd.md is committed # to it. Hand-pushed feature/quickfix without a PRD is ignored. has_prd() { - git cat-file -e "origin/feature/${1}:prds/${1}/prd.md" 2>/dev/null + git -C "$ENV_PATH" cat-file -e "origin/feature/${1}:prds/${1}/prd.md" 2>/dev/null } # Convergence handoff: the reviewer reported no Important findings, so @@ -76,7 +96,7 @@ has_prd() { # Deterministic and once (the human-review-posted- marker prevents re-posting). signal_human_review() { local feature="$1" branch="feature/${feature}" - local body=".harness/human-review-body-${feature}.md" + local body="$STATE_DIR/human-review-body-${feature}.md" { echo "## Ready for your review" echo @@ -91,7 +111,7 @@ signal_human_review() { echo render_sessions_table "$feature" } > "$body" - gh pr comment "$branch" --body-file "$body" 2>/dev/null || true + ghe pr comment "$branch" --body-file "$body" 2>/dev/null || true } # Signal STUCK to the human via the PR. Opens a draft PR if none exists yet @@ -101,9 +121,9 @@ signal_human_review() { # the single human-facing surface; nothing else is captured to disk for the human. signal_stuck() { local feature="$1" step="$2" cap="$3" output_file="${4:-}" - touch ".harness/stuck-${feature}" + touch "$STATE_DIR/stuck-${feature}" local branch="feature/${feature}" - local body=".harness/stuck-body-${feature}.md" + local body="$STATE_DIR/stuck-body-${feature}.md" { echo "## STUCK — your turn" echo @@ -114,7 +134,7 @@ signal_stuck() { echo "the agent saw and concluded; for a CI/server harness, your project should" echo "upload these as workflow artifacts):" echo - if [[ -f ".harness/sessions-${feature}.tsv" ]]; then + if [[ -f "$STATE_DIR/sessions-${feature}.tsv" ]]; then render_sessions_table "$feature" echo fi @@ -129,7 +149,7 @@ signal_stuck() { echo "## Diagnosis-first" echo echo "Your **first** job is not the code — it is to identify which piece of" - echo "context (\`AGENTS.md\`, an Expert shard, a spec, or the PRD) misled the" + echo "context (\`AGENTS.md\`, an Expert reference file, a spec, or the PRD) misled the" echo "agent or was missing. Correct it on this branch *before* the code fix;" echo "the merge carries both into main and \`/learn\` picks up the context fix." echo @@ -138,10 +158,10 @@ signal_stuck() { echo "- [ ] Code fixed; \`./prds/${feature}/run-prd-test.sh\` passes locally" echo "- [ ] Ready to merge" } > "$body" - if gh pr view "$branch" >/dev/null 2>&1; then - gh pr comment "$branch" --body-file "$body" 2>/dev/null || true + if ghe pr view "$branch" >/dev/null 2>&1; then + ghe pr comment "$branch" --body-file "$body" 2>/dev/null || true else - gh pr create --base main --head "$branch" --draft \ + ghe pr create --base main --head "$branch" --draft \ --title "STUCK: ${feature} (${step})" --body-file "$body" 2>/dev/null || true fi } @@ -161,17 +181,17 @@ reviewer_converged() { # hid it, and this ALWAYS returned false — the HUMAN_REVIEW handoff never fired and # every PR ground to FEEDBACK_CAP and false-STUCK. Pull the comment bodies and # fixed-string match the marker in the shell instead. - gh pr view "$branch" --json comments --jq '.comments[]?.body' 2>/dev/null \ + ghe pr view "$branch" --json comments --jq '.comments[]?.body' 2>/dev/null \ | grep -qF "$REVIEW_CLEAN_MARKER" } -git fetch --quiet origin +git -C "$ENV_PATH" fetch --quiet origin # 1. Re-attach worktrees for in-flight feature/* that lost theirs. # In-flight work always continues regardless of MAX_WORKTREES — the cap is on # NEW intake, not continuation. Order is FIFO by remote branch committerdate. in_flight=() -for feature in $(git for-each-ref --sort=committerdate \ +for feature in $(git -C "$ENV_PATH" for-each-ref --sort=committerdate \ --format='%(refname:lstrip=4)' \ refs/remotes/origin/feature/ 2>/dev/null || true); do has_prd "$feature" || continue @@ -184,11 +204,11 @@ for feature in $(git for-each-ref --sort=committerdate \ # the local worktree. Empty state — no PR opened yet (pre-PR pipeline phase) or a # transient gh failure — is fail-SAFE: treat as in-flight and keep advancing, # never silently abandon live work. Merged is terminal, so this never rewinds. (Inv 9) - pr_state="$(gh pr view "feature/${feature}" --json state --jq .state 2>/dev/null || echo "")" + pr_state="$(ghe pr view "feature/${feature}" --json state --jq .state 2>/dev/null || echo "")" [[ "$pr_state" == "MERGED" || "$pr_state" == "CLOSED" ]] && continue wt="$(worktree_for "$feature")" if [[ ! -d "$wt" ]]; then - git worktree add "$wt" "feature/${feature}" 2>/dev/null && bootstrap_worktree "$wt" || true + git -C "$ENV_PATH" worktree add "$wt" "feature/${feature}" 2>/dev/null && bootstrap_worktree "$wt" || true fi [[ -d "$wt" ]] && in_flight+=("$feature") done @@ -199,19 +219,20 @@ done # Shared pool: set WATCH_PATTERN=prd/*/*. capacity=$(( MAX_WORKTREES - ${#in_flight[@]} )) if (( capacity > 0 )); then - for branch in $(git for-each-ref --sort=committerdate \ + for branch in $(git -C "$ENV_PATH" for-each-ref --sort=committerdate \ --format='%(refname:lstrip=3)' \ "refs/remotes/origin/${WATCH}" 2>/dev/null || true); do (( capacity > 0 )) || break feature="${branch##*/}" # Atomic rename = claim. Push failure (non-fast-forward) means another node # claimed first; skip silently. (Inv 2 + 7) - if git push origin "origin/${branch}:refs/heads/feature/${feature}" \ + if git -C "$ENV_PATH" push origin "origin/${branch}:refs/heads/feature/${feature}" \ ":refs/heads/${branch}" 2>/dev/null; then wt="$(worktree_for "$feature")" - git worktree add "$wt" "feature/${feature}" 2>/dev/null && bootstrap_worktree "$wt" || true + git -C "$ENV_PATH" worktree add "$wt" "feature/${feature}" 2>/dev/null && bootstrap_worktree "$wt" || true in_flight+=("$feature") capacity=$(( capacity - 1 )) + TRANSITIONS=$(( TRANSITIONS + 1 )) fi done fi @@ -226,17 +247,17 @@ for feature in ${in_flight[@]+"${in_flight[@]}"}; do # STUCK guard: a cap-hit handed this feature to the human via the PR. # Don't keep poking it — wait for merge/close (cleanup pass clears the # sentinel and the worktree). - [[ -f ".harness/stuck-${feature}" ]] && continue + [[ -f "$STATE_DIR/stuck-${feature}" ]] && continue # HUMAN_REVIEW guard: the reviewer converged and the PR was handed # to the human for /evaluate-pr. Halt — no advance — until the human merges or # closes (cleanup clears the sentinels + worktree). The human drives from here: # evaluate, then merge, close, or fix locally and push. The loop never re-engages. # Post the session trail exactly once (guarded by the -posted marker). - if [[ -f ".harness/human-review-${feature}" ]]; then - if [[ ! -f ".harness/human-review-posted-${feature}" ]]; then + if [[ -f "$STATE_DIR/human-review-${feature}" ]]; then + if [[ ! -f "$STATE_DIR/human-review-posted-${feature}" ]]; then signal_human_review "$feature" - touch ".harness/human-review-posted-${feature}" + touch "$STATE_DIR/human-review-posted-${feature}" fi continue fi @@ -253,13 +274,20 @@ for feature in ${in_flight[@]+"${in_flight[@]}"}; do && git clean -fd --quiet \ && git reset --hard "origin/$branch" --quiet ) + # Re-link the tier-1 skill symlinks after the wipe. They're untracked, and on + # a branch whose .gitignore predates the managed block (e.g. a PRD filed + # before env-init merged), `git clean -fd` just deleted them — and bootstrap + # only runs at worktree creation. Idempotent and cheap; never let a skill run + # skill-less. + "$CONTEXT_SPECS_HOME/bin/context-specs" link "$wt" >/dev/null 2>&1 || true + # State machine: walk forward by exactly one step. Sentinel files gate each # transition. Every step has a bounded retry; at cap, signal_stuck posts to the # PR (opening one as a draft if necessary) with the step, the session log, an # optional failing-output tail, and the diagnosis-first checklist. The stuck # sentinel above halts further advance until the human merges or closes. if [[ ! -f "${wt}/specs/${feature}/.planning-done" ]]; then - attempts_file=".harness/planning-attempts-${feature}" + attempts_file="$STATE_DIR/planning-attempts-${feature}" attempts=$(cat "${attempts_file}" 2>/dev/null || echo 0) if (( attempts >= PLANNING_CAP )); then echo "STUCK: ${feature} at /spec-planning (${PLANNING_CAP}x)." >&2 @@ -270,7 +298,7 @@ for feature in ${in_flight[@]+"${in_flight[@]}"}; do fi elif [[ ! -f "${wt}/specs/${feature}/.validated" ]]; then - attempts_file=".harness/validate-attempts-${feature}" + attempts_file="$STATE_DIR/validate-attempts-${feature}" attempts=$(cat "${attempts_file}" 2>/dev/null || echo 0) if (( attempts >= VALIDATE_CAP )); then echo "STUCK: ${feature} at /spec-validate (${VALIDATE_CAP}x)." >&2 @@ -298,17 +326,17 @@ for feature in ${in_flight[@]+"${in_flight[@]}"}; do && touch "specs/${feature}/.prd-passed" \ && git add "specs/${feature}/.prd-passed" \ && git commit -m "harness: PRD runner green for ${feature}" --quiet \ - && git push --quiet ) || true + && git push --quiet ) && TRANSITIONS=$(( TRANSITIONS + 1 )) || true else # Bounded retry: if the PRD runner keeps failing, the *plan* may be broken, # not just the code, and re-invoking implement forever burns compute. (Inv 8) - attempts_file=".harness/implement-attempts-${feature}" + attempts_file="$STATE_DIR/implement-attempts-${feature}" attempts=$(cat "${attempts_file}" 2>/dev/null || echo 0) if (( attempts >= IMPLEMENT_CAP )); then # Capture the latest failing output for the human's diagnosis dossier. - ( cd "${wt}" && ./prds/${feature}/run-prd-test.sh ) > ".harness/stuck-output-${feature}.log" 2>&1 || true + ( cd "${wt}" && ./prds/${feature}/run-prd-test.sh ) > "$STATE_DIR/stuck-output-${feature}.log" 2>&1 || true echo "STUCK: ${feature} at /implement-mainspec — PRD runner ${IMPLEMENT_CAP}x." >&2 - signal_stuck "$feature" "implement-mainspec" "$IMPLEMENT_CAP" ".harness/stuck-output-${feature}.log" + signal_stuck "$feature" "implement-mainspec" "$IMPLEMENT_CAP" "$STATE_DIR/stuck-output-${feature}.log" else echo $((attempts + 1)) > "${attempts_file}" run_claude implement-mainspec "$feature" $((attempts + 1)) "$wt" "/implement-mainspec ${feature}" @@ -319,18 +347,19 @@ for feature in ${in_flight[@]+"${in_flight[@]}"}; do && ! ( cd "${wt}" && ./scripts/local-checks.sh ); then # Optional gate. Auto-fix first (attempt 0), focused LLM fix (attempt 1), # then STUCK at LOCAL_CHECKS_CAP. (Inv 8) - attempts_file=".harness/local-check-attempts-${feature}" + attempts_file="$STATE_DIR/local-check-attempts-${feature}" attempts=$(cat "${attempts_file}" 2>/dev/null || echo 0) if (( attempts >= LOCAL_CHECKS_CAP )); then - ( cd "${wt}" && ./scripts/local-checks.sh ) > ".harness/stuck-output-${feature}.log" 2>&1 || true + ( cd "${wt}" && ./scripts/local-checks.sh ) > "$STATE_DIR/stuck-output-${feature}.log" 2>&1 || true echo "STUCK: ${feature} at local-checks (${LOCAL_CHECKS_CAP}x)." >&2 - signal_stuck "$feature" "local-checks" "$LOCAL_CHECKS_CAP" ".harness/stuck-output-${feature}.log" + signal_stuck "$feature" "local-checks" "$LOCAL_CHECKS_CAP" "$STATE_DIR/stuck-output-${feature}.log" elif (( attempts == 0 )); then ( cd "${wt}" && ./scripts/local-checks.sh fix || true ) if ! ( cd "${wt}" && git diff --quiet ); then ( cd "${wt}" \ && git commit -am "chore: auto-fix lint/format" --quiet \ && git push --quiet ) + TRANSITIONS=$(( TRANSITIONS + 1 )) fi echo 1 > "${attempts_file}" else @@ -338,30 +367,31 @@ for feature in ${in_flight[@]+"${in_flight[@]}"}; do echo $((attempts + 1)) > "${attempts_file}" fi - elif ! gh pr view "${branch}" >/dev/null 2>&1; then - gh pr create --base main --head "${branch}" --fill - rm -f ".harness/planning-attempts-${feature}" - rm -f ".harness/validate-attempts-${feature}" - rm -f ".harness/local-check-attempts-${feature}" - rm -f ".harness/implement-attempts-${feature}" + elif ! ghe pr view "${branch}" >/dev/null 2>&1; then + ghe pr create --base main --head "${branch}" --fill + TRANSITIONS=$(( TRANSITIONS + 1 )) + rm -f "$STATE_DIR/planning-attempts-${feature}" + rm -f "$STATE_DIR/validate-attempts-${feature}" + rm -f "$STATE_DIR/local-check-attempts-${feature}" + rm -f "$STATE_DIR/implement-attempts-${feature}" # No reviewer configured (empty marker) → nothing to converge on; hand # straight to the human for /evaluate-pr. Guard halts next tick. - [[ -z "$REVIEW_CLEAN_MARKER" ]] && touch ".harness/human-review-${feature}" + [[ -z "$REVIEW_CLEAN_MARKER" ]] && touch "$STATE_DIR/human-review-${feature}" # CONVERGED — the reviewer posted the clean marker. Hand the PR to the human # for /evaluate-pr. The guard above halts the feature next tick. # Checked BEFORE the findings loop so a clean PR never marches to a false STUCK # on a sticky historical COMMENTED review. elif reviewer_converged "${branch}"; then - touch ".harness/human-review-${feature}" + touch "$STATE_DIR/human-review-${feature}" # Reviewer has findings and hasn't signalled done. Address them if budget # remains; at FEEDBACK_CAP rounds without convergence, STUCK (covers genuine # non-convergence AND a reviewer that flubbed the marker — human takes over). - elif gh pr view "${branch}" --json reviews \ + elif ghe pr view "${branch}" --json reviews \ --jq '.reviews[] | select(.state=="CHANGES_REQUESTED" or .state=="COMMENTED")' \ 2>/dev/null | grep -q .; then - rounds_file=".harness/feedback-rounds-${feature}" + rounds_file="$STATE_DIR/feedback-rounds-${feature}" rounds=$(cat "${rounds_file}" 2>/dev/null || echo 0) if (( rounds >= FEEDBACK_CAP )); then echo "STUCK: ${feature} at PR review (${FEEDBACK_CAP} rounds)." >&2 @@ -376,34 +406,45 @@ done # 4. Cleanup pass: tear down per-feature worktree + counter files for # closed/merged PRs. Each finished feature is torn down independently. -for feature in $(git for-each-ref --format='%(refname:lstrip=4)' \ +for feature in $(git -C "$ENV_PATH" for-each-ref --format='%(refname:lstrip=4)' \ refs/remotes/origin/feature/ 2>/dev/null || true); do - state=$(gh pr view "feature/${feature}" --json state --jq .state 2>/dev/null || echo "") + state=$(ghe pr view "feature/${feature}" --json state --jq .state 2>/dev/null || echo "") if [[ "${state}" == "MERGED" || "${state}" == "CLOSED" ]]; then - git worktree remove "$(worktree_for "$feature")" --force 2>/dev/null || true - rm -f ".harness/feedback-rounds-${feature}" - rm -f ".harness/local-check-attempts-${feature}" - rm -f ".harness/implement-attempts-${feature}" - rm -f ".harness/planning-attempts-${feature}" - rm -f ".harness/validate-attempts-${feature}" - rm -f ".harness/stuck-${feature}" - rm -f ".harness/stuck-output-${feature}.log" - rm -f ".harness/stuck-body-${feature}.md" - rm -f ".harness/human-review-${feature}" - rm -f ".harness/human-review-posted-${feature}" - rm -f ".harness/human-review-body-${feature}.md" - rm -f ".harness/sessions-${feature}.tsv" + git -C "$ENV_PATH" worktree remove "$(worktree_for "$feature")" --force 2>/dev/null || true + # The worktree add created a local feature/ branch in the developer's + # clone; drop it so a reused feature name can't collide with a stale branch. + git -C "$ENV_PATH" branch -D "feature/${feature}" 2>/dev/null || true + rm -f "$STATE_DIR/feedback-rounds-${feature}" + rm -f "$STATE_DIR/local-check-attempts-${feature}" + rm -f "$STATE_DIR/implement-attempts-${feature}" + rm -f "$STATE_DIR/planning-attempts-${feature}" + rm -f "$STATE_DIR/validate-attempts-${feature}" + rm -f "$STATE_DIR/stuck-${feature}" + rm -f "$STATE_DIR/stuck-output-${feature}.log" + rm -f "$STATE_DIR/stuck-body-${feature}.md" + rm -f "$STATE_DIR/human-review-${feature}" + rm -f "$STATE_DIR/human-review-posted-${feature}" + rm -f "$STATE_DIR/human-review-body-${feature}.md" + rm -f "$STATE_DIR/sessions-${feature}.tsv" fi done # Post-merge memory update is NOT here. /learn runs in its own loop -# (scripts/learn-tick.sh, driven by a separate `/loop … /learn-loop`), in its own -# dedicated worktree, so a multi-minute memory rebuild never blocks — and is never -# blocked by — this feature/build loop. The two loops coordinate only through git -# (the refs/harness/last-learned watermark + ls-remote idempotency on learn/). -# See references/dispatcher-explained.md ("The memory loop"). +# (scripts/learn-dispatch.sh, driven by the CLI supervisor on its own interval), +# in its own dedicated worktree, so a multi-minute memory rebuild never blocks — +# and is never blocked by — this feature/build loop. The two loops coordinate only +# through git (the refs/harness/last-learned watermark + ls-remote idempotency on +# learn/). See references/dispatcher-explained.md ("The memory loop"). # STUCK escalation is handled inline in step 3 by signal_stuck (which opens or # comments on the PR with the session log + checklist). The human takes over # from there — diagnoses the context defect first, corrects it on the branch, # fixes the code, then merges. /learn picks up the context correction at merge. + +# Exit contract for the supervisor: 10 = advanced (re-invoke immediately to +# drain), 0 = idle (sleep the full interval). STUCK-only ticks made no +# transition, so they exit 0 — a stuck environment never hot-loops. +if (( TRANSITIONS > 0 )); then + exit 10 +fi +exit 0 diff --git a/signal.png b/signal.png deleted file mode 100644 index 7b4185a..0000000 Binary files a/signal.png and /dev/null differ diff --git a/skills/harness/address-feedback/SKILL.md b/skills/harness/address-feedback/SKILL.md index efd7c2a..a907150 100644 --- a/skills/harness/address-feedback/SKILL.md +++ b/skills/harness/address-feedback/SKILL.md @@ -123,7 +123,7 @@ git); you write no "handled" bookkeeping file. feature worktree (the dispatcher `cd`s into it; there is no print-mode `--cwd` flag), when the reviewer has unresolved findings on the PR. - **You do NOT track rounds.** The dispatcher owns the counter - (`.harness/feedback-rounds-`), increments it before **every** invocation regardless of + (`/state//feedback-rounds-`), increments it before **every** invocation regardless of bucket, and decides when to STUCK. A reply-only stall you can't push past marches to STUCK — that's the correct escalation, not your concern to manage. - **Completion:** your commits (for Clear) and replies (for the rest). The dispatcher diff --git a/skills/harness/env-init/SKILL.md b/skills/harness/env-init/SKILL.md new file mode 100644 index 0000000..3d31688 --- /dev/null +++ b/skills/harness/env-init/SKILL.md @@ -0,0 +1,278 @@ +--- +name: env-init +description: One-time setup of a project as a harness ENVIRONMENT — the Software 3.0 half that follows `context-specs add`. Generates the project-specific artifacts an LLM must read the repo to write (AGENTS.md, bootstrap-worktree.sh, local-checks.sh), installs the project-owned /intent, seeds the Expert (long-term memory) skeleton, wires the reviewer, and gathers it all onto a feature/env-init PR. Use when a developer wants to set up, initialize, or onboard a project/environment for the coding harness. +--- + +# env-init + +Stand up a project as a harness **environment**. You are an autonomous +installer: you write the artifacts end-to-end, explain the *significance* of +each as you go, and gather everything onto a `feature/env-init` PR the human +reviews and merges. + +You are the second half of a two-command setup, and the split is the point — +it is harness engineering in miniature: + +- **`context-specs add ` already ran** (deterministic CLI, tier 1): it + registered this environment with the harness repo, symlinked the canonical + skills/agents into `.claude/` (gitignored), and wrote the managed `.gitignore` + block. Everything a script can do without reading the project, a script did. +- **You do what needs judgment** (Software 3.0, tier 2): every artifact below + requires *reading this project* — its manifests, README, CI, conventions. + Scan, generate, cite the evidence. **Never invent a command you didn't see + evidence for** — ask instead. + +The tier test, if you're ever unsure what belongs to you: *could this be +written without reading the project's code?* Yes → it's the CLI's/harness +repo's, leave it alone. No → it's yours to generate here. + +## Operating mode + +Run the steps end-to-end without stopping for per-step approval — **the user +reviews everything in the `feature/env-init` PR.** Your job while installing is +to explain *significance*: for each artifact, one or two sentences on what it +is, why it exists, and what would break without it. Lead with the why, then +write the file. + +Read first: `references/mental-model.md` (so you can explain what the harness +*is*) and `references/invariants-to-preserve.md` (the rules nothing you +generate may break). Load other references as each step needs them. + +**Stop-and-confirm points — the only interruptions:** + +1. **Secret-copying** (bootstrap step) — before writing any `cp` line that + touches a `.env` or credentials file: name the exact files, confirm copy + direction (source → worktree, never reverse, never delete), get explicit consent. +2. **Reviewer choice** — wires CI and billing; the user's call. +3. **The PR merge** — after the PR opens, stop and wait for the human. + +## Preconditions + +- Run in the **developer's checkout** of the environment, on `main`, clean tree, + `origin` remote. +- `context-specs add` has run: `.claude/skills/` contains symlinks into the + harness repo (you are reading this SKILL.md through one). If it hasn't, + stop and have the user run it first; `context-specs doctor` diagnoses. + +--- + +## The install chain + +### Step 1 — Config (`.harness/env`) + +Read `references/config-options.md`. Write `.harness/env` from +`assets/harness-env.template` with the defaults (`MAX_WORKTREES=1`, per-dev +`WATCH_PATTERN` derived from `git config user.email`) unless the user has a +concrete reason otherwise. This file is **committed** — it is the environment's +project-owned dial on the tier-1 dispatcher. Runtime counters live in the +harness repo's `state//`, so there is nothing to gitignore here. + +### Step 2 — Install `/intent` (project-owned, one of the two levers) + +`/intent` is deliberately **not** symlinked like the other skills — it is a +committed, project-owned artifact, because *how well this project expresses +intent is one of the biggest levers it has*. Frame it exactly that way to the +user: **/intent and the Expert are the two developer-owned levers** — /intent +shapes what goes *into* every feature (the PRD + runnable definition of done), +the Expert shapes what every feature *knows*. Developers should expect to tune +both over time. + +Copy the canonical template from the harness repo: +`$CONTEXT_SPECS_HOME/skills/human-loop/intent/` → `.claude/skills/intent/` +(SKILL.md + references/). (`CONTEXT_SPECS_HOME` = the symlinks' target root: +`readlink .claude/skills/spec-planning` and strip `/skills/...`.) Then offer +the choice: + +- **Generic (fine to start):** install verbatim; customize later by editing — + it's the project's file either way. +- **Customized now:** scan the repo and tailor the marked hackable seams — + `references/prd-template.md` (project DoD conventions: what can + `run-prd-test.sh` actually exercise here — e2e harness? LLM-judge hybrid?), + `references/elicitation.md` (domain vocabulary, the questions this project's + features always end up needing), `references/runner-recipes.md` (this + project's test idioms). Cite the scan evidence for every tailored line. + +### Step 3 — Seed the Expert (long-term memory) skeleton + +Copy `assets/expert-skeleton/` → `.claude/skills/expert/`. Structure only — +empty routing table, no invented content: shards are added by `/learn` (post- +merge), by the implementer's Reflect step (post-slice), **and by the developer +directly**. + +Narrate the ownership model loudly; it is the heart of the system: the Expert +is the project's long-term memory, and **the developer owns it**. The loops +are continuous helpers that file things into it; editing it directly is +expected, not exceptional. Long-term memory informs short-term memory — every +`/spec-planning` run reads this to plan the next feature — so improving a shard +improves every future feature. The skeleton's conventions (one topic per shard, +prefix names, `USE WHEN:` openers, routing table inline in SKILL.md) exist so a +weak plan can be traced back to the specific shard that should have taught it. + +### Step 4 — `AGENTS.md` (Software 3.0) + +Read `references/agents-md-guidance.md` and `references/project-discovery.md`. +Scan the repo, fill `assets/AGENTS.md.template`'s bracketed parts (project name, +the verification-layer tooling), keep the rest verbatim. AGENTS.md is the +neutral, eagerly-loaded contract — a map that points into the Expert, not an +encyclopedia; keep it tight (~150 lines). Cite the scan finding behind each +filled-in line. The Expert section is intentional even while the Expert is a +skeleton. + +### Step 5 — `scripts/local-checks.sh` (optional, Software 3.0) + +The deterministic gate the dispatcher runs before opening a PR (two-strike: +`local-checks.sh fix` → `/fix-local-checks` → STUCK). Cheapest place to catch +correctness issues — left of the reviewer and CI. **Read +`references/local-checks-design.md` first** and narrate the *why*; the user +owns and tunes this script. Two responsibilities: + +- **Wire the project's deterministic checks** — lint/format (with a `fix` + subcommand), typecheck, the **fast** unit suite (slow/integration → CI), a + **skip-detection** check, and everything in `scripts/lints/`. Prefer commands + the project already defines. +- **Propose custom correctness lints** from the codebase as-is (snapshot + discovery), behind the five guards in the reference. + +The gate proves **correctness, not coverage** — block only +correctness/structural, warn on legibility. The **skip rule** is load-bearing: +an agent may never add a test-skip marker; a legitimate skip is the human's +call at STUCK. If the project has no deterministic checks, skip this step — +the dispatcher treats the script as absent and works fine. + +### Step 6 — `scripts/bootstrap-worktree.sh` (Software 3.0 below the line) + +Read `references/worktree-bootstrap.md`. Generate from +`assets/bootstrap-worktree.template.sh`, whose **deterministic header is +verbatim and untouchable**: it calls `context-specs link` so every fresh +worktree gets the tier-1 skill symlinks (gitignored symlinks never materialize +in new worktrees — without this line, every headless step would run skill-less). +The dispatcher exports `CONTEXT_SPECS_HOME` when it calls this script, so +nothing machine-specific is committed. + +You generate only the section **below the marker**: copy gitignored runtime +files, install dependencies, run codegen — discovered from +README/CI/manifests (`references/project-discovery.md`); if discovery is thin, +ask the user how they set the project up on a fresh machine. Idempotent, +non-interactive, well-commented. + +**The secret-copy lines are stop-and-confirm point 1** — see Operating mode. +If the project has no gitignored secrets, skip that part and say so. + +### Step 7 — Reviewer (stop-and-confirm point 2) + +Read `references/reviewer-options.md`. Three paths (self-hosted via +`claude-code-action` = recommended default; managed Code Review; none); +switching later is cheap. **Present the paths, recommend self-hosted, and +confirm — including auth — before configuring anything.** + +- **Self-hosted:** copy `assets/REVIEW.md` → repo root and + `assets/workflows/claude-review.yml` → `.github/workflows/`, then: + 1. **Pick auth with the user:** subscription (`CLAUDE_CODE_OAUTH_TOKEN` from + `claude setup-token`) vs metered API (`ANTHROPIC_API_KEY`). Uncomment the + chosen line, have them add the matching repo secret. + 2. **Pin the action** to the current release tag + (`gh release view --repo anthropics/claude-code-action`). + 3. **Tell them the v1 same-content rule:** the workflow only takes effect + once merged to `main`; it can't be tested from a feature branch. +- **Managed:** copy `REVIEW.md`; give the GitHub App install instructions. +- **None:** create nothing; set `REVIEW_CLEAN_MARKER=` (empty) in + `.harness/env` so the dispatcher converges at PR-open. + +**Convergence marker (both reviewer paths):** `REVIEW.md` instructs the +reviewer to post a PR comment containing `HARNESS_REVIEW_CLEAN` once no +Important findings remain; the dispatcher sees it and hands the PR to the human +for `/evaluate-pr`. Configurable via `REVIEW_CLEAN_MARKER` (must match +REVIEW.md). If the reviewer never posts it, the feedback loop STUCKs at +`FEEDBACK_CAP` instead — a clean PR the human merges. + +### Step 8 — Commit on `feature/env-init`, push, open PR, wait for merge + +**Hard rule: never commit setup artifacts directly to `main`.** Everything from +Steps 1–7 — plus the `.gitignore` block `context-specs add` wrote — lands on +`feature/env-init` and reaches `main` only via a merged PR: the whole install +in one reviewable diff. + +``` +git checkout -b feature/env-init +git add .harness/env .gitignore scripts/ AGENTS.md REVIEW.md .github/ \ + .claude/skills/intent .claude/skills/expert +git commit -m "harness: environment setup via env-init" +git push -u origin feature/env-init +gh pr create --base main --head feature/env-init \ + --title "harness: environment setup" --fill +``` + +(The symlinked skills are gitignored and stay out of the diff — that's the +tier split showing up in git: canonical stays in the harness repo, project-owned +gets committed here.) + +Walk the user through the PR, then **stop and ask them to merge before +continuing (stop-and-confirm point 3).** Why merge first: the dispatcher works +entirely through `origin` — it claims PRDs from origin refs and creates +worktrees from `origin/feature/*` branches that fork off `main`. Until this +merge lands, those branches would contain none of these artifacts (no +`.harness/env`, no bootstrap, no Expert), so the first feature would run +against an unconfigured project. + +### Step 9 — Prove the wiring, then hand off to the CLI + +Once merged (`git fetch && git pull`), have the user run, from the harness repo: + +``` +context-specs doctor # every check green for this environment +context-specs run # one foreground tick: fetch, find nothing, exit idle +``` + +A clean no-op `run` proves the whole chain — registry, dispatcher, config, +worktree base — without doing any work. Then the daily flow: + +``` +context-specs start # background supervisor: build loop + memory loop +``` + +1. **`/intent`** in this checkout → confirm a PRD → walk away. The harness + picks it up (a tick that advances work re-fires immediately — real work + drains at machine speed; idle ticks nap the interval). +2. Either: the reviewer converges → the harness posts **"Ready for your + review"** with the build-session trail → run **`/evaluate-pr `**, + understand it, merge. Or: a step hits its cap → **STUCK** post with the + session log + diagnosis-first checklist. **Your first job is the context + defect, not the code** — find which AGENTS.md / Expert / spec / PRD content + misled the agent, correct it on the branch, then fix the code, merge. +3. After each merge, the memory loop raises a `learn/` PR — the project + updating its own long-term memory. Review it with the same care as code: + it decides what every future feature knows. + +`context-specs status` shows every environment's features and phases; +`context-specs logs -f` follows the loop. + +--- + +## Inner skill dependency + +The dispatcher invokes `/spec-planning`, `/spec-validate`, `/implement-mainspec`, +`/fix-local-checks`, and `/address-feedback`; the memory loop invokes `/learn`. +All arrive as symlinks from the harness repo via `context-specs add` — verify +they resolve (`context-specs doctor`). Two are **human-invoked**: `/intent` +(installed by Step 2 as project-owned) and `/evaluate-pr`. env-init generates +the project-specific artifacts; it does not author skills. + +## Re-running + +Safe to re-run. For canonical files (REVIEW.md, workflow, the bootstrap +header), diff and offer to update. For generated files (`AGENTS.md`, +`local-checks.sh`, the bootstrap body, /intent customizations), re-scan and +show a diff rather than overwriting blind. Never overwrite the Expert's +references/ — that is live memory now, not yours. If `feature/env-init` +already exists from a prior run, update that branch rather than creating a +fresh one. + +## Hard rules (from invariants-to-preserve.md) + +- Never generate an LLM call into the dispatcher's decision path (Invariant 5). +- Never replace the atomic-rename claim with a marker/lock file (Invariant 2/7). +- Never generate a "go backward" / sentinel-deleting step (Invariant 9). +- `bootstrap-worktree.sh` copies secrets source→worktree only, never deletes, + never the reverse (Invariant 6) — and its `context-specs link` header stays. +- Never commit setup artifacts directly to `main` — everything rides the + `feature/env-init` PR (Step 8). diff --git a/skills/harness/harness-init/assets/AGENTS.md.template b/skills/harness/env-init/assets/AGENTS.md.template similarity index 98% rename from skills/harness/harness-init/assets/AGENTS.md.template rename to skills/harness/env-init/assets/AGENTS.md.template index 6a1b3da..ceb4cdc 100644 --- a/skills/harness/harness-init/assets/AGENTS.md.template +++ b/skills/harness/env-init/assets/AGENTS.md.template @@ -76,7 +76,7 @@ leaves the next invocation a clean re-derivation from disk. Four layers gate "done." All four must pass; none can be talked past. 1. **Pre-commit hooks** — linters, formatters, type checks ([PROJECT CONFIG]). -2. **Slice signals** — each slice's `Signal` section runs during `/implement-slice`. +2. **Slice unit tests** — each slice's tests run during `/implement-slice`. 3. **CI** — full test suite, full type check, full lint (incl. custom invariant lints in `scripts/lints/`). 4. **PRD runner** — `./prds//run-prd-test.sh` must exit 0 before merge. This is the definition of done. diff --git a/skills/harness/harness-init/assets/REVIEW.md b/skills/harness/env-init/assets/REVIEW.md similarity index 100% rename from skills/harness/harness-init/assets/REVIEW.md rename to skills/harness/env-init/assets/REVIEW.md diff --git a/skills/harness/env-init/assets/bootstrap-worktree.template.sh b/skills/harness/env-init/assets/bootstrap-worktree.template.sh new file mode 100644 index 0000000..3e65272 --- /dev/null +++ b/skills/harness/env-init/assets/bootstrap-worktree.template.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# bootstrap-worktree.sh — make a fresh git worktree runnable for this project. +# +# The dispatcher runs this after every `git worktree add`. A bare worktree has +# tracked files only: no dependencies, no secrets, no generated code — and no +# tier-1 skill symlinks (gitignored symlinks never materialize in new +# worktrees). This script closes that gap. It must be IDEMPOTENT (it re-runs on +# worktree re-attach) and NON-INTERACTIVE (it runs unattended inside the loop). +# +# Usage: scripts/bootstrap-worktree.sh +set -euo pipefail +WT="${1:?usage: bootstrap-worktree.sh }" + +# --- context-specs deterministic section (do not edit or remove) ------------- +# Re-create the tier-1 skill/agent symlinks in the new worktree so headless +# skills resolve there. CONTEXT_SPECS_HOME is exported by the dispatcher (which +# derives it from its own location); a human running this by hand exports it to +# their harness repo path. Idempotent and fast. +"${CONTEXT_SPECS_HOME:?export CONTEXT_SPECS_HOME=, or invoke via the harness}/bin/context-specs" link "$WT" +# --- project-specific provisioning below (generated by /env-init) ------------ + +# [Generated per project by /env-init — typical steps, in order: +# 1. copy gitignored runtime files (.env, credentials) from the source checkout +# (derive it via `git -C "$WT" rev-parse --git-common-dir` → its parent); +# copy direction is ALWAYS source → worktree, never the reverse, never delete +# 2. install dependencies with the lockfile-frozen flavor (npm ci, uv sync, ...) +# 3. run codegen the project needs before it can run (prisma generate, ...) +# Every step gets a one-line comment saying what it provisions and why.] diff --git a/skills/harness/env-init/assets/expert-skeleton/SKILL.md b/skills/harness/env-init/assets/expert-skeleton/SKILL.md new file mode 100644 index 0000000..5db97e4 --- /dev/null +++ b/skills/harness/env-init/assets/expert-skeleton/SKILL.md @@ -0,0 +1,41 @@ +--- +name: expert +description: This project's long-term memory — how to run, validate, and extend it; its architecture, patterns, hard invariants, and worked examples. Consult when planning a feature, validating a spec, implementing a slice, or writing intent for this project. Routing table below points to one small reference file per topic. +--- + +# The Expert — this project's long-term memory + + + +## How to use this memory + +Scan the routing table, open **only** the reference files whose `USE WHEN` matches +the task at hand. Each file is one topic; files cross-link with `[[wikilinks]]` +(resolve `[[name]]` to `references/name.md`). Do not page through everything. + +## Routing table + + + +| Reference | USE WHEN | +|---|---| +| _(empty — the first /learn, Reflect, or your own edit adds the first shard)_ | | + +## Writing to this memory + +- **One topic per file**, named `-.md`, opening with a `USE WHEN:` line. +- **Ground truth only** — facts true of the code on `main`, cited (file paths, shas). +- **Reconcile, don't accumulate** — a change that stales a shard edits or deletes it. +- Add the routing-table line in the same commit as the shard. diff --git a/skills/harness/env-init/assets/expert-skeleton/references/README.md b/skills/harness/env-init/assets/expert-skeleton/references/README.md new file mode 100644 index 0000000..d366e94 --- /dev/null +++ b/skills/harness/env-init/assets/expert-skeleton/references/README.md @@ -0,0 +1,3 @@ +# references/ + +One small file per topic, named `-.md` (how-to-, concept-, pattern-, invariant-, example-), each opening with a `USE WHEN:` line and listed in the SKILL.md routing table. Created by /learn, Reflect, and you. diff --git a/skills/harness/harness-init/assets/harness-env.template b/skills/harness/env-init/assets/harness-env.template similarity index 82% rename from skills/harness/harness-init/assets/harness-env.template rename to skills/harness/env-init/assets/harness-env.template index 9120043..8a43e66 100644 --- a/skills/harness/harness-init/assets/harness-env.template +++ b/skills/harness/env-init/assets/harness-env.template @@ -1,8 +1,9 @@ -# .harness/env — harness configuration, sourced by scripts/poll-and-dispatch.sh +# .harness/env — this environment's harness configuration. # -# This file is committed so a team's checkouts share defaults. The per-feature -# COUNTER files in .harness/ (feedback-rounds-*, local-check-attempts-*, -# last-main-sha) are runtime state and are gitignored — never commit those. +# Sourced by the tier-1 dispatcher (/scripts/poll-and-dispatch.sh +# and learn-dispatch.sh) on every tick. Committed so a team's checkouts share +# defaults. Runtime state (retry counters, stuck sentinels, session logs) lives +# in the harness repo under state//, not in this repo. # How many features the harness works in parallel. # 1 = single worktree, FIFO (recommended default for a single developer). diff --git a/skills/harness/harness-init/assets/workflows/claude-review.yml b/skills/harness/env-init/assets/workflows/claude-review.yml similarity index 100% rename from skills/harness/harness-init/assets/workflows/claude-review.yml rename to skills/harness/env-init/assets/workflows/claude-review.yml diff --git a/skills/harness/harness-init/references/agents-md-guidance.md b/skills/harness/env-init/references/agents-md-guidance.md similarity index 97% rename from skills/harness/harness-init/references/agents-md-guidance.md rename to skills/harness/env-init/references/agents-md-guidance.md index 767b766..e51215e 100644 --- a/skills/harness/harness-init/references/agents-md-guidance.md +++ b/skills/harness/env-init/references/agents-md-guidance.md @@ -9,7 +9,7 @@ non-bypassable verification layers. Using the neutral file (rather than a tool-specific one) also keeps the project's memory portable and its own — no lock-in to a single tool. -harness-init generates it from `assets/AGENTS.md.template` plus a codebase scan +env-init generates it from `assets/AGENTS.md.template` plus a codebase scan (project-discovery.md). Most of the template is canonical and stays verbatim; only the bracketed parts are filled from the scan. diff --git a/skills/harness/env-init/references/config-options.md b/skills/harness/env-init/references/config-options.md new file mode 100644 index 0000000..5712c3d --- /dev/null +++ b/skills/harness/env-init/references/config-options.md @@ -0,0 +1,128 @@ +# Config options — `.harness/env` + +The harness has a small, deliberately tiny config surface. Per-environment +knobs live in the environment's committed `.harness/env` (sourced by the tier-1 +dispatcher on every tick); scheduling knobs live with the CLI. When you walk +the user through config, explain each knob's tradeoff and recommend the +default; only change a default if the user has a concrete reason. + +## The knobs (`.harness/env`, committed in the environment) + +### `MAX_WORKTREES` (default `1`) + +How many features the harness works in parallel *in this environment*. + +- **`1` (recommended default).** Single worktree, FIFO. One feature at a time. + No disk multiplication, simplest mental model. Right for essentially every + single-developer project. (Parallelism across *environments* is free — each + has its own supervisor.) +- **`2`+.** Bounded parallelism. Each concurrent feature gets its own sibling + worktree, so disk and rebuild cost multiply. + +In-flight work always continues regardless of this value — it caps *new intake*, +not continuation. + +### `WATCH_PATTERN` (default `prd//*`) + +Which PRD branches this harness claims. `` derives from +`git config user.email` (the part before `@`). + +| Mode | Pattern | Behavior | When | +|------|---------|----------|------| +| **Per-dev (default)** | `prd//*` | Each dev's harness claims only their own PRDs, on their own machine, their own API quota. | Default. Best attribution, cost, and observability. | +| **Shared pool** | `prd/*/*` | All harnesses race for everything; atomic rename keeps it safe. | A team deliberately sharing a work pool. | + +### STUCK caps + +Bounded-retry circuit breakers — every step has one, so the loop can't run away. +At cap, the dispatcher signals STUCK on the PR (opening it as a draft if no PR +exists yet) with the session log + a diagnosis-first checklist, and halts that +feature. A STUCK-only tick exits 0, so a stuck environment never hot-loops. + +| Var | Default | Step it bounds | +|---|---|---| +| `PLANNING_CAP` | 2 | `/spec-planning` | +| `VALIDATE_CAP` | 2 | `/spec-validate` | +| `IMPLEMENT_CAP` | 3 | `/implement-mainspec` (PRD runner keeps failing) | +| `LOCAL_CHECKS_CAP` | 2 | `scripts/local-checks.sh` two-strike | +| `FEEDBACK_CAP` | 5 | reviewer feedback rounds | + +### `REVIEW_CLEAN_MARKER` (convergence signal) + +Default `HARNESS_REVIEW_CLEAN`. When the reviewer has no Important findings left, +`REVIEW.md` instructs it to post a PR comment containing this token; the dispatcher +(`reviewer_converged`) sees it and hands the PR to the human for `/evaluate-pr`. +Detection is intentionally simple — marker present → converge. If the reviewer +never posts it, the feedback loop reaches `FEEDBACK_CAP` and STUCKs instead (a +clean PR the human merges). Set it **empty** when there is **no reviewer**, so +the dispatcher converges at PR-open. Must match `REVIEW.md`. + +### `CLAUDE_PERM_ARGS` + +Headless permission posture for the `claude -p` subprocesses; see the template's +comments. Default `( --permission-mode auto )`. + +## Scheduling knobs (the CLI's, not `.harness/env`) + +- **Build interval** — `INTERVAL` env var on `context-specs start`, or a per-env + `interval` in `environments.toml`. Default 300s. This is only the *idle* poll + rate: a tick that advanced work (exit 10) re-fires immediately, so real work + drains at machine speed regardless of the interval. +- **Learn interval** — `LEARN_INTERVAL`, default 600s. The memory loop can tick + lazily: it only acts when `origin/main` has advanced past the watermark AND no + `learn/` PR is open. Most ticks are cheap no-ops. + +Both are UX knobs, not correctness ones — the loops self-serialize (per-env +locks) and re-derive their state every tick, so a missed or slow tick never +corrupts anything. + +### The memory loop's watermark (`refs/harness/last-learned`) + +Not a config value — a git **ref** on the environment's remote, the memory +loop's only durable state. It records how far into `main`'s history `/learn` +has digested. `learn-dispatch.sh` reads it as the `--since` for the next run +and advances it (atomic `--force-with-lease`) after each run. It lives in its +own ref namespace, so it survives `learn/` branch deletion and is shared +across nodes. To force a full re-learn, delete the ref +(`git push origin :refs/harness/last-learned`) or run `/learn --rebuild` by hand. + +## What does NOT go in config + +- **The claim mechanism** — the atomic rename, hardcoded. Not configurable. +- **Completion signals** — sentinel files, hardcoded in the dispatcher. +- **The pipeline order** — the `if/elif` chain in the tier-1 dispatcher, edited + there (see dispatcher-explained.md), not a config value. +- **Where state lives** — always the harness repo's `state//`. + +## Runtime state — `state//` in the harness repo + +Holds per-environment runtime state, all re-derivable, none of it secret, all +gitignored in the harness repo (nothing of it lives in the environment): + +- `tick.lock`, `learn.lock` — per-env `flock` files (tick serialization). +- `planning-attempts-`, `validate-attempts-`, `implement-attempts-`, + `local-check-attempts-`, `feedback-rounds-` — per-feature retry counters + (each gates a STUCK circuit breaker). +- `stuck-`, `stuck-output-.log`, `stuck-body-.md` — written when a cap + hits; cleared on merge/close. +- `human-review-`, `human-review-posted-`, `human-review-body-.md` — + written at reviewer convergence; cleared on merge/close. +- `sessions-.tsv` — every `claude -p` invocation for this feature (timestamp, + step, attempt, session_id, exit, duration). The STUCK PR body quotes its tail + so the human can open the trace by session id. +- `supervisor.pid`, `logs/{build,learn,supervisor}.log` — the CLI's. + +Deleting this directory is safe (it unregisters nothing): counters reset, +sentinels re-derive from git and PR state on the next tick. + +The environment's `.harness/env` is the one committed piece — commit it so a +team's checkouts share defaults. + +## Multi-developer reminder + +Per-dev is the default for a reason (attribution, cost isolation, "my harness is +my assistant" mental model). The harness *repo* is shareable — a team improves +skills and scripts in one place, while each dev's registry (`environments.toml`) +and state stay per-machine. Don't steer a user to shared-pool claiming unless +they explicitly describe a shared-queue workflow; when a team truly outgrows +local harnesses, the move is a server/CI harness. diff --git a/skills/harness/env-init/references/dispatcher-explained.md b/skills/harness/env-init/references/dispatcher-explained.md new file mode 100644 index 0000000..7129be0 --- /dev/null +++ b/skills/harness/env-init/references/dispatcher-explained.md @@ -0,0 +1,253 @@ +# The dispatcher, explained + +When you explain the harness to the user, walk them through the dispatcher +using this file. The goal: the user understands every section well enough to +decide whether to tweak it (in the **harness repo** — the dispatcher is tier 1, +shared by every environment). Never present it as a black box. + +## What it is + +`/scripts/poll-and-dispatch.sh [env-name]` — bash, +`git`, and `gh`. No LLM in the decision path. It runs once per tick, invoked by +the `context-specs` supervisor (or by hand via `context-specs run`). The +`if/elif` chain is the state machine; the artifacts on disk are the state. It +operates on the environment entirely through `git -C ` (refs, +fetches, pushes, sibling worktrees) and keeps its runtime state in the harness +repo under `state//`. + +**The exit code is the scheduling protocol:** + +| Exit | Means | The supervisor does | +|------|-------|---------------------| +| `0` | idle — no transitions (includes STUCK-only ticks) | sleep the interval (default 5m) | +| `10` | advanced — at least one transition happened | re-invoke IMMEDIATELY (drain) | +| else | error | exponential backoff, capped at 1h | + +Drain-on-10 is why a claimed PRD marches through planning → validate → +implement at machine speed instead of one step per 5 minutes; idle polling +stays cheap. STUCK deliberately exits 0 — a stuck environment must never +hot-loop. + +## The seven load-bearing properties + +Explain these as the "why it's safe" of the script. Each maps to an invariant. + +1. **One transition per tick per branch.** The `if/elif` fires at most one + branch. Next tick, the artifact this tick wrote satisfies the condition and + the *next* `elif` fires. Forward, one step at a time. (Inv 5) +2. **Artifacts are the only state.** A sentinel existing IS that phase being + done. No checkpoint file. Crash recovery is free — next tick re-derives from + disk. (Inv 1 + 4) Most sentinels are written by the skills; `specs//.prd-passed` + is the exception — the *dispatcher* commits+pushes it the first time + `run-prd-test.sh` exits 0, so a runner that shells out to an LLM-as-judge runs + once instead of every tick (and can't non-deterministically re-kick implement). +3. **Wipe + HEAD-check before every advance.** `git reset --hard && git clean + -fd` discards a crashed skill's uncommitted mess; the HEAD guard skips any + worktree whose branch doesn't match the feature being advanced, so a + manually-checked-out branch is never clobbered. Runs only in per-feature + worktrees — never in the developer's clone. (Inv 3 + 6) +4. **Dispatcher never invokes an LLM directly.** It only spawns `claude -p` + subprocesses or shells to `git`/`gh`. Each subprocess is a fresh process and + a clean context window. (Inv 5) +5. **Local checks are optional and bounded.** If the environment has + `scripts/local-checks.sh`, the PR is gated on it with a two-strike retry + (auto-fix, then focused LLM fix, then stop). No script = gate skipped. (Inv 8) +6. **`MAX_WORKTREES` is the one concurrency knob.** Default 1 = FIFO single + worktree. Claims are lazy: PRDs over capacity stay in `prd//*` as the + visible queue. In-flight work always continues regardless of cap changes. + (Inv 2 + 3) +7. **Convergence has its own exit — HUMAN_REVIEW — separate from STUCK.** The + review loop's healthy end is the reviewer posting `REVIEW_CLEAN_MARKER`; + `reviewer_converged` sees it and writes `human-review-`, the guard posts the + session trail once and halts the feature for the human's `/evaluate-pr`. The + marker (not sticky review state) is the signal, so a clean PR can't false-STUCK + on a stale `COMMENTED` review. If the reviewer never marks clean, the feedback + cap STUCKs instead — fine, the human merges. + +## Section-by-section map + +| Section | What | Tweakable? | +|---------|------|------------| +| derivation block | `ENV_PATH`, `ENV_NAME`, `STATE_DIR`, `WORKTREE_BASE`, `TRANSITIONS` | No — the argv contract the supervisor calls. `WORKTREE_BASE` derives from the env's path, so per-feature paths are `-harness-` siblings. | +| `flock -n` | per-env lock on `state//tick.lock` | No — serializes ticks per environment; the script is shared, so the lock must not be. | +| config | `SLUG`, `WATCH`, caps, marker | Via the env's committed `.harness/env`, not by editing here. | +| `has_prd()` | Invariant-2 ownership filter | No — defines what "harness-owned" means. | +| step 1 | re-attach in-flight worktrees | The `bootstrap_worktree` hook call is the provisioning hook (see below). The PR-state **liveness gate** (skip `MERGED`/`CLOSED` features) is load-bearing — don't drop it; see "Liveness" below. | +| step 2 | lazy claim up to capacity | No — atomic rename is the claim lock. | +| step 3 | advance each feature one step | **This is where you add/remove pipeline steps** — one `elif` per step. | +| step 4 | cleanup merged/closed PRs (worktree + local branch + counters) | Safe to extend (e.g., notify on cleanup). | +| exit block | `TRANSITIONS > 0 → exit 10` | No — it IS the supervisor protocol. New steps that advance work should bump `TRANSITIONS` (run_claude does it for you). | +| post-merge `/learn` | **NOT in this script** — runs in the separate memory loop (`learn-dispatch.sh`). See "The memory loop" below. | Watermark/idempotency live there. | +| helpers (shared) | `run_claude` (session-tagged invocation + TSV log + transition count), `render_sessions_table`, `ghe` (gh-in-the-env), `worktree_for`, `bootstrap_worktree` — live in `harness-lib.sh`, both loops use them. | Edit `run_claude` in one place (e.g. the `--session-id` swap). | +| helpers (dispatcher) | `signal_stuck`, `signal_human_review`, `reviewer_converged`, `has_prd` | Used by §3 steps that call `claude -p`, hit a cap, or detect convergence. | + +One wrapper deserves a callout: **every `gh` call goes through `ghe()`**, which +runs gh with the *environment* as cwd. gh resolves "which GitHub repo" from its +working directory, and the dispatcher's cwd is the harness repo — the wrong +repo. A bare `gh` call here would silently act on the harness repo and its +`2>/dev/null` guard would hide it. Keep new gh calls behind `ghe`. + +## The bootstrap hook — environment-owned worktree provisioning + +A bare worktree has no `node_modules`, no `.env`, no generated code — **and no +tier-1 skill symlinks** (gitignored symlinks never materialize in new +worktrees) — so slice signals and the PRD runner would fail for reasons +unrelated to the feature. So immediately after every `git worktree add`, the +dispatcher runs the environment's `scripts/bootstrap-worktree.sh "$wt"` if it +exists and is executable, exporting `CONTEXT_SPECS_HOME` so the script's +deterministic header can call `context-specs link` on the new worktree. The +hook itself is canonical; the script it calls is **environment-owned** — +env-init generates it per project (see `worktree-bootstrap.md`). + +Make sure the user knows: this hook is why the per-feature worktrees the +harness spins up are actually runnable *and* have their skills. + +## Liveness — branch existence is not in-flight (the step-1 PR-state gate) + +`has_prd()` answers *ownership* ("is this a harness feature?"), not *liveness* +("is there still work to do?"). A `feature/` whose PR has **merged** keeps both +its branch (GitHub's delete-on-merge is off by default) and its committed PRD — +so `has_prd()` stays true forever. Without a liveness check, step 1 re-attaches +that finished feature as in-flight on *every* tick: step 3 re-fires +`/address-feedback` on the merged PR (wasted tokens), and the phantom occupies a +`MAX_WORKTREES` slot so a fresh PRD sits unclaimed. The fix is one gate in step +1, right after `has_prd`: `ghe pr view` the feature's PR and `continue` past any +`MERGED`/`CLOSED` one. Notes: + +- **No branch deletion on the remote.** The harness stops treating it as live + work; consumers keep their branch-retention policy. (The *local* branch in the + developer's clone IS deleted at cleanup — worktree-add created it, and a stale + one would collide when a feature name is reused.) +- **Fail-safe on empty state.** A feature in a pre-PR phase has no PR, and a + transient `gh` outage also returns empty — both are kept in-flight so live + work is never silently dropped. +- **Forward-only (Inv 9).** Merged is terminal; the gate only skips, never rewinds. + +## How the dispatcher stays fresh (no tick wrapper anymore) + +Earlier revisions installed the dispatcher *inside* each project and needed a +wrapper (`harness-tick.sh`) to resync it every tick without overwriting its own +running file. The two-tier split dissolves all of that: the dispatcher lives in +the harness repo and never operates on the repo containing its own bytes. +Updating it is ordinary tool hygiene — `git pull upstream` in the harness repo +(or `context-specs` updates), applied between ticks. Feature **pipeline** skills +never needed a sync at all: they run inside per-feature worktrees that branch +from the PRD branch, so new features pick up skill updates on their own. + +## The memory loop — `/learn` as its own loop (`learn-dispatch.sh`) + +`/learn` is **not** a dispatcher step. It runs in a second, independent loop the +supervisor schedules on its own interval (default 10m): +`scripts/learn-dispatch.sh `. This is the deliberate decoupling that +keeps a multi-minute Expert bootstrap from blocking (or being blocked by) the +build loop. The two loops share nothing but git (Inv 1 + 7): separate locks +(`state//learn.lock` vs `tick.lock`), separate worktrees. + +What `learn-dispatch.sh` does each tick: + +1. `flock -n` its own per-env lock (a long bootstrap just no-ops later ticks). +2. `git -C fetch` `main` **and** the watermark ref namespace + (`+refs/harness/*:refs/harness/*`). Refs only — never a working tree. +3. **Pause** if a `learn/` PR is already open (`ghe pr list …`): memory + edits serialize behind human review. Nothing is queued — the backlog is + implicit in `(watermark, origin/main)` and recomputed every tick. +4. Compute `since = refs/harness/last-learned` (or a bounded look-back on the + first run), `to = origin/main`; if equal, no-op. +5. `git ls-remote origin learn/` idempotency (cross-node first-to-push-wins). +6. Ensure the dedicated, **reused** `-harness-learn` worktree exists + (create + `bootstrap_worktree` on first run), re-run `context-specs link` on + it (persistent worktree, evolving skill set), reset it to clean + `origin/main`, check out `learn/`. +7. Run `/learn --since --sha ` there. `/learn` writes memory, + pushes the branch, opens the PR. +8. `signal_learn_review` posts the session trail on the PR (if one opened). +9. Advance `refs/harness/last-learned` to `` with an atomic CAS + (`--force-with-lease`) — the same first-to-push-wins primitive used to claim + PRDs. The advance happens whether or not a PR opened (a no-op merge is still + "learned"). + +**Why a ref watermark, not a state file.** A ref lives in its own namespace +nothing cleans, so it survives `learn/` branch deletion; it's on the +remote, so it's shared across nodes; and `--force-with-lease` gives a lock-free +compare-and-swap. To force a full re-learn: delete the ref or run +`/learn --rebuild` by hand. + +## Human-steering points: Evaluate (HUMAN_REVIEW) and STUCK + +The human steers the machine at three touchpoints (confirm a PRD, +evaluate-then-merge a PR, unstick a STUCK). Two of those live in this script. + +**HUMAN_REVIEW (the healthy one).** When the reviewer converges (posts +`REVIEW_CLEAN_MARKER`), `reviewer_converged` writes `human-review-`; the guard +posts the build-session trail once and halts the feature. The human runs +`/evaluate-pr` to walk the change, run it, and merge (or fix-and-push, or close). +The loop does not re-engage — the human is the last mile. + +**STUCK (the failure one).** Memory still has its post-merge write path — +`/learn` in the memory loop, ground truth only. There is no separate write path +for failed features. Instead, **STUCK** (a step in §3 hitting its cap) is a +first-class escalation: the dispatcher posts to the PR (opening it as a draft if +needed) with the session log, the failing output tail, and a diagnosis-first +checklist, then halts the feature. The human's first job at STUCK is to identify +the **context defect** (which `AGENTS.md` / Expert / spec / PRD content misled +the agent), correct it on the branch, *then* fix the code, *then* merge. Those +context corrections ride into main with the merge, where `/learn` picks them up. + +## The session log + STUCK signal + +`run_claude` and `render_sessions_table` are **shared** (in `harness-lib.sh`); +the rest are dispatcher-local: + +- **`run_claude ""`** (shared) — + wraps every `claude -p` call. `cd`s into the worktree `` first (there is + no print-mode `--cwd` flag, and the `cd` is what gives the skill its + `.claude/` command + `AGENTS.md` discovery). Generates a UUID session id, runs + `claude -p --session-id "${CLAUDE_PERM_ARGS[@]}"`, bumps the dispatcher's + `TRANSITIONS` counter, and appends + `\t\t\t\t\t` to + `state//sessions-.tsv` (cleared on merge/close). A non-zero + skill exit is recorded in the row but never aborts the tick (`return 0`). +- **`signal_stuck [output-file]`** — touches the stuck + sentinel, composes a PR body (step, cap, session-log tail, optional failing + output tail, diagnosis-first checklist), and opens a draft PR or comments on + the existing one. The single human-facing surface for failures. +- **`reviewer_converged `** — true iff a PR comment contains + `REVIEW_CLEAN_MARKER`. One `ghe` call, no LLM. Empty marker = no reviewer → false. +- **`signal_human_review `** — the convergence counterpart: posts the + "Ready for your review" comment with the session trail for `/evaluate-pr`. +- **`signal_learn_review `** — lives in **`learn-dispatch.sh`**: + after `/learn` opens its `learn/` PR, posts the headless session trail + framed for troubleshooting *why* `/learn` routed each fact as it did. + +If your `claude -p` version doesn't support `--session-id`, swap that flag for +`--output-format json` and parse `session_id` from stdout — `run_claude` is the +one place to change. + +## Common tweaks to offer + +- **Add a pipeline step** (e.g. `/security-review` between validate and + implement): insert one `elif` in step 3 with its own sentinel check, its own + `_CAP`, and a `run_claude` / `signal_stuck` pair like the others. + Remember this edits the harness repo — every environment gets the new step. +- **STUCK caps**: `PLANNING_CAP`, `VALIDATE_CAP`, `IMPLEMENT_CAP`, + `LOCAL_CHECKS_CAP`, `FEEDBACK_CAP` — per-environment, in `.harness/env`. +- **Tick cadence**: `INTERVAL` / `LEARN_INTERVAL` env vars on + `context-specs start`, or a per-env `interval` in `environments.toml`. UX + knobs, not correctness ones — the loops self-serialize and re-derive state + every tick. + +## What NOT to let the user do + +- Add `&` to any `claude -p` call (breaks synchronous one-step discipline). +- Add an LLM call to the decision logic (breaks Inv 5, makes it non-reproducible + and a cost surface). +- Remove the wipe or the HEAD guard (breaks crash recovery and Inv 6). +- Replace the atomic-rename claim with a marker file (breaks Inv 2 + 7). +- Call `gh` directly instead of `ghe` (acts on the wrong repo, silently). +- Exit 10 from an error path, or bump `TRANSITIONS` at STUCK (either would make + the supervisor hot-loop a broken environment). +- Fold `/learn` back into the dispatcher as a step — that re-couples the loops, + so a multi-minute Expert bootstrap again blocks every feature step. +- Make `learn-dispatch.sh` do working-tree ops in the developer's clone instead + of `-harness-learn` — the developer's tree is never the harness's + surface (Inv 6). diff --git a/skills/harness/harness-init/references/invariants-to-preserve.md b/skills/harness/env-init/references/invariants-to-preserve.md similarity index 54% rename from skills/harness/harness-init/references/invariants-to-preserve.md rename to skills/harness/env-init/references/invariants-to-preserve.md index 9f36270..275b684 100644 --- a/skills/harness/harness-init/references/invariants-to-preserve.md +++ b/skills/harness/env-init/references/invariants-to-preserve.md @@ -1,17 +1,17 @@ # Invariants the setup must not break The harness rests on nine invariants (listed below, each with its setup-time -implication). harness-init must not generate anything that violates them — this +implication). env-init must not generate anything that violates them — this is what *you*, running this skill, must be careful about. | # | Invariant | What it means at setup time | |---|-----------|------------------------------| -| 1 | **State lives on disk + branches** | Never generate a daemon, a background queue, or a config that caches "current work" in memory. Counters go in `.harness/` files; sentinels are committed. | +| 1 | **State lives on disk + branches** | Never generate a daemon, a background queue, or a config that caches "current work" in memory. Retry counters go in the harness repo's `state//`; sentinels are committed to the feature branch. | | 2 | **Branch namespace IS the registry** | The claim is the atomic rename `prd//` → `feature/`. Do not add a `.claimed` marker or a lock service. A `feature/` is harness-owned **iff** `prds//prd.md` is committed to it. | -| 3 | **Worktree ↔ branch is 1:1** | Each feature gets its own worktree path `${WORKTREE_BASE}-${feature}`. Never generate logic that switches one worktree between branches. | +| 3 | **Worktree ↔ branch is 1:1** | Each feature gets its own sibling worktree `-harness-`. Never generate logic that switches one worktree between branches. | | 4 | **Skill idempotency (write-then-touch)** | Sentinels are written *after* artifacts are committed. The dispatcher checks only the sentinel. Don't add completion checks based on partial artifact presence. | -| 5 | **Dispatcher discipline** | Zero LLM calls in the dispatcher. One step per branch per tick. `claude -p` invocations are synchronous, never backgrounded with `&`. `flock -n` serializes ticks. The build loop and the memory loop (`learn-tick.sh`) are two independent loops, each `flock`-serialized on its own script and operating on its own worktree, so neither blocks the other; they coordinate only through git (the `refs/harness/last-learned` watermark + `ls-remote` idempotency on `learn/`). | -| 6 | **Human's working tree is sandboxed** | THE TRUST INVARIANT. The harness dispatch never runs in the human's checkout. harness-init may write/commit setup files there (the human is present and consenting), but the running harness operates only in `*-harness*` worktrees. `bootstrap-worktree.sh` copies secrets FROM the human checkout INTO worktrees — never the reverse, never deletes. | +| 5 | **Dispatcher discipline** | Zero LLM calls in the dispatcher. One step per branch per tick. `claude -p` invocations are synchronous, never backgrounded with `&`. Ticks serialize per environment on `state//tick.lock` (`flock -n`), and the exit code is the whole scheduling protocol (0 idle / 10 advanced / else error). The build loop and the memory loop (`learn-dispatch.sh`) are two independent loops with separate locks and separate worktrees, so neither blocks the other; they coordinate only through git (the `refs/harness/last-learned` watermark + `ls-remote` idempotency on `learn/`). | +| 6 | **The developer's working tree is sandboxed** | THE TRUST INVARIANT. The harness operates on the environment only through its *refs* (`git -C fetch/push`) and through *sibling worktrees* — it never touches the developer's checked-out files. env-init may write/commit setup files in the checkout (the developer is present and consenting), but the running harness never does. `bootstrap-worktree.sh` copies secrets FROM the developer's checkout INTO worktrees — never the reverse, never deletes. | | 7 | **Cross-node safety** | Safety comes from atomic git ref ops + `ls-remote` idempotency checks, not coordination services. Keep those primitives even when N=1 — they cost nothing and are the upgrade path. | | 8 | **Verification is non-bypassable** | The dispatcher cannot vouch for "done"; only `run-prd-test.sh` exit 0 + CI gates can. Never generate a path that opens/merges a PR without the runner passing. `local-checks.sh` is an *additional* gate, not a replacement. Caching the first green run in `specs//.prd-passed` is **not** a bypass — the runner still ran and passed; don't "optimize" by skipping the run before the sentinel exists. | | 9 | **Forward-only state machine** | Skills walk forward; none un-touches another's sentinel. Recovery from a wrong plan is human-triggered (delete branch, refile PRD), never a dispatcher rewind. Don't generate "go back a step" logic. | @@ -20,15 +20,16 @@ is what *you*, running this skill, must be careful about. **Invariant 6.** If a developer ever fears the harness will stomp their uncommitted work, the whole design loses their trust regardless of how clean it -is. When the skill explains anything that writes to disk, be explicit about -*where* it writes and confirm that the human's main checkout is never the -dispatch surface. The secret-copying step in `bootstrap-worktree.sh` is the one -place the skill touches sensitive files — flag it loudly and get explicit -consent. +is. The two-tier split makes this structural — the dispatcher lives in the +harness repo and only ever runs `git -C ` ref operations plus sibling +worktrees — but what you *generate* must uphold it too. When the skill explains +anything that writes to disk, be explicit about *where* it writes. The +secret-copying step in `bootstrap-worktree.sh` is the one place the skill +touches sensitive files — flag it loudly and get explicit consent. ## Mode independence -These invariants hold for local laptop, server, and CI alike. harness-init sets -up the *local* mode today, but must not generate anything that would have to be +These invariants hold for local laptop, server, and CI alike. env-init sets up +the *local* mode today, but must not generate anything that would have to be torn out to move to server/SDK mode later. Concretely: keep the cross-node primitives, keep state on disk, keep the dispatcher LLM-free. diff --git a/skills/harness/harness-init/references/local-checks-design.md b/skills/harness/env-init/references/local-checks-design.md similarity index 100% rename from skills/harness/harness-init/references/local-checks-design.md rename to skills/harness/env-init/references/local-checks-design.md diff --git a/skills/harness/env-init/references/mental-model.md b/skills/harness/env-init/references/mental-model.md new file mode 100644 index 0000000..40abacb --- /dev/null +++ b/skills/harness/env-init/references/mental-model.md @@ -0,0 +1,142 @@ +# Mental model — what the harness is + +Read this first. It is the condensed version of the design, written so the +skill can explain the harness to a developer who has never seen it. When you +walk a user through setup, narrate from this. + +## One sentence + +The harness is a system of deterministic code driving probabilistic code: a +polling loop watches a git branch namespace and, for each feature in flight, +runs exactly one spec-driven step per tick in an isolated worktree — until the +system emits its one well-defined output, **a PR that is either ready to merge +or STUCK with a diagnosis**. + +## Two tiers + +``` +HARNESS REPO (tier 1) one repo, N environments. Project-agnostic: + bin/context-specs the CLI (registry, symlinks, supervisor) + scripts/poll-and-dispatch.sh the build-loop dispatcher + scripts/learn-dispatch.sh the memory-loop dispatcher + skills/ the canonical skills (symlinked into envs) + state// per-env runtime state (counters, locks, logs) + +ENVIRONMENT REPO (tier 2) each project. Project-specific (Software 3.0): + AGENTS.md the eager contract + .claude/skills/intent/ project-owned /intent (committed) + .claude/skills/expert/ the Expert — long-term memory (committed) + scripts/bootstrap-worktree.sh makes fresh worktrees runnable (+ re-links skills) + scripts/local-checks.sh the deterministic pre-PR gate + .harness/env this env's dials on the dispatcher (committed) + .claude/skills/ gitignored SYMLINKS into the harness repo +``` + +The tier test: *could you write it without reading the project's code?* +Yes → harness repo (improve it once, every environment benefits). +No → environment (generated by /env-init, evolves with the project). + +## Three layers, independent + +``` +SUPERVISOR context-specs start — one background process per environment. + | Owns TIMING only. Exit 10 from a tick = work advanced → + | re-invoke immediately (drain); 0 = idle → sleep the interval. + v +DISPATCHER scripts/poll-and-dispatch.sh . Pure bash, zero LLM + | calls. Owns ROUTING: reads refs + disk, decides the next skill + v per branch, shells out. The if/elif chain IS the state machine. +INNER LOOP one fresh `claude -p "/skill ..."` subprocess per step. + Owns THE WORK. Fresh context every time, exits when done. +``` + +You can swap any layer without touching the others. This stack is the **build +loop** (features). The **memory loop** has the same shape — supervisor → +`scripts/learn-dispatch.sh` → one `claude -p "/learn …"` in the dedicated +`-harness-learn` worktree — and runs post-merge memory updates without ever +blocking (or being blocked by) the build loop; the two coordinate only through +git (the `refs/harness/last-learned` watermark ref). + +## The worktree topology + +``` +/ the developer's clone. The harness touches only its + REFS (fetch/push) and hangs sibling worktrees off it — + never its working tree. (Inv 6) +-harness-/ EPHEMERAL per-feature worktree. One per in-flight + feature; the dispatcher creates it (off feature/) + and tears it down on merge/close. (Inv 3) +-harness-learn/ the memory loop's dedicated worktree. Persistent + + reused; reset to a clean learn/ off main each run. +``` + +There is no third "host" checkout: the dispatcher lives in the harness repo, so +nothing needs a place to run *inside* the environment. `WORKTREE_BASE` derives +from the environment's path, so per-feature paths are always +`-harness-` siblings. + +## Why the split matters for setup + +- **The dispatcher is deterministic.** No model in the decision path. This is + what makes crash recovery free and behavior predictable from disk state. +- **Every step runs in a fresh context window.** The supervisor and dispatcher + never do real work; each skill runs in its own `claude -p` subprocess, so no + step inherits another's polluted window. +- **The install itself follows the same split**: `context-specs add` did the + deterministic part; /env-init (you) does the part that requires reading the + project. + +## The state machine in one table + +| Branch state | Means | Next action the dispatcher takes | +|------------------------|--------------------|-----------------------------------------| +| `prd//` | waiting in queue | atomic-rename to `feature/` (claim) | +| `feature/` no specs | claimed, unplanned | `/spec-planning` | +| `.planning-done` | planned | `/spec-validate` | +| `.validated` | validated | `/implement-mainspec` until runner == 0 | +| runner exits 0 | implemented | open PR against `main` | +| PR has findings | in review | `/address-feedback` (bounded) | +| PR merged | done | cleanup worktree (the memory loop runs `/learn` separately) | + +"Done" is one thing only: `./prds//run-prd-test.sh` exits 0. The harness +contracts on that exit code and nothing else — that is the **goal** the loop +pursues until it's met or the retry cap turns it into STUCK. + +## The artifacts ARE the state + +There is no database, no in-memory queue, no "what step are we on" file. A +sentinel file existing IS that phase being done. The branch namespace IS the +work registry. Retry counters live in the harness repo's `state//`. +Everything the dispatcher needs is re-derivable from `git` + the filesystem on +every tick. That is the whole recovery story: `kill -9` mid-step leaves nothing +the next tick can't sort out. + +## Memory: long-term informs short-term + +The Expert (+ AGENTS.md) is the project's **long-term memory**; the mainspec + +slices `/spec-planning` writes are **short-term memory** — the plan for one +feature, informed by the long-term memory every time. Improve a shard of +long-term memory and every future feature plans better. Three write paths: +`/learn` after each merge (off the hot path), the implementer's Reflect step +after each slice (on the hot path), and **the developer directly** — the Expert +is developer-owned; the loops are helpers. + +## Where the human steers + +Exactly three points: +1. **Confirming a PRD** (via the project-owned `/intent`, in their own checkout). +2. **Merging a PR** (the feature PR after `/evaluate-pr`, and the `learn/` + memory PR). +3. **Unsticking a STUCK** (diagnosis-first: fix the context, then the code). + +Everything between is the harness. The loop never merges — that is the steering +input the system is built around. + +## What env-init is actually standing up + +The environment's half of the contract: its config dial (`.harness/env`), its +eager contract (`AGENTS.md`), its two owned levers (`/intent`, the Expert), its +provisioning (`bootstrap-worktree.sh`), its deterministic gate +(`local-checks.sh`), and its reviewer. The loops, the dispatcher, and the +canonical skills all live in the harness repo — `context-specs add` already +wired them. diff --git a/skills/harness/harness-init/references/project-discovery.md b/skills/harness/env-init/references/project-discovery.md similarity index 98% rename from skills/harness/harness-init/references/project-discovery.md rename to skills/harness/env-init/references/project-discovery.md index 1449382..825f1ce 100644 --- a/skills/harness/harness-init/references/project-discovery.md +++ b/skills/harness/env-init/references/project-discovery.md @@ -1,6 +1,6 @@ # Project discovery — how to read a codebase before generating -Three artifacts harness-init generates are codebase-dependent and must NOT be +Three artifacts env-init generates are codebase-dependent and must NOT be hardcoded: `AGENTS.md`, `scripts/local-checks.sh`, and `scripts/bootstrap-worktree.sh`. This file is the shared discovery procedure that feeds all three. Run the scan once, then reuse the findings. diff --git a/skills/harness/harness-init/references/reviewer-options.md b/skills/harness/env-init/references/reviewer-options.md similarity index 98% rename from skills/harness/harness-init/references/reviewer-options.md rename to skills/harness/env-init/references/reviewer-options.md index 7a0e652..b8e0140 100644 --- a/skills/harness/harness-init/references/reviewer-options.md +++ b/skills/harness/env-init/references/reviewer-options.md @@ -20,7 +20,7 @@ conversational mode). - **Setup:** drop `assets/workflows/claude-review.yml` into `.github/workflows/`, drop `REVIEW.md` at repo root, add the auth secret (next bullet), and **pin** - the action: replace `@v1` with the current release tag (harness-init looks up + the action: replace `@v1` with the current release tag (env-init looks up and pins the latest at setup time). - **Auth — two options, pick one:** - **`CLAUDE_CODE_OAUTH_TOKEN`** — uses a Claude **Max/Pro subscription** diff --git a/skills/harness/env-init/references/worktree-bootstrap.md b/skills/harness/env-init/references/worktree-bootstrap.md new file mode 100644 index 0000000..ecea36b --- /dev/null +++ b/skills/harness/env-init/references/worktree-bootstrap.md @@ -0,0 +1,98 @@ +# Worktree bootstrap — making a fresh worktree runnable + +`scripts/bootstrap-worktree.sh` (environment-owned) provisions a newly created +git worktree so the project can actually run inside it. Generate it from +`assets/bootstrap-worktree.template.sh`: the template's **deterministic header +is verbatim** — only the section below the marker is Software 3.0, generated by +reading the project (see project-discovery.md), explained to the user, and +confirmed before it's written. + +## Why it exists + +`git worktree add` gives you a checkout of tracked files only. Everything +gitignored — `node_modules`, `target`, `.venv`, `.env`, generated code, **and +the tier-1 skill symlinks** — does NOT come along. So a fresh worktree is like +a new developer's laptop on day one: the code is there, but it won't run until +someone installs deps, drops in secrets, runs codegen — and re-links the +skills, or every headless `claude -p` step runs skill-less. + +## The deterministic header (do not modify) + +```bash +"${CONTEXT_SPECS_HOME:?...}/bin/context-specs" link "$WT" +``` + +Re-creates the skill/agent symlinks in the new worktree. `CONTEXT_SPECS_HOME` +is exported by the dispatcher (which derives it from its own location), so +nothing machine-specific is committed; a human running the script by hand +exports it to their harness repo path. `link` is idempotent and fast — safe on +every invocation. + +## Callers + +1. **The dispatcher**, after every `git worktree add` (per-feature worktrees). +2. **The memory loop** (`learn-dispatch.sh`), when it first creates the + persistent `-harness-learn` worktree. + +So the script must be **idempotent** and **non-interactive** — it runs +unattended inside the loop. + +## Contract + +``` +scripts/bootstrap-worktree.sh +``` + +- Takes the target worktree path as its one argument. +- Determines the **source checkout** (the developer's clone, where the real + gitignored files live) via `git -C rev-parse --git-common-dir` + → its parent is the main worktree. Use that as the copy source. +- Exits 0 on success, non-zero with a clear message on failure. +- Re-running on an already-provisioned worktree is a cheap no-op. + +## What the generated section typically does, in order + +1. **Copy gitignored runtime files** from the source checkout: `.env`, + `.env.local`, local credentials, certs — whatever `.env.example` and + `.gitignore` reveal the project needs. Copy only files that exist in the + source; never fail because an optional one is absent. +2. **Install dependencies** using the project's package manager and lockfile + (`npm ci`, `pnpm i --frozen-lockfile`, `uv sync`, `cargo fetch`, ...). +3. **Run codegen** if the project needs it before it runs (`prisma generate`, + protobuf/GraphQL codegen, migrations against a local/test DB, ...). +4. **Any project-specific "make it runnable" step** the README/CI revealed. + +## The secret-copying caution — flag this loudly + +Step 1 copies REAL secrets from the developer's clone into the sibling +worktree. This is the one place the harness touches sensitive files. When you +present this step: + +- Say explicitly what files will be copied and from where. +- Confirm the destination is a `*-harness*` worktree on the same machine, owned + by the same user — secrets don't leave the box. +- Copy direction is **always** source-checkout → worktree. Never the reverse. + Never delete from the source. (Invariant 6.) +- If the project has no gitignored secrets, skip step 1 entirely and say so. +- Get explicit consent before writing a script that copies secrets. + +## Things to get right + +- **Idempotency.** It runs on every `git worktree add`. Guard expensive steps + (`[[ -d node_modules ]] && lockfile unchanged → skip install`). +- **Non-interactive.** No prompts; it runs in the loop. Use `--frozen`/`ci` + install flavors that don't prompt. +- **Port awareness.** If codegen or a setup step starts a server, it can collide + with the developer's dev server. Prefer steps that don't bind ports; if + unavoidable, read the port from env (note this in the script's comments). +- **Fail loudly, not silently.** A worktree that's missing deps should make + bootstrap exit non-zero with a clear message — otherwise the failure surfaces + later as a confusing PRD-runner failure. +- **Comment the script.** The human owns it. Every step gets a one-line comment. + +## If discovery comes up empty + +If you cannot determine how the project installs/runs (no manifest, sparse +README, no CI), do not guess. Ask the user directly: "How do you set up this +project on a fresh machine? What do you install, and what secret/config files +do you copy in?" Their answer becomes the generated section. diff --git a/skills/harness/fix-local-checks/SKILL.md b/skills/harness/fix-local-checks/SKILL.md index 80c8ec6..4ead74c 100644 --- a/skills/harness/fix-local-checks/SKILL.md +++ b/skills/harness/fix-local-checks/SKILL.md @@ -88,7 +88,7 @@ decision. The ability to skip is the human's, never yours. feature worktree (the dispatcher `cd`s into it; there is no print-mode `--cwd` flag), after the auto-fix pass left the gate red. - **You do NOT track rounds.** The dispatcher owns the counter - (`.harness/local-check-attempts-`) and decides when to STUCK. You do one focused + (`/state//local-check-attempts-`) and decides when to STUCK. You do one focused pass per invocation. - **Completion:** your commit + push. The dispatcher re-runs `local-checks.sh` next tick; if green it moves to PR, if red it re-invokes you (or STUCKs at cap). diff --git a/skills/harness/harness-init/SKILL.md b/skills/harness/harness-init/SKILL.md deleted file mode 100644 index 531e018..0000000 --- a/skills/harness/harness-init/SKILL.md +++ /dev/null @@ -1,324 +0,0 @@ ---- -name: harness-init -description: One-time, guided setup of the local agent-first coding harness — the polling loop, the deterministic dispatcher, its config, the AGENTS.md contract, and the worktree provisioning that makes per-feature worktrees runnable. Use when a developer wants to set up, bootstrap, install, or initialize the coding harness for the first time in a project. ---- - -# harness-init - -Stand up the **local** coding harness for a project, step by step, as a guided -expert session. The developer should finish understanding exactly what was -created and why, with every artifact reviewed and tweakable. - -> Scope: this sets up the **local** harness (Claude Code `/loop` outer runtime). -> Server / Claude Agent SDK / OpenSWE modes are future iterations — the -> artifacts here are built so they don't have to be torn out to move there. - -## How to run this skill - -You are a guide, not a script runner. For **every** step that writes to disk or -runs a git operation: **explain what you're about to do and why → confirm with -the user → do it → show the result → take feedback and adjust.** You have rich -context in `references/` so you never have to guess — read the relevant -reference before each step and narrate from it in plain language. - -Two kinds of artifact: -- **Canonical** (drop in verbatim from `assets/`): the dispatcher, the shim - skill, `REVIEW.md`, the env template, the AGENTS.md template skeleton, the - review workflow. Show them, explain them, invite tweaks — but they're the same - everywhere. -- **Software 3.0** (generated by reading the repo): `AGENTS.md`'s filled-in - parts, `scripts/local-checks.sh`, and `scripts/bootstrap-worktree.sh`. Scan, - propose, let the user correct. **Never invent a command you didn't see - evidence for** — ask instead. - -Read first, before talking to the user: -- `references/mental-model.md` — so you can explain what the harness *is*. -- `references/invariants-to-preserve.md` — the rules nothing you generate may break. - -Then load other references as each step needs them. - -## Preconditions - -The skill runs in the **human's checkout** (their normal working copy), on -`main`, with a clean tree and an `origin` remote. The harness will operate in -sibling worktrees, never here (Invariant 6) — but setup writes/commits happen -here because the human is present and consenting. - ---- - -## The guided flow - -### Step 0 — Preflight - -Run `scripts/preflight.sh` from the repo root and walk the user through the -report together. It checks the environment, detects the toolchain, inventories -which inner skills are installed, and flags any harness artifacts that already -exist (re-run detection). - -- `[MISS]` on environment (no git repo, no `gh`) → resolve before continuing. -- `[MISS]` on inner skills → see "Inner skill dependency" below; note them, don't - block. -- `[warn]` on existing artifacts → this is a re-run; diff against what's there - rather than clobbering. - -Summarize what you found in plain language and confirm the project is ready. - -### Step 1 — Config (`.harness/env`) - -Read `references/config-options.md`. Derive the author-slug from -`git config user.email`. Explain `MAX_WORKTREES` (default 1) and `WATCH_PATTERN` -(per-dev default) and their tradeoffs; recommend the defaults and only change on -a concrete reason. Write `.harness/env` from `assets/harness-env.template` with -the chosen values. Add the runtime counter files to `.gitignore`: - -``` -.harness/feedback-rounds-* -.harness/local-check-attempts-* -.harness/implement-attempts-* -.harness/planning-attempts-* -.harness/validate-attempts-* -.harness/stuck-* -.harness/human-review-* -.harness/sessions-*.tsv -.harness/learn-review-body-* -``` - -(The memory loop keeps no local watermark file — the `refs/harness/last-learned` -ref on the remote is the watermark — so there is no `last-main-sha` to ignore.) - -(Commit `.harness/env` itself; ignore only the counters.) - -### Step 2 — The dispatcher, the shared lib, and the two tick wrappers - -Read `references/dispatcher-explained.md`. Copy these from `assets/` into `scripts/` -and `chmod +x` the shell scripts: - -- `poll-and-dispatch.sh` — the build-loop dispatcher (features). -- `harness-tick.sh` — the build-loop wrapper. -- `harness-lib.sh` — shared helpers (`run_claude`, `render_sessions_table`, - `worktree_for`, `bootstrap_worktree`), sourced by both loops. **No `chmod +x`** — - it's sourced, never executed. -- `learn-tick.sh` — the **memory-loop** wrapper (the post-merge `/learn` driver). - -Then **walk the user through them** — the load-bearing properties of the dispatcher -(including the HUMAN_REVIEW convergence exit — the reviewer's `REVIEW_CLEAN_MARKER` -comment hands the PR to the human for `/evaluate-pr`), the section map, the -**bootstrap hook** (the project-owned worktree provisioning; explain why it's there), -and the **`harness-tick.sh` wrapper**: the build loop targets the wrapper, which -force-syncs the host worktree to a clean `origin/main` and *then* `exec`s the -(HEAD-agnostic) dispatcher. Explain why the sync lives in the wrapper, not the -dispatcher: the dispatcher must never reset its own running file, and this is how -loop-infrastructure updates merged to main (dispatcher, `.harness/env`) reach the -loop — feature *pipeline* skills ride the PRD branch and need no sync. - -Then explain **the memory loop is a separate loop** (`references/dispatcher-explained.md` -→ "The memory loop"): `/learn` is NOT a dispatcher step. `learn-tick.sh` runs it under -its own `/loop … /learn-loop`, in its own dedicated `../-harness-learn` worktree, -gated by the `refs/harness/last-learned` watermark ref and paused while a `learn/` -PR is open. The two loops never block each other and coordinate only through git. Offer -the common tweaks (PRD-runner stuck cap, feedback round cap, extra pipeline steps, the -memory-loop interval). Do not let them break the "what NOT to do" list. - -### Step 3 — The shim skills - -Copy **both** shims: `assets/poll-and-dispatch-SKILL.md` → -`.claude/skills/poll-and-dispatch/SKILL.md` and `assets/learn-loop-SKILL.md` → -`.claude/skills/learn-loop/SKILL.md`. Explain the `/loop` gotcha: an outer session must -never do real work — each shim only calls its tick script (`harness-tick.sh` / -`learn-tick.sh`), and all real work happens in fresh `claude -p` subprocesses. One -paragraph; then move on. - -### Step 4 — `AGENTS.md` (Software 3.0) - -Read `references/agents-md-guidance.md` and `references/project-discovery.md`. -Scan the repo, fill `assets/AGENTS.md.template`'s bracketed parts (project name, -the verification-layer tooling), keep the rest verbatim. AGENTS.md is the neutral, -eagerly-loaded contract — a map that points into the Expert, not an encyclopedia; -keep it tight (the freshness lint caps the root at ~150 lines). Present it as a -diff against the template, citing the scan finding behind each filled-in line. Let -the user edit before committing. Tell them the Expert section is intentional even -though the Expert is empty until their first merge. - -### Step 5 — `scripts/local-checks.sh` (optional, Software 3.0) - -The deterministic gate the dispatcher runs before opening a PR (two-strike retry: -`local-checks.sh fix` → `/fix-local-checks` → STUCK). It's the cheapest place to -catch correctness issues — left of the reviewer and CI. **Read -`references/local-checks-design.md` first** and narrate the *why* to the user; they -own and tune this script. Detect commands via `references/project-discovery.md`. - -It has two responsibilities (the reference details both): - -- **Wire the project's deterministic checks** — lint/format (with a `fix` - subcommand), typecheck, the **fast** unit suite (slow/integration → CI), a - **skip-detection** check, and everything in `scripts/lints/`. Prefer commands the - project already defines; call existing pre-commit hooks rather than duplicating. -- **Propose custom correctness lints** from the codebase as-is (snapshot discovery) - — observed existing invariants + surface-scoped best-practice lints, behind the - five guards in the reference. **Propose; the user disposes.** - -The gate proves **correctness, not coverage** — block only correctness/structural, -*warn* on legibility/observability, and don't add checks for volume (mutation -testing is a sensor, not here). The **skip rule** is load-bearing: an agent may -never add a test-skip marker; a legitimate skip is the human's call at STUCK. - -If the user doesn't want this gate, skip it — the dispatcher treats the script as -absent and works fine. **If you do generate it,** mind the Step 6 dependency: -because it runs typecheck and tests, the worktree must be fully runnable, so -`bootstrap-worktree.sh` has to be solid or these legs fail for the wrong reason. - -### Step 6 — `scripts/bootstrap-worktree.sh` (Software 3.0) - -Read `references/worktree-bootstrap.md`. This makes a fresh worktree runnable: -copy gitignored runtime files (`.env`, secrets) from the human's checkout, -install dependencies, run codegen. Discover the setup from README/CI/manifests -(project-discovery.md); if discovery is thin, **ask the user how they set the -project up on a fresh machine**. - -**Flag the secret-copying loudly.** This is the one place the harness touches -sensitive files: name exactly which files get copied and from where, confirm the -destination is a same-machine `*-harness*` worktree, confirm copy direction is -always source→worktree and nothing is deleted, and get explicit consent. If the -project has no gitignored secrets, skip that part and say so. Generate an -idempotent, non-interactive, well-commented script. - -### Step 7 — Reviewer (optional) - -Read `references/reviewer-options.md`. Present the three paths (self-hosted via -`claude-code-action` = recommended default; managed Code Review; none). Explain -the tradeoffs and that switching later is cheap (one workflow file). Then: -- **Self-hosted:** copy `assets/REVIEW.md` to repo root + `assets/workflows/claude-review.yml` - to `.github/workflows/`, and: - 1. **Pick auth with the user.** The template ships with `claude_code_oauth_token` - active and `anthropic_api_key` commented. Ask which they want: - **subscription** (`CLAUDE_CODE_OAUTH_TOKEN` from `claude setup-token` — uses - their Max/Pro plan, shares its usage limits) vs **metered API** - (`ANTHROPIC_API_KEY` from console.anthropic.com — separate billing, better for - teams/CI). Uncomment the chosen line, comment the other, and have them add the - matching repo secret. (Max and the API are separate billing products.) - 2. **Pin the action.** Look up the current `anthropics/claude-code-action` - release (e.g. `gh release view --repo anthropics/claude-code-action`) and - replace `@v1` with that tag (e.g. `@v1.2.3`). - 3. **Tell them the v1 same-content rule:** the workflow must be byte-identical on - the PR branch and the default branch, so it only takes effect once merged to - `main` and can't be tested from a feature branch. - Note the template is the v1 API (`prompt` + `track_progress` + `claude_args` + - `id-token: write`); it does NOT use the pre-v1 `mode`/`review_instructions_path` - inputs. The reviewer is PR-triggered and one-way; if the user also wants an - interactive `@claude` agent, that's the stock `examples/claude.yml` added - separately (see reviewer-options.md) — not bundled here. -- **Managed:** copy `REVIEW.md`; give the GitHub App install instructions. -- **None:** create nothing; set `REVIEW_CLEAN_MARKER=` (empty) in `.harness/env` so - the dispatcher converges at PR-open and hands straight to `/evaluate-pr`; note a - reviewer is addable later. - -**Convergence marker (both reviewer paths).** `REVIEW.md` instructs the reviewer to -post a PR comment containing `HARNESS_REVIEW_CLEAN` once it has no Important findings -left. That marker is how the dispatcher knows the review loop converged and hands the -PR to the human for `/evaluate-pr` — it's a single deterministic signal, -the same for managed and self-hosted. The token is configurable via -`REVIEW_CLEAN_MARKER` in `.harness/env` (must match what `REVIEW.md` tells the -reviewer to post). If the reviewer never posts it, the feedback loop reaches -`FEEDBACK_CAP` and STUCKs instead — a clean PR the human merges. - -### Step 8 — Host worktree + provision - -Explain the three-checkout model (see `references/mental-model.md`): the -human's checkout stays on `main`; the -harness **host** worktree is a sibling that stays **detached at `origin/main`** -(re-synced every tick by `harness-tick.sh`); per-feature work happens in -*ephemeral* `../-harness-` worktrees the dispatcher creates and -tears down. Create the host worktree **detached** — git won't let two worktrees -check out `main` at once, and the human's checkout already holds it: - -``` -git worktree add --detach ../-harness origin/main -``` - -Then run `scripts/bootstrap-worktree.sh ../-harness` to provision it. This -**doubles as validation of the bootstrap script** — if the host worktree is -runnable afterward, the script works. Fix and re-run if not. - -### Step 9 — Dry-run the tick - -From the **harness host worktree** (`../-harness`), run -`./scripts/harness-tick.sh` once. It force-syncs the worktree to `origin/main` -and then runs the dispatcher; with no PRDs filed it should be a clean no-op -(fetch, sync, find nothing, exit 0). This proves the build-loop wiring — sync -wrapper + dispatcher — without doing any work. - -Then run `./scripts/learn-tick.sh` once. On a fresh repo (Expert not yet built, no -merges since the watermark) it creates and bootstraps the `../-harness-learn` -worktree, finds no new range, and exits — proving the memory-loop wiring. Walk -through both outputs with the user. If anything errors, debug before starting the -loops. (Note: with no `origin` yet, the fetch fails fast — that's expected until the -remote exists.) - -### Step 10 — Commit, then start the loop - -Commit the setup artifacts on `main` (or on a setup branch for the user to merge -— ask). Then explain how to start the harness. It runs as **two independent loops**, -each in its own long-lived Claude Code session pinned to the harness worktree — the -build loop (features) and the memory loop (`/learn`). They never block each other: - -``` -cd ../-harness - -# session 1 — the build loop (features): -claude -/loop 5m /poll-and-dispatch - -# session 2 — the memory loop (/learn), in a SEPARATE session: -claude -/loop 10m /learn-loop -``` - -The memory loop can tick lazily (10m is fine) — it only acts when main has advanced -past the `refs/harness/last-learned` watermark and no learn PR is already open. Offer -to start them if they're ready. Finish with the **daily flow** recap: -1. `/intent` in their normal checkout → confirm a PRD → walk away. -2. Harness picks it up, runs the chain, opens a PR. -3. Either: - - **Normal path:** when the reviewer converges, the harness posts a "Ready for - your review" comment with the build-session trail and halts. Run - **`/evaluate-pr `** to walk the change, run it locally, and build a - firm understanding — then merge it (or fix-and-push, or close). Then review and - merge the `/learn` PR that follows (memory updates). - - **STUCK path:** if any step hits its retry cap, the dispatcher posts a STUCK - PR (or comment) with the session log + a diagnosis-first checklist. **Your - first job is the context defect, not the code** — figure out which - `AGENTS.md` / Expert / spec / PRD content misled the agent, correct it on - the branch, *then* fix the code and merge. `/learn` picks up the context - correction at merge. - ---- - -## Inner skill dependency - -The dispatcher calls `/spec-planning`, `/spec-validate`, `/implement-mainspec`, -`/fix-local-checks`, and `/address-feedback`. `/learn` is driven by the **separate -memory loop** (`learn-tick.sh`), not the dispatcher. Two more are **human-invoked**, -not dispatched: `/intent` (front of the chain) and `/evaluate-pr` (the Evaluate phase -— run after the harness posts its "Ready for your review" handoff). harness-init wires -both **outer** loops; it does not author these. Preflight -reports which are installed. If some are missing, set up the harness anyway and -tell the user clearly which skills must be installed from the catalog before the -chain runs end to end — the harness will dispatch to them the moment they exist. -(`/learn` is the memory skill under `.claude/skills/memory/`; the project's -Expert is born from the first `/learn`.) - -## Re-running - -Safe to re-run. Preflight flags existing artifacts. For canonical files, diff -and offer to update. For generated files (`AGENTS.md`, `local-checks.sh`, -`bootstrap-worktree.sh`), re-scan and show a diff rather than overwriting blind. -Never delete the human's host worktree or secrets on a re-run. - -## Hard rules (from invariants-to-preserve.md) - -- Never run the dispatch loop in the human's checkout (Invariant 6). -- Never put an LLM call in the dispatcher's decision path (Invariant 5). -- Never replace the atomic-rename claim with a marker/lock file (Invariant 2/7). -- Never generate a "go backward" / sentinel-deleting step (Invariant 9). -- `bootstrap-worktree.sh` copies secrets source→worktree only, never deletes, - never the reverse (Invariant 6). diff --git a/skills/harness/harness-init/assets/harness-tick.sh b/skills/harness/harness-init/assets/harness-tick.sh deleted file mode 100755 index 2d487d4..0000000 --- a/skills/harness/harness-init/assets/harness-tick.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -# harness-tick.sh — one outer-loop tick: sync the HOST worktree to main, then dispatch. -# -# This is the target the build loop (/loop, cron, …) invokes — NOT the dispatcher -# directly. It exists to keep the harness's own operating context current. -# -# WHY A WRAPPER. The dispatcher, scripts/bootstrap-worktree.sh, and .harness/env all -# live in the HOST worktree (../-harness) and are read FROM it as the loop runs. -# The host worktree sits at a detached HEAD and does NOT auto-advance when main moves, -# so without a sync step those would freeze at whatever commit the worktree was created -# on. Feature PIPELINE skills are unaffected — they run inside per-feature worktrees -# that branch from the PRD branch (off main), so new features pick up skill updates on -# their own. Only this loop-level infrastructure needs syncing; that's what this -# wrapper does. (The /learn skill + memory used to need this sync too, back when /learn -# ran in the host worktree; it now runs in its own dedicated worktree under its own -# loop — scripts/learn-tick.sh — which syncs itself, so the host worktree is purely the -# build loop's.) -# -# WHY HERE AND NOT IN THE DISPATCHER. The dispatcher must stay HEAD-agnostic: if it -# reset its own checkout mid-run it would overwrite the very file it's executing -# (and its flock is on $0). Doing the sync HERE, before exec, means the dispatcher -# always loads its freshest version cleanly. This wrapper is tiny and stable; on the -# rare occasion it changes on main, that change simply takes effect the next tick. - -set -euo pipefail -cd "$(dirname "${BASH_SOURCE[0]}")/.." # repo root of the host worktree - -git fetch --quiet origin - -# Force a clean, current detached checkout of origin/main. `-f --detach` is -# idempotent and self-healing: it recovers no matter what state a crash left the -# worktree in. This is the host-worktree analogue of the dispatcher's per-feature -# wipe. The memory loop never touches this worktree's working tree (it works in -# ../-harness-learn), so there is nothing here to race. -git checkout --quiet -f --detach origin/main -git clean -qfd # -d not -x: keep node_modules & .harness/* runtime state - -exec ./scripts/poll-and-dispatch.sh "$@" diff --git a/skills/harness/harness-init/assets/learn-loop-SKILL.md b/skills/harness/harness-init/assets/learn-loop-SKILL.md deleted file mode 100644 index 1b6d986..0000000 --- a/skills/harness/harness-init/assets/learn-loop-SKILL.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: learn-loop -description: One tick of the harness MEMORY loop. Checks whether main advanced since the last memory update and, if so, runs /learn in a dedicated worktree to open a learn/ PR. Run on an interval via /loop, alongside the build loop. ---- - -# learn-loop - -Run `./scripts/learn-tick.sh` from the harness host worktree root and report what it did. - -That is the entire skill. The intelligence is in the script — this exists only so -`/loop` has a slash-command-shaped target. Do not do any memory work in this session; -the script shells out to a fresh `claude -p "/learn …"` subprocess for the real work, -in its own `../-harness-learn` worktree, which is what keeps this outer session's -context from bloating across ticks. - -`learn-tick.sh` is the memory-loop counterpart of `harness-tick.sh`. It self-serializes -with its own `flock` (independent of the build loop), syncs a dedicated, reused learn -worktree to a clean `origin/main`, and — unless a `learn/` PR is already open -(it pauses while one awaits your review) — computes the range from the -`refs/harness/last-learned` watermark to current main and runs `/learn` over it. It -never touches the build loop's host worktree, so the two loops run fully in parallel. - -Start it alongside the build loop, from the same harness host worktree: - -``` -/loop 10m /learn-loop -``` diff --git a/skills/harness/harness-init/assets/poll-and-dispatch-SKILL.md b/skills/harness/harness-init/assets/poll-and-dispatch-SKILL.md deleted file mode 100644 index f662ac3..0000000 --- a/skills/harness/harness-init/assets/poll-and-dispatch-SKILL.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: poll-and-dispatch -description: One tick of the harness outer loop. Syncs the host worktree to main, then dispatches one skill per branch. Run on an interval via /loop. ---- - -# poll-and-dispatch - -Run `./scripts/harness-tick.sh` from the harness host worktree root and report what it did. - -That is the entire skill. The intelligence is in the scripts — this exists only -so `/loop` has a slash-command-shaped target. Do not do any feature work in this -session; the scripts shell out to fresh `claude -p` subprocesses for all real -work, which is what keeps this outer session's context from bloating across ticks. - -`harness-tick.sh` first force-syncs this host worktree to a clean `origin/main` -(so the dispatcher, its shared `harness-lib.sh`, and `.harness/env` are always -current), then `exec`s `./scripts/poll-and-dispatch.sh` — the deterministic, -HEAD-agnostic dispatcher. Never point `/loop` at the dispatcher directly, or -loop-infrastructure updates merged to main won't reach the loop. - -This is the **build loop** (features). The post-merge memory update (`/learn`) runs -in a separate loop — `/loop … /learn-loop` (`scripts/learn-tick.sh`) — so the two -never block each other. Start it in its own session alongside this one. diff --git a/skills/harness/harness-init/references/config-options.md b/skills/harness/harness-init/references/config-options.md deleted file mode 100644 index cac380a..0000000 --- a/skills/harness/harness-init/references/config-options.md +++ /dev/null @@ -1,142 +0,0 @@ -# Config options — `.harness/env` - -The harness has a small, deliberately tiny config surface. All of it lives in -`.harness/env` (sourced by the dispatcher) or in the `/loop` invocation. When -you walk the user through config, explain each knob's tradeoff and recommend the -default; only change a default if the user has a concrete reason. - -## The knobs - -### `MAX_WORKTREES` (default `1`) - -How many features the harness works in parallel. - -- **`1` (recommended default).** Single worktree, FIFO. One feature at a time. - No disk multiplication, no duplicate `node_modules`/`target`, simplest mental - model. Right for essentially every single-developer project. -- **`2`+.** Bounded parallelism. Each concurrent feature gets its own worktree - (`${WORKTREE_BASE}-${feature}`), so disk and rebuild cost multiply. Flip this - only when you have several independent features queued AND surplus disk/CI. - -In-flight work always continues regardless of this value — it caps *new intake*, -not continuation. Lowering it mid-flight just stops new claims; existing -features finish. - -### `WATCH_PATTERN` (default `prd//*`) - -Which PRD branches this harness claims. `` derives from -`git config user.email` (the part before `@`). - -| Mode | Pattern | Behavior | When | -|------|---------|----------|------| -| **Per-dev (default)** | `prd//*` | Each dev's harness claims only their own PRDs, on their own laptop, their own API quota. | Default. Best attribution, cost, and observability. | -| **Shared pool** | `prd/*/*` | All harnesses race for everything; atomic rename keeps it safe. | A team deliberately sharing a work pool. | - -Note the glob: `*` matches one path component. Per-dev is `prd/alice/*`; shared -is `prd/*/*` (author component + feature component). - -### STUCK caps - -Bounded-retry circuit breakers — every step has one, so the loop can't run away. -At cap, the dispatcher signals STUCK on the PR (opening it as a draft if no PR -exists yet) with the session log + a diagnosis-first checklist for the human, and -halts that feature. - -| Var | Default | Step it bounds | -|---|---|---| -| `PLANNING_CAP` | 2 | `/spec-planning` (skill crashes or never writes sentinel) | -| `VALIDATE_CAP` | 2 | `/spec-validate` (same) | -| `IMPLEMENT_CAP` | 3 | `/implement-mainspec` (PRD runner keeps failing) | -| `LOCAL_CHECKS_CAP` | 2 | `scripts/local-checks.sh` two-strike (auto-fix → focused LLM fix → STUCK) | -| `FEEDBACK_CAP` | 5 | reviewer feedback rounds | - -All env-overridable in `.harness/env`. Raise for hard features; lower to fail -faster. - -### `REVIEW_CLEAN_MARKER` (convergence signal) - -Default `HARNESS_REVIEW_CLEAN`. When the reviewer has no Important findings left, -`REVIEW.md` instructs it to post a PR comment containing this token; the dispatcher -(`reviewer_converged`) sees it and hands the PR to the human for `/evaluate-pr` (it -writes `.harness/human-review-`, posts the build-session trail once, and halts the -feature until the human merges/closes). Detection is intentionally simple — marker -present → converge, no freshness or author check. If the reviewer never posts it, the -feedback loop reaches `FEEDBACK_CAP` and STUCKs instead (a clean PR the human merges). -Set it **empty** in `.harness/env` when there is **no reviewer**, so the dispatcher -converges at PR-open. Must match whatever `REVIEW.md` tells the reviewer to post. - -### Loop intervals (in the `/loop` invocations, not `.harness/env`) - -There are **two** loops, each its own `/loop` session in the harness host worktree: - -- `/loop 5m /poll-and-dispatch` — the **build loop** (features). `5m` is a sane - default. Shorter = more responsive, more API ticks (most are cheap no-ops). -- `/loop 10m /learn-loop` — the **memory loop** (`/learn`). It can tick lazily: it - only acts when `origin/main` has advanced past the `refs/harness/last-learned` - watermark AND no `learn/` PR is already open. Most ticks are cheap no-ops. - -Both are UX knobs, not correctness ones — the loops self-serialize (`flock`) and -re-derive their state every tick, so a missed or slow tick never corrupts anything. - -### The memory loop's watermark (`refs/harness/last-learned`) - -Not a `.harness/env` value — a git **ref** on the remote, the memory loop's only -durable state. It records how far into `main`'s history `/learn` has digested. -`learn-tick.sh` reads it as the `--since` for the next run and advances it (atomic -`--force-with-lease`) after each run. It lives in its own ref namespace, so it -survives `learn/` branch deletion and is shared across nodes — there is **no** -local `last-main-sha` file anymore. To force a full re-learn, delete the ref -(`git push origin :refs/harness/last-learned`) or run `/learn --rebuild` by hand. - -### `WORKTREE_BASE` (rarely set) - -Where per-feature worktrees are created: `${WORKTREE_BASE}-${feature}`. The -dispatcher derives the default from the **main worktree** name — `../-harness` -— so per-feature paths come out `../-harness-` regardless of which -worktree the dispatcher runs from. Override in `.harness/env` only if you want them -somewhere else (e.g. a different disk). Leave unset otherwise. - -## What does NOT go in config - -- **The claim mechanism** — it's the atomic rename, hardcoded. Not configurable. -- **Completion signals** — sentinel files, hardcoded in the dispatcher. -- **The pipeline order** — that's the `if/elif` chain in the script, edited - directly (see dispatcher-explained.md), not a config value. -- **The host-worktree sync** — done by `scripts/harness-tick.sh` (the `/loop` - target), not a config value. The loop must target the wrapper, not the dispatcher. - -## The `.harness/` directory - -Holds runtime state, all re-derivable, none of it secret: -- `env` — the config above (committed, or kept local — see below). -- `planning-attempts-`, `validate-attempts-`, `implement-attempts-`, - `local-check-attempts-`, `feedback-rounds-` — per-feature retry counters - (each gates a STUCK circuit breaker). -- `stuck-` (sentinel), `stuck-output-.log` (tee'd failing output), - `stuck-body-.md` (the PR-body composed at STUCK) — written when a cap hits; - cleared on merge/close. -- `human-review-` (sentinel — reviewer converged, PR handed to the human), - `human-review-posted-` (marks the trail comment already posted, so it posts - once), `human-review-body-.md` (the "Ready for your review" comment) — written - at convergence; cleared on merge/close. -- `sessions-.tsv` — every `claude -p` invocation for this feature, one row each - (timestamp, step, attempt, session_id, exit, duration). The STUCK PR body - quotes the tail of this file so the human can open the trace by session_id. - -**Counters and session logs must be gitignored.** They are per-node runtime state, -not project artifacts. harness-init adds `.harness/feedback-rounds-*`, -`.harness/local-check-attempts-*`, `.harness/implement-attempts-*`, -`.harness/planning-attempts-*`, `.harness/validate-attempts-*`, -`.harness/stuck-*`, `.harness/human-review-*`, `.harness/sessions-*.tsv`, and -`.harness/learn-review-body-*` to `.gitignore` (the memory loop's watermark is the -`refs/harness/last-learned` ref, not a file). Whether `.harness/env` is committed is a choice: commit it to -share defaults across a team's checkouts; keep it local (gitignored) if each -dev tunes their own. Recommend committing `env` and ignoring the counters. - -## Multi-developer reminder - -Per-dev is the default for a reason (attribution, cost isolation, "my harness is -my assistant" mental model). Don't steer a user to shared-pool unless they -explicitly describe a shared-queue workflow. When a team truly outgrows local -harnesses, the move is to transition to a server/CI harness — not to run many -local harnesses in shared-pool mode forever. diff --git a/skills/harness/harness-init/references/dispatcher-explained.md b/skills/harness/harness-init/references/dispatcher-explained.md deleted file mode 100644 index e7149ea..0000000 --- a/skills/harness/harness-init/references/dispatcher-explained.md +++ /dev/null @@ -1,270 +0,0 @@ -# The dispatcher, explained - -When you drop `assets/poll-and-dispatch.sh` into the project, walk the user -through it using this file. The goal: the user understands every section well -enough to decide whether to tweak it. Never present it as a black box. - -## What it is - -`scripts/poll-and-dispatch.sh` — ~150 lines of bash, `git`, and `gh`. No LLM in -the decision path. It runs once per tick. The `if/elif` chain is the state -machine; the artifacts on disk are the state. - -## The seven load-bearing properties - -Explain these as the "why it's safe" of the script. Each maps to an invariant. - -1. **One transition per tick per branch.** The `if/elif` fires at most one - branch. Next tick, the artifact this tick wrote satisfies the condition and - the *next* `elif` fires. Forward, one step at a time. (Inv 5) -2. **Artifacts are the only state.** A sentinel existing IS that phase being - done. No checkpoint file. Crash recovery is free — next tick re-derives from - disk. (Inv 1 + 4) Most sentinels are written by the skills; `specs//.prd-passed` - is the exception — the *dispatcher* commits+pushes it the first time - `run-prd-test.sh` exits 0, so a runner that shells out to an LLM-as-judge runs - once instead of every tick (and can't non-deterministically re-kick implement). -3. **Wipe + HEAD-check before every advance.** `git reset --hard && git clean - -fd` discards a crashed skill's uncommitted mess; the HEAD guard skips any - worktree whose branch doesn't match the feature being advanced, so a - manually-checked-out branch is never clobbered. Runs only in per-feature - worktrees. (Inv 3 + 6) -4. **Dispatcher never invokes an LLM directly.** It only spawns `claude -p` - subprocesses or shells to `git`/`gh`. Each subprocess is a fresh process and - a clean context window. (Inv 5) -5. **Local checks are optional and bounded.** If `scripts/local-checks.sh` - exists, the PR is gated on it with a two-strike retry (auto-fix, then focused - LLM fix, then stop). No script = gate skipped. (Inv 8) -6. **`MAX_WORKTREES` is the one concurrency knob.** Default 1 = FIFO single - worktree. Claims are lazy: PRDs over capacity stay in `prd//*` as the - visible queue. In-flight work always continues regardless of cap changes. - (Inv 2 + 3) -7. **Convergence has its own exit — HUMAN_REVIEW — separate from STUCK.** The - review loop's healthy end is the reviewer posting `REVIEW_CLEAN_MARKER`; - `reviewer_converged` sees it and writes `human-review-`, the guard posts the - session trail once and halts the feature for the human's `/evaluate-pr`. The - marker (not sticky review state) is the signal, so a clean PR can't false-STUCK - on a stale `COMMENTED` review. If the reviewer never marks clean, the feedback - cap STUCKs instead — fine, the human merges. - -## Section-by-section map - -| Lines (approx) | Section | Tweakable? | -|----------------|---------|------------| -| top | `flock -n` self-lock | No — serializes ticks; load-bearing. | -| config block | `SLUG`, `WATCH`, `WORKTREE_BASE`, `MAX_WORKTREES` | Via `.harness/env`, not by editing here. `WORKTREE_BASE` derives `` from the **main worktree**, not `$PWD` — so per-feature paths are `-harness-` even though the dispatcher runs from the `-harness` host worktree. | -| `has_prd()` | Invariant-2 ownership filter | No — defines what "harness-owned" means. | -| step 1 | re-attach in-flight worktrees | The `bootstrap_worktree` hook call is the local addition (see below). The PR-state **liveness gate** (skip `MERGED`/`CLOSED` features) is load-bearing — don't drop it; see "Liveness" below. | -| step 2 | lazy claim up to capacity | No — atomic rename is the claim lock. | -| step 3 | advance each feature one step | **This is where you add/remove pipeline steps** — one `elif` per step. | -| step 4 | cleanup merged/closed PRs | Safe to extend (e.g., notify on cleanup). | -| post-merge `/learn` | **NOT in this script** — runs in the separate memory loop (`learn-tick.sh`). See "The memory loop" below. | Watermark/idempotency live there. | -| step 3 (review) | the feedback gate has three branches: `reviewer_converged` (marker present) → write `human-review-`; else findings → `/address-feedback`; else cap → STUCK | Convergence is the marker; the gate order matters (converge before the findings loop). | -| helpers (shared) | `run_claude` (session-tagged invocation + TSV log), `render_sessions_table` (shared trail renderer) live in `harness-lib.sh` — both loops use them. | Edit `run_claude` in one place (e.g. the `--session-id` swap). | -| helpers (dispatcher) | `signal_stuck` (PR-body STUCK signal), `signal_human_review` (the convergence handoff comment), `reviewer_converged` (marker check), `has_prd` (ownership filter) | Used by §3 steps that call `claude -p`, hit a cap, or detect convergence. | - -## The bootstrap hook — project-owned worktree provisioning - -A bare worktree has no `node_modules`, no `.env`, no generated code — so slice -signals and the PRD runner fail for reasons unrelated to the feature. So -immediately after every `git worktree add`, the dispatcher runs -`scripts/bootstrap-worktree.sh "$wt"` if that script exists and is executable. -The hook itself is canonical (the design doc's dispatcher includes it); what's -**project-owned** is the `bootstrap-worktree.sh` it calls — harness-init generates -that per project (see `worktree-bootstrap.md`). If the project has no bootstrap -script, the hook is a no-op. - -Make sure the user knows: this hook is why the per-feature worktrees the harness -spins up are actually runnable. - -## Liveness — branch existence is not in-flight (the step-1 PR-state gate) - -`has_prd()` answers *ownership* ("is this a harness feature?"), not *liveness* -("is there still work to do?"). A `feature/` whose PR has **merged** keeps both -its branch (GitHub's delete-on-merge is off by default, and you can't make every -consumer enable it) and its committed PRD — so `has_prd()` stays true forever. -Without a liveness check, step 1 re-attaches that finished feature as in-flight on -*every* tick, which does two bad things at once: - -1. step 3 re-fires `/address-feedback` on the merged PR — wasted tokens, every tick; -2. the phantom occupies a `MAX_WORKTREES` slot, so `capacity` (step 2) drops and a - freshly-filed PRD sits unclaimed in the queue behind a feature that's already done. - -Step 4 cleanup notices the merge and removes the *local* worktree, but it runs -*after* step 3 and doesn't delete the *remote* branch — so the next tick's step 1 -re-attaches the same ghost. The fix is one gate in step 1, right after `has_prd`: -`gh pr view` the feature's PR and `continue` past any `MERGED`/`CLOSED` one. Because -the in-flight set is what feeds the capacity math, that single skip cures both -symptoms. Notes: - -- **No branch deletion.** We never `git push --delete` a merged feature branch — - the harness simply stops treating it as live work. Consumers keep whatever - branch-retention policy they like. -- **Fail-safe on empty state.** A feature still in a pre-PR phase - (planning/validate/implement) has no PR, and a transient `gh` outage also returns - empty — both are kept in-flight so live work is never silently dropped. -- **Forward-only (Inv 9).** Merged is terminal; the gate only ever skips, never rewinds. - -## The tick wrapper — keeping the host worktree current (`harness-tick.sh`) - -The dispatcher is **HEAD-agnostic**: it operates on git *refs* (`origin/feature/*`, -`feature/*`) and per-feature worktrees, never on its own checkout's HEAD. But the -files it executes — the dispatcher itself, `harness-lib.sh`, `bootstrap-worktree.sh`, -`.harness/env` — are read from the **host worktree**, which sits at a detached HEAD -and does not auto-advance when `main` moves. Without a sync step, loop infrastructure -freezes at the commit the host worktree was created on. - -So the build loop targets **`scripts/harness-tick.sh`**, not the dispatcher -directly. The wrapper: - -1. `git fetch origin` -2. `git checkout -f --detach origin/main` + `git clean -fd` — force the host worktree - to a clean, current `main` (idempotent; self-heals whatever state a crash left it in) -3. `exec ./scripts/poll-and-dispatch.sh` - -The sync lives in the wrapper, **before** `exec`, for a reason: if the dispatcher -reset its own checkout mid-run it would overwrite the very file it's executing (and -its `flock` is on `$0`). Doing it in the wrapper means the dispatcher always loads -its freshest version cleanly. The wrapper is tiny and stable; on the rare tick where -it changes on main, that change takes effect the next tick. - -Note the asymmetry: feature **pipeline** skills (`/spec-planning`, `/implement-*`, -`/address-feedback`, …) do **not** need this sync — they run inside per-feature -worktrees that branch from the PRD branch (off main), so new features pick up skill -updates on their own. Only loop-level infra rides the host worktree, and that's -exactly what the wrapper refreshes. - -## The memory loop — `/learn` as its own loop (`learn-tick.sh`) - -`/learn` is **not** a dispatcher step. It runs in a second, independent loop: -`/loop … /learn-loop` → `scripts/learn-tick.sh`. This is the deliberate decoupling -that keeps a multi-minute Expert bootstrap from blocking (or being blocked by) the -build loop. The two loops share a node but never wait on each other — separate -`flock`s on separate scripts — and coordinate only through git (Inv 1 + 7). - -What `learn-tick.sh` does each tick (it is the memory-loop analogue of -`harness-tick.sh` + the old dispatcher step 5): - -1. `flock -n` on itself (self-serialize — a long bootstrap just no-ops later ticks). -2. `git fetch` `main` **and** the watermark ref namespace - (`+refs/harness/*:refs/harness/*`). Fetch only — never the host *working tree*. -3. Ensure the dedicated, **reused** `../-harness-learn` worktree exists (create - + `bootstrap_worktree` on first run — so a drafted lint and `check-agents-md.sh` - can actually run), then reset it to clean `origin/main` (`-fd`, keeping deps). -4. **Pause** if a `learn/` PR is already open (`gh pr list … startswith("learn/")`): - memory edits serialize behind human review. Nothing is queued — the backlog is - implicit in `(watermark, origin/main)` and recomputed every tick. -5. Compute `since = refs/harness/last-learned` (or a bounded look-back on the first - run), `to = origin/main`; if equal, no-op. -6. `git ls-remote origin learn/` idempotency (cross-node first-to-push-wins). -7. Check out `learn/` in the learn worktree and run `/learn --since - --sha ` there. `/learn` writes memory, pushes the branch, opens the PR. -8. `signal_learn_review` posts the session trail on the PR (if one opened). -9. Advance `refs/harness/last-learned` to `` with an atomic CAS - (`--force-with-lease`) — the same first-to-push-wins primitive used to claim PRDs. - -**Why a ref watermark, not the old `.harness/last-main-sha` file.** A ref lives in -its own namespace nothing cleans, so it survives `learn/` branch deletion; it's -on the remote, so it's shared across nodes; and `--force-with-lease` gives a -lock-free compare-and-swap. The file was per-node local state with none of those -properties. The advance happens whether or not `/learn` opened a PR (a 0/3 no-op -merge is still "learned"), matching the old step-5 semantics — only now the anchor is -durable and shared. - -**Why this never races the build loop.** `learn-tick.sh` does all working-tree ops -(`checkout`/`reset`/`clean`) in the `-harness-learn` worktree, never the host -worktree — so the build loop's per-tick host force-sync has nothing of the memory -loop's to wipe. That is what let us delete the old `.harness/learn-running` PID guard -entirely: there is no longer a skill running *in* the host worktree to protect. - -## Human-steering points: Evaluate (HUMAN_REVIEW) and STUCK - -The human steers the machine at three touchpoints (the Human Loop — confirm a PRD, -evaluate-then-merge a PR, unstick a STUCK). Two of those live in this script. - -**HUMAN_REVIEW (the healthy one).** When the reviewer converges (posts -`REVIEW_CLEAN_MARKER`), `reviewer_converged` writes `human-review-`; the guard -posts the build-session trail once and halts the feature. The human runs -`/evaluate-pr` to walk the change, run it, and merge (or fix-and-push, or close). -The loop does not re-engage — the human is the last mile. - -**STUCK (the failure one).** Memory still has **one write path** — `/learn` in the -memory loop, post-merge, ground truth only. There is no separate write path for failed features. -Instead, **STUCK** (a step in §3 hitting its cap) is a first-class escalation: the -dispatcher posts -to the PR (opening it as a draft if needed) with the session log, the failing -output tail, and a diagnosis-first checklist, then halts the feature. The -human's first job at STUCK is to identify the **context defect** (which -`AGENTS.md` / Expert / spec / PRD content misled the agent), correct it on the -branch, *then* fix the code, *then* merge. Those context corrections ride into -main with the merge, where `/learn` picks them up and routes any follow-ups. - -This is why the design no longer has any "Path B" / `lessons.md` machinery — -the human's hands-on diagnosis + correction at STUCK is the learning, and the -merge carries it home. - -## The session log + STUCK signal - -A few small helpers do the legwork. `run_claude` and `render_sessions_table` are -**shared** — they live in `harness-lib.sh`, sourced by both the dispatcher and -`learn-tick.sh`; the rest are dispatcher-local, defined above step 1: - -- **`run_claude ""`** (shared) — wraps every - `claude -p` call. `cd`s into the worktree `` first (there is no print-mode - `--cwd` flag, and the `cd` is what gives the skill its `.claude/` command + - `AGENTS.md` discovery; the build loop passes the per-feature worktree, the memory - loop passes the `-harness-learn` worktree). - Generates a UUID session id, runs `claude -p --session-id "${CLAUDE_PERM_ARGS[@]}"`, - and appends `\t\t\t\t\t` - to `.harness/sessions-.tsv` (gitignored, cleared on merge/close). A - non-zero skill exit is recorded in the row but never aborts the tick (`return 0`). -- **`signal_stuck [output-file]`** — touches the stuck - sentinel, composes a PR body (step, cap, tail of the session log, optional tail - of the failing output, diagnosis-first checklist), and either opens a draft PR - or comments on an existing one. The single human-facing surface for failures. -- **`reviewer_converged `** — true iff a PR comment contains - `REVIEW_CLEAN_MARKER`. One `gh` call, no LLM; deliberately no freshness/author - check (see the helper's comment). Empty marker = no reviewer → false. -- **`signal_human_review `** — the convergence counterpart of - `signal_stuck`: posts the "Ready for your review" comment with the session trail - (via the shared `render_sessions_table`) so the human can `/evaluate-pr`. -- **`signal_learn_review `** — lives in **`learn-tick.sh`**, not the - dispatcher: after `/learn` opens its `learn/` PR, posts the headless session - trail (via the shared `render_sessions_table`) framed for troubleshooting *why* - `/learn` routed each fact as it did, so the human evaluating the memory PR can open - the trace before accepting or revising it. Uses a per-sha session label - (`learn-`) so the table shows exactly that run. - -If your `claude -p` version doesn't support `--session-id`, swap that flag for -`--output-format json` and parse `session_id` from stdout — `run_claude` is the -one place to change. - -## Common tweaks to offer - -- **Add a pipeline step** (e.g. `/security-review` between validate and - implement): insert one `elif` in step 3 with its own sentinel check, its own - `_CAP`, and a `run_claude` / `signal_stuck` pair like the others. -- **STUCK caps** (all built in): `PLANNING_CAP`, `VALIDATE_CAP`, `IMPLEMENT_CAP`, - `LOCAL_CHECKS_CAP`, `FEEDBACK_CAP`. All env-overridable in `.harness/env`. - Raise for hard features, lower to fail fast. -- **Memory-loop cadence** for `/learn`: it lives in `learn-tick.sh`, not here. It - fires whenever `origin/main` advanced past the `refs/harness/last-learned` watermark - and no `learn/` PR is open. A team merging many times per minute can simply run - `/loop` at a longer interval — each run batches every merge since the watermark. - -## What NOT to let the user do - -- Add `&` to any `claude -p` call (breaks synchronous one-step discipline). -- Add an LLM call to the decision logic (breaks Inv 5, makes it non-reproducible - and a cost surface). -- Remove the wipe or the HEAD guard (breaks crash recovery and Inv 6). -- Replace the atomic-rename claim with a marker file (breaks Inv 2 + 7). -- Point `/loop` at `poll-and-dispatch.sh` directly, bypassing `harness-tick.sh` - (loop-infra updates merged to main would never reach the running loop). -- Move the host-worktree sync *into* the dispatcher (it would overwrite its own - running file; the sync belongs in the wrapper, before `exec`). -- Point the memory loop's `/loop` at `poll-and-dispatch.sh`, or fold `/learn` back - into the dispatcher as a step — that re-couples the loops, so a multi-minute Expert - bootstrap again blocks every feature step (the whole reason it's a separate loop). -- Make `learn-tick.sh` do working-tree ops (`checkout`/`reset`/`clean`) in the **host** - worktree instead of `-harness-learn` — it would then race the build loop's - per-tick host force-sync. The memory loop must stay in its own worktree. diff --git a/skills/harness/harness-init/references/mental-model.md b/skills/harness/harness-init/references/mental-model.md deleted file mode 100644 index d266946..0000000 --- a/skills/harness/harness-init/references/mental-model.md +++ /dev/null @@ -1,110 +0,0 @@ -# Mental model — what the harness is - -Read this first. It is the condensed version of the design doc, written so the -skill can explain the harness to a developer who has never seen it. When you -walk a user through setup, narrate from this — do not assume they have read the -design doc. - -## One sentence - -The harness is a polling loop that watches a git branch namespace, and for each -feature in flight, runs exactly one spec-driven-development step per tick in an -isolated worktree, until a PR is open against `main` for a human to merge. - -## Three layers, independent - -``` -OUTER LOOP a long-lived Claude Code session (/loop 5m), or cron, or CI. - | Owns TIMING only. Knows nothing about the work. - v -TICK WRAPPER scripts/harness-tick.sh. Syncs the host worktree to a clean - | origin/main, then execs the dispatcher. Keeps loop infra fresh. - v -DISPATCHER scripts/poll-and-dispatch.sh. Pure bash, zero LLM calls. - | Owns ROUTING: reads disk, decides the next skill per branch, - v shells out. The if/elif chain IS the state machine. -INNER LOOP one fresh `claude -p "/skill ..."` subprocess per step. - Owns THE WORK. Fresh context every time, exits when done. -``` - -You can swap any layer without touching the others. harness-init sets up the -outer loop, the tick wrapper, and the dispatcher; the inner skills come from the -catalog. - -This stack is the **build loop** (features). There is a second, independent loop -with the same shape — the **memory loop**: `/loop … /learn-loop` → -`scripts/learn-tick.sh` → one `claude -p "/learn …"` in the dedicated -`-harness-learn` worktree. It runs post-merge memory updates without ever -blocking (or being blocked by) the build loop; the two coordinate only through git -(the `refs/harness/last-learned` watermark ref). Same three-layer discipline, its own -`/loop` session. - -## Three checkouts (the worktree topology) - -``` -/ the human's checkout. On `main`. Never touched by the harness. (Inv 6) --harness/ the harness HOST worktree. Detached at origin/main; runs BOTH loops' - outer sessions. harness-tick.sh re-syncs it to main every tick. Does - NOT move between feature branches. --harness-/ EPHEMERAL per-feature worktree. One per in-flight feature; the - dispatcher creates it (off feature/) and tears it down on - merge/close. (Inv 3: worktree ↔ branch 1:1.) --harness-learn/ the memory loop's dedicated worktree. Persistent + reused; learn-tick.sh - resets it to a clean learn/ off main each run. Where /learn runs. -``` - -Two reasons the host is **detached**, not on a `main` branch: git refuses to check -out `main` in two worktrees at once (the human's checkout holds it), and the host -never needs a writable branch of its own — it only reads loop infra and spawns -per-feature worktrees. `WORKTREE_BASE` derives `` from the **main worktree** -(not `$PWD`), so per-feature paths are always `-harness-` no matter -which worktree the dispatcher runs from. - -## Why the split matters for setup - -- **The outer loop must never do real work.** `/loop 5m /poll-and-dispatch` - re-runs in the *same* session every tick; if it did work there, context would - bloat and rot by tick 10. The shim skill does one thing: call the script. All - real work happens in `claude -p` subprocesses with fresh context. -- **The dispatcher is deterministic.** No model in the decision path. This is - what makes crash recovery free and behavior predictable from disk state. - -## The state machine in one table - -| Branch state | Means | Next action the dispatcher takes | -|------------------------|--------------------|-----------------------------------------| -| `prd//` | waiting in queue | atomic-rename to `feature/` (claim) | -| `feature/` no specs | claimed, unplanned | `/spec-planning` | -| `.planning-done` | planned | `/spec-validate` | -| `.validated` | validated | `/implement-mainspec` until runner == 0 | -| runner exits 0 | implemented | open PR against `main` | -| PR has findings | in review | `/address-feedback` (bounded) | -| PR merged | done | cleanup worktree (the memory loop runs `/learn` separately) | - -"Done" is one thing only: `./prds//run-prd-test.sh` exits 0. The harness -contracts on that exit code and nothing else. - -## The artifacts ARE the state - -There is no database, no in-memory queue, no "what step are we on" file. A -sentinel file existing IS that phase being done. The branch namespace IS the -work registry. Counters live in `.harness/`. Everything the dispatcher needs is -re-derivable from `git` + the filesystem on every tick. That is the whole -recovery story: `kill -9` mid-step leaves nothing the next tick can't sort out. - -## Where the human steers - -Exactly two points: -1. **Confirming a PRD** (via `/intent`, in their own checkout). -2. **Merging a PR** (the feature PR, and later the `/learn` memory PR). - -Everything between is the harness. The loop never merges — that is the steering -input the system is built around. - -## What harness-init is actually standing up - -Both outer loops (two `/loop` sessions — build + memory), the dispatcher and -`learn-tick.sh` scripts plus their shared `harness-lib.sh`, the config, the -agent contract (`AGENTS.md`), and the provisioning that makes per-feature -worktrees runnable. The inner skills (`/intent`, `/spec-planning`, etc.) are -installed from the catalog; harness-init wires them, it does not author them. diff --git a/skills/harness/harness-init/references/worktree-bootstrap.md b/skills/harness/harness-init/references/worktree-bootstrap.md deleted file mode 100644 index bb80de1..0000000 --- a/skills/harness/harness-init/references/worktree-bootstrap.md +++ /dev/null @@ -1,95 +0,0 @@ -# Worktree bootstrap — making a fresh worktree runnable - -`scripts/bootstrap-worktree.sh` provisions a newly created git worktree so the -project can actually run inside it. This is Software 3.0: the script is -generated by reading the project (see project-discovery.md), explained to the -user, and confirmed before it's written. - -## Why it exists - -`git worktree add` gives you a checkout of tracked files only. Everything in -`.gitignore` — `node_modules`, `target`, `.venv`, `.env`, generated code, build -output — does NOT come along. So a fresh worktree is like a new developer's -laptop on day one: the code is there, but it won't run until someone installs -deps, drops in secrets, and runs codegen. - -The dispatcher creates a worktree per feature. Without bootstrap, slice signals -and `run-prd-test.sh` fail for reasons that have nothing to do with the feature -(missing deps, missing `.env`). Bootstrap closes that gap. - -## Two callers - -1. **harness-init**, once, against the host worktree — also serves as the - script's own validation (if the host worktree runs after bootstrap, the - script works). -2. **the dispatcher**, after every `git worktree add` (the hook documented in - dispatcher-explained.md). - -So the script must be **idempotent** and **non-interactive** — it runs -unattended inside the loop. - -## Contract - -``` -scripts/bootstrap-worktree.sh -``` - -- Takes the target worktree path as its one argument. -- Determines the **source checkout** (the human's primary checkout, where the - real gitignored files live) via `git -C rev-parse --git-common-dir` - → its parent is the main worktree. Use that as the copy source. -- Exits 0 on success, non-zero with a clear message on failure. -- Re-running on an already-provisioned worktree is a cheap no-op (skip installs - if the lockfile is unchanged, skip copying a file that already exists and - matches, etc.). - -## What it typically does, in order - -1. **Copy gitignored runtime files** from the source checkout: `.env`, - `.env.local`, local credentials, certs, local config — whatever - `.env.example` and `.gitignore` reveal the project needs. Copy only files - that exist in the source; never fail because an optional one is absent. -2. **Install dependencies** using the project's package manager and lockfile - (`npm ci`, `pnpm i --frozen-lockfile`, `uv sync`, `cargo fetch`, - `go mod download`, `bundle install`, ...). -3. **Run codegen** if the project needs it before it runs (`prisma generate`, - protobuf/GraphQL codegen, `sqlc generate`, migrations against a local/test - DB, ...). -4. **Any project-specific "make it runnable" step** the README/CI revealed. - -## The secret-copying caution — flag this loudly - -Step 1 copies REAL secrets (`.env` with live keys, credential files) from the -human's checkout into the sibling worktree. This is the one place the harness -touches sensitive files. When you present this step: - -- Say explicitly what files will be copied and from where. -- Confirm the destination is a `*-harness*` worktree on the same machine, owned - by the same user — secrets don't leave the box. -- Copy direction is **always** source-checkout → worktree. Never the reverse. - Never delete from the source. (Invariant 6.) -- If the project has no gitignored secrets (e.g. all config via real env vars or - a secrets manager), skip step 1 entirely and say so. -- Get explicit consent before writing a script that copies secrets. - -## Things to get right - -- **Idempotency.** It runs on every `git worktree add`. Guard expensive steps - (`[[ -d node_modules ]] && lockfile unchanged → skip install`). -- **Non-interactive.** No prompts; it runs in the loop. Use `--frozen`/`ci` - install flavors that don't prompt. -- **Port awareness.** If codegen or a setup step starts a server, it can collide - with the human's dev server. Prefer steps that don't bind ports; if - unavoidable, read the port from env (note this in the script's comments). -- **Fail loudly, not silently.** A worktree that's missing deps should make - bootstrap exit non-zero with a clear message, not limp along — otherwise the - failure surfaces later as a confusing PRD-runner failure. -- **Comment the script.** The human owns it. Every step gets a one-line comment - saying what it provisions and why, so they can tweak it. - -## If discovery comes up empty - -If you cannot determine how the project installs/runs (no manifest, sparse -README, no CI), do not guess. Ask the user directly: "How do you set up this -project on a fresh machine? What do you install, and what secret/config files -do you copy in?" Their answer becomes the script. diff --git a/skills/harness/harness-init/scripts/preflight.sh b/skills/harness/harness-init/scripts/preflight.sh deleted file mode 100755 index 2a918a8..0000000 --- a/skills/harness/harness-init/scripts/preflight.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash -# preflight.sh — deterministic environment + inventory report for /harness-init. -# Prints a checklist; does NOT gate. The skill reads this and decides what to do. -# Run from the consumer project's root (the human's checkout). - -ok() { printf ' [ok] %s\n' "$1"; } -warn() { printf ' [warn] %s\n' "$1"; } -miss() { printf ' [MISS] %s\n' "$1"; } -have() { command -v "$1" >/dev/null 2>&1; } - -echo "=== Environment ===" -if git rev-parse --git-dir >/dev/null 2>&1; then - ok "git repository" - branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "?") - [[ "$branch" == "main" || "$branch" == "master" ]] && ok "on $branch" || warn "on '$branch' (expected main)" - git diff --quiet && git diff --cached --quiet 2>/dev/null && ok "working tree clean" || warn "working tree has uncommitted changes" - git remote get-url origin >/dev/null 2>&1 && ok "origin remote: $(git remote get-url origin 2>/dev/null)" || warn "no 'origin' remote (needed for branch-as-queue)" - echo " git email: $(git config user.email 2>/dev/null || echo '(unset!)')" - echo " derived slug: $(git config user.email 2>/dev/null | cut -d@ -f1)" -else - miss "not a git repository — run 'git init' and add an origin remote first" -fi -have git && ok "git CLI" || miss "git CLI" -have gh && { gh auth status >/dev/null 2>&1 && ok "gh CLI (authenticated)" || warn "gh CLI present but not authenticated (run: gh auth login)"; } || miss "gh CLI (needed for PR ops)" -have claude && ok "claude CLI" || warn "claude CLI not on PATH (the dispatcher shells out to 'claude -p')" - -echo -echo "=== Toolchain signals (for AGENTS.md / local-checks / bootstrap) ===" -declare -A M=( - [package.json]="Node" [pnpm-lock.yaml]="pnpm" [yarn.lock]="yarn" [bun.lockb]="bun" - [pyproject.toml]="Python" [requirements.txt]="pip" [uv.lock]="uv" [Pipfile]="pipenv" - [Cargo.toml]="Rust" [go.mod]="Go" [Gemfile]="Ruby" [pom.xml]="Maven" - [build.gradle]="Gradle" [composer.json]="PHP" [mix.exs]="Elixir" -) -found=0 -for f in "${!M[@]}"; do [[ -e "$f" ]] && { ok "${M[$f]} ($f)"; found=1; }; done -(( found )) || warn "no dependency manifest found at root — discovery will rely on README/CI or asking" -for f in .env.example .env.sample .env.template; do [[ -e "$f" ]] && warn "secrets template: $f (bootstrap will need to copy the real file)"; done -[[ -e Makefile || -e Justfile || -e Taskfile.yml ]] && ok "task runner present (read it for canonical commands)" -[[ -d .github/workflows ]] && ok ".github/workflows present (read CI for setup+test recipe)" -[[ -e prisma/schema.prisma ]] && warn "prisma — bootstrap likely needs 'prisma generate'" -ls ./*.proto >/dev/null 2>&1 && warn "protobuf — bootstrap likely needs a codegen step" - -echo -echo "=== Inner skills (chain the dispatcher calls) ===" -SK=.claude/skills -for s in intent spec-planning spec-validate implement-mainspec fix-local-checks address-feedback learn expert; do - if find "$SK" -type f -path "*/$s/SKILL.md" 2>/dev/null | grep -q . ; then ok "/$s" - elif find "$SK" -type d -name "$s" 2>/dev/null | grep -q . ; then warn "/$s (dir present but no SKILL.md — stub?)" - else miss "/$s (not installed)"; fi -done - -echo -echo "=== Harness artifacts (re-run / already-present check) ===" -[[ -f scripts/poll-and-dispatch.sh ]] && warn "scripts/poll-and-dispatch.sh exists (re-run?)" || ok "scripts/poll-and-dispatch.sh absent (fresh)" -[[ -f scripts/harness-tick.sh ]] && warn "scripts/harness-tick.sh exists (re-run?)" || ok "scripts/harness-tick.sh absent (fresh)" -[[ -f scripts/harness-lib.sh ]] && warn "scripts/harness-lib.sh exists (re-run?)" || ok "scripts/harness-lib.sh absent (fresh)" -[[ -f scripts/learn-tick.sh ]] && warn "scripts/learn-tick.sh exists (re-run?)" || ok "scripts/learn-tick.sh absent (fresh)" -[[ -f .harness/env ]] && warn ".harness/env exists (re-run?)" || ok ".harness/env absent (fresh)" -[[ -s AGENTS.md ]] && warn "AGENTS.md is non-empty (re-run? will diff)" || ok "AGENTS.md absent/empty (fresh)" -[[ -f scripts/bootstrap-worktree.sh ]] && warn "scripts/bootstrap-worktree.sh exists (re-run?)" || ok "scripts/bootstrap-worktree.sh absent (fresh)" -[[ -f REVIEW.md ]] && warn "REVIEW.md exists" || ok "REVIEW.md absent" -# Preflight runs from the HUMAN's checkout (bare repo name), so basename "$PWD" is -# the right base for the expected host-worktree path here. (The dispatcher, which -# runs from the host worktree, instead derives this from the main worktree.) -base="../$(basename "$PWD")-harness" -[[ -d "$base" ]] && warn "host worktree $base already exists" || ok "host worktree $base absent (fresh)" - -echo -echo "Preflight complete. [MISS] items block; [warn] items need a decision." diff --git a/skills/harness/learn/SKILL.md b/skills/harness/learn/SKILL.md index 88da022..e1336c5 100644 --- a/skills/harness/learn/SKILL.md +++ b/skills/harness/learn/SKILL.md @@ -1,6 +1,6 @@ --- name: learn -description: Update the project's long-term memory after a merge to main. Reads the merged diff and the current memory, then writes Expert shards, discovered invariants, candidate lints, and AGENTS.md pointers — all on a reviewable learn/ PR. Use post-merge (the harness invokes it automatically) or with --rebuild to regenerate memory from scratch. Triggers - learn, expert-update, update memory, update expert, post-merge memory, self-improve (project) +description: Update the project's long-term memory after a merge to main. Reads the merged diff and reconciles the current memory — adding, editing, and deleting Expert reference files — then drafts candidate lints and AGENTS.md pointers. Opens a reviewable learn/ PR, or prints "nothing to learn" and exits if the merge produced no material change. Use post-merge (the harness invokes it automatically) or with --rebuild to regenerate memory from scratch. Triggers - learn, expert-update, update memory, update expert, post-merge memory, self-improve (project) --- # learn @@ -12,10 +12,10 @@ updates the **Expert** (procedural + semantic memory, pulled on demand) and the project invariants, and drafts candidate **lints** (the highest-value memory, because a lint is a rule the agent cannot ship past). -You run **headless**, invoked by the **memory loop** (`scripts/learn-tick.sh`, driven -by its own `/loop … /learn-loop`) as the post-merge step — in a dedicated +You run **headless**, invoked by the **memory loop** (`scripts/learn-dispatch.sh` in the harness repo, driven +by the `context-specs` supervisor on its own interval) as the post-merge step — in a dedicated `../-harness-learn` worktree on a `learn/` branch off `origin/main`, never -in the build loop's host worktree. The memory loop runs independently of the +in the developer's clone. The memory loop runs independently of the feature/build loop, so a from-scratch Expert bootstrap blocks neither. Your output is a single reviewable PR — never an auto-merge. Humans steer at merge. @@ -42,28 +42,42 @@ into main with the feature merge, and **you observe them in the diff you read**. the bar for putting something in AGENTS.md is **far higher** than for the Expert. - **P3 — The four destinations.** Every fact worth remembering routes to exactly one place: a **lint** (if mechanically checkable), **eager prose** (AGENTS.md, if - it clears the high bar), **lazy prose** (an Expert shard), or **nowhere**. Most - things go nowhere or to the Expert. See `references/routing-rules.md`. + it clears the high bar), **lazy prose** (an Expert reference file — one of + `how-to-*` / `concept-*` / `pattern-*` / `invariant-*` / `example-*`), or + **nowhere**. Most things go nowhere or to the Expert. See + `references/routing-rules.md` and `references/expert-structure.md`. - **P4 — Map, not encyclopedia.** AGENTS.md is the table of contents that *points into* the Expert; it never duplicates it. A monolithic AGENTS.md rots, crowds out the task, and turns "everything important" into "nothing important." Keep it a map. See `references/agents-md-guidance.md`. -- **P5 — Consensus gates a write.** A few cheap reviewers vote per surface before - anything changes. Routine merges (vuln fixes, refactors that don't change shape) - typically produce no change — that's correct, not a failure. See - `references/consensus.md`. +- **P5 — Progressive disclosure inside the Expert.** Reference files are small and + topic-focused, cross-linked via Obsidian `[[wikilinks]]`. SKILL.md is an index — + one line per file. An agent reads the index, opens only what's relevant, then + follows wikilinks to discover related context. See + `references/expert-structure.md` and `references/wikilink-convention.md`. - **P6 — Invariants are discovered, then promoted.** You may notice architectural - rules the codebase upholds. Record them as prose first; flag the mechanically - checkable ones as candidate **lints** (the highest-value memory, because a lint - is a rule the agent *cannot* ship past). See `references/invariant-discovery.md`. + rules the codebase upholds. Record each as its own `invariant-.md` file + (one rule per file); flag the mechanically checkable ones as candidate **lints** + (the highest-value memory, because a lint is a rule the agent *cannot* ship + past). See `references/invariant-discovery.md`. - **P7 — Human-authored memory edits are authoritative.** When the merged diff - *already* touches AGENTS.md, an Expert shard, or a spec, treat those changes as - **ground truth, not as a proposal to second-guess.** They came from a human - resolving a STUCK or making a deliberate correction. Your job there is to - *extend* (what else, given this correction, now needs to change?) — not to - vote on whether to apply it. -- **P8 — Reviewable, revertible, human-merged.** Everything lands on `learn/` - with a changelog entry citing the merge. Never auto-merge. + *already* touches AGENTS.md, an Expert reference file, or a spec, treat those + changes as **ground truth, not as a proposal to second-guess.** They came from a + human resolving a STUCK or making a deliberate correction. Your job there is to + *extend* (what else, given this correction, now needs to change?) — not to vote + on whether to apply it. +- **P8 — Reviewable, revertible, human-merged.** Everything lands on a `learn/` + PR. Never auto-merge. +- **P9 — Reconcile, don't accumulate.** Memory is a *current model of main*, not + an append-only log. Every run must look for deleted concepts (the code is gone → + the reference file goes), inter-file contradictions (two files disagree → merge + or scope-qualify), and claims invalidated by the merged diff. **Adds, edits, and + deletes** all ride on the same PR. See `references/reconcile.md`. +- **P10 — Nothing to learn is a valid outcome.** Most merges produce nothing for + memory. When `reconcile` finds zero adds, edits, deletes, AGENTS.md pointer + changes, or candidate lints, print `learn: nothing to learn from , skipping + PR` to stdout, exit 0, and do not push a branch or open a PR. The outer loop + advances `refs/harness/last-learned` regardless. ## How to run this skill @@ -71,82 +85,125 @@ Read the seam references before acting; they are the hackable contract a project tunes to its taste: - `references/routing-rules.md` — the four destinations + the five-predicate test - for eager placement + the line-count caps. *(Primary hackable seam.)* -- `references/expert-shards.md` — the shard taxonomy and templates. + for eager placement + the five sub-destinations for the Expert + the line-count + caps. *(Primary hackable seam.)* +- `references/expert-structure.md` — the flat prefix-named layout, the five + prefixes, the SKILL.md index, the bootstrap seed list. +- `references/wikilink-convention.md` — the `[[basename]]` cross-link syntax used + inside `expert/references/` and the lint that enforces it. +- `references/reconcile.md` — the three-pass add/edit/delete logic plus the + nothing-to-learn early exit. - `references/agents-md-guidance.md` — map-not-manual; which folders earn a nested AGENTS.md; the freshness contract. - `references/invariant-discovery.md` — how to surface invariants and draft lints without overfitting. -- `references/consensus.md` — the voting threshold (2/3 default) and how to run it. ## The flow ### Step 0 — Mode + idempotency Determine mode: -- **Bootstrap** — no `.claude/skills/expert/` exists (or `--rebuild`): create the - Expert from scratch by scanning committed code, seed all shards + the root - `AGENTS.md`. This is also the recovery path (see `--rebuild`). -- **Incremental** — the Expert exists: reconcile against the merged diff. +- **Bootstrap** — `.claude/skills/expert/` is empty (no `SKILL.md` index inside), + or `--rebuild` was passed: create the Expert from scratch by scanning committed + code, seed the minimum-viable file set (see `references/expert-structure.md`) + + the root `AGENTS.md`. This is also the recovery path. Check for the presence + of `SKILL.md` inside the dir, not for the dir itself. +- **Incremental** — the Expert exists (contains a `SKILL.md`): reconcile against the merged diff. Idempotency: if `learn/` already exists on origin, another node handled this merge — exit cleanly. (The memory loop pre-checks this too, via `git ls-remote`.) ### Step 1 — Gather -Read: the diff for `--since ..--sha `; the current Expert shards -(`.claude/skills/expert/references/*.md`); the AGENTS.md files the diff touches -(root + any in changed folders); and the PRD(s)/spec(s) for the merged feature if -present (`prds//`, `specs//`) for the *why*. - -**Notice whether the diff itself touches memory files** (AGENTS.md, Expert shards, -spec sections). If so, you're looking at a human's context correction — see P7; -those changes are ground truth. - -### Step 2 — Consensus -Spawn 2–3 cheap parallel reviewers (`references/consensus.md`). Each reads the diff -+ current memory and votes, **per surface**, on whether anything must change: -shards, invariants, AGENTS.md. (No votes against human-authored memory edits — P7.) -Keep only changes that clear the threshold. A 0/3 on every surface is a valid, -common outcome — log it and no-op. +Read: the diff for `--since ..--sha `; the current Expert reference +files (`ls .claude/skills/expert/references/*.md` plus their contents); the +AGENTS.md files the diff touches (root + any in changed folders); and the +PRD(s)/spec(s) for the merged feature if present (`prds//`, `specs//`) for +the *why*. + +**Notice whether the diff itself touches memory files** (AGENTS.md, Expert +reference files, spec sections). If so, you're looking at a human's context +correction — see P7; those changes are ground truth. + +### Step 2 — Reconcile +Run the three-pass reconcile (`references/reconcile.md`) in the main agent — no +subagent fanout. Produce three candidate lists: `adds[]`, `edits[]`, `deletes[]`. +Every candidate carries a **one-line justification** citing the diff hunk +(file:line range) or the contradicting reference file. If you can't write the +justification, drop the candidate. + +The three passes: +1. **Deleted-concept** — references whose anchor code is gone → `delete`. +2. **Inter-file contradiction** — pairs that disagree → rewrite both to + scope-qualify, merge them, or drop the stale one. +3. **Diff-invalidates-file** — references contradicted by the merge → `edit`. ### Step 3 — Route -For each surviving candidate fact, apply `references/routing-rules.md`: lint / -eager AGENTS.md / lazy Expert shard / nowhere. When in doubt, prefer the Expert -(cheap) over AGENTS.md (eager), and prefer *nothing* over noise. +For each surviving candidate, apply `references/routing-rules.md`: lint / +eager AGENTS.md / lazy Expert reference (then pick the prefix: `how-to-*` / +`concept-*` / `pattern-*` / `invariant-*` / `example-*`) / nowhere. When in +doubt, prefer the Expert (cheap) over AGENTS.md (eager), and prefer *nothing* +over noise. + +### Step 3.5 — Nothing-to-learn check +If after Reconcile + Route the totals are empty: + +``` +len(adds) + len(edits) + len(deletes) == 0 +AND no AGENTS.md pointer change +AND no candidate lint +``` + +…print `learn: nothing to learn from , skipping PR` to stdout, exit 0. +Do **not** push a branch, do **not** open a PR, do **not** commit. The outer +loop advances `refs/harness/last-learned` regardless. ### Step 4 — Write -- **Expert shards** — apply diffs to the relevant shard(s); add to `core-files.md`, - `patterns.md`, etc. -- **invariants.md** — add any discovered hard rules. For mechanically checkable - ones, draft a candidate lint (code + remediation-message-as-prompt) under - `scripts/lints/` and wire it into `scripts/local-checks.sh`. **The drafted lint - MUST pass against the just-merged code before you include it** — run it; if it - fails on current main it's wrong (the inverse of `/intent`'s right-reason check). +- **Expert references** — apply adds/edits/deletes to + `.claude/skills/expert/references/`. New files use the prefix layout + (`how-to-.md`, `concept-.md`, etc.). Cross-link to related + files with `[[wikilinks]]` (no path, no extension). Update + `.claude/skills/expert/SKILL.md`'s one-line-per-file index to match the new set. +- **Invariants** — each discovered hard rule lands as its own + `invariant-.md` (one rule per file). For mechanically checkable ones, + draft a candidate lint (code + remediation-message-as-prompt) under + `scripts/lints/` and wire it into `scripts/local-checks.sh`. **The drafted + lint MUST pass against the just-merged code before you include it** — run it; + if it fails on current main it's wrong. - **AGENTS.md** — update the relevant map's pointer(s). Propose a *new* nested AGENTS.md only when the merge introduced a folder-local rule that clears the - five-predicate bar and no file exists. Stay under the caps. + five-predicate bar and no file exists. Stay under the caps. AGENTS.md uses + regular `[text](path)` markdown links — **not** wikilinks. ### Step 5 — Validate -Run `scripts/check-agents-md.sh` (referenced paths exist, cross-links resolve, -under caps). Re-run any drafted lint against current main (must pass). +Run `scripts/check-agents-md.sh` (AGENTS.md pointers exist, cross-links resolve, +under caps) and `scripts/check-expert-links.sh` (every `[[wikilink]]` resolves +to a file in `expert/references/`). Re-run any drafted lint against current +main (must pass). -### Step 6 — Changelog + PR -Append a `changelog.md` entry citing the merge sha and listing every surface -touched and the consensus vote. Open the `learn/` PR. Done. +### Step 6 — Open PR +Commit the changes and open the `learn/` PR. The PR body lists every +add/edit/delete with its one-line justification. **No changelog file is +appended** — the git history of `learn/` PRs is the changelog. ## Invocation & output contract - **Invoked by:** the memory loop, `claude -p "/learn --since --sha "`, inside the `../-harness-learn` worktree. The `--since` is the `refs/harness/last-learned` watermark (how far memory has already been digested); `--sha` is current `origin/main`. Also runnable by a human with `--rebuild` - (regenerate memory from main). -- **Writes:** under `.claude/skills/expert/` (shards incl. `invariants.md` and - `changelog.md`), `scripts/lints/*` + `scripts/local-checks.sh` wiring, and - `AGENTS.md` files across the repo. -- **Completion signal:** the pushed `learn/` branch + open PR. Idempotent via - `git ls-remote origin learn/`. After the PR opens, the memory loop advances - `refs/harness/last-learned` to `` (atomic CAS) and pauses new runs until you - merge or close the PR. -- **Session trail:** after the PR opens, the memory loop (`signal_learn_review`) + (regenerate memory from main) or `--dry-run` (print the candidate set plus + justifications; open no PR; useful for evaluation runs). +- **Writes:** under `.claude/skills/expert/` (prefix-named reference files plus + the `SKILL.md` index), `scripts/lints/*` + `scripts/local-checks.sh` wiring, + and `AGENTS.md` files across the repo. **No `changelog.md`** — the git history + of `learn/` PRs is the changelog. +- **Completion signal — two valid outcomes:** + - **PR opened.** The pushed `learn/` branch + open PR. Idempotent via + `git ls-remote origin learn/`. After the PR opens, the memory loop + advances `refs/harness/last-learned` to `` (atomic CAS) and pauses new + runs until you merge or close the PR. + - **Nothing to learn.** Stdout `learn: nothing to learn from , skipping + PR`; exit 0; no branch pushed. The memory loop advances the watermark + regardless, so the next merge is picked up cleanly. +- **Session trail:** when a PR opens, the memory loop (`signal_learn_review`) attaches this run's headless `claude -p` session as a PR comment, so the human evaluating the memory changes can open the trace and troubleshoot *why* `/learn` routed a fact as it did. No action needed in this skill — the loop posts it. @@ -158,8 +215,15 @@ touched and the consensus vote. Open the `learn/` PR. Done. the caps (P4). - **Never include a candidate lint you haven't run against current main.** A lint that reddens the merge it was born from is wrong. -- **Never frame an invariant as taste.** If it needs judgment, it's a pattern - (Expert), not an invariant (lint/hard rule). +- **Never frame an invariant as taste.** If it needs judgment, it's a `pattern-*` + file (Expert), not an `invariant-*` file (lint/hard rule). - **Never second-guess a memory edit that's already in the merged diff** (P7) — the human resolved a STUCK or made a deliberate correction; your job is to *extend* it, not vote on it. +- **Never accumulate without reconciling** (P9). A concept deleted from code is a + reference file deleted in this PR. Inbound wikilinks get rewritten in the same + commit. +- **Never write a wikilink that does not resolve** to a file in + `expert/references/`. `check-expert-links.sh` will fail the PR. +- **Never open a PR for nothing-to-learn** (P10). Print the stdout line, exit 0, + push nothing. diff --git a/skills/harness/learn/references/agents-md-guidance.md b/skills/harness/learn/references/agents-md-guidance.md index 8771782..a951d45 100644 --- a/skills/harness/learn/references/agents-md-guidance.md +++ b/skills/harness/learn/references/agents-md-guidance.md @@ -36,6 +36,11 @@ path it references exists, cross-links resolve, and it's under the line cap. Wir this into `local-checks.sh`/CI so a stale pointer fails the build. This is the same "enforce invariants mechanically" instinct turned on the memory files themselves. +> AGENTS.md uses regular `[text](path)` markdown links — **not** wikilinks. The +> Obsidian-style `[[wikilinks]]` convention is internal to `expert/references/` +> only; it has its own validator (`check-expert-links.sh`). Keep the two link +> styles separate so each validator stays simple. + ## Caps (tweakable, enforced by check-agents-md.sh) - Root: ≤ 150 lines. - Nested: ≤ 80 lines. diff --git a/skills/harness/learn/references/consensus.md b/skills/harness/learn/references/consensus.md deleted file mode 100644 index 70dd0be..0000000 --- a/skills/harness/learn/references/consensus.md +++ /dev/null @@ -1,35 +0,0 @@ -# Consensus - -**Hackable seam.** The voting threshold that gates a memory write. Mirrors the -multi-agent consensus `/spec-validate` already uses, so the project has one mental -model for "a panel decided," not two. - -## Why consensus here -Memory is long-lived and compounding. A wrong write poisons every future session -that reads it. The trade is cheap: a few extra LLM calls per merge in exchange for -not corrupting long-term memory on every commit. Most merges (vuln fixes, refactors -that don't change shape, routine bug fixes) produce **no change** — 0/3 across all -surfaces is the common, correct outcome. - -## How to run it -1. Spawn **2–3** parallel reviewers (cheap model is fine — this is judgment over a - bounded diff, not generation). -2. Give each: the merged diff, the current Expert shards, the touched AGENTS.md - files, and the PRD/spec for the *why*. -3. Each votes **per surface** (shards / invariants / AGENTS.md): does anything - need to change, and if so what? -4. Apply only changes that meet the **threshold (default 2/3)**. Discard 1/3. - -## Tuning -- Raise to **3/3** for a stricter bar (fewer, higher-confidence writes). -- Lower to a single reviewer only for low-stakes projects that want speed over - precision — not recommended for invariant/lint promotion, which should stay - consensus-gated *and* human-confirmed. -- **Human-authored memory edits already in the diff bypass voting** (see SKILL.md - P7) — they're ground truth. Vote on what to *extend*, not whether to apply them. - -## Drift signal -If consensus returns 0/3 for *many* consecutive merges, that's worth noticing: the -Expert may be too coarse to capture what's changing, or the project is genuinely -stable. Either way, it's an observability signal, not a bug — surface it, don't -force a write. diff --git a/skills/harness/learn/references/expert-shards.md b/skills/harness/learn/references/expert-shards.md deleted file mode 100644 index c1f9be1..0000000 --- a/skills/harness/learn/references/expert-shards.md +++ /dev/null @@ -1,44 +0,0 @@ -# Expert shard taxonomy - -**Hackable seam.** A project may add, rename, or split shards. These are the -defaults. The Expert is the project's *lazy* memory — pulled on demand by `/intent`, -`/spec-planning`, `/spec-validate`, and `/implement-*` via the catalog, or read -directly. It is shaped exactly like a framework expert (so it composes with them): - -``` -.claude/skills/expert/ -├── SKILL.md # Expert Mode entry: quick-reference table → references/ -├── references/ -│ ├── architecture.md # Decisions, layering, boundaries, the "why" of the shape -│ ├── verification.md # How features get tested/verified in THIS project -│ ├── patterns.md # Soft DO/DON'T, with examples (taste, judgment) -│ ├── procedural.md # "How to add a new feature here" — the steps -│ ├── core-files.md # Key files & abstractions, with paths -│ ├── invariants.md # HARD rules the codebase upholds (see below) -│ └── changelog.md # Provenance: what /learn changed, when, why, vote -└── scripts/ - └── (optional signal scripts, like any expert) -``` - -## The shard this design adds: invariants.md (hard rules, distinct from patterns.md) - -`patterns.md` is *soft* ("prefer composition here"); `invariants.md` is *hard* -("Repo layer never imports Service"). The distinguishing test: **can it be -mechanically checked?** If yes, it's an invariant and the highest-value ones become -lints (see `invariant-discovery.md`). If it needs judgment, it's a pattern. - -Each invariant entry: the rule, *why* it holds, and — once promoted — a pointer to -its lint (`scripts/lints/.sh`). - -## SKILL.md shape -Mirror the framework-expert shape (`expert-sdd-creator`'s `expert-structure.md`): a -short Expert-Mode entry with a quick-reference table mapping topics → reference -files. The body stays small; the references hold the density. Front-matter -`description` ends with `(project)` and lists triggers so the catalog can activate -it during the SDD phases. - -## Bootstrap / --rebuild -On a fresh project (no Expert) or `--rebuild`, seed every shard by scanning -committed code: derive `core-files.md` from the actual tree, `verification.md` from -the test/CI setup, `architecture.md` from the module layering, `invariants.md` from -rules the code visibly upholds (conservatively — see `invariant-discovery.md`). diff --git a/skills/harness/learn/references/expert-structure.md b/skills/harness/learn/references/expert-structure.md new file mode 100644 index 0000000..63cc8a6 --- /dev/null +++ b/skills/harness/learn/references/expert-structure.md @@ -0,0 +1,129 @@ +# Expert structure + +**Hackable seam.** A project may add, rename, or split files. These are the +defaults. The Expert is the project's *lazy* memory — pulled on demand by `/intent`, +`/spec-planning`, `/spec-validate`, and `/implement-*` via the catalog, or read +directly. + +The Expert is a **flat directory of many small, topic-focused files**, each named +by a prefix that tells the agent (and `/learn`) what kind of memory it is. Files +cross-link to each other with Obsidian-style `[[wikilinks]]` so an agent reading +one file can discover related context by following links. `SKILL.md` is the +**index**: one line per reference file plus a short note about the link convention. + +``` +.claude/skills/expert/ +├── SKILL.md # INDEX: one-line description per reference +└── references/ + ├── how-to-run-the-project.md # procedural SOP + ├── how-to-validate.md # procedural SOP + ├── how-to-e2e-test.md # procedural SOP (setup, exercise, teardown) + ├── concept-architecture.md # semantic + ├── concept-core-files.md # semantic + ├── pattern-.md # soft prefer/avoid (judgment) + ├── invariant-.md # one hard rule per file + └── example-.md # past trace, cited from a real sha +``` + +## The five prefixes + +Each fact `/learn` decides to remember lands in exactly one prefixed file. The +filename slug is the topic; the prefix is the *kind* of memory. + +- **`how-to-.md` — procedural.** A sequence of steps a contributor (or + agent) follows. Imperative voice. Answers "How do I X?" Examples: + `how-to-run-the-project.md`, `how-to-validate.md`, `how-to-e2e-test.md`, + `how-to-add-a-feature.md`. Setup/teardown belongs inside the relevant how-to. + +- **`concept-.md` — semantic (nouns).** The shape of the system, a model, + a definition. Declarative voice. Answers "What is X?" Examples: + `concept-architecture.md`, `concept-core-files.md`, `concept-routing.md`. + +- **`pattern-.md` — semantic (soft rules).** A DO/DON'T that requires + judgment. Has counterexamples. Cannot be mechanically checked. Example: + `pattern-error-handling.md` ("prefer typed errors at boundaries; raw exceptions + fine inside a module"). + +- **`invariant-.md` — semantic (hard rules).** A single rule the codebase + upholds, mechanically checkable in principle. **One rule per file.** Filename + is the rule. Example: `invariant-no-repo-imports-service.md`. The highest-value + invariants get drafted as lints (see `invariant-discovery.md`). + +- **`example-.md` — episodic.** A concrete past trace — a real PR or + merge, input → reasoning → output. Cited from a real sha. Never synthetic. + Acts as a few-shot demonstration. Example: + `example-added-search-endpoint-2024-q3.md`. + +The split exists so an agent reading the SKILL.md index can ask one question of +each file ("does this topic apply now?") and open only what's relevant — instead +of paging through a 500-line `patterns.md` for one fact. + +## Wikilinks between reference files + +Reference files cross-link to each other with **Obsidian-style `[[wikilinks]]`**: +two square brackets around the *basename of another file in `references/`*, no +extension, no path. + +```markdown +The e2e flow stands up the test DB; see [[how-to-run-the-project]] for the +non-test boot path, and [[invariant-no-repo-imports-service]] for why the +fixture loader lives in `test/` and not `repo/`. +``` + +Resolution: `[[name]]` → `references/name.md`. The agent already knows how to +traverse these — wikilinks are a near-universal convention. + +`scripts/check-expert-links.sh` mechanically validates every wikilink resolves +to a file in the same directory. Broken links fail the PR. See +`wikilink-convention.md` for the formal spec. + +> AGENTS.md does **not** use wikilinks. It uses regular `[text](path)` markdown +> links so `check-agents-md.sh` keeps working unchanged. Wikilinks are an +> internal Expert-only convention. + +## SKILL.md as the index + +`SKILL.md` is generated by `/learn` and is a thin **catalog**, not a knowledge +file. Body: + +1. A one-paragraph header naming the project and stating the wikilink convention. +2. A table: `file | one-line description`. One row per reference file in the + directory. + +That's it. An agent reads the index, decides which references look relevant to +the task at hand, opens those, then follows wikilinks from there. + +The front-matter `description` ends with `(project)` and lists triggers so the +catalog can activate the Expert during the SDD phases. + +## Bootstrap / `--rebuild` + +On a fresh project (no Expert) or when invoked with `--rebuild`, seed a small +**minimum viable Expert** by scanning committed code. Bias toward fewer, broader +files; the incremental post-merge path will split them later when topics get +specific. + +**Always-seed (5 files):** + +1. `how-to-run-the-project.md` — derived from `package.json` / `Makefile` / + `pyproject.toml` / equivalent entry-points. +2. `how-to-validate.md` — derived from `scripts/local-checks.sh` and CI config. +3. `how-to-add-a-feature.md` — the SDD skill chain order for this project. +4. `concept-architecture.md` — module layering from the directory scan. +5. `concept-core-files.md` — key files and abstractions with paths. + +**Conditionally-seed (only on clear visible signal):** + +- `how-to-e2e-test.md` — if end-to-end tests exist. +- `concept-verification.md` — if testing/CI is non-trivial enough to warrant a + separate file from `how-to-validate.md`. +- One `invariant-.md` per rule the code *visibly* upholds. Skip on + uncertainty; let post-merge discovery promote them. +- `pattern-*.md` — **default to zero on bootstrap.** Patterns emerge from + recurring evidence across merges, not from one snapshot. +- `example-*.md` — **zero on bootstrap.** Episodic memory needs real past + experiences, not synthetic ones. + +**Soft cap: ~15 files total** on bootstrap. If the scan suggests more, fold +related items into one `concept-*.md` and let `/learn` split it later when a +topic genuinely warrants its own file. diff --git a/skills/harness/learn/references/invariant-discovery.md b/skills/harness/learn/references/invariant-discovery.md index dbe59dd..bf2e438 100644 --- a/skills/harness/learn/references/invariant-discovery.md +++ b/skills/harness/learn/references/invariant-discovery.md @@ -72,23 +72,28 @@ check would reward). - **Structured logging only** — no `console.log(`/`print(` in `src/`. - **Naming/format** — migration filenames match `^\d{14}_`. - **File-size cap / public-API drift** — `wc -l` ceiling; exports match `openapi.yaml`. -- **Memory freshness** — `check-agents-md.sh` itself (referenced paths exist). +- **Memory freshness** — `check-agents-md.sh` (AGENTS.md pointers exist) and + `check-expert-links.sh` (Expert wikilinks resolve). ## What stays prose (NOT a lint) Anything needing judgment: "is this the right abstraction?", "prefer composition -here." Those are `patterns.md` (Expert), or — if pre-emptively load-bearing — -AGENTS.md. The test: **if pass/fail is unambiguous, lint it; if it needs taste, it -stays prose.** +here." Those live in `pattern-.md` files in the Expert, or — if pre-emptively +load-bearing — AGENTS.md. The test: **if pass/fail is unambiguous, lint it; if it +needs taste, it stays prose.** ## The discipline (avoid overfitting) A discovered "invariant" can be an incidental pattern, or a quirk of the current model's failures rather than a real codebase property. So: -1. **Consensus-gate it** like every `/learn` write. -2. **Prose first, lint on recurrence.** First sighting → record in `invariants.md` - and flag a candidate lint. Same invariant seen across merges → promote to an - enforced lint. One occurrence is a pattern; repetition is an invariant. +1. **Justify it.** In a single inline pass, write a one-line justification citing + the diff hunk (file + line range) that motivated this rule. If you can't, drop + it. This is the inline replacement for the old consensus gate — same bar, + applied by the main agent. +2. **Prose first, lint on recurrence.** First sighting → record as a new + `invariant-.md` (one rule per file) and flag a candidate lint. Same + invariant seen across merges → promote to an enforced lint. One occurrence is + a pattern; repetition is an invariant. 3. **Frame the *reason*.** "Repo→Service imports break the build because layers are compiled bottom-up" survives model upgrades; a bare prohibition doesn't. 4. **The drafted lint MUST pass current main.** Run it against the just-merged code diff --git a/skills/harness/learn/references/reconcile.md b/skills/harness/learn/references/reconcile.md new file mode 100644 index 0000000..4f77128 --- /dev/null +++ b/skills/harness/learn/references/reconcile.md @@ -0,0 +1,138 @@ +# Reconcile — the new write logic + +**Hackable seam.** How `/learn` decides what to **add, edit, and delete** in the +Expert. Replaces the old consensus pass: reasoning happens inline in the main +agent (no subagent fanout), but every proposed change carries a one-line +justification cited from the diff or the contradicting reference file. + +## The premise + +Memory must reflect what is true *on main right now* — not what was true at the +last merge plus everything observed since. So every `/learn` run does three +things, in order: + +1. **Add** facts that appear in the merged diff and aren't in the Expert yet. +2. **Edit** existing reference files whose claims are still relevant but + incomplete or outdated. +3. **Delete** reference files whose underlying concept no longer exists in code, + or that contradict a more recent file beyond rescue. + +## The three reconcile passes + +Run these in order, in the main agent (no subagents). Each produces entries on +one of three candidate lists: `adds[]`, `edits[]`, `deletes[]`. Every entry +carries a **one-line justification** citing either a specific diff hunk +(file:line range) or a specific contradicting reference file. If you can't write +the justification, drop the candidate + +### Pass 1 — Deleted-concept pass + +For each `concept-*.md` and `how-to-*.md` in the Expert: + +1. Extract the file's **key code references**: paths it mentions, function or + class names, module identifiers, command names. +2. For each, check whether it still exists post-merge: `git cat-file -e + :`, `git grep -E `, or equivalent. +3. Decide: + - **Wholly gone** (no references survive) → mark **delete**. + - **Partially gone** (some references survive, some don't) → mark **edit**; + prune the dead pointers and update the prose. + - **All present** → no action from this pass. + +Do the same for `invariant-*.md` and `pattern-*.md`: if the rule's anchor code +is gone (the layer, the module, the API surface), the rule is dead. + +### Pass 2 — Inter-file contradiction pass + +Build a list of **topic-related file pairs**: + +- Pairs that share a wikilink (one references `[[the-other]]`). +- Pairs whose slugs share two or more tokens (e.g. `pattern-error-handling.md` + and `how-to-handle-errors.md`). +- Pairs that mention the same code paths or identifiers. + +For each pair, read both files in full and ask: **do they assert opposing +facts?** Common patterns: + +- **Stale + Fresh.** One was written before a refactor; the other after. + Resolution: drop the stale file (`delete`); the fresh one already covers the + current truth. Justification cites the merge sha that made the older claim + false. + +- **Two facets of one rule.** Two files about the same underlying concept that + disagree because they were each scoped narrowly. Resolution: **merge them** + into one file (`add` the merged file, `delete` both originals). Update any + inbound wikilinks. Justification names both prior files. + +- **Both true at different scopes.** Each is correct in its own context. + Resolution: `edit` both to scope-qualify ("inside the repo layer, …"; "at the + service boundary, …"). Justification cites the scope distinction. + +**Default heuristic: rewrite both / scope-qualify.** Drop one only when one is +clearly stale (its anchor code is gone, it predates a refactor that already +landed, or a human-authored P7 edit in this diff supersedes it). Never silently +delete on contradiction — every `delete` shows up in the PR body with its +one-line justification. + +### Pass 3 — Diff-invalidates-file pass + +For each code path touched by the merged diff, scan all references that mention +that path. For each reference: + +- Does the merge **contradict** a claim in the file? → mark **edit**. +- Does the merge **extend** a claim (new module added, new step in a how-to)? + → mark **edit** to incorporate, or **add** a new sibling file if the new + topic deserves its own. +- Does the merge **replace** the concept the file is about? → see Pass 1 (may + become a `delete`). + +## Routing and writing + +After reconcile, each entry on `adds[]` / `edits[]` / `deletes[]` is routed +through `routing-rules.md` to pick its prefix (`how-to-*` / `concept-*` / +`pattern-*` / `invariant-*` / `example-*`) or — for an `add` — to be redirected +to AGENTS.md, a lint, or nowhere instead. + +Write logic: + +- **Add** → create the file, add it to `SKILL.md`'s index, populate wikilinks to + related references. +- **Edit** → rewrite the relevant section. If a wikilink target was renamed or + deleted, fix the inbound link. +- **Delete** → `rm` the file, remove its row from `SKILL.md`'s index, and + rewrite any inbound wikilinks (drop them or repoint). + +`check-expert-links.sh` will fail the PR if any inbound wikilink was missed. +That's the safety net. + +## Nothing to learn + +If after all three passes plus routing the candidate sets are empty: + +``` +len(adds) + len(edits) + len(deletes) == 0 +AND no candidate AGENTS.md pointer change +AND no candidate lint +``` + +…then `/learn` prints: + +``` +learn: nothing to learn from , skipping PR +``` + +…to stdout, exits 0, and does **not** push a branch or open a PR. This is a +valid, common outcome — most merges are routine and produce nothing for memory. +The harness's outer loop advances `refs/harness/last-learned` regardless of +whether a PR opened, so the watermark keeps moving forward. + +## Why no subagent consensus + +The old design ran 2–3 cheap reviewers and required a 2/3 vote. That gated +hallucinations, but at the cost of a fanout step and the parallel review +overhead. The inline justification rule does the same job more cheaply: the +single agent must cite the diff hunk or contradicting file for *every* +candidate. Anything without a citation is dropped. A `--dry-run` flag (for +human-driven evaluation) prints the candidate set with justifications and +opens no PR — useful when a project wants periodic spot-checks on how `/learn` +is reasoning. diff --git a/skills/harness/learn/references/routing-rules.md b/skills/harness/learn/references/routing-rules.md index ce6c03a..d09e779 100644 --- a/skills/harness/learn/references/routing-rules.md +++ b/skills/harness/learn/references/routing-rules.md @@ -10,9 +10,30 @@ to the last two rows. |---|---|---| | **A lint** (`scripts/lints/*` → `local-checks.sh`) | The rule is *mechanically checkable* — pass/fail needs no judgment (layer-dependency direction, "parse at boundaries", naming, no raw SQL interpolation, structured-logging-only, file-size caps) | Best: enforced on every PR forever, and the failure message doubles as a fix prompt. See `invariant-discovery.md`. | | **Eager prose** (`AGENTS.md`) | It clears **all five predicates** below | Paid in tokens on *every* session that touches the folder — strict bar. | -| **Lazy prose** (an Expert shard) | Useful when an agent is *deliberately reasoning* about this area, but not needed pre-emptively | Paid only when consulted — looser bar. The default home for real knowledge. | +| **Lazy prose** (an Expert reference file) | Useful when an agent is *deliberately reasoning* about this area, but not needed pre-emptively | Paid only when consulted — looser bar. The default home for real knowledge. Splits into five sub-destinations (see below). | | **Nowhere** | Inferable from the code, taste-only, or transient | — | +## Sub-routing for lazy prose: the five prefixes + +A fact destined for the Expert lands in exactly one prefixed file. See +`expert-structure.md` for the directory layout. + +| Sub-destination | Use when | +|---|---| +| `how-to-.md` | Procedural SOP — a sequence of steps. Imperative voice. "How do I X?" | +| `concept-.md` | Semantic — a noun, a model, the shape of the system. "What is X?" | +| `pattern-.md` | Soft DO/DON'T requiring judgment. Has counterexamples. Cannot be linted. | +| `invariant-.md` | A single hard rule, mechanically checkable in principle. One rule per file; filename = the rule. The highest-value ones get drafted as lints. | +| `example-.md` | Episodic / few-shot — a *past* concrete trace (PR, merge, debugging session) cited from a real sha. Never synthetic. | + +**Tiebreakers:** + +- A step list → `how-to`. A noun or definition → `concept`. A rule → `invariant` + if linable, else `pattern`. A past trace with a sha → `example`. +- If a fact is *both* procedural and load-bearing as a rule, write both: the + `how-to-*` mentions the rule and wikilinks `[[invariant-*]]`. +- One rule per `invariant-*.md` file — never bundle multiple invariants. + ## The five-predicate test for *eager* (AGENTS.md) placement A fact earns a line in AGENTS.md only if **all five** hold. If any fails, it goes @@ -47,3 +68,22 @@ prefer nothing over noise.** AGENTS.md **points into** the Expert; it never copies it. A pointer ("this folder does X; consult `expert/references/Y.md` before non-trivial work") stays correct as the Expert grows. A copied paragraph drifts. Reference, don't duplicate. + +## Deletes — routing the other direction + +Routing rules also decide when an existing reference file should be **removed**. +A `delete` candidate is justified when one of these holds: + +- **Deleted concept.** The code the file anchors to is gone from main — the + module, route, command, or identifier it cites no longer exists. (See pass 1 + in `reconcile.md`.) +- **Superseded by a fresher file.** Two reference files contradict, and one is + clearly stale (its anchor code is gone, or a more recent merge supersedes its + claim). The stale one is deleted. (See pass 2 in `reconcile.md`.) +- **Merged into another.** Two files covered overlapping topics and have been + combined; the originals are deleted in favor of the merged file. + +Every `delete` carries a one-line justification in the PR body. Inbound +wikilinks must be rewritten or removed in the same PR; +`scripts/check-expert-links.sh` fails the build if any inbound link still +points at a deleted target. diff --git a/skills/harness/learn/references/wikilink-convention.md b/skills/harness/learn/references/wikilink-convention.md new file mode 100644 index 0000000..1055dbc --- /dev/null +++ b/skills/harness/learn/references/wikilink-convention.md @@ -0,0 +1,68 @@ +# Wikilink convention + +**Hackable seam.** The link syntax `/learn` uses inside `expert/references/`, +and the lint that enforces it. AGENTS.md is **out of scope** — it uses regular +markdown links and has its own validator (`check-agents-md.sh`). + +## Why wikilinks + +Reference files cross-link each other (a `how-to-*` file points at a +`[[concept-*]]` it depends on, an `invariant-*` points at the `[[pattern-*]]` it +hardens, etc.). Two options: + +- Regular markdown links: `[text](concept-architecture.md)`. Verbose, ties text + to filename, and rots when files are renamed. +- Wikilinks: `[[concept-architecture]]`. Short, basename-only, near-universal in + Obsidian-style notes. **Agents already know how to traverse these** without + prompting. + +We pick wikilinks for the Expert's internal cross-references because the cost +of one extra link-resolution rule is bought back many times over in +readability and stable refactoring. + +## The spec + +A wikilink is the literal token `[[]]` where `` matches +`[a-z0-9-]+`. + +Resolution: `[[]]` refers to the file `.md` in the **same directory** +as the file that contains the link (i.e. `.claude/skills/expert/references/`). + +- No paths. No leading `./`. No subdirectories. +- No `.md` extension inside the brackets. +- No alias form (`[[slug|display text]]`) for now — keep the syntax minimal. +- The link target file must exist. + +## Valid + +```markdown +For setup, see [[how-to-run-the-project]]. +The repo layer must obey [[invariant-no-repo-imports-service]]. +This pattern complements [[concept-architecture]]. +``` + +## Invalid + +```markdown +See [[./how-to-run-the-project]]. ← no path +See [[how-to-run-the-project.md]]. ← no extension +See [[How-To-Run-The-Project]]. ← lowercase only +See [[how-to-run-the-project|setup guide]]. ← no alias form +See [[expert/how-to-run-the-project]]. ← no directory prefix +``` + +## Enforcement + +`scripts/check-expert-links.sh` (next to `check-agents-md.sh`) walks every +`expert/references/*.md`, extracts every `[[...]]` token, and asserts each +resolves to an existing `.md` file in the same directory. + +- **Broken link → exit 1.** The PR fails. Fix the link or restore the file. +- **Orphan files** (no inbound wikilink from any other reference) → warn only, + do not fail. Top-level concepts can legitimately be orphans. +- **Cycles** (A → B → A) → informational only. Small cycles between mutually + related concepts are fine. + +Wire the script into `scripts/local-checks.sh` next to `check-agents-md.sh` so +the same gate that flags a stale AGENTS.md pointer also flags a stale Expert +wikilink. diff --git a/skills/harness/learn/scripts/check-expert-links.sh b/skills/harness/learn/scripts/check-expert-links.sh new file mode 100755 index 0000000..afd6884 --- /dev/null +++ b/skills/harness/learn/scripts/check-expert-links.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# check-expert-links.sh — mechanical link gate for the project's lazy memory. +# +# Reference files under .claude/skills/expert/references/ cross-link via Obsidian +# wikilinks ([[basename]] resolving to basename.md in the same directory). Renames, +# deletions, and typos break these silently — this lint makes those failures loud. +# Wire it into scripts/local-checks.sh (and CI) so a broken Expert link fails the +# build. +# +# Checks: +# 1. Every [[]] token in every references/*.md resolves to .md in +# the same directory. +# 2. Slugs match the convention [a-z0-9-]+ (lowercase, hyphenated). +# +# Warns (does NOT fail): +# - Orphan files (no inbound wikilink from any other reference) — top-level +# concepts can legitimately be orphans. +# +# Exit 0 = clean. Exit 1 = at least one broken or malformed link. +# +# The Expert root is tweakable via env; default is .claude/skills/expert. + +set -euo pipefail + +EXPERT_ROOT="${EXPERT_ROOT:-.claude/skills/expert}" +REF_DIR="${EXPERT_ROOT}/references" + +fail=0 +note() { echo "❌ $*" >&2; fail=1; } +warn() { echo "⚠️ $*" >&2; } + +if [[ ! -d "$REF_DIR" ]]; then + echo "no Expert at ${REF_DIR} (nothing to check)" + exit 0 +fi + +mapfile -t files < <(find "$REF_DIR" -maxdepth 1 -type f -name '*.md' 2>/dev/null | sort) + +if (( ${#files[@]} == 0 )); then + echo "no reference files in ${REF_DIR} (nothing to check)" + exit 0 +fi + +# Build the set of valid slugs (filenames without .md) for fast existence check. +declare -A valid_slug +for f in "${files[@]}"; do + base="$(basename "$f" .md)" + valid_slug["$base"]=1 +done + +# Track inbound link counts to surface orphan warnings. +declare -A inbound_count +for f in "${files[@]}"; do + base="$(basename "$f" .md)" + inbound_count["$base"]=0 +done + +slug_re='^[a-z0-9-]+$' + +for f in "${files[@]}"; do + # Extract every [[...]] token. + while IFS= read -r token; do + [[ -z "$token" ]] && continue + # Strip leading [[ and trailing ]] (escape the brackets so bash treats them + # as literals, not as bracket-expression delimiters in the pattern). + slug="${token##\[\[}" + slug="${slug%%\]\]}" + + # Convention check: lowercase, alphanumeric + hyphen, no path, no extension, + # no alias. + if ! [[ "$slug" =~ $slug_re ]]; then + note "$f: malformed wikilink '[[${slug}]]' — slugs must match [a-z0-9-]+ (no paths, no extensions, no aliases). See wikilink-convention.md." + continue + fi + + # Existence check: target file must exist in references/. + if [[ -z "${valid_slug[$slug]:-}" ]]; then + note "$f: wikilink '[[${slug}]]' does not resolve — no file ${REF_DIR}/${slug}.md. Fix the link or restore the file." + continue + fi + + # Don't count self-links as inbound. + src_base="$(basename "$f" .md)" + if [[ "$slug" != "$src_base" ]]; then + inbound_count["$slug"]=$((inbound_count["$slug"] + 1)) + fi + done < <(grep -oE '\[\[[^]]+\]\]' "$f" || true) +done + +# Orphan warnings (informational only — SKILL.md / top-level concepts are +# expected to be orphans). +for slug in "${!inbound_count[@]}"; do + if (( inbound_count[$slug] == 0 )); then + warn "${REF_DIR}/${slug}.md has no inbound wikilinks (orphan — informational only)." + fi +done + +if (( fail )); then + echo "check-expert-links: FAIL" >&2 + exit 1 +fi +echo "check-expert-links: OK (${#files[@]} file(s))" diff --git a/skills/human-loop/evaluate-sessions/SKILL.md b/skills/human-loop/evaluate-sessions/SKILL.md index 428b766..35db078 100644 --- a/skills/human-loop/evaluate-sessions/SKILL.md +++ b/skills/human-loop/evaluate-sessions/SKILL.md @@ -13,9 +13,9 @@ harness ran to produce it — into two durable outcomes: behaved well or badly given the context it had, freeze that as a runnable check under `evals/`. This is the flywheel Chase/LangSmith describe — *observe a trace → capture it as an eval → fix the prompt → it persists as a regression test* — except the "prompt" - here is the project's **context** (Expert shards, AGENTS.md, a skill's own text). + here is the project's **context** (Expert reference files, AGENTS.md, a skill's own text). - **Context fixes.** When a session reveals that a piece of context **misled** an agent or - was **missing**, fix it — the Expert shard, the AGENTS.md pointer, or the skill — *with* + was **missing**, fix it — the Expert reference file, the AGENTS.md pointer, or the skill — *with* the human, on a branch. `/evaluate-pr` evaluates **what was built** (the change); `/evaluate-sessions` evaluates @@ -86,7 +86,7 @@ not auditing a single PR, you're tuning the project's context so the *whole harn an agent would think to consult the Expert; non-inferable from the code; harmful if violated (breaks behavior/data, not style); stable; and local-or-truly-global. Eager memory is paid in tokens on every session, so the bar is high. - - **Lazy prose (an Expert shard)** — the default home for real knowledge: useful when an + - **Lazy prose (an Expert reference file)** — the default home for real knowledge: useful when an agent *deliberately reasons* about the area, paid only when consulted. - **Nowhere** — inferable from the code, taste-only, or transient. @@ -128,7 +128,7 @@ Pick the lens — it only changes emphasis, not mechanics: Run `scripts/resolve-sessions.sh `. It parses the **PR comment** (the durable, dispatcher-posted artifact) for session IDs and globs `~/.claude/projects/*/.jsonl` for each trace, flagging any whose file is missing (a remote/CI run — see the degradation note in -`references/trace-reading.md`). Do **not** depend on `.harness/sessions-.tsv`; it's +`references/trace-reading.md`). Do **not** depend on the harness repo’s `state//sessions-.tsv`; it’s ephemeral and deleted on PR cleanup. ### Step 2 — Triage from the table (S7) @@ -189,7 +189,7 @@ re-examines the current state. ## Hard nevers - **Never write `main` or memory autonomously.** Changes land on a branch; `/learn` writes memory post-merge from the merged diff (S5). -- **Never depend on `.harness/sessions-.tsv`.** It's ephemeral and gone post-merge; the PR +- **Never depend on the harness repo’s `state//sessions-.tsv`.** It's ephemeral and gone post-merge; the PR comment is the contract (S8 / `trace-reading.md`). - **Never fabricate an eval that passes trivially.** An eval that doesn't fail against the context that misled the agent proves nothing (`references/evals.md`, right-reason check). diff --git a/skills/human-loop/evaluate-sessions/references/evals.md b/skills/human-loop/evaluate-sessions/references/evals.md index 535face..006111f 100644 --- a/skills/human-loop/evaluate-sessions/references/evals.md +++ b/skills/human-loop/evaluate-sessions/references/evals.md @@ -48,7 +48,7 @@ If the project has no `evals/` dir yet, create it when you author the first eval # `evals/` — regression tests over this project's harness skills/context Authored by `/evaluate-sessions` when a build trail reveals a skill behaving well or badly -because of this project's context (an Expert shard, an AGENTS.md pointer, a skill's text, a +because of this project's context (an Expert reference file, an AGENTS.md pointer, a skill's text, a spec). Evals test the **harness** ("given this context, does the skill behave?"); `prds// run-prd-test.sh` tests the **product** ("does the feature work?"). Keep them separate. diff --git a/skills/human-loop/evaluate-sessions/references/outcomes.md b/skills/human-loop/evaluate-sessions/references/outcomes.md index ba3e983..f80b82f 100644 --- a/skills/human-loop/evaluate-sessions/references/outcomes.md +++ b/skills/human-loop/evaluate-sessions/references/outcomes.md @@ -26,14 +26,14 @@ Two reasons, both load-bearing: ## Where in the project a context fix is written -You route the fix to a destination in SKILL.md's S9 (lint / AGENTS.md / Expert shard / +You route the fix to a destination in SKILL.md's S9 (lint / AGENTS.md / Expert reference file / nowhere). To find the exact file, read the **project's own** artifacts — not any skill's internals: -- **Expert shard** — open `.claude/skills/expert/references/` in the project and pick the shard - whose subject matches (architecture, verification, patterns, procedural, core-files, - invariants). If none fits and the fact is real, the project's memory updater can place it on - its post-merge pass; your job is to write a clear, correctly-scoped note in the closest shard. +- **Expert reference file** — open `.claude/skills/expert/references/` in the project and pick the + file whose subject matches, by prefix (`how-to-*`, `concept-*`, `pattern-*`, `invariant-*`, + `example-*`). If none fits and the fact is real, the project's memory updater can place it on + its post-merge pass; your job is to write a clear, correctly-scoped note in the closest file. - **AGENTS.md** — the root `AGENTS.md`, or the nested one in the folder the rule is local to. Only if it clears the five-predicate bar (S9); otherwise prefer the Expert. - **A skill defect** — if a *skill's own text* steered the agent wrong, the fix is that skill's diff --git a/skills/human-loop/evaluate-sessions/references/trace-reading.md b/skills/human-loop/evaluate-sessions/references/trace-reading.md index 8dcd4e2..978c540 100644 --- a/skills/human-loop/evaluate-sessions/references/trace-reading.md +++ b/skills/human-loop/evaluate-sessions/references/trace-reading.md @@ -23,7 +23,7 @@ The table is `| Time | Step | Attempt | Session ID | Exit |`, with the step and backtick-wrapped. `scripts/resolve-sessions.sh ` parses exactly this and emits `\t` per row. -**Do not read `.harness/sessions-.tsv`.** It carries an extra `duration_s` column and +**Do not read the harness repo’s `state//sessions-.tsv`.** It carries an extra `duration_s` column and untruncated rows, but the cleanup pass `rm -f`s it on every merged/closed PR — so for a merged PR (the `/learn`-audit case) it's already gone. The PR comment is the contract; the TSV is ephemeral working state. (One consequence: `render_sessions_table` does `tail -n 20` @@ -111,7 +111,7 @@ first: become evals and/or context fixes. - **Inherent difficulty** — the task was just hard; no context change would have helped. Name it and move on. Calling difficulty a "defect" over-fits memory with noise — the - thing `/learn`'s consensus gate and this skill's S4 both guard against. + thing `/learn`'s reconcile justification bar and this skill's S4 both guard against. ## CI / remote degradation diff --git a/skills/human-loop/evaluate-sessions/scripts/resolve-sessions.sh b/skills/human-loop/evaluate-sessions/scripts/resolve-sessions.sh index fee524f..9ebb5af 100755 --- a/skills/human-loop/evaluate-sessions/scripts/resolve-sessions.sh +++ b/skills/human-loop/evaluate-sessions/scripts/resolve-sessions.sh @@ -4,7 +4,7 @@ # The session table is posted on the PR DETERMINISTICALLY BY THE DISPATCHER # (render_sessions_table -> signal_human_review / signal_stuck / signal_learn_review, # each ending in `gh pr comment`). That comment is the durable artifact — the local -# .harness/sessions-.tsv is ephemeral and `rm -f`'d on PR cleanup, so we never read it. +# The harness repo's state//sessions-.tsv is ephemeral and rm -f'd on PR cleanup, so we never read it. # # Output (one line per session, tab-separated): # \t diff --git a/skills/human-loop/wiki-init/SKILL.md b/skills/human-loop/wiki-init/SKILL.md index 0074d36..c70ee82 100644 --- a/skills/human-loop/wiki-init/SKILL.md +++ b/skills/human-loop/wiki-init/SKILL.md @@ -181,7 +181,7 @@ path to point it at; otherwise this wiki is theirs to read and query directly. ## Invocation & output contract - **Invoked by:** a human (`/wiki-init`, optionally `--yolo`). Not the dispatcher — this - is a human-attentive setup skill, like `/intent` and `/harness-init`. + is a human-attentive setup skill, like `/intent` and `/env-init`. - **Outputs:** a standalone wiki vault at the chosen external path — `raw/`, `wiki/`, `_meta/`, the tailored conventions doc (`CLAUDE.md`), the three commands under `.claude/commands/`, a seeded `MOC.md`, a `README.md`, and an initialized git repo. diff --git a/skills/sdd/expert-sdd-creator/SKILL.md b/skills/sdd/expert-sdd-creator/SKILL.md deleted file mode 100644 index 086e2e3..0000000 --- a/skills/sdd/expert-sdd-creator/SKILL.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -name: expert-sdd-creator -description: Meta skill for creating new SDD experts from domain documentation. Provide a markdown file path with domain knowledge + optional steering context. Automatically synthesizes references, generates signal scripts, validates, and registers in experts.md/signals.md catalogs. Single-prompt creation with follow-up tweaks. Triggers - create expert, new expert, make expert, expert for, domain expert, add expert (project) ---- - -# Expert SDD Creator - -Create SDD experts from domain documentation in a single prompt. - -## Required Input - -1. **Domain docs** (required): Path to markdown file with domain knowledge -2. **Steering** (optional but recommended): What to focus on, specific use cases - -Example prompt: -``` -Create an expert for my custom testing framework. -Docs: /path/to/custom-docs.md -Focus on: component testing, live dependency testing, Cucumber BDD patterns -``` - ---- - -## Creation Process - -### 1. Analyze Input - -Read the markdown file and steering context to determine: -- Expert name (kebab-case, prefixed with `expert-`) -- Triggers (keywords that invoke this expert) -- Key topics for reference files -- Signal validation approach - -### 2. Generate Expert Structure - -Create directory at `.claude/skills/expert-{name}/` (sibling to this skill's directory): - -``` -expert-{name}/ -├── SKILL.md # Frontmatter + Expert Mode + Signal Mode -├── references/ -│ ├── {topic-1}.md # Synthesized from input docs -│ ├── {topic-N}.md -│ └── signal-workflow.md # How signal validates behavior -└── scripts/ - ├── run_signal.sh # Main signal execution - └── {helper}.sh # Additional helpers as needed -``` - -### 3. Synthesize References - -From the input markdown: -- Extract key topics and organize into separate reference files -- Keep each file focused on one topic -- Include practical examples and patterns -- Create `signal-workflow.md` describing validation approach - -### 4. Generate Signal Scripts - -Analyze domain to determine appropriate signal approach: - -| Domain Type | Signal Approach | -|-------------|-----------------| -| Testing (Cucumber, JUnit) | Run tests, parse results | -| API/Service (REST, GraphQL) | Call endpoints, check responses | -| Build/Deploy (Maven, Gradle) | Run build, check artifacts | -| Config/Data (YAML, JSON) | Validate syntax, check schema | -| CLI tools (flyway, kubectl) | Run commands, parse output | - -Generate `run_signal.sh` with domain-specific checks and TODOs for customization. - -### 5. Write SKILL.md - -Structure the expert's SKILL.md: - -```yaml ---- -name: expert-{name} -description: {domain} expertise with signal capability. {what it provides}. Triggers: {trigger-list} (project) ---- -``` - -Body includes: -- **Expert Mode**: Quick reference table, navigation to references -- **Signal Mode**: Quick commands, reference to signal-workflow.md - -### 6. Validate & Register - -Run validation: -```bash -scripts/validate_expert.sh /path/to/expert-{name} -``` - -If valid, register: -```bash -scripts/register_expert.sh /path/to/expert-{name} -``` - -### 7. Report Results - -Output to user: -- Created directory structure -- List of synthesized reference files -- Signal approach explanation -- Registration locations (line numbers in experts.md/signals.md) -- Path to expert for tweaking -- Any clarifying questions about signal scripts - ---- - -## Expert Structure Requirements - -Every expert must have: - -1. **SKILL.md** with valid YAML frontmatter: - - `name`: expert-{name} format - - `description`: includes triggers and "(project)" suffix - -2. **references/** directory with: - - At least one domain knowledge file - - `signal-workflow.md` describing validation approach - -3. **scripts/** directory with: - - `run_signal.sh` (executable) - - Helper scripts as needed - -→ See `references/expert-structure.md` for detailed patterns - ---- - -## Signal Mode - -Validate and register created experts. - -### Quick Commands - -| Action | Command | -|--------|---------| -| Validate | `scripts/validate_expert.sh ` | -| Register | `scripts/register_expert.sh ` | - -### Validation Checks - -- Directory structure complete (SKILL.md, references/, scripts/) -- YAML frontmatter valid (name, description) -- references/ contains files -- scripts/run_signal.sh exists and is executable -- signal-workflow.md present - -### Registration - -- Parses expert metadata from SKILL.md -- Generates entries for experts.md and signals.md -- Inserts at end of Expert Entries / Signal Entries section -- Reports line numbers of insertions - -→ See `references/catalog-formats.md` for entry formats -→ See `references/signal-patterns.md` for signal script patterns diff --git a/skills/sdd/expert-sdd-creator/references/catalog-formats.md b/skills/sdd/expert-sdd-creator/references/catalog-formats.md deleted file mode 100644 index 0d6085e..0000000 --- a/skills/sdd/expert-sdd-creator/references/catalog-formats.md +++ /dev/null @@ -1,189 +0,0 @@ -# Catalog Entry Formats - -Formats for registering experts in `experts.md` and `signals.md` catalogs. - -Location of catalogs (sibling to this skill's install directory): -- `.claude/skills/spec-planning/references/experts.md` -- `.claude/skills/spec-planning/references/signals.md` - ---- - -## experts.md Entry Format - -Add under `## Expert Entries` section: - -```markdown -### N. {expert-name} - -**Description:** {2-3 sentence description of what the expert provides} - -**Triggers:** -- {trigger-1} -- {trigger-2} -- {trigger-N} - -**What It Provides:** -- {capability-1} -- {capability-2} -- {capability-N} - -**Signal Capability:** -- {signal-validation-1} -- {signal-validation-N} - -**Skill Name:** `{expert-name}` - ---- -``` - -### Field Details - -| Field | Description | -|-------|-------------| -| N | Sequential number (next after last entry) | -| expert-name | Matches `name` from SKILL.md frontmatter | -| Description | Domain expertise summary + signal capability mention | -| Triggers | Keywords that invoke this expert (from frontmatter) | -| What It Provides | Expert mode capabilities (from references) | -| Signal Capability | What signal mode validates | -| Skill Name | Exact skill name for invocation | - -### Example Entry - -```markdown -### 4. expert-migrations - -**Description:** Database migration expertise for Flyway and Liquibase. Provides schema versioning patterns, rollback strategies, and CI/CD integration guidance. Includes signal capability for migration status validation. - -**Triggers:** -- Flyway, Liquibase, database migrations -- Schema versioning, DDL changes -- Rollback, migration status -- Signal, verify migrations - -**What It Provides:** -- Migration file naming and versioning conventions -- Rollback strategy patterns -- CI/CD pipeline integration -- Troubleshooting common migration failures - -**Signal Capability:** -- Migration status check (pending, applied, failed) -- Schema validation -- Rollback verification - -**Skill Name:** `expert-migrations` - ---- -``` - ---- - -## signals.md Entry Format - -Add under `## Signal Entries` section: - -```markdown -### N. {expert-name} (with signal capability) - -**Description:** {1 sentence describing what the signal validates} - -**Triggers:** -- {signal-trigger-1} -- {signal-trigger-2} -- {signal-trigger-N} - -**What It Validates:** -- {validation-1} -- {validation-2} -- {validation-N} - -**How to Invoke:** `skill: "{expert-name}"` - -**Typical Expected Behaviors:** -- {expected-behavior-1} -- {expected-behavior-2} -- {expected-behavior-N} - -**Typical Failure Indicators:** -- {failure-indicator-1} -- {failure-indicator-2} -- {failure-indicator-N} - -**Skill Name:** `{expert-name}` - ---- -``` - -### Field Details - -| Field | Description | -|-------|-------------| -| N | Sequential number (next after last entry) | -| expert-name | Matches `name` from SKILL.md frontmatter | -| Description | What signal mode validates (1 sentence) | -| Triggers | Keywords that invoke signal mode | -| What It Validates | Specific validations performed | -| How to Invoke | Skill invocation syntax | -| Expected Behaviors | What success looks like | -| Failure Indicators | What failure looks like | - -### Example Entry - -```markdown -### 4. expert-migrations (with signal capability) - -**Description:** Runtime validation for database migration status and schema consistency. - -**Triggers:** -- Check migration status, verify migrations -- Run flyway info, liquibase status -- Signal, migration validation - -**What It Validates:** -- Migration execution status (pending, applied, failed) -- Schema version consistency -- Rollback capability verification - -**How to Invoke:** `skill: "expert-migrations"` - -**Typical Expected Behaviors:** -- All migrations show "applied" status -- Schema version matches expected version -- No pending migrations in production - -**Typical Failure Indicators:** -- Migrations show "pending" or "failed" status -- Schema version mismatch -- Connection errors to database -- Checksum validation failures - -**Skill Name:** `expert-migrations` - ---- -``` - ---- - -## Insertion Rules - -1. **Position**: Add after the last `### N.` entry in each file -2. **Numbering**: Increment N from the last entry -3. **Separator**: Include `---` after each entry -4. **Both files**: Always update both experts.md AND signals.md - ---- - -## Extraction from SKILL.md - -The `register_expert.sh` script extracts: - -| Catalog Field | Source | -|---------------|--------| -| expert-name | `name` from frontmatter | -| Description | `description` from frontmatter (truncated/adapted) | -| Triggers | Parsed from `description` after "Triggers:" | -| What It Provides | Derived from reference file names | -| Signal Capability | Derived from signal-workflow.md | -| Expected Behaviors | From signal-workflow.md "Success" section | -| Failure Indicators | From signal-workflow.md "Failure" section | diff --git a/skills/sdd/expert-sdd-creator/references/expert-structure.md b/skills/sdd/expert-sdd-creator/references/expert-structure.md deleted file mode 100644 index c550bc6..0000000 --- a/skills/sdd/expert-sdd-creator/references/expert-structure.md +++ /dev/null @@ -1,231 +0,0 @@ -# Expert Structure Guide - -Reference for creating well-structured SDD experts. - -## Directory Layout - -``` -expert-{name}/ -├── SKILL.md # Required: main entry point -├── references/ # Required: domain knowledge -│ ├── {topic-1}.md -│ ├── {topic-N}.md -│ └── signal-workflow.md # Required: signal documentation -└── scripts/ # Required: signal scripts - ├── run_signal.sh # Required: main signal entry - └── {helpers}.sh # Optional: additional scripts -``` - ---- - -## SKILL.md Structure - -### Frontmatter (YAML) - -```yaml ---- -name: expert-{name} -description: {domain} expertise with signal capability. {1-2 sentences about what it provides}. Use for {use cases}. Triggers: {comma-separated triggers} (project) ---- -``` - -**Rules:** -- `name` must be `expert-` prefix + kebab-case name -- `description` must include triggers for auto-discovery -- `description` must end with `(project)` -- Triggers should be comprehensive but not overly broad - -### Body Structure - -```markdown -# {Expert Name} - -{One-line description} - -## Quick Reference - -| Topic | Reference | Mode | -|-------|-----------|------| -| {topic} | `{file}.md` | Expert | -| Signal Workflow | `signal-workflow.md` | Signal | - ---- - -## Expert Mode - -**Use during:** Spec planning, learning, implementation guidance - -### Quick Navigation - -- **{Category}?** → `{file}.md` - {description} - ---- - -## Signal Mode - -**Use during:** Implementation phase (after spec exists) - -### Quick Commands - -| Action | Command | -|--------|---------| -| {action} | `scripts/{script}.sh {args}` | - -→ See `references/signal-workflow.md` for detailed workflow -``` - ---- - -## Reference Files - -### Organization Principles - -1. **One topic per file** - keep files focused -2. **Practical over theoretical** - include examples -3. **Scannable structure** - use headers, tables, code blocks -4. **Link between files** - reference related content - -### Required: signal-workflow.md - -Every expert must have `references/signal-workflow.md` containing: - -```markdown -# {Expert} Signal Workflow - -**Use during:** Implementation phase -**NOT for:** Spec planning or general questions - -## Workflow Overview - -{Diagram or description of feedback loop} - -## Signal Scripts - -| Script | Purpose | -|--------|---------| -| `run_signal.sh` | {main purpose} | -| `{helper}.sh` | {purpose} | - -## Script Usage - -### {Script Name} - -```bash -scripts/{script}.sh {args} -``` - -{Description of what it does} - -## Interpreting Results - -### Success -{What success looks like} - -### Failure -{What failure looks like and how to fix} -``` - ---- - -## Scripts - -### run_signal.sh Requirements - -1. **Shebang**: `#!/bin/bash` -2. **Strict mode**: `set -euo pipefail` -3. **Usage info**: Show help when called incorrectly -4. **Structured output**: JSON or clearly formatted for AI parsing -5. **Exit codes**: 0=success, non-zero=failure - -### Template Structure - -```bash -#!/bin/bash -# run_signal.sh - {Expert} signal verification -# -# Usage: run_signal.sh [options] -# -# Options: -# --option1 Description -# --verbose Show detailed output - -set -euo pipefail - -# Parse arguments -if [[ $# -lt 1 ]]; then - echo "Usage: run_signal.sh [options]" - exit 1 -fi - -# Main logic -{domain-specific checks} - -# Output results -echo "{" -echo " \"status\": \"$STATUS\"," -echo " \"details\": \"$DETAILS\"" -echo "}" - -exit $EXIT_CODE -``` - ---- - -## Naming Conventions - -| Item | Convention | Example | -|------|------------|---------| -| Expert directory | `expert-{domain}` | `expert-migrations` | -| Reference files | `{topic}.md` (kebab-case) | `rollback-strategies.md` | -| Signal workflow | `signal-workflow.md` | Always this name | -| Main signal script | `run_signal.sh` | Always this name | -| Helper scripts | `{verb}_{noun}.sh` | `parse_results.sh` | - ---- - -## Common Patterns - -### Expert for Testing Domain - -``` -expert-{test-framework}/ -├── SKILL.md -├── references/ -│ ├── test-patterns.md -│ ├── configuration.md -│ ├── troubleshooting.md -│ └── signal-workflow.md -└── scripts/ - ├── run_signal.sh # Runs tests - └── parse_results.sh # Parses test output -``` - -### Expert for API/Service Domain - -``` -expert-{service}/ -├── SKILL.md -├── references/ -│ ├── endpoint-patterns.md -│ ├── authentication.md -│ ├── error-handling.md -│ └── signal-workflow.md -└── scripts/ - ├── run_signal.sh # Calls endpoints - └── check_health.sh # Health checks -``` - -### Expert for Tool/CLI Domain - -``` -expert-{tool}/ -├── SKILL.md -├── references/ -│ ├── commands.md -│ ├── configuration.md -│ ├── best-practices.md -│ └── signal-workflow.md -└── scripts/ - ├── run_signal.sh # Runs tool commands - └── parse_output.sh # Parses CLI output -``` diff --git a/skills/sdd/expert-sdd-creator/references/signal-patterns.md b/skills/sdd/expert-sdd-creator/references/signal-patterns.md deleted file mode 100644 index b0c4380..0000000 --- a/skills/sdd/expert-sdd-creator/references/signal-patterns.md +++ /dev/null @@ -1,367 +0,0 @@ -# Signal Script Patterns - -Common patterns for generating signal scripts based on domain type. - ---- - -## Pattern Selection - -| Domain Keywords | Pattern | Primary Check | -|-----------------|---------|---------------| -| test, cucumber, junit, karate | Testing | Run tests, parse results | -| api, rest, endpoint, http, graphql | API/Service | Call endpoints, check responses | -| build, maven, gradle, npm | Build | Run build, check artifacts | -| config, yaml, json, properties | Config/Data | Validate syntax | -| cli, flyway, kubectl, terraform | CLI Tool | Run commands, parse output | - ---- - -## Testing Pattern - -For test frameworks (Cucumber, JUnit, Karate). - -### run_signal.sh - -```bash -#!/bin/bash -# run_signal.sh - Test execution signal -# -# Usage: run_signal.sh [--environment ] [--tags ] - -set -euo pipefail - -ENVIRONMENT="${1:-local}" -TAGS="${2:-}" - -echo "═══════════════════════════════════════════════════════════════" -echo "TEST SIGNAL" -echo "═══════════════════════════════════════════════════════════════" -echo "" -echo "Environment: $ENVIRONMENT" -echo "Tags: ${TAGS:-all}" -echo "" - -# TODO: Replace with actual test command -# Examples: -# mvn test -Dcucumber.filter.tags="$TAGS" -# ./gradlew test -# customCli run test --environment "$ENVIRONMENT" -echo "Running tests..." -TEST_OUTPUT=$(mvn test 2>&1) || TEST_EXIT=$? -TEST_EXIT=${TEST_EXIT:-0} - -# Parse results -# TODO: Adjust parsing based on test framework -TESTS_RUN=$(echo "$TEST_OUTPUT" | grep -oP 'Tests run: \K\d+' | tail -1 || echo "0") -FAILURES=$(echo "$TEST_OUTPUT" | grep -oP 'Failures: \K\d+' | tail -1 || echo "0") -ERRORS=$(echo "$TEST_OUTPUT" | grep -oP 'Errors: \K\d+' | tail -1 || echo "0") - -echo "" -echo "═══════════════════════════════════════════════════════════════" -echo "RESULTS" -echo "═══════════════════════════════════════════════════════════════" -echo "" -echo "{" -echo " \"tests_run\": $TESTS_RUN," -echo " \"failures\": $FAILURES," -echo " \"errors\": $ERRORS," -echo " \"status\": \"$([ $TEST_EXIT -eq 0 ] && echo PASS || echo FAIL)\"" -echo "}" - -exit $TEST_EXIT -``` - ---- - -## API/Service Pattern - -For REST APIs, GraphQL, HTTP services. - -### run_signal.sh - -```bash -#!/bin/bash -# run_signal.sh - API endpoint signal check -# -# Usage: run_signal.sh [--method ] [--data ] - -set -euo pipefail - -if [[ $# -lt 2 ]]; then - echo "Usage: run_signal.sh [options]" - exit 1 -fi - -BASE_URL="$1" -ENDPOINT="$2" -METHOD="${3:-GET}" -DATA="${4:-}" - -echo "═══════════════════════════════════════════════════════════════" -echo "API SIGNAL CHECK" -echo "═══════════════════════════════════════════════════════════════" -echo "" -echo "URL: ${BASE_URL}${ENDPOINT}" -echo "Method: $METHOD" -echo "" - -# Build curl command -CURL_CMD=(curl -s -w "\n%{http_code}\n%{time_total}" -X "$METHOD") -CURL_CMD+=(-H "Accept: application/json") -CURL_CMD+=(-H "Content-Type: application/json") - -if [[ -n "$DATA" ]]; then - CURL_CMD+=(-d "$DATA") -fi - -CURL_CMD+=("${BASE_URL}${ENDPOINT}") - -# Execute request -RESPONSE=$("${CURL_CMD[@]}" 2>&1) || true - -# Parse response -BODY=$(echo "$RESPONSE" | head -n -2) -HTTP_CODE=$(echo "$RESPONSE" | tail -2 | head -1) -TIME_TOTAL=$(echo "$RESPONSE" | tail -1) - -# Determine status -if [[ "$HTTP_CODE" =~ ^2 ]]; then - STATUS="SUCCESS" - EXIT_CODE=0 -elif [[ "$HTTP_CODE" =~ ^4 ]]; then - STATUS="CLIENT_ERROR" - EXIT_CODE=4 -else - STATUS="SERVER_ERROR" - EXIT_CODE=1 -fi - -echo "═══════════════════════════════════════════════════════════════" -echo "RESULTS" -echo "═══════════════════════════════════════════════════════════════" -echo "" -echo "{" -echo " \"http_status\": $HTTP_CODE," -echo " \"response_time_seconds\": $TIME_TOTAL," -echo " \"status\": \"$STATUS\"" -echo "}" -echo "" -echo "Response Body:" -echo "$BODY" | head -c 500 - -exit $EXIT_CODE -``` - ---- - -## Build Pattern - -For build tools (Maven, Gradle, npm). - -### run_signal.sh - -```bash -#!/bin/bash -# run_signal.sh - Build verification signal -# -# Usage: run_signal.sh [--skip-tests] - -set -euo pipefail - -SKIP_TESTS="${1:-}" - -echo "═══════════════════════════════════════════════════════════════" -echo "BUILD SIGNAL" -echo "═══════════════════════════════════════════════════════════════" -echo "" - -# TODO: Replace with actual build command -# Examples: -# mvn clean package -DskipTests -# ./gradlew build -# npm run build -BUILD_CMD="mvn clean package" -if [[ "$SKIP_TESTS" == "--skip-tests" ]]; then - BUILD_CMD="$BUILD_CMD -DskipTests" -fi - -echo "Running: $BUILD_CMD" -echo "" - -BUILD_OUTPUT=$($BUILD_CMD 2>&1) || BUILD_EXIT=$? -BUILD_EXIT=${BUILD_EXIT:-0} - -# Check for artifacts -# TODO: Adjust artifact path -ARTIFACT_PATH="target/*.jar" -ARTIFACT_COUNT=$(ls $ARTIFACT_PATH 2>/dev/null | wc -l || echo "0") - -echo "" -echo "═══════════════════════════════════════════════════════════════" -echo "RESULTS" -echo "═══════════════════════════════════════════════════════════════" -echo "" -echo "{" -echo " \"build_status\": \"$([ $BUILD_EXIT -eq 0 ] && echo SUCCESS || echo FAILED)\"," -echo " \"artifacts_found\": $ARTIFACT_COUNT" -echo "}" - -exit $BUILD_EXIT -``` - ---- - -## CLI Tool Pattern - -For command-line tools (flyway, kubectl, terraform). - -### run_signal.sh - -```bash -#!/bin/bash -# run_signal.sh - CLI tool signal check -# -# Usage: run_signal.sh [args...] - -set -euo pipefail - -if [[ $# -lt 1 ]]; then - echo "Usage: run_signal.sh [args...]" - exit 1 -fi - -COMMAND="$1" -shift -ARGS="$*" - -echo "═══════════════════════════════════════════════════════════════" -echo "CLI SIGNAL" -echo "═══════════════════════════════════════════════════════════════" -echo "" -echo "Command: $COMMAND $ARGS" -echo "" - -# Execute command -# TODO: Replace with actual CLI invocation -OUTPUT=$($COMMAND $ARGS 2>&1) || EXIT_CODE=$? -EXIT_CODE=${EXIT_CODE:-0} - -echo "Output:" -echo "$OUTPUT" -echo "" - -echo "═══════════════════════════════════════════════════════════════" -echo "RESULTS" -echo "═══════════════════════════════════════════════════════════════" -echo "" -echo "{" -echo " \"command\": \"$COMMAND $ARGS\"," -echo " \"exit_code\": $EXIT_CODE," -echo " \"status\": \"$([ $EXIT_CODE -eq 0 ] && echo SUCCESS || echo FAILED)\"" -echo "}" - -exit $EXIT_CODE -``` - ---- - -## Config Validation Pattern - -For configuration files (YAML, JSON, properties). - -### run_signal.sh - -```bash -#!/bin/bash -# run_signal.sh - Config validation signal -# -# Usage: run_signal.sh - -set -euo pipefail - -if [[ $# -lt 1 ]]; then - echo "Usage: run_signal.sh " - exit 1 -fi - -CONFIG_FILE="$1" - -echo "═══════════════════════════════════════════════════════════════" -echo "CONFIG VALIDATION SIGNAL" -echo "═══════════════════════════════════════════════════════════════" -echo "" -echo "File: $CONFIG_FILE" -echo "" - -# Check file exists -if [[ ! -f "$CONFIG_FILE" ]]; then - echo "ERROR: File not found" - exit 1 -fi - -# Validate based on extension -EXT="${CONFIG_FILE##*.}" -VALID=true -ERROR_MSG="" - -case "$EXT" in - yaml|yml) - # TODO: Use yq or python for validation - if command -v python3 &> /dev/null; then - python3 -c "import yaml; yaml.safe_load(open('$CONFIG_FILE'))" 2>&1 || { - VALID=false - ERROR_MSG="Invalid YAML syntax" - } - fi - ;; - json) - if command -v jq &> /dev/null; then - jq . "$CONFIG_FILE" > /dev/null 2>&1 || { - VALID=false - ERROR_MSG="Invalid JSON syntax" - } - elif command -v python3 &> /dev/null; then - python3 -c "import json; json.load(open('$CONFIG_FILE'))" 2>&1 || { - VALID=false - ERROR_MSG="Invalid JSON syntax" - } - fi - ;; - *) - echo "Warning: Unknown extension, skipping syntax check" - ;; -esac - -echo "═══════════════════════════════════════════════════════════════" -echo "RESULTS" -echo "═══════════════════════════════════════════════════════════════" -echo "" -echo "{" -echo " \"file\": \"$CONFIG_FILE\"," -echo " \"valid\": $VALID," -echo " \"error\": \"$ERROR_MSG\"" -echo "}" - -$VALID && exit 0 || exit 1 -``` - ---- - -## Output Format - -All signal scripts should output structured JSON for AI parsing: - -```json -{ - "status": "SUCCESS|FAILED|ERROR", - "details": "Human-readable summary", - "metrics": { - "key": "value" - } -} -``` - -Exit codes: -- `0` = Success -- `1` = Failure (generic) -- `4` = Client error (for API patterns) diff --git a/skills/sdd/expert-sdd-creator/scripts/register_expert.sh b/skills/sdd/expert-sdd-creator/scripts/register_expert.sh deleted file mode 100755 index 71ea4a9..0000000 --- a/skills/sdd/expert-sdd-creator/scripts/register_expert.sh +++ /dev/null @@ -1,262 +0,0 @@ -#!/bin/bash -# register_expert.sh - Register expert in experts.md and signals.md catalogs -# -# Usage: register_expert.sh [--dry-run] -# -# Parses expert metadata from SKILL.md and inserts entries into: -# - .claude/skills/spec-planning/references/experts.md (resolved via SKILLS_BASE) -# - .claude/skills/spec-planning/references/signals.md (resolved via SKILLS_BASE) -# -# Exit codes: -# 0 = Success -# 1 = Error - -set -euo pipefail - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -if [[ $# -lt 1 ]]; then - echo "Usage: register_expert.sh [--dry-run]" - exit 1 -fi - -EXPERT_DIR="$1" -DRY_RUN=false - -if [[ "${2:-}" == "--dry-run" ]]; then - DRY_RUN=true -fi - -# Derive skills base from script location. -# Script is at: .claude/skills/expert-sdd-creator/scripts/register_expert.sh -# Skills base (.claude/skills/) is 2 directories up from SCRIPT_DIR. -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SKILLS_BASE="$(cd "$SCRIPT_DIR/../.." && pwd)" - -if [[ ! -d "$SKILLS_BASE/spec-planning" ]]; then - echo -e "${RED}ERROR:${NC} Could not locate spec-planning skill." - echo " Expected at: $SKILLS_BASE/spec-planning/" - echo " Install it with: npx skills add " - exit 1 -fi - -EXPERTS_MD="$SKILLS_BASE/spec-planning/references/experts.md" -SIGNALS_MD="$SKILLS_BASE/spec-planning/references/signals.md" - -echo "═══════════════════════════════════════════════════════════════" -echo "EXPERT REGISTRATION" -echo "═══════════════════════════════════════════════════════════════" -echo "" -echo "Expert directory: $EXPERT_DIR" -echo "Experts catalog: $EXPERTS_MD" -echo "Signals catalog: $SIGNALS_MD" -if $DRY_RUN; then - echo -e "${YELLOW}Mode: DRY RUN (no changes will be made)${NC}" -fi -echo "" - -# Check expert directory exists -if [[ ! -d "$EXPERT_DIR" ]]; then - echo -e "${RED}ERROR:${NC} Expert directory not found: $EXPERT_DIR" - exit 1 -fi - -# Check SKILL.md exists -if [[ ! -f "$EXPERT_DIR/SKILL.md" ]]; then - echo -e "${RED}ERROR:${NC} SKILL.md not found in $EXPERT_DIR" - exit 1 -fi - -# Check catalog files exist -if [[ ! -f "$EXPERTS_MD" ]]; then - echo -e "${RED}ERROR:${NC} Experts catalog not found: $EXPERTS_MD" - exit 1 -fi -if [[ ! -f "$SIGNALS_MD" ]]; then - echo -e "${RED}ERROR:${NC} Signals catalog not found: $SIGNALS_MD" - exit 1 -fi - -# Parse SKILL.md frontmatter -echo "Parsing SKILL.md..." - -# Extract frontmatter -FRONTMATTER=$(sed -n '/^---$/,/^---$/p' "$EXPERT_DIR/SKILL.md" | sed '1d;$d') - -# Extract name -EXPERT_NAME=$(echo "$FRONTMATTER" | grep "^name:" | sed 's/name: *//') -if [[ -z "$EXPERT_NAME" ]]; then - echo -e "${RED}ERROR:${NC} Could not extract 'name' from frontmatter" - exit 1 -fi - -# Extract description -DESCRIPTION=$(echo "$FRONTMATTER" | grep "^description:" | sed 's/description: *//') -if [[ -z "$DESCRIPTION" ]]; then - echo -e "${RED}ERROR:${NC} Could not extract 'description' from frontmatter" - exit 1 -fi - -# Extract triggers from description (after "Triggers:") -TRIGGERS="" -if echo "$DESCRIPTION" | grep -q "Triggers:"; then - TRIGGERS=$(echo "$DESCRIPTION" | sed 's/.*Triggers: *//' | sed 's/ *(project).*//') -fi - -echo " Name: $EXPERT_NAME" -echo " Description: ${DESCRIPTION:0:60}..." -echo " Triggers: ${TRIGGERS:0:60}..." -echo "" - -# Check if expert already registered -if grep -q "### [0-9]*\. $EXPERT_NAME$" "$EXPERTS_MD"; then - echo -e "${YELLOW}WARNING:${NC} Expert '$EXPERT_NAME' already exists in experts.md" - echo "Skipping registration to avoid duplicates." - exit 0 -fi - -# Count existing entries to determine next number -LAST_NUM=$(grep '### [0-9]*\.' "$EXPERTS_MD" | sed 's/### \([0-9]*\)\..*/\1/' | tail -1) -if [[ -z "$LAST_NUM" ]]; then - LAST_NUM=0 -fi -NEXT_NUM=$((LAST_NUM + 1)) - -echo "Next entry number: $NEXT_NUM" -echo "" - -# Get reference files for "What It Provides" -PROVIDES="" -if [[ -d "$EXPERT_DIR/references" ]]; then - for ref in "$EXPERT_DIR/references"/*.md; do - if [[ -f "$ref" && "$(basename "$ref")" != "signal-workflow.md" ]]; then - # Convert filename to readable form - TOPIC=$(basename "$ref" .md | tr '-' ' ' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)}1') - PROVIDES="${PROVIDES}- ${TOPIC} guidance\n" - fi - done -fi - -# Generate experts.md entry -read -r -d '' EXPERTS_ENTRY << EOF || true - -### ${NEXT_NUM}. ${EXPERT_NAME} - -**Description:** ${DESCRIPTION%% Triggers:*} - -**Triggers:** -$(echo "$TRIGGERS" | tr ',' '\n' | sed 's/^ */- /') - -**What It Provides:** -$(echo -e "$PROVIDES") -**Signal Capability:** -- Runtime validation (see signal-workflow.md) - -**Skill Name:** \`${EXPERT_NAME}\` - ---- -EOF - -# Generate signals.md entry -read -r -d '' SIGNALS_ENTRY << EOF || true - -### ${NEXT_NUM}. ${EXPERT_NAME} (with signal capability) - -**Description:** Runtime validation for ${EXPERT_NAME#expert-} domain - -**Triggers:** -$(echo "$TRIGGERS" | tr ',' '\n' | sed 's/^ */- /') -- signal, verify, validate - -**What It Validates:** -- Domain-specific behavior validation -- See references/signal-workflow.md for details - -**How to Invoke:** \`skill: "${EXPERT_NAME}"\` - -**Typical Expected Behaviors:** -- Signal scripts return exit code 0 -- Validation checks pass - -**Typical Failure Indicators:** -- Signal scripts return non-zero exit code -- Validation errors in output - -**Skill Name:** \`${EXPERT_NAME}\` - ---- -EOF - -echo "═══════════════════════════════════════════════════════════════" -echo "ENTRIES TO ADD" -echo "═══════════════════════════════════════════════════════════════" -echo "" -echo -e "${BLUE}experts.md entry:${NC}" -echo "$EXPERTS_ENTRY" -echo "" -echo -e "${BLUE}signals.md entry:${NC}" -echo "$SIGNALS_ENTRY" -echo "" - -if $DRY_RUN; then - echo "═══════════════════════════════════════════════════════════════" - echo -e "${YELLOW}DRY RUN - No changes made${NC}" - echo "═══════════════════════════════════════════════════════════════" - echo "" - echo "To apply changes, run without --dry-run flag" - exit 0 -fi - -# Find insertion point in experts.md (before "## Example:") -echo "Inserting into experts.md..." -# Find line number of "## Example:" section -EXPERTS_INSERT_LINE=$(grep -n "^## Example:" "$EXPERTS_MD" | head -1 | cut -d: -f1) -if [[ -z "$EXPERTS_INSERT_LINE" ]]; then - # If no Example section, append to end - echo "$EXPERTS_ENTRY" >> "$EXPERTS_MD" - EXPERTS_INSERT_LINE="end" -else - # Insert before Example section - EXPERTS_INSERT_LINE=$((EXPERTS_INSERT_LINE - 1)) - # Create temp file with insertion - head -n "$EXPERTS_INSERT_LINE" "$EXPERTS_MD" > "$EXPERTS_MD.tmp" - echo "$EXPERTS_ENTRY" >> "$EXPERTS_MD.tmp" - tail -n +"$((EXPERTS_INSERT_LINE + 1))" "$EXPERTS_MD" >> "$EXPERTS_MD.tmp" - mv "$EXPERTS_MD.tmp" "$EXPERTS_MD" -fi -echo -e " ${GREEN}Done${NC} (inserted at line $EXPERTS_INSERT_LINE)" - -# Find insertion point in signals.md (before "## How to Use") -echo "Inserting into signals.md..." -SIGNALS_INSERT_LINE=$(grep -n "^## How to Use" "$SIGNALS_MD" | head -1 | cut -d: -f1) -if [[ -z "$SIGNALS_INSERT_LINE" ]]; then - # If no How to Use section, append to end - echo "$SIGNALS_ENTRY" >> "$SIGNALS_MD" - SIGNALS_INSERT_LINE="end" -else - # Insert before How to Use section - SIGNALS_INSERT_LINE=$((SIGNALS_INSERT_LINE - 1)) - # Create temp file with insertion - head -n "$SIGNALS_INSERT_LINE" "$SIGNALS_MD" > "$SIGNALS_MD.tmp" - echo "$SIGNALS_ENTRY" >> "$SIGNALS_MD.tmp" - tail -n +"$((SIGNALS_INSERT_LINE + 1))" "$SIGNALS_MD" >> "$SIGNALS_MD.tmp" - mv "$SIGNALS_MD.tmp" "$SIGNALS_MD" -fi -echo -e " ${GREEN}Done${NC} (inserted at line $SIGNALS_INSERT_LINE)" - -echo "" -echo "═══════════════════════════════════════════════════════════════" -echo -e "${GREEN}REGISTRATION COMPLETE${NC}" -echo "═══════════════════════════════════════════════════════════════" -echo "" -echo "{" -echo " \"expert\": \"$EXPERT_NAME\"," -echo " \"status\": \"REGISTERED\"," -echo " \"experts_md_line\": \"$EXPERTS_INSERT_LINE\"," -echo " \"signals_md_line\": \"$SIGNALS_INSERT_LINE\"" -echo "}" diff --git a/skills/sdd/expert-sdd-creator/scripts/validate_expert.sh b/skills/sdd/expert-sdd-creator/scripts/validate_expert.sh deleted file mode 100755 index 119ee7f..0000000 --- a/skills/sdd/expert-sdd-creator/scripts/validate_expert.sh +++ /dev/null @@ -1,202 +0,0 @@ -#!/bin/bash -# validate_expert.sh - Validate expert directory structure -# -# Usage: validate_expert.sh -# -# Validates: -# - Directory structure (SKILL.md, references/, scripts/) -# - YAML frontmatter (name, description) -# - References directory has files -# - signal-workflow.md exists -# - scripts/run_signal.sh exists and is executable -# -# Exit codes: -# 0 = Valid -# 1 = Invalid - -set -euo pipefail - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -if [[ $# -lt 1 ]]; then - echo "Usage: validate_expert.sh " - exit 1 -fi - -EXPERT_DIR="$1" -ERRORS=0 -WARNINGS=0 - -echo "═══════════════════════════════════════════════════════════════" -echo "EXPERT VALIDATION" -echo "═══════════════════════════════════════════════════════════════" -echo "" -echo "Directory: $EXPERT_DIR" -echo "" - -# Check directory exists -if [[ ! -d "$EXPERT_DIR" ]]; then - echo -e "${RED}[FAIL]${NC} Directory does not exist: $EXPERT_DIR" - exit 1 -fi - -# Extract expert name from directory -EXPERT_NAME=$(basename "$EXPERT_DIR") - -echo "Validating: $EXPERT_NAME" -echo "" - -# 1. Check SKILL.md exists -echo -n "[1/7] SKILL.md exists: " -if [[ -f "$EXPERT_DIR/SKILL.md" ]]; then - echo -e "${GREEN}PASS${NC}" -else - echo -e "${RED}FAIL${NC}" - ((ERRORS++)) -fi - -# 2. Check YAML frontmatter -echo -n "[2/7] Valid YAML frontmatter: " -if [[ -f "$EXPERT_DIR/SKILL.md" ]]; then - # Extract frontmatter (between --- markers) - FRONTMATTER=$(sed -n '/^---$/,/^---$/p' "$EXPERT_DIR/SKILL.md" | sed '1d;$d') - - # Check for name field - if echo "$FRONTMATTER" | grep -q "^name:"; then - # Check for description field - if echo "$FRONTMATTER" | grep -q "^description:"; then - echo -e "${GREEN}PASS${NC}" - else - echo -e "${RED}FAIL${NC} - missing 'description' field" - ((ERRORS++)) - fi - else - echo -e "${RED}FAIL${NC} - missing 'name' field" - ((ERRORS++)) - fi -else - echo -e "${YELLOW}SKIP${NC} - SKILL.md not found" -fi - -# 3. Check name matches directory -echo -n "[3/7] Name matches directory: " -if [[ -f "$EXPERT_DIR/SKILL.md" ]]; then - SKILL_NAME=$(sed -n '/^---$/,/^---$/p' "$EXPERT_DIR/SKILL.md" | grep "^name:" | sed 's/name: *//') - if [[ "$SKILL_NAME" == "$EXPERT_NAME" ]]; then - echo -e "${GREEN}PASS${NC}" - else - echo -e "${YELLOW}WARN${NC} - name '$SKILL_NAME' != directory '$EXPERT_NAME'" - ((WARNINGS++)) - fi -else - echo -e "${YELLOW}SKIP${NC}" -fi - -# 4. Check references/ directory -echo -n "[4/7] references/ directory: " -if [[ -d "$EXPERT_DIR/references" ]]; then - REF_COUNT=$(find "$EXPERT_DIR/references" -name "*.md" -type f | wc -l | tr -d ' ') - if [[ $REF_COUNT -gt 0 ]]; then - echo -e "${GREEN}PASS${NC} ($REF_COUNT files)" - else - echo -e "${RED}FAIL${NC} - no .md files in references/" - ((ERRORS++)) - fi -else - echo -e "${RED}FAIL${NC} - directory not found" - ((ERRORS++)) -fi - -# 5. Check signal-workflow.md exists -echo -n "[5/7] signal-workflow.md: " -if [[ -f "$EXPERT_DIR/references/signal-workflow.md" ]]; then - echo -e "${GREEN}PASS${NC}" -else - echo -e "${RED}FAIL${NC} - not found" - ((ERRORS++)) -fi - -# 6. Check scripts/ directory -echo -n "[6/7] scripts/ directory: " -if [[ -d "$EXPERT_DIR/scripts" ]]; then - echo -e "${GREEN}PASS${NC}" -else - echo -e "${RED}FAIL${NC} - directory not found" - ((ERRORS++)) -fi - -# 7. Check for executable signal scripts -echo -n "[7/7] Signal scripts: " -if [[ -d "$EXPERT_DIR/scripts" ]]; then - # Look for run_signal.sh or any *signal*.sh script - SIGNAL_SCRIPTS=$(find "$EXPERT_DIR/scripts" -name "*.sh" -type f \( -name "run_signal.sh" -o -name "*signal*.sh" \) 2>/dev/null || true) - if [[ -n "$SIGNAL_SCRIPTS" ]]; then - SCRIPT_NAME=$(basename "$(echo "$SIGNAL_SCRIPTS" | head -1)") - # Check if at least one is executable - EXECUTABLE_COUNT=0 - for script in $SIGNAL_SCRIPTS; do - if [[ -x "$script" ]]; then - ((EXECUTABLE_COUNT++)) - fi - done - if [[ $EXECUTABLE_COUNT -gt 0 ]]; then - echo -e "${GREEN}PASS${NC} ($SCRIPT_NAME)" - else - echo -e "${YELLOW}WARN${NC} - found but not executable" - ((WARNINGS++)) - fi - else - # Fallback: check for any .sh scripts - ANY_SCRIPTS=$(find "$EXPERT_DIR/scripts" -name "*.sh" -type f 2>/dev/null | head -1 || true) - if [[ -n "$ANY_SCRIPTS" ]]; then - SCRIPT_NAME=$(basename "$ANY_SCRIPTS") - echo -e "${YELLOW}WARN${NC} - found $SCRIPT_NAME (not named *signal*)" - ((WARNINGS++)) - else - echo -e "${RED}FAIL${NC} - no .sh scripts found" - ((ERRORS++)) - fi - fi -else - echo -e "${RED}FAIL${NC} - scripts/ not found" - ((ERRORS++)) -fi - -# Summary -echo "" -echo "═══════════════════════════════════════════════════════════════" -echo "SUMMARY" -echo "═══════════════════════════════════════════════════════════════" -echo "" - -if [[ $ERRORS -eq 0 ]]; then - echo -e "${GREEN}VALID${NC} - Expert structure is valid" - if [[ $WARNINGS -gt 0 ]]; then - echo -e "${YELLOW}$WARNINGS warning(s)${NC}" - fi - echo "" - echo "{" - echo " \"expert\": \"$EXPERT_NAME\"," - echo " \"status\": \"VALID\"," - echo " \"errors\": $ERRORS," - echo " \"warnings\": $WARNINGS" - echo "}" - exit 0 -else - echo -e "${RED}INVALID${NC} - $ERRORS error(s) found" - if [[ $WARNINGS -gt 0 ]]; then - echo -e "${YELLOW}$WARNINGS warning(s)${NC}" - fi - echo "" - echo "{" - echo " \"expert\": \"$EXPERT_NAME\"," - echo " \"status\": \"INVALID\"," - echo " \"errors\": $ERRORS," - echo " \"warnings\": $WARNINGS" - echo "}" - exit 1 -fi diff --git a/skills/sdd/implement-mainspec/SKILL.md b/skills/sdd/implement-mainspec/SKILL.md index 4bbedfa..7324648 100644 --- a/skills/sdd/implement-mainspec/SKILL.md +++ b/skills/sdd/implement-mainspec/SKILL.md @@ -1,53 +1,42 @@ --- name: implement-mainspec -description: Implements a mainspec end-to-end by auto-detecting mode. Sequential mode (≤3 slices) commits slices in order on the current `feature/` branch. Parallel mode (>3 slices) uses dependency-aware tiered execution with per-slice worktrees, branches, PRs, and auto-merge into the feature branch. Agent-first — invoked headless by the harness dispatcher with the feature slug as its single argument. No human-in-the-loop, no approval gates. +description: Implements a mainspec end-to-end by delegating each slice to a `slice-implementer` subagent in dependency order, committing to `feature/` directly. Agent-first — invoked headless by the harness dispatcher with the feature slug. No human-in-the-loop, no approval gates. --- # Implement Mainspec -Implements all slices from a mainspec in dependency order. Auto-detects **sequential** (≤3 slices) or **parallel** (>3 slices) mode based on tier computation. Agent-first: invoked headless by the harness dispatcher, runs end-to-end with no approval gates, exits. +Implements all slices from a mainspec in dependency order, sequentially. Agent-first: invoked headless by the harness dispatcher, runs end-to-end with no approval gates, exits. ## Invocation Contract -**Invoked by the dispatcher as:** `claude -p "/implement-mainspec "`, run from inside the feature worktree (the dispatcher `cd`s into it — there is no print-mode `--cwd` flag). +**Invoked by the dispatcher as:** `claude -p "/implement-mainspec "`, run from inside the feature worktree (the dispatcher `cd`s into it). **Single argument:** `` — kebab-case feature slug. The mainspec path is `specs//mainspec.md` relative to cwd. **Inputs read from disk (paths relative to cwd):** - `specs//mainspec.md` - `specs//slices/*.md` -- Current `feature/` branch state (worktree HEAD). +- Current `feature/` branch state. **Outputs to disk / remote:** -- Per-slice branches `slice/-` pushed to origin (**not deleted after merge** — they remain as archaeological evidence so the eventual `feature/ → main` PR review can trace which commits belong to which slice). -- Per-slice PRs merged into `feature/` with `gh pr merge --merge` (preserves slice commits + a merge commit). -- All implementation code committed and pushed on `feature/`. +- All implementation code committed and pushed on `feature/`, one commit per slice. -**No sentinel.** The dispatcher's success signal for this step is `./prds//run-prd-test.sh` exits 0, verified externally. The PRD test is encoded into a slice's Signal by spec-planning, so completing all slices implies the PRD test passes. We do not write a sentinel file — the *dispatcher* records the green result in `specs//.prd-passed` once the runner first exits 0 (so a possibly-LLM-judge runner isn't re-run every tick); this skill neither writes nor reads that file. +**No sentinel.** The dispatcher's success signal for this step is `./prds//run-prd-test.sh` exits 0, verified externally. The final slice's Success Criteria encodes this into a slice so completing all slices implies the PRD test passes. We do not write a sentinel file — the *dispatcher* records the green result in `specs//.prd-passed` once the runner first exits 0; this skill neither writes nor reads that file. **Idempotency:** -- On every invocation, inspect `feature/` git history. Determine which slices are already merged (look for merge commits like `Merge slice/-` or branches whose commits match slice scope). -- Compute the remaining slices via the DAG (`compute_tiers.py`) minus the already-merged set. +- On every invocation, inspect `feature/` git history. Determine which slices are already committed (look for commits like `Implement slice : `). +- The orchestrator reads the mainspec's Slice Dependency Map inline to plan its execution order. - Work only on remaining slices. -- If all slices appear merged, exit cleanly. The dispatcher will re-verify the PRD test externally. +- If all slices appear committed, exit cleanly. The dispatcher will re-verify the PRD test externally. -**No approval gates anywhere.** No `AskUserQuestion` for plan approval, default-branch warning, tier review, retry decisions, or branch-state ambiguity. Every decision is deterministic or deferred to the next dispatcher tick. +**No approval gates anywhere.** No `AskUserQuestion` for plan approval, default-branch warning, or any other decision. Every decision is deterministic or deferred to the next dispatcher tick. ## Workflow Overview ``` -Phase 0: Detection — Run compute_tiers.py, check total_slices to select mode -Phase 1: Parse & Resume — Read mainspec, parse DAG, inspect feature/ for already-merged slices - ---- SEQUENTIAL MODE (≤3 slices) --- -Phase 2: Implement — Delegate each remaining slice to slice-implementer subagent (sequential, foreground). Orchestrator handles git directly on feature/. +Phase 1: Parse & Resume — Read mainspec Slice Dependency Map, determine execution order, inspect feature/ for already-committed slices +Phase 2: Implement — Delegate each remaining slice to slice-implementer subagent (foreground). Orchestrator handles git directly on feature/. Phase 3: Log — Log final status of this invocation to stdout for observability - ---- PARALLEL MODE (>3 slices) --- -Phase 2: Tier 0 — Delegate foundation slices to slice-implementer subagents (sequential, foreground). Orchestrator handles git directly on feature/. -Phase 3: Parallel Tiers — For each subsequent tier: create worktrees, spawn slice-implementer subagents (background), orchestrator handles git after completion. -Phase 4: Auto-Merge — Create PRs and auto-merge into feature/ with `gh pr merge --merge`. Branches are NOT deleted on merge. -Phase 5: Log — Log final status of this invocation to stdout for observability ``` ## Pre-conditions @@ -57,106 +46,54 @@ The dispatcher guarantees a clean worktree on `feature/` before invokin 1. **Git repository** — `git rev-parse --git-dir` succeeds. If not, exit non-zero. 2. **Current branch matches `feature/`** — must match the argument exactly. If not, exit. (Never create the feature branch; the dispatcher's atomic rename from `prd//` did that.) 3. **Clean working tree** — `git status --porcelain` empty. If not, exit (the dispatcher's wipe should guarantee this; if it trips, something upstream is wrong). -4. **Remote configured** — `git remote get-url origin` succeeds. If not, exit. Parallel mode requires remote. -5. **Worktree gitignore** — verify `.claude/worktrees/` is in `.gitignore`. Auto-append if missing (idempotent; no prompt). - -## Phase 0: Mode Detection - -Run the tier computation script and select the execution mode deterministically: - -``` -1. Run: python3 .claude/skills/implement-mainspec/scripts/compute_tiers.py specs//mainspec.md -2. Parse the JSON output which includes: - - mainspec_name, feature_branch - - tiers: array of { tier: N, slices: [{ number, name, file }] } - - file: absolute path to the slice file (can be used directly by subagents) - - total_slices, max_parallel -3. Mode detection (no override, no prompt): - - total_slices ≤ 3 → SEQUENTIAL MODE - - total_slices > 3 → PARALLEL MODE -4. Log to stdout: "Detected slices → mode" for observability. -``` +4. **Remote configured** — `git remote get-url origin` succeeds. If not, exit. ## Phase 1: Parse & Resume -### DAG Parsing - -`compute_tiers.py` parses the mainspec's "Slice Dependency Map" table: +### Slice Ordering -| Field | Source | Example | -|-------|--------|---------| -| Slice number | First column, before ` — ` | `3.1` | -| Slice name | First column, after ` — ` | `SVG Path Animation Utilities` | -| Dependencies | "Depends On" column | `3.1` or `—` (none) | -| Blocks | "Blocks" column | `3.2, 3.3, 3.4, 3.5` or `—` (none) | -| Slice file path | From compute_tiers.py output | absolute path to slice file | +Read the mainspec's "Slice Dependency Map" table directly. For each slice, note its number, name, and `Depends On` list. Use that understanding to plan the execution order — slices whose dependencies are already committed (or have no dependencies) can run next; slices with outstanding dependencies wait. -The script handles topological sort and tier assignment via the algorithm: +Reference the slice file path convention: `specs//slices/-.md`. -``` -1. Start with all slices. remaining = all slices. -2. Tier 0 = slices with no dependencies (Depends On = "—" or "Nothing") -3. Remove Tier 0 from remaining -4. Tier N = slices in remaining whose ALL dependencies are in Tiers 0..N-1 -5. Remove Tier N from remaining -6. Repeat until remaining is empty -7. If remaining is not empty and no progress was made → circular dependency error -``` +### Resume Detection (Idempotency) -### Resume detection (idempotency) - -Before implementing anything, inspect what's already merged on `feature/`: +Before implementing anything, inspect what's already committed on `feature/`: ``` 1. git fetch origin -2. For each slice in the mainspec DAG: - - Check whether a merge commit referencing `slice/-` exists in - `git log feature/` (e.g., `git log --merges --grep "slice/-"`). - - If yes, mark slice as ALREADY-MERGED. Skip it. +2. For each slice in the mainspec: + - Check whether a commit referencing "Implement slice :" exists in + git log feature/ (e.g., git log --oneline --grep "Implement slice :"). + - If yes, mark slice as ALREADY-COMMITTED. Skip it. - Otherwise, mark slice as REMAINING. -3. If all slices are ALREADY-MERGED, log "all slices merged, nothing to do" and exit cleanly. -4. Otherwise, proceed with REMAINING slices in their original tier assignments. +3. If all slices are ALREADY-COMMITTED, log "all slices committed, nothing to do" and exit cleanly. +4. Otherwise, proceed with REMAINING slices in their dependency order. ``` -### Plan logging (no approval) - -Log the computed plan to stdout for observability — what mode, which tiers, which slices are remaining vs. already-merged. **No approval gate, no `AskUserQuestion`.** Proceed directly to Phase 2. +### Plan Logging (No Approval) -**Sequential mode log format:** +Log the computed plan to stdout for observability — which slices are remaining vs. already-committed. **No approval gate, no `AskUserQuestion`.** Proceed directly to Phase 2. ``` ## Execution Plan: -Mode: Sequential -Total slices: N (already-merged: M, remaining: K) +Total slices: N (already-committed: M, remaining: K) Remaining order: 1. Slice X.Y: 2. Slice X.Y: ``` -**Parallel mode log format:** - -``` -## Execution Plan: -Mode: Parallel -Feature branch: feature/ -Total slices: N (already-merged: M, remaining: K) -Total tiers: T (remaining tiers to process: T') -Tier 0 (Foundation): X.Y , X.Y -Tier 1 (Parallel — N subagents): X.Y , X.Y -Tier 2 (Parallel — N subagents): X.Y -``` +## Phase 2: Implementation -## Phase 2: Sequential Implementation +The preconditions already guaranteed we are on `feature/`, so we commit directly to it. No default-branch warning, no approval prompt. -**Applies only to SEQUENTIAL MODE (≤3 slices).** The preconditions section already guaranteed we are on `feature/`, so we commit directly to it. No default-branch warning, no approval prompt. +For each **REMAINING** slice in dependency order (skipping already-committed ones from Phase 1): -For each **REMAINING** slice in dependency order (skipping already-merged ones from Phase 1): - -1. Create TODO list — One item per slice with sub-items for: Implement, Signal Validation, Unit Tests. +1. Create TODO list — One item per slice. 2. Mark slice in_progress. 3. Delegate to a `slice-implementer` subagent (foreground, NOT background): - Use `subagent_type: "slice-implementer"` and `mode: "bypassPermissions"`. - - Use the prompt template from `references/subagent-prompt-template.md` (Sequential Mode section). The template passes `max_signal_iterations: 3` to bound the inner signal-fix loop. + - Use the prompt template from `references/subagent-prompt-template.md`. - Set working directory to the repo root. - Wait for subagent to complete. 4. After subagent completes successfully: @@ -166,345 +103,50 @@ For each **REMAINING** slice in dependency order (skipping already-merged ones f - Orchestrator runs `git push origin feature/`. 5. If subagent reports FAILURE: - Log the failure (slice number, error text) to stdout. - - Commit any safe partial work attributable to a completed earlier slice (we should never have partial state at this point since each slice's commits happen synchronously after subagent success — but if `git status` shows changes, they should not be committed). + - Do not commit any partial state. - Exit. The dispatcher will re-fire on the next tick; idempotency in Phase 1 will skip the already-completed slices and retry this one. 6. Mark slice complete. After all remaining slices: proceed to Phase 3 (Log). -## Phases 2–5: Parallel Implementation - -**Applies only to PARALLEL MODE (>3 slices).** See below for detailed instructions: - -- **Phase 2 (Tier 0)** — See "Tier 0: Foundation Slices" section. -- **Phase 3 (Parallel Tiers)** — See "Parallel Tier Execution" section. -- **Phase 4 (Auto-Merge)** — See "Tier Auto-Merge" section. -- **Phase 5 (Log)** — Stdout log only. - -## Feature Branch Verification & Git Strategy - -**Applies only to PARALLEL MODE.** The dispatcher's atomic rename already created `feature/` before this skill ran. We verify and proceed; we never create the feature branch. - -``` -1. Verify HEAD branch == feature/: - actual="$(git rev-parse --abbrev-ref HEAD)" - [[ "${actual}" == "feature/" ]] || exit 1 -2. Verify the branch tracks origin: - git rev-parse --abbrev-ref --symbolic-full-name @{u} >/dev/null 2>&1 || git push -u origin feature/ -``` - -**Naming conventions:** -- Feature branch: `feature/` (e.g., `feature/svg-and-charts`). -- Slice branch: `slice/-` (e.g., `slice/3.3-barchart`). -- Worktree path: `.claude/worktrees//slice--`. - -**Spec file access:** Spec files are referenced by absolute path (resolved from `compute_tiers.py` output). Subagents read specs from the original location, not from their worktree. - -**Git state recovery (deterministic, no prompts):** -- Feature branch exists locally and matches origin: proceed. -- Feature branch exists but is behind origin: `git pull --ff-only origin feature/` before any tier work. -- Slice branch already exists on origin: this means a prior `/implement-mainspec` tick was interrupted. Check whether the corresponding PR is merged via `gh pr view slice/- --json state -q .state`: - - State `MERGED` → slice is done; skip it. - - State `OPEN` → PR was created but not merged; auto-merge it as part of Phase 4. - - No PR found → branch has commits but no PR; create the PR in Phase 4 and proceed. - - Branch exists with no commits past `feature/` HEAD → unusable, but harmless; recreate the worktree on top. - -## Tier 0: Foundation Slices - -**Applies only to PARALLEL MODE.** Execute on `feature/` directly. - -Tier 0 slices are delegated to `slice-implementer` subagents sequentially (one at a time, foreground). The orchestrator handles all git operations. - -For each REMAINING Tier 0 slice (in dependency order): - -1. Spawn a `slice-implementer` subagent (foreground, NOT background): - - Use `subagent_type: "slice-implementer"` and `mode: "bypassPermissions"`. - - Use the prompt template from `references/subagent-prompt-template.md` (Tier 0 section). - - Set working directory to the repo root (subagent works on feature branch directly). - - Wait for subagent to complete. -2. After subagent completes successfully: - - Orchestrator runs `git status` to detect changed files. - - Orchestrator runs `git add ` (only files the subagent created/modified). - - Orchestrator runs `git commit -m "Implement slice : "`. - - Orchestrator runs `git push origin feature/`. -3. If subagent reports FAILURE: - - Log the failure. - - Do not commit any subagent partial state. - - Exit. Dispatcher will re-fire; idempotency will skip merged work and retry this slice. -4. Proceed to next Tier 0 slice (if multiple foundation slices). - -Tier 0 slices are committed directly to `feature/` — no PRs. This is the foundation that all subsequent tiers depend on. - -## Worktree Management - -**Applies only to PARALLEL MODE.** Create worktrees before spawning subagents for each tier. - -**Critical:** Do NOT use `isolation: "worktree"` from the Agent tool — it branches from the remote default branch and won't have Tier 0 code. - -**Preflight — bootstrap availability.** Before creating any worktrees, check for `scripts/bootstrap-worktree.sh`. If it is **not** an executable file, emit a loud warning to stdout — e.g. `WARN: no executable scripts/bootstrap-worktree.sh found; slice worktrees will lack deps/.env/codegen, so signals and unit tests may fail. Continuing anyway.` — then proceed. Do **not** halt: a missing bootstrap script is a warning, not a precondition. When the script IS present, each worktree is bootstrapped right after creation (see below). - -### Creating Worktrees - -For each REMAINING slice in the current tier (skip slices whose PR is already merged): - -```bash -# Derive names from compute_tiers.py output -worktree_name="slice--" -branch_name="slice/-" -worktree_path=".claude/worktrees//${worktree_name}" - -# Create worktree branching FROM the feature branch. -# If branch already exists locally or on origin, attach without -b. -if git show-ref --verify --quiet "refs/heads/${branch_name}" \ - || git show-ref --verify --quiet "refs/remotes/origin/${branch_name}"; then - git worktree add "${worktree_path}" "${branch_name}" -else - git worktree add "${worktree_path}" -b "${branch_name}" feature/ -fi - -# Record the absolute worktree path for the subagent prompt -abs_worktree_path="$(realpath ${worktree_path})" - -# Make the fresh worktree runnable: a bare worktree has no gitignored files -# (node_modules, .env, generated code), so slice signals and unit tests would fail -# for reasons unrelated to the feature. Mirrors the dispatcher's bootstrap hook. -# Idempotent + non-interactive; a failure here is logged but does not stop the slice. -if [[ -x scripts/bootstrap-worktree.sh ]]; then - ./scripts/bootstrap-worktree.sh "${abs_worktree_path}" || \ - echo "WARN: bootstrap-worktree.sh failed for ${abs_worktree_path} — signals/tests may fail" >&2 -fi -``` - -### Cleaning Up Worktrees - -After PRs for a tier have been merged (or on a controlled exit): - -``` -1. For each worktree: git worktree remove -2. If removal fails (uncommitted changes): log the failure and exit. - The dispatcher's next tick will wipe state and re-derive what to do. -3. After all tiers complete: git worktree prune -``` - -## Parallel Tier Execution - -**Applies only to PARALLEL MODE.** Execute for each REMAINING tier after Tier 0. - -### 3a. Create Worktrees - -Create worktrees for all REMAINING slices in this tier per the Worktree Management section above. - -### 3b. Spawn Subagents - -Read `references/subagent-prompt-template.md` for the full prompt template. - -Spawn one background subagent per slice: -- Use the Agent tool with `run_in_background: true`. -- Use `subagent_type: "slice-implementer"` and `mode: "bypassPermissions"`. -- Do NOT use `isolation: "worktree"` (worktree already created in 3a). -- Spawn all subagents for this tier in a SINGLE message (maximizes parallelism). -- Capture the agent_id / task_id for each subagent. -- The prompt template includes `max_signal_iterations: 3` to bound the inner signal-fix loop. - -**Batching:** If a tier has more than 7 slices, split into batches of up to 7. Spawn batch 1, wait for all to complete, then spawn batch 2. +## Phase 3: Stdout Log (Observability Only) -### 3c. Wait for Completion - -- Subagents will notify when done (automatic delivery). -- Alternatively, use `TaskOutput(task_id=, block=true)` per subagent. -- Collect results: success/failure per slice. - -### 3d. Git Operations (Orchestrator) - -After all subagents for the tier complete, the orchestrator handles git for each worktree of a successful slice: - -``` -For each successful slice in the tier: -1. cd -2. git status (detect changed files) -3. git add (only files the subagent created/modified, NOT git add -A) -4. git commit -m "Implement slice : " -5. git push -u origin -``` - -### 3e. Handle Failures - -- If a subagent fails, log the slice number and error details to stdout. -- Other subagents in the same tier are NOT affected — they finish their work. -- The failed slice will NOT get a PR in Phase 4. -- Read `references/error-handling.md` for detailed recovery procedures. - -## Tier Auto-Merge - -**Applies only to PARALLEL MODE.** Execute after each tier's subagents complete and orchestrator git operations finish. There is no review gate, no `AskUserQuestion`, no "Review & merge / Stop here" prompt. Slice PRs are created and merged automatically. - -### 4a. Create PRs - -For each successfully completed slice in the current tier whose PR does not already exist: - -```bash -gh pr create \ - --head slice/- \ - --base feature/ \ - --title "Slice : " \ - --body "Implements slice of . Part of Tier . - -Signal: " -``` - -Create all PRs for the tier in parallel. - -### 4b. Auto-Merge PRs - -For each PR (in slice-number order): - -```bash -gh pr merge --merge -``` - -**Key constraints:** -- Use `--merge` (NOT `--squash`) — preserves the slice's individual commits plus a merge commit. The merge commit clearly delineates which commits belong to which slice when reviewing the eventual `feature/ → main` PR. -- **Do NOT pass `--delete-branch`** — slice branches stay on origin as archaeological evidence. Reviewers of the feature→main PR can navigate to `slice/-` to see the focused, in-context diff for any slice. - -After each successful merge: -- `git pull origin feature/` in the orchestrator's checkout to bring the merged commits in. - -### 4c. Handle Merge Conflicts - -If `gh pr merge` fails for conflict reasons: -- Attempt automatic resolution for simple conflicts (barrel exports, registry additions, import lists). Same heuristic patterns as the historical code. -- If unresolvable: log which PR failed and which files conflicted, then exit. The dispatcher will re-fire `/implement-mainspec` on the next tick; that invocation's idempotency check will see the failed PR is still open and either retry the merge (if origin now has the prerequisite commits) or skip if the work has moved on. - -### 4d. Worktree Cleanup - -After all PRs for the tier are merged: -- `git worktree remove ` for each slice worktree in this tier. -- Continue to the next tier (subsequent-tier worktrees branch from the updated `feature/`). - -### 4e. Failed Slices Within a Tier - -If any slice in the tier failed in Phase 3 (slice-implementer reported FAILURE): -- Do not create a PR for it. -- Log the failure to stdout with slice number and error details. -- The other tier slices proceed normally through PR creation and auto-merge. -- After the tier completes auto-merge, exit. The dispatcher will re-fire on the next tick; idempotency in Phase 1 will identify the failed slice as REMAINING and retry it. Subsequent tiers must wait until this tier is fully merged, so the exit is the correct behavior. - -## Phase 3/5: Stdout Log (Observability Only) - -There is no human to report to. Write a concise stdout log of what this invocation did so the dispatcher's log is readable. The dispatcher does not parse this output; it only checks `./prds//run-prd-test.sh` exit code externally. - -**Sequential mode log:** +There is no human to report to. Write a concise stdout log of what this invocation did so the dispatcher's log is readable. ``` ## /implement-mainspec invocation summary: -Mode: Sequential -Skipped (already-merged): (or none) +Skipped (already-committed): (or none) Implemented this invocation: Failed this invocation: (or none) Branch: feature/ (pushed) ``` -**Parallel mode log:** - -``` -## /implement-mainspec invocation summary: -Mode: Parallel -Feature branch: feature/ -Skipped (already-merged): (or none) -Tier 0 implemented: -Tier 1 implemented (auto-merged): -Tier 2 implemented (auto-merged): -... -Failed this invocation: (or none) - -If failures: dispatcher will re-fire on next tick; idempotency will skip merged work. -If no failures: dispatcher will verify `./prds//run-prd-test.sh` exits 0 on next tick. -``` - -## Signal Processing - -Signal skills provide runtime feedback during implementation. They validate that code works correctly in real environments. - -### Reading the Signal Section - -Each slice includes a Signal section after the Objective: - -``` -## Signal - -**Signal Skill:** {signal-skill-name | None} - -**Expected Behavior:** -- what should succeed when correctly implemented -``` - -### Signal Workflow - -1. After implementing slice code, check the Signal section -2. If Signal Skill is specified: - - Invoke the signal: `skill: "{signal-name}"` - - Wait for signal output - - Follow the guidance from Signal -3. If signal indicates success: Continue to unit tests -4. If signal indicates failure: - - Review signal output to identify specific issue - - Fix the implementation - - Re-invoke signal - - Repeat until signal validates success -5. If Signal Skill is "None": Skip signal validation, proceed to unit tests - ## TODO Structure -**Sequential mode:** -``` -[ ] X.Y- - Implement -[ ] X.Y- - Signal Validation -[ ] X.Y- - Unit Tests -... -``` - -**Parallel mode:** ``` -[ ] Phase 0 - Mode Detection -[ ] Phase 1 - Parse DAG & resume detection -[ ] Phase 2 - Tier 0 foundation slices (sequential) -[ ] Tier 0: X.Y- - Delegate to slice-implementer -[ ] Tier 0: X.Y- - Git commit & push -[ ] Tier 1: Create worktrees -[ ] Tier 1: Spawn slice-implementer subagents (N slices) -[ ] Tier 1: Wait for completion -[ ] Tier 1: Git commit & push for each worktree -[ ] Tier 1: Create PRs -[ ] Tier 1: Auto-merge PRs (`gh pr merge --merge`) -[ ] Tier 1: Worktree cleanup +[ ] X.Y- +[ ] X.Y- ... -[ ] Phase 5 - Stdout log ``` ## References -Detailed reference material for parallel mode execution: - -- **Subagent prompt template**: Read `references/subagent-prompt-template.md` when spawning subagents in Phase 4 +- **Subagent prompt template**: Read `references/subagent-prompt-template.md` when spawning subagents in Phase 2 - **Error handling**: Read `references/error-handling.md` when any phase encounters errors -- **Release strategy**: Read `references/release-strategy.md` when presenting the summary report or when the user asks about releasing ## Guidelines -### Both Modes - **DO:** - Log the execution plan to stdout for observability (no approval gate; proceed directly). - Use absolute paths for all spec file references. - Delegate slice implementation to `slice-implementer` subagents — orchestrator stays lean. - Use `subagent_type: "slice-implementer"` and `mode: "bypassPermissions"` for all subagent spawns. -- Pass `max_signal_iterations: 3` to each subagent (via the prompt template) so the inner signal-fix loop is bounded. -- Handle all git operations (add, commit, push) in the orchestrator — subagents do NOT run git. +- Handle all git operations (add, commit, push) in the orchestrator — subagents do NOT run git. This includes any Expert-file changes a subagent's Reflect step wrote. - Use `git status` to detect changed files after subagent completion. - Read only the current slice file — never read all slices at once. - Treat the current branch as the feature branch (must match `feature/`); never create it. -- Inspect `feature/` git history at the start of every invocation to determine which slices are already merged; idempotency depends on this. -- On any failure (subagent FAILURE, merge conflict, etc.): log clearly, commit any safe partial state, exit. The dispatcher's next tick is the retry mechanism. +- Inspect `feature/` git history at the start of every invocation to determine which slices are already committed; idempotency depends on this. +- On any failure: log clearly, exit. The dispatcher's next tick is the retry mechanism. **DON'T:** - Use `AskUserQuestion` anywhere. The dispatcher provides no input, and the human is not in the loop. @@ -513,28 +155,5 @@ Detailed reference material for parallel mode execution: - Skip slices or implement out of order (within remaining work). - Proceed with failing tests. - Over-engineer beyond slice scope. -- Skip signal validation when a signal is specified. -- Ignore signal failure indicators. +- Create worktrees, per-slice branches, or PRs for individual slices. - Create a sentinel file for this skill. The dispatcher's success check is `./prds//run-prd-test.sh` exit code, not a sentinel. - -### Sequential Mode Only - -**DON'T:** -- Create PRs for individual slices. -- Create worktrees. - -### Parallel Mode Only - -**DO:** -- Bootstrap every slice worktree via `scripts/bootstrap-worktree.sh` right after `git worktree add`; if the script is absent, warn and continue (do not fail the run). -- Spawn all subagents for a tier in a single message. -- Auto-merge slice PRs with `gh pr merge --merge` (preserving slice commits + a merge commit). Never `--squash`. Never `--delete-branch` — slice branches stay on origin as evidence. -- Clean up worktrees after each tier's PRs are merged. -- Check for existing slice branches/PRs at the start of every invocation; reuse merged ones (skip), reuse open ones (continue auto-merge), recreate branchless ones from `feature/`. - -**DON'T:** -- Use `isolation: "worktree"` — it branches from default branch, missing Tier 0 code. -- Create PRs targeting `main` — always target `feature/`. -- Spawn more than 7 subagents simultaneously (batch if needed). -- Pause for human tier review — there is no human; auto-merge each tier and continue. -- Eagerly merge `main` into `feature/` — only when needed for conflicts during the eventual feature→main PR. \ No newline at end of file diff --git a/skills/sdd/implement-mainspec/references/error-handling.md b/skills/sdd/implement-mainspec/references/error-handling.md index 1b1bfa4..0fe461f 100644 --- a/skills/sdd/implement-mainspec/references/error-handling.md +++ b/skills/sdd/implement-mainspec/references/error-handling.md @@ -2,69 +2,24 @@ Reference for handling errors during mainspec implementation. Read this when any phase encounters errors. -Agent-first model: there is no human in the loop and no orchestrator session waiting on stdout. The retry mechanism is the harness dispatcher: each tick wipes uncommitted state in the worktree and re-invokes `/implement-mainspec`. Idempotency (Phase 1 resume detection) is what makes retries safe — the skill identifies which slices are already merged and works only on remaining ones. +Agent-first model: there is no human in the loop and no orchestrator session waiting on stdout. The retry mechanism is the harness dispatcher: each tick wipes uncommitted state and re-invokes `/implement-mainspec`. Idempotency (Phase 1 resume detection) is what makes retries safe — the skill identifies which slices are already committed and works only on remaining ones. ## 1. Subagent Fails Implementation - Log error details from subagent output to stdout for observability. -- Mark the slice as failed for this invocation; do not create a PR for it. -- Other slices in the same tier are NOT affected — let them finish and auto-merge. -- After the tier finishes auto-merge, exit. The dispatcher's next tick will re-fire `/implement-mainspec`; Phase 1's idempotency check will skip merged slices and retry this one. -- There is no skill-level retry cap. The dispatcher has no attempt counter for this skill (only for `local-checks`). If the same slice keeps failing across many ticks, that is an observability concern, not a skill-design concern. +- Mark the slice as failed for this invocation; do not commit any partial state. +- Exit. The dispatcher's next tick will re-fire `/implement-mainspec`; Phase 1's idempotency check will skip committed slices and retry this one. -## 2. Signal Validation Fails Inside Subagent -- The subagent's `implement-slice` workflow handles this via the bounded inner loop (`max_signal_iterations: 3` — fix code, re-invoke signal, repeat up to 3 times). -- If the cap is hit, the subagent returns FAILURE with `reason: signal_failure` and the last signal output. -- Treat this like §1 — mark slice failed, let tier finish, exit, dispatcher re-fires. - -## 3. PR Creation Fails - -- Usually means the slice branch wasn't pushed or origin is unavailable. -- Log error, continue creating PRs for other slices in the tier. -- The next dispatcher tick will re-enter Phase 4; idempotency will detect the missing PR and retry creation. - -## 4. Tier Auto-Merge — Out-of-Order or Missing Dependency - -- With auto-merge, the orchestrator processes PRs in slice-number order within each tier, and only moves to the next tier after the current tier is fully merged. Out-of-order should not happen. -- If a tier's PR somehow lacks a merged dependency (shouldn't, given Phase 1's DAG-ordered execution): exit. Dispatcher re-fires; Phase 1 will recompute and retry. - -## 5. Merge Conflicts During PR Auto-Merge - -- Attempt automatic resolution for simple, well-known conflict patterns: barrel exports (`index.ts` re-export lists), registry additions (append-only arrays/objects), import lists at file tops. -- If a conflict is outside these patterns or auto-resolution fails: - - Log affected files and the PR number to stdout. - - Commit any safe partial work (typically nothing — the auto-merge step doesn't leave the worktree dirty). - - Exit. The dispatcher's next tick re-fires; the worktree wipe clears any aborted merge state. The next invocation will retry the merge with origin's updated state. - -## 6. Worktree Issues - -- **Path already exists:** the worktree was created by a prior interrupted tick. The dispatcher's tick-start wipe operates on the **outer** worktree; nested per-slice worktrees survive. Check whether the existing slice worktree's branch matches what we want: - - If yes: attach to it via `git worktree add ` (without `-b`). - - If no: `git worktree remove --force ` then recreate. This is safe because per-slice worktrees only ever hold uncommitted slice-implementer output, which is recoverable. -- **Removal fails (uncommitted changes):** log and exit. The next tick will retry. -- **Stale references:** run `git worktree prune` at the end of a successful tier to clean up. - -## 7. Branch Already Exists - -- **Feature branch:** must exist (dispatcher's atomic rename created it). The precondition check verifies HEAD matches `feature/`; exit if not. -- **Slice branch already on origin:** auto-detect via `gh pr view --json state -q .state`: - - `MERGED` → slice is done; skip in this invocation. - - `OPEN` → PR was created but not yet merged; pick up at the Phase 4 auto-merge step. - - No PR found → branch has commits but no PR was created. Continue: Phase 4 creates the PR, then merges it. - - Branch with no commits past `feature/` HEAD → recreate the worktree on top. -- Never delete a slice branch automatically. Branches are evidence; preservation is intentional. - -## 8. Push Fails +## 2. Push Fails - Preconditions require a remote, so this means transient network / auth failure or remote rejection. - Log and exit. The dispatcher's next tick will retry. There is no fallback to "local-only mode" — the harness model assumes remote is available. -## 9. Resuming After Interruption +## 3. Resuming After Interruption - Every dispatcher tick wipes uncommitted state in the worktree (`git reset --hard HEAD && git clean -fd`) and re-invokes `/implement-mainspec`. - The skill re-derives all state from disk on every invocation: - - What's merged on `feature/` (Phase 1 idempotency check). - - Which slice branches exist on origin and the state of their PRs (Phase 4 prep). - - Which worktrees exist locally (Worktree Management section in SKILL.md). -- No checkpoint files or in-memory state to persist; the artifacts on disk ARE the state. + - What's committed on `feature/` (Phase 1 idempotency check via `git log --oneline --grep "Implement slice"`). +- No checkpoint files or in-memory state to persist; the git history IS the state. +- **Feature branch:** must exist (dispatcher's atomic rename created it). The precondition check verifies HEAD matches `feature/`; exit if not. diff --git a/skills/sdd/implement-mainspec/references/release-strategy.md b/skills/sdd/implement-mainspec/references/release-strategy.md deleted file mode 100644 index e568a32..0000000 --- a/skills/sdd/implement-mainspec/references/release-strategy.md +++ /dev/null @@ -1,49 +0,0 @@ -# Release Strategy (Parallel Mode) - -The feature branch IS the release candidate. PR `feature/` to `main` directly. No separate release branches. - -## When to Release - -The developer can release at any point during implementation: -- After Tier 0 (foundation only) -- After any tier's PRs are merged -- After all tiers are complete - -The natural release unit is "everything merged to feat so far." - -For partial release: leave unwanted slice PRs unmerged on feat, then PR feat to main with only the desired slices. - -## How to Release - -```bash -gh pr create --base main --head feature/ \ - --title "Release: " \ - --body "Includes slices: " -``` - -## Lazy Merge Policy - -After releasing (merging feat to main), do NOT immediately merge main back into feat. Only merge main into feat when: - -1. Next release PR has merge conflicts with main -2. The feat branch needs changes that landed on main after the release - -Git/GitHub handles the diff correctly — it knows what was already merged. - -### If Merging main into feat for Conflict Resolution - -```bash -git checkout feature/ -git merge main -# Resolve any conflicts -# Run tests to verify nothing broke -git push origin feature/ -# Then create the release PR -``` - -## Partial Release Constraints - -The dependency DAG determines valid release subsets: -- Can't ship a slice without its dependencies (e.g., can't ship 3.6 without 3.2-3.5) -- Only merge slice PRs whose dependencies are also merged -- The summary report (Phase 6) lists valid release subsets diff --git a/skills/sdd/implement-mainspec/references/subagent-prompt-template.md b/skills/sdd/implement-mainspec/references/subagent-prompt-template.md index 3228f60..de804cc 100644 --- a/skills/sdd/implement-mainspec/references/subagent-prompt-template.md +++ b/skills/sdd/implement-mainspec/references/subagent-prompt-template.md @@ -1,6 +1,6 @@ # Subagent Prompt Template -Use this template when spawning subagents for any tier (including Tier 0). Fill in the placeholder values for each slice. +Use this template when spawning the `slice-implementer` subagent. Fill in the placeholder values for each slice. ## Template @@ -15,23 +15,19 @@ Read the slice file at: (This is an absolute path — it may be outside your working directory) -Configuration: - max_signal_iterations: 3 - (Cap on the inner signal-fix-retry loop in implement-slice. If signal validation - has not passed after 3 fix attempts, return FAILURE with reason: signal_failure - and the last signal output. Do not loop indefinitely.) - -Follow the implement-slice workflow (preloaded in your context) to implement the slice. +Follow the implement-slice workflow (preloaded in your context) to implement the slice: +Implement → Unit Tests → Reflect Report your result: - - SUCCESS: all code implemented, tests pass. List key files created/modified. - - FAILURE: describe what went wrong and where you stopped. If signal-related, - include reason: signal_failure and the last signal output. + - SUCCESS: all code implemented, tests pass, Reflect outcome (no-op | add | edit | delete). + List key files created/modified (including any Expert reference files touched during Reflect). + - FAILURE: describe what went wrong and where you stopped. Do NOT use AskUserQuestion — no human is in the loop. Do NOT run any git commands (add, commit, push) — the orchestrator handles git. -Do NOT create a PR — the orchestrator handles PR creation. -Do NOT modify files outside your assigned slice scope. +Do NOT create a PR — the orchestrator handles all git operations. +Do NOT modify files outside your assigned slice scope (exception: `.claude/skills/expert/references/` + edits during the Reflect step are explicitly in-scope). Do NOT use git worktree commands — you are already in the correct directory. ``` @@ -39,16 +35,14 @@ Do NOT use git worktree commands — you are already in the correct directory. | Placeholder | Source | Example | |------------|--------|---------| -| `` | compute_tiers.py output → slice number | `3.3` | -| `` | compute_tiers.py output → slice name | `BarChart Component` | -| `` | compute_tiers.py output → mainspec_name | `svg-and-charts` | -| `` | For Tier 0: repo root. For Tier 1+: `realpath .claude/worktrees//` | `/home/user/repo/.claude/worktrees/svg-and-charts/slice-3.3-barchart` | -| `` | compute_tiers.py output → slice file | `/home/user/specs/svg-and-charts/slices/3.3-barchart-component.md` | +| `` | Slice Dependency Map → slice number | `3.3` | +| `` | Slice Dependency Map → slice name | `BarChart Component` | +| `` | mainspec.md → feature slug | `svg-and-charts` | +| `` | Repo root | `/home/user/repo` | +| `` | `specs//slices/-.md` (resolved to absolute path) | `/home/user/repo/specs/svg-and-charts/slices/3.3-barchart-component.md` | ## Usage -### Tier 0 (Sequential — foreground) - Spawn one subagent at a time. Wait for completion before spawning the next. ```python @@ -61,48 +55,5 @@ Agent( ) # Wait for completion # Orchestrator runs: git add, git commit, git push -# Then spawn next Tier 0 slice -``` - -### Tier 1+ (Parallel — background) - -Spawn all subagents for a tier in a **single message** for maximum parallelism: - -```python -Agent( - subagent_type="slice-implementer", - run_in_background=True, - mode="bypassPermissions", - name="slice-3.2", - description="Implement slice 3.2", - prompt="" -) -Agent( - subagent_type="slice-implementer", - run_in_background=True, - mode="bypassPermissions", - name="slice-3.3", - description="Implement slice 3.3", - prompt="" -) -# ... one per slice in the tier -# Wait for all to complete -# Orchestrator handles git for each worktree -``` - -### Sequential Mode - -Same as Tier 0 — spawn one at a time, foreground, orchestrator handles git after each. - -```python -Agent( - subagent_type="slice-implementer", - mode="bypassPermissions", - name="slice-1.1", - description="Implement slice 1.1", - prompt="" -) -# Wait for completion -# Orchestrator runs: git add, git commit # Then spawn next slice ``` diff --git a/skills/sdd/implement-mainspec/scripts/compute_tiers.py b/skills/sdd/implement-mainspec/scripts/compute_tiers.py deleted file mode 100644 index 06e5d99..0000000 --- a/skills/sdd/implement-mainspec/scripts/compute_tiers.py +++ /dev/null @@ -1,253 +0,0 @@ -#!/usr/bin/env python3 -"""Compute parallelizable tiers from a mainspec's Slice Dependency Map. - -Parses the markdown dependency table, builds a DAG, runs topological sort -to group slices into tiers, and outputs JSON with tier assignments and -naming conventions. - -Usage: - python compute_tiers.py -""" - -import json -import re -import sys -from dataclasses import dataclass, field -from pathlib import Path - - -class CircularDependencyError(Exception): - """Raised when the dependency graph contains a cycle.""" - - pass - - -@dataclass -class SliceInfo: - number: str - name: str - depends_on: list[str] = field(default_factory=list) - blocks: list[str] = field(default_factory=list) - file: str = "" - - -def _parse_refs(col: str) -> list[str]: - """Parse a comma-separated list of slice references. - - Returns empty list for '—', '–', '-', 'Nothing', or empty string. - """ - col = col.strip() - if col in ("—", "–", "-", "Nothing", ""): - return [] - return [ref.strip() for ref in col.split(",") if ref.strip()] - - -def parse_dependency_table(content: str) -> list[SliceInfo]: - """Parse the Slice Dependency Map table from mainspec content. - - Expects a section headed '## Slice Dependency Map' with a markdown table - having at minimum Slice and Depends On columns. Blocks column is optional. - """ - lines = content.split("\n") - - # Find the "## Slice Dependency Map" section - in_section = False - table_lines = [] - for line in lines: - if re.match(r"^##\s+Slice Dependency Map", line): - in_section = True - continue - if in_section: - if line.startswith("## "): - break - if line.startswith("|"): - table_lines.append(line) - - if len(table_lines) < 3: - raise ValueError( - "No valid dependency table found in 'Slice Dependency Map' section" - ) - - # Determine column indices from header - header_cols = [c.strip().lower() for c in table_lines[0].split("|")] - # Filter empty strings from leading/trailing pipes - col_map = {} - for i, col in enumerate(header_cols): - if "slice" in col and "slice" not in col_map: - col_map["slice"] = i - elif "depends" in col: - col_map["depends_on"] = i - elif "blocks" in col: - col_map["blocks"] = i - - if "slice" not in col_map or "depends_on" not in col_map: - raise ValueError( - f"Table must have 'Slice' and 'Depends On' columns. Found: {header_cols}" - ) - - has_blocks = "blocks" in col_map - - # Parse data rows (skip header + separator) - slices = [] - for row in table_lines[2:]: - cols = [c.strip() for c in row.split("|")] - if len(cols) <= col_map["slice"]: - continue - - slice_col = cols[col_map["slice"]].strip() - depends_col = cols[col_map["depends_on"]].strip() - blocks_col = cols[col_map["blocks"]].strip() if has_blocks and len(cols) > col_map["blocks"] else "" - - # Parse slice column: "X.Y — Name" or "X.Y - Name" - match = re.match(r"^([\d.]+)\s*[—–\-]\s*(.+)$", slice_col) - if not match: - continue - - number = match.group(1) - name = match.group(2).strip() - - slices.append( - SliceInfo( - number=number, - name=name, - depends_on=_parse_refs(depends_col), - blocks=_parse_refs(blocks_col), - file="", - ) - ) - - return slices - - -def compute_tiers(slices: list[SliceInfo]) -> list[list[SliceInfo]]: - """Compute parallelizable tiers via topological sort. - - Tier 0 = slices with no dependencies. - Tier N = slices whose ALL dependencies are in tiers 0..N-1. - - Raises CircularDependencyError if the graph has cycles. - """ - if not slices: - return [] - - remaining = {s.number: s for s in slices} - assigned: set[str] = set() - tiers: list[list[SliceInfo]] = [] - - while remaining: - current_tier = [ - s - for s in remaining.values() - if all(dep in assigned for dep in s.depends_on) - ] - - if not current_tier: - raise CircularDependencyError( - f"Circular dependency detected. Remaining slices: {list(remaining.keys())}" - ) - - # Sort by slice number for deterministic output - current_tier.sort(key=lambda s: [int(x) for x in s.number.split(".")]) - tiers.append(current_tier) - - for s in current_tier: - assigned.add(s.number) - del remaining[s.number] - - return tiers - - -def to_kebab(name: str) -> str: - """Convert a name to kebab-case. - - Handles CamelCase, spaces, and special characters. - """ - # Insert hyphens before uppercase letters (CamelCase → camel-case) - s = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", name) - # Replace non-alphanumeric with hyphens - s = re.sub(r"[^a-z0-9]+", "-", s.lower()) - # Collapse multiple hyphens, strip leading/trailing - s = re.sub(r"-+", "-", s).strip("-") - return s - - -def feature_branch_name(mainspec_name: str) -> str: - """Compute feature branch name from mainspec directory name.""" - return f"feature/{mainspec_name}" - - -def slice_branch_name(number: str, name: str) -> str: - """Compute slice branch name.""" - return f"slice/{number}-{to_kebab(name)}" - - -def worktree_path(mainspec_name: str, number: str, name: str) -> str: - """Compute worktree path for a slice, namespaced under the mainspec.""" - return f".claude/worktrees/{mainspec_name}/slice-{number}-{to_kebab(name)}" - - -def batch_slices(slices: list, max_agents: int = 7) -> list[list]: - """Split a tier's slices into batches of max_agents.""" - if not slices: - return [] - return [slices[i : i + max_agents] for i in range(0, len(slices), max_agents)] - - -def resolve_slice_files(slices: list[SliceInfo], mainspec_path: str) -> None: - """Resolve slice file paths to absolute paths from the slices/ directory. - - Mutates each SliceInfo.file in place. Paths are absolute so subagents - in worktrees can read them regardless of working directory. - """ - spec_dir = Path(mainspec_path).resolve().parent - slices_dir = spec_dir / "slices" - - if not slices_dir.exists(): - return - - files = list(slices_dir.glob("*.md")) - for s in slices: - for f in files: - if f.name.startswith(f"{s.number}-"): - s.file = str(f.resolve()) - break - - -def analyze_mainspec(mainspec_path: str) -> dict: - """Full analysis: parse, compute tiers, resolve files, return JSON-ready dict.""" - path = Path(mainspec_path) - content = path.read_text() - - mainspec_name = path.parent.name - - slices = parse_dependency_table(content) - resolve_slice_files(slices, mainspec_path) - tiers = compute_tiers(slices) - - max_parallel = max(len(t) for t in tiers) if tiers else 0 - - return { - "mainspec_name": mainspec_name, - "feature_branch": feature_branch_name(mainspec_name), - "tiers": [ - { - "tier": i, - "slices": [ - {"number": s.number, "name": s.name, "file": s.file} - for s in tier - ], - } - for i, tier in enumerate(tiers) - ], - "total_slices": sum(len(t) for t in tiers), - "max_parallel": max_parallel, - } - - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: compute_tiers.py ", file=sys.stderr) - sys.exit(1) - - result = analyze_mainspec(sys.argv[1]) - print(json.dumps(result, indent=2)) diff --git a/skills/sdd/implement-mainspec/scripts/tests/test_tier_computation.py b/skills/sdd/implement-mainspec/scripts/tests/test_tier_computation.py deleted file mode 100644 index dc5893c..0000000 --- a/skills/sdd/implement-mainspec/scripts/tests/test_tier_computation.py +++ /dev/null @@ -1,332 +0,0 @@ -"""Tests for compute_tiers.py — tier computation from mainspec dependency tables. - -Validates the algorithm against real mainspecs and synthetic DAG patterns. -""" - -import sys -from pathlib import Path - -import pytest - -# Add scripts directory to path so we can import compute_tiers -SCRIPTS_DIR = Path(__file__).resolve().parent.parent -sys.path.insert(0, str(SCRIPTS_DIR)) - -from compute_tiers import ( - CircularDependencyError, - SliceInfo, - analyze_mainspec, - batch_slices, - compute_tiers, - feature_branch_name, - parse_dependency_table, - slice_branch_name, - to_kebab, - worktree_path, -) - -PROJECT_ROOT = Path(__file__).resolve().parents[5] -SPECS_DIR = PROJECT_ROOT / "specs" - - -# --------------------------------------------------------------------------- -# Helper: build SliceInfo list from a compact dict -# --------------------------------------------------------------------------- - - -def _make_slices(deps: dict[str, list[str]]) -> list[SliceInfo]: - """Create SliceInfo list from {number: [dep_numbers]} dict.""" - return [ - SliceInfo(number=num, name=f"Slice {num}", depends_on=dep_list) - for num, dep_list in deps.items() - ] - - -def _tier_numbers(tiers: list[list[SliceInfo]]) -> list[list[str]]: - """Extract just the slice numbers from tier results.""" - return [sorted([s.number for s in tier]) for tier in tiers] - - -# =========================================================================== -# Real mainspec tests -# =========================================================================== - - -class TestSvgChartsTiers: - """specs/svg-and-charts/mainspec.md → 3 tiers.""" - - @pytest.fixture() - def result(self): - path = SPECS_DIR / "svg-and-charts" / "mainspec.md" - return analyze_mainspec(str(path)) - - def test_tier_count(self, result): - assert len(result["tiers"]) == 3 - - def test_tier_0(self, result): - numbers = [s["number"] for s in result["tiers"][0]["slices"]] - assert numbers == ["3.1"] - - def test_tier_1(self, result): - numbers = sorted([s["number"] for s in result["tiers"][1]["slices"]]) - assert numbers == ["3.2", "3.3", "3.4", "3.5"] - - def test_tier_2(self, result): - numbers = [s["number"] for s in result["tiers"][2]["slices"]] - assert numbers == ["3.6"] - - def test_total_slices(self, result): - assert result["total_slices"] == 6 - - def test_max_parallel(self, result): - assert result["max_parallel"] == 4 - - def test_feature_branch(self, result): - assert result["feature_branch"] == "feature/svg-and-charts" - - def test_slice_files_resolved(self, result): - for tier in result["tiers"]: - for s in tier["slices"]: - assert s["file"], f"Slice {s['number']} has no resolved file" - assert Path(s["file"]).is_absolute(), f"Slice {s['number']} file is not absolute: {s['file']}" - assert Path(s["file"]).exists(), f"Slice {s['number']} file does not exist: {s['file']}" - - -class TestSceneTransitionsTiers: - """specs/scene-transitions/mainspec.md → 4 tiers.""" - - @pytest.fixture() - def result(self): - path = SPECS_DIR / "scene-transitions" / "mainspec.md" - return analyze_mainspec(str(path)) - - def test_tier_count(self, result): - assert len(result["tiers"]) == 4 - - def test_tier_0(self, result): - numbers = [s["number"] for s in result["tiers"][0]["slices"]] - assert numbers == ["2.1"] - - def test_tier_1(self, result): - numbers = [s["number"] for s in result["tiers"][1]["slices"]] - assert numbers == ["2.2"] - - def test_tier_2(self, result): - numbers = sorted([s["number"] for s in result["tiers"][2]["slices"]]) - assert numbers == ["2.3", "2.5"] - - def test_tier_3(self, result): - numbers = [s["number"] for s in result["tiers"][3]["slices"]] - assert numbers == ["2.4"] - - -class TestImplementParallelTiers: - """specs/implement-mainspec-parallel/mainspec.md → 3 tiers (linear chain).""" - - @pytest.fixture() - def result(self): - path = SPECS_DIR / "implement-mainspec-parallel" / "mainspec.md" - return analyze_mainspec(str(path)) - - def test_tier_count(self, result): - assert len(result["tiers"]) == 3 - - def test_tier_0(self, result): - numbers = [s["number"] for s in result["tiers"][0]["slices"]] - assert numbers == ["1.1"] - - def test_tier_1(self, result): - numbers = [s["number"] for s in result["tiers"][1]["slices"]] - assert numbers == ["1.2"] - - def test_tier_2(self, result): - numbers = [s["number"] for s in result["tiers"][2]["slices"]] - assert numbers == ["1.3"] - - -# =========================================================================== -# Synthetic DAG pattern tests -# =========================================================================== - - -class TestLinearChain: - """A → B → C → D → 4 tiers, 1 slice each.""" - - def test_tiers(self): - slices = _make_slices({ - "1.1": [], - "1.2": ["1.1"], - "1.3": ["1.2"], - "1.4": ["1.3"], - }) - tiers = compute_tiers(slices) - assert len(tiers) == 4 - for tier in tiers: - assert len(tier) == 1 - - -class TestFullFanout: - """A → {B, C, D, E} → 2 tiers: [A], [B, C, D, E].""" - - def test_tiers(self): - slices = _make_slices({ - "1.1": [], - "1.2": ["1.1"], - "1.3": ["1.1"], - "1.4": ["1.1"], - "1.5": ["1.1"], - }) - tiers = compute_tiers(slices) - assert _tier_numbers(tiers) == [["1.1"], ["1.2", "1.3", "1.4", "1.5"]] - - -class TestDiamond: - """A → {B, C}, {B, C} → D → 3 tiers: [A], [B, C], [D].""" - - def test_tiers(self): - slices = _make_slices({ - "1.1": [], - "1.2": ["1.1"], - "1.3": ["1.1"], - "1.4": ["1.2", "1.3"], - }) - tiers = compute_tiers(slices) - assert _tier_numbers(tiers) == [["1.1"], ["1.2", "1.3"], ["1.4"]] - - -class TestCircularDependency: - """A → B → C → A → raises CircularDependencyError.""" - - def test_raises(self): - slices = _make_slices({ - "1.1": ["1.3"], - "1.2": ["1.1"], - "1.3": ["1.2"], - }) - with pytest.raises(CircularDependencyError): - compute_tiers(slices) - - -class TestNoDependencies: - """{A, B, C} independent → 1 tier: [A, B, C].""" - - def test_tiers(self): - slices = _make_slices({ - "1.1": [], - "1.2": [], - "1.3": [], - }) - tiers = compute_tiers(slices) - assert len(tiers) == 1 - assert _tier_numbers(tiers) == [["1.1", "1.2", "1.3"]] - - -# =========================================================================== -# Naming convention tests -# =========================================================================== - - -class TestFeatureBranchNaming: - def test_basic(self): - assert feature_branch_name("svg-and-charts") == "feature/svg-and-charts" - - def test_with_hyphens(self): - assert feature_branch_name("implement-mainspec-parallel") == "feature/implement-mainspec-parallel" - - -class TestSliceBranchNaming: - def test_basic(self): - assert slice_branch_name("3.3", "barchart") == "slice/3.3-barchart" - - def test_multi_word(self): - assert slice_branch_name("3.1", "svg path utilities") == "slice/3.1-svg-path-utilities" - - -class TestWorktreePath: - def test_basic(self): - assert worktree_path("svg-and-charts", "3.3", "barchart") == ".claude/worktrees/svg-and-charts/slice-3.3-barchart" - - def test_multi_word(self): - assert worktree_path("scene-transitions", "2.1", "scene plan types") == ".claude/worktrees/scene-transitions/slice-2.1-scene-plan-types" - - -class TestToKebab: - def test_lowercase(self): - assert to_kebab("barchart") == "barchart" - - def test_camelcase(self): - assert to_kebab("BarChart") == "bar-chart" - - def test_spaces(self): - assert to_kebab("SVG Path Utilities") == "svg-path-utilities" - - def test_special_chars(self): - assert to_kebab("Registry, Docs & Planner") == "registry-docs-planner" - - -# =========================================================================== -# Batching tests -# =========================================================================== - - -class TestBatching: - def test_large_tier(self): - """12 slices with max_agents=7 → 2 batches: [7, 5].""" - items = list(range(12)) - batches = batch_slices(items, max_agents=7) - assert len(batches) == 2 - assert len(batches[0]) == 7 - assert len(batches[1]) == 5 - - def test_exact_fit(self): - """7 slices with max_agents=7 → 1 batch.""" - items = list(range(7)) - batches = batch_slices(items, max_agents=7) - assert len(batches) == 1 - assert len(batches[0]) == 7 - - def test_small_tier(self): - """3 slices with max_agents=7 → 1 batch.""" - items = list(range(3)) - batches = batch_slices(items, max_agents=7) - assert len(batches) == 1 - assert len(batches[0]) == 3 - - def test_empty(self): - assert batch_slices([], max_agents=7) == [] - - -# =========================================================================== -# Table parsing tests -# =========================================================================== - - -class TestParseDependencyTable: - def test_basic_table(self): - content = """## Slice Dependency Map - -| Slice | Depends On | Blocks | -|-------|-----------|--------| -| 1.1 — Foundation | — | 1.2 | -| 1.2 — Feature | 1.1 | — | -""" - slices = parse_dependency_table(content) - assert len(slices) == 2 - assert slices[0].number == "1.1" - assert slices[0].name == "Foundation" - assert slices[0].depends_on == [] - assert slices[1].depends_on == ["1.1"] - - def test_missing_section_raises(self): - with pytest.raises(ValueError): - parse_dependency_table("# No dependency table here") - - def test_multiple_deps(self): - content = """## Slice Dependency Map - -| Slice | Depends On | Blocks | -|-------|-----------|--------| -| 1.3 — Final | 1.1, 1.2 | — | -""" - slices = parse_dependency_table(content) - assert slices[0].depends_on == ["1.1", "1.2"] diff --git a/skills/sdd/implement-slice/SKILL.md b/skills/sdd/implement-slice/SKILL.md index 5d0071e..045e5fe 100644 --- a/skills/sdd/implement-slice/SKILL.md +++ b/skills/sdd/implement-slice/SKILL.md @@ -1,11 +1,11 @@ --- name: implement-slice -description: Implements a single slice with Signal validation and unit tests. Agent-first — invoked by the slice-implementer subagent (under implement-mainspec). No human-in-the-loop; signal validation iterates up to a bounded `max_signal_iterations` (default 3) before reporting FAILURE. +description: Implements a single slice with unit tests and a Reflect step. Agent-first — invoked by the slice-implementer subagent (under implement-mainspec). No human-in-the-loop. --- # Implement Slice -Executes a single slice with Signal validation and unit tests. Agent-first: invoked by the `slice-implementer` subagent in a worktree the parent orchestrator (`implement-mainspec`) has set up. No human prompts; bounded inner loop. +Executes a single slice with unit tests and a post-implementation Reflect step. Agent-first: invoked by the `slice-implementer` subagent under the parent orchestrator (`implement-mainspec`). No human prompts. ## Invocation Contract @@ -13,72 +13,78 @@ This skill is invoked by the `slice-implementer` subagent, not directly by the h **Inputs:** - `slice_path` — absolute path to the slice file (may be outside the working directory). -- `working_directory` — absolute path to the worktree where work happens. -- `max_signal_iterations` — cap on the inner signal-fix-retry loop. Default: 3. +- `working_directory` — absolute path to the feature branch checkout (repo root). **Outputs (uncommitted, in the working directory):** - Implemented code matching the slice's specification. -- Signal validation completed (passed or skipped per the slice's Signal section). - Unit tests created/updated. +- Optional: edits under `.claude/skills/expert/references/` if Reflect produced a high-bar update. The parent orchestrator (`implement-mainspec`) handles git (add, commit, push) after this skill exits. Do NOT commit, do NOT run git operations, do NOT create PRs. **Result reporting (to the calling subagent):** -- **SUCCESS**: list key files changed, signal status (passed | skipped), tests passing. -- **FAILURE**: describe what went wrong. Reasons include: `signal_failure` (cap hit), `test_failure` (unit tests red), `implementation_blocked` (spec contradicts the codebase). +- **SUCCESS**: list key files changed, tests passing, Reflect outcome (no-op | add | edit | delete). +- **FAILURE**: describe what went wrong. Reasons include: `test_failure` (unit tests red), `implementation_blocked` (spec contradicts the codebase). ## Workflow 1. **Read slice** - Load the entire slice file into context. 2. **Create TODO list** - Three items: - `{slice-name} - Implement` - - `{slice-name} - Signal Validation` - `{slice-name} - Unit Tests` + - `{slice-name} - Reflect` 3. **Implement** - Mark in_progress, implement all code specified in the slice. -4. **Signal validation** - Check the Signal section: - - If Signal Skill specified: invoke `skill: "[signal-name]"`, wait for output. - - Compare against Expected Behavior. - - Fix issues and re-invoke. Track iterations. - - If iterations reach `max_signal_iterations` (default 3) without success, return FAILURE with `reason: signal_failure` and the last signal output. Do not loop indefinitely. - - If Signal Skill is "None": skip to unit tests. -5. **Unit tests** - Create/update unit tests for the implemented functionality. +4. **Unit tests** - Create/update unit tests for the implemented functionality. All tests must pass before proceeding. +5. **Reflect** - See the Reflection Protocol below. 6. **Complete** - Mark all TODOs complete, stop. -The working directory is set up freshly by `implement-mainspec` (a per-slice git worktree branched from `feature/`). There should be no prior partial state to resume from — work as if the slate is clean. +The working directory is the feature branch checkout. There should be no prior partial state to resume from — work as if the slate is clean. -## Signal Processing +## Reflection Protocol -Each slice includes a Signal section after the Objective: +Run only after Implement + Unit Tests are green. -```markdown -## Signal +### Step A — Load long-term memory -**Signal Skill:** [signal-skill-name | None] +Invoke `/expert` so the skill body and its reference files are pulled into your context: -**Expected Behavior:** -- What should succeed when correctly implemented ``` +skill: "expert" +``` +If absent, Reflect is a no-op. Never fail the slice on missing `/expert`. + +Otherwise, read the reference files that overlap with what this slice touched (architecture area, patterns used, file types modified). + +### Step B — Judge: add, edit, delete, or no-op + +You now hold two things: +1. **Your experience implementing this slice** — what was confusing, what pattern you implemented, where the spec contradicted the actual codebase (already in your context window). +2. **The current long-term memory** — what `/expert` says about those areas (loaded in Step A). + +High bar — default to **no-op** unless you have a strong reason to add, edit, or delete. Use the following table to guide your decision: + +| Outcome | When to use | +|---------|------------| +| **NO-OP** (default) | Routine work, small adjustments, no contradiction. Prefer this. | +| **ADD** | A new fact, few-shot example, or procedure the project lacks. Use reference-file prefixes: `how-to-*` / `concept-*` / `pattern-*` / `invariant-*` / `example-*`. | +| **EDIT** | An existing reference is partially wrong; correct it in place. | +| **DELETE** | An existing reference contradicts merged code reality. | + +Strong signals to act: slice got stuck for many iterations, or the slice spec explicitly contradicted code that already exists. Small deviations are noise — prefer NO-OP. -### Signal Workflow +### Step C — Apply directly (only for add/edit/delete) -1. After implementing slice code, check the Signal section -2. If Signal Skill is specified: - - Invoke the signal: `skill: "[signal-name]"` - - Wait for signal output - - Follow the guidance from Signal -3. If signal indicates success: Continue to unit tests -4. If signal indicates failure: - - Review signal output to identify specific issue - - Fix the implementation - - Re-invoke signal until success -5. If Signal Skill is "None": Skip to unit tests +- Write the change to `.claude/skills/expert/references/`. +- If you added or deleted a reference file, update `.claude/skills/expert/SKILL.md`'s one-line index. +- Do NOT touch `AGENTS.md` — that is owned by `/learn` post-merge. +- The orchestrator commits these Expert-file changes with the slice and has full authority to resolve any resulting merge conflicts. ## TODO Structure ``` [ ] 1.3-user-auth - Implement -[ ] 1.3-user-auth - Signal Validation [ ] 1.3-user-auth - Unit Tests +[ ] 1.3-user-auth - Reflect ``` ## Guidelines @@ -86,7 +92,7 @@ Each slice includes a Signal section after the Objective: **DON'T:** - Use `AskUserQuestion`. No human is in the loop. - Implement beyond the slice scope. -- Proceed with failing signal validation — but also do not exceed `max_signal_iterations`; if the cap is hit, return FAILURE with `reason: signal_failure` and the last signal output. -- Skip signal validation if specified. +- Proceed with failing unit tests. - Implement dependent slices (that's for implement-mainspec). -- Run git commands or create PRs — the parent orchestrator handles git. \ No newline at end of file +- Run git commands or create PRs — the parent orchestrator handles git. +- Fail the slice because `/expert` is unavailable or Reflect produced no update. diff --git a/skills/sdd/spec-planning/SKILL.md b/skills/sdd/spec-planning/SKILL.md index 5c4eab2..079927d 100644 --- a/skills/sdd/spec-planning/SKILL.md +++ b/skills/sdd/spec-planning/SKILL.md @@ -48,13 +48,14 @@ Spec planning starts with the end in mind. You create a mainspec that defines th - **Ground entirely in the PRD and codebase** — Operate from `prds//prd.md` and the codebase. Do NOT use AskUserQuestion. There is no human in the loop. If the PRD is too ambiguous to ground (contradictions, undefined terms, missing definition of done), follow the Ambiguous PRD handling protocol in the Invocation Contract above. - **Research codebase first** - Verify what exists today before planning. Look at specs folder to see what's been done, but verify against actual code since specs may be outdated. - **Reference the real codebase** - Ground specs in reality with actual file paths, existing patterns, and current implementations. Show what exists today as context for what should exist tomorrow. -- **Encode the PRD test as a slice success criterion** — The PRD's `run-prd-test.sh` is the definition of done. The mainspec must include a slice (typically the final one) whose Signal section names the PRD test runner (`./prds//run-prd-test.sh`) as the validation command. When this slice completes, `./prds//run-prd-test.sh` must exit 0. Document this requirement explicitly in the slice's Objective so the implementing agent does not miss it. The runner is intentionally opaque to spec-planning: it may invoke a unit test, an LLM-as-judge prompt, deterministic shell checks, or any mix — the slice's job is to make it pass, not to assume its internals. +- **Encode the PRD test as the final slice's Success Criteria** — The PRD's `run-prd-test.sh` is the definition of done. The mainspec must include a final slice whose `## Success Criteria` section names the PRD test runner (`./prds//run-prd-test.sh`) and requires it to exit 0. Document this requirement explicitly in the slice's Objective so the implementing agent does not miss it. The runner is intentionally opaque to spec-planning: it may invoke a unit test, an LLM-as-judge prompt, deterministic shell checks, or any mix — the slice's job is to make it pass, not to assume its internals. - **Think temporally** - Order mainspecs (which feature comes first?) and slices (which slice enables the next?). Document dependencies clearly. - **Right level of detail** - Clear enough for implementation agents to understand intent, but not so detailed you make up features or constrain solutions unnecessarily. - **Document forward requirements** - In each slice, capture what future slices will need from the current work. Prevents rework and enables temporal planning. - **Focus on WHAT not HOW** - Specs define intent and outcomes, not implementation steps. Use code snippets, exact file paths, and examples for context (see Context Engineering below), but don't write full implementation plans. Paint the picture of WHAT needs to exist and WHY, leaving HOW to the implementation phase. -- **Auto-invoke experts when triggers match** - Read the experts catalog at start of planning and invoke relevant experts to curate domain-specific context. -- **Write Signal section into every slice** - Every slice must include a Signal section to indicate how to validate the implementation. If no Signal matches, mark as None. +- **Consult experts** — Two tiers complement each other: + - **`/expert`** (project's long-term memory) — invoke on any non-trivial slice, design question, or architecture decision. Gives you what the project already knows about its own codebase: patterns, invariants, lessons from past slices. + - **`/expert-*`** (outside experts) — invoke when a slice touches a shared library, internal platform, or vendor SDK for which an outside expert exists. Read `references/experts.md` to discover what outside experts are available. - **Write Slice Dependency Map into every mainspec** - Every mainspec must include a "Slice Dependency Map" section with a `Slice | Depends On | Blocks` table and a Mermaid flowchart visualizing the DAG. This is the single source of truth for slice dependencies. ## Output Structure @@ -279,61 +280,34 @@ backend/ - Workspace-specific package.json: Email dependencies isolated from main backend ``` -## Expert & Signal Integration +## Expert Integration -Experts and Signals enhance spec planning by curating domain knowledge (Experts) and defining runtime validation (Signals). +Experts give planning the right domain context before slice content is written. Two tiers: -### What Are Experts? +### `/expert` — Project long-term memory -Experts are Agent Skills that provide domain-specific guidance during spec planning. They help curate better context by offering framework-specific patterns, security best practices, and internal library documentation, etc. +The project's own accumulated knowledge: architecture patterns, invariants, lessons from past slices, naming conventions, where things live. If present, consult it on any non-trivial design or architecture question. -**MUST Read the experts catalog at START of planning:** `references/experts.md` +**MUST consult at start of planning (if available):** `skill: "expert"` -### What Are Signals? +Read the skill body + relevant reference files. Use what you learn to: +- Avoid repeating patterns the project has already encoded +- Ground BEFORE/AFTER examples in real file paths and current patterns +- Avoid contradicting known invariants -Signals are Agent Skills that provide runtime feedback during implementation. They validate that code works as expected beyond just unit tests passing. +### `/expert-*` — Outside experts -**MUST Read the signals catalog at START of planning:** `references/signals.md` +Domain knowledge for shared libraries, internal platforms, and vendor SDKs the project depends on. Not project knowledge — but the project's slices may rely on these. -### How to Use Experts During Planning +**Read the catalog at start of planning:** `references/experts.md` - this is your inventory for outside experts. -1. Read `references/experts.md` at the start of spec planning -2. Match triggers - Check if the feature description matches any expert triggers -3. Auto-invoke when matched - If triggers match, invoke with `skill: "{expert-name}"` -4. Incorporate guidance - Use expert recommendations to: - - Inform BEFORE/AFTER examples with framework-specific patterns - - Create DO/DON'T sections based on common pitfalls - - Define type contracts that follow framework conventions +Match triggers — check if the feature description or slice content touches any outside expert's domain. Auto-invoke when matched. -### How to Use Signals When Writing Slices +Use outside expert guidance to: +- Inform BEFORE/AFTER examples with framework-specific patterns +- Create DO/DON'T sections based on common pitfalls +- Define type contracts that follow framework conventions -1. Read `references/signals.md` at the start of spec planning -2. For each slice being planned: - - Determine if the slice needs runtime validation - - Match slice type against signal triggers - - Write Signal section into the slice +### Check availability -### Signal Section Format - -Every slice must include a Signal section after the Objective: - -```markdown -# Slice X.Y: Name - -## Objective - -... - -## Signal - -**Signal Skill:** {signal-skill-name | None} - -**Expected Behavior:** -- Specific validations for this slice -- what should succeed when correctly implemented - -## BEFORE/AFTER Directory Structure -[rest of slice...] -``` - ---- \ No newline at end of file +If `/expert` is absent, skip silently — no fallback needed. If no `/expert-*` triggers match, skip. Never fail planning on expert unavailability. \ No newline at end of file diff --git a/skills/sdd/spec-planning/references/signals.md b/skills/sdd/spec-planning/references/signals.md deleted file mode 100644 index ee11f34..0000000 --- a/skills/sdd/spec-planning/references/signals.md +++ /dev/null @@ -1 +0,0 @@ -# Signals \ No newline at end of file diff --git a/skills/sdd/spec-validate/SKILL.md b/skills/sdd/spec-validate/SKILL.md index 507d426..1d3ea47 100644 --- a/skills/sdd/spec-validate/SKILL.md +++ b/skills/sdd/spec-validate/SKILL.md @@ -156,14 +156,14 @@ For each candidate finding, classify as **impactful** or **nitpick**: - **Impactful** — Would mislead the implementing agent. Examples: - Missing or wrong type contracts (interfaces, schemas) - Incorrect BEFORE/AFTER file paths - - Ambiguous Signal section (no clear validation command) + - Ambiguous final-slice Success Criteria (no clear PRD-test command) - Missing forward-looking requirements that block downstream slices - Security gaps in DO/DON'T examples - Wrong file path references - Missing slice dependency in the Dependency Map - Broken slice DAG (cycle, dangling reference) - Slice that contradicts the PRD's definition of done - - Slice missing the run-prd-test gate (the final slice's Signal must invoke `./prds//run-prd-test.sh` and require exit 0 as the PRD-completion criterion) + - Final slice missing Success Criteria (must invoke `./prds//run-prd-test.sh` and require exit 0 as the PRD-completion criterion) **→ Apply the fix.** @@ -197,7 +197,7 @@ Structure: ## Subagent consensus ### 3/3 (Very High Confidence) -- **[Applied]** Slice 1.2 / Signal section — finding text — fix applied: +- **[Applied]** Slice 1.2 / Success Criteria — finding text — fix applied: - **[Skipped: nitpick]** Slice 2.1 / Wording — finding text — reason for skip ### 2/3 (High Confidence) diff --git a/subagents/slice-implementer.md b/subagents/slice-implementer.md index 1456678..0378742 100644 --- a/subagents/slice-implementer.md +++ b/subagents/slice-implementer.md @@ -1,6 +1,6 @@ --- name: slice-implementer -description: Implements a single slice in a worktree. Used by implement-mainspec for parallel tier execution. +description: Implements a single slice. Used by implement-mainspec to implement one slice on the feature branch. skills: - implement-slice permissionMode: bypassPermissions @@ -8,7 +8,7 @@ permissionMode: bypassPermissions # Slice Implementer -You are a focused slice implementation agent. The `implement-slice` skill is preloaded — follow its workflow for implementation, signal validation, and unit tests. +You are a focused slice implementation agent. The `implement-slice` skill is preloaded — follow its workflow for implementation, unit tests, and reflection. ## Working Directory @@ -25,16 +25,15 @@ All file paths in the slice spec are relative to this directory. 1. **No git commands** — Do NOT run `git add`, `git commit`, `git push`, or any other git commands. The orchestrator handles all git operations after you finish. 2. **No PRs** — Do NOT create pull requests. 3. **No worktree commands** — Do NOT run `git worktree` commands. You are already in a worktree (or the correct branch). -4. **Stay in scope** — Only modify files specified by or implied by your assigned slice. Do not modify files outside your slice scope. +4. **Stay in scope** — Only modify files specified by or implied by your assigned slice. Exception: during the Reflect step, editing files under `.claude/skills/expert/references/` is explicitly in-scope. 5. **No human prompts** — Do NOT use `AskUserQuestion`. No human is in the loop. -6. **Honor `max_signal_iterations`** — If the prompt provides this value (default 3), pass it to `implement-slice` so the inner signal-fix-retry loop is bounded. Return FAILURE if the cap is hit; do not loop indefinitely. ## Workflow 1. Read the slice spec file (path provided in your prompt) -2. Follow the `implement-slice` workflow: Implement → Signal Validation → Unit Tests +2. Follow the `implement-slice` workflow: Implement → Unit Tests → Reflect 3. Report your result: - - **SUCCESS**: All code implemented, tests pass. Briefly list key files created/modified. + - **SUCCESS**: All code implemented, tests pass, Reflect outcome (no-op | add | edit | delete). Briefly list key files created/modified (including any Expert reference files from Reflect). - **FAILURE**: Describe what went wrong and where you stopped. ## Spec File Access