loop-engineering-harness 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +266 -0
  2. package/bin/cli.js +166 -0
  3. package/docs/langfuse-dashboards.md +39 -0
  4. package/package.json +34 -0
  5. package/src/detect.js +79 -0
  6. package/src/install.js +58 -0
  7. package/src/loop.js +181 -0
  8. package/src/merge.js +173 -0
  9. package/src/metrics.js +70 -0
  10. package/src/uninstall.js +63 -0
  11. package/template/.claude/agents/context-researcher.md +47 -0
  12. package/template/.claude/agents/planner.md +71 -0
  13. package/template/.claude/agents/reviewer.md +64 -0
  14. package/template/.claude/agents/specialist-backend.md +34 -0
  15. package/template/.claude/agents/specialist-database.md +32 -0
  16. package/template/.claude/agents/specialist-frontend.md +35 -0
  17. package/template/.claude/commands/feature.md +147 -0
  18. package/template/.claude/hooks/artifact-size.py +71 -0
  19. package/template/.claude/hooks/commit-gate.py +85 -0
  20. package/template/.claude/hooks/dispatch-gate.py +193 -0
  21. package/template/.claude/hooks/divergence-monitor.py +98 -0
  22. package/template/.claude/hooks/harness_lib.py +260 -0
  23. package/template/.claude/hooks/loop_lib.py +108 -0
  24. package/template/.claude/hooks/precompact-guard.py +91 -0
  25. package/template/.claude/hooks/req_lib.py +148 -0
  26. package/template/.claude/hooks/requirements-gate.py +62 -0
  27. package/template/.claude/hooks/retrieval-nudge.py +160 -0
  28. package/template/.claude/hooks/session-rehydrate.py +65 -0
  29. package/template/.claude/hooks/state-updater.py +119 -0
  30. package/template/.claude/hooks/trace-emitter.py +128 -0
  31. package/template/.claude/hooks/version-gate.py +86 -0
  32. package/template/.claude/hooks/version-tracker.py +67 -0
  33. package/template/.claude/hooks/version_lib.py +243 -0
  34. package/template/.claude/scripts/approve-plan.py +91 -0
  35. package/template/.claude/scripts/changeset.py +18 -0
  36. package/template/.claude/scripts/extract-deps.py +288 -0
  37. package/template/.claude/scripts/version.py +144 -0
  38. package/template/.claude/skills/contract-writing/SKILL.md +50 -0
  39. package/template/.claude/skills/discovery-interview/SKILL.md +56 -0
  40. package/template/.claude/skills/review-taxonomy/SKILL.md +51 -0
  41. package/template/CLAUDE.harness.md +68 -0
  42. package/template/harness.config.json +58 -0
  43. package/template/settings.harness.json +87 -0
package/README.md ADDED
@@ -0,0 +1,266 @@
1
+ # Loop Engineering — an agent harness for brownfield Claude Code
2
+
3
+ Turn Claude Code into a disciplined, observable, **continuously-looping** multi-agent
4
+ development pipeline you can drop into an existing codebase without disrupting it.
5
+
6
+ > **The thesis:** policy lives in exit codes and artifacts on disk, not in prompts.
7
+ > Agents exercise judgment; hooks and scripts enforce the rules; files carry the
8
+ > state. Anything an agent could forget or rationalize past is enforced deterministically.
9
+
10
+ Built entirely on Claude Code's native primitives — subagents, skills, hooks, slash
11
+ commands — plus **graphify** (deterministic code knowledge graph) and **Langfuse**
12
+ (trace observability). Design record: [Architecture.md](Architecture.md),
13
+ [Implementation.md](Implementation.md), [Usecase.md](Usecase.md),
14
+ [DevelopmentUpdates.md](DevelopmentUpdates.md), [HookDevelopment.md](HookDevelopment.md).
15
+
16
+ ---
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npx loop-engineering-harness detect . # dry run — show exactly what will change
22
+ npx loop-engineering-harness init . # install (prints a plan, asks to confirm)
23
+ npx loop-engineering-harness uninstall . # remove everything; keeps your specs/
24
+ ```
25
+
26
+ > **Note:** You can also install directly from GitHub without publishing:
27
+ > ```bash
28
+ > npx github:AakashRajendran0926/Loop-Engineering init .
29
+ > ```
30
+
31
+ Brownfield-safe by construction:
32
+
33
+ - **`CLAUDE.md`** gains a delimited `<!-- harness:begin -->…<!-- harness:end -->`
34
+ block below your content — never a rewrite. Upgrades replace only the block.
35
+ - **`.claude/settings.json`** is deep-merged: your hooks are preserved and never
36
+ reordered; harness hooks are appended with a `"_harness": true` marker. Invalid
37
+ JSON aborts the install rather than guessing.
38
+ - **File collisions** (e.g. you already have `agents/reviewer.md`) are written as
39
+ `reviewer.harness.md` with a warning — your file is never clobbered.
40
+ - **A manifest** records exactly what was written, so `uninstall` restores a
41
+ **byte-identical** repo (minus `specs/`, which is your work product).
42
+
43
+ ---
44
+
45
+ ## The gate stack — the whole point
46
+
47
+ Every gate is a hook that reads disk and exits non-zero to block. The harness
48
+ summons a human at bad **inputs**, bad **flow**, goal **drift**, failed **outputs**,
49
+ and unversioned **change** — each decided mechanically, never by an agent grading
50
+ itself.
51
+
52
+ | Gate | Fires on | Blocks unless… |
53
+ |---|---|---|
54
+ | `retrieval-nudge.py` | Grep/Glob | broad search goes graph-first (`graphify query`); auto-inits graphify if absent |
55
+ | `requirements-gate.py` | Task | intent is unambiguous, requirements structured (AC1, AC2…), flow in order, plan covers exactly the acceptance criteria |
56
+ | `dispatch-gate.py` | Task | G0–G6: approval-hash matches, graph fresh, breaker healthy, upstream done, under retry cap, prompt within context budget |
57
+ | `divergence-monitor.py` | Edit/Write | writes stay inside the task's declared footprint (else trips the breaker) |
58
+ | `artifact-size.py` | Edit/Write | context-pack / review stay within their compression budget |
59
+ | `commit-gate.py` | Bash | a passing **integration review** exists for the current diff |
60
+ | `version-gate.py` | Bash | major deps acknowledged, code version + changelog recorded, dev history captured |
61
+
62
+ Supporting hooks: `state-updater.py` (single writer of `state.json` + derived
63
+ breaker `health`), `precompact-guard.py`/`session-rehydrate.py` (compaction
64
+ indifference + resume), `trace-emitter.py` (Langfuse + local traces),
65
+ `version-tracker.py` (dependency deltas).
66
+
67
+ ---
68
+
69
+ ## Using it — `/feature`
70
+
71
+ ```
72
+ /feature add order cancellation with refund handling
73
+ ```
74
+
75
+ 1. **Discovery interview** (main thread, human) → `discovery.md` with `AC1, AC2…` acceptance criteria
76
+ 2. **context-researcher** (isolated window, graph-first) → `context-pack.md` (~2k tokens, sectioned per work area)
77
+ 3. **planner** → `contracts/` then `plan.md` (footprints, `satisfies:` coverage tags, `VERSION-IMPACT`, token estimates) → runs `extract-deps.py` → `task-graph.json`
78
+ 4. **Plan approval** (human, or `--auto-approve`) → `approvals.json` signs the decomposition; automation is frozen until it matches `plan.md`'s hash
79
+ 5. **Specialists** dispatched per the computed schedule — parallel only where footprints are *literally disjoint*, sequential along edges; task-scoped prompts (Rule 1), stateless retries (Rule 3)
80
+ 6. **reviewer** per task, then one **integration review** over the combined diff
81
+ 7. **Version** the feature (bump + changelog + snapshot)
82
+ 8. **Commit** — the commit gate *and* version gate open; the PR is backed by the full artifact chain in `specs/`, not a bare diff
83
+
84
+ `/feature` is the only opt-in — ordinary Claude Code usage in the repo is untouched.
85
+
86
+ ---
87
+
88
+ ## Retrieval is graph-first: graphify → skills → memory
89
+
90
+ `retrieval-nudge.py` redirects broad `Grep`/`Glob` toward a dependency-aware
91
+ `graphify query`. Narrow, file-scoped searches pass through. If no graph exists it
92
+ routes to `/graphify .` (which self-installs and builds one). It blocks a session
93
+ at most **once**, so it steers the first broad search but can never wedge a subagent.
94
+
95
+ ---
96
+
97
+ ## The human gate — summoned by evidence, before work goes wrong
98
+
99
+ Four conditions freeze automation until a person resolves them
100
+ (`requirements-gate.py`, and `approve-plan.py` refuses to sign a failing plan):
101
+
102
+ | Condition | Detected as |
103
+ |---|---|
104
+ | Ambiguous / unorganized intention | `discovery.md` missing Intent & Scope, or unresolved markers (TBD / ??? / "clarify") |
105
+ | Vague / unstructured requirements | required sections absent, or no structured acceptance criteria (AC1, AC2…) |
106
+ | Wrong flow | implementation dispatched before its phase artifacts exist (discovery → context-pack → plan → task-graph) |
107
+ | Diverted from user requirements | an acceptance criterion covered by no task (under-delivery), or a task citing an AC absent from discovery (scope creep) |
108
+
109
+ A block is not a retry — the message says *re-enter interview mode*. `--force` on
110
+ `approve-plan.py` overrides but is recorded in `approvals.json` as a logged decision.
111
+
112
+ ---
113
+
114
+ ## Version Controller — dependency, code, and development versioning
115
+
116
+ A feature does not commit until it is versioned (`version-tracker.py` +
117
+ `version-gate.py` + [`version.py`](template/.claude/scripts/version.py)):
118
+
119
+ - **Dependency** — the pre-feature dependency set is baselined at approval; every
120
+ manifest edit (`package.json`, `requirements.txt`, `pyproject.toml`, `go.mod`,
121
+ `Cargo.toml`) is diffed and classified major/minor/patch into
122
+ `dependencies.json`. A **major** bump must be acknowledged after review.
123
+ - **Code** — `version.py bump` derives a semver bump from the plan's
124
+ `VERSION-IMPACT`, updates `VERSION`, and writes a `CHANGELOG.md` entry.
125
+ - **Development** — `version.py snapshot` hashes the spec artifacts into
126
+ `history.json` at each milestone (`approved` → `integrated` → …) so a feature's
127
+ evolution is auditable, not overwritten.
128
+
129
+ ```bash
130
+ python .claude/scripts/version.py status <feature>
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Autonomous workflow — the outer loop
136
+
137
+ The driver processes a **feature queue**, running each in a fresh headless
138
+ `claude -p` session to one of three terminal states read back from `state.json`.
139
+ Escalations **park** (with a question for a human) instead of stalling the queue;
140
+ a parked feature resumes from artifacts in a brand-new session — recover-from-
141
+ compaction and resume-from-parking are the same mechanism.
142
+
143
+ ```bash
144
+ agent-harness queue add "add order cancellation with refunds"
145
+ agent-harness queue add "nightly CSV export" --auto-approve # headless self-approve
146
+ agent-harness loop # drive the queue to terminal states
147
+ agent-harness queue list # done / parked (+ the questions)
148
+ # ...human appends the answer to specs/<f>/discovery.md...
149
+ agent-harness requeue <feature-id> # resume in a fresh session
150
+ agent-harness metrics # per-feature burndown + tokens-per-passing-task
151
+ ```
152
+
153
+ The human gate (interview + plan approval) is deliberate — ambiguity is routed to
154
+ people, not retried into confident guesses. `--auto-approve` is a **logged**
155
+ opt-out for trusted/CI features; every downstream gate still enforces. Token
156
+ burndown is fed from each session's real usage into the circuit breaker (halts at
157
+ 100% of the approved budget). Dashboards: [docs/langfuse-dashboards.md](docs/langfuse-dashboards.md).
158
+
159
+ ---
160
+
161
+ ## The artifact chain
162
+
163
+ The pipeline's real data structure is `specs/<feature>/` — agents are transient,
164
+ artifacts are the pipeline:
165
+
166
+ ```
167
+ specs/<feature>/
168
+ ├── discovery.md # interview output; AC1, AC2… ; appended on escalation
169
+ ├── context-pack.md # ~2k-token brief, sectioned per work area
170
+ ├── plan.md # tasks + footprints + satisfies: + VERSION-IMPACT + est_tokens
171
+ ├── contracts/ # interface agreements, written before any specialist runs
172
+ ├── task-graph.json # computed schedule (extract-deps.py)
173
+ ├── approvals.json # the signed autonomy boundary (plan hash + budget)
174
+ ├── state.json # per-task status, attempts, phase, breaker health (hook-written)
175
+ ├── review.<task>.<n>.json # reviewer verdicts, one per attempt
176
+ ├── dependencies.json # dependency deltas vs baseline (+ .baseline.json)
177
+ ├── version.json # code semver bump for this feature
178
+ ├── history.json # development-version snapshots per milestone
179
+ ├── divergence.json # footprint violations + token burndown
180
+ └── pipeline-context.md # PreCompact snapshot (load-bearing minimum)
181
+ specs/_queue.json · _answers.json · _active.json # the loop driver's ledgers
182
+ ```
183
+
184
+ ---
185
+
186
+ ## What gets installed
187
+
188
+ ```
189
+ .claude/
190
+ ├── settings.json # hook registrations (merged)
191
+ ├── agents/ # context-researcher, planner, specialist-{db,backend,frontend}, reviewer
192
+ ├── skills/ # discovery-interview, contract-writing, review-taxonomy
193
+ ├── commands/feature.md # /feature — pipeline entry point
194
+ ├── hooks/ # 12 hooks + harness_lib, loop_lib, req_lib, version_lib
195
+ └── scripts/ # extract-deps.py, approve-plan.py, changeset.py, version.py
196
+ CLAUDE.md # merged harness block
197
+ harness.config.json # every knob (below)
198
+ ```
199
+
200
+ ---
201
+
202
+ ## Configuration — `harness.config.json`
203
+
204
+ ```jsonc
205
+ {
206
+ "graphify": { "binary": "graphify", "index_path": "graphify-out/" },
207
+ "retries": { "mechanical": 2, "contract": 1, "ambiguity": 0, "security": 0 },
208
+ "dependency_depth": 1,
209
+ "retrieval_nudge": { "mode": "block", "auto_init": true },
210
+ "budgets": { "context_pack": 2000, "review_findings": 500, "dispatch_prompt_max": 8000 },
211
+ "circuit_breaker": { "footprint_violations": { "warn": 1, "halt": 2 },
212
+ "max_escalations_per_feature": 2, "burndown": { "warn_pct": 80, "halt_pct": 100 } },
213
+ "requirements": { "enabled": true, "min_acceptance_criteria": 1, "require_coverage": true },
214
+ "versioning": { "enabled": true, "require_major_dep_ack": true, "require_code_bump": true, "require_dev_history": true },
215
+ "loop": { "queue": "specs/_queue.json", "answer_queue": "specs/_answers.json", "auto_approve": false },
216
+ "observability": { "provider": "langfuse", "enabled": true, "host": "env:LANGFUSE_HOST",
217
+ "public_key": "env:LANGFUSE_PUBLIC_KEY", "secret_key": "env:LANGFUSE_SECRET_KEY" },
218
+ "gates": { "commit_requires_integration_review": true, "commit_requires_version": true }
219
+ }
220
+ ```
221
+
222
+ Secrets are `env:` references only — never literal keys on disk. Any gate is a
223
+ config toggle; bypasses are logged decisions, not silent workarounds.
224
+
225
+ ---
226
+
227
+ ## Observability & the flywheel
228
+
229
+ `trace-emitter.py` streams one Langfuse trace per session (trace id = `session_id`),
230
+ with subagent spans, tool child-spans, and review verdicts as scores. Every failed
231
+ review and escalation lands in a regression dataset labeled with `failure_class`.
232
+ If Langfuse is unreachable it falls back to `.claude/traces/` — observability never
233
+ becomes an availability dependency. Offline view: `agent-harness metrics`.
234
+
235
+ ## Standalone tool
236
+
237
+ Try the deterministic dependency extractor with zero agents:
238
+
239
+ ```bash
240
+ python .claude/scripts/extract-deps.py --files src/auth/ # "what does this change touch?"
241
+ ```
242
+
243
+ ## Testing
244
+
245
+ ```bash
246
+ npm test # node tests/run.js — 84 checks across extract-deps determinism,
247
+ # every hook (stdin fixtures), the loop driver (mock runner),
248
+ # metrics, the Version Controller, and installer byte-identity
249
+ ```
250
+
251
+ ## Requirements
252
+
253
+ - **Node ≥ 16** (installer + loop driver; zero runtime dependencies)
254
+ - **Python 3.8+** on PATH (`python`, `python3`, or `py`) for hooks/scripts
255
+ - **graphify** for graph-first retrieval — `/graphify` self-installs it on first run
256
+ - **Claude Code** (for the `claude -p` headless loop) and, optionally, **Langfuse**
257
+
258
+ ## Reversibility
259
+
260
+ `uninstall` removes the `_harness` hooks, the harness files, and the delimited
261
+ `CLAUDE.md` block, and leaves `specs/`. A pre-populated `.claude/` returns
262
+ byte-identical. Reversibility is an adoption feature, not an afterthought.
263
+
264
+ ## License
265
+
266
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Loop Engineering harness installer.
4
+ // agent-harness init [target] [--yes] [--python <cmd>]
5
+ // agent-harness detect [target]
6
+ // agent-harness uninstall [target] [--yes]
7
+
8
+ const path = require('path');
9
+ const readline = require('readline');
10
+ const { detect, formatReport } = require('../src/detect');
11
+ const { install } = require('../src/install');
12
+ const { uninstall } = require('../src/uninstall');
13
+ const loop = require('../src/loop');
14
+ const { computeMetrics, formatMetrics } = require('../src/metrics');
15
+
16
+ function parseArgs(argv) {
17
+ const out = { _: [], yes: false, python: null, dryRun: false, once: false, autoApprove: false };
18
+ for (let i = 0; i < argv.length; i++) {
19
+ const a = argv[i];
20
+ if (a === '--yes' || a === '-y') out.yes = true;
21
+ else if (a === '--dry-run') out.dryRun = true;
22
+ else if (a === '--once') out.once = true;
23
+ else if (a === '--auto-approve') out.autoApprove = true;
24
+ else if (a === '--python') out.python = argv[++i];
25
+ else if (a.startsWith('--python=')) out.python = a.slice(9);
26
+ else out._.push(a);
27
+ }
28
+ return out;
29
+ }
30
+
31
+ function confirm(question) {
32
+ return new Promise((resolve) => {
33
+ if (!process.stdin.isTTY) return resolve(false);
34
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
35
+ rl.question(question, (ans) => { rl.close(); resolve(/^y(es)?$/i.test(ans.trim())); });
36
+ });
37
+ }
38
+
39
+ async function main() {
40
+ const args = parseArgs(process.argv.slice(2));
41
+ const cmd = args._[0] || 'help';
42
+ const target = path.resolve(args._[1] || '.');
43
+
44
+ if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
45
+ console.log([
46
+ 'Loop Engineering — agent harness for brownfield Claude Code repos',
47
+ '',
48
+ 'Usage:',
49
+ ' agent-harness init [target] Install into a repo (prints a plan first)',
50
+ ' agent-harness detect [target] Show the install plan, write nothing',
51
+ ' agent-harness uninstall [target] Remove the harness (keeps specs/)',
52
+ '',
53
+ ' agent-harness queue add "<desc>" Add a feature to the work queue',
54
+ ' agent-harness queue list Show the queue and parked features',
55
+ ' agent-harness loop Run the autonomous driver over the queue',
56
+ ' agent-harness requeue <id> Re-queue a parked feature after answering',
57
+ ' agent-harness metrics Per-feature burndown + tokens-per-task',
58
+ '',
59
+ 'Flags: --yes | -y skip confirmation --python <cmd> force interpreter',
60
+ ' --once one feature then stop --auto-approve headless self-approval',
61
+ ' --dry-run plan the loop, run nothing',
62
+ ].join('\n'));
63
+ return 0;
64
+ }
65
+
66
+ if (cmd === 'queue') {
67
+ const sub = args._[1];
68
+ const root = path.resolve(args._[2] || '.');
69
+ if (sub === 'add') {
70
+ const desc = args._[2];
71
+ if (!desc) { console.error('usage: agent-harness queue add "<description>"'); return 1; }
72
+ const id = loop.addToQueue(process.cwd(), desc, { autoApprove: args.autoApprove, now: new Date().toISOString() });
73
+ console.log(`queued '${id}'`);
74
+ return 0;
75
+ }
76
+ if (sub === 'list' || !sub) {
77
+ const q = loop.loadQueue(root);
78
+ const ans = require('fs').existsSync(loop.answersPath(root))
79
+ ? JSON.parse(require('fs').readFileSync(loop.answersPath(root), 'utf8')) : { parked: [] };
80
+ console.log('Queue:');
81
+ for (const f of q.features) console.log(` [${f.status}] ${f.id} — ${f.description}`);
82
+ if (ans.parked && ans.parked.length) {
83
+ console.log('\nParked (need a human):');
84
+ for (const p of ans.parked) console.log(` ${p.id} (${p.terminal}): ${p.question}`);
85
+ }
86
+ return 0;
87
+ }
88
+ console.error(`unknown: queue ${sub}`); return 1;
89
+ }
90
+
91
+ if (cmd === 'requeue') {
92
+ loop.requeue(process.cwd(), args._[1], new Date().toISOString());
93
+ console.log(`re-queued '${args._[1]}'`);
94
+ return 0;
95
+ }
96
+
97
+ if (cmd === 'loop') {
98
+ const root = path.resolve(args._[1] || '.');
99
+ if (args.dryRun) {
100
+ const q = loop.loadQueue(root);
101
+ const pending = q.features.filter((f) => f.status === 'pending');
102
+ console.log(`dry-run: ${pending.length} pending feature(s): ${pending.map((f) => f.id).join(', ') || '(none)'}`);
103
+ return 0;
104
+ }
105
+ const summary = await loop.processQueue(root, {
106
+ once: args.once, autoApprove: args.autoApprove, log: (m) => console.log(m),
107
+ });
108
+ console.log(`\nDone. committed: ${summary.committed.length}, ` +
109
+ `escalated: ${summary.escalated.length}, needs_approval: ${summary.needs_approval.length}, ` +
110
+ `incomplete: ${summary.incomplete.length}`);
111
+ if (summary.escalated.length || summary.needs_approval.length) {
112
+ console.log('Parked features await a human — see: agent-harness queue list');
113
+ }
114
+ return 0;
115
+ }
116
+
117
+ if (cmd === 'metrics') {
118
+ console.log(formatMetrics(computeMetrics(path.resolve(args._[1] || '.'))));
119
+ return 0;
120
+ }
121
+
122
+ if (cmd === 'detect') {
123
+ console.log(formatReport(detect(target, args.python)));
124
+ return 0;
125
+ }
126
+
127
+ if (cmd === 'init' || cmd === 'install') {
128
+ const report = detect(target, args.python);
129
+ console.log(formatReport(report));
130
+ console.log('');
131
+ if (!report.settings.valid) { console.error('Aborted: fix settings.json first.'); return 1; }
132
+ if (!args.yes) {
133
+ const ok = await confirm('Proceed with install? [y/N] ');
134
+ if (!ok) {
135
+ console.log('Aborted. (Re-run with --yes for non-interactive install.)');
136
+ return process.stdin.isTTY ? 0 : 1;
137
+ }
138
+ }
139
+ const { manifest } = install(target, { python: args.python, now: new Date().toISOString() });
140
+ console.log(`Installed. CLAUDE.md: ${manifest.claudeMd}; ` +
141
+ `${manifest.filesAdded.length} files added` +
142
+ (manifest.filesRenamed.length ? `, ${manifest.filesRenamed.length} collision(s) renamed to *.harness.*` : '') +
143
+ `. Next: /feature <description>`);
144
+ return 0;
145
+ }
146
+
147
+ if (cmd === 'uninstall') {
148
+ if (!args.yes) {
149
+ const ok = await confirm(`Uninstall the harness from ${target}? (specs/ is kept) [y/N] `);
150
+ if (!ok) { console.log('Aborted.'); return process.stdin.isTTY ? 0 : 1; }
151
+ }
152
+ const res = uninstall(target);
153
+ console.log(`Uninstalled. Removed ${res.removed.length} files; ` +
154
+ `settings: ${res.settings}; CLAUDE.md: ${res.claudeMd}` +
155
+ (res.usedManifest ? '' : ' (no manifest — heuristic uninstall)') + '. specs/ left in place.');
156
+ return 0;
157
+ }
158
+
159
+ console.error(`Unknown command: ${cmd}. Try: agent-harness help`);
160
+ return 1;
161
+ }
162
+
163
+ main().then((code) => process.exit(code || 0)).catch((err) => {
164
+ console.error('Error: ' + err.message);
165
+ process.exit(1);
166
+ });
@@ -0,0 +1,39 @@
1
+ # Langfuse Dashboards (Phase L5)
2
+
3
+ `trace-emitter.py` streams one trace per session (trace id = `session_id`), with
4
+ subagent spans, tool child-spans, and review verdicts as scores. These three
5
+ dashboards turn that stream into the flywheel's instrument panel. Build them in
6
+ the Langfuse UI (Dashboards → New) over your project; the metric each needs is
7
+ noted. The same numbers are available offline via `agent-harness metrics`.
8
+
9
+ ## 1. Per-feature burndown
10
+ **Question:** is a feature burning toward or through its approved budget?
11
+ - **Source:** score/observation `tokens_spent` accumulated per feature vs the
12
+ `budget` recorded in `approvals.json` (the driver writes cumulative tokens into
13
+ `divergence.json`; trace metadata carries `feature`).
14
+ - **Chart:** line — cumulative tokens over session time, with a horizontal marker
15
+ at 80% (warn) and 100% (halt) of budget. A feature crossing 100% is exactly
16
+ what trips the circuit breaker's burndown signal.
17
+
18
+ ## 2. Tokens-per-passing-task, by specialist
19
+ **Question:** which compression stage is leaking? (Rule 5's leak detector.)
20
+ - **Source:** per subagent span, `output_tokens` ÷ passing tasks for that
21
+ specialist type (`specialist-backend`, `-frontend`, `-database`).
22
+ - **Chart:** time series, one line per specialist type. A steady creep on one
23
+ line localizes the leak to that specialist's retrieval/context slice — the
24
+ trace then shows which span carries the extra tokens.
25
+
26
+ ## 3. Estimate-vs-actual, by task type
27
+ **Question:** is the planner's cost estimate trustworthy?
28
+ - **Source:** planner-emitted per-task token estimate (in the approval package)
29
+ vs actual tokens for that task's spans.
30
+ - **Chart:** scatter — estimate (x) vs actual (y), colored by failure class. Points
31
+ far above the diagonal are underestimated task types; feed that back into the
32
+ planner's estimates and the config `budgets.per_task_default`.
33
+
34
+ ## Regression dataset (the flywheel closes here)
35
+ Every `"status": "fail"` review and every escalation lands as a dataset item
36
+ labeled with `failure_class`, attempts, and resolution. Before merging any change
37
+ to a skill, agent prompt, or hook, replay the dataset and compare scores — the
38
+ reviewer agent is the native evaluation producer, so "evaluate" never waits on
39
+ human labeling.
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "loop-engineering-harness",
3
+ "version": "1.0.0",
4
+ "description": "Loop Engineering — a reversible, observable multi-agent Claude Code harness for brownfield codebases. Policy lives in exit codes and artifacts on disk, not in prompts.",
5
+ "keywords": [
6
+ "claude-code",
7
+ "loop-engineering-harness",
8
+ "context-engineering",
9
+ "graphify",
10
+ "langfuse",
11
+ "brownfield"
12
+ ],
13
+ "license": "MIT",
14
+ "type": "commonjs",
15
+ "bin": {
16
+ "agent-harness": "bin/cli.js",
17
+ "loop-engineering": "bin/cli.js"
18
+ },
19
+ "files": [
20
+ "bin/",
21
+ "src/",
22
+ "template/",
23
+ "docs/",
24
+ "README.md"
25
+ ],
26
+ "scripts": {
27
+ "test": "node tests/run.js",
28
+ "install-here": "node bin/cli.js init .",
29
+ "uninstall-here": "node bin/cli.js uninstall ."
30
+ },
31
+ "engines": {
32
+ "node": ">=16"
33
+ }
34
+ }
package/src/detect.js ADDED
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+ // Builds the pre-flight detection report: what exists, what will be added,
3
+ // merged, or renamed. Pure inspection — writes nothing.
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const m = require('./merge');
8
+
9
+ function templateDir() { return path.join(__dirname, '..', 'template'); }
10
+
11
+ // The set of files the harness ships into the adopter's .claude/ tree.
12
+ function planClaudeFiles(target) {
13
+ const src = path.join(templateDir(), '.claude');
14
+ return m.walk(src).map((abs) => {
15
+ const rel = path.relative(templateDir(), abs); // e.g. .claude/hooks/x.py
16
+ return { src: abs, rel, dest: path.join(target, rel) };
17
+ });
18
+ }
19
+
20
+ function classify(dest, src) {
21
+ if (!fs.existsSync(dest)) return { status: 'add' };
22
+ const a = fs.readFileSync(dest), b = fs.readFileSync(src);
23
+ if (a.equals(b)) return { status: 'identical' };
24
+ const ext = path.extname(dest);
25
+ return { status: 'collision', renamedTo: dest.slice(0, -ext.length || undefined) + '.harness' + ext };
26
+ }
27
+
28
+ function detect(target, python) {
29
+ const report = { target, python: python || m.detectPython(), files: [] };
30
+
31
+ for (const f of planClaudeFiles(target)) {
32
+ report.files.push(Object.assign({ rel: f.rel.replace(/\\/g, '/') }, classify(f.dest, f.src)));
33
+ }
34
+
35
+ const cfgDest = path.join(target, 'harness.config.json');
36
+ report.config = { exists: fs.existsSync(cfgDest), action: fs.existsSync(cfgDest) ? 'keep' : 'add' };
37
+
38
+ const claudeDest = path.join(target, 'CLAUDE.md');
39
+ const claudeRaw = m.read(claudeDest);
40
+ report.claudeMd = {
41
+ exists: claudeRaw !== null,
42
+ action: claudeRaw === null ? 'create'
43
+ : claudeRaw.includes(m.BEGIN) ? 'upgrade-block' : 'append-block',
44
+ };
45
+
46
+ const setDest = path.join(target, '.claude', 'settings.json');
47
+ const setRaw = m.read(setDest);
48
+ let valid = true;
49
+ if (setRaw !== null && setRaw.trim() !== '') { try { JSON.parse(setRaw); } catch (_) { valid = false; } }
50
+ report.settings = { exists: setRaw !== null, valid, action: 'merge-hooks' };
51
+
52
+ return report;
53
+ }
54
+
55
+ function formatReport(r) {
56
+ const L = [];
57
+ L.push('Loop Engineering — installation plan');
58
+ L.push(' target: ' + r.target);
59
+ L.push(' python: ' + r.python);
60
+ L.push('');
61
+ const adds = r.files.filter((f) => f.status === 'add');
62
+ const same = r.files.filter((f) => f.status === 'identical');
63
+ const cols = r.files.filter((f) => f.status === 'collision');
64
+ L.push(` files to add: ${adds.length}`);
65
+ if (same.length) L.push(` already present: ${same.length} (identical, skipped)`);
66
+ for (const c of cols) L.push(` ! COLLISION: ${c.rel} -> ${path.basename(c.renamedTo)} (yours kept)`);
67
+ L.push('');
68
+ L.push(` harness.config.json: ${r.config.action}`);
69
+ L.push(` CLAUDE.md: ${r.claudeMd.action}`);
70
+ if (!r.settings.valid) {
71
+ L.push(' .claude/settings.json: INVALID JSON — install will abort. Fix it first.');
72
+ } else {
73
+ L.push(` .claude/settings.json: ${r.settings.action} (existing hooks preserved, ` +
74
+ `harness hooks appended with _harness markers)`);
75
+ }
76
+ return L.join('\n');
77
+ }
78
+
79
+ module.exports = { detect, formatReport, planClaudeFiles, templateDir };
package/src/install.js ADDED
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+ // Applies the plan from detect.js. Writes a manifest so uninstall is exact.
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const m = require('./merge');
7
+ const { detect, planClaudeFiles, templateDir } = require('./detect');
8
+
9
+ const MANIFEST_REL = path.join('.claude', '.harness', 'manifest.json');
10
+
11
+ function install(target, opts) {
12
+ opts = opts || {};
13
+ target = path.resolve(target);
14
+ const python = opts.python || m.detectPython();
15
+ const report = detect(target, python);
16
+
17
+ if (!report.settings.valid) {
18
+ throw new Error('.claude/settings.json is not valid JSON — aborting (no files written).');
19
+ }
20
+
21
+ const manifest = {
22
+ version: '1.0', python, installedAt: opts.now || null,
23
+ filesAdded: [], filesRenamed: [], configWritten: false,
24
+ claudeMd: null, settingsEvents: [],
25
+ };
26
+
27
+ // 1) .claude tree — collision-safe copy
28
+ for (const f of planClaudeFiles(target)) {
29
+ const res = m.copyFileSafe(f.src, f.dest);
30
+ const rel = path.relative(target, res.dest).replace(/\\/g, '/');
31
+ if (res.status === 'added') manifest.filesAdded.push(rel);
32
+ else if (res.status === 'renamed') {
33
+ manifest.filesRenamed.push(path.relative(target, res.renamedTo).replace(/\\/g, '/'));
34
+ }
35
+ }
36
+
37
+ // 2) harness.config.json — add only if absent (never clobber adopter's tuning)
38
+ const cfgDest = path.join(target, 'harness.config.json');
39
+ if (!fs.existsSync(cfgDest)) {
40
+ fs.copyFileSync(path.join(templateDir(), 'harness.config.json'), cfgDest);
41
+ manifest.configWritten = true;
42
+ manifest.filesAdded.push('harness.config.json');
43
+ }
44
+
45
+ // 3) CLAUDE.md — delimited block
46
+ const block = m.read(path.join(templateDir(), 'CLAUDE.harness.md'));
47
+ manifest.claudeMd = m.mergeClaudeMd(path.join(target, 'CLAUDE.md'), block).action;
48
+
49
+ // 4) settings.json — deep-merge hooks
50
+ const payload = JSON.parse(m.read(path.join(templateDir(), 'settings.harness.json')));
51
+ const res = m.mergeSettings(path.join(target, '.claude', 'settings.json'), payload.hooks, python);
52
+ manifest.settingsEvents = res.appended;
53
+
54
+ m.writeJSON(path.join(target, MANIFEST_REL), manifest);
55
+ return { report, manifest };
56
+ }
57
+
58
+ module.exports = { install, MANIFEST_REL };