@lemoncode/lemony 0.4.1 → 0.5.1

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/catalog/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.1
1
+ 0.5.1
@@ -54,6 +54,16 @@ what you run, only how many calls carry it.
54
54
  behavior (vertical slices, never all-tests-then-all-code). When your invocation
55
55
  says **auto-commit is OFF**, the whole run obeys §Staging protocol (below) —
56
56
  zero commits, staged save-points.
57
+ **The checkbox is yours** (L1 — a task with a `tasks.md`; L2 has none). A task's
58
+ `- [ ]` is its done-marker: flip it to `- [x]` the moment the task reaches green
59
+ (suite passing), and let the tick ride the task's own commit (auto-commit OFF:
60
+ the same `git add` — at the human's OK the Orchestrator's pathspec split lands it
61
+ in the step's state commit beside the code commit; the index is where the two
62
+ move together) — so the file always says where the work is. Nobody
63
+ else ticks for you: `lemony review-ledger validate` reports a task still `[ ]` in
64
+ a group the loop has passed (`unticked-completed-task`) and the human reads it as
65
+ a forgotten mark **or as work never done** — the honest reason to keep the two in
66
+ step.
57
67
  **Scope: exactly what the invocation hands you.** By default that is the whole
58
68
  `tasks.md` list (all-at-once). In **step-by-step mode** the Orchestrator
59
69
  invokes you per step — scoped to **one group**, handed **by reference** (the
@@ -124,9 +134,10 @@ iteration only knows what this contract and the worktree tell it:
124
134
 
125
135
  - **Zero commits, zero pushes.** Never `git commit`, never `git push` — not for
126
136
  code, not for state. The Orchestrator commits once the human OKs.
127
- - **Stage after every green task** — `git add -A` when a checkbox/task reaches
128
- green (suite passing). The index is your save-point ladder: everything staged
129
- is a proven-green floor.
137
+ - **Stage after every green task** — tick the task's checkbox in `tasks.md`
138
+ (`- [ ]` → `- [x]`, step 2 of the procedure), then `git add -A`, when a task
139
+ reaches green (suite passing). The index is your save-point ladder: everything
140
+ staged is a proven-green floor, and the tick is part of the floor.
130
141
  - **Never stage mid-experiment.** A `git add` while red or mid-refactor silently
131
142
  **overwrites the save-point** — `git status` looks identical afterwards, and
132
143
  recovery yields nameless blobs (practically unrecoverable). Stage only at
@@ -365,7 +365,9 @@ numbers still never shift. For each group, in order:
365
365
  (fresh context, as always) scoped to **this one group**: give it the branch, the
366
366
  task-state paths, and the group's id + header line in `tasks.md` — by reference
367
367
  (§Sub-agent invocation), it reads the group's tasks from the file itself (`tdd`
368
- skill — checkbox by checkbox with small commits, as always). It commits to the
368
+ skill — checkbox by checkbox with small commits, as always, and it **ticks each
369
+ checkbox at green** — the tick rides the task's commit, or its `git add` under
370
+ auto-commit OFF; the file is the group's progress record). It commits to the
369
371
  branch (auto-commit OFF: it **stages** instead of committing — point the
370
372
  invocation at the staging protocol in `implementer.md`), logs to `progress.md`,
371
373
  and signals done. **No PR yet** — the PR opens after the last group,
@@ -422,13 +424,15 @@ numbers still never shift. For each group, in order:
422
424
  gates or the bare real-run floor (`undeclared`).
423
425
  Red → **an APPROVE with a red ledger is never relayed**, and a red ledger is not a
424
426
  REJECT of the code: the Implementer is not at fault and is **never** re-invoked for
425
- it. Route on the `[kind]` lines the verb prints — it closes with
427
+ the red itself (a human's `changes` at the checkpoint it feeds is the human's call, as
428
+ at every checkpoint). Route on the `[kind]` lines the verb prints — it closes with
426
429
  `N spec-side problem(s) … do not retry the Reviewer while any of these stands.`
427
430
  whenever any problem lives outside the sidecar:
428
431
 
429
432
  - **any spec-side problem** — the verb counts them out loud (`unknown-risk-class`, a
430
433
  tag or ref list that did not parse, a duplicated group number, an orphan task, an
431
- empty group, a dangling requirement ref, a step with no group) → no retry. They
434
+ empty group, a dangling requirement ref, a step with no group, a task still
435
+ `[ ]` in a group the loop has passed) → no retry. They
432
436
  live in `tasks.md` / `requirements.md`, which the Reviewer cannot fix: stop and
433
437
  bring them to the human as an **anticipated checkpoint** (below) with the verb's
434
438
  lines as the content — the spec needs a decision, and the fix routes as a discovery
@@ -436,6 +440,37 @@ numbers still never shift. For each group, in order:
436
440
  the sidecar on disk: green → the checkpoint; a sidecar-side red then follows the
437
441
  one-retry rule below. The human's `ok` on an anticipated checkpoint is their call,
438
442
  as at every gate — what never happens is you relaying a red as an APPROVE.
443
+ **`unticked-completed-task` is the one spec-side kind that needs no discovery**:
444
+ the Implementer ticks each task at green, so an unticked task in a passed group
445
+ is a forgotten mark or work that was never done, and only the human can tell
446
+ which. Name the task ids in the checkpoint and ask. `ok` = "done, the mark was
447
+ forgotten" → tick it yourself (`- [ ]` → `- [x]`) and **stage it the moment you
448
+ do** (`git add .claude/state/tasks/<id>/spec/tasks.md`) — a mediated edit, staged
449
+ on arrival like a Spec Author update, so the checkpoint contract's spec check
450
+ (item 4) never reads it as an unconfirmed human spec edit — then re-run the verb.
451
+ Wherever an OK's composite commits state, the staged tick rides its
452
+ `.claude/state` half and needs nothing more: every step OK (both knob states),
453
+ and auto-commit OFF's deferral-ending checkpoint OK, which its pre-gate full pass
454
+ feeds. **At the merge-gate presentation nothing else commits state before the
455
+ merge** — the step-8 full pass in either mode, all-at-once under auto-commit ON
456
+ having had no checkpoint at all — so there, commit it yourself as you stage it,
457
+ in the same composite turn as the re-run (§Turn economy):
458
+
459
+ ```bash
460
+ git commit -m "task(<id>): tick <T-ids> — human ok on unticked-completed-task" \
461
+ -- .claude/state/tasks/<id>/spec/tasks.md; \
462
+ git push # best-effort — a failure warns, never blocks
463
+ ```
464
+
465
+ A merged `tasks.md` still reading `[ ]` is exactly the harm the kind exists to
466
+ stop. That post-APPROVE state write moves the stale-approve fingerprint the way
467
+ the ledger's own write does, and routes as the merge gate's exit-40 rule says
468
+ (§Merge gate) — a fresh APPROVE re-records the hashes; `--force` stays the
469
+ human's separate, informed call. `changes` = the
470
+ task is not done → the normal fix iteration, feedback naming the task. Never tick
471
+ on your own judgment: the mark is the Implementer's claim, and the verb exists so
472
+ a missing one is read by a person, not smoothed over by you.
473
+
439
474
  - **anything else** (a problem in the sidecar itself) → re-invoke the **Reviewer**
440
475
  (fresh, as always) **once**, with the
441
476
  verb's `[kind] message` lines **verbatim** in the spawn prompt — the delta is the
@@ -461,7 +496,12 @@ numbers still never shift. For each group, in order:
461
496
  `step_completed` emit (here `review_iterations` is 3) and the same transient
462
497
  `awaiting human checkpoint (step N/M)` line in `progress.md` — except you present
463
498
  the unresolved disagreement (both positions, the spec slice) instead of a clean
464
- step.
499
+ step. When its content is the verb's `unticked-completed-task` lines, `ok` carries
500
+ one act before the state commit — tick and stage the named tasks (step 2's routing
501
+ bullet) — and the transient line names them,
502
+ `awaiting human checkpoint (step N/M) [unticked: T3, T4]` (step 5), so a cold
503
+ `/resume` re-presents the same content and owes the same act (the clean-step line
504
+ carries no suffix).
465
505
 
466
506
  In auto-commit OFF, `OK` is also the moment the group's single code commit
467
507
  lands, and `changes` sends the fresh Implementer to iterate **over the worktree**
@@ -571,8 +611,10 @@ numbers still never shift. For each group, in order:
571
611
  step's line is transient — update it in place as the loop progresses
572
612
  (`fix-loop iteration K — in progress` while implementing/reviewing,
573
613
  `awaiting ledger retry (step N/M, retry 1/1)` while the fresh Reviewer redoes a red
574
- ledger, `awaiting human checkpoint (step N/M)` while waiting on the human), then
575
- replace it with the resolved outcome:
614
+ ledger, `awaiting human checkpoint (step N/M)` while waiting on the human — with
615
+ the `[unticked: T3, T4]` suffix when the checkpoint is the anticipated one an
616
+ `unticked-completed-task` red raised, in all-at-once too, where the counter is
617
+ absent), then replace it with the resolved outcome:
576
618
 
577
619
  ```markdown
578
620
  Mode: step-by-step
@@ -611,7 +653,10 @@ spec-side problem to the human, and never an APPROVE relayed on a red ledger. Ou
611
653
  the step loop the transient line is `awaiting ledger retry (full pass, retry 1/1)` in
612
654
  `progress.md` (no step counter), and "the human" is the gate the pass feeds: the
613
655
  auto-commit-OFF checkpoint when there is one, otherwise the merge-gate presentation,
614
- with the verb's lines as the content.
656
+ with the verb's lines as the content. An `unticked-completed-task` there resolves as
657
+ in step 2's routing bullet — on `ok`, tick and stage the named tasks; at the
658
+ auto-commit-OFF checkpoint the OK's composite commits them, at the merge-gate
659
+ presentation you commit them yourself — then re-run the verb.
615
660
 
616
661
  ## Checkpoint contract (how a human gate presents work)
617
662
 
@@ -668,7 +713,10 @@ contract, not a vibe:
668
713
  agent-staged floor, so this check fires only on **unmediated** edits and
669
714
  the re-presented checkpoint is clean; an unstaged edit that matches the
670
715
  recorded `**Resolution**` in `discoveries.md` is confirmed content — stage
671
- it, don't re-raise.
716
+ it, don't re-raise. The one mediated edit that is **not** a discovery — your
717
+ own `- [ ]` → `- [x]` tick on the human's `ok` to an `unticked-completed-task`
718
+ checkpoint (step 2) — is staged the same way, as you make it; it has no
719
+ `discoveries.md` entry to match, and needs none: the `ok` is its record.
672
720
 
673
721
  The **spec check runs in both knob states** — in auto-commit ON, run it
674
722
  **before** step 3's `awaiting` state commit, which would otherwise silently
@@ -228,9 +228,15 @@ What the validator enforces in this version, so you never have to guess:
228
228
  - **Spec-side problems are reported, never dropped — and they are not yours to fix.** A
229
229
  `[risk: …]` tag outside the vocabulary (`unknown-risk-class`), a tag or a `(R<n>)` ref
230
230
  list that did not parse, a duplicated group number, a task above the first header, an
231
- empty group, a ref `requirements.md` never declares, a step with no group: each names
231
+ empty group, a ref `requirements.md` never declares, a step with no group, a task
232
+ still `[ ]` in a group the loop has passed (`unticked-completed-task` — the
233
+ Implementer's done-marker is missing, and whether the work is too is the human's
234
+ call, not yours): each names
232
235
  a defect in `tasks.md` / `requirements.md`, and the verb counts them out loud as
233
- **spec-side**. Finish the ledger (a dangling ref leaves the slice — never invent an
236
+ **spec-side**. The ticks themselves, wherever a diff shows them (the full pass's PR
237
+ diff does), are the Implementer's done-marks flipped at green — **not a spec
238
+ change**: never drift, never a finding on their own. Finish the ledger (a dangling
239
+ ref leaves the slice — never invent an
234
240
  entry for a requirement that does not exist), return your verdict as usual, and name
235
241
  them in it — the Orchestrator takes them to the human instead of sending you back.
236
242
 
@@ -9,8 +9,9 @@ A two-step pause:
9
9
 
10
10
  1. **Write the resume narrative** so future-you (or whoever resumes) can pick
11
11
  up cold:
12
- - Read `.claude/state/current-<your-user>.md` and the active task's
13
- `progress.md` (if any).
12
+ - Read the active task's `progress.md` (if any) and the latest note under
13
+ `.claude/state/sessions/<your-user>/` (if any) — that is where the
14
+ narrative lives; `current-<your-user>.md` only holds session timestamps.
14
15
  - Generate a UTC timestamp slug — run `date -u +%Y-%m-%dT%H:%M:%SZ` and use
15
16
  it as `<ts>`. Pick a short topic slug from the work in progress.
16
17
  - Write `.claude/state/sessions/<your-user>/<ts>-<topic>.md` with this
@@ -19,8 +20,8 @@ A two-step pause:
19
20
  ```markdown
20
21
  ---
21
22
  session_close_ts: <ts>
22
- active_task: <issue-id or null>
23
- branch: <current branch>
23
+ active_task: <issue-id or null — the current branch's id: `harness/<id>-<slug>` → `<id>`>
24
+ branch: <current branch, or (unknown) if HEAD is detached>
24
25
  topic: <topic>
25
26
  reason: manual
26
27
  auto_close: false
@@ -42,7 +43,9 @@ A two-step pause:
42
43
  ```
43
44
 
44
45
  2. **Trigger the session-close hook** so the `session_closed` event lands in
45
- `events.jsonl` and `current-<your-user>.md` pointers update:
46
+ `events.jsonl` (its `task_id` is derived from the current branch when it is
47
+ a `harness/<id>-<slug>` task branch) and `current-<your-user>.md` gets its
48
+ `last_close_ts` stamped:
46
49
 
47
50
  ```bash
48
51
  .claude/hooks/session-close.sh --manual
@@ -18,7 +18,9 @@ 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
@@ -89,6 +89,10 @@
89
89
  "items": {
90
90
  "type": "string"
91
91
  }
92
+ },
93
+ "verify": {
94
+ "type": "string",
95
+ "minLength": 1
92
96
  }
93
97
  },
94
98
  "additionalProperties": false
@@ -46,8 +46,17 @@ WARNINGS=()
46
46
  # awk/grep/git (preinstalled) cover the rest.
47
47
  CONFIG_VERSION=""
48
48
  CONFIG_REPO=""
49
- if [ ! -f "$CONFIG" ]; then
49
+ if [ ! -e "$CONFIG" ]; then
50
50
  ERRORS+=("harness.config.yml not found at $REPO_ROOT. Run \`lemony install\`.")
51
+ elif [ ! -f "$CONFIG" ]; then
52
+ # There, but not a file awk can read: a directory, or a FIFO it would block on
53
+ # until something wrote to it. `-e`/`-f` follow a symlink, so a dead link stays
54
+ # "not found" above and a link to one of these lands here.
55
+ 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.")
56
+ elif [ ! -r "$CONFIG" ]; then
57
+ # A file awk cannot open failed the key check below, so the boot blamed missing keys
58
+ # under a raw awk error.
59
+ ERRORS+=("harness.config.yml at $REPO_ROOT cannot be read (check its permissions), so it was not read.")
51
60
  elif ! awk '
52
61
  { sub(/\r$/, "") }
53
62
  /^[^[:space:]#]/ {
@@ -138,7 +147,10 @@ fi
138
147
  # ladder, and following `git pull --rebase` with `rebase.autostash` set flattens
139
148
  # that index — the documented rollback then reverts to the last commit and the
140
149
  # group's whole uncommitted work is gone. So on any other branch: silence.
141
- CURRENT_BRANCH="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
150
+ # The full ref with `refs/heads/` stripped, not `--short`: the short form
151
+ # lengthens to `heads/<name>` when a tag shares the branch's name, which would
152
+ # fail the `harness/*` test below (same read as `session-close.sh` / `status`).
153
+ CURRENT_BRANCH="$(git symbolic-ref --quiet HEAD 2>/dev/null | sed -n 's#^refs/heads/##p')"
142
154
  DEFAULT_BRANCH="$(git symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')"
143
155
  if [ -n "$DEFAULT_BRANCH" ] && [ "$CURRENT_BRANCH" = "$DEFAULT_BRANCH" ]; then
144
156
  BEHIND="$(git rev-list --count "HEAD..origin/$DEFAULT_BRANCH" 2>/dev/null || echo 0)"
@@ -187,12 +199,23 @@ if [ "${#ERRORS[@]}" -eq 0 ] && [ -n "$GIT_USER_EMAIL" ]; then
187
199
  CURRENT_PATH="$REPO_ROOT/.claude/state/current-$USER_SLUG.md"
188
200
  if [ -z "$USER_SLUG" ]; then
189
201
  : # path-unsafe slug — pointer write skipped above
202
+ elif [ -L "$REPO_ROOT/.claude" ] || [ -L "$REPO_ROOT/.claude/state" ]; then
203
+ # A linked directory takes the pointer, and every refresh after it, out of the
204
+ # repo — the same write-through the dangling-link branch below refuses.
205
+ 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.")
206
+ elif [ -e "$CURRENT_PATH" ] && [ ! -f "$CURRENT_PATH" ]; then
207
+ # There, but not a file: writing to a FIFO blocks until something reads it, and
208
+ # awk reading one blocks until something writes. The pointer is not critical, so
209
+ # the boot goes on without it — a warning, not a blocking error.
210
+ 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.")
211
+ elif [ -L "$CURRENT_PATH" ] && [ ! -e "$CURRENT_PATH" ]; then
212
+ # A symlink whose target is gone: `-e`/`-f` follow it and see nothing, and the
213
+ # `cat >` below would create the target wherever the link points.
214
+ 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.")
190
215
  elif [ ! -f "$CURRENT_PATH" ]; then
191
216
  mkdir -p "$REPO_ROOT/.claude/state"
192
217
  cat > "$CURRENT_PATH" <<EOF
193
218
  ---
194
- active_task: null
195
- branch: $(git symbolic-ref --quiet --short HEAD 2>/dev/null || echo "(unknown)")
196
219
  session_start_ts: $NOW_ISO
197
220
  last_close_ts: ""
198
221
  ---
@@ -201,22 +224,45 @@ last_close_ts: ""
201
224
 
202
225
  Per-dev pointer (gitignored). The lifecycle hooks read \`session_start_ts\`
203
226
  to compute \`session_active_h\` and reset it on each SessionStart that orients.
204
-
205
- ## Resume hint
206
-
207
- _(One paragraph — what to pick up next. Updated by \`/pause\`.)_
227
+ The active task and its branch are not recorded here — \`session-close.sh\`
228
+ derives them from the live branch (\`harness/<id>-<slug>\`) at close time — and
229
+ the narrative resume lives under \`sessions/<user>/\` (written by \`/pause\`).
208
230
  EOF
209
231
  else
210
232
  # Refresh session_start_ts on every orient so close-time math is accurate.
211
233
  # awk in-place rewrite of the frontmatter scalar — without this,
212
234
  # `session_start_ts` would stay frozen at the first orient and
213
235
  # `session_active_h` would be cumulative across every clear/resume.
236
+ # The same pass drops the dead `active_task` / `branch` scalars a pointer
237
+ # created by an earlier version still carries — nothing ever updated them,
238
+ # so they lie (`null` / the init-day branch); the live branch is the source.
239
+ # Its "Resume hint" section goes too, but ONLY while it is still exactly
240
+ # the untouched placeholder — the heading, blank lines, the one placeholder
241
+ # line, and nothing else up to the next heading or EOF (its "Updated by
242
+ # `/pause`" promise was never kept). Anything a person wrote there — the
243
+ # placeholder replaced, a paragraph added below it, even a `---` rule —
244
+ # makes the section theirs and it stays, heading included; so does a
245
+ # section stripped to its bare heading. Decided by a read-only pre-pass so
246
+ # the rewrite below never looks ahead; `[[:space:]]*$` tolerates CRLF.
247
+ PRUNE_HINT="$(awk '
248
+ inhint && /^---/ { other = 1; next }
249
+ /^---/ { block++; next }
250
+ block < 2 { next }
251
+ /^## Resume hint[[:space:]]*$/ { inhint = 1; seen = 1; next }
252
+ inhint && /^## / { inhint = 0 }
253
+ !inhint { next }
254
+ /^[[:space:]]*$/ { next }
255
+ /Updated by `\/pause`/ { placeholder++; next }
256
+ { other = 1 }
257
+ END { print (seen && placeholder == 1 && !other) ? 1 : 0 }
258
+ ' "$CURRENT_PATH" 2>/dev/null)"
259
+ [ "$PRUNE_HINT" = "1" ] || PRUNE_HINT=0
214
260
  # Write through a mktemp file, not a predictable `$CURRENT_PATH.tmp`: the
215
261
  # latter is a known path an attacker could pre-plant as a symlink for the
216
262
  # redirect to follow. `mktemp` refuses to reuse an existing path, and `mv`
217
263
  # over CURRENT_PATH itself safely replaces a symlink with a regular file.
218
264
  if tmp="$(mktemp "$REPO_ROOT/.claude/state/.ptr.XXXXXX")"; then
219
- awk -v ts="$NOW_ISO" '
265
+ awk -v ts="$NOW_ISO" -v prune="$PRUNE_HINT" '
220
266
  /^---/ {
221
267
  block++
222
268
  if (block == 2 && !seen) print "session_start_ts: " ts
@@ -224,6 +270,10 @@ EOF
224
270
  next
225
271
  }
226
272
  block == 1 && /^session_start_ts:/ { print "session_start_ts: " ts; seen = 1; next }
273
+ block == 1 && /^(active_task|branch):/ { next }
274
+ prune && block >= 2 && /^## Resume hint[[:space:]]*$/ { inhint = 1; next }
275
+ inhint && /^## / { inhint = 0 }
276
+ inhint { next }
227
277
  { print }
228
278
  ' "$CURRENT_PATH" > "$tmp" && mv "$tmp" "$CURRENT_PATH" || rm -f "$tmp"
229
279
  fi
@@ -309,12 +309,13 @@ fi
309
309
  # same recipe as playbook-scan.sh): emit `<key>\t<value>` for the uncommented
310
310
  # `checks_timeout_secs` / `allow_no_checks` entries — inline comments stripped
311
311
  # (on the block header too) and surrounding quotes removed. Any column-0 line
312
- # ends the block. Absent/unreadable config → baked defaults.
312
+ # ends the block. Absent/unreadable/non-regular config → baked defaults.
313
313
  CONFIG_TIMEOUT=""
314
314
  CONFIG_ALLOW_NO_CHECKS=""
315
315
  read_merge_config() {
316
316
  local config="$ROOT/harness.config.yml"
317
- [ -r "$config" ] || return 0
317
+ # `-f` as well as `-r`: a FIFO is readable, and awk on it would hang the merge.
318
+ { [ -f "$config" ] && [ -r "$config" ]; } || return 0
318
319
 
319
320
  local key value
320
321
  while IFS=$'\t' read -r key value; do
@@ -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
- [ -r "$config" ] || return 0
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
@@ -65,8 +65,6 @@ NOW_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
65
65
  # (preinstalled) cover the rest.
66
66
  CURRENT_PATH="$REPO_ROOT/.claude/state/current-$USER_SLUG.md"
67
67
  SESSION_START_TS=""
68
- ACTIVE_TASK=""
69
- TASK_BRANCH=""
70
68
  read_frontmatter_field() {
71
69
  local field="$1"
72
70
  awk -v field="$field" '
@@ -89,10 +87,20 @@ read_frontmatter_field() {
89
87
  }
90
88
  if [ -f "$CURRENT_PATH" ]; then
91
89
  SESSION_START_TS="$(read_frontmatter_field session_start_ts)"
92
- ACTIVE_TASK="$(read_frontmatter_field active_task)"
93
- TASK_BRANCH="$(read_frontmatter_field branch)"
94
90
  fi
95
91
 
92
+ # ── Derive the task context from the live branch ───────────────────────────
93
+ # The pointer never carried a real `active_task` / `branch` — nothing in the
94
+ # contract wrote them after init, so every auto-close record copied `null` /
95
+ # the init-day branch. The branch is the source of truth: the Orchestrator
96
+ # works every task on `harness/<id>-<slug>`, so HEAD names the task. The full
97
+ # ref is read and `refs/heads/` stripped rather than `--short`: the short form
98
+ # lengthens to `heads/<name>` when a tag shares the branch's name, which would
99
+ # silently drop the id. Empty (→ `(unknown)`, no task) on a detached HEAD, a
100
+ # rebase in progress, or outside a repo — never a stale guess.
101
+ TASK_BRANCH="$(git -C "$REPO_ROOT" symbolic-ref --quiet HEAD 2>/dev/null | sed -n 's#^refs/heads/##p')"
102
+ ACTIVE_TASK="$(printf '%s' "$TASK_BRANCH" | sed -n 's#^harness/\([0-9][0-9]*\)-..*$#\1#p')"
103
+
96
104
  # Fallback: a session with no recorded start defaults to "started now" so the
97
105
  # math is well-defined; duration becomes 0 hours, which the schema accepts.
98
106
  if [ -z "$SESSION_START_TS" ]; then
@@ -143,9 +151,9 @@ EMIT_ARGS=(
143
151
  --reason="$REASON"
144
152
  --auto-close="$AUTO_CLOSE"
145
153
  )
146
- # Skip a literal "null" — a buggy frontmatter may emit the string instead of a
147
- # real YAML null. The schema would accept it but the line is forensic noise.
148
- if [ -n "$ACTIVE_TASK" ] && [ "$ACTIVE_TASK" != "null" ]; then
154
+ # Only a task branch yields an id; a session closed on `main` (or detached)
155
+ # emits the envelope without `task_id`, as the schema allows.
156
+ if [ -n "$ACTIVE_TASK" ]; then
149
157
  EMIT_ARGS+=(--task-id="$ACTIVE_TASK")
150
158
  fi
151
159
  # Resolve the CLI via the launcher (local devDependency → global → fail-fast)
@@ -31,6 +31,20 @@ Empty sections may be omitted.
31
31
 
32
32
  ---
33
33
 
34
+ ## 0.4.2 — 2026-09-10
35
+
36
+ ### Changed
37
+
38
+ - **`session_closed.task_id` — now derived from the live branch at close time**
39
+ (`harness/<id>-<slug>` → `<id>`); absent on the default branch or a detached
40
+ HEAD. Previously the field was **never populated** on this event: the emitter
41
+ read it from a per-dev pointer field nothing ever wrote. **No field renamed,
42
+ added, or removed** — the envelope's optional `task_id` is simply present from
43
+ this version on when a session closes on a task branch. Readers bucketing
44
+ `session_closed` by task should expect the value to appear at this version.
45
+
46
+ ---
47
+
34
48
  ## 0.2.0 — 2026-07-31
35
49
 
36
50
  ### Changed
@@ -36,14 +36,14 @@ Every event line starts with this envelope. Per-type fields are added at the
36
36
  same top level — there is no nested `payload`, so Zod discriminated unions key on
37
37
  `type`.
38
38
 
39
- | Field | Type | Required | Axis | Notes |
40
- | ----------------- | ------ | -------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
41
- | `type` | string | yes | `internal-enum` | One of the 9 event types listed below. Discriminator. |
42
- | `ts` | string | yes | `metric` | UTC ISO 8601 with `Z` suffix (e.g. `2026-05-28T14:30:00.000Z`). **No local offsets.** |
43
- | `user` | string | yes | `local-only` | `git config user.email` of the actor. Never exported in any tier. |
44
- | `project` | string | yes | `identity` | `task_storage.repo` slug (e.g. `acme/widgets`), from `harness.config.yml`. **Never `OWNER/REPO`** — the CLI refuses to emit while that placeholder is the value (see [Placeholder guard](#placeholder-guard)). |
45
- | `task_id` | string | no | `identity` | Task issue id (e.g. `42`) when the event has a task context. Absent on session/global events. A per-project correlator — only meaningful alongside `project`, so it shares the `identity` axis. |
46
- | `harness_version` | string | yes | `metric` | `version` of the **installed** `@lemoncode/lemony` package — _not_ `vendor_version` from config. |
39
+ | Field | Type | Required | Axis | Notes |
40
+ | ----------------- | ------ | -------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
41
+ | `type` | string | yes | `internal-enum` | One of the 9 event types listed below. Discriminator. |
42
+ | `ts` | string | yes | `metric` | UTC ISO 8601 with `Z` suffix (e.g. `2026-05-28T14:30:00.000Z`). **No local offsets.** |
43
+ | `user` | string | yes | `local-only` | `git config user.email` of the actor. Never exported in any tier. |
44
+ | `project` | string | yes | `identity` | `task_storage.repo` slug (e.g. `acme/widgets`), from `harness.config.yml`. **Never `OWNER/REPO`** — the CLI refuses to emit while that placeholder is the value (see [Placeholder guard](#placeholder-guard)). |
45
+ | `task_id` | string | no | `identity` | Task issue id (e.g. `42`) when the event has a task context. Absent on global events; `session_closed` carries it when HEAD is a `harness/<id>-<slug>` task branch. A per-project correlator — only meaningful alongside `project`, so it shares the `identity` axis. |
46
+ | `harness_version` | string | yes | `metric` | `version` of the **installed** `@lemoncode/lemony` package — _not_ `vendor_version` from config. |
47
47
 
48
48
  ### Placeholder guard
49
49
 
@@ -114,7 +114,9 @@ forward-compatible — readers dispatch on `type` and ignore unknowns.
114
114
  ### 1. `session_closed` _(P5)_
115
115
 
116
116
  Emitted by `session-close.sh` on `SessionEnd` or `/pause` (manual). One per
117
- session.
117
+ session. The envelope's `task_id` is derived from the live branch at close time
118
+ (`harness/<id>-<slug>` → `<id>`); a session closed on the default branch or a
119
+ detached HEAD carries none.
118
120
 
119
121
  | Field | Type | Required | Axis | Notes |
120
122
  | ------------------ | ------- | -------- | --------------- | --------------------------------------------------------------------------------------------------------------- |
@@ -29,7 +29,9 @@ Run the tiers in order; each adds evidence the next builds on.
29
29
  token pair — including any dark-mode override — and exits non-zero on a pair below its
30
30
  floor. This is the one a11y measurement doable offline, because colour comes from the
31
31
  token file. It is complementary to `lemony design-tokens validate` (validate proves colour
32
- is a token reference; contrast proves the pair meets its floor).
32
+ is a token reference; contrast proves the pair meets its floor). When the project declares
33
+ `design_tokens.verify` in `harness.config.yml`, the same command also runs the design
34
+ system's own verifier and forwards its report — read that output as T0 evidence too.
33
35
  - **T1 — source a11y lint (rides the project).** The project's own linter usually carries an
34
36
  accessibility plugin (`eslint-plugin-jsx-a11y`, Svelte's or Vue's a11y rules). Run the
35
37
  project's `lint`; don't reimplement it. Read what it flags.
@@ -70,34 +70,67 @@ variables and the DTCG JSON:
70
70
  - `name` — the variable's dotted path. Map it to/from the tool's own grouping separator.
71
71
  - `value` for a literal; `ref` for an alias (the name of the variable it points at).
72
72
  - `modes` — per-theme overrides (the base theme is `value`/`ref`, not repeated).
73
+ - Every `value` and mode value is a **string** — `"1024"`, not `1024` — and every `ref` a
74
+ non-empty one; each variable carries a `value` or a `ref` (a `value` beside a `ref` is
75
+ ignored), and `modes` is an object whose mode names are not blank nor `__proto__`. A name
76
+ is dot-separated segments, none empty, none starting with `$`, none `__proto__`, not just
77
+ a tier (`primitive`, `semantic`, `component`), and no two variables share one. The CLI
78
+ refuses a file that breaks any of these, naming the variable (and the mode, for a mode
79
+ value).
73
80
 
74
81
  The CLI maps this to the 3-tier DTCG model: `ref` → an alias `{path}`, `modes` →
75
- `$extensions["com.lemony.modes"]`, and the tier follows the path (a literal defaults to
76
- `primitive`, an alias to `semantic` when the name carries no tier).
82
+ `$extensions["com.lemony.modes"]`, and the tier follows the path. A name without a tier
83
+ lands on the one tier `docs/design-tokens.json` already has it in; otherwise a literal
84
+ defaults to `primitive` and an alias to `semantic`. A bare `ref` — or a bare `{alias}` mode
85
+ value — resolves to the tiered path its target lands at in the same neutral file, or to the
86
+ one tier the token file has it in.
77
87
 
78
88
  ## import — tool → JSON
79
89
 
80
90
  1. **Read** the tool's variables over MCP and write them to a neutral file (a temp path).
81
91
  2. **Preview** the diff (writes nothing):
82
92
  `lemony design-tokens import --from=<neutral-file>`. It prints, per token, the target tier
83
- and `new` / `changed` (with the old → new value) / `unchanged`.
93
+ and `new` / `changed` (each thing that changes, old → new: the value, the `$type`, each
94
+ mode) / `unchanged`. When
95
+ `docs/design-tokens.json` already fails validation it adds a `Warning:` saying so: the
96
+ apply will refuse any slice that leaves it failing, so tell the human before curating.
84
97
  3. **Present and curate.** Walk the human through it: which new tokens to take, which changes
85
98
  to accept, and — where a tool-origin name is ambiguous — whether a literal belongs in
86
99
  `primitive` or `semantic`. The human owns the slice.
87
100
  4. **Apply** the agreed slice:
88
101
  `lemony design-tokens import --from=<neutral-file> --apply --only=<dotted,paths>`.
89
102
  This does the additive merge into `docs/design-tokens.json` deterministically (it
90
- bootstraps the file if it does not exist yet). Then run `lemony design-tokens validate` to
91
- confirm the result is well-formed.
103
+ bootstraps the file if it does not exist yet and the slice writes something). A changed
104
+ token keeps what the neutral file does not carry — `$description`, a contrast pairing,
105
+ other extensions — and an unchanged one is left as it is. A token the tool cannot hold
106
+ (see export) is never written over: the preview shows it as a change from
107
+ `(outside the sync: <why>)`, and the apply lists it as not applied. It writes nothing
108
+ when the merged file would fail validation — typically a `semantic` alias whose
109
+ `primitive` target is neither in the file nor in the slice: add the target to the same
110
+ `--only`, or fix the file first. When the neutral file does not carry the target at
111
+ all, the refusal says so: create it in the tool first, or leave out what references
112
+ it. An `--only` path no variable lands at is refused rather than skipped. A refusal
113
+ prints the preview again, so the slice can be re-curated from the paths it lists. Then run
114
+ `lemony design-tokens validate` to confirm the result is well-formed.
92
115
 
93
116
  ## export — JSON → tool
94
117
 
95
- 1. **Plan.** Read the tool's current variables over MCP into a neutral file, then:
118
+ 1. **Plan.** `export` refuses (exit 1) a `docs/design-tokens.json` that fails
119
+ `lemony design-tokens validate` — fix the file first. Read the tool's current variables
120
+ over MCP into a neutral file, then:
96
121
  `lemony design-tokens export --tool-state=<tool-state> --out=<projection-file>`. It prints
97
122
  the additive upsert plan (how many to create, how many to update — **never any deletes**)
98
123
  and writes the projection the tool should hold to `<projection-file>`. The `--tool-state`
99
124
  is optional; without it every variable is planned as a create (the upsert is idempotent by
100
- name either way).
125
+ name either way). A tool variable named without its tier (`color.brand`) is matched to the
126
+ path `import` lands it at (`primitive.color.brand`), so when you push, write into that
127
+ existing variable rather than creating a tiered twin. A token a tool variable cannot hold
128
+ is not projected: a composite (a shadow, a typography, a cubic Bézier — an object or
129
+ array `$value`), a token whose modes the neutral format cannot carry (a mode value that
130
+ is not text, a number or a boolean, a blank or `__proto__` mode name, a modes extension
131
+ that is not an object), and any alias to one of those. The plan names each as "not
132
+ projected", with why — say so to the human. `--out` takes a file path (not a pipe, nor
133
+ the token file itself).
101
134
  2. **Confirm.** Show the plan; the human approves.
102
135
  3. **Push.** Write the projection's variables into the tool over MCP — create new ones, update
103
136
  changed ones, leave tool-only variables untouched.
@@ -215,6 +215,11 @@ The review evidence ledger's validator enumerates each group's review slice from
215
215
  refs; a task it cannot read is reported as `malformed-task-refs` — a spec defect that
216
216
  pauses the loop, never read as "declares none".
217
217
 
218
+ The checkbox itself is the **Implementer's done-marker**: it flips `- [ ]` → `- [x]`
219
+ when the task reaches green, with the task's commit. Write every task `- [ ]`; the same
220
+ validator reports a task still `[ ]` in a group the loop has passed
221
+ (`unticked-completed-task`), so the file tracks the progress its shape promises.
222
+
218
223
  Rules: order so the first task is a tracer bullet; never "write all tests" then
219
224
  "write all code"; keep each task small enough to verify on its own. Grouping never
220
225
  changes task granularity — checkboxes stay atomic and TDD runs per task; only review
@@ -68,12 +68,12 @@ decision **stated in full** — at this moment it exists nowhere on disk; the en
68
68
  `**Resolution**` block is only written at step 4 — plus the `discoveries.md` entry
69
69
  **by path** for the surrounding context (it reads the entry itself):
70
70
 
71
- | Artifact changed by the decision | Owner to invoke |
72
- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
73
- | `spec/requirements.md`, `spec/design.md`, `spec/tasks.md` | **Spec Author** |
74
- | Implementation code, `progress.md`, `notes.md` | **Implementer** (often just the resumed sub-agent) |
75
- | A child issue's trace lines, a triage issue's fix plan, the parent partition-plan issue | **Orchestrator** (you) — `gh issue edit --body-file`, read-modify-write (an oversize discovery answered "partition": `.claude/agents/partition.md`) |
76
- | `docs/adr/NNNN-<slug>.md`, `docs/architecture.md`, `docs/playbooks/` | **Architect** (on-demand) — `write-adr` to record the decision, `update-architecture` to keep the map true, `playbook-iterate` for a `T6 PLAYBOOK_CONFLICT` |
71
+ | Artifact changed by the decision | Owner to invoke |
72
+ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
73
+ | `spec/requirements.md`, `spec/design.md`, `spec/tasks.md` | **Spec Author** — except a task's checkbox: `- [ ]` → `- [x]` is the Implementer's done-mark at green (the Orchestrator's on a checkpoint `ok`), never a spec edit |
74
+ | Implementation code, `progress.md`, `notes.md` | **Implementer** (often just the resumed sub-agent) |
75
+ | A child issue's trace lines, a triage issue's fix plan, the parent partition-plan issue | **Orchestrator** (you) — `gh issue edit --body-file`, read-modify-write (an oversize discovery answered "partition": `.claude/agents/partition.md`) |
76
+ | `docs/adr/NNNN-<slug>.md`, `docs/architecture.md`, `docs/playbooks/` | **Architect** (on-demand) — `write-adr` to record the decision, `update-architecture` to keep the map true, `playbook-iterate` for a `T6 PLAYBOOK_CONFLICT` |
77
77
 
78
78
  Not every resolution needs an artifact update first. If the decision is simply "do X"
79
79
  with no change to the contract, skip straight to recording it and resuming. If it