Reply shapes reach the reply, a Stop hook checks completion reports, and hook pipes keep their output #156

Merged
bvandeusen merged 6 commits from dev into main 2026-09-14 18:57:07 -04:00
Owner

Six commits from dev. They finish milestone 410 "One owner per piece of guidance" (step 8) and build steps 4–5 of milestone 409 "Response shapes — replies say where the work stands", with two hook fixes found along the way.

Guidance

  • using-scribe states the retrieval reflex once (#4039). "Rules arrive by retrieval; ask before a consequential act; an empty session isn't an empty rulebook" now lives in reflex 2 alone. The adapter's session-start text no longer restates it.

Response shapes (milestone 409)

  • An operator's own reply shapes reach the reply they're about (#4013).
    • Closing a task with update_task returns matching preferences as reply_preferences. It uses a kind-filtered preference search keyed on each preference's own trigger and logs under report_preference in retrieval_logs.
    • For every other kind of reply, the reporting-back skill has the agent search for its shape preferences before writing.
  • A Stop hook checks completion reports (#4014). When a turn closes a task, scribe_report_check.sh checks the final reply says where the work sits, what needs the operator, and what comes next.
    • If any of those are missing, it sends the reply back once, using the reason returned by the new GET /api/plugin/report-check.
    • It blocks only when the block was recorded, so an unconfigured or unreachable instance never blocks.
    • Outcomes go to app_logs (category plugin, action report_check) for the milestone's step 6 to read.

Fixes

  • Stop hook prefilter (#4041). tail | grep -q under pipefail failed exactly when it matched. Test transcripts also weren't compact JSON.
  • Duplicate check wiped its own output (#4042). Under pipefail, $(… | head -N) || v="" discarded head's output when the producer was still writing, so the by-name duplicate arm went silent for heavily duplicated names. Fixed at five sites, and check_plugin now carries a known-bad pattern for the shape.
  • Goal line cut at a word (#4036). The session-start Goal line now cuts at a word and says where the rest is.

Verification

  • CI green on the dev head 0fab082 (run #519): lint, typecheck, plugin hooks, Python tests, integration, image build.
  • Plugin version minted: 2026.09.14.2254.

🤖 Generated with Claude Code

Six commits from dev. They finish milestone 410 "One owner per piece of guidance" (step 8) and build steps 4–5 of milestone 409 "Response shapes — replies say where the work stands", with two hook fixes found along the way. ## Guidance - **using-scribe states the retrieval reflex once** (#4039). "Rules arrive by retrieval; ask before a consequential act; an empty session isn't an empty rulebook" now lives in reflex 2 alone. The adapter's session-start text no longer restates it. ## Response shapes (milestone 409) - **An operator's own reply shapes reach the reply they're about** (#4013). - Closing a task with `update_task` returns matching preferences as `reply_preferences`. It uses a kind-filtered preference search keyed on each preference's own trigger and logs under `report_preference` in retrieval_logs. - For every other kind of reply, the reporting-back skill has the agent search for its shape preferences before writing. - **A Stop hook checks completion reports** (#4014). When a turn closes a task, `scribe_report_check.sh` checks the final reply says where the work sits, what needs the operator, and what comes next. - If any of those are missing, it sends the reply back once, using the reason returned by the new `GET /api/plugin/report-check`. - It blocks only when the block was recorded, so an unconfigured or unreachable instance never blocks. - Outcomes go to app_logs (category `plugin`, action `report_check`) for the milestone's step 6 to read. ## Fixes - **Stop hook prefilter** (#4041). `tail | grep -q` under pipefail failed exactly when it matched. Test transcripts also weren't compact JSON. - **Duplicate check wiped its own output** (#4042). Under pipefail, `$(… | head -N) || v=""` discarded head's output when the producer was still writing, so the by-name duplicate arm went silent for heavily duplicated names. Fixed at five sites, and check_plugin now carries a known-bad pattern for the shape. - **Goal line cut at a word** (#4036). The session-start Goal line now cuts at a word and says where the rest is. ## Verification - CI green on the dev head `0fab082` (run #519): lint, typecheck, plugin hooks, Python tests, integration, image build. - Plugin version minted: `2026.09.14.2254`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bvandeusen added 6 commits 2026-09-14 18:57:02 -04:00
fix(plugin): the session-start Goal line cuts at a word and says where the rest is (#4036)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 58s
CI & Build / Python tests (push) Successful in 1m27s
CI & Build / Build & push image (push) Successful in 26s
21343dc3aa
A raw 200-char slice ended mid-word with nothing marking the cut, so a
reader took half a sentence for the whole goal. _goal_line flattens the
goal to one line, trims at a word break with an ellipsis, and points at
enter_project(id) when it cut.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(410): using-scribe states the retrieval reflex once, and the adapter stops restating it (#4039)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 55s
CI & Build / Python tests (push) Successful in 1m27s
CI & Build / Build & push image (push) Successful in 13s
9071cb05da
Step 7's live check (#4034 "a real session shows each topic stated once")
found nothing copied across surfaces, but found the owner repeating itself:
using-scribe said "rules are retrieved; ask before a consequential act; an
empty session is not an empty rulebook" in "Do this first", in reflex 2, and
twice more further down reflex 2.

- using-scribe: reflex 2 now holds the one full statement (how rules arrive,
  why that is the surface working, what silence means, when to ask). "Do this
  first" keeps the enter_project step and a short lead-in pointing at reflex
  2. "A retrieved rule outranks a default habit" and "Ask hardest where you
  feel most certain" stay, since each adds something.
- Adapter static context: "Lines injected beside your work" keeps only the
  Claude Code timing and leaves "never the whole set" to the skill. "Keep one
  copy" names CLAUDE.md and auto-memory and points at using-scribe, which
  already says to leave a client's settings as they are.
- The ownership registry's markers and statement for the topic still sit on
  using-scribe, and nothing else changed owner. Plugin version minted.

Sizes: using-scribe 18,823 -> 18,197 chars; scribe_static_context.md
1,797 -> 1,714.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(409): an operator's own reply shapes reach the reply they are about (#4013)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / integration (push) Successful in 48s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / Python tests (push) Successful in 1m24s
CI & Build / Build & push image (push) Successful in 23s
921565696c
The reporting-back skill ships default shapes; an operator's adjustments to
them are preference records. Prompt-time retrieval matches the operator's
message, and a shape preference is about the reply, so those preferences were
on file and never arrived. Operator's decision (logged on #4013): the server
delivers them for a completion report, and the skill asks for every other kind.

- Completion reports (option C): closing a task with update_task runs a
  kind-filtered preference search for the moment "writing the completion
  report after finishing a task" and returns matches as `reply_preferences`
  ({id, title, statement, kind}), with a sentence added to `report_back`
  naming the key. A preference says it is about completion reports through
  its own when_to_apply; no tag or column. Omitted when nothing matches, and
  the lookup fails open.
- Telemetry: every call logs to retrieval_logs under `report_preference`
  (empty calls included; a search that never ran writes no row) and hits are
  recorded surfaced. The source is ranked, so it counts toward pull-through.
  The bar is the prompt arm's setting until step 6 reads this source's near
  misses.
- Every other reply (option A): reporting-back gains "The operator's own
  shapes come first". Before a finding, decision, handoff or "where are we",
  search(content_type="rule") in the words of that moment and follow what
  comes back. Registered in the ownership guard with reporting-back as owner.
- Loading reply shapes at session start (option B) was rejected: it would be
  a small copy of the preloading milestone 394 retired.

Domain-neutral query (pinned); works on an install with no preferences.
Plugin version minted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(409): a Stop hook checks that a reply closing a task has the completion sections (#4014)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 47s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / Python tests (push) Failing after 59s
CI & Build / Build & push image (push) Skipped
dd80e2bc86
Everything else Scribe gives an agent arrives before the reply is written. A
Stop hook is the one moment the finished reply exists, so it is the last
chance to fix a report the operator can't read, and the only place adherence
to the shape can be measured.

- plugin/hooks/scribe_report_check.sh (Stop): deterministic, no model call.
  1. Did this turn close a task? That means an update_task/create_task call
     with status "done" since the turn's prompt, whose tool_result is not an
     error. Otherwise it stays silent, which covers most turns (one grep).
  2. Does the reply that ends the turn say where the work sits (a record by
     id and title, or step N of M), what needs the operator, and what comes
     next? Matched on those words, not on exact headings.
  3. If sections are missing, it blocks once. With stop_hook_active set, a
     rewrite is recorded (passed_after_rewrite / missing_after_rewrite) and
     never blocked again. A block loop started by another plugin (no marker
     from this hook) is left alone.
- Measured: every checked reply is reported to GET /api/plugin/report-check
  (passed / blocked / after rewrite). Turns that close nothing are not
  reported; they would cost a request per turn and add nothing to the rate.
  Outcomes go to app_logs as category "plugin", action "report_check".
- It blocks only when the block was recorded, and only in the server's words.
  The endpoint returns the block reason, so the hook carries timing and
  transport only (PACKAGING.md), and an unconfigured or unreachable instance
  never stops a session.
- The transcript format is read from real transcripts and marked in the hook
  as observed rather than documented. The Stop contract (transcript_path,
  stop_hook_active, decision/reason, no matcher, SubagentStop separate) was
  checked against the Claude Code hooks docs. A prompt-type hook was not
  needed: the deterministic check passed a real completion report from this
  session and blocked a stripped one.
- A pipefail trap was caught while exercising the hook: `tail | grep -q`
  reports failure exactly when grep matches, because tail dies of SIGPIPE.
  The prefilter reads through process substitution; the section checks use
  here-strings.
- Tests: an end-to-end hook suite over synthetic transcripts and the shared
  HTTP sink (silence, pass, server-worded block, rewrite recorded, foreign
  loop, errored write, earlier turn, unwritten reply, no recorded check, bare
  id), and service tests for the reason wording and the outcome record. Smoke
  event added to check_plugin; README and PACKAGING list the hook and
  endpoint. Plugin version minted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(409): the report check's prefilter no longer depends on compact JSON (#4014)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 43s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / Python tests (push) Successful in 1m31s
CI & Build / Build & push image (push) Successful in 23s
c7531d3700
CI run #517 failed five of the new hook tests, all of them expecting a
request that never went out. The prefilter matched `"name":"…update_task"`
with no space after the colon, which is how Claude Code writes its
transcripts, while the tests wrote theirs with json.dumps defaults
(`"name": "…"`). The hook exited at the prefilter for every test transcript.
The silent-case tests passed for the same wrong reason.

- The prefilter now allows whitespace after the colon. The jq parse behind it
  never depended on formatting.
- The test transcripts are written compact, matching the real file. The
  silent-case tests now reach the turn parse rather than stopping at the
  prefilter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(plugin): an early-exiting head no longer voids its own output under pipefail (#4042)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 56s
CI & Build / Python tests (push) Successful in 1m31s
CI & Build / Build & push image (push) Successful in 14s
0fab08276c
In a repo where 3,000 files define `slug`, scribe_local_dups printed
nothing; with 3 files it printed the duplicate line. Every hook runs under
`set -uo pipefail`, and `hits=$(git grep -l … | head -4) || hits=""` lost
head's four lines whenever git grep was still writing when head exited. That
is a SIGPIPE, the substitution fails, and the outer fallback wipes the result.
So the by-name duplicate arm went silent for exactly the most-duplicated
names. Found while fixing the same trap in the new Stop hook (#4041).

- The fallback moves inside the substitution, `$(… | head -N || true)`, at all
  five sites: scribe_defs.sh (local dups), scribe_prior_art.sh (names,
  old_first, shapes) and scribe_after_write.sh (names). A real upstream
  failure still yields empty output.
- check_plugin gains a known-bad pattern for the shape, so no hook can bring
  it back.
- Tests: the real function against a 3,000-file repo (past the pipe buffer),
  and the pattern shown to flag the old shape and pass the fix. Plugin
  version minted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bvandeusen merged commit 0b52f894d8 into main 2026-09-14 18:57:07 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/FabledScribe#156