- Added support for caching the stable prefix of the system prompt for Anthropic models. - Introduced a new option `cacheSystemPrefix` in SessionOptions to enable caching. - Implemented a nudge escalation system that reminds users to update the TODO.md file, capped at three nudges. - Created a new `scaffoldWorkflowFiles` function to generate TODO.md, ROADMAP.md, and docs/ directory when they are missing. - Updated the App component to scaffold workflow files during initialization. - Added hooks functionality to allow external scripts to modify tool input and manage approvals. - Implemented tests for the new hooks functionality, ensuring proper approval and execution flow.
6.5 KiB
v9 — Provider prompt caching, registry trust, external hooks, /init scaffold, workflow plan nudge, nudge escalation, skill autoload
Date: 2026-09-09 Status: spec
Why
User asked what was missing; recommended: provider-side prompt caching (#1),
registry trust (#3), external hooks (#4), /init TODO/ROADMAP scaffold (#5),
workflow->plan skill nudge (#7), nudge escalation (#6). Structured diff
review (#2) is deferred: the SDK's tool-approval model cannot express
per-hunk approval, and it needs a rewrite hook that does not exist yet. This
spec covers the six implementable items.
Scope per item
1. Provider prompt caching (ROADMAP Next #1)
Client-side memoization already makes the system prompt byte-identical across steps when nothing volatile changed. The remaining half: tell the provider to cache the stable prefix.
- Anthropic:
cache_control: { type: 'ephemeral' }on the system prompt block when the model is Anthropic. SDK supportssystem: [{ type: 'text', text, cache_control }]. - OpenAI: automatic prefix caching — nothing to send; skip.
- Implement:
src/prompt.tssplits the rendered system prompt into a stable prefix (everything before the volatile notebook/memory/skills suffix) and the volatile suffix;session.tsdecides where to cut based on version counters. When only volatile parts changed (notebook/memory/etc), keep the stable prefix byte-identical and mark it cacheable; the volatile tail rides the same request but does not invalidate the prefix cache. - Provider routing:
Sessionknows its model provider viaopts.modelId/ aprovideroption. AddcacheSystemPrefix?: booleanoption; enable when provider is anthropic. SDK's owncache_controlfor Anthropic system arrays:system: [...]accepts per-block cache_control. - Tests: unit test that the splitter produces the same stable prefix across
differing notebook versions; an Anthropic-format prompt carries
cache_controlon the stable block; OpenAI-format omits it.
3. Registry trust (ROADMAP Next)
An index is trusted for its contents, not its authorship. Add publisher signature verification for registry entries:
src/registry.tsmanifest gains optionalsignature+signerfields.- A pinned public key per publisher in config (
registry.publishers[<name>] = ed25519 pubkey). Use Node'scrypto.verifywith ed25519 (Node ≥ 16 hascrypto.verifyfor ed25519 viacreatePublicKey). - On install: when a manifest has
signature, verify against the configured publisher key; mismatch → refusal naming the publisher and the expected key. When the manifest has no signature → refused with "unsigned; add the publisher key or install manually" unlessregistry.allowUnsigned. - Legacy manifests (no signature field) remain installable only with
allowUnsigned; a signed one with an unknown publisher → refusal. - Tests: signature verifies; tampered body fails; unsigned refused unless allowUnsigned; unknown publisher refused.
4. External hooks (ROADMAP Later)
Pre-tool hooks that can rewrite tool input, with a trust story like Codex:
.shiro/hooks/directory; each hook is an executable + a small manifest (name,hook=pre_tool|after_turn| ...;tools=["*"]or names).- On load, hash each hook file (sha256); first run, hash is shown and the
user approves or denies (approval prompt). The hash is recorded in
~/.shiro-neko/hooks.json(name → hash → approved). A hook whose hash changed since approval is refused until re-approved. pre_tool: stdin gets{tool, input, cwd}, stdout gets{"allow": true, "input": {...rewritten...}}or{"allow": false, "reason": ...}or{"blocked": "..."}. A throwing/odd exit is treated as a block.after_turn: stdin gets a turn summary; output ignored.- These extend
PluginHost.guard(beforeToolCall) — a hook runs after the compiled plugins and before the permission check, so a hook cannot bypass the safety rules but can rewrite or refuse. - Cap:
hookTimeoutMsdefault 5000; a hook that hangs is killed. - Tests: manifest parse; pre_tool rewrites input (default allow); pre_tool refusal blocks; unknown hook hash blocks until approved; after_turn runs; timeout kills a hung hook; a hook that returns garbage blocks.
5. /init scaffold TODO/ROADMAP/docs
/init already writes AGENTS.md. Extend it to optionally scaffold the
project-workflow files too:
initPromptstays; after the agent writes AGENTS.md, if the repo lacks TODO.md/ROADMAP.md/docs and the user wants the scaffold, write:TODO.md:# TODO\n\nNow\n\nNext\n\nMaintenance\n\nROADMAP.md:# Roadmap\n\n## Next\n\ndocs/empty dir (or a placeholder README)
- CLI flag
/initgains no new args; the agent's prompt gains the option. Headless:--init-scaffoldflag writes the files directly (no model). - Tests: scaffold produces the three files when missing; does not overwrite existing ones.
6. Workflow nudge escalation
Today: one nudge per session, then silence. Change to a gentle ladder:
- Nudge 1: same as today.
- Nudge 2 (if a later turn also writes without todo_update): "still no update to TODO.md; the project expects its task list kept current."
- Nudge 3: final, "last reminder; update TODO.md when you have a moment."
- Cap at 3 total (never infinite).
workflowNudged→workflowNudgeCount. - Tests: third nudge is the last; count capped.
7. Workflow → plan skill autoload
When the workflow policy renders (repo tracks progress), inject one extra line
so the model knows the plan skill exists and to load it before non-trivial
work:
- before non-trivial work, load the `plan` skill (spec-first) and follow it
workflowPolicy()gains that line when enabled; test asserts it.
Files touched
- src/prompt.ts — splitter + cache_control for Anthropic
- src/session.ts — provider routing, cacheSystemPrefix, nudge ladder, plan line
- src/registry.ts — signature verification
- src/hooks.ts — new: external hook loader/runner + hash trust
- src/plugins.ts — guard extension for hooks
- src/cli.tsx — --init-scaffold, hooks approval wiring
- src/config.ts — registry.publishers, registry.allowUnsigned, hooks.enabled
- src/init.ts or cli — scaffold writer
- docs/: hooks.md, registry.md, workflow.md, headless.md updates
- test/hooks.test.ts, registry-signature.test.ts, prompt-cache-split.test.ts, init-scaffold.test.ts, nudge-ladder.test.ts
Verification
- bun run typecheck
- bun test (new files + full suite)
- bun run build
- Manual: headless --init-scaffold, a hook that rewrites input, a signed registry entry