@lemoncode/lemony 0.5.0 → 0.6.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.
- package/README.md +17 -16
- package/catalog/VERSION +1 -1
- package/catalog/agents/implementer.md +36 -1
- package/catalog/agents/orchestrator.md +206 -83
- package/catalog/agents/reviewer.md +8 -3
- package/catalog/agents/spinoff.md +3 -2
- package/catalog/agents/ui-design.md +3 -1
- package/catalog/agents/ui-designer.md +6 -3
- package/catalog/commands/pause.md +50 -5
- package/catalog/commands/resume.md +13 -2
- package/catalog/commands/spinoff.md +5 -3
- package/catalog/commands/sync-design-tokens.md +6 -2
- package/catalog/harness.config.schema.json +4 -0
- package/catalog/hooks/init.sh +80 -7
- package/catalog/hooks/lib/live-branch.sh +37 -0
- package/catalog/hooks/lib/merge-pr.sh +17 -6
- package/catalog/hooks/lib/playbook-scan.sh +4 -2
- package/catalog/hooks/session-close.sh +26 -7
- package/catalog/schemas/tier2-events-history.md +33 -2
- package/catalog/schemas/tier2-events.md +30 -22
- package/catalog/skills/build-ui/SKILL.md +5 -2
- package/catalog/skills/design-tool-sync/SKILL.md +48 -7
- package/catalog/skills/grill-ui/SKILL.md +4 -1
- package/catalog/skills/grill-ui/ui-handoff-format.md +2 -1
- package/catalog/skills/mutation-testing/SKILL.md +22 -8
- package/catalog/skills/prd-to-spec/SKILL.md +27 -5
- package/catalog/skills/review-pr/SKILL.md +7 -1
- package/catalog/skills/review-pr/reference.md +3 -2
- package/catalog/templates/claude-code/agents.md.tpl +2 -1
- package/catalog/templates/claude-code/harness.config.yml.tpl +12 -3
- package/dist/cli.mjs +2579 -775
- package/package.json +11 -10
|
@@ -74,7 +74,9 @@ This is opt-in surfacing of the client's own answers, not the harness authoring
|
|
|
74
74
|
## Design-tokens & design-tool on-ramp (offer)
|
|
75
75
|
|
|
76
76
|
`docs/design-tokens.json` and a design-tool connection are **client-owned inputs** — consumed if
|
|
77
|
-
present, never imposed.
|
|
77
|
+
present, never imposed. When `harness.config.yml` sets `design_tokens.file`, the path it names is
|
|
78
|
+
the token file wherever this file says `docs/design-tokens.json`; never create a second file at
|
|
79
|
+
the default. A repo adopting the harness fresh has neither, and silence there is a dead
|
|
78
80
|
end. So when your `grill-ui` interview finds **either absent**, surface it as an opt-in offer (a
|
|
79
81
|
human-facing choice, so it is yours) rather than only an open question:
|
|
80
82
|
|
|
@@ -58,7 +58,8 @@ Judge against your taste and the 11-section contract, then **return findings —
|
|
|
58
58
|
decisions-altitude (not the placeholder template); sections that don't apply are marked
|
|
59
59
|
N/A, not left blank.
|
|
60
60
|
- **Internally consistent.** Dials, layout, components, states and motion agree; §9 sets an
|
|
61
|
-
a11y target; §11 points at `docs/design-tokens.json
|
|
61
|
+
a11y target; §11 points at the token file — `docs/design-tokens.json`, or the path
|
|
62
|
+
`design_tokens.file` names in `harness.config.yml` (no invented tokens); §1 personas are
|
|
62
63
|
coherent and actually shape the decisions.
|
|
63
64
|
- **One verdict.** Return `pass` / `needs-another-pass` with specific findings. The
|
|
64
65
|
Orchestrator resolves them — tighten the handoff, re-ask the human, or record an open
|
|
@@ -82,7 +83,8 @@ Reviewer's own shape — a **mechanical pre-pass**, then **judgment**, then **on
|
|
|
82
83
|
design-aware — that separation is deliberate.
|
|
83
84
|
|
|
84
85
|
**Token sync (on-demand, not a step).** If the project binds a design tool (a
|
|
85
|
-
`com.lemony.design-tool` provider in `docs/design-tokens.json
|
|
86
|
+
`com.lemony.design-tool` provider in `docs/design-tokens.json`, or in the file
|
|
87
|
+
`design_tokens.file` names in `harness.config.yml` when set), check the drift state
|
|
86
88
|
(`lemony status`). When an export is pending **and the tool is connected**, the Orchestrator
|
|
87
89
|
offers to project the change and dispatches you to run the **`design-tool-sync`** skill —
|
|
88
90
|
never sync silently, and skip gracefully if the tool isn't connected. The human's explicit
|
|
@@ -100,7 +102,8 @@ skill (`ui-handoff-format.md`): (1) personas; (2) aesthetic direction — the di
|
|
|
100
102
|
variant, state what matters); the full component anatomy only for a novel or critical
|
|
101
103
|
component.
|
|
102
104
|
- **Text-first, tokens by reference.** Screens travel as text — an inventory, a nav/flow
|
|
103
|
-
map and structural intent; tokens point at `docs/design-tokens.json`,
|
|
105
|
+
map and structural intent; tokens point at the token file (`docs/design-tokens.json`, or
|
|
106
|
+
`design_tokens.file`), never inlined.
|
|
104
107
|
|
|
105
108
|
A `Status:` field marks the handoff `in_progress` (design being defined) vs `completed`.
|
|
106
109
|
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Pause the current session — write a narrative resume + emit session_closed telemetry.
|
|
3
|
-
allowed-tools: Read, Write, Bash(.claude/hooks/session-close.sh --manual), Bash(date -u *)
|
|
3
|
+
allowed-tools: Read, Write, Bash(.claude/hooks/session-close.sh --manual), Bash(.claude/hooks/lib/lemony.sh status --local), Bash(.claude/hooks/lib/lemony.sh checkpoint *), Bash(date -u *), Bash(git diff --stat *), Bash(git ls-files --others *)
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /pause
|
|
7
7
|
|
|
8
|
+
**A checkpoint answer comes first.** When the human answered a pending checkpoint
|
|
9
|
+
with this `/pause`, or just before it (`ok`, `changes`, `ok_downgrade`), resolve it
|
|
10
|
+
before step 1, whatever the auto-commit setting, exactly as the orchestrator does
|
|
11
|
+
at any checkpoint (§Checkpoint contract, items 4–5): the spec check and the
|
|
12
|
+
human-delta revalidation first (a red one blocks the OK), on an OK under
|
|
13
|
+
auto-commit ON the human-delta commit, then the resolved `progress.md` line and the `checkpoint` verb.
|
|
14
|
+
Its commits and emit belong to the checkpoint, not to the pause.
|
|
15
|
+
|
|
8
16
|
A two-step pause:
|
|
9
17
|
|
|
10
18
|
1. **Write the resume narrative** so future-you (or whoever resumes) can pick
|
|
@@ -14,14 +22,19 @@ A two-step pause:
|
|
|
14
22
|
narrative lives; `current-<your-user>.md` only holds session timestamps.
|
|
15
23
|
- Generate a UTC timestamp slug — run `date -u +%Y-%m-%dT%H:%M:%SZ` and use
|
|
16
24
|
it as `<ts>`. Pick a short topic slug from the work in progress.
|
|
25
|
+
- Read the task and branch — run `.claude/hooks/lib/lemony.sh status --local`
|
|
26
|
+
(local reads only, no network call): its `Active task:` and `Branch:` lines
|
|
27
|
+
are read off the live branch (a task branch, the `harness/closeout-<id>`
|
|
28
|
+
record branch, or the branch being rebased while a rebase has HEAD
|
|
29
|
+
detached). If it fails, write `null` and `(unknown)`.
|
|
17
30
|
- Write `.claude/state/sessions/<your-user>/<ts>-<topic>.md` with this
|
|
18
31
|
skeleton (fill the body — what you did, where you are, the next step):
|
|
19
32
|
|
|
20
33
|
```markdown
|
|
21
34
|
---
|
|
22
35
|
session_close_ts: <ts>
|
|
23
|
-
active_task: <
|
|
24
|
-
branch: <
|
|
36
|
+
active_task: <the `Active task:` id, or null when it reads (none)>
|
|
37
|
+
branch: <the `Branch:` name without its parenthesised suffix — (unknown) as printed>
|
|
25
38
|
topic: <topic>
|
|
26
39
|
reason: manual
|
|
27
40
|
auto_close: false
|
|
@@ -29,6 +42,8 @@ A two-step pause:
|
|
|
29
42
|
|
|
30
43
|
# Session — <topic> (<ts>)
|
|
31
44
|
|
|
45
|
+
<auto-commit OFF only — the unstaged-work line, see below>
|
|
46
|
+
|
|
32
47
|
## What I did
|
|
33
48
|
|
|
34
49
|
- …
|
|
@@ -42,9 +57,38 @@ A two-step pause:
|
|
|
42
57
|
- …
|
|
43
58
|
```
|
|
44
59
|
|
|
60
|
+
- **Auto-commit OFF: the note opens with the unstaged work.** When the task
|
|
61
|
+
runs auto-commit OFF (config pins `implementation.auto_commit: off`, or the
|
|
62
|
+
task's `progress.md` carries the `Auto-commit: off` line), run — `':/'`
|
|
63
|
+
scopes both to the whole repository, whatever the shell's directory:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
git diff --stat -- ':/' ':(top,exclude).claude/state'; \
|
|
67
|
+
git ls-files --others --exclude-standard --full-name -- ':/' ':(top,exclude).claude/state'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
and make the first line under the heading say what the output is, which
|
|
71
|
+
depends on where the task stands:
|
|
72
|
+
- **Mid-implementation** (`progress.md` not at `awaiting human checkpoint`):
|
|
73
|
+
the staged index is the last save-point (green, except the red starting
|
|
74
|
+
point of a `(red human delta)` fix-loop) and everything unstaged is
|
|
75
|
+
**unverified by construction** — a hand probe stopped between its mutation
|
|
76
|
+
and its revert looks exactly like work in progress.
|
|
77
|
+
`**Unverified (unstaged since the last save-point):** <files + stat summary>`
|
|
78
|
+
— or `**Unverified:** none` when both print nothing.
|
|
79
|
+
- **At an `awaiting` checkpoint**: every agent save-point was staged at
|
|
80
|
+
presentation, so unstaged work is the human's own edits.
|
|
81
|
+
`**Human delta (unstaged, to revalidate at the checkpoint):** <files + stat summary>`
|
|
82
|
+
— or `**Human delta:** none`.
|
|
83
|
+
|
|
84
|
+
Report only: stage, revert, or fix nothing here (a stage now is the
|
|
85
|
+
mid-experiment `add` the staging protocol forbids). Task state under
|
|
86
|
+
`.claude/state` is left out — it is disk-only by design in this mode.
|
|
87
|
+
|
|
45
88
|
2. **Trigger the session-close hook** so the `session_closed` event lands in
|
|
46
89
|
`events.jsonl` (its `task_id` is derived from the current branch when it is
|
|
47
|
-
a `harness/<id>-<slug>` task branch
|
|
90
|
+
a `harness/<id>-<slug>` or `harness/closeout-<id>` task branch, or is being
|
|
91
|
+
rebased) and `current-<your-user>.md` gets its
|
|
48
92
|
`last_close_ts` stamped:
|
|
49
93
|
|
|
50
94
|
```bash
|
|
@@ -54,7 +98,8 @@ A two-step pause:
|
|
|
54
98
|
The hook prints `git status --porcelain` after emitting — it never
|
|
55
99
|
auto-commits. Decide what (if anything) to stage and commit before leaving.
|
|
56
100
|
Exception — a task running **auto-commit OFF** (orchestrator §Auto-commit
|
|
57
|
-
OFF): do **not** commit; the zero-commit deferral binds `/pause` too
|
|
101
|
+
OFF): do **not** commit; the zero-commit deferral binds `/pause` too (an answered
|
|
102
|
+
checkpoint was already resolved above, by the `checkpoint` verb).
|
|
58
103
|
Leave the staged save-point untouched (it survives the session; staging
|
|
59
104
|
anything now is the mid-experiment `add` the protocol forbids), note the pause
|
|
60
105
|
in `progress.md` — disk-only, per the mode's contract.
|
|
@@ -37,7 +37,13 @@ drop `harness:needs-design` and continue toward spec-ready. When `progress.md` r
|
|
|
37
37
|
exactly there: `awaiting human checkpoint (step N/M)` re-presents that pending
|
|
38
38
|
checkpoint (inspect / run / OK / changes / OK+downgrade; in all-at-once under
|
|
39
39
|
auto-commit OFF the line carries no `(step N/M)` counter — same
|
|
40
|
-
re-entry)
|
|
40
|
+
re-entry). Every answer — `changes` too — resolves in the turn that records it,
|
|
41
|
+
after the human-delta check (orchestrator §Checkpoint contract, item 4), through one
|
|
42
|
+
call, `.claude/hooks/lib/lemony.sh checkpoint <answer> …` (orchestrator
|
|
43
|
+
§Checkpoint contract, item 5): it makes the commits and, step-by-step, the
|
|
44
|
+
`step_completed` emit together (the all-at-once gate omits `--step` and emits
|
|
45
|
+
nothing) — never make them by hand or copy them from `git log`. A
|
|
46
|
+
`fix-loop iteration K`
|
|
41
47
|
line re-enters the per-step implement→review loop at that iteration; an
|
|
42
48
|
`awaiting ledger retry (step N/M, retry 1/1)` line — or its full-pass twin
|
|
43
49
|
`awaiting ledger retry (full pass, retry 1/1)` — re-runs
|
|
@@ -71,7 +77,12 @@ makes same-machine resume lossless). From a **different** machine, anything comm
|
|
|
71
77
|
after the last successful push is unreachable and undetectable (origin is
|
|
72
78
|
self-consistent, just older): resume from the pushed state and tell the human which
|
|
73
79
|
state that is (`progress.md`'s last entry), so a re-run of an already-done review or
|
|
74
|
-
checkpoint is a conscious replay, not a silent assumption.
|
|
80
|
+
checkpoint is a conscious replay, not a silent assumption. Same machine, auto-commit
|
|
81
|
+
OFF, mid-implementation (`progress.md` not at `awaiting human checkpoint`): work found
|
|
82
|
+
**unstaged** in the worktree is **unverified by construction** — never stage or present
|
|
83
|
+
it on sight; the fresh Implementer confirms or redoes it. At an `awaiting` checkpoint,
|
|
84
|
+
unstaged work is the human's own delta instead (authority: the orchestrator §Auto-commit
|
|
85
|
+
OFF). An `in-review` one resumes at the
|
|
75
86
|
**merge gate**: surface the open PR's review comments and run the merge-gate procedure
|
|
76
87
|
(route change-requests back to the Implementer, then offer to post threaded replies —
|
|
77
88
|
authority is the orchestrator). A `closeout-pending` task has **nothing to check out** —
|
|
@@ -16,9 +16,11 @@ code pointer). If empty, ask one short question to capture the symptom, then pro
|
|
|
16
16
|
|
|
17
17
|
## Procedure
|
|
18
18
|
|
|
19
|
-
1. **Identify the parent task** (best-effort): the task in flight.
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
1. **Identify the parent task** (best-effort): the task in flight. Run
|
|
20
|
+
`.claude/hooks/lib/lemony.sh status --local` and read its `Active task:` line — it
|
|
21
|
+
derives the id from the live branch (`harness/<id>-<slug>`, the `harness/closeout-<id>`
|
|
22
|
+
record branch, or the branch being rebased while a rebase has HEAD detached). If it
|
|
23
|
+
reads `(none)`, or the command fails, capture with no parent (a deferred stub).
|
|
22
24
|
|
|
23
25
|
2. **Capture the stub** with the `spinoff` CLI verb via the launcher. In one call it
|
|
24
26
|
opens a `harness:managed` + `harness:status:pending` issue (the level is decided at
|
|
@@ -18,12 +18,16 @@ Designer; follow the steps there.
|
|
|
18
18
|
deletes tool-only variables). Preview the plan, push on confirmation, then record the drift
|
|
19
19
|
baseline only after the push succeeds.
|
|
20
20
|
- _empty_ — check the drift state (`lemony status`) and, if an export is pending and the tool
|
|
21
|
-
is connected, offer `export`; otherwise report the current state.
|
|
21
|
+
is connected, offer `export`; otherwise report the current state. `status` shows no drift
|
|
22
|
+
line both when there is no token file and when the file cannot be read or fails
|
|
23
|
+
validation — tell the two apart with `lemony doctor`'s `design-tool-drift` check.
|
|
22
24
|
|
|
23
25
|
The design tool is a **projection** of the canonical JSON, never a peer source of truth.
|
|
24
26
|
Detect the tool at runtime: read the `com.lemony.design-tool` binding at the root of
|
|
25
27
|
`docs/design-tokens.json`; if no tool is declared, the project is pure-code and there is
|
|
26
28
|
nothing to sync. If the declared tool's MCP server is not connected, skip gracefully with a
|
|
27
|
-
note — the deterministic drift check still works without it.
|
|
29
|
+
note — the deterministic drift check still works without it. When `harness.config.yml` sets
|
|
30
|
+
`design_tokens.file`, the path it names is the token file wherever this command says
|
|
31
|
+
`docs/design-tokens.json`.
|
|
28
32
|
|
|
29
33
|
The human reviews every write (to the JSON and to the tool). Never sync silently.
|
package/catalog/hooks/init.sh
CHANGED
|
@@ -11,6 +11,22 @@
|
|
|
11
11
|
|
|
12
12
|
set -u
|
|
13
13
|
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
|
|
16
|
+
# shellcheck source=lib/live-branch.sh
|
|
17
|
+
if [ -f "$SCRIPT_DIR/lib/live-branch.sh" ] && [ -r "$SCRIPT_DIR/lib/live-branch.sh" ]; then
|
|
18
|
+
source "$SCRIPT_DIR/lib/live-branch.sh"
|
|
19
|
+
LIVE_BRANCH_LIB=1
|
|
20
|
+
else
|
|
21
|
+
# A hand-copied or partial install without the helper (`lemony install`
|
|
22
|
+
# copies all of lib/): read HEAD's branch only — a rebase in progress reads
|
|
23
|
+
# as no branch — and say so once instead of failing on every call.
|
|
24
|
+
live_branch() {
|
|
25
|
+
git -C "$1" symbolic-ref --quiet HEAD 2>/dev/null | sed -n 's#^refs/heads/##p'
|
|
26
|
+
}
|
|
27
|
+
LIVE_BRANCH_LIB=0
|
|
28
|
+
fi
|
|
29
|
+
|
|
14
30
|
# Read stdin only when Claude Code is piping a payload; if a developer runs
|
|
15
31
|
# this hook interactively (no pipe), default to an empty payload so the script
|
|
16
32
|
# doesn't hang on `cat` waiting for input.
|
|
@@ -35,6 +51,9 @@ CONFIG="$REPO_ROOT/harness.config.yml"
|
|
|
35
51
|
|
|
36
52
|
ERRORS=()
|
|
37
53
|
WARNINGS=()
|
|
54
|
+
if [ "$LIVE_BRANCH_LIB" -eq 0 ]; then
|
|
55
|
+
WARNINGS+=(".claude/hooks/lib/live-branch.sh is missing or unreadable — the task branch is read from HEAD only, so a rebase in progress reads as no branch. Run \`lemony repair\` to restore it.")
|
|
56
|
+
fi
|
|
38
57
|
|
|
39
58
|
# ── Block 1 ─ harness.config.yml present and parseable ─────────────────────
|
|
40
59
|
# Pure-bash validation (no yq): presence of the four keys the harness
|
|
@@ -46,8 +65,42 @@ WARNINGS=()
|
|
|
46
65
|
# awk/grep/git (preinstalled) cover the rest.
|
|
47
66
|
CONFIG_VERSION=""
|
|
48
67
|
CONFIG_REPO=""
|
|
49
|
-
|
|
68
|
+
# An install's baseline: a version directory (not a dot entry, not a link) under
|
|
69
|
+
# `.claude/.harness/baseline/` — the CLI's `hasInstallBaseline`. The `baseline/` entry
|
|
70
|
+
# alone is no trace: an ignored file in it keeps it on a branch from before the install.
|
|
71
|
+
has_install_baseline() {
|
|
72
|
+
local base="$REPO_ROOT/.claude/.harness/baseline" dir
|
|
73
|
+
# One it may not list is there all the same: "run install" would clobber edits.
|
|
74
|
+
if [ -d "$base" ] && { [ ! -r "$base" ] || [ ! -x "$base" ]; }; then
|
|
75
|
+
return 0
|
|
76
|
+
fi
|
|
77
|
+
for dir in "$base"/*/; do
|
|
78
|
+
[ -d "$dir" ] && [ ! -L "${dir%/}" ] && return 0
|
|
79
|
+
done
|
|
80
|
+
return 1
|
|
81
|
+
}
|
|
82
|
+
if [ -L "$CONFIG" ] && [ ! -e "$CONFIG" ]; then
|
|
83
|
+
# `-e` follows the link, so a dead one (or a loop, or one behind a directory it
|
|
84
|
+
# may not search) read as "not found" — and `lemony install` refuses to write
|
|
85
|
+
# through a symlink, so "run install" went in a circle. On an installed repo,
|
|
86
|
+
# install after removing it re-scaffolds over the user's edits: restore first.
|
|
87
|
+
ERRORS+=("harness.config.yml at $REPO_ROOT is a symlink that does not resolve (its target is missing or cannot be reached, or it loops). Repoint it at the config file, or replace it with a copy of the config (from git history or a backup); remove it and run \`lemony install\` only in a repo that was never installed.")
|
|
88
|
+
elif [ ! -e "$CONFIG" ] && has_install_baseline; then
|
|
89
|
+
# No config next to an install's baseline: removed from an installed repo, or an
|
|
90
|
+
# install that did not finish (it writes the config last). "Run install" is wrong
|
|
91
|
+
# for the first — with no TTY it keeps the vendor copy of every edited file.
|
|
92
|
+
ERRORS+=("harness.config.yml not found at $REPO_ROOT, but .claude/.harness/baseline exists: the harness was installed here, or an install did not finish. If a \`lemony install\` did not finish here, re-run it; otherwise restore harness.config.yml from git history or a backup (\`lemony install\` would, with no TTY, replace the files you edited with the vendor copy).")
|
|
93
|
+
elif [ ! -e "$CONFIG" ]; then
|
|
50
94
|
ERRORS+=("harness.config.yml not found at $REPO_ROOT. Run \`lemony install\`.")
|
|
95
|
+
elif [ ! -f "$CONFIG" ]; then
|
|
96
|
+
# There, but not a file awk can read: a directory, or a FIFO it would block on
|
|
97
|
+
# until something wrote to it. `-e`/`-f` follow a symlink, so a link to one of
|
|
98
|
+
# these lands here.
|
|
99
|
+
ERRORS+=("harness.config.yml at $REPO_ROOT is not a regular file (a directory, a FIFO, a socket or a device), so it was not read. Replace it with the config file.")
|
|
100
|
+
elif [ ! -r "$CONFIG" ]; then
|
|
101
|
+
# A file awk cannot open failed the key check below, so the boot blamed missing keys
|
|
102
|
+
# under a raw awk error.
|
|
103
|
+
ERRORS+=("harness.config.yml at $REPO_ROOT cannot be read (check its permissions), so it was not read.")
|
|
51
104
|
elif ! awk '
|
|
52
105
|
{ sub(/\r$/, "") }
|
|
53
106
|
/^[^[:space:]#]/ {
|
|
@@ -138,12 +191,18 @@ fi
|
|
|
138
191
|
# ladder, and following `git pull --rebase` with `rebase.autostash` set flattens
|
|
139
192
|
# that index — the documented rollback then reverts to the last commit and the
|
|
140
193
|
# group's whole uncommitted work is gone. So on any other branch: silence.
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
|
|
194
|
+
# Silence too while a rebase has HEAD detached, even a rebase of the default
|
|
195
|
+
# branch: the count would be taken from the half-replayed HEAD, and the advice
|
|
196
|
+
# cannot be followed mid-rebase — hence the attached-HEAD check.
|
|
197
|
+
# `live_branch` (same read as `session-close.sh` / `status`): the full ref
|
|
198
|
+
# with `refs/heads/` stripped — `--short` lengthens to `heads/<name>` when a
|
|
199
|
+
# tag shares the branch's name, failing the `harness/*` test below — or, while
|
|
200
|
+
# a rebase has HEAD detached, the branch being rebased (a rebase of a task
|
|
201
|
+
# branch is not a parked task).
|
|
202
|
+
CURRENT_BRANCH="$(live_branch "$REPO_ROOT")"
|
|
145
203
|
DEFAULT_BRANCH="$(git symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')"
|
|
146
|
-
if [ -n "$DEFAULT_BRANCH" ] && [ "$CURRENT_BRANCH" = "$DEFAULT_BRANCH" ]
|
|
204
|
+
if [ -n "$DEFAULT_BRANCH" ] && [ "$CURRENT_BRANCH" = "$DEFAULT_BRANCH" ] \
|
|
205
|
+
&& git symbolic-ref --quiet HEAD >/dev/null 2>&1; then
|
|
147
206
|
BEHIND="$(git rev-list --count "HEAD..origin/$DEFAULT_BRANCH" 2>/dev/null || echo 0)"
|
|
148
207
|
if [ "${BEHIND:-0}" -gt 0 ]; then
|
|
149
208
|
WARNINGS+=("local branch is $BEHIND commit(s) behind origin/$DEFAULT_BRANCH. Consider \`git pull --rebase\` before starting.")
|
|
@@ -190,6 +249,19 @@ if [ "${#ERRORS[@]}" -eq 0 ] && [ -n "$GIT_USER_EMAIL" ]; then
|
|
|
190
249
|
CURRENT_PATH="$REPO_ROOT/.claude/state/current-$USER_SLUG.md"
|
|
191
250
|
if [ -z "$USER_SLUG" ]; then
|
|
192
251
|
: # path-unsafe slug — pointer write skipped above
|
|
252
|
+
elif [ -L "$REPO_ROOT/.claude" ] || [ -L "$REPO_ROOT/.claude/state" ]; then
|
|
253
|
+
# A linked directory takes the pointer, and every refresh after it, out of the
|
|
254
|
+
# repo — the same write-through the dangling-link branch below refuses.
|
|
255
|
+
WARNINGS+=(".claude or .claude/state is a symlink, so the session start was not recorded (the pointer would be written wherever it points). Replace it with a directory.")
|
|
256
|
+
elif [ -e "$CURRENT_PATH" ] && [ ! -f "$CURRENT_PATH" ]; then
|
|
257
|
+
# There, but not a file: writing to a FIFO blocks until something reads it, and
|
|
258
|
+
# awk reading one blocks until something writes. The pointer is not critical, so
|
|
259
|
+
# the boot goes on without it — a warning, not a blocking error.
|
|
260
|
+
WARNINGS+=(".claude/state/current-$USER_SLUG.md is not a regular file (a directory, a FIFO, a socket or a device), so the session start was not recorded. Remove it; the next session recreates it.")
|
|
261
|
+
elif [ -L "$CURRENT_PATH" ] && [ ! -e "$CURRENT_PATH" ]; then
|
|
262
|
+
# A symlink whose target is gone: `-e`/`-f` follow it and see nothing, and the
|
|
263
|
+
# `cat >` below would create the target wherever the link points.
|
|
264
|
+
WARNINGS+=(".claude/state/current-$USER_SLUG.md is a symlink whose target is missing, so the session start was not recorded (writing through it would create the file it points at). Remove it; the next session recreates it.")
|
|
193
265
|
elif [ ! -f "$CURRENT_PATH" ]; then
|
|
194
266
|
mkdir -p "$REPO_ROOT/.claude/state"
|
|
195
267
|
cat > "$CURRENT_PATH" <<EOF
|
|
@@ -203,7 +275,8 @@ last_close_ts: ""
|
|
|
203
275
|
Per-dev pointer (gitignored). The lifecycle hooks read \`session_start_ts\`
|
|
204
276
|
to compute \`session_active_h\` and reset it on each SessionStart that orients.
|
|
205
277
|
The active task and its branch are not recorded here — \`session-close.sh\`
|
|
206
|
-
derives them from the live branch (\`harness/<id>-<slug>\`
|
|
278
|
+
derives them from the live branch (\`harness/<id>-<slug>\` or
|
|
279
|
+
\`harness/closeout-<id>\`, the rebasing branch mid-rebase) at close time — and
|
|
207
280
|
the narrative resume lives under \`sessions/<user>/\` (written by \`/pause\`).
|
|
208
281
|
EOF
|
|
209
282
|
else
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# The branch a session is working on, read off live git state — sourced by
|
|
3
|
+
# `session-close.sh` (task id of the auto-close record + `session_closed`) and
|
|
4
|
+
# `init.sh` (the parked-branch nudge). The TS twin is `readLiveBranch`
|
|
5
|
+
# (`src/status/live-branch.ts`, `lemony status`); `live-branch.spec.ts` runs
|
|
6
|
+
# both against the same real repositories.
|
|
7
|
+
#
|
|
8
|
+
# `live_branch <repo-root>` prints the branch name (no `refs/heads/`), or
|
|
9
|
+
# nothing when there is none — never a guess:
|
|
10
|
+
#
|
|
11
|
+
# 1. HEAD's full ref with `refs/heads/` stripped. Not `--short`: the short
|
|
12
|
+
# form lengthens to `heads/<name>` when a tag shares the branch's name.
|
|
13
|
+
# 2. When HEAD is detached, git's own record of a rebase in progress —
|
|
14
|
+
# `rebase-merge/head-name` (merge backend, `rebase -i`) or
|
|
15
|
+
# `rebase-apply/head-name` (apply backend) under the git dir. A conflicted
|
|
16
|
+
# or stopped rebase detaches HEAD for its whole duration, and that is
|
|
17
|
+
# exactly when a session is likely to pause. `--absolute-git-dir` resolves
|
|
18
|
+
# inside a linked worktree. `git am` writes no `head-name`, and a rebase
|
|
19
|
+
# of an already-detached HEAD records `detached HEAD` — both stay empty,
|
|
20
|
+
# as does a plain `git checkout --detach` and anything outside a repo.
|
|
21
|
+
|
|
22
|
+
live_branch() {
|
|
23
|
+
local root="$1" branch git_dir head_name
|
|
24
|
+
branch="$(git -C "$root" symbolic-ref --quiet HEAD 2>/dev/null | sed -n 's#^refs/heads/##p')"
|
|
25
|
+
if [ -z "$branch" ]; then
|
|
26
|
+
git_dir="$(git -C "$root" rev-parse --absolute-git-dir 2>/dev/null)"
|
|
27
|
+
if [ -n "$git_dir" ]; then
|
|
28
|
+
for head_name in "$git_dir/rebase-merge/head-name" "$git_dir/rebase-apply/head-name"; do
|
|
29
|
+
if [ -f "$head_name" ]; then
|
|
30
|
+
branch="$(sed -n 's#^refs/heads/##p' "$head_name" 2>/dev/null)"
|
|
31
|
+
break
|
|
32
|
+
fi
|
|
33
|
+
done
|
|
34
|
+
fi
|
|
35
|
+
fi
|
|
36
|
+
printf '%s' "$branch"
|
|
37
|
+
}
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
# APPROVE comment) and verify the PR's current content is
|
|
39
39
|
# what that APPROVE reviewed. Three outcomes: tree equal →
|
|
40
40
|
# merge; tree differs but diff fingerprint equal → the
|
|
41
|
-
# base advanced (update-branch)
|
|
41
|
+
# base advanced (update-branch) and/or only task state
|
|
42
|
+
# under .claude/state changed → merge, informing on
|
|
42
43
|
# stderr; fingerprint differs → exit 40, never merge. The
|
|
43
44
|
# merge call is then pinned with --match-head-commit to
|
|
44
45
|
# the exact head the guard verified, so a push racing the
|
|
@@ -309,12 +310,13 @@ fi
|
|
|
309
310
|
# same recipe as playbook-scan.sh): emit `<key>\t<value>` for the uncommented
|
|
310
311
|
# `checks_timeout_secs` / `allow_no_checks` entries — inline comments stripped
|
|
311
312
|
# (on the block header too) and surrounding quotes removed. Any column-0 line
|
|
312
|
-
# ends the block. Absent/unreadable config → baked defaults.
|
|
313
|
+
# ends the block. Absent/unreadable/non-regular config → baked defaults.
|
|
313
314
|
CONFIG_TIMEOUT=""
|
|
314
315
|
CONFIG_ALLOW_NO_CHECKS=""
|
|
315
316
|
read_merge_config() {
|
|
316
317
|
local config="$ROOT/harness.config.yml"
|
|
317
|
-
|
|
318
|
+
# `-f` as well as `-r`: a FIFO is readable, and awk on it would hang the merge.
|
|
319
|
+
{ [ -f "$config" ] && [ -r "$config" ]; } || return 0
|
|
318
320
|
|
|
319
321
|
local key value
|
|
320
322
|
while IFS=$'\t' read -r key value; do
|
|
@@ -401,7 +403,14 @@ fi
|
|
|
401
403
|
# before merge is standing practice and must not invalidate verdicts: a clean
|
|
402
404
|
# base advance changes the tree but not the PR's own diff (verified
|
|
403
405
|
# empirically, including that a base edit to a file the PR also touched DOES
|
|
404
|
-
# change it — exactly the case that needs fresh eyes).
|
|
406
|
+
# change it — exactly the case that needs fresh eyes). The digest excludes
|
|
407
|
+
# `.claude/state`: the Reviewer writes its evidence ledger and
|
|
408
|
+
# `progress.md` section after posting the APPROVE, by contract, and the
|
|
409
|
+
# Orchestrator keeps writing task state until the merge — none of it is
|
|
410
|
+
# reviewed content, so a commit of it must not read as a stale approve.
|
|
411
|
+
# `:(top,…)` anchors the exclude at the repo root: a bare `:(exclude)` is
|
|
412
|
+
# cwd-relative and silently excludes nothing when the recipe runs from a
|
|
413
|
+
# subdirectory. `--no-abbrev` is
|
|
405
414
|
# load-bearing: --raw abbreviates blob OIDs to a repo-size-dependent width by
|
|
406
415
|
# default, so two machines could digest different bytes for the same diff.
|
|
407
416
|
# The parity of this command with the Reviewer's recipe (reviewer.md) is
|
|
@@ -518,9 +527,11 @@ EOF
|
|
|
518
527
|
fi
|
|
519
528
|
# Canonical fingerprint command — byte-for-byte the Reviewer's recipe
|
|
520
529
|
# (reviewer.md records the APPROVE side with the same pinned flags).
|
|
530
|
+
# --no-literal-pathspecs: under GIT_LITERAL_PATHSPECS=1 the exclude is read
|
|
531
|
+
# as a literal path, the diff comes out empty and every head digests alike.
|
|
521
532
|
cur_fp="$(
|
|
522
533
|
set -o pipefail
|
|
523
|
-
git -C "$ROOT" -c core.quotePath=true diff --raw --no-abbrev --no-renames --no-color "$merge_root" "$head_oid" \
|
|
534
|
+
git -C "$ROOT" --no-literal-pathspecs -c core.quotePath=true diff --raw --no-abbrev --no-renames --no-color "$merge_root" "$head_oid" -- ':(top,exclude).claude/state' \
|
|
524
535
|
| git -C "$ROOT" hash-object --stdin
|
|
525
536
|
)"
|
|
526
537
|
if [ $? -ne 0 ] || [ -z "$cur_fp" ]; then
|
|
@@ -536,7 +547,7 @@ EOF
|
|
|
536
547
|
echo "A human's informed \"merge anyway\" re-runs with --force."
|
|
537
548
|
fail 40
|
|
538
549
|
fi
|
|
539
|
-
warn "stale-approve guard: the tree differs from the APPROVE on issue #$APPROVE_ISSUE but the diff fingerprint matches — the base advanced (e.g. update-branch); merging the approved content
|
|
550
|
+
warn "stale-approve guard: the tree differs from the APPROVE on issue #$APPROVE_ISSUE but the diff fingerprint matches — the base advanced (e.g. update-branch) and/or only task state under .claude/state changed; merging the approved content."
|
|
540
551
|
fi
|
|
541
552
|
|
|
542
553
|
# Pin the merge to the exact head this guard verified: a push landing
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
# which would breach the per-fire budget) and with no materialized state file to
|
|
40
40
|
# go stale before an `update` command exists (P7). Falls back to the baked
|
|
41
41
|
# defaults when the config, its `paths` block, or a key is absent or commented
|
|
42
|
-
# out (fresh clone, the vendor repo dogfooding itself, an un-customized install)
|
|
42
|
+
# out (fresh clone, the vendor repo dogfooding itself, an un-customized install),
|
|
43
|
+
# and when the config is not a regular file.
|
|
43
44
|
_resolve_playbook_dirs() {
|
|
44
45
|
local repo_root="$1"
|
|
45
46
|
local home_dir="$2"
|
|
@@ -47,7 +48,8 @@ _resolve_playbook_dirs() {
|
|
|
47
48
|
PLAYBOOKS_GLOBAL_DIR="$home_dir/.claude/playbooks"
|
|
48
49
|
|
|
49
50
|
local config="$repo_root/harness.config.yml"
|
|
50
|
-
|
|
51
|
+
# `-f` as well as `-r`: a FIFO is readable, and awk on it would hang the hook.
|
|
52
|
+
{ [ -f "$config" ] && [ -r "$config" ]; } || return 0
|
|
51
53
|
|
|
52
54
|
# One awk pass: within the top-level `paths:` block, emit `<key>\t<value>` for
|
|
53
55
|
# the (uncommented) `playbooks` / `playbooks_global` entries — inline comments
|
|
@@ -12,6 +12,22 @@
|
|
|
12
12
|
|
|
13
13
|
set -u
|
|
14
14
|
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
|
|
17
|
+
# shellcheck source=lib/live-branch.sh
|
|
18
|
+
if [ -f "$SCRIPT_DIR/lib/live-branch.sh" ] && [ -r "$SCRIPT_DIR/lib/live-branch.sh" ]; then
|
|
19
|
+
source "$SCRIPT_DIR/lib/live-branch.sh"
|
|
20
|
+
LIVE_BRANCH_LIB=1
|
|
21
|
+
else
|
|
22
|
+
# A hand-copied or partial install without the helper (`lemony install`
|
|
23
|
+
# copies all of lib/): read HEAD's branch only — a rebase in progress reads
|
|
24
|
+
# as no branch — and say so once instead of failing on every call.
|
|
25
|
+
live_branch() {
|
|
26
|
+
git -C "$1" symbolic-ref --quiet HEAD 2>/dev/null | sed -n 's#^refs/heads/##p'
|
|
27
|
+
}
|
|
28
|
+
LIVE_BRANCH_LIB=0
|
|
29
|
+
fi
|
|
30
|
+
|
|
15
31
|
MANUAL=0
|
|
16
32
|
for arg in "$@"; do
|
|
17
33
|
case "$arg" in
|
|
@@ -93,13 +109,16 @@ fi
|
|
|
93
109
|
# The pointer never carried a real `active_task` / `branch` — nothing in the
|
|
94
110
|
# contract wrote them after init, so every auto-close record copied `null` /
|
|
95
111
|
# the init-day branch. The branch is the source of truth: the Orchestrator
|
|
96
|
-
# works every task on `harness/<id>-<slug
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
TASK_BRANCH="$(
|
|
102
|
-
|
|
112
|
+
# works every task on `harness/<id>-<slug>` and closes it out on
|
|
113
|
+
# `harness/closeout-<id>`, so the branch names the task. `live_branch` reads
|
|
114
|
+
# HEAD, or the rebasing branch while a rebase has HEAD detached; empty
|
|
115
|
+
# (→ `(unknown)`, no task) on any other detached HEAD or outside a repo —
|
|
116
|
+
# never a stale guess.
|
|
117
|
+
TASK_BRANCH="$(live_branch "$REPO_ROOT")"
|
|
118
|
+
if [ "$LIVE_BRANCH_LIB" -eq 0 ]; then
|
|
119
|
+
echo "session-close: .claude/hooks/lib/live-branch.sh is missing or unreadable — the task branch was read from HEAD only (a rebase in progress reads as no branch). Run \`lemony repair\` to restore it." >&2
|
|
120
|
+
fi
|
|
121
|
+
ACTIVE_TASK="$(printf '%s' "$TASK_BRANCH" | sed -n -e 's#^harness/\([0-9][0-9]*\)-..*$#\1#p' -e 's#^harness/closeout-\([0-9][0-9]*\)$#\1#p')"
|
|
103
122
|
|
|
104
123
|
# Fallback: a session with no recorded start defaults to "started now" so the
|
|
105
124
|
# math is well-defined; duration becomes 0 hours, which the schema accepts.
|
|
@@ -27,11 +27,42 @@ Format per entry (one block per release):
|
|
|
27
27
|
- `<event_type>.<field>` — <semantics-only change: shape untouched, meaning/unit changed>
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Empty sections may be omitted.
|
|
30
|
+
Empty sections may be omitted. `<version>` is the release that ships the entry —
|
|
31
|
+
written ahead, under the version the train expects; the version cut fails if a heading
|
|
32
|
+
names a version it neither ships nor already shipped (rename it to the real one).
|
|
31
33
|
|
|
32
34
|
---
|
|
33
35
|
|
|
34
|
-
## 0.
|
|
36
|
+
## 0.6.0 — 2026-09-27
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- `task_done.checkpoints` — optional count of `step_completed` events for the
|
|
41
|
+
task (≥ 1): one per resolved human checkpoint, so a step the human sent back
|
|
42
|
+
counts again. Carries the number `task_done.steps` reported before this
|
|
43
|
+
version. Sent with `steps` on step-by-step tasks; `checkpoints − steps` is the
|
|
44
|
+
task's extra checkpoint rounds — normally its `changes` answers at step
|
|
45
|
+
checkpoints. `lemony emit` refuses a `task_done` whose counts contradict each
|
|
46
|
+
other (see the Writer contract); lines already written are not re-checked.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- **`task_done.steps` — now counts distinct steps**, not `step_completed`
|
|
51
|
+
events. Before this version the Orchestrator counted every event, including
|
|
52
|
+
checkpoints that ended in `changes`, so a task with 4 groups could report 10.
|
|
53
|
+
Shape untouched; readers comparing `steps` across versions should treat
|
|
54
|
+
earlier lines as checkpoint counts (the same unit as `checkpoints`), not
|
|
55
|
+
group counts.
|
|
56
|
+
|
|
57
|
+
- **`session_closed.task_id` — present in two more task contexts**: on a
|
|
58
|
+
`harness/closeout-<id>` branch (the `task-closeout` record branch), and while a
|
|
59
|
+
rebase has HEAD detached (the id of the branch being rebased, read from git's
|
|
60
|
+
own rebase record). Shape untouched; the value simply appears where it used to
|
|
61
|
+
be absent.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 0.5.0 — 2026-09-10
|
|
35
66
|
|
|
36
67
|
### Changed
|
|
37
68
|
|