A directory names its project; a compaction is told what to keep; list rows say what a record is #161
Merged
bvandeusen
merged 5 commits from 2026-09-16 08:57:46 -04:00
dev into main
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dd08f9858c |
fix(tests): the rule-33 contract follows the scope key into the shared helper (#4085)
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 52s
CI & Build / Python tests (push) Successful in 1m29s
CI & Build / Build & push image (push) Successful in 21s
Two interface-contract tests read each hook's own source for the query args it sends. The project-scope key now comes from scribe_scope_query, so `repo=` was no longer spelled in the hook and both read that as the hook having stopped sending it. They now pin the pair: the hook delegates to the helper, and the helper emits exactly `repo=` and `project_id=`. That is a stronger assertion than the one it replaces — it catches a hook rolling its own scope resolution again, and it pins the new key, which the route must read for a non-git session to reach its project at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy |
||
|
|
aa94c73d9e |
feat(plugin): a directory says which project it belongs to, git repo or not (#4085)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 47s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Failing after 1m1s
CI & Build / Build & push image (push) Skipped
All six hooks scoped their requests one way: `git remote get-url origin`,
resolved server-side through the repo bindings. That key does not exist
outside a git repo, so a session in a plain directory was unscoped in every
hook at once — no project context, no prior-art scoping, no project rules —
and silently, because a missing remote is indistinguishable from a remote
nobody bound.
A `.scribe` file is the second key, read by the shared scribe_scope_query so a
directory scopes the same way everywhere:
{"instance": "https://scribe.example.com", "project_id": 2, "project": "…"}
`instance` is why the file is not just a number: an id is a different project
on every Scribe, so a marker that travels — a copied directory, a shared
machine, a repo someone else clones — would otherwise scope the session to the
wrong project without a word. Compared host-only, and a mismatch drops the id:
no project beats the wrong project. A bare integer is accepted too, since it
is what a person writes by hand. The marker beats a git remote — someone put
the file there on purpose — which is also how a directory overrides its
binding.
Two things it found on the way:
* An explicit project_id that did not resolve rendered NO message at all —
the branch hung off `if project_id` as an `elif`, so a caller holding a
pointer it believed in got a context that silently omitted the project it
had asked for. Now reported.
* The refusal reason was a global set inside a function every caller reads
through `$( )`. The assignment died with the subshell, leaving the caller
to read an unset variable under `set -u` — which aborts the hook and costs
the whole session's SessionStart context, to fetch a warning about a file.
It comes back through stdout with the id instead, and a test pins it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
|
||
|
|
47388eda36 |
fix(tests): the no-block guard reads the shell, not the comment explaining it (#3680)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 42s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / Python tests (push) Successful in 1m33s
CI & Build / Build & push image (push) Successful in 19s
The hook's header quotes `{"decision":"block"}` while explaining why this hook
must never emit one, and the instructions it prints use "decision" in a
sentence. Grepping the whole file caught both and failed the guard on the file
doing its job.
Strips comments and the heredoc first, and guards the stripper: `_code()`
returning nothing would make all three static checks pass against an empty
string, which is the circularity rule 167 is about.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
|
||
|
|
4b8d22e3ec |
feat(plugin): a PreCompact hook tells the summarizer what must survive (#3680)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 45s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Failing after 1m0s
CI & Build / Build & push image (push) Skipped
Spike #3680 asked whether a PreCompact hook can reach the model. Read out of the installed Claude Code build (2.1.273), the answer is yes — through a different channel than note #3679 assumed: * `hookSpecificOutput.additionalContext` is NEVER read on PreCompact. The hook-output schema has no PreCompact variant; the field is honoured for SessionStart, SubagentStart and Stop, and silently dropped here. * A PreCompact hook's STDOUT becomes `newCustomInstructions`, merged with the operator's own `/compact` instructions and passed into the prompt that writes the summary. Manual, auto and partial compaction all do this. So the hook does not interrupt the compaction — it steers the summary, which is what the next turn reads. Blocking is the thing not to do: a PreCompact block SKIPS compaction, tells the model nothing, and leaves the session running on uncompacted with no summary at all. scribe_precompact_preserve.sh names what the summary is the only copy of: Scribe record ids WITH titles, the in-progress task and its milestone, work done but not yet recorded, governing rules, and unfinished operator asks. No network and no config — what must survive is already in the conversation being summarized; the hook only says which parts are load-bearing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy |
||
|
|
5e4fd017ae |
fix(mcp): list tools return rows that say what a record is, not what it says (#4061)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 49s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m31s
CI & Build / Build & push image (push) Successful in 22s
list_tasks returned every row's to_dict(), body included: a project's todo list came to 93-165k characters, past what an MCP client accepts inline, so the list arrived as a file to page through (the #4045 failure, one call over). - notes.brief_row: id, title, type, project, tags, updated_at; for tasks, status, kind, priority, milestone id and title; description, parent and due date only when set. - milestones.titles_for: one query for the milestone titles a page of rows names. - Brief rows on list_tasks, list_notes, get_milestone's steps, get_system and list_system_records. get_task / get_note / get_snippet read a record in full, and each docstring says so. - tests/test_list_rows_brief.py pins the ceiling: 100 rows of ~5k-character step plans stay under 40k characters. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy |