@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.
- package/LICENSE +21 -0
- package/PRIVACY.md +147 -0
- package/README.md +189 -0
- package/catalog/VERSION +1 -0
- package/catalog/agents/README.md +29 -0
- package/catalog/agents/architect.md +81 -0
- package/catalog/agents/fit-assessment.md +94 -0
- package/catalog/agents/implementer.md +67 -0
- package/catalog/agents/orchestrator.md +627 -0
- package/catalog/agents/reviewer.md +124 -0
- package/catalog/agents/spec-author.md +69 -0
- package/catalog/agents/ui-designer.md +25 -0
- package/catalog/commands/add-capability.md +69 -0
- package/catalog/commands/bypass.md +40 -0
- package/catalog/commands/define.md +24 -0
- package/catalog/commands/hotfix.md +47 -0
- package/catalog/commands/pause.md +52 -0
- package/catalog/commands/resume.md +56 -0
- package/catalog/commands/spinoff.md +59 -0
- package/catalog/commands/triage.md +24 -0
- package/catalog/harness.config.schema.json +116 -0
- package/catalog/hooks/README.md +56 -0
- package/catalog/hooks/init.sh +281 -0
- package/catalog/hooks/lib/lemony.sh +41 -0
- package/catalog/hooks/lib/playbook-scan.sh +394 -0
- package/catalog/hooks/lib/transcript-grep.sh +56 -0
- package/catalog/hooks/require-playbook.sh +97 -0
- package/catalog/hooks/session-close.sh +232 -0
- package/catalog/hooks/suggest-playbook.sh +72 -0
- package/catalog/playbook-format.md +198 -0
- package/catalog/schemas/README.md +13 -0
- package/catalog/schemas/tier2-events-history.md +104 -0
- package/catalog/schemas/tier2-events.md +286 -0
- package/catalog/skills/README.md +62 -0
- package/catalog/skills/bootstrap-architecture/SKILL.md +78 -0
- package/catalog/skills/code-explorer/SKILL.md +76 -0
- package/catalog/skills/grill-with-docs/ADR-FORMAT.md +49 -0
- package/catalog/skills/grill-with-docs/CONTEXT-FORMAT.md +77 -0
- package/catalog/skills/grill-with-docs/SKILL.md +270 -0
- package/catalog/skills/grill-with-docs/reference.md +236 -0
- package/catalog/skills/mutation-testing/SKILL.md +84 -0
- package/catalog/skills/note-side-finding/SKILL.md +89 -0
- package/catalog/skills/playbook-iterate/SKILL.md +78 -0
- package/catalog/skills/prd-to-spec/SKILL.md +181 -0
- package/catalog/skills/raise-discovery/SKILL.md +112 -0
- package/catalog/skills/resolve-discovery/SKILL.md +123 -0
- package/catalog/skills/review-pr/SKILL.md +106 -0
- package/catalog/skills/review-pr/reference.md +105 -0
- package/catalog/skills/security-review/SKILL.md +90 -0
- package/catalog/skills/senior-review/SKILL.md +99 -0
- package/catalog/skills/silent-failure-hunter/SKILL.md +76 -0
- package/catalog/skills/spec-compliance-check/SKILL.md +74 -0
- package/catalog/skills/spec-to-issue/SKILL.md +88 -0
- package/catalog/skills/task-closeout/SKILL.md +229 -0
- package/catalog/skills/tdd/SKILL.md +171 -0
- package/catalog/skills/test-gap-report/SKILL.md +71 -0
- package/catalog/skills/triage-issue/SKILL.md +102 -0
- package/catalog/skills/update-architecture/SKILL.md +69 -0
- package/catalog/skills/verify/SKILL.md +90 -0
- package/catalog/skills/write-adr/SKILL.md +77 -0
- package/catalog/templates/README.md +32 -0
- package/catalog/templates/claude-code/.claude/settings.json.tpl +34 -0
- package/catalog/templates/claude-code/agents.md.tpl +109 -0
- package/catalog/templates/claude-code/docs/playbooks/README.md.tpl +96 -0
- package/catalog/templates/claude-code/harness.config.yml.tpl +59 -0
- package/catalog/templates/claude-code/state/history.md.tpl +6 -0
- package/dist/cli.mjs +5691 -0
- 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.)_
|