agent-harness-kit 0.11.0 → 0.11.2

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.
@@ -11,9 +11,9 @@
11
11
  "source": {
12
12
  "source": "github",
13
13
  "repo": "tuanle96/agent-harness-kit",
14
- "ref": "v0.11.0"
14
+ "ref": "v0.11.2"
15
15
  },
16
- "version": "0.11.0",
16
+ "version": "0.11.2",
17
17
  "description": "Solo-dev harness engineering kit — layered architecture, GC ritual, structural tests, review subagents.",
18
18
  "category": "development",
19
19
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-harness-kit",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "Solo-dev harness engineering kit — layered architecture, garbage-collection ritual, structural tests, review subagents. Optimized for Claude Code 2.1+.",
5
5
  "author": {
6
6
  "name": "Tuan Le"
package/README.md CHANGED
@@ -33,9 +33,11 @@ Option B: install as a Claude Code plugin
33
33
 
34
34
  ## What ships
35
35
 
36
- - 10 skills (`inspect-module`, `add-feature`, `garbage-collection`,
37
- `propose-harness-improvement`, `write-skill`, `debug-flow`, `eval-runner`,
38
- `add-adr`, `doc-drift-scan`, `structural-test-author`)
36
+ - 17 skills (`add-adr`, `add-feature`, `debug-flow`, `deliver-html`,
37
+ `doc-drift-scan`, `eval-runner`, `garbage-collection`, `i18n-add-locale`,
38
+ `inspect-app`, `inspect-module`, `map-domain`,
39
+ `propose-harness-improvement`, `refactor-feature`, `review-this-pr`,
40
+ `setup-nightly-eval`, `structural-test-author`, `write-skill`)
39
41
  - 5 read-only review subagents (`architecture-reviewer`, `security-reviewer`,
40
42
  `reliability-reviewer`, `performance-reviewer`, `api-consistency-reviewer`)
41
43
  - 1 PostToolUse hook (structural-test on every edit) + 1 Stop hook
@@ -60,6 +62,12 @@ Option B: install as a Claude Code plugin
60
62
  | `/doc-drift-scan` | Find stale path/command references in `docs/` |
61
63
  | `/debug-flow` | Run the failing flow before fixing it |
62
64
  | `/deliver-html` | Ship an analysis/audit/plan as a self-contained HTML |
65
+ | `/i18n-add-locale <code>` | Scaffold a new translation locale for skills + CLAUDE.md |
66
+ | `/inspect-app` | Boot dev server + drive the failing flow before edits |
67
+ | `/map-domain` | Render layer config + flag config-vs-filesystem drift |
68
+ | `/refactor-feature` | Restructure steps in `feature_list.json` with proof gate |
69
+ | `/review-this-pr` | Deterministic diff review against the current base |
70
+ | `/setup-nightly-eval` | Enable the nightly eval GitHub Actions workflow |
63
71
 
64
72
  ## Philosophy (5 axioms)
65
73
 
@@ -104,7 +112,7 @@ your-repo/
104
112
  ├── harness.config.json
105
113
  ├── .claude/
106
114
  │ ├── settings.json
107
- │ ├── skills/ # 10 skills as SKILL.md files
115
+ │ ├── skills/ # 17 skills as SKILL.md files
108
116
  │ ├── agents/ # 5 reviewer personas
109
117
  │ └── hooks/hooks.json
110
118
  ├── .harness/
@@ -159,7 +167,7 @@ agent-harness-kit --version
159
167
  What this kit **does** differentiate from bare claude-cli (anecdotal + design-level):
160
168
 
161
169
  - Opinionated CLAUDE.md template (50–80 lines) so context isn't blown on style
162
- - 10 skills (`/add-feature`, `/garbage-collection`, `/propose-harness-improvement`, …) that codify Hashimoto/OpenAI rituals
170
+ - 17 skills (`/add-feature`, `/garbage-collection`, `/propose-harness-improvement`, …) that codify Hashimoto/OpenAI rituals
163
171
  - 5 read-only review subagents for cheap second-opinion passes
164
172
  - `feature_list.json` + ADR template + GC ritual for solo-scale planning hygiene
165
173
  - Solo-dev cost defaults (~$2/day) and per-run budget enforcement
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-harness-kit",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "Solo-dev harness engineering kit for Claude Code. Layered architecture, structural tests, garbage-collection ritual, review subagents — without the enterprise overhead.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,7 +42,8 @@
42
42
  "lint": "echo 'no-op (kit is plain ESM JS)'",
43
43
  "selftest": "node bin/cli.mjs --version",
44
44
  "harness:eval": "node src/templates/_adapter-typescript/harness/eval-runner.mjs",
45
- "harness:check": "node scripts/kit-structural-check.mjs"
45
+ "harness:check": "node scripts/kit-structural-check.mjs",
46
+ "check:skill-count": "node scripts/check-skill-count.mjs"
46
47
  },
47
48
  "dependencies": {
48
49
  "@inquirer/prompts": "^7.0.0",
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://www.schemastore.org/claude-code-keybindings.json",
3
+ "$docs": "https://code.claude.com/docs/en/keybindings",
4
+ "$comment": "agent-harness-kit sample. RENAME to ~/.claude/keybindings.json after editing. Claude Code's keybinding action list is FIXED — there is no action that runs a slash command (no `chat:runCommand` or similar), so we cannot bind keys to /gc, /add-feature, etc. The bindings below tune the chat workflow only. To run a slash command, type `/` and use autocomplete — that is the supported UX.",
5
+ "bindings": [
6
+ {
7
+ "context": "Chat",
8
+ "bindings": {
9
+ "ctrl+e": "chat:externalEditor"
10
+ }
11
+ },
12
+ {
13
+ "context": "Global",
14
+ "bindings": {
15
+ "ctrl+shift+t": "app:toggleTodos",
16
+ "ctrl+shift+r": "app:toggleTranscript"
17
+ }
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,118 @@
1
+ ---
2
+ name: setup-nightly-eval
3
+ description: Use this skill when the user wants to schedule the harness eval to run every night, asks "how do I set up nightly evals", "schedule the eval", "run evals on a cron", or "nightly regression for the harness". The kit already ships a GitHub Actions workflow at .github/workflows/eval-nightly.yml — this skill walks the user through enabling it (secret setup, smoke run via workflow_dispatch, verifying the first scheduled run). Do NOT use this skill to "remind me to run eval every night in this Claude session" — that is the /loop skill or CronCreate (which is session-only), a different concern.
4
+ allowed-tools: Read, Bash(gh:*), Bash(ls:*), Bash(cat:*), Bash(test:*)
5
+ suggested-turns: 4
6
+ ---
7
+
8
+ ## Background — why GitHub Actions, not CronCreate
9
+
10
+ A common request is "use CronCreate to run the eval every night". That
11
+ does not do what the user wants:
12
+
13
+ - `CronCreate` jobs live only in the **current Claude Code session**.
14
+ Closing the REPL deletes them. Auto-expire after 7 days regardless.
15
+ - Jobs only fire while the REPL is **idle**, not when the laptop is
16
+ asleep or off.
17
+ - They run *Claude turns*, which spend tokens for every fire.
18
+
19
+ For a real nightly cadence ("runs at 6am whether I'm at the keyboard or
20
+ not"), the right substrate is OS-level cron / launchd / GitHub Actions.
21
+ This kit ships a GitHub Actions workflow as the default because:
22
+
23
+ 1. No local daemon to babysit.
24
+ 2. Free for public repos and within the free tier for most private ones.
25
+ 3. Results land in workflow artifacts — visible from anywhere.
26
+
27
+ ## When to use
28
+
29
+ Trigger phrases (English / Vietnamese):
30
+
31
+ - "set up nightly eval" / "lập lịch eval mỗi đêm"
32
+ - "schedule the harness eval"
33
+ - "make the eval run on a cron"
34
+ - "nightly regression for the harness"
35
+
36
+ Do **NOT** invoke for:
37
+
38
+ - One-off ad-hoc eval runs — use `/eval-runner` directly.
39
+ - In-session polling ("re-run every 10 min until I say stop") — that's
40
+ the `/loop` skill.
41
+ - Local-machine cron setup (launchd / crontab) — that path is on the
42
+ user's machine and a skill cannot install OS daemons. Print the
43
+ recipe and let them paste it.
44
+
45
+ ## Steps
46
+
47
+ 1. **Verify the workflow file exists.** It ships via `installCi: true`.
48
+
49
+ ```bash
50
+ test -f .github/workflows/eval-nightly.yml && echo OK || echo MISSING
51
+ ```
52
+
53
+ If MISSING: the user opted out of CI files at scaffold time. Tell
54
+ them to re-run `agent-harness-kit upgrade --ci` (or to manually copy
55
+ from `node_modules/agent-harness-kit/src/templates/_ci/`).
56
+
57
+ 2. **Check the eval transport.** The workflow defaults to `mock`
58
+ transport unless `ANTHROPIC_API_KEY` is set in repo secrets. Decide
59
+ with the user:
60
+
61
+ - **Mock (free):** smoke-tests the eval runner shape — catches a
62
+ broken JSONL writer, but does not exercise the model. Good
63
+ default for forks / OSS.
64
+ - **claude-cli (real, costs tokens):** runs the actual model on
65
+ each task. Catches regressions caused by prompt/skill changes.
66
+ Costs ~$0.05–0.50/night depending on task set size.
67
+
68
+ 3. **(If real transport) ensure the secret is set:**
69
+
70
+ ```bash
71
+ gh secret list | grep ANTHROPIC_API_KEY
72
+ ```
73
+
74
+ If absent, ask the user to set it via:
75
+
76
+ ```bash
77
+ gh secret set ANTHROPIC_API_KEY
78
+ # paste the key when prompted (it never appears in shell history)
79
+ ```
80
+
81
+ 4. **Trigger a first manual run** via `workflow_dispatch` so the user
82
+ does not wait 24h to confirm the wiring:
83
+
84
+ ```bash
85
+ gh workflow run eval-nightly.yml --field set=quick --field transport=mock
86
+ # then watch:
87
+ gh run watch
88
+ ```
89
+
90
+ 5. **Print the contract.** What the user just enabled:
91
+
92
+ ```
93
+ ### Nightly eval enabled
94
+ **Workflow:** .github/workflows/eval-nightly.yml
95
+ **Cron:** 0 6 * * * UTC (offset: see `gh run list` for actual fire times)
96
+ **Transport:** mock | claude-cli
97
+ **Set:** quick (3 tasks) | full (all tasks)
98
+ **Results:** uploaded as `eval-results` artifact on each run
99
+ ```
100
+
101
+ ## Output contract
102
+
103
+ The skill prints a single block matching the shape above. Do not edit
104
+ the workflow file from here — if the user wants to change the cron, the
105
+ task set, or the transport default, they edit
106
+ `.github/workflows/eval-nightly.yml` directly (it is a normal yml file,
107
+ not a templated artifact, after install).
108
+
109
+ ## When the workflow file is owned by the kit
110
+
111
+ Re-running `agent-harness-kit upgrade` will refresh
112
+ `.github/workflows/eval-nightly.yml`. If the user has hand-tuned cron
113
+ or transport defaults, mention this — they should either:
114
+
115
+ - Move their customisation into `harness.config.json#evals` (kit reads
116
+ it on next render) and let the workflow stay vanilla, or
117
+ - Document the customisation in a comment so the next upgrade does not
118
+ silently overwrite it.
@@ -0,0 +1,54 @@
1
+ # Environment variables
2
+
3
+ Every kit hook and side-car honors one or more `AHK_*` env vars for opt-out,
4
+ debugging, or non-default behavior. Defaults are tuned for "just works" —
5
+ override only when you have a specific reason.
6
+
7
+ ## Opt-out
8
+
9
+ | Var | Default | Effect |
10
+ | --- | --- | --- |
11
+ | `AHK_DISABLE_TELEMETRY` | unset | When `1`, the `telemetry-on-skill` and `subagent-stop` hooks exit before reading stdin — no `.harness/telemetry.jsonl` is created. Use when you do not want per-skill activity recorded. |
12
+ | `AHK_DISABLE_NOTIFY` | unset | When `1`, the `notify-on-block` hook skips the OS-native notification (osascript / notify-send). The telemetry row still logs the notification event. |
13
+ | `AHK_DISABLE_HTML_OPEN` | unset | When `1`, `/deliver-html` writes the HTML file but does not auto-open it in the browser. Also implied when `CI=true`. |
14
+ | `AHK_DISABLE_HTML_NUDGE` | unset | When `1`, suppresses the inline reminder that `/deliver-html` is available for analysis-style tasks. |
15
+ | `AHK_DISABLE_JQ` | unset | When `1`, hooks pretend `jq` is not on `$PATH` and use the Node fallback (`scripts/_lib/json-pick.mjs`). Used by tests to exercise the fallback path. |
16
+
17
+ ## Bypass (audited)
18
+
19
+ | Var | Default | Effect |
20
+ | --- | --- | --- |
21
+ | `AHK_ALLOW_BYPASS` | unset | When `1`, `userprompt-guard`, `pretooluse-bash-guard`, and `pretooluse-edit-guard` allow the action through but append a record to `.harness/bypass.log` (timestamp + sha + reason + payload). The bypass leaves a paper trail so it cannot be silent. Use only with explicit intent — e.g. a mass-rename refactor that legitimately touches `.claude/`. |
22
+ | `AHK_HOOK_MODE` | unset | When `warn`, every gate hook (structural-test-on-edit, pretooluse-edit-guard, subagent-stop) logs the would-be violation to stderr but does not deny. Useful for one-off debugging; do not leave set in normal use. |
23
+
24
+ ## Tuning
25
+
26
+ | Var | Default | Effect |
27
+ | --- | --- | --- |
28
+ | `AHK_TELEMETRY_MAX_LINES` | `5000` | Soft cap on `.harness/telemetry.jsonl` size. The `telemetry_append` helper rotates via `tail -n <N>` once the file grows past this number of lines. Set `0` to disable rotation entirely. Numeric only — non-numeric values fall back to the default rather than failing the hook. |
29
+ | `AHK_HEADLESS_RECOVER` | `0` | When `1`, the Stop hook spawns `claude -p` for one turn of recovery on failure. Costs tokens; off by default. Persistent equivalent: `harness.config.json#recovery.headless`. |
30
+ | `AHK_RECOVERY_LOCK_STALE_SECS` | `300` | How long the Stop-hook recovery lock is considered stale before a new recovery attempt can take it. Prevents stuck locks after a killed session. |
31
+ | `AHK_STATUSLINE_NO_COLOR` | unset | When `1`, the statusline emits plain text — no ANSI color escapes. Useful on terminals that do not render colors well, or when piping the output. |
32
+
33
+ ## Where each variable lives
34
+
35
+ ```
36
+ AHK_DISABLE_TELEMETRY → scripts/telemetry-on-skill.sh, scripts/subagent-stop.sh
37
+ AHK_DISABLE_NOTIFY → scripts/notify-on-block.sh
38
+ AHK_DISABLE_HTML_OPEN → .claude/skills/deliver-html/scripts/wrap-html.mjs
39
+ AHK_DISABLE_HTML_NUDGE → .claude/skills/deliver-html/SKILL.md
40
+ AHK_DISABLE_JQ → scripts/_lib/jp.sh (probed by every hook that parses JSON)
41
+ AHK_ALLOW_BYPASS → scripts/userprompt-guard.sh, scripts/pretooluse-*.sh
42
+ AHK_HOOK_MODE → scripts/structural-test-on-edit.sh, scripts/pretooluse-edit-guard.sh, scripts/subagent-stop.sh
43
+ AHK_TELEMETRY_MAX_LINES→ scripts/_lib/telemetry.sh (used by telemetry-on-skill, subagent-stop, notify-on-block)
44
+ AHK_HEADLESS_RECOVER → scripts/precompletion-checklist.sh
45
+ AHK_STATUSLINE_NO_COLOR→ scripts/statusline.mjs
46
+ ```
47
+
48
+ ## Disabling vs. removing
49
+
50
+ Prefer env-var opt-out over removing a hook from `.claude/settings.json` —
51
+ the kit's structural-test and version-sync checks expect every hook listed in
52
+ `hooks.json` to be present. Removing a hook leaves the index claiming a
53
+ contract the file system no longer fulfills, and `agent-harness-kit upgrade`
54
+ will keep re-installing it.
@@ -0,0 +1,82 @@
1
+ # Auto-memory cheat sheet
2
+
3
+ Claude Code maintains a per-project memory directory at
4
+ `~/.claude/projects/<repo-slug>/memory/`. It persists across sessions and is
5
+ automatically loaded into the next conversation. The kit does **not**
6
+ manage this — Claude Code does. This file is a cheat sheet on when to push
7
+ something into it and when to leave it out.
8
+
9
+ ## The four types
10
+
11
+ | Type | When to write | Half-life | Example |
12
+ | --- | --- | --- | --- |
13
+ | **user** | You learn something durable about the person you're working with — role, expertise, team, preferences | months to years | "data scientist, 10 years Go, new to React" |
14
+ | **feedback** | The user corrects an approach ("don't X") *or* validates an unusual choice ("yes the bundled PR was right") | months | "integration tests must hit a real DB, not mocks — burned by mock/prod divergence in Q3" |
15
+ | **project** | A fact about the work that the codebase itself does not show — deadlines, stakeholder decisions, the *why* behind a refactor | weeks to months | "auth rewrite is driven by legal compliance, not tech debt — scope toward compliance" |
16
+ | **reference** | A pointer to an external system — Linear project, Grafana dashboard, on-call runbook | until that system moves | "pipeline bugs in Linear project INGEST" |
17
+
18
+ ## What's actually worth saving
19
+
20
+ Save when **all three** apply:
21
+
22
+ 1. **Non-obvious from the code.** If `git log` or reading the file shows
23
+ it, the memory is dead weight.
24
+ 2. **Survives across sessions.** Today's in-progress task is not memory —
25
+ it's a task list.
26
+ 3. **Decision-shaping.** A future-you (or future-Claude) would behave
27
+ differently knowing it.
28
+
29
+ Trigger words from the user that mean "save this":
30
+
31
+ - "remember that …"
32
+ - "next time, do X / don't do X"
33
+ - "this is how we always do it"
34
+ - "the reason is …"
35
+
36
+ ## What's NOT worth saving
37
+
38
+ - Code patterns, file paths, architecture diagrams — re-read the code.
39
+ - Git history, "who changed X last week" — `git log` is authoritative.
40
+ - Today's debug recipe — the fix lives in the commit; the commit message
41
+ has the context.
42
+ - A list of files you just edited — the diff has it.
43
+ - Anything documented in `CLAUDE.md` — it's already loaded.
44
+
45
+ Reject the request even if the user explicitly asks. Bigger memory is
46
+ *not* better memory — every dead entry is noise the next session will
47
+ have to scan past.
48
+
49
+ ## Working with what's there
50
+
51
+ Claude Code loads `MEMORY.md` (the index) into every conversation. So:
52
+
53
+ - If you want a memory honored, make sure its index line is concise and
54
+ specific. Bad: `notes.md — stuff`. Good: `feedback_tests.md — never
55
+ mock the database in integration tests`.
56
+ - If a memory turns out wrong or stale, ask Claude to remove or update it
57
+ — don't let it accumulate.
58
+ - Memory is **not** automatically synced across machines. If your
59
+ workflow spans laptops, treat it as scratch, not source-of-truth.
60
+
61
+ ## Privacy and scope
62
+
63
+ - Memory lives in `~/.claude/` — local-only. Nothing is uploaded.
64
+ - A project-scoped memory is keyed by repo slug. Cloning the repo on a
65
+ new machine does **not** carry memory over.
66
+ - If a memory contains something sensitive (credentials, customer names),
67
+ delete it. The kit's `userprompt-guard` hook does not see memory; the
68
+ burden of redaction is on you.
69
+
70
+ ## Related kit features
71
+
72
+ - The **Stop hook** writes a JSONL row to `.harness/telemetry.jsonl` on
73
+ every Skill invocation — that is observability for the kit, not memory.
74
+ - The **SessionStart hook** inject branch + uncommitted diff + current
75
+ feature as `additionalContext`. That is per-session context, not
76
+ memory.
77
+ - The **PROGRESS.md** file at `.harness/PROGRESS.md` is the human-readable
78
+ session log appended by `SessionEnd`. Useful next-day rehydration; not
79
+ memory.
80
+
81
+ Memory ≠ context. Memory persists. Context is rebuilt every session from
82
+ files in the repo.
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bash
2
+ # _lib/jp.sh — source-only library. DO NOT execute directly.
3
+ #
4
+ # Provides three shared helpers used by every hook script that parses Claude
5
+ # Code's JSON stdin:
6
+ #
7
+ # have_jq — true iff jq is on PATH AND not disabled via env
8
+ # have_jp — true iff EITHER jq OR (node + _lib/json-pick.mjs) is usable
9
+ # jp <expr> [f] — run a jq-subset expression, preferring jq when present,
10
+ # else the Node fallback. Accepts optional file arg (some
11
+ # callers pass it; most read from stdin).
12
+ #
13
+ # Why this exists: the same ~14 lines were duplicated across 12 hook scripts.
14
+ # Single source of truth so fixing one bug (e.g. the "json-pick.mjs only
15
+ # supports one `// default` per expression" footgun documented in
16
+ # session-end.sh.hbs) only needs one edit, not twelve.
17
+ #
18
+ # Sourcing convention — the calling script MUST set _LIB_DIR before . :
19
+ #
20
+ # SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
21
+ # _LIB_DIR="$SCRIPT_DIR/_lib"
22
+ # . "$_LIB_DIR/jp.sh"
23
+ #
24
+ # Env vars:
25
+ # AHK_DISABLE_JQ=1 → pretend jq is missing; forces the Node fallback path.
26
+ # Lets us test the fallback on machines that have jq.
27
+
28
+ have_jq() {
29
+ [ "${AHK_DISABLE_JQ:-}" = "1" ] && return 1
30
+ command -v jq >/dev/null 2>&1
31
+ }
32
+
33
+ have_jp() {
34
+ have_jq && return 0
35
+ command -v node >/dev/null 2>&1 \
36
+ && [ -f "$_LIB_DIR/json-pick.mjs" ] \
37
+ && return 0
38
+ return 1
39
+ }
40
+
41
+ jp() {
42
+ if have_jq; then
43
+ if [ -n "${2:-}" ]; then jq -r "$1" "$2"
44
+ else jq -r "$1"
45
+ fi
46
+ else
47
+ if [ -n "${2:-}" ]; then
48
+ node "$_LIB_DIR/json-pick.mjs" "$1" "$2"
49
+ else
50
+ node "$_LIB_DIR/json-pick.mjs" "$1"
51
+ fi
52
+ fi
53
+ }
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bash
2
+ # _lib/telemetry.sh — source-only library. DO NOT execute directly.
3
+ #
4
+ # Provides telemetry_append <jsonl-line> — write one line to
5
+ # .harness/telemetry.jsonl and rotate when the file grows past
6
+ # AHK_TELEMETRY_MAX_LINES (default 5000).
7
+ #
8
+ # Why centralised:
9
+ # - Two hooks append (telemetry-on-skill, notify-on-block). Rotation logic
10
+ # written once = one place to fix when the file format evolves.
11
+ # - harness-report.mjs only ever inspects the last 14 days; older lines are
12
+ # pure I/O cost. Bounding lines bounds report time at O(1).
13
+ #
14
+ # Env vars:
15
+ # AHK_DISABLE_TELEMETRY=1 → caller is expected to early-exit; this
16
+ # helper does NOT re-check (avoids double
17
+ # work) — gate in the caller.
18
+ # AHK_TELEMETRY_MAX_LINES=N → cap (default 5000). Set 0 to disable
19
+ # rotation entirely (keep unbounded).
20
+
21
+ telemetry_append() {
22
+ local line="$1"
23
+ [ -z "$line" ] && return 0
24
+ mkdir -p .harness
25
+ printf '%s\n' "$line" >> .harness/telemetry.jsonl
26
+
27
+ local limit="${AHK_TELEMETRY_MAX_LINES:-5000}"
28
+ # 0 = caller opted out of rotation explicitly.
29
+ [ "$limit" = "0" ] && return 0
30
+ # Non-numeric → fall back to default rather than failing the hook.
31
+ case "$limit" in
32
+ ''|*[!0-9]*) limit=5000 ;;
33
+ esac
34
+
35
+ # wc -l is sub-millisecond on files we care about (< 1MB at the default
36
+ # cap). Cheap enough to run every append; avoids needing a daemon.
37
+ local lines
38
+ lines=$(wc -l < .harness/telemetry.jsonl 2>/dev/null || echo 0)
39
+ if [ "$lines" -gt "$limit" ]; then
40
+ # tail to tmp + mv = atomic on POSIX. Reader can't catch a half-written
41
+ # file mid-rotate.
42
+ tail -n "$limit" .harness/telemetry.jsonl > .harness/telemetry.jsonl.tmp \
43
+ && mv .harness/telemetry.jsonl.tmp .harness/telemetry.jsonl
44
+ fi
45
+ }
@@ -6,26 +6,9 @@ set -eo pipefail
6
6
 
7
7
  INPUT=$(cat)
8
8
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
- have_jq() {
10
- [ "${AHK_DISABLE_JQ:-}" = "1" ] && return 1
11
- command -v jq >/dev/null 2>&1
12
- }
13
- have_jp() {
14
- have_jq && return 0
15
- command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/_lib/json-pick.mjs" ] && return 0
16
- return 1
17
- }
18
- jp() {
19
- if have_jq; then
20
- if [ -n "$2" ]; then jq -r "$1" "$2"; else jq -r "$1"; fi
21
- else
22
- if [ -n "$2" ]; then
23
- node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1" "$2"
24
- else
25
- node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1"
26
- fi
27
- fi
28
- }
9
+ _LIB_DIR="$SCRIPT_DIR/_lib"
10
+ . "$_LIB_DIR/jp.sh"
11
+ . "$_LIB_DIR/telemetry.sh"
29
12
 
30
13
  if [ "${AHK_DISABLE_NOTIFY:-}" = "1" ]; then
31
14
  exit 0
@@ -46,12 +29,12 @@ if [ -n "$TYPE" ]; then
46
29
  fi
47
30
  [ -z "$BODY" ] && BODY="Claude Code wants your attention."
48
31
 
49
- mkdir -p .harness
50
32
  TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
51
33
  ESCAPED_TITLE=${TITLE//\"/\\\"}
52
34
  ESCAPED_BODY=${BODY//\"/\\\"}
53
- printf '{"ts":"%s","hook":"Notification","type":"%s","title":"%s","body":"%s"}\n' \
54
- "$TS" "$TYPE" "$ESCAPED_TITLE" "$ESCAPED_BODY" >> .harness/telemetry.jsonl
35
+ LINE=$(printf '{"ts":"%s","hook":"Notification","type":"%s","title":"%s","body":"%s"}' \
36
+ "$TS" "$TYPE" "$ESCAPED_TITLE" "$ESCAPED_BODY")
37
+ telemetry_append "$LINE"
55
38
 
56
39
  OS_KIND=$(uname -s 2>/dev/null || echo "Unknown")
57
40
  case "$OS_KIND" in
@@ -28,26 +28,8 @@ set -eo pipefail
28
28
 
29
29
  INPUT=$(cat)
30
30
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
31
- have_jq() {
32
- [ "${AHK_DISABLE_JQ:-}" = "1" ] && return 1
33
- command -v jq >/dev/null 2>&1
34
- }
35
- have_jp() {
36
- have_jq && return 0
37
- command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/_lib/json-pick.mjs" ] && return 0
38
- return 1
39
- }
40
- jp() {
41
- if have_jq; then
42
- if [ -n "$2" ]; then jq -r "$1" "$2"; else jq -r "$1"; fi
43
- else
44
- if [ -n "$2" ]; then
45
- node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1" "$2"
46
- else
47
- node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1"
48
- fi
49
- fi
50
- }
31
+ _LIB_DIR="$SCRIPT_DIR/_lib"
32
+ . "$_LIB_DIR/jp.sh"
51
33
 
52
34
  TRIGGER=""
53
35
  TOKENS=""
@@ -8,26 +8,8 @@ set -eo pipefail
8
8
  # Without this fallback, `jq` missing on a fresh CI image silently disabled
9
9
  # the baseline-monotonic guard — a known audit hole.
10
10
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
11
- have_jq() {
12
- [ "${AHK_DISABLE_JQ:-}" = "1" ] && return 1
13
- command -v jq >/dev/null 2>&1
14
- }
15
- have_jp() {
16
- have_jq && return 0
17
- command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/_lib/json-pick.mjs" ] && return 0
18
- return 1
19
- }
20
- jp() {
21
- if have_jq; then
22
- if [ -n "$2" ]; then jq -r "$1" "$2"; else jq -r "$1"; fi
23
- else
24
- if [ -n "$2" ]; then
25
- node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1" "$2"
26
- else
27
- node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1"
28
- fi
29
- fi
30
- }
11
+ _LIB_DIR="$SCRIPT_DIR/_lib"
12
+ . "$_LIB_DIR/jp.sh"
31
13
 
32
14
  # Baseline monotonic guard. .harness/structural-baseline.json is decreasing-
33
15
  # only — fixes REMOVE entries; no path should ADD them. Catches the "mask
@@ -14,37 +14,11 @@ INPUT=$(cat)
14
14
 
15
15
  # Resolve the directory this hook lives in (used to find _lib/json-pick.mjs).
16
16
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17
-
18
- # have_jq env-overridable probe. AHK_DISABLE_JQ=1 forces the Node fallback,
19
- # used by tests to exercise the jq-less code path on machines that have jq
20
- # installed locally.
21
- have_jq() {
22
- [ "${AHK_DISABLE_JQ:-}" = "1" ] && return 1
23
- command -v jq >/dev/null 2>&1
24
- }
25
- # jp — JSON picker. Uses `jq` when available, else falls back to a bundled
26
- # Node script with a jq-subset implementation. Keeps hooks portable on
27
- # minimal CI / Windows where jq is not installed by default. Without this
28
- # fallback, the entire pre-completion check used to be a silent no-op.
29
- jp() {
30
- if have_jq; then
31
- if [ -n "$2" ]; then jq -r "$1" "$2"; else jq -r "$1"; fi
32
- else
33
- if [ -n "$2" ]; then
34
- node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1" "$2"
35
- else
36
- node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1"
37
- fi
38
- fi
39
- }
40
- # Probe: do we have either jq or the Node fallback? Node is always
41
- # present (kit's `engines` field requires >=20), so this is just an explicit
42
- # probe and a fail-loud branch if even node is missing.
43
- have_jp() {
44
- have_jq && return 0
45
- command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/_lib/json-pick.mjs" ] && return 0
46
- return 1
47
- }
17
+ _LIB_DIR="$SCRIPT_DIR/_lib"
18
+ # have_jq / have_jp / jp shared across all hook scripts. AHK_DISABLE_JQ=1
19
+ # forces the Node fallback, used by tests to exercise the jq-less code path
20
+ # on machines that have jq installed locally.
21
+ . "$_LIB_DIR/jp.sh"
48
22
 
49
23
  # CRITICAL: avoid infinite loops. If the hook already ran, do not block again.
50
24
  if have_jp; then
@@ -32,26 +32,8 @@ set -eo pipefail
32
32
 
33
33
  INPUT=$(cat)
34
34
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
35
- have_jq() {
36
- [ "${AHK_DISABLE_JQ:-}" = "1" ] && return 1
37
- command -v jq >/dev/null 2>&1
38
- }
39
- have_jp() {
40
- have_jq && return 0
41
- command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/_lib/json-pick.mjs" ] && return 0
42
- return 1
43
- }
44
- jp() {
45
- if have_jq; then
46
- if [ -n "$2" ]; then jq -r "$1" "$2"; else jq -r "$1"; fi
47
- else
48
- if [ -n "$2" ]; then
49
- node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1" "$2"
50
- else
51
- node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1"
52
- fi
53
- fi
54
- }
35
+ _LIB_DIR="$SCRIPT_DIR/_lib"
36
+ . "$_LIB_DIR/jp.sh"
55
37
 
56
38
  if ! have_jp; then
57
39
  # Without a JSON parser we can't read the command. Skip rather than
@@ -23,20 +23,8 @@ set -eo pipefail
23
23
 
24
24
  INPUT=$(cat)
25
25
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
26
- have_jq() {
27
- [ "${AHK_DISABLE_JQ:-}" = "1" ] && return 1
28
- command -v jq >/dev/null 2>&1
29
- }
30
- have_jp() {
31
- have_jq && return 0
32
- command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/_lib/json-pick.mjs" ] && return 0
33
- return 1
34
- }
35
- jp() {
36
- if have_jq; then jq -r "$1"
37
- else node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1"
38
- fi
39
- }
26
+ _LIB_DIR="$SCRIPT_DIR/_lib"
27
+ . "$_LIB_DIR/jp.sh"
40
28
  if ! have_jp; then exit 0; fi
41
29
 
42
30
  # Resolve target file. Write/Edit ship .tool_input.file_path; MultiEdit ships
@@ -19,20 +19,8 @@ set -eo pipefail
19
19
 
20
20
  INPUT=$(cat)
21
21
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
22
- have_jq() {
23
- [ "${AHK_DISABLE_JQ:-}" = "1" ] && return 1
24
- command -v jq >/dev/null 2>&1
25
- }
26
- have_jp() {
27
- have_jq && return 0
28
- command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/_lib/json-pick.mjs" ] && return 0
29
- return 1
30
- }
31
- jp() {
32
- if have_jq; then jq -r "$1"
33
- else node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1"
34
- fi
35
- }
22
+ _LIB_DIR="$SCRIPT_DIR/_lib"
23
+ . "$_LIB_DIR/jp.sh"
36
24
 
37
25
  REASON=""
38
26
  SESSION_ID=""
@@ -22,26 +22,8 @@ set -eo pipefail
22
22
 
23
23
  INPUT=$(cat)
24
24
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
25
- have_jq() {
26
- [ "${AHK_DISABLE_JQ:-}" = "1" ] && return 1
27
- command -v jq >/dev/null 2>&1
28
- }
29
- have_jp() {
30
- have_jq && return 0
31
- command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/_lib/json-pick.mjs" ] && return 0
32
- return 1
33
- }
34
- jp() {
35
- if have_jq; then
36
- if [ -n "$2" ]; then jq -r "$1" "$2"; else jq -r "$1"; fi
37
- else
38
- if [ -n "$2" ]; then
39
- node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1" "$2"
40
- else
41
- node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1"
42
- fi
43
- fi
44
- }
25
+ _LIB_DIR="$SCRIPT_DIR/_lib"
26
+ . "$_LIB_DIR/jp.sh"
45
27
 
46
28
  SOURCE=""
47
29
  if have_jp; then
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- // statusLine — "Two-line Dashboard" (V4).
2
+ // statusLine — "Dashboard Style" (Variant 4).
3
3
  //
4
- // LINE 1 — vitals (always emitted when any segment resolves):
5
- // ▶ Opus│terse│⏱1h12m main(±3) feat:health-endpoint ▓▓▓▓░ 42% $0.83 +156/-23
4
+ // LINE 1 — vitals with rich icons (always emitted when any segment resolves):
5
+ // ▶ Opus 4.7 📝 terse │ ⏱ 1h12m │ ⎇ main(±3) │ ✓ clean │ ▓▓▓▓░ 42% $0.83 +156/-23
6
6
  //
7
- // LINE 2 — alerts (only when ≥1 trigger fires; otherwise omitted):
8
- // ⚠ >200K — auto-compact next msg ctx 84% ⏳ 5h limit 78%, resets in 1h12m 🚫 last-block: <title>
7
+ // LINE 2 — alerts with clear segmentation (only when ≥1 trigger fires):
8
+ // ⚠ >200K — auto-compact next msg ⏳ 5h limit 78%, resets in 1h12m🚫 last-block: <title>
9
9
  //
10
10
  // Payload (Claude Code v2.1.132+ schema):
11
11
  // model.display_name, output_style.name, session_id, version,
@@ -236,61 +236,72 @@ function bar(pct, width = 10) {
236
236
  }
237
237
 
238
238
  // ---------------------------------------------------------------------------
239
- // Line 1 — vitals.
239
+ // Line 1 — vitals (Dashboard Style with rich icons).
240
240
  // ---------------------------------------------------------------------------
241
241
  function renderLine1(payload, git, feat, config) {
242
- const left = []; // identity group: model, style, duration
243
- const mid = []; // workspace group: branch, feat
244
- const right = []; // burn group: ctx, cost, lines
242
+ const segments = [];
245
243
 
244
+ // Segment 1: Model with play icon
246
245
  const modelName = payload?.model?.display_name;
247
- if (modelName) left.push(cyan(`▶ ${modelName}`));
246
+ if (modelName) {
247
+ segments.push(`${green("▶")} ${cyan(modelName)}`);
248
+ }
248
249
 
250
+ // Segment 2: Output style with document icon
249
251
  const styleName = payload?.output_style?.name;
250
- if (styleName && styleName !== "default") left.push(dim(styleName));
252
+ if (styleName && styleName !== "default") {
253
+ segments.push(`${dim("📝")} ${dim(styleName)}`);
254
+ }
251
255
 
256
+ // Segment 3: Duration with timer icon
252
257
  const durMs = payload?.cost?.total_duration_ms;
253
- if (durMs && durMs >= 1000) left.push(dim(`⏱${fmtDuration(durMs)}`));
258
+ if (durMs && durMs >= 1000) {
259
+ segments.push(`${dim("⏱")} ${dim(fmtDuration(durMs))}`);
260
+ }
254
261
 
262
+ // Segment 4: Branch with git icon
255
263
  if (git?.branch) {
256
- const tag = git.conflict ? red(`${git.branch}!CONFLICT`)
257
- : git.dirty > 0 ? yellow(`${git.branch}(±${git.dirty})`)
258
- : green(git.branch);
259
- mid.push(tag);
264
+ const branchIcon = dim("⎇");
265
+ const branchText = git.conflict ? red(`${git.branch}!CONFLICT`)
266
+ : git.dirty > 0 ? `${yellow(git.branch)}${dim("(")}${yellow(`±${git.dirty}`)}${dim(")")}`
267
+ : yellow(git.branch);
268
+ segments.push(`${branchIcon} ${branchText}`);
260
269
  }
261
270
 
271
+ // Segment 5: Feature status with checkmark/cross icon
262
272
  if (feat) {
263
- mid.push(feat.open ? magenta(`feat:${feat.open}`) : dimGreen("feat:clean"));
273
+ const featIcon = feat.open ? red("✗") : green("");
274
+ const featText = feat.open ? magenta(feat.open) : green("clean");
275
+ segments.push(`${featIcon} ${featText}`);
264
276
  }
265
277
 
278
+ // Segment 6: Context usage (bar + percentage)
266
279
  const pct = payload?.context_window?.used_percentage;
267
280
  if (typeof pct === "number") {
268
281
  const col = ctxColor(pct);
269
- right.push(`${col(bar(pct))} ${col(`${Math.round(pct)}%`)}`);
282
+ segments.push(`${col(bar(pct))} ${col(`${Math.round(pct)}%`)}`);
270
283
  }
271
284
 
285
+ // Segment 7: Cost
272
286
  const cost = payload?.cost?.total_cost_usd;
273
287
  if (typeof cost === "number" && cost > 0) {
274
- right.push(costStr(cost));
288
+ segments.push(costStr(cost));
275
289
  }
276
290
 
291
+ // Segment 8: Lines changed
277
292
  if (config.showLines) {
278
293
  const add = num(payload?.cost?.total_lines_added, 0);
279
294
  const rem = num(payload?.cost?.total_lines_removed, 0);
280
295
  if (add > 0 || rem > 0) {
281
- right.push(`${green("+" + add)}/${dimRed("-" + rem)}`);
296
+ segments.push(`${green("+" + add)}/${dimRed("-" + rem)}`);
282
297
  }
283
298
  }
284
299
 
285
- const parts = [];
286
- if (left.length) parts.push(left.join(dim("│")));
287
- if (mid.length) parts.push(mid.join(" "));
288
- if (right.length) parts.push(right.join(" "));
289
- return parts.join(" ");
300
+ return segments.join(` ${dim("│")} `);
290
301
  }
291
302
 
292
303
  // ---------------------------------------------------------------------------
293
- // Line 2 — alerts.
304
+ // Line 2 — alerts (Dashboard Style with rich icons and clear segmentation).
294
305
  // ---------------------------------------------------------------------------
295
306
  function renderLine2(payload, sessionId, config, lang) {
296
307
  if (config.compact) return "";
@@ -299,19 +310,19 @@ function renderLine2(payload, sessionId, config, lang) {
299
310
 
300
311
  // Order by severity: hardest stop first.
301
312
  if (payload?.exceeds_200k_tokens === true) {
302
- alerts.push(red(`⚠ ${t.over_200k}`));
313
+ alerts.push(`${red("⚠")} ${red(t.over_200k)}`);
303
314
  }
304
315
 
305
316
  const pct = payload?.context_window?.used_percentage;
306
317
  if (typeof pct === "number" && pct >= 80 && payload?.exceeds_200k_tokens !== true) {
307
- alerts.push(red(`⚠ ctx ${Math.round(pct)}%${t.compact_soon}`));
318
+ alerts.push(`${red("⚠")} ${red(`ctx ${Math.round(pct)}%${t.compact_soon}`)}`);
308
319
  }
309
320
 
310
321
  if (config.showRateLimit) {
311
322
  const five = payload?.rate_limits?.five_hour;
312
323
  if (five && typeof five.used_percentage === "number" && five.used_percentage >= 75) {
313
324
  const resetTxt = five.resets_at ? `${t.rate_resets}${fmtCountdown(five.resets_at)}` : "";
314
- alerts.push(yellow(`⏳ 5h limit ${Math.round(five.used_percentage)}%${resetTxt}`));
325
+ alerts.push(`${yellow("⏳")} ${yellow(`5h limit ${Math.round(five.used_percentage)}%${resetTxt}`)}`);
315
326
  }
316
327
  }
317
328
 
@@ -319,11 +330,11 @@ function renderLine2(payload, sessionId, config, lang) {
319
330
  const lb = fetchLastBlock(sessionId);
320
331
  if (lb) {
321
332
  const title = String(lb.title || "").slice(0, 40);
322
- alerts.push(red(`🚫 ${t.last_block}${title}`));
333
+ alerts.push(`${red("🚫")} ${red(`${t.last_block}${title}`)}`);
323
334
  }
324
335
  }
325
336
 
326
- return alerts.join(" ");
337
+ return alerts.join(` ${dim("")} `);
327
338
  }
328
339
 
329
340
  // ---------------------------------------------------------------------------
@@ -13,20 +13,8 @@ INPUT=$(cat)
13
13
  # when jq is missing — silently skipping the structural check on jq-less
14
14
  # environments (minimal CI, Windows without WSL+brew) was a known audit hole.
15
15
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
16
- have_jq() {
17
- [ "${AHK_DISABLE_JQ:-}" = "1" ] && return 1
18
- command -v jq >/dev/null 2>&1
19
- }
20
- have_jp() {
21
- have_jq && return 0
22
- command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/_lib/json-pick.mjs" ] && return 0
23
- return 1
24
- }
25
- jp() {
26
- if have_jq; then jq -r "$1"
27
- else node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1"
28
- fi
29
- }
16
+ _LIB_DIR="$SCRIPT_DIR/_lib"
17
+ . "$_LIB_DIR/jp.sh"
30
18
  if ! have_jp; then
31
19
  echo "[ahk] structural-test-on-edit: no JSON parser available (need jq OR node + scripts/_lib/json-pick.mjs)." >&2
32
20
  exit 0
@@ -14,20 +14,9 @@ set -eo pipefail
14
14
 
15
15
  INPUT=$(cat)
16
16
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17
- have_jq() {
18
- [ "${AHK_DISABLE_JQ:-}" = "1" ] && return 1
19
- command -v jq >/dev/null 2>&1
20
- }
21
- have_jp() {
22
- have_jq && return 0
23
- command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/_lib/json-pick.mjs" ] && return 0
24
- return 1
25
- }
26
- jp() {
27
- if have_jq; then jq -r "$1"
28
- else node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1"
29
- fi
30
- }
17
+ _LIB_DIR="$SCRIPT_DIR/_lib"
18
+ . "$_LIB_DIR/jp.sh"
19
+ . "$_LIB_DIR/telemetry.sh"
31
20
 
32
21
  SUBAGENT="(unknown)"
33
22
  if have_jp; then
@@ -35,12 +24,12 @@ if have_jp; then
35
24
  fi
36
25
 
37
26
  # Telemetry first so we record every subagent boundary, even if the
38
- # structural-test bails below.
39
- mkdir -p .harness
27
+ # structural-test bails below. telemetry_append handles rotation.
40
28
  TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
41
29
  SHA=$(git rev-parse --short HEAD 2>/dev/null || echo 'no-git')
42
- printf '{"ts":"%s","event":"subagent_stop","subagent":"%s","sha":"%s"}\n' \
43
- "$TS" "$SUBAGENT" "$SHA" >> .harness/telemetry.jsonl
30
+ LINE=$(printf '{"ts":"%s","event":"subagent_stop","subagent":"%s","sha":"%s"}' \
31
+ "$TS" "$SUBAGENT" "$SHA")
32
+ telemetry_append "$LINE"
44
33
 
45
34
  # Skip if structural test disabled.
46
35
  if [ -f harness.config.json ] \
@@ -8,25 +8,20 @@
8
8
  # v0.7: migrated from `command -v jq` fail-open gate to the kit's jp() helper
9
9
  # so the telemetry record still gets written on jq-less CI / Windows. Without
10
10
  # the migration, telemetry quietly went dark anywhere jq wasn't installed.
11
+ # v0.10.3: jp/have_jq/have_jp extracted to _lib/jp.sh; AHK_DISABLE_TELEMETRY
12
+ # opt-out + AHK_TELEMETRY_MAX_LINES rotation added.
11
13
  set -e
12
14
 
15
+ # Opt-out: respect AHK_DISABLE_TELEMETRY=1 before reading stdin so the user
16
+ # can fully disable observability without removing the hook from settings.
17
+ [ "${AHK_DISABLE_TELEMETRY:-}" = "1" ] && exit 0
18
+
13
19
  INPUT=$(cat)
14
20
 
15
21
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
16
- have_jq() {
17
- [ "${AHK_DISABLE_JQ:-}" = "1" ] && return 1
18
- command -v jq >/dev/null 2>&1
19
- }
20
- have_jp() {
21
- have_jq && return 0
22
- command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/_lib/json-pick.mjs" ] && return 0
23
- return 1
24
- }
25
- jp() {
26
- if have_jq; then jq -r "$1"
27
- else node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1"
28
- fi
29
- }
22
+ _LIB_DIR="$SCRIPT_DIR/_lib"
23
+ . "$_LIB_DIR/jp.sh"
24
+ . "$_LIB_DIR/telemetry.sh"
30
25
  if ! have_jp; then exit 0; fi
31
26
 
32
27
  TOOL=$(echo "$INPUT" | jp '.tool_name // empty')
@@ -35,14 +30,13 @@ TOOL=$(echo "$INPUT" | jp '.tool_name // empty')
35
30
  SKILL=$(echo "$INPUT" | jp '.tool_input.skill // empty')
36
31
  [ -z "$SKILL" ] && exit 0
37
32
 
38
- mkdir -p .harness
39
33
  TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
40
34
  SHA=$(git rev-parse --short HEAD 2>/dev/null || echo 'no-git')
41
35
 
42
36
  # Compose JSONL line by hand — same shape as the previous jq-built record.
43
- # Quoting via printf '%s' so embedded spaces in skill names don't break the
44
- # line. Skill names are constrained to `[a-z0-9-]+` upstream so we don't
45
- # need full JSON escaping here.
46
- printf '{"ts":"%s","event":"skill_invoked","skill":"%s","sha":"%s"}\n' \
47
- "$TS" "$SKILL" "$SHA" >> .harness/telemetry.jsonl
37
+ # Skill names are constrained to `[a-z0-9-]+` upstream so we don't need full
38
+ # JSON escaping here. telemetry_append handles mkdir, append, and rotation.
39
+ LINE=$(printf '{"ts":"%s","event":"skill_invoked","skill":"%s","sha":"%s"}' \
40
+ "$TS" "$SKILL" "$SHA")
41
+ telemetry_append "$LINE"
48
42
  exit 0
@@ -14,26 +14,8 @@ set -eo pipefail
14
14
 
15
15
  INPUT=$(cat)
16
16
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17
- have_jq() {
18
- [ "${AHK_DISABLE_JQ:-}" = "1" ] && return 1
19
- command -v jq >/dev/null 2>&1
20
- }
21
- have_jp() {
22
- have_jq && return 0
23
- command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/_lib/json-pick.mjs" ] && return 0
24
- return 1
25
- }
26
- jp() {
27
- if have_jq; then
28
- if [ -n "$2" ]; then jq -r "$1" "$2"; else jq -r "$1"; fi
29
- else
30
- if [ -n "$2" ]; then
31
- node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1" "$2"
32
- else
33
- node "$SCRIPT_DIR/_lib/json-pick.mjs" "$1"
34
- fi
35
- fi
36
- }
17
+ _LIB_DIR="$SCRIPT_DIR/_lib"
18
+ . "$_LIB_DIR/jp.sh"
37
19
 
38
20
  if ! have_jp; then
39
21
  exit 0