@lemoncode/lemony 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/PRIVACY.md +147 -0
  3. package/README.md +189 -0
  4. package/catalog/VERSION +1 -0
  5. package/catalog/agents/README.md +29 -0
  6. package/catalog/agents/architect.md +81 -0
  7. package/catalog/agents/fit-assessment.md +94 -0
  8. package/catalog/agents/implementer.md +67 -0
  9. package/catalog/agents/orchestrator.md +627 -0
  10. package/catalog/agents/reviewer.md +124 -0
  11. package/catalog/agents/spec-author.md +69 -0
  12. package/catalog/agents/ui-designer.md +25 -0
  13. package/catalog/commands/add-capability.md +69 -0
  14. package/catalog/commands/bypass.md +40 -0
  15. package/catalog/commands/define.md +24 -0
  16. package/catalog/commands/hotfix.md +47 -0
  17. package/catalog/commands/pause.md +52 -0
  18. package/catalog/commands/resume.md +56 -0
  19. package/catalog/commands/spinoff.md +59 -0
  20. package/catalog/commands/triage.md +24 -0
  21. package/catalog/harness.config.schema.json +116 -0
  22. package/catalog/hooks/README.md +56 -0
  23. package/catalog/hooks/init.sh +281 -0
  24. package/catalog/hooks/lib/lemony.sh +41 -0
  25. package/catalog/hooks/lib/playbook-scan.sh +394 -0
  26. package/catalog/hooks/lib/transcript-grep.sh +56 -0
  27. package/catalog/hooks/require-playbook.sh +97 -0
  28. package/catalog/hooks/session-close.sh +232 -0
  29. package/catalog/hooks/suggest-playbook.sh +72 -0
  30. package/catalog/playbook-format.md +198 -0
  31. package/catalog/schemas/README.md +13 -0
  32. package/catalog/schemas/tier2-events-history.md +104 -0
  33. package/catalog/schemas/tier2-events.md +286 -0
  34. package/catalog/skills/README.md +62 -0
  35. package/catalog/skills/bootstrap-architecture/SKILL.md +78 -0
  36. package/catalog/skills/code-explorer/SKILL.md +76 -0
  37. package/catalog/skills/grill-with-docs/ADR-FORMAT.md +49 -0
  38. package/catalog/skills/grill-with-docs/CONTEXT-FORMAT.md +77 -0
  39. package/catalog/skills/grill-with-docs/SKILL.md +270 -0
  40. package/catalog/skills/grill-with-docs/reference.md +236 -0
  41. package/catalog/skills/mutation-testing/SKILL.md +84 -0
  42. package/catalog/skills/note-side-finding/SKILL.md +89 -0
  43. package/catalog/skills/playbook-iterate/SKILL.md +78 -0
  44. package/catalog/skills/prd-to-spec/SKILL.md +181 -0
  45. package/catalog/skills/raise-discovery/SKILL.md +112 -0
  46. package/catalog/skills/resolve-discovery/SKILL.md +123 -0
  47. package/catalog/skills/review-pr/SKILL.md +106 -0
  48. package/catalog/skills/review-pr/reference.md +105 -0
  49. package/catalog/skills/security-review/SKILL.md +90 -0
  50. package/catalog/skills/senior-review/SKILL.md +99 -0
  51. package/catalog/skills/silent-failure-hunter/SKILL.md +76 -0
  52. package/catalog/skills/spec-compliance-check/SKILL.md +74 -0
  53. package/catalog/skills/spec-to-issue/SKILL.md +88 -0
  54. package/catalog/skills/task-closeout/SKILL.md +229 -0
  55. package/catalog/skills/tdd/SKILL.md +171 -0
  56. package/catalog/skills/test-gap-report/SKILL.md +71 -0
  57. package/catalog/skills/triage-issue/SKILL.md +102 -0
  58. package/catalog/skills/update-architecture/SKILL.md +69 -0
  59. package/catalog/skills/verify/SKILL.md +90 -0
  60. package/catalog/skills/write-adr/SKILL.md +77 -0
  61. package/catalog/templates/README.md +32 -0
  62. package/catalog/templates/claude-code/.claude/settings.json.tpl +34 -0
  63. package/catalog/templates/claude-code/agents.md.tpl +109 -0
  64. package/catalog/templates/claude-code/docs/playbooks/README.md.tpl +96 -0
  65. package/catalog/templates/claude-code/harness.config.yml.tpl +59 -0
  66. package/catalog/templates/claude-code/state/history.md.tpl +6 -0
  67. package/dist/cli.mjs +5691 -0
  68. package/package.json +80 -0
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env bash
2
+ # Lemony — Claude Code SessionEnd hook + `/pause` adaptor (decision #52).
3
+ #
4
+ # Fires:
5
+ # • automatically on SessionEnd (no `--manual`) → auto-close path: writes a
6
+ # skeleton `sessions/<user>/<ts>-auto.md`.
7
+ # • from `/pause` with `--manual` → Claude has already written
8
+ # `sessions/<user>/<ts>-<topic>.md`; the hook only emits + updates pointers.
9
+ #
10
+ # Both paths emit a `session_closed` event and print `git status --porcelain`
11
+ # without auto-committing (the user decides what to commit).
12
+
13
+ set -u
14
+
15
+ MANUAL=0
16
+ for arg in "$@"; do
17
+ case "$arg" in
18
+ --manual) MANUAL=1 ;;
19
+ esac
20
+ done
21
+
22
+ REPO_ROOT="$(pwd)"
23
+ INPUT=""
24
+ if [ "$MANUAL" -eq 0 ]; then
25
+ # Claude Code pipes a JSON payload on SessionEnd. The hook reads stdin once;
26
+ # if no payload was sent (manual invocation, missing pipe) we proceed with
27
+ # `reason=other`.
28
+ if [ -t 0 ]; then
29
+ INPUT=""
30
+ else
31
+ INPUT="$(cat || true)"
32
+ fi
33
+ fi
34
+
35
+ # ── Resolve the user (filename + envelope `user` derive from git config) ───
36
+ EMAIL="$(git config user.email 2>/dev/null || true)"
37
+ if [ -z "$EMAIL" ]; then
38
+ # Self-identify so a maintainer reading the log can locate the source
39
+ # without a grep — same treatment as the playbook hooks.
40
+ echo "session-close (${BASH_SOURCE[0]}): git config user.email is empty — skipping (init.sh would have blocked the session, so this is a defensive guard)." >&2
41
+ exit 0
42
+ fi
43
+ # `%%@*` strips from the FIRST `@` (parity with status.ts's `email.split('@')[0]`),
44
+ # not `%@*` which strips from the last — they diverge only on a malformed multi-`@`
45
+ # address, but the pointer filename must match what status.ts reads back.
46
+ USER_SLUG="${EMAIL%%@*}"
47
+ # The slug becomes a filename — bail on anything path-significant so a crafted
48
+ # user.email (`../../x@y`) can't make CURRENT_PATH escape `.claude/state/`.
49
+ # Mirrors the readPointer guard in status.ts; fail-open like the empty-email case.
50
+ case "$USER_SLUG" in
51
+ '' | */* | *'\'* | *..*)
52
+ echo "session-close (${BASH_SOURCE[0]}): git config user.email derives a path-unsafe slug — skipping." >&2
53
+ exit 0
54
+ ;;
55
+ esac
56
+
57
+ # ── Timestamps ─────────────────────────────────────────────────────────────
58
+ NOW_EPOCH="$(date -u +%s)"
59
+ NOW_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
60
+
61
+ # ── Read current-<user>.md frontmatter ─────────────────────────────────────
62
+ # An awk scrape of the leading frontmatter block — works for the shallow
63
+ # scalars the harness writes, fails gracefully on anything richer. yq was
64
+ # removed harness-wide (#41): jq is the sole hard dependency; awk/grep/git
65
+ # (preinstalled) cover the rest.
66
+ CURRENT_PATH="$REPO_ROOT/.claude/state/current-$USER_SLUG.md"
67
+ SESSION_START_TS=""
68
+ ACTIVE_TASK=""
69
+ TASK_BRANCH=""
70
+ read_frontmatter_field() {
71
+ local field="$1"
72
+ awk -v field="$field" '
73
+ /^---/ { block++; next }
74
+ block == 1 {
75
+ n = index($0, ":")
76
+ if (n > 0) {
77
+ key = substr($0, 1, n - 1)
78
+ sub(/^[[:space:]]+|[[:space:]]+$/, "", key)
79
+ if (key == field) {
80
+ value = substr($0, n + 1)
81
+ sub(/^[[:space:]]+|[[:space:]]+$/, "", value)
82
+ gsub(/"/, "", value)
83
+ print value
84
+ exit
85
+ }
86
+ }
87
+ }
88
+ ' "$CURRENT_PATH" 2>/dev/null
89
+ }
90
+ if [ -f "$CURRENT_PATH" ]; then
91
+ 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
+ fi
95
+
96
+ # Fallback: a session with no recorded start defaults to "started now" so the
97
+ # math is well-defined; duration becomes 0 hours, which the schema accepts.
98
+ if [ -z "$SESSION_START_TS" ]; then
99
+ SESSION_START_TS="$NOW_ISO"
100
+ fi
101
+
102
+ # Convert SESSION_START_TS to epoch (portable between GNU and BSD date).
103
+ iso_to_epoch() {
104
+ local ts="$1"
105
+ # Strip fractional seconds and the trailing Z for BSD `date -j -f`.
106
+ local clean="${ts%%.*}"
107
+ clean="${clean%Z}"
108
+ if date -u -d "@0" +%s >/dev/null 2>&1; then
109
+ date -u -d "$ts" +%s
110
+ else
111
+ date -juf "%Y-%m-%dT%H:%M:%S" "$clean" +%s 2>/dev/null
112
+ fi
113
+ }
114
+ START_EPOCH="$(iso_to_epoch "$SESSION_START_TS")"
115
+ if [ -z "${START_EPOCH:-}" ]; then
116
+ START_EPOCH="$NOW_EPOCH"
117
+ fi
118
+ DURATION_H="$(awk -v start="$START_EPOCH" -v now="$NOW_EPOCH" 'BEGIN { printf "%.4f", (now - start) / 3600.0 }')"
119
+ # Floor at zero — a clock skew between session_start_ts and now must not yield
120
+ # a negative duration that fails Zod validation.
121
+ if awk -v d="$DURATION_H" 'BEGIN { exit !(d < 0) }'; then
122
+ DURATION_H="0.0000"
123
+ fi
124
+
125
+ # ── Resolve reason + auto_close flag ───────────────────────────────────────
126
+ if [ "$MANUAL" -eq 1 ]; then
127
+ REASON="manual"
128
+ AUTO_CLOSE="false"
129
+ else
130
+ REASON="$(printf '%s' "$INPUT" \
131
+ | grep -o '"reason"[[:space:]]*:[[:space:]]*"[^"]*"' \
132
+ | head -1 \
133
+ | sed 's/.*"\([^"]*\)"$/\1/')"
134
+ REASON="${REASON:-other}"
135
+ AUTO_CLOSE="true"
136
+ fi
137
+
138
+ # ── Emit the event via the CLI (Zod-validates the line) ────────────────────
139
+ EMIT_ARGS=(
140
+ emit session_closed
141
+ --session-start-ts="$SESSION_START_TS"
142
+ --session-active-h="$DURATION_H"
143
+ --reason="$REASON"
144
+ --auto-close="$AUTO_CLOSE"
145
+ )
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
149
+ EMIT_ARGS+=(--task-id="$ACTIVE_TASK")
150
+ fi
151
+ # Resolve the CLI via the launcher (local devDependency → global → fail-fast, #107/#113)
152
+ # rather than a bare `command -v` on PATH, which an npx install or an fnm Node
153
+ # switch leaves empty. Fail open — a missing launcher or a failed emit warns and
154
+ # continues; the session must never be blocked by telemetry.
155
+ LF_CLI="$REPO_ROOT/.claude/hooks/lib/lemony.sh"
156
+ if [ -x "$LF_CLI" ]; then
157
+ "$LF_CLI" "${EMIT_ARGS[@]}" || \
158
+ echo "session-close: \`lemony emit\` failed (continuing — see previous error)." >&2
159
+ else
160
+ echo "session-close: CLI launcher missing ($LF_CLI) — skipping event emit. Re-run \`lemony install\`." >&2
161
+ fi
162
+
163
+ # ── Fire-and-forget telemetry send (#225, Phase 1 walking skeleton) ─────────
164
+ # Flush the unsent tail of events.jsonl to the ingest Worker. Detached in a
165
+ # subshell (`( … & )`) so SessionEnd never waits on the network; `nohup` lets it
166
+ # outlive a SIGHUP if the session tears down its process group before the send
167
+ # completes. The CLI has its own hard per-request timeout (D4) and never throws —
168
+ # output is discarded and a failure leaves the cursor untouched, so the next run
169
+ # (or init.sh catch-up, #228) retries the same bytes. No-op when no endpoint is
170
+ # configured. Telemetry must never block or break session exit.
171
+ if [ -x "$LF_CLI" ]; then
172
+ ( nohup "$LF_CLI" telemetry send >/dev/null 2>&1 & )
173
+ fi
174
+
175
+ # ── Auto-close path: write the session skeleton ───────────────────────────
176
+ if [ "$MANUAL" -eq 0 ]; then
177
+ SESSIONS_DIR="$REPO_ROOT/.claude/state/sessions/$USER_SLUG"
178
+ mkdir -p "$SESSIONS_DIR"
179
+ SESSION_FILE="$SESSIONS_DIR/$NOW_ISO-auto.md"
180
+ cat > "$SESSION_FILE" <<EOF
181
+ ---
182
+ session_start_ts: $SESSION_START_TS
183
+ session_close_ts: $NOW_ISO
184
+ session_active_h: $DURATION_H
185
+ active_task: ${ACTIVE_TASK:-null}
186
+ branch: ${TASK_BRANCH:-(unknown)}
187
+ reason: $REASON
188
+ auto_close: true
189
+ ---
190
+
191
+ # Session — auto-close ($NOW_ISO)
192
+
193
+ _Closed by Claude Code's SessionEnd event with no narrative model run._
194
+ _If you want a written resume, use \`/pause\` before exiting next time._
195
+ EOF
196
+ fi
197
+
198
+ # ── Update current-<user>.md frontmatter pointers ──────────────────────────
199
+ # Blank session_start_ts (the next SessionStart re-stamps it) and record the
200
+ # close ts. awk in-place rewrite of the frontmatter scalars — mirrors the read
201
+ # fallback above and the session_start_ts rewrite in init.sh (#41: no yq).
202
+ if [ -f "$CURRENT_PATH" ]; then
203
+ # Rewrite each scalar if present; if a legacy/hand-edited pointer lacks the
204
+ # line, append it just before the closing `---` (parity with the `yq -i` set
205
+ # this replaced — `block == 2` is the frontmatter's closing fence).
206
+ # Write through a mktemp file, not a predictable `$CURRENT_PATH.tmp`: the
207
+ # latter is a known path an attacker could pre-plant as a symlink for the
208
+ # redirect to follow. `mktemp` refuses to reuse an existing path, and `mv`
209
+ # over CURRENT_PATH itself safely replaces a symlink with a regular file.
210
+ if tmp="$(mktemp "$REPO_ROOT/.claude/state/.ptr.XXXXXX")"; then
211
+ awk -v ts="$NOW_ISO" '
212
+ /^---/ {
213
+ block++
214
+ if (block == 2) {
215
+ if (!seen_start) print "session_start_ts: \"\""
216
+ if (!seen_close) print "last_close_ts: " ts
217
+ }
218
+ print
219
+ next
220
+ }
221
+ block == 1 && /^session_start_ts:/ { print "session_start_ts: \"\""; seen_start = 1; next }
222
+ block == 1 && /^last_close_ts:/ { print "last_close_ts: " ts; seen_close = 1; next }
223
+ { print }
224
+ ' "$CURRENT_PATH" > "$tmp" && mv "$tmp" "$CURRENT_PATH" || rm -f "$tmp"
225
+ fi
226
+ fi
227
+
228
+ # ── Print git status without committing ────────────────────────────────────
229
+ echo "git status --porcelain ($NOW_ISO):"
230
+ git -C "$REPO_ROOT" status --porcelain 2>/dev/null || true
231
+
232
+ exit 0
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # Lemony — Claude Code UserPromptSubmit hook (playbook suggestion).
3
+ #
4
+ # Non-blocking. Scans the user's freshly submitted prompt and, when it mentions
5
+ # a topic covered by a playbook (per the `keywords` regex list in the
6
+ # frontmatter) that hasn't been read yet in this conversation, emits a
7
+ # <system-reminder> on stdout — Claude Code attaches it as guidance for the
8
+ # agent's next turn.
9
+
10
+ set -uo pipefail
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+
14
+ # shellcheck source=lib/playbook-scan.sh
15
+ source "$SCRIPT_DIR/lib/playbook-scan.sh"
16
+ # shellcheck source=lib/transcript-grep.sh
17
+ source "$SCRIPT_DIR/lib/transcript-grep.sh"
18
+
19
+ if [ -t 0 ]; then
20
+ PAYLOAD=""
21
+ else
22
+ PAYLOAD="$(cat 2>/dev/null || true)"
23
+ fi
24
+ [ -n "$PAYLOAD" ] || exit 0
25
+
26
+ command -v jq >/dev/null 2>&1 || exit 0
27
+
28
+ PROMPT="$(printf '%s' "$PAYLOAD" | jq -r '.prompt // empty')"
29
+ [ -n "$PROMPT" ] || exit 0
30
+
31
+ REPO_ROOT="$(pwd)"
32
+ HOME_DIR="${HOME:-$REPO_ROOT}"
33
+
34
+ playbook_scan_for_prompt "$REPO_ROOT" "$HOME_DIR" "$PROMPT"
35
+ if [ ${#MATCHED_KEYWORD_PLAYBOOKS[@]} -eq 0 ]; then
36
+ exit 0
37
+ fi
38
+
39
+ TRANSCRIPT_PATH="$(printf '%s' "$PAYLOAD" | jq -r '.transcript_path // empty')"
40
+
41
+ NEW_SUGGESTIONS=()
42
+ for pb in "${MATCHED_KEYWORD_PLAYBOOKS[@]}"; do
43
+ if [ -n "$TRANSCRIPT_PATH" ] && [ -r "$TRANSCRIPT_PATH" ]; then
44
+ if transcript_contains_playbook "$TRANSCRIPT_PATH" "$pb"; then
45
+ continue
46
+ fi
47
+ fi
48
+ NEW_SUGGESTIONS+=("$pb")
49
+ done
50
+
51
+ if [ ${#NEW_SUGGESTIONS[@]} -eq 0 ]; then
52
+ exit 0
53
+ fi
54
+
55
+ {
56
+ printf '<system-reminder>\n'
57
+ printf 'Topic detection on your prompt suggests reading these playbook(s) before planning:\n\n'
58
+ for pb in "${NEW_SUGGESTIONS[@]}"; do
59
+ printf ' - %s\n' "$pb"
60
+ done
61
+ printf '\nUse the Read tool on each. Playbooks contain this project'\''s canonical patterns\n'
62
+ printf 'for that kind of work — reading them avoids re-discovering already-solved problems.\n'
63
+ printf '\nIf a playbook clearly does not apply to the current context, you may skip it —\n'
64
+ printf 'this hook errs on the side of suggesting too much rather than too little.\n'
65
+ # Name the actual file the agent / human reader can inspect (same treatment
66
+ # as require-playbook.sh — works both at an installed `.claude/hooks/`
67
+ # location and when the hook runs from the vendor repo).
68
+ printf '\n(Hook: %s)\n' "${BASH_SOURCE[0]}"
69
+ printf '</system-reminder>\n'
70
+ }
71
+
72
+ exit 0
@@ -0,0 +1,198 @@
1
+ # Playbook format — specification
2
+
3
+ The canonical structure for a **playbook** (the renamed _blueprint_): a project-agnostic
4
+ document describing **how to build** a category of software (a backend service, an SPA, a
5
+ testing strategy, a release flow). Playbooks live in the **client's** repo (and an optional
6
+ global layer); the vendor ships **no concrete playbooks** (decision #8 — client-owned).
7
+
8
+ The two enforcement hooks shipped in P5 slice 2 (`require-playbook.sh`,
9
+ `suggest-playbook.sh`) read this format. Skills that consult playbooks read it too.
10
+
11
+ ---
12
+
13
+ ## File layout
14
+
15
+ A playbook is a markdown file with leading YAML frontmatter, located by **topic
16
+ filename** across two layers:
17
+
18
+ | Layer | Path | Notes |
19
+ | --------------- | -------------------------------- | ------------------------ |
20
+ | Project (local) | `docs/playbooks/<topic>.md` | Committed with the repo. |
21
+ | Global | `~/.claude/playbooks/<topic>.md` | Personal, generic. |
22
+
23
+ The **filename stem is the topic** — there is no index. Lookup is **local → global**: if
24
+ both layers define `<topic>.md`, the **local one wins** and the global one is ignored for
25
+ that topic. A missing playbook is **not an error** — agents that consult one degrade
26
+ gracefully when it is absent.
27
+
28
+ > Both layers are **configurable** via `harness.config.yml`: set `paths.playbooks`
29
+ > (repo-relative) to relocate the local layer, and `paths.playbooks_global`
30
+ > (absolute or `~`-based) to relocate the global one. The hooks read those keys
31
+ > directly on each fire; editing them takes effect on the next fire (no install
32
+ > or restart). Omit a key to keep the default shown above.
33
+
34
+ ---
35
+
36
+ ## Frontmatter
37
+
38
+ ```yaml
39
+ ---
40
+ name: testing
41
+ applies_to:
42
+ - '**/*.spec.ts'
43
+ - '**/*.spec.tsx'
44
+ - '**/*.bench.ts'
45
+ keywords:
46
+ - 'vitest'
47
+ - 'test runner'
48
+ - 'AAA pattern'
49
+ status: active
50
+ ---
51
+ ```
52
+
53
+ ### Fields
54
+
55
+ | Field | Required | Type | Purpose |
56
+ | ------------ | -------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------- |
57
+ | `name` | yes | kebab-case string | Identifier. Must equal the filename stem (`testing.md` → `name: testing`). |
58
+ | `applies_to` | no | list of bash globs | Files whose `Write`/`Edit` should be **blocked** until this playbook is read. Empty/absent → never blocks. |
59
+ | `keywords` | no | list of regex strings | Prompt patterns that **suggest** reading this playbook (non-blocking). Empty/absent → never suggests. |
60
+ | `status` | no | `draft \| active \| deprecated` | Author intent. Hooks ignore status today; reserved for skills/tooling. |
61
+
62
+ `applies_to` and `keywords` are **independently optional**: a playbook can be require-only
63
+ (globs but no keywords), suggest-only (keywords but no globs), both, or neither (a doc
64
+ agents read on demand but is not enforced).
65
+
66
+ > **Frontmatter subset (awk-parsed).** The hooks read frontmatter with a single
67
+ > `awk` pass over every discovered playbook — no `yq` (decision #29: one `yq`
68
+ > fork per playbook blew the <50ms p99 budget, and its only edge here — YAML
69
+ > escape-sequence fidelity — is unused because keywords are constrained to the
70
+ > awk subset anyway). Supported: block lists (`field:\n - item`) and simple
71
+ > inline lists (`field: [a, b]`) of scalar strings, optionally single- or
72
+ > double-quoted (quotes are stripped). The reader does **not** unescape YAML double-quote
73
+ > sequences (`"\b"` stays the two characters `\b`, not a backspace) and does
74
+ > **not** respect quotes when splitting an inline list on commas — a
75
+ > `keywords: ["foo, bar"]` splits into `"foo` and `bar"`. Author keywords
76
+ > within this subset: no `\b` word boundaries, no comma-bearing strings.
77
+ > (`yq` has since left the harness entirely — decision #41 moved `init.sh` config
78
+ > validation and `session-close.sh` state to pure bash, so `jq` is the only hard
79
+ > dependency.)
80
+
81
+ ### `applies_to` semantics
82
+
83
+ Each pattern is a glob-style string, transliterated to a POSIX-ERE regex and
84
+ matched against the **full file path passed to Write/Edit** with bash
85
+ `[[ =~ ]]`. The regex path (rather than `shopt -s globstar`) keeps the hooks
86
+ runnable on stock macOS `/bin/bash` 3.2. Supported subset:
87
+
88
+ | Glob | Means |
89
+ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
90
+ | `*` | any sequence within a single path component (no `/`) |
91
+ | `**/` | zero or more leading path components (so `**/x` matches `x` at root too) |
92
+ | `**` | any sequence including `/` |
93
+ | `?` | any single non-separator character |
94
+ | `[abc]` | character class (passthrough) |
95
+ | `[!abc]` | negated class (translated to ERE `[^abc]`) |
96
+ | `{a,b,c}` | alternation, becomes `(a\|b\|c)`. Nested braces and **empty alternatives** (`{,X}`, `{X,}`, `{,}`) are not supported — BSD ERE rejects empty branches; the playbook silently fails to match. |
97
+ | `\X` | literal `X` (escape) |
98
+
99
+ Examples (with the absolute path Claude Code passes in mind):
100
+
101
+ | Pattern | Matches |
102
+ | ----------------------------------- | --------------------------------------------------------------- |
103
+ | `**/*.spec.ts` | any `*.spec.ts` at any depth, including root |
104
+ | `**/apps/web/**/*.tsx` | TSX files under any `apps/web/` |
105
+ | `**/Dockerfile` | a Dockerfile at any depth, including root |
106
+ | `**/Dockerfile*` | `Dockerfile` plus variants (`Dockerfile.web`, `Dockerfile.dev`) |
107
+ | `**/.github/workflows/*.{yml,yaml}` | CI workflow files |
108
+
109
+ Tip: prefix patterns with `**/` rather than relying on a bare filename — Claude
110
+ Code passes **absolute** file paths to `Write`/`Edit`, so a literal `Dockerfile`
111
+ (without `**/`) only matches a target whose path is exactly that string.
112
+
113
+ Order **within a playbook** does not matter — any pattern matching the file path triggers
114
+ the require. Order **across playbooks** does not matter either — if two playbooks both
115
+ match the same path, the agent must read **both** before the write passes.
116
+
117
+ **Chicken-and-egg:** a playbook whose `applies_to` matches its own location —
118
+ e.g. `**/*.md` — will block the very file you're trying to author. Prefer
119
+ narrow globs (file-type-specific), and if you ever need a generic-Markdown
120
+ playbook, carve out `docs/playbooks/**` from the pattern.
121
+
122
+ ### `keywords` semantics
123
+
124
+ Each entry is a **case-insensitive regex** evaluated against the raw user prompt
125
+ (multi-line aware — the matcher slurps the prompt into a single string before
126
+ testing) with `jq`'s Oniguruma engine (PCRE-like). Use `\b` for word
127
+ boundaries, `|` for alternation inside a single pattern, etc.
128
+
129
+ Examples:
130
+
131
+ | Pattern | Fires on |
132
+ | -------------------------- | -------------------------------------- |
133
+ | `\bvitest\b` | "let's add vitest", "Vitest config" |
134
+ | `\b(new pod\|nuevo pod)\b` | English or Spanish |
135
+ | `\bMongo(DB)? aggregate\b` | "MongoDB aggregate", "Mongo aggregate" |
136
+
137
+ Multiple playbooks may match the same prompt — all matching playbooks are suggested in
138
+ a single `<system-reminder>` block.
139
+
140
+ ---
141
+
142
+ ## How the hooks interpret it
143
+
144
+ ### `require-playbook.sh` (PreToolUse on `Write`/`Edit`)
145
+
146
+ 1. Scan all playbooks: `docs/playbooks/*.md` (local) + `~/.claude/playbooks/*.md` (global,
147
+ only for topics not shadowed by a local file).
148
+ 2. For each playbook with non-empty `applies_to`, test every glob against the target
149
+ `file_path`. If any matches, the playbook is **required**.
150
+ 3. For each required playbook, grep the conversation transcript (and any sub-agent
151
+ transcripts under `<transcript>/subagents/*.jsonl`) for evidence that **Claude actually
152
+ read it** via the `Read` tool with the playbook's absolute path.
153
+ 4. If at least one required playbook is unread, exit 2 with a message naming the missing
154
+ playbook(s) and the exact `Read(...)` call the agent must make. Otherwise exit 0.
155
+
156
+ ### `suggest-playbook.sh` (UserPromptSubmit, non-blocking)
157
+
158
+ 1. Scan all playbooks (same precedence as above).
159
+ 2. For each playbook with non-empty `keywords`, test every regex against the prompt. If
160
+ any matches, the playbook is **suggested**.
161
+ 3. Drop playbooks already read in this conversation (transcript grep, same as above).
162
+ 4. If any remain, emit a `<system-reminder>` listing them. Always exit 0.
163
+
164
+ ### Fail-open philosophy
165
+
166
+ Both **playbook** hooks (`require-playbook.sh`, `suggest-playbook.sh`) fail
167
+ **open** — when `yq` is missing, the transcript is unreadable, or a playbook
168
+ frontmatter cannot be parsed, they exit 0 (suggest) or 0 with a warning
169
+ (require) rather than blocking the agent.
170
+
171
+ The lifecycle hook `init.sh` (`SessionStart`) is the one exception: it
172
+ **blocks** by design (decision #54) on hard errors that the session cannot
173
+ proceed past — missing/invalid config, state corruption, missing git user,
174
+ missing `gh` auth. See `catalog/hooks/README.md` for the full fail-open vs
175
+ blocking matrix.
176
+
177
+ ### No cache
178
+
179
+ Each hook fire scans the catalog fresh. Playbooks are read sparingly (PreToolUse fires
180
+ only on Write/Edit; UserPromptSubmit fires once per turn) and the catalog is small. A
181
+ cache would add a staleness failure mode without measurably improving latency.
182
+
183
+ ---
184
+
185
+ ## Authoring conventions
186
+
187
+ - One topic per file. Split when a topic grows beyond a single screen of frontmatter.
188
+ - Project-**specific** rules — a business rule, a quirk of one app — do **not** belong
189
+ here. Put them in `CLAUDE.md`, a project-local ADR (`docs/adr/`), or `CONTEXT.md`.
190
+ - Reference other playbooks with `[topic](./topic.md)` — relative links so they resolve
191
+ in any Markdown viewer.
192
+
193
+ ### Iteration
194
+
195
+ Playbooks evolve with the codebase. The **Architect**'s `playbook-iterate` skill proposes
196
+ edits when a spec collides with an existing playbook (a `T6 PLAYBOOK_CONFLICT` review
197
+ finding) or when a recurring pattern in the code is not yet captured. The Architect
198
+ proposes; the client decides.
@@ -0,0 +1,13 @@
1
+ # schemas/ — telemetry & config schemas
2
+
3
+ Schema definitions the harness writes/validates against. Tier 1 (client-local)
4
+ writes events in these formats from day one so the data is forward-compatible with
5
+ the Tier 2 central backend designed in Fase 1+.
6
+
7
+ ## Documents
8
+
9
+ | File | Decision | Status | Purpose |
10
+ | ---------------------------------------------------- | -------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
+ | [`tier2-events.md`](tier2-events.md) | #27, #61 | **Authority — P5.** | Event schema for `events.jsonl`: 9 event types (`session_closed`, `spec_created`, `spec_approved`, `task_done`, `review_rejected`, `bug_post_merge`, `l3_bypass`, `followup_captured`, `step_completed`), the common envelope (`type`, `ts`, `user`, `project`, `task_id`, `harness_version`), and per-field tags (`sensitive`/`internal`/`metric`) for forward-sanitization. |
12
+ | [`tier2-events-history.md`](tier2-events-history.md) | #51 | Seeded at `0.1.0-alpha.0`. | Forward-only per-release deltas (ADDED/REMOVED/DEPRECATED/RENAMED fields, new event types) — supports forward-only, dispatch-on-read. ~30 min/release. |
13
+ | `harness.config.schema.json` | #53(e) | Pending (S3). | JSON Schema generated from the Zod config schema via `zod-to-json-schema`, distributed for IDE autocomplete. |
@@ -0,0 +1,104 @@
1
+ # Tier 2 events — schema history
2
+
3
+ > **Forward-only changelog** of the event schema defined in
4
+ > [`tier2-events.md`](tier2-events.md). Readers add aliases here when they care
5
+ > about renamed fields; the schema document is always the current truth (decision
6
+ > \#51). One entry per release that touches the schema. ~30 min/release.
7
+
8
+ Format per entry (one block per release):
9
+
10
+ ```
11
+ ## <version> — <YYYY-MM-DD>
12
+
13
+ ### Added
14
+ - `<event_type>.<field>` — <one-line rationale>
15
+ - `<event_type>` — <new event type rationale>
16
+
17
+ ### Removed
18
+ - `<event_type>.<field>` — <readers that need the value should pin to ≤ <prev-version>>
19
+
20
+ ### Deprecated
21
+ - `<event_type>.<field>` — replaced by `<new_field>`; both written until <future-version>
22
+
23
+ ### Renamed
24
+ - `<event_type>.<old_field>` → `<new_field>` — <rationale>
25
+ ```
26
+
27
+ Empty sections may be omitted.
28
+
29
+ ---
30
+
31
+ ## 0.1.0-alpha.9 — 2026-06-18
32
+
33
+ ### Added
34
+
35
+ - `review_rejected.attributed_kind` — optional `agent` | `skill` | `playbook`: the
36
+ kind of component the friction is attributed to (#217). Omitted when the emitter
37
+ can't attribute.
38
+ - `review_rejected.attributed_name` — optional free string (1-200): the component's
39
+ name (#217). Free-string by design this phase (measure-then-decide).
40
+ - `step_completed.attributed_kind` — optional, as above (#217).
41
+ - `step_completed.attributed_name` — optional, as above (#217).
42
+
43
+ ### Changed
44
+
45
+ - **Field-tag model → 5 axes, assigned per `(event_type, field)`** (#226, D9). The
46
+ old `sensitive` / `internal` / `metric` tags become `local-only` / `identity` /
47
+ `free-text` / `internal-enum` / `metric`. **No field renamed, added, or removed** —
48
+ only the axis column changed (the tag is now a property of the occurrence, fixing
49
+ `reason` = enum in `session_closed` vs free-text in `review_rejected` / `l3_bypass`).
50
+ The export sanitizer (`src/telemetry/sanitize.ts`) reads these axes: the
51
+ `anonymous` tier (on by default) drops `user`, `project`, `task_id`,
52
+ `parent_task_id`, `topic`, and free-text `reason`, keeping enums, metrics, and
53
+ `attributed_name`. Tier-2 readers of the anonymous corpus therefore see **no
54
+ per-install identifier** — only counts, durations, enums, and roster names.
55
+
56
+ ---
57
+
58
+ ## 0.1.0-alpha.4 — 2026-06-12
59
+
60
+ ### Added
61
+
62
+ - `step_completed` — new event type emitted by the Orchestrator at each resolved
63
+ human checkpoint in step-by-step mode (#176). Fields: `task_id` (required),
64
+ `step` (required, 1-based `tasks.md` task number), `review_iterations`
65
+ (required, ≥ 1), `checkpoint_result` (required, `ok` | `changes` |
66
+ `ok_downgrade`). One event per checkpoint — a "changes" step emits again when
67
+ it re-checkpoints.
68
+ - `task_done.mode` — optional `all_at_once` | `step_by_step`: the mode chosen at
69
+ the L1 approval gate (#176). Absent on L2 and on pre-#176 lines.
70
+ - `task_done.steps` — optional count of `step_completed` events for the task
71
+ (≥ 1). Only meaningful when `mode` is `step_by_step`.
72
+ - `review_rejected.step` — optional 1-based task number when the rejection came
73
+ from a per-step review (#176). Absent on full-pass and all-at-once rejections.
74
+
75
+ ---
76
+
77
+ ## 0.1.0-alpha.3 — 2026-06-05
78
+
79
+ ### Added
80
+
81
+ - `followup_captured` — new event type emitted by `/spinoff` (#112) when a
82
+ non-blocking defect found mid-task is parked as a stub. Fields: `task_id`
83
+ (required, the stub), `parent_task_id` (optional, the originating task),
84
+ `severity` (optional, best-effort). Distinct from `bug_post_merge` (post-merge
85
+ signal) by design.
86
+
87
+ ---
88
+
89
+ ## 0.1.0-alpha.0 — 2026-05-28
90
+
91
+ Initial schema. The seven event types of this release and their fields are
92
+ listed in `tier2-events.md` (the authority doc tracks the current set, since
93
+ expanded — see later entries).
94
+
95
+ ### Added
96
+
97
+ - Envelope: `type`, `ts`, `user`, `project`, `task_id` (optional), `harness_version`.
98
+ - `session_closed` — `session_start_ts`, `session_active_h`, `reason`, `auto_close`.
99
+ - `spec_created` — `task_id` (required), `topic`, `requirements`.
100
+ - `spec_approved` — `task_id` (required), `iterations`.
101
+ - `task_done` — `task_id` (required), `level`, `cycle_time_h`, `review_rejections`.
102
+ - `review_rejected` — `task_id` (required), `reason`, `iteration`.
103
+ - `bug_post_merge` — `task_id`, `discovered_h`, `severity`. _(Schema only, emitter lands in P8.)_
104
+ - `l3_bypass` — `topic`, `reason`. _(Schema only, emitter lands in P6.)_