a49e7ed2af94e7bfe5a67134b6212dfa0d2ec01a
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a49e7ed2af |
fix(plugin): the hooks need no jq and no tac (#4107)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 49s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / Python tests (push) Failing after 1m7s
CI & Build / Build & push image (push) Skipped
Every hook opened `command -v jq >/dev/null 2>&1 || exit 0`, so on a machine without jq the operator got no session context, no rules, no prior art and no process sync — and not one word saying why, because `exit 0` is indistinguishable from "ran fine, nothing to say". jq is absent by default on macOS, on the Debian/Ubuntu slim images, on Alpine and in most CI containers. That is not a prerequisite to document; it is the plugin handing its own packaging problem to whoever installs it. `tac` was worse: GNU-only, so the prior-art hook's enclosing-definition arm did nothing at all on every Mac, silently, from the day it shipped. It is not replaced but removed — scribe_defs judges each line independently, so extracting forward and taking `tail -1` is the same answer as reversing and taking the head, and it drops the early-exit `head` that #4042 was filed for. No server contract changed, so a lagging plugin cache keeps working. scribe_json.awk JSON -> IDX<TAB>PATH<TAB>VALUE. Two modes: `whole` for an event or a response body, `lines` for a transcript, where an unparseable record is dropped and the rest still read — the `map(try fromjson catch empty)` the jq program opened with. Arrays also report their LENGTH at `[#]`, which is what keeps "zero notes" distinct from "no answer" (#2932). scribe_turn.awk the turn-bounding program, replacing the thirty lines of jq in the Stop hook. scribe_defs.sh scribe_json_flat / _pick / _list / _len / _list_minus read, scribe_json_out writes the envelope (five copies of one shape, gone), scribe_urlenc replaces `jq -sRr '@uri'`. Percent-encoding goes through `od -tu1` rather than an awk character loop on purpose: awk's idea of a character follows the locale, so gawk reads an accented letter as one and mawk as two, and an encoder built on substr() would emit a different URL depending on which awk is installed. Encoding is defined on bytes. Verified byte-identical to `jq -sRr '@uri'`. Measured, not assumed. The per-event path costs 8ms against jq's 3ms. The transcript path was 70x slower until two fixes: the Stop hook now finds where the turn starts with a fixed-string grep before parsing (a needle carrying unescaped quotes cannot occur inside a JSON string, so it matches only at a record's top level — checked against a full JSON parse of a 27MB transcript: 152 prompt records, 152 matches, no misses, no extras), and the parser reads each token out of a 1024-byte window instead of copying the rest of the buffer per token, which was quadratic in line length on the 400KB tool results a transcript carries. Differential-tested against the jq program it replaces over 724 windows cut from three real transcripts — 724 identical, 0 mismatched, 45 of them exercising a real task close and a real reply. That sweep is what caught `scribe_turn.awk` never setting FS, which truncated every multi-word reply at its first space and was invisible to a test whose replies were all empty. check_plugin.py's `jq -R` lint becomes a guard against either binary coming back, and three smoke checks lose their `shutil.which("jq")` skip. jq is not in `ci-python` either, so those three announced a skip on every CI run and had never once run there: removing the dependency from the product also closed a permanent hole in its verification. They pass now across all ten hooks. tests/test_hook_json_reader.py is a differential against Python's `json` over nested objects, arrays, unicode, escapes, control characters, empty cases and a value longer than the token window, plus the envelope, the encoder and the turn analyzer. 139 cases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy |
||
|
|
a8f35e465e |
refactor(plugin+tests): the last two parallel-family gaps — pageable list tools, one config preamble (#2278)
CI & Build / Python lint (push) Successful in 4s
CI & Build / integration (push) Successful in 1m34s
CI & Build / Python tests (push) Successful in 2m21s
CI & Build / Plugin hooks (push) Successful in 27s
CI & Build / TypeScript typecheck (push) Successful in 36s
CI & Build / Build & push image (push) Successful in 25s
DRY pass 3's remainder. Both halves start from enumeration, because the task's
candidate list was hypotheses and the process requires counting before
proposing — and counting changed the answer twice.
## The list_* family: a limit with no offset
Enumerated all 19 `list_*` MCP tools first. They are genuinely heterogeneous —
8 take `project_id`, 6 take `limit`, six take no arguments at all — so a
common-parameter guard would invent a convention the API does not have, which
is the over-DRY trap (§5). One contract IS real: a `limit` without an `offset`
is a truncation with no continuation. The caller is told there are 250 results,
handed 50, and given no way to ask for the rest.
Two tools had it, and both were capped over a service that already accepted an
offset: `snippets_svc.list_snippets(offset=0)` was simply not exposed, and
`list_processes` passed a hardcoded `offset=0` into `query_knowledge`. The
capability existed one layer down in both; only the door was missing — the
missing-sibling shape exactly. Both now expose it.
`tests/test_mcp_list_family.py` guards it, with `list_tags` exempted for a
stated reason (a ranked top-N over a bounded vocabulary has no "rest" to page
into). Candidates derived, decision explicit, same design as test_mcp_auth —
plus the reverse checks: a stale exemption, and an offset with no limit, which
would page through an unbounded result set. Verified non-vacuous by running the
sweep against the pre-fix tree, where it fails naming both tools.
## The verb pairs: no finding, which is the finding
`preview`/`apply` and `dry_run`/`commit` do not exist anywhere in the 102
tools — those were guesses about a shape Scribe never adopted. `count_*` does
not exist either. Of the create/delete stems only `project_rule` lacks a
`delete_X`, and deliberately: a project rule IS a rule, `delete_rule` removes
it, and the docstring says so. `force` sits on 6 of 7 duplicate-gated creates;
the exception is `create_system`, whose gate is an exact normalized-NAME match
rather than a semantic near-match — forcing it would split one area's records
across two piles, which its own message explains. No guard added: it would
need a seven-entry exemption list to defend against a hypothetical. Recorded
on the leave-alone list instead, which the process asks for by name.
## The hook config preamble
Not 3 of 6 hooks as recorded — all FIVE carried their own copy, and of four
lines rather than two. The extra two are a guard treating an unexpanded
`${...}` placeholder as unset, so it is never sent as a garbage Bearer token:
precisely the correctness detail a sixth hook would omit with nothing failing
loudly. Now `scribe_config` in scribe_defs.sh, which also declares the two
names it owns. It sets globals rather than echoing, so a token never passes
through a subshell's output where xtrace or a log could catch it, and returns
a status so a caller can bail (`|| exit 0`) or continue degraded — the
session-context hook still owes its static floor when Scribe is unconfigured.
`check_plugin.py` now runs shellcheck with `-x`. Without it the shared helpers
were invisible: every variable they set read as unassigned and every bug inside
them went unlinted at the call site, which is the opposite of what sharing them
was for. All twelve fail-open scenarios still pass, and all five hooks were
probed live against the instance — prior_art and after_write both still name
canon, autoinject returns context, session_context serves 11k chars of rules,
sync_processes stays silent. Plugin 0.1.46 (#2209).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
c569cdd0eb |
fix(plugin): every hook's credential + URL encoding was broken
Three defects, each of which independently made a hook a no-op, and all three failing silently — which is why the whole dynamic side of the plugin looked "shipped" while doing nothing. 1. Wrong env var case. Claude Code exports userConfig to hooks as CLAUDE_PLUGIN_OPTION_<KEY> with the key UPPERCASED. All four hooks read CLAUDE_PLUGIN_OPTION_api_endpoint / _api_token, so both values were always empty. That killed the SessionStart dynamic tier, process sync, prompt auto-inject, and the write-path prior-art trigger at once. 2. jq -rR is line-oriented. `@uri` under -R encodes input LINE BY LINE, so a multi-line payload came back as several encoded lines joined by raw newlines — an invalid URL, curl fails, hook exits 0 in silence. Now -sRr. This one hid behind (1): auto-inject only ever worked for single-line prompts, and prior-art (which posts code, always multi-line) could never have worked at all. 3. cut -c1-1200 caps each LINE, not the payload, so the prior-art code budget wasn't a budget. Now head -c 1200. Also widens the SessionStart warning: "neither URL nor token arrived" used to be treated as a benign unconfigured install and stayed quiet. That is exactly the state defect (1) produced, so the one install state that most needed a signal was the only one that emitted none. It now says so, and names the two other features it silently disables. Verified against the live instance: dynamic rules + project context load, auto-inject surfaces #2192 on a multi-line prompt, and the write-path trigger returns the [here] place-arm hit on a multi-line edit with session dedup suppressing the repeat. Refs #2198, #2082 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs |
||
|
|
33f9a0a4d4 |
feat(plugin): Phase 4 — Scribe Processes auto-surface as local skills
#755 Phase 4. Saved Scribe Processes (DRY pass, Drift Audit, …) now surface as auto-triggered Claude Code skills instead of pull-only get_process calls. Design correction vs the plan: stubs live in the USER's ~/.claude/skills/, NOT plugin/skills/_instance/. The plugin is git-cloned and identical per install, so instance-specific generated files can't ride in it; personal skills are live-detected within the session (verified via claude-code-guide). MCP prompts were the alternative but are pull-only (no relevance auto-surface), so skills are the right primitive. - backend: GET /api/plugin/processes manifest (services/plugin_context. build_process_manifest) — {name, slug, description} per Process; description is the auto-surface trigger (title + preview); slugs deduped, blanks skipped. - plugin: scribe_sync_processes.sh writes ~/.claude/skills/scribe-proc-<slug>/ SKILL.md (body = "call get_process(name), follow verbatim") and PRUNES stale scribe-proc-* stubs. Fail-open + silent; a transient fetch failure never wipes existing stubs. Runs as a 2nd SessionStart hook + via the /scribe:sync command. - plugin.json 0.1.7 → 0.1.8; README updated. - tests: build_process_manifest (render, slug dedupe, blank-title skip, preview truncation). Sync script's write+prune validated in isolation (plugin/** is not CI-covered): correct stubs created, stale pruned, unrelated skills untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |