Files
redsen-lean-harness/README.md
T
mozempkandCopilot 8330cf7900 ci: publish @redsentech/lean-harness to GitHub Packages
- Rename npm package scope @redsen -> @redsentech (GitHub Packages
  requires the scope to match the owning org/user login).
- Add publishConfig.registry pointing at npm.pkg.github.com.
- Add .github/workflows/publish.yml: on push of a vX.Y.Z tag (or manual
  dispatch), runs validate + test, checks the tag matches
  package.json's version, then npm publish using the auto-issued
  GITHUB_TOKEN (packages: write permission, no secret to manage).
- Update README/QUICKSTART lh-CLI install instructions for the private,
  org-scoped registry (.npmrc scope + auth token setup).
- Verified locally with npm publish --dry-run: 68 files, correct
  registry target, correct tarball contents.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-10 00:51:06 +02:00

344 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# redsen-lean-harness
An **imperative**, **token-lean**, **self-documenting** agent harness for
**GitHub Copilot CLI** and **VS Code Copilot**.
It imposes a spec-driven pipeline, runs the implement→verify stage as a Ralph loop, drives
**parallel dynamic workflows on its own** using git worktree lanes, and writes down everything
it did. Mostly markdown. One runtime dependency.
---
## Why it exists
Coding agents fail in predictable ways: they guess at requirements, load far too much context,
lose what they learned between sessions, work sequentially when the work is parallel, and leave
no trace of *why* anything was done.
This harness fixes each of those with a specific mechanism, not with prompt-engineering hope.
| Failure | Mechanism |
| --- | --- |
| Guessing at requirements | `interrogator` asks questions **with recommended answers**; the pipeline blocks until answered |
| Context bloat | Only `memory/INDEX.md` is ever auto-loaded; everything else is pulled on demand |
| Amnesia between runs | Categorised memory shards, committed to the repo |
| Sequential work | Dynamic DAG + git worktree lanes with file-scope leases |
| No audit trail | ADRs, living specs, and a per-run journal — written by a cheap agent, continuously |
| Unverifiable "done" | Five explicit Ralph exit criteria, all machine-checkable |
## Design
Three layers, one hard boundary — **we never reimplement an agent runtime**
([ADR 0001](docs/adr/0001-markdown-behaviour-node-determinism.md)).
| Layer | Owns | Where |
| --- | --- | --- |
| Markdown | Behaviour — agents, skills, instructions | `.github/` |
| Node CLI (`lh`) | Determinism — index, gates, lanes, telemetry, memory | `src/` |
| Host | Execution — subagents, fleet, tools, models | Copilot CLI / VS Code |
`lh` never calls a model. Agents never do git plumbing, parsing, or token arithmetic by hand.
## Install
### GitHub Copilot CLI
```bash
# preferred: via the marketplace this repo publishes (verified end-to-end, no warning)
copilot plugin marketplace add redsentech/lean-harness
copilot plugin install redsen-lean-harness@redsen
# also works, but Copilot CLI prints a deprecation warning for direct-source installs
copilot plugin install redsentech/lean-harness
```
Either form installs only the **behaviour layer** (agents, skills, instructions, MCP config),
host-wide across every repo you open with that Copilot CLI account. You still need the `lh`
CLI on `PATH` separately — see below.
### VS Code Copilot
Clone the repo and point VS Code's Copilot customization settings at it, or copy the `.github/`
tree into your project. `.github/agents/`, `.github/instructions/` and `.github/mcp.json` are
read by both hosts.
> `.github/skills/` is **Copilot CLI only**. VS Code gets equivalent behaviour through
> `.github/agents/` and `.github/prompts/`.
### The `lh` CLI
The behaviour layer above only works if the agents can actually call `lh` — it's what runs
`lh init`/`lh doctor`/`lh graph`/etc. under the hood. Published to **GitHub Packages**
(`npm.pkg.github.com`), a private, org-scoped registry — not the public npm registry, so it
needs one extra step:
```bash
# one-time: point the @redsentech scope at GitHub Packages, with a token that has read:packages
echo "@redsentech:registry=https://npm.pkg.github.com" >> ~/.npmrc
echo "//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}" >> ~/.npmrc
npm install -g @redsentech/lean-harness # or: npx @redsentech/lean-harness <command>
lh doctor # verify the environment
```
No published version yet? Clone and link instead:
```bash
git clone git@github.com:redsentech/lean-harness.git && cd lean-harness && npm install
npm link # puts `lh` on PATH globally
lh doctor # verify the environment
```
`scripts/onboard.mjs` (see [Onboarding a new project](#onboarding-a-new-project)) does the
clone-free equivalent of `npm link`, scoped to one target repo, in a single command.
### Context7
Mandatory before any external-library work. The key is read from the environment **only** — the
harness never writes secrets to disk.
```bash
export CONTEXT7_API_KEY="<your key>" # add to ~/.bashrc or ~/.zshrc
```
## Quick start
> Full walkthrough — install paths compared, troubleshooting, and exactly when you (vs. the
> agents) need to run `lh` yourself: [docs/QUICKSTART.md](docs/QUICKSTART.md)
```bash
cd your-repo
lh init # first-run wizard; writes .agents/harness.config.json
lh doctor # confirm everything is wired
```
Then, in Copilot CLI or VS Code Copilot:
```
@conductor build a rate limiter for the public API
```
For a small brownfield change, skip the ceremony:
```
/fast-track fix the off-by-one in pagination
```
To learn an unfamiliar codebase first:
```
/onboard
```
## Onboarding a new project
Before the plugin is published to a marketplace (or if you want to try it against a local repo
first), `scripts/onboard.mjs` does the whole bootstrap in one shot: copies the behaviour layer
(`.github/agents`, `skills`, `instructions`, `prompts`, `mcp.json`, `copilot-instructions.md`,
`AGENTS.md`) into a target repo, `npm link`s the `lh` CLI so it's on `PATH` there, runs
`lh init` and `lh doctor`, and prints next steps.
```bash
# from inside this repo
node scripts/onboard.mjs /path/to/target-repo --dry-run # preview, writes nothing
node scripts/onboard.mjs /path/to/target-repo --yes # do it
```
| Flag | Effect |
| --- | --- |
| `--yes` | Non-interactive: auto `git init` if needed, run `lh init` without asking |
| `--force` | Overwrite target files that already exist and differ (default: skip + warn) |
| `--dry-run` | Print the plan, write and run nothing |
| `--no-npm-link` | Skip `npm link`; print the absolute `node .../src/cli.mjs` invocation instead |
Safe to re-run: files that are already identical are left alone, and re-running `lh init` on an
already-initialized repo warns instead of failing (rerun with `lh init --force` to reset).
`npm install -g @redsentech/lean-harness` (once your `.npmrc` points `@redsentech` at GitHub
Packages — see [The `lh` CLI](#the-lh-cli)) replaces steps 3–5 of the script with a normal
global install.
## Pipeline
```
design → interrogator asks, user answers → decisions.md [USER GATE]
plan → architect writes spec + acceptance criteria
splitter emits plan.dag.json (lanes + file scopes)
build → lh host picks the strategy
read lanes → shared checkout, N-wide scout fan-out
write lanes → lh lane create → git worktree + branch
builder ⇄ verifier (RALPH loop)
checkpoint → conductor RE-PLANS, may spawn/kill/re-scope lanes
failure → isolate → bounded retry → re-plan around it
integrate → integrator merges lanes SEQUENTIALLY, then one full verify
document → scribe writes journal, ADRs, spec, conventions (throughout)
```
### Ralph exit criteria
A lane is done only when **all** of these hold. Otherwise it iterates; at max iterations it
escalates to you.
1. Every declared verify command exits 0
2. Every acceptance criterion is individually checked off
3. `lh graph` structural gate passes
4. `reviewer` approves
5. No files were changed outside the lane's declared scope
## Agents
| Agent | Tier | Invokable | Responsibility |
| --- | --- | --- | --- |
| `conductor` | strong | yes | Entry point. Owns the pipeline and the dynamic DAG. |
| `interrogator` | strong | yes | Design-phase Q&A with recommended answers. |
| `scout` | cheap | – | Read-only recon, fanned out N-wide. |
| `architect` | strong | yes | Spec, acceptance criteria, architecture doc, ADRs. |
| `splitter` | mid | – | Decomposes the spec into lanes with file-scope globs. |
| `builder` | mid | – | Implements one lane inside its worktree. |
| `verifier` | cheap | – | Runs verify commands + the structural gate. |
| `reviewer` | strong | yes | Acceptance-criteria and scope gate. |
| `integrator` | strong | – | Sequential merge, conflict escalation, full verify. |
| `scribe` | cheap | – | Journal, ADRs, living spec, conventions. |
Tiers map to models in `harness.config.json` and are fully overridable. The default map is
`cheap → claude-haiku-4.5`, `mid → claude-sonnet-5`, `strong → claude-opus-5`. Expensive
exploration is deliberately pushed onto the cheap tier; only summaries return to the main context.
## `lh` commands
| Command | Purpose |
| --- | --- |
| `lh init` | First-run wizard → `.agents/harness.config.json` |
| `lh index [--budget N] [--focus g] [--fetch]` | Token-budgeted tree-sitter repo map |
| `lh graph` | Structural gate, terse output by default. **Exit 1 on violations** |
| `lh lane create\|list\|status\|merge\|drop` | Worktree lanes + file-scope leases |
| `lh run start\|event\|end` | NDJSON telemetry + live board |
| `lh memory get\|put\|compact\|scan\|list` | Memory shards, compaction, secret scanning |
| `lh host` | Detect host capabilities, print the orchestration strategy |
| `lh report [runId] [--journal]` | Markdown telemetry report / run journal |
| `lh doctor` | Environment checks |
### Full flag reference
<details>
<summary>Every flag each subcommand reads (click to expand)</summary>
- `lh init [--yes] [--force] [--json]` — `--yes` skips prompts with defaults; `--force` re-initializes
an existing `.agents/harness.config.json`.
- `lh index [--budget N] [--focus <glob>] [--fetch] [--force] [--stats] [--json]` — `--fetch` lazily
downloads/caches missing tree-sitter grammars; `--force` rebuilds the cache instead of reusing it;
`--stats` prints token counts instead of the map.
- `lh graph [--severity <level>] [--fix-manifest] [--json]` — terse brief is the default output;
`--fix-manifest` rewrites `codegraph.manifest.json`-style baselines.
- `lh lane create --id <id> [--title t] [--kind read\|write] [--scope glob...] [--depends-on id]
[--base ref] [--json]`
- `lh lane list [--status s] [--run-id id] [--json]`
- `lh lane status --id <id> [--json]`
- `lh lane merge --id <id> [--abort] [--strategy s] [--json]`
- `lh lane drop --id <id> [--force] [--json]`
- `lh run start --objective "..." [--spec slug] [--host h] [--strategy s] [--attrs '{"k":"v"}']
[--json]`
- `lh run event --run <id> --type <t> [--name n] [--status ok\|fail] [--agent a] [--lane id]
[--model m] [--operation op] [--input-tokens N] [--output-tokens N] [--duration-ms N]
[--exit-code N] [--attrs '{"k":"v"}'] [--json]`
- `lh run end --run <id> [--status ok\|fail] [--summary "..."] [--json]`
- `lh run list [--json]` / `lh run show [runId] [--json]`
- `lh memory list [--shard s] [--json]`
- `lh memory get --shard s [--query q] [--limit N] [--json]`
- `lh memory put --shard s --title t [--body "..." | piped via stdin] [--tags a,b] [--allow-secrets]
[--json]` — writes are refused if a secret pattern matches unless `--allow-secrets` is set.
- `lh memory compact [--force] [--json]`
- `lh memory scan` — secret scan only, no write
- `lh host [--strategy s] [--json]`
- `lh report [runId] [--run-id id] [--json] [--board] [--journal] [--spec slug] [--write]`
- `lh doctor [--json]`
</details>
## Repository footprint
The harness writes into one configurable directory:
```
.agents/
harness.config.json
architecture.md # living, ADR log inside [committed]
conventions.md # living [committed]
memory/INDEX.md # the ONLY always-loaded file [committed]
memory/{seed,failures,corrections,insights,conventions,quirks}.md
specs/<slug>/{questionnaire,decisions,spec}.md · plan.dag.json
runs/<id>/journal.md [committed]
runs/<id>/{board.md,events.ndjson} [gitignored]
.cache/ # repomap, symbols, wasm, lanes [gitignored]
```
The committed/gitignored split is chosen during `lh init`.
## Observability
`lh run event` appends OTEL-GenAI-shaped NDJSON. No collector, no server, works offline.
- **`board.md`** regenerates on every event: lanes in flight, ralph iterations, elapsed, tokens,
and **live burn rate**.
- **`lh report`** gives the post-run breakdown by phase, agent, model and lane.
- **`journal.md`** is the committed, human-readable record of what happened and why.
> There is **no hard budget cap** — a deliberate choice. The control is visibility: live burn
> rate while running, full cost breakdown after. See
> [ADR 0006](docs/adr/0006-local-ndjson-telemetry.md).
## Token discipline
1. Only `memory/INDEX.md` is always loaded; shards are pulled on demand.
2. `lh index --budget` caps the repo map and degrades signatures → names → counts.
3. Cheap-tier subagents absorb exploration; only their summaries re-enter the main context.
4. `lh graph` and `lh report` emit briefs, never dumps.
5. Skill bodies stay short; procedures live in `templates/`, loaded only when used.
6. `lh memory compact` merges the oldest entries when the budget is approached.
## Architecture decisions
| ADR | Decision |
| --- | --- |
| [0001](docs/adr/0001-markdown-behaviour-node-determinism.md) | Markdown owns behaviour, Node owns determinism |
| [0002](docs/adr/0002-web-tree-sitter-index.md) | web-tree-sitter WASM index with an offline regex fallback |
| [0003](docs/adr/0003-worktree-lane-isolation.md) | git worktree per write-lane; pluggable isolation backend |
| [0004](docs/adr/0004-no-vendoring-elastic-licensed-code.md) | No vendoring of Elastic-licensed code |
| [0005](docs/adr/0005-file-only-memory.md) | File-only memory with a single always-loaded index |
| [0006](docs/adr/0006-local-ndjson-telemetry.md) | Local NDJSON telemetry, no server, no budget cap |
## Development
```bash
npm install
npm run validate # manifests + every markdown frontmatter block
npm test
node src/cli.mjs --help
node scripts/onboard.mjs <target-dir> --dry-run # preview bootstrapping another repo
```
`npm run validate` is the distribution gate: it checks `plugin.json`, `marketplace.json`,
agent/skill/instruction frontmatter, delegation targets, and scans `.github/mcp.json` for
committed secrets. `npm test` covers config, memory, lane scope safety, glob matching, secret
scanning, telemetry, and the onboarding script end to end (`tests/onboard.test.mjs`).
## Prior art
Ideas taken (no code, no dependencies) from `pi-hermes-memory` (categorised memory shards,
secret scanning, consolidation) and `pi-context-mode` (token budgeting, compaction, checkpoint
anchors — **Elastic Licensed, deliberately not vendored**, see ADR 0004). The structural gate is
modelled on `codegraph`; the Ralph loop and run-state telemetry on `rapid-prototyping-agent`;
the packaging on `redsen-copilot-agents`.
## Roadmap
**v1** — spec pipeline, Ralph loop, memory, index + structural gate, worktree lanes, integrator,
telemetry.
**v2** — devcontainer isolation backend with a generator/manager/updater ecosystem
(`lh lane` is already backend-pluggable), semantic memory retrieval behind the existing
`getMemory()` interface, and OTEL export.
## License
MIT © Redsen