@lemoncode/lemony 0.5.1 → 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 +3 -1
- package/catalog/harness.config.schema.json +4 -0
- package/catalog/hooks/init.sh +60 -9
- package/catalog/hooks/lib/live-branch.sh +37 -0
- package/catalog/hooks/lib/merge-pr.sh +14 -4
- 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 +15 -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 +8 -2
- package/dist/cli.mjs +1694 -513
- 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
|
|
@@ -26,6 +26,8 @@ The design tool is a **projection** of the canonical JSON, never a peer source o
|
|
|
26
26
|
Detect the tool at runtime: read the `com.lemony.design-tool` binding at the root of
|
|
27
27
|
`docs/design-tokens.json`; if no tool is declared, the project is pure-code and there is
|
|
28
28
|
nothing to sync. If the declared tool's MCP server is not connected, skip gracefully with a
|
|
29
|
-
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`.
|
|
30
32
|
|
|
31
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,12 +65,37 @@ 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\`.")
|
|
51
95
|
elif [ ! -f "$CONFIG" ]; then
|
|
52
96
|
# 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
|
|
54
|
-
#
|
|
97
|
+
# until something wrote to it. `-e`/`-f` follow a symlink, so a link to one of
|
|
98
|
+
# these lands here.
|
|
55
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.")
|
|
56
100
|
elif [ ! -r "$CONFIG" ]; then
|
|
57
101
|
# A file awk cannot open failed the key check below, so the boot blamed missing keys
|
|
@@ -147,12 +191,18 @@ fi
|
|
|
147
191
|
# ladder, and following `git pull --rebase` with `rebase.autostash` set flattens
|
|
148
192
|
# that index — the documented rollback then reverts to the last commit and the
|
|
149
193
|
# group's whole uncommitted work is gone. So on any other branch: silence.
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
|
|
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")"
|
|
154
203
|
DEFAULT_BRANCH="$(git symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')"
|
|
155
|
-
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
|
|
156
206
|
BEHIND="$(git rev-list --count "HEAD..origin/$DEFAULT_BRANCH" 2>/dev/null || echo 0)"
|
|
157
207
|
if [ "${BEHIND:-0}" -gt 0 ]; then
|
|
158
208
|
WARNINGS+=("local branch is $BEHIND commit(s) behind origin/$DEFAULT_BRANCH. Consider \`git pull --rebase\` before starting.")
|
|
@@ -225,7 +275,8 @@ last_close_ts: ""
|
|
|
225
275
|
Per-dev pointer (gitignored). The lifecycle hooks read \`session_start_ts\`
|
|
226
276
|
to compute \`session_active_h\` and reset it on each SessionStart that orients.
|
|
227
277
|
The active task and its branch are not recorded here — \`session-close.sh\`
|
|
228
|
-
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
|
|
229
280
|
the narrative resume lives under \`sessions/<user>/\` (written by \`/pause\`).
|
|
230
281
|
EOF
|
|
231
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
|
|
@@ -402,7 +403,14 @@ fi
|
|
|
402
403
|
# before merge is standing practice and must not invalidate verdicts: a clean
|
|
403
404
|
# base advance changes the tree but not the PR's own diff (verified
|
|
404
405
|
# empirically, including that a base edit to a file the PR also touched DOES
|
|
405
|
-
# 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
|
|
406
414
|
# load-bearing: --raw abbreviates blob OIDs to a repo-size-dependent width by
|
|
407
415
|
# default, so two machines could digest different bytes for the same diff.
|
|
408
416
|
# The parity of this command with the Reviewer's recipe (reviewer.md) is
|
|
@@ -519,9 +527,11 @@ EOF
|
|
|
519
527
|
fi
|
|
520
528
|
# Canonical fingerprint command — byte-for-byte the Reviewer's recipe
|
|
521
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.
|
|
522
532
|
cur_fp="$(
|
|
523
533
|
set -o pipefail
|
|
524
|
-
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' \
|
|
525
535
|
| git -C "$ROOT" hash-object --stdin
|
|
526
536
|
)"
|
|
527
537
|
if [ $? -ne 0 ] || [ -z "$cur_fp" ]; then
|
|
@@ -537,7 +547,7 @@ EOF
|
|
|
537
547
|
echo "A human's informed \"merge anyway\" re-runs with --force."
|
|
538
548
|
fail 40
|
|
539
549
|
fi
|
|
540
|
-
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."
|
|
541
551
|
fi
|
|
542
552
|
|
|
543
553
|
# Pin the merge to the exact head this guard verified: a push landing
|
|
@@ -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
|
|
|
@@ -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 global events; `session_closed` carries it when
|
|
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 the live branch is a `harness/<id>-<slug>` or `harness/closeout-<id>` 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
|
|
|
@@ -115,8 +115,9 @@ forward-compatible — readers dispatch on `type` and ignore unknowns.
|
|
|
115
115
|
|
|
116
116
|
Emitted by `session-close.sh` on `SessionEnd` or `/pause` (manual). One per
|
|
117
117
|
session. The envelope's `task_id` is derived from the live branch at close time
|
|
118
|
-
(`harness/<id>-<slug>` → `<id>`)
|
|
119
|
-
detached
|
|
118
|
+
(`harness/<id>-<slug>` or `harness/closeout-<id>` → `<id>`) — while a rebase has
|
|
119
|
+
HEAD detached, the branch being rebased; a session closed on the default branch or
|
|
120
|
+
any other detached HEAD carries none.
|
|
120
121
|
|
|
121
122
|
| Field | Type | Required | Axis | Notes |
|
|
122
123
|
| ------------------ | ------- | -------- | --------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
@@ -148,17 +149,18 @@ Emitted by the Orchestrator when it transitions `spec-in-progress → spec-ready
|
|
|
148
149
|
|
|
149
150
|
### 4. `task_done` _(P5)_
|
|
150
151
|
|
|
151
|
-
Emitted by the Orchestrator at closeout
|
|
152
|
-
|
|
152
|
+
Emitted by the Orchestrator at closeout **finalize** — after the closeout PR,
|
|
153
|
+
which archives the spec and drops the task's `progress.md`, has merged.
|
|
153
154
|
|
|
154
|
-
| Field | Type | Required | Axis | Notes
|
|
155
|
-
| ------------------- | ------ | -------- | --------------- |
|
|
156
|
-
| `task_id` | string | yes | `identity` | Required for this type.
|
|
157
|
-
| `level` | string | yes | `internal-enum` | `L1` \| `L2` \| `L3` — the task-fit dial value used.
|
|
158
|
-
| `cycle_time_h` | number | yes | `metric` | Wall-clock hours from issue creation to merge. ≥ 0, finite.
|
|
159
|
-
| `review_rejections` | number | yes | `metric` | Count of `review_rejected` events for this `task_id` (≥ 0, int).
|
|
160
|
-
| `mode` | string | no | `internal-enum` | `all_at_once` \| `step_by_step` — the mode chosen at the L1 approval gate. **Absent on L2** (the question only exists where `tasks.md` does).
|
|
161
|
-
| `steps` | number | no | `metric` | Count of `
|
|
155
|
+
| Field | Type | Required | Axis | Notes |
|
|
156
|
+
| ------------------- | ------ | -------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
157
|
+
| `task_id` | string | yes | `identity` | Required for this type. |
|
|
158
|
+
| `level` | string | yes | `internal-enum` | `L1` \| `L2` \| `L3` — the task-fit dial value used. |
|
|
159
|
+
| `cycle_time_h` | number | yes | `metric` | Wall-clock hours from issue creation to merge. ≥ 0, finite. |
|
|
160
|
+
| `review_rejections` | number | yes | `metric` | Count of `review_rejected` events for this `task_id` (≥ 0, int). |
|
|
161
|
+
| `mode` | string | no | `internal-enum` | `all_at_once` \| `step_by_step` — the mode chosen at the L1 approval gate. **Absent on L2** (the question only exists where `tasks.md` does), and on an L1 task whose gate choice closeout could not recover. |
|
|
162
|
+
| `steps` | number | no | `metric` | Count of **distinct steps** (distinct `step` values) across this task's `step_completed` events (≥ 1, int). Only meaningful when `mode` is `step_by_step`; < total groups after a mid-task downgrade. Before 0.6.0 it counted every event — see [history](tier2-events-history.md). |
|
|
163
|
+
| `checkpoints` | number | no | `metric` | Count of `step_completed` events for this task (≥ 1, int) — one per resolved human checkpoint, so a step sent back counts again. Sent together with `steps`; `checkpoints − steps` is the task's extra checkpoint rounds — normally its `changes` answers at step checkpoints. Not counted: any human gate after a downgrade to all-at-once (it emits nothing), and change requests at the merge gate. |
|
|
162
164
|
|
|
163
165
|
### 5. `review_rejected` _(P5)_
|
|
164
166
|
|
|
@@ -211,7 +213,9 @@ post-merge / production signal; conflating them would dirty the post-merge metri
|
|
|
211
213
|
### 9. `step_completed` _(step-by-step mode)_
|
|
212
214
|
|
|
213
215
|
Emitted by the Orchestrator each time a human checkpoint **resolves** in
|
|
214
|
-
step-by-step mode (L1 opt-in, chosen at the approval gate)
|
|
216
|
+
step-by-step mode (L1 opt-in, chosen at the approval gate), through the
|
|
217
|
+
`checkpoint` verb that also makes the resolution's commits — or by hand,
|
|
218
|
+
late, when that call never ran for an answer. One event per
|
|
215
219
|
checkpoint, not per step: a step the human sends back ("changes") emits again
|
|
216
220
|
when it re-checkpoints, with the same `step`. This is the signal that justifies
|
|
217
221
|
(or condemns) the mode — the rate of checkpoints that catch things, and where
|
|
@@ -272,7 +276,11 @@ A writer (the `lemony emit` CLI):
|
|
|
272
276
|
2. Merges per-type fields into the same top level (no nested `payload`).
|
|
273
277
|
3. Validates against the Zod schema for `type` (each schema is `.strict()`,
|
|
274
278
|
so an unknown key — typically a typo'd `--task-iid` flag — **rejects**
|
|
275
|
-
loud).
|
|
279
|
+
loud). `task_done` also rejects step counts that contradict each other:
|
|
280
|
+
`steps`/`checkpoints` on a task whose `mode` is not `step_by_step`,
|
|
281
|
+
`checkpoints` without `steps`, and `checkpoints < steps`. These rules run at
|
|
282
|
+
emit time only — readers do not re-check lines already written. Never writes
|
|
283
|
+
a partial line.
|
|
276
284
|
4. Appends the JSON line via `fs.appendFile` (single `O_APPEND` `write(2)`,
|
|
277
285
|
POSIX-atomic up to `PIPE_BUF`) to `.claude/state/events.jsonl`. Creates the
|
|
278
286
|
file (and parent dir, scaffolded by `install`) when missing.
|
|
@@ -10,7 +10,9 @@ invoked-by: [implementer]
|
|
|
10
10
|
# Build UI
|
|
11
11
|
|
|
12
12
|
The implementer's **build method**: turn a `ui-handoff.md` (the design contract) plus
|
|
13
|
-
`docs/design-tokens.json` (the token source of truth
|
|
13
|
+
`docs/design-tokens.json` (the token source of truth — or the path `design_tokens.file` names
|
|
14
|
+
in `harness.config.yml`, when set; read that file wherever this skill says
|
|
15
|
+
`docs/design-tokens.json`) into UI code that applies the
|
|
14
16
|
project's tokens correctly, carries the design's point of view instead of generic
|
|
15
17
|
defaults, and is accessible by construction.
|
|
16
18
|
|
|
@@ -44,7 +46,8 @@ v3→v4 jump turns it into a lie); the live model does not. So this skill names
|
|
|
44
46
|
|
|
45
47
|
## Tokens-as-code — the contract
|
|
46
48
|
|
|
47
|
-
`docs/design-tokens.json` is the **single, client-owned source of truth
|
|
49
|
+
`docs/design-tokens.json` is the **single, client-owned source of truth** (or the path
|
|
50
|
+
`harness.config.yml` names in `design_tokens.file`, when set — read that file instead), a 3-tier
|
|
48
51
|
W3C-DTCG model: **primitive** (raw scale values) → **semantic** (intent: `color.surface`,
|
|
49
52
|
`space.inset.md`) → **component** (a component's specific slots). Two rules hold on every
|
|
50
53
|
stack:
|