uni-harness 0.1.1 → 0.2.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.
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env bash
2
2
  # ════════════════════════════════════════════════════════════════
3
3
  # SessionStart context injection (startup|resume|clear|compact)
4
- # (1) If an in-progress checkpoint (progress.json) exists, surface
4
+ # (1) If an in-progress checkpoint (.harness/state/progress.json,
5
+ # legacy: ./progress.json) exists, surface
5
6
  # it so completed steps aren't repeated. Re-injected after
6
7
  # compaction too.
7
8
  # (2) If tool failures have piled up in the last 7 days of logs,
@@ -36,15 +37,21 @@ try:
36
37
  except Exception:
37
38
  pass
38
39
 
39
- # (1) in-progress checkpoint
40
+ # (1) in-progress checkpoint (.harness/state/; legacy installs kept it at the root)
40
41
  try:
41
- with open(os.path.join(proj, "progress.json")) as f:
42
- ckpt = json.load(f)
43
- if ckpt.get("status") not in ("completed", "abandoned"):
42
+ ckpt, where = None, None
43
+ for rel in (".harness/state/progress.json", "progress.json"):
44
+ p = os.path.join(proj, rel)
45
+ if os.path.exists(p):
46
+ with open(p) as f:
47
+ ckpt = json.load(f)
48
+ where = os.path.dirname(rel) or "."
49
+ break
50
+ if ckpt and ckpt.get("status") not in ("completed", "abandoned"):
44
51
  parts.append(
45
52
  "[harness memory] An in-progress checkpoint exists. Before starting "
46
- "work, read progress.json / plan.md / decisions.jsonl, do not repeat "
47
- "completed steps, and resume from the next step.\n"
53
+ f"work, read progress.json / plan.md / decisions.jsonl in {where}/, "
54
+ "do not repeat completed steps, and resume from the next step.\n"
48
55
  f"Checkpoint summary: {json.dumps(ckpt, ensure_ascii=False)[:1500]}")
49
56
  except Exception:
50
57
  pass
@@ -1,17 +1,19 @@
1
1
  ---
2
2
  name: checkpoint
3
- description: Save current work state to plan.md / decisions.jsonl / progress.json checkpoints. Use after completing a meaningful step, before pausing a long task, or when the user invokes /checkpoint.
3
+ description: Save current work state to .harness/state/ checkpoints (plan.md / decisions.jsonl / progress.json). Use after completing a meaningful step, before pausing a long task, or when the user invokes /checkpoint.
4
4
  ---
5
5
 
6
6
  # Checkpoint — Saving Work State
7
7
 
8
- The filesystem is the memory. Maintain these three files at the project root.
9
- The single test: **if this session died right now, could the next session
10
- pick up where it left off?**
8
+ The filesystem is the memory. Maintain these three files in
9
+ **`.harness/state/`** (created by the installer; gitignored by default
10
+ remove the `.gitignore` entry if you want decisions.jsonl in version
11
+ control). The single test: **if this session died right now, could the
12
+ next session pick up where it left off?**
11
13
 
12
14
  ## Procedure
13
15
 
14
- 1. **plan.md** — update if the plan changed. Format:
16
+ 1. **`.harness/state/plan.md`** — update if the plan changed. Format:
15
17
  ```markdown
16
18
  # Plan: [task title]
17
19
  - [x] completed step
@@ -19,13 +21,13 @@ pick up where it left off?**
19
21
  - [ ] later step
20
22
  ```
21
23
 
22
- 2. **decisions.jsonl** — append only *settled decisions* from this step,
24
+ 2. **`.harness/state/decisions.jsonl`** — append only *settled decisions* from this step,
23
25
  one per line (no exploratory reasoning or transient facts):
24
26
  ```json
25
27
  {"ts": "ISO8601", "decision": "what was decided", "why": "one-line rationale", "alternatives_rejected": ["rejected option"]}
26
28
  ```
27
29
 
28
- 3. **progress.json** — rewrite in full:
30
+ 3. **`.harness/state/progress.json`** — rewrite in full:
29
31
  ```json
30
32
  {
31
33
  "task_id": "task identifier",
@@ -40,6 +42,11 @@ pick up where it left off?**
40
42
 
41
43
  ## Rules
42
44
 
45
+ - Legacy layout: if plan.md / progress.json / decisions.jsonl sit at the
46
+ project root (installs before v0.2), propose moving them into
47
+ `.harness/state/` — ask first, since a root plan.md might be the
48
+ project's own document rather than the harness's.
49
+
43
50
  - When the task is fully done, set `status: "completed"`. The SessionStart
44
51
  hook does not inject completed checkpoints — forget this closing step and
45
52
  the next session will read stale state.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: harness-init
3
- description: Scan the repository of a new project to fill in the PROJECT section of CLAUDE.md and .harness/commands.env, and propose initial RULES/ANTI-PATTERNS. Run once right after installing the harness, or whenever CLAUDE.md is still in placeholder state. Verifies detected commands by actually running them before applying.
3
+ description: Scan the repository to fill in the PROJECT section of CLAUDE.md and .harness/commands.env, and propose initial RULES/ANTI-PATTERNS. Run once right after installing the harness, or whenever CLAUDE.md is still in placeholder state. Verifies detected commands by actually running them before applying. If the repo has no project code yet (greenfield), offers to scaffold a minimal project so the sensors start active from the first line of code.
4
4
  ---
5
5
 
6
6
  # Harness Init — Initialize the Harness by Scanning the Repo
@@ -16,9 +16,11 @@ running them.
16
16
  the project. Never let it drive language detection, and never propose the
17
17
  kit's self-test (`harness/tests/test_hooks.sh`) as the project's TEST_CMD:
18
18
  it verifies the harness, not the user's code, and pointing the stop gate
19
- at it would "pass" every turn regardless of what broke. If no project
20
- code exists outside those directories yet, say so and leave the commands
21
- blank until there is something real to verify.
19
+ at it would "pass" every turn regardless of what broke.
20
+ - The scan decides which mode you are in: project code exists → continue
21
+ with Step 2 (and the brownfield section if the project predates the
22
+ harness). Nothing but harness files → switch to the **New Projects
23
+ (greenfield)** section below. Do not stop at "nothing to configure".
22
24
  - Language/version: from manifests (package.json, pyproject.toml, go.mod,
23
25
  Cargo.toml, *.csproj, ...). Prefer versions pinned in lockfiles/config.
24
26
  - BUILD / TEST / LINT candidates: collect from manifest scripts, Makefile,
@@ -46,6 +48,32 @@ running them.
46
48
  truisms ("write good code") are forbidden — only things observable in
47
49
  this repository.
48
50
 
51
+ ## New Projects (greenfield — the harness was installed before any code)
52
+
53
+ If the scan finds no project code at all (no manifest, no sources, no CI),
54
+ the user is starting a project *with* the harness — the whole point is
55
+ that sensors guard the very first code written. Ending with "come back
56
+ later" leaves them off during the riskiest phase. Instead, offer to
57
+ bootstrap:
58
+
59
+ 1. **Ask, don't guess:** what is being built, which language/runtime,
60
+ which framework and test runner (offer the ecosystem defaults, e.g.
61
+ pytest / vitest / go test).
62
+ 2. **Propose a minimal scaffold** and apply it only after approval: the
63
+ manifest (package.json / pyproject.toml / go.mod ...), a source entry
64
+ point, a test setup with **one real passing test**, and a
65
+ linter/formatter config if the ecosystem has a standard one. Announce
66
+ any dependency installs with the reason before running them.
67
+ 3. If it is not a git repository, offer `git init` (the guard hooks and
68
+ future ANTI-PATTERNS mining assume git history).
69
+ 4. Then continue with Step 2 exactly as usual: run the scaffold's
70
+ BUILD/TEST/LINT commands, confirm they pass, and fill CLAUDE.md and
71
+ commands.env with the **verified** commands.
72
+
73
+ The exit criterion for greenfield: sensors ACTIVE against a real (if tiny)
74
+ test suite. If the user declines the scaffold, leave everything blank as
75
+ before and tell them to re-run /harness-init once code exists.
76
+
49
77
  ## Existing Projects (brownfield)
50
78
 
51
79
  - If CLAUDE.md existed before the harness was installed, never replace it.
package/CLAUDE.md CHANGED
@@ -31,7 +31,8 @@
31
31
 
32
32
  ## Work Loop
33
33
 
34
- - For any task with 3+ steps, write the plan to `plan.md` before starting.
34
+ - For any task with 3+ steps, write the plan to `.harness/state/plan.md`
35
+ before starting.
35
36
  - Retry ceiling: at most 3 fix attempts per goal. Beyond that, stop and
36
37
  write an escalation packet.
37
38
  - Escalation packet format (report to the user):
@@ -45,10 +46,11 @@
45
46
 
46
47
  ## Checkpoints
47
48
 
48
- - At session start, if `progress.json` (checkpoint) exists, read it first
49
- and do not repeat completed steps. (The SessionStart hook will remind you.)
49
+ - At session start, if `.harness/state/progress.json` (checkpoint) exists,
50
+ read it first and do not repeat completed steps. (The SessionStart hook
51
+ will remind you.)
50
52
  - After completing each meaningful step, run `/checkpoint` to update
51
- plan.md / decisions.jsonl / progress.json.
53
+ plan.md / decisions.jsonl / progress.json in `.harness/state/`.
52
54
  - Keep transient facts and exploratory reasoning out of checkpoints. Record
53
55
  only settled decisions in decisions.jsonl.
54
56
 
package/README.md CHANGED
@@ -56,7 +56,6 @@ anything you've customized is skipped (listed, with `--force` to override).
56
56
  | `.claude/hooks/guard-pre-bash.sh` | Blocks destructive commands and verification bypasses (`--no-verify`) before execution |
57
57
  | `.claude/hooks/session-start.sh` | Re-injects in-progress checkpoints at session start/resume/compaction; nudges `/ratchet` when failures pile up |
58
58
  | `.claude/hooks/observe-log.sh` | Logs every tool call as JSONL + tripwires (same failure 3x, call surge) |
59
- | `.claude/agents/TEMPLATE.md` | Custom agent template (inactive until you uncomment `name`; example: fresh-context code reviewer) |
60
59
  | `harness/harness_report.py` | Health scorecard from the logs |
61
60
  | `harness/tests/` | Self-tests for the hooks and the installer |
62
61
  | `bin/cli.js` | The installer (init / update / doctor / uninstall) |
@@ -66,7 +65,7 @@ Skills:
66
65
  | Skill | Role |
67
66
  |---|---|
68
67
  | `/harness-init` | Scan the repo → fill PROJECT section & commands.env (once, at install) |
69
- | `/checkpoint` | Save state to plan.md / decisions.jsonl / progress.json |
68
+ | `/checkpoint` | Save state to `.harness/state/` (plan.md / decisions.jsonl / progress.json) |
70
69
  | `/ratchet [mistake]` | Reproduce → classify → propose rule/sensor/permission → verify (no args: diagnose the logs) |
71
70
  | `/guide-audit` | Audit CLAUDE.md rules — keep / delete / convert-to-sensor (monthly) |
72
71
 
package/bin/cli.js CHANGED
@@ -9,8 +9,8 @@
9
9
  * npx uni-harness uninstall [dir] --yes remove machinery files
10
10
  *
11
11
  * File boundary (the core contract of this installer):
12
- * machinery (managed) — .claude/hooks/, .claude/skills/, .claude/agents/,
13
- * harness/ → init installs them, update refreshes
12
+ * machinery (managed) — .claude/hooks/, .claude/skills/, harness/
13
+ * → init installs them, update refreshes
14
14
  * them. Hashes are recorded in
15
15
  * .harness/kit-manifest.json so files the user has
16
16
  * modified are NEVER overwritten (unless --force).
@@ -26,7 +26,7 @@ const crypto = require('crypto');
26
26
  const { spawnSync } = require('child_process');
27
27
 
28
28
  const PKG_ROOT = path.resolve(__dirname, '..');
29
- const MACHINERY_DIRS = ['.claude/hooks', '.claude/skills', '.claude/agents', 'harness'];
29
+ const MACHINERY_DIRS = ['.claude/hooks', '.claude/skills', 'harness'];
30
30
  // Files that live in these dirs but belong to the kit repository only.
31
31
  // test_installer.sh tests bin/cli.js, which is never installed into target
32
32
  // projects — shipping it there guarantees a failing test suite.
@@ -103,6 +103,24 @@ function runHookTests(target) {
103
103
 
104
104
  const log = (s) => process.stdout.write(s + '\n');
105
105
 
106
+ // runtime dirs: logs (observability) and state (checkpoints — plan.md,
107
+ // progress.json, decisions.jsonl). Both gitignored by default; users who
108
+ // want decisions.jsonl in version control can drop the state entry.
109
+ // Called by init AND update so upgrades pick up newly added dirs/entries.
110
+ function ensureRuntimeDirs(target) {
111
+ fs.mkdirSync(path.join(target, '.harness/logs'), { recursive: true });
112
+ fs.mkdirSync(path.join(target, '.harness/state'), { recursive: true });
113
+ const gi = path.join(target, '.gitignore');
114
+ const giBody = fs.existsSync(gi) ? fs.readFileSync(gi, 'utf8') : '';
115
+ const giLines = giBody.split('\n').map((l) => l.trim().replace(/\/$/, ''));
116
+ const giAdd = ['.harness/logs', '.harness/state'].filter((d) => !giLines.includes(d));
117
+ if (giAdd.length) {
118
+ fs.writeFileSync(gi, giBody + (giBody && !giBody.endsWith('\n') ? '\n' : '') +
119
+ giAdd.map((d) => d + '/\n').join(''));
120
+ log(' ~ .gitignore (+' + giAdd.map((d) => d + '/').join(', +') + ')');
121
+ }
122
+ }
123
+
106
124
  // ── settings.json merge (init only) ──────────────────────────────
107
125
  // Hooks: append only the entries whose hook script is not yet registered
108
126
  // for that event. Everything the user already has is left untouched.
@@ -193,13 +211,7 @@ function init(target, force) {
193
211
  }
194
212
  mergeSettings(target);
195
213
 
196
- fs.mkdirSync(path.join(target, '.harness/logs'), { recursive: true });
197
- const gi = path.join(target, '.gitignore');
198
- const giBody = fs.existsSync(gi) ? fs.readFileSync(gi, 'utf8') : '';
199
- if (!giBody.split('\n').some((l) => l.trim().replace(/\/$/, '') === '.harness/logs')) {
200
- fs.writeFileSync(gi, giBody + (giBody && !giBody.endsWith('\n') ? '\n' : '') + '.harness/logs/\n');
201
- log(' ~ .gitignore (+.harness/logs/)');
202
- }
214
+ ensureRuntimeDirs(target);
203
215
 
204
216
  writeManifest(target, files);
205
217
 
@@ -253,14 +265,30 @@ function update(target, force) {
253
265
  const dst = path.join(target, rel);
254
266
  if (fs.existsSync(dst) && sha256(dst) === manifest.files[rel]) {
255
267
  fs.unlinkSync(dst);
268
+ try { fs.rmdirSync(path.dirname(dst)); } catch { /* keep if not empty */ }
256
269
  log(' - ' + rel + ' (removed from kit)');
257
270
  changed++;
258
271
  }
259
272
  }
260
273
 
261
274
  writeManifest(target, files);
275
+ ensureRuntimeDirs(target);
262
276
  log(`\n${changed} file(s) updated (v${manifest.kitVersion} -> v${KIT_VERSION}). ` +
263
277
  'CLAUDE.md / commands.env / settings.json were not touched.');
278
+
279
+ // pre-v0.2 installs kept checkpoints at the project root and their
280
+ // (owned, never-touched) CLAUDE.md still says so — tell, don't touch
281
+ try {
282
+ const cm = fs.readFileSync(path.join(target, 'CLAUDE.md'), 'utf8');
283
+ if (cm.includes('progress.json') && !cm.includes('.harness/state')) {
284
+ log('\nNote: checkpoint files (plan.md / progress.json / decisions.jsonl)');
285
+ log('now live in .harness/state/ instead of the project root. Your');
286
+ log('CLAUDE.md still references the old locations — update its Work Loop');
287
+ log('and Checkpoints sections, and move any existing checkpoint files');
288
+ log('into .harness/state/. Until then the SessionStart hook still reads');
289
+ log('a root progress.json as a fallback.');
290
+ }
291
+ } catch { /* no CLAUDE.md — nothing to migrate */ }
264
292
  if (skipped.length) {
265
293
  log('Skipped files with local modifications (use --force to overwrite):');
266
294
  for (const s of skipped) log(' ! ' + s);
@@ -102,12 +102,18 @@ echo "session-start.sh"
102
102
  fresh_project
103
103
  OUT=$(bash "$HOOKS/session-start.sh")
104
104
  check "silent without checkpoint" "EMPTY" "$OUT"
105
- echo '{"task_id":"t1","status":"in_progress","next_step":"do X"}' > "$TMP/progress.json"
105
+ mkdir -p "$TMP/.harness/state"
106
+ echo '{"task_id":"t1","status":"in_progress","next_step":"do X"}' > "$TMP/.harness/state/progress.json"
106
107
  OUT=$(bash "$HOOKS/session-start.sh")
107
- check "injects in-progress checkpoint" "progress.json" "$OUT"
108
- echo '{"task_id":"t1","status":"completed"}' > "$TMP/progress.json"
108
+ check "injects in-progress checkpoint" ".harness/state" "$OUT"
109
+ echo '{"task_id":"t1","status":"completed"}' > "$TMP/.harness/state/progress.json"
109
110
  OUT=$(bash "$HOOKS/session-start.sh")
110
111
  check "skips completed checkpoint" "EMPTY" "$OUT"
112
+ rm "$TMP/.harness/state/progress.json"
113
+ # legacy layout (pre-v0.2): checkpoint at the project root still read
114
+ echo '{"task_id":"t1","status":"in_progress","next_step":"do X"}' > "$TMP/progress.json"
115
+ OUT=$(bash "$HOOKS/session-start.sh")
116
+ check "reads legacy root checkpoint" "progress.json" "$OUT"
111
117
  rm "$TMP/progress.json"
112
118
  NOW=$(python3 -c 'import datetime; print(datetime.datetime.now().isoformat(timespec="seconds"))')
113
119
  for _ in 1 2 3; do
@@ -38,6 +38,8 @@ check "installs CLAUDE.md" "yes" "$([ -f "$TGT/CLAUDE.md" ] && echo yes)"
38
38
  check "installs settings.json" "yes" "$([ -f "$TGT/.claude/settings.json" ] && echo yes)"
39
39
  check "writes manifest" "yes" "$([ -f "$TGT/.harness/kit-manifest.json" ] && echo yes)"
40
40
  check "gitignores logs" ".harness/logs/" "$(cat "$TGT/.gitignore")"
41
+ check "creates state dir" "yes" "$([ -d "$TGT/.harness/state" ] && echo yes)"
42
+ check "gitignores state" ".harness/state/" "$(cat "$TGT/.gitignore")"
41
43
  check "init runs hook tests" "Hook tests passed" "$OUT"
42
44
  check "prints next step" "/harness-init" "$OUT"
43
45
  # regression: test_installer.sh tests bin/cli.js, which only exists in the
@@ -101,6 +103,21 @@ OUT=$(node "$CLI" update "$TGT" 2>&1)
101
103
  check "update removes stale kit-only file" "removed from kit" "$OUT"
102
104
  check "stale file is gone" "EMPTY" "$([ -f "$TGT/harness/tests/test_installer.sh" ] && echo left)"
103
105
 
106
+ # update on a pre-v0.2 install must also add the new gitignore entry
107
+ sed -i.bak '/.harness\/state/d' "$TGT/.gitignore" && rm -f "$TGT/.gitignore.bak"
108
+ OUT=$(node "$CLI" update "$TGT" 2>&1)
109
+ check "update re-adds state gitignore entry" ".harness/state/" "$(cat "$TGT/.gitignore")"
110
+
111
+ # pre-v0.2 CLAUDE.md pointing checkpoints at the project root -> migration
112
+ # note (told, never touched); the current template must NOT trigger it
113
+ printf '# old project\n- read progress.json first\n' > "$TGT/CLAUDE.md"
114
+ OUT=$(node "$CLI" update "$TGT" 2>&1)
115
+ check "prints checkpoint migration note" "now live in .harness/state/" "$OUT"
116
+ check "migration note does not edit CLAUDE.md" "# old project" "$(cat "$TGT/CLAUDE.md")"
117
+ cp "$ROOT/CLAUDE.md" "$TGT/CLAUDE.md" # restore for the doctor section
118
+ OUT=$(node "$CLI" update "$TGT" 2>&1)
119
+ check "no migration note for current template" "EMPTY" "$(printf '%s' "$OUT" | grep -F 'now live in' || true)"
120
+
104
121
  # ── doctor ───────────────────────────────────────────────────────
105
122
  echo "doctor"
106
123
  OUT=$(node "$CLI" doctor "$TGT" 2>&1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uni-harness",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Agent harness kit for Claude Code — installs verification sensors, destructive-command guards, checkpoint recovery, observability logs, and ratchet skills into your project",
5
5
  "bin": {
6
6
  "uni-harness": "bin/cli.js"
@@ -1,64 +0,0 @@
1
- ---
2
- # ── Custom agent template (this file itself is inactive) ────────
3
- # Claude Code treats a file without a `name` in its frontmatter as
4
- # documentation and ignores it. To create an agent: copy this file →
5
- # uncomment and fill the fields below → replace the body with that
6
- # agent's system prompt.
7
- #
8
- # name: lowercase-hyphen identifier (becomes the call name) [required]
9
- # description: WHEN to delegate to this agent — the main agent
10
- # reads this sentence to decide auto-delegation [required]
11
- # tools: allowed tools (omit to inherit all)
12
- # disallowedTools: explicit denials (take precedence over tools)
13
- # model: sonnet | opus | haiku (omit to inherit)
14
- #
15
- # name: code-reviewer
16
- # description: Reviews code changes in a fresh context, without seeing the implementation process. Use after finishing a feature or fix, before commit/PR. Judges plan compliance and defects, and reports.
17
- # tools: Read, Grep, Glob, Bash
18
- #
19
- # Note: creating the first active agent in agents/ requires one session
20
- # restart. The harness hooks and permissions apply to agent tool calls too.
21
- # ────────────────────────────────────────────────────────────────
22
- ---
23
-
24
- The body below is the system prompt of the example (fresh-context code
25
- reviewer). Replace it entirely when creating a new agent.
26
-
27
- ---
28
-
29
- You are a code reviewer. You see only the result, not the implementer's
30
- reasoning — that is your value. Find what the implementer missed, without
31
- inheriting their assumptions.
32
-
33
- ## Procedure
34
-
35
- 1. Read the full change via `git diff` (or the scope you were given).
36
- 2. If `plan.md` exists, read it and check the change against the plan and
37
- requirements.
38
- 3. Read enough surrounding code to judge each change in context.
39
- 4. If `.harness/commands.env` has a TEST_CMD, run it and check the result.
40
-
41
- ## Judging Criteria (in priority order)
42
-
43
- 1. **Compliance**: implemented differently from the plan/requirements, or
44
- requirements missing
45
- 2. **Defects**: code that produces wrong results or crashes on real input —
46
- report only what you can pair with a concrete failure scenario
47
- 3. **Verification gaps**: changed behavior that no test covers
48
- 4. **Simplification**: duplication replaceable by existing code (only when
49
- confident)
50
-
51
- ## Rules
52
-
53
- - Report only. Never modify code.
54
- - Anchor every finding to `file:line` with evidence. If it's speculation,
55
- say so.
56
- - If there is nothing to report, say so. Never invent findings to fill space.
57
- - No style preferences — that's the linter's job.
58
-
59
- ## Output Format
60
-
61
- - **Verdict**: approvable / needs changes (one-line rationale)
62
- - **Findings**: ordered by severity, each with location, problem, evidence,
63
- failure scenario
64
- - **Scope checked**: files read, verification commands run and their results