diff --git a/content/how-tos/command-rewriting-howto.md b/content/how-tos/command-rewriting-howto.md index 5db5866..df0efec 100644 --- a/content/how-tos/command-rewriting-howto.md +++ b/content/how-tos/command-rewriting-howto.md @@ -1,248 +1,128 @@ +++ -title = "Command Rewriting How-To" -description = "Rewrite shell commands (npm install -> bun add, pip install -> uv add) via terraphim-agent's knowledge graph and a tool-execution hook" -date = 2026-04-15 - +title="A failed command, a saved correction, and the next run" +date=2026-04-15 +updated=2026-10-13 +description="Capture a real command failure, review its correction, and verify what an OpenCode hook changes and executes. Includes a narrated demo and reproducible public fixture." +[taxonomies] +categories = ["Tutorials"] +tags = ["Terraphim", "hooks", "OpenCode", "knowledge-graph", "developer-tools"] [extra] toc = true +++ -# How-To: Learning-Driven Command Rewriting +A coding agent uses the wrong flag. You explain the correction, and the command works. The useful question is what happens when another session makes the same mistake. -This guide shows how to use terraphim-agent to rewrite shell commands before -execution — for example `npm install` -> `bun add` or `pip install` -> `uv -add` — by plugging a knowledge-graph-backed thesaurus into your AI coding -agent's tool-execution hook. +This walkthrough follows one harmless error from failure to saved correction to actual execution. It uses the Harbour project brief from the [memory walkthrough](/posts/cold-start-tax-agentic-memory/), a released Terraphim agent, and an OpenCode adapter included in the download. -The mechanism composes three pieces that already exist in terraphim-agent: + -1. A Logseq-style knowledge graph of command synonyms under - `~/.config/terraphim/docs/src/kg/` (or any role-configured path). -2. `terraphim-agent replace` — Aho-Corasick replacement that rewrites text - using a role's compiled thesaurus. -3. A plugin hook in your AI agent (OpenCode, Claude Code, etc.) that - intercepts every Bash tool call, pipes the command through `replace`, - and writes the result back into the tool's args. + -## Prerequisites +[Download the Harbour hooks example](/demos/agent-hooks/harbour-hooks-example.zip) · [Read the transcript](/demos/agent-hooks/transcript.txt) -- `terraphim-agent` on `PATH` (any recent release; 1.16.33 or later). -- A role whose KG directory you control. The default ships with a - `Terraphim Engineer` role pointing at `~/.config/terraphim/docs/src/kg/`. -- An AI agent that exposes a `tool.execute.before` style plugin API - (OpenCode has one; Claude Code exposes equivalent hooks via shell scripts). +## Start with the command that fails -## 1. Curate the knowledge graph +Harbour's validator checks that its brief contains a decision, a risk and an owner. The correct command ends with `--check`. In the baseline run, OpenCode actually attempts: -Each concept is one markdown file. The filename stem becomes the concept -key; the H1 heading provides the display name used as the replacement; the -`synonyms::` line lists terms that should be rewritten to it. +```sh +python3 validate-brief.py --chek +``` -Example `~/.config/terraphim/docs/src/kg/bun install.md`: +The argument is misspelled. Python's argument parser reports that `--check` is required and exits with status 2. No `validation.json` is created. The downloadable trace contains the host's actual tool result, including that output and exit status. -```markdown -# bun add +That distinction matters: the coding host itself can finish normally after reporting a failed tool. Its overall process exit code is not evidence that the validator succeeded. Here we inspect the shell tool's result and the expected output file. + +## Capture the failure that really happened + +The included project-local adapter receives OpenCode's after-tool event. It translates the real command, combined output and exit status into Terraphim's learning-hook schema, then calls the released agent. + +The saved Markdown learning contains the command, exit code 2 and the parser's actual error. A new CLI process can list it again. This gives the correction a concrete source: an observed failure, with a saved record that can be inspected. -Install dependencies using Bun package manager. +The adapter is part of this example. In the tested release, passing a native OpenCode event directly with `--format opencode` does not by itself establish that a learning was captured. The script checks the resulting file and later readback. -synonyms:: npm install, yarn install, pnpm install, npm i, yarn add, pnpm add +## Review and promote the correction + +We add the intended command to the failure record with `learn correct`. That records a proposed correction. It does not automatically make it a replacement rule: compiling the failure record alone produces zero correction entries in this release. + +The next step is explicit human review and promotion to a tool preference. The reproduction script runs this command inside its isolated store: + +```sh +terraphim-agent learn correction add \ + --correction-type tool-preference \ + --original 'python3 validate-brief.py --chek' \ + --corrected 'python3 validate-brief.py --check' \ + --context 'Manually reviewed correction from the captured failure' +terraphim-agent learn compile --output compiled-corrections.json +terraphim-agent learn export-kg --output kg ``` -Conventions that matter in practice: - -- **Filename uses spaces, not underscores** if the concept has multiple - words. The matcher compares against the filename stem (`bun install`). -- **Multi-word synonyms are supported.** `python -m pip install` is a valid - synonym and is matched as a whole phrase; the Aho-Corasick automaton uses - LeftmostLongest, so the longer phrase wins when a shorter one would also - match. -- **Do not overlap synonyms across files.** If both `uv.md` and `uv add.md` - claim `pip install`, the behaviour becomes non-deterministic at rebuild - time. Keep single-token synonyms in the short file (`pip` -> `uv`) and - multi-token phrases in the specific file (`pip install` -> `uv add`). -- **Keep domain vocabulary separate from command vocabulary** if you need - both. Create a dedicated role with its own KG path rather than bleeding - domain terms into shell commands. - -### Seed set shipped with the `Terraphim Engineer` role - -| File | Maps to | Covers | -| ------------------------------------ | -------- | ---------------------------------------- | -| `bun.md` | bun | npm, yarn, pnpm | -| `bun install.md` | bun add | npm install, yarn install, pnpm install, npm i, yarn add, pnpm add | -| `bun run.md` | bun run | npm run, yarn run, pnpm run | -| `bunx.md` | bunx | npx, pnpx, yarn dlx | -| `uv.md` | uv | pip, pip3, pipx | -| `uv add.md` | uv add | pip install, pip3 install, pip add, pipx install, python -m pip install | -| `uv sync.md` | uv sync | pip install -r requirements.txt | - -## 2. Verify with the CLI - -```bash -printf "npm install express" \ - | terraphim-agent replace --role "Terraphim Engineer" --fail-open --json +Compilation now reports one correction. The selected **Harbour Validator** role loads that compiled JSON. The exported Markdown makes the relationship readable: + +```markdown +# python3 validate-brief.py --check +synonyms:: python3 validate-brief.py --chek ``` -Expected output: +This uses the same explicit `synonyms::` idea as the [editor-to-search flow](/how-tos/synonym-search/). The correction says which command this project prefers. In this walkthrough the hook reads the compiled JSON; exporting a Markdown file alone is not the activation step. + +## Inspect the hook before involving execution + +First we call the pre-tool hook on a saved input. Without `--rewrite`, the returned command keeps `--chek` and includes a warning about a replaceable match. With `--rewrite`, the returned JSON contains: ```json -{"result":"bun add express","original":"npm install express","replacements":1,"changed":true} +{ + "tool_input": {"command": "python3 validate-brief.py --check"}, + "tool_name": "Bash" +} ``` -Flags worth knowing: +That is a hook result. It has not executed Python. The example retains both results so you can compare the disabled and enabled behavior. An unrelated command and a role without this correction remain unchanged. -- `--fail-open` — on any error, emits the input unchanged. Mandatory in - hooks so a misconfigured terraphim-agent never wedges the agent. -- `--json` — structured output with `result`, `changed`, `replacements`. - Use this if the hook needs to branch on whether anything changed. -- `--format plain|markdown|wiki|html` — how the replacement is wrapped. - Hooks want `plain`. +![The actual hook outputs preserve the typo with rewriting disabled and return the corrected command with rewriting enabled](/demos/agent-hooks/screenshots/hook-output.jpg) -## 3. Flush the cache after KG edits +## Verify what the coding host actually runs -Terraphim caches compiled thesauri in a SQLite database at -`/tmp/terraphim_sqlite/terraphim.db` (path configured by -`crates/terraphim_settings/default/settings.toml`). Editing a KG markdown -file does **not** invalidate this cache; `replace` keeps returning the old -mapping until you flush it. +Now start a fresh OpenCode process and ask it to try the same misspelled command. Its before-tool event reaches the adapter. The adapter calls Terraphim with rewriting enabled, then applies the returned command through OpenCode V2's mutable tool input. -```bash -sqlite3 /tmp/terraphim_sqlite/terraphim.db \ - "DELETE FROM terraphim_kv WHERE key LIKE 'thesaurus_%' OR key LIKE 'document_ripgrep_%';" -``` +The after-tool event records `python3 validate-brief.py --check`. Its exit status is 0. The validator prints a valid Harbour result and writes this file: -Because `/tmp/` is wiped on reboot, a fresh boot always gives the -up-to-date thesaurus. - -## 4. Wire up the hook (OpenCode example) - -OpenCode plugins expose `tool.execute.before(input, output)` where -`output.args.command` is the mutable shell command about to run. The same -pattern works in Claude Code via the `PreToolUse` hook script, just with -shell-stdin instead of a JS closure. - -```js -// ~/.config/opencode/plugin/terraphim-hooks.js -const REWRITE_MODE = process.env.TERRAPHIM_REWRITE_MODE || "suggest" -const REWRITE_ROLE = process.env.TERRAPHIM_REWRITE_ROLE || "Terraphim Engineer" -const AUDIT_LOG = `${process.env.HOME}/Library/Application Support/terraphim/rewrites.log` - -// Narrow whitelist of commands whose argument grammar survives a synonym swap. -const REWRITEABLE_HEADS = - /^\s*(npm|yarn|pnpm|npx|pnpx|pip|pip3|pipx|python\s+-m\s+pip|python3\s+-m\s+pip)\b/i - -export const TerraphimHooks = async ({ $ }) => ({ - "tool.execute.before": async (input, output) => { - if (input.tool !== "Bash" || !output.args?.command) return - const command = output.args.command - - const agent = `${process.env.HOME}/.cargo/bin/terraphim-agent` - - // Always run the destructive-command guard first. - const g = await $`${agent} guard ${command} --json --fail-open 2>/dev/null || echo '{"decision":"allow"}'` - const guard = JSON.parse(g.stdout) - if (guard.decision === "block") { - throw new Error(`BLOCKED: ${guard.reason}`) - } - - const isGitCommit = /git\s+(-C\s+\S+\s+)?commit/i.test(command) - const isRewriteable = REWRITEABLE_HEADS.test(command) - if (!isGitCommit && !isRewriteable) return - - const res = await $`echo ${command} | ${agent} replace --role ${REWRITE_ROLE} --fail-open --json 2>/dev/null` - const parsed = JSON.parse(res.stdout) - const rewrite = (parsed.result || "").trim() - if (!parsed.changed || !rewrite || rewrite === command) return - - const line = [ - new Date().toISOString(), REWRITE_MODE, - isGitCommit ? "git-commit" : "pkg-mgr", - command.replace(/[\t\n\r]/g, " "), - rewrite.replace(/[\t\n\r]/g, " "), - ].join("\t") + "\n" - await $`mkdir -p "$(dirname ${AUDIT_LOG})" < /dev/null && printf %s ${line} >> ${AUDIT_LOG}` - - if (REWRITE_MODE === "apply" || isGitCommit) { - output.args.command = rewrite - } - }, -}) +```json +{ + "project": "Harbour", + "status": "valid", + "fields": ["Decision", "Risk", "Owner"] +} ``` -Design notes: +The host transcript can still display the model's original attempted input. That is why the download includes adapter events, the actual tool result and `validation.json`, as well as the transcript. Together they show the attempted command, the applied correction and the resulting execution. -- **Whitelist, not blacklist.** Arbitrary shell is never rewritten. Only - commands whose head matches `REWRITEABLE_HEADS` are candidates. -- **Suggest mode by default.** Set `TERRAPHIM_REWRITE_MODE=apply` once you - trust the diffs. Git commit rewriting always applies because commit - messages are prose, not syntax. -- **Audit log.** Every rewrite is logged tab-separated to - `~/Library/Application Support/terraphim/rewrites.log` so you can diff - before flipping modes. -- **Fail-open.** Each external call is wrapped in try/catch with `||` - fallbacks. If terraphim-agent is missing, commands pass through unchanged. +![The real after-tool event and saved validation JSON confirm exit zero and the expected Harbour fields](/demos/agent-hooks/screenshots/executed-result.jpg) -## 5. Confirm end-to-end +## Reproduce it in a new folder -With the hook installed and the cache flushed, open your agent, ask it to -run `npm install express`, and inspect the audit log: +The example was verified with [terraphim-agent v1.21.19](https://github.com/terraphim/terraphim-clients/releases/tag/v1.21.19) and OpenCode 2.0.20. Follow the ZIP's README to check the release archive, choose your already authenticated coding model and run: -```bash -tail -n 5 ~/Library/Application\ Support/terraphim/rewrites.log +```sh +python3 reproduce.py \ + --agent /absolute/path/to/terraphim-agent \ + --model kimi-code-plan-global/k3 \ + --output /absolute/path/to/a-new-harbour-run ``` -You should see a line like: +Use your configured model identifier if it differs. The script uses a new folder for the fixture, settings, learning store and project-local adapter. It does not install global hooks or select a paid fallback. The recorded run used the existing Kimi coding-plan route. -``` -2026-04-15T11:32:51.129Z suggest pkg-mgr npm install express bun add express -``` +Read `evidence/verification.json`, the host events and `validation.json`. The script fails if the host does not perform the expected command or the saved outputs do not match. You can inspect the source before running it; only the two local validator commands are permitted by the fixture's host configuration. + +## Keep the scope of the example visible + +This demonstrates one reviewed correction with one tested host adapter. It does not prove every coding host applies the same JSON, or that failure capture should automatically approve arbitrary substitutions. The earlier memory example and this learning store also remain separate; neither silently copies into the other. + +Use public sample data for this release's learning capture. Tests with deliberately fake secret-shaped values found redaction gaps in saved command and working-directory fields, and in some hook passthrough values. The example makes no blanket secret-redaction claim. The coding host uses its configured online model; Terraphim's demonstrated correction lookup uses the local compiled graph. + +[Download the example and saved outputs](/demos/agent-hooks/harbour-hooks-example.zip) to follow the whole chain: a failure that happened, a correction someone reviewed, and a later command whose result can be checked. -In `suggest` mode the command still executes as `npm install express`; in -`apply` mode the agent actually runs `bun add express`. - -## 6. Capturing user corrections (preview) - -`terraphim-agent learn hook --format ` has three -modes driven by `--learn-hook-type`: - -- `post-tool-use` — the default, captures failed Bash commands as - learnings. This is already wired into the OpenCode plugin's - `tool.execute.after` callback. -- `pre-tool-use` — checks if the command matches a past failure pattern and - stashes the hint to `~/.local/share/terraphim/session-hints.txt` for LLM - consumption. Does not block and does not print to the user terminal. -- `user-prompt-submit` — scans the user's prompt for patterns like "use X - instead of Y" or "prefer X over Y" and records a `ToolPreference` - correction under - `~/Library/Application Support/terraphim/learnings/correction-*.md`. - -At present these corrections are stored but **not yet fed back** into the -replacement thesaurus. Closing that loop is tracked as future work — see -the accompanying GitHub issue "Learning-driven command correction: Phase 2 -& 3". - -## Troubleshooting - -**`replace` returns the original unchanged.** -Run `terraphim-agent search "" --role ""` — if the concept -appears, the KG is loaded but the synonym is not. Confirm the synonym is on -the `synonyms::` line (case-insensitive; commas separate entries). Flush -the cache (section 3) and retry. - -**`Failed to load thesaurus: NotFound("thesaurus_...")` in stderr.** -Cosmetic. The agent looked for a pre-compiled JSON thesaurus first, didn't -find one, and fell back to building from markdown. Expected on first run. - -**Hook does nothing in OpenCode.** -Check the plugin loaded: `grep terraphim-hooks -~/.local/share/opencode/log/$(ls -t ~/.local/share/opencode/log/ | head -1)`. -You should see a line like `service=plugin path=...terraphim-hooks.js -loading plugin`. If absent, the plugin file is in the wrong directory — -OpenCode autoloads from `~/.config/opencode/plugin/` and -`~/.config/opencode/plugins/`. - -**Commands get double-rewritten on retry.** -The hook only touches `tool.execute.before`; the agent does not loop back -through the hook on its own retries. If you see double rewrites, check -whether `input.tool === "Bash"` is spelt exactly — OpenCode passes -`"Bash"`, not `"bash"`. +*Updated 13 October 2026. The video uses synthetic British female narration and readable excerpts from actual released-binary and host outputs. The Harbour project is fictional public sample data.* diff --git a/content/posts/native-hook-support.md b/content/posts/native-hook-support.md index 9e3553e..d57fbc2 100644 --- a/content/posts/native-hook-support.md +++ b/content/posts/native-hook-support.md @@ -10,6 +10,8 @@ toc = true comments = true +++ +> This is the February 2026 announcement for v1.8.1. For the tested failure → reviewed correction → execution flow, follow the [current command-rewriting guide](/how-tos/command-rewriting-howto/). + We are announcing **native hook support** for terraphim-agent v1.8.1. This feature captures failed commands from AI agents (Claude Code, Codex, OpenCode) and learns from them, creating a personal knowledge base of mistakes and corrections. No more jq dependency, no more bash wrappers: just `terraphim-agent learn hook`. diff --git a/publication/agent-memory-hooks/hooks/render_demo.py b/publication/agent-memory-hooks/hooks/render_demo.py new file mode 100644 index 0000000..5d219ac --- /dev/null +++ b/publication/agent-memory-hooks/hooks/render_demo.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Render readable excerpts from the verified released hook and host receipts.""" +import html +import json +from pathlib import Path +import subprocess +from playwright.sync_api import sync_playwright + +ROOT = Path(__file__).resolve().parent +EVIDENCE = ROOT / 'evidence/fresh-run' +OUT = ROOT / 'rendered' +OUT.mkdir(exist_ok=True) +(OUT / 'screenshots').mkdir(exist_ok=True) +read = lambda name: json.loads((EVIDENCE / name).read_text()) +verification = read('verification.json') +assert verification['pass'] and verification['baseline_exit'] == 2 and verification['corrected_exit'] == 0 +events = [json.loads(line) for line in (EVIDENCE / 'host-events.jsonl').read_text().splitlines()] +after = [event['data'] for event in events if event['type'] == 'native-after'] +assert len(after) == 2 +baseline, corrected = after +assert corrected['input']['command'] == verification['host_executed'] +assert corrected['result']['metadata']['exit'] == 0 +saved = read('validation.json') +assert json.loads(corrected['result']['output']['output']) == saved +plan = json.loads((ROOT / 'storyboard.json').read_text()) +identity = read('identities.json') +esc = html.escape + +def box(label, value, kind=''): + return f'

{esc(label)}

{esc(value)}
' + +disabled = json.loads(read('rewrite-disabled.json')['stdout']) +enabled = json.loads(read('rewrite-enabled.json')['stdout']) +learning = (EVIDENCE / 'learning-before-correction.md').read_text() +learning_excerpt = '\n'.join(line for line in learning.splitlines() if line.startswith(('id:', 'command:', 'exit_code:', 'captured_at:'))) +items = [ + box('Actual host command', baseline['input']['command'], 'command') + box('Actual tool output · exit 2', baseline['result']['output']['output'], 'result') + '

A mistake worth
remembering.

', + box('Saved learning · Markdown excerpt', learning_excerpt) + box('Error retained in the learning', baseline['result']['output']['output']) + '

Actual after-tool event → project adapter → released learning hook

', + box('Explicit reviewed mapping', 'python3 validate-brief.py --chek\n→ python3 validate-brief.py --check') + box('Actual compilation output', read('compile-corrections.json')['stdout'], 'result') + '

Manual promotion to a tool preference · selected role loads compiled JSON

', + box('Rewrite disabled · returned command', disabled['tool_input']['command'], 'command') + box('Rewrite enabled · actual returned JSON', json.dumps(enabled, indent=2), 'result') + '

Standalone hook result. No validator has executed in this comparison.

', + box('New host process · original attempt', verification['host_attempted'], 'command') + box('Actual after-tool event · excerpt', json.dumps({'command': corrected['input']['command'], 'exit': corrected['result']['metadata']['exit']}, indent=2), 'result') + '

Included OpenCode V2 adapter applies the command through event.input.

', + box('validation.json · actual saved file', json.dumps(saved, indent=2), 'result') + '

Unrelated command: unchanged
Unrelated role: unchanged
Baseline: exit 2 → corrected execution: exit 0

', + '

A reviewed correction.
A result you can check.

Public brief + local adapter + saved traces
Release download + reproduction script

terraphim.ai/how-tos/command-rewriting-howto/

' +] +css = '*{box-sizing:border-box}body{margin:0;background:#0c1422;color:#f5f7fa;font-family:Arial,sans-serif}.frame{width:1440px;height:960px;padding:38px 64px;display:flex;flex-direction:column}.top{font-size:19px;letter-spacing:.06em;color:#a2b8ce;display:flex;justify-content:space-between}.title{font-size:42px;margin:30px 0 24px;line-height:1.12}.content{flex:1;overflow:hidden}.box{background:#142237;border:1px solid #36516a;border-radius:14px;padding:20px 25px;margin-bottom:18px}h2{font-size:19px;color:#aec6dc;margin:0 0 12px;font-weight:normal}pre{font:25px/1.42 Menlo,monospace;white-space:pre-wrap;overflow-wrap:anywhere;margin:0}.command pre{color:#9edac2;font-size:22px}.result pre{font-size:23px}.hero{font-size:55px;line-height:1.3;margin:27px 0}.hero em{font-style:normal;color:#a7e5c9}.note{font-size:24px;line-height:1.5;color:#aec6dc}.controls{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin:25px 0}.control{border:1px solid #36516a;border-radius:14px;padding:28px;background:#142237}.control strong{display:block;font-size:76px;color:#a7e5c9;margin:18px 0}.control p{font-size:23px}.download{font-size:30px;line-height:1.6;color:#c8d5e3}.url{font-size:25px;color:#a7e5c9;margin-top:40px}.footer{border-top:1px solid #36516a;padding-top:17px;font-size:18px;color:#a2b8ce;display:flex;justify-content:space-between}.badge{color:#f5ce84}' +with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page(viewport={'width':1440, 'height':960}, device_scale_factor=1) + for i, (beat, body) in enumerate(zip(plan['beats'], items)): + doc = f'
TERRAPHIM · LEARNING AND HOOKSAGENT 1.21.19 · OPENCODE 2.0.20

{esc(beat["title"])}

{body}
' + (OUT / f'scene-{i}.html').write_text(doc) + page.set_content(doc) + assert page.locator('.content').evaluate('(e)=>e.scrollHeight<=e.clientHeight'), f'overflow in scene {i}' + page.screenshot(path=str(OUT / f'scene-{i}.png')) + if i in (0,3,5): + name = {0:'poster',3:'hook-output',5:'executed-result'}[i] + page.screenshot(path=str(OUT / 'screenshots' / f'{name}.jpg'), type='jpeg', quality=92) + browser.close() +concat=[] +for i,beat in enumerate(plan['beats']): + concat += [f"file 'scene-{i}.png'", f"duration {beat['end']-beat['start']}"] +concat.append("file 'scene-6.png'") +(OUT / 'frames.txt').write_text('\n'.join(concat)+'\n') +subprocess.run(['ffmpeg','-hide_banner','-loglevel','error','-y','-f','concat','-safe','0','-i',str(OUT/'frames.txt'),'-i',str(ROOT/'narration.wav'),'-t',str(plan['durationSeconds']),'-vf','fps=25,format=yuv420p','-c:v','libx264','-preset','medium','-crf','19','-c:a','aac','-b:a','160k','-movflags','+faststart',str(OUT/'agent-hooks.mp4')],check=True) +(OUT/'render-receipt.json').write_text(json.dumps({'qualification':'released-binary','identities':identity,'durationSeconds':plan['durationSeconds'],'source':'evidence/fresh-run','outputPresentation':'Readable excerpts preserving actual source values; no simulated terminal execution.','manualPromotion':True,'adapterRequired':True},indent=2)+'\n') +print(OUT/'agent-hooks.mp4') diff --git a/publication/agent-memory-hooks/hooks/render_voice.py b/publication/agent-memory-hooks/hooks/render_voice.py new file mode 100644 index 0000000..07ed4a5 --- /dev/null +++ b/publication/agent-memory-hooks/hooks/render_voice.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Use the existing local Emma voice at its natural rate, then verify timings.""" +from pathlib import Path +import hashlib +import json +import wave +import numpy as np +from kokoro_onnx import Kokoro + +ROOT = Path(__file__).resolve().parent +plan = json.loads((ROOT / "storyboard.json").read_text()) +voice_dir = ROOT / "voice" +voice_dir.mkdir(exist_ok=True) +engine = Kokoro('/private/tmp/terraphim-kokoro-v1.0.onnx', '/private/tmp/terraphim-kokoro-voices-v1.0.bin') +engine.sess.disable_telemetry_events() if hasattr(engine.sess, 'disable_telemetry_events') else None +audio = np.zeros(plan['durationSeconds'] * 24000, dtype=np.float64) +for i, beat in enumerate(plan['beats']): + path = voice_dir / (hashlib.sha256(beat['text'].encode()).hexdigest()[:16] + '.wav') + if not path.exists(): + samples, rate = engine.create(beat['text'], voice='bf_emma', speed=1.0, lang='en-gb') + with wave.open(str(path), 'wb') as f: + f.setnchannels(1); f.setsampwidth(2); f.setframerate(rate) + f.writeframes(np.round(np.clip(samples, -1, 1) * 32767).astype(' .01 +audio *= 10**(-2/20) / peak +with wave.open(str(ROOT / 'narration.wav'), 'wb') as f: + f.setnchannels(1); f.setsampwidth(2); f.setframerate(24000) + f.writeframes(np.round(audio * 32767).astype(' {stamp(t1)}', ' '.join(words[start:end]), ''] +(ROOT / 'captions.vtt').write_text('\n'.join(vtt)) +(ROOT / 'transcript.txt').write_text(plan['title'] + '\nSynthetic British female voice: Emma (Kokoro).\n\n' + '\n\n'.join(f"{b['start']}–{b['end']}s | {b['title']}\n{b['text']}" for b in plan['beats']) + '\n') diff --git a/publication/agent-memory-hooks/hooks/storyboard.json b/publication/agent-memory-hooks/hooks/storyboard.json new file mode 100644 index 0000000..6dddd62 --- /dev/null +++ b/publication/agent-memory-hooks/hooks/storyboard.json @@ -0,0 +1,77 @@ +{ + "title": "A failed command, a saved correction, and the next run", + "durationSeconds": 75, + "voice": { + "name": "Emma", + "id": "bf_emma", + "locale": "en-gb", + "engine": "Kokoro ONNX", + "synthetic": true, + "processing": "Fixed gain and scheduled silence; no time stretching or pitch change" + }, + "beats": [ + { + "start": 0, + "end": 9.575, + "title": "One wrong flag", + "text": "The Harbour brief needs checking. In a real coding session, the agent uses a misspelled flag. The validator fails, with exit code two.", + "audio": "voice/bad0a08847a137dc.wav", + "audioDuration": 8.174625, + "audioSha256": "213f8e94b96e57d3933a8d516e1b9f96b6c5d6e48fc70b102a694f045af0995d" + }, + { + "start": 9.575, + "end": 18.354, + "title": "Keep the failure that happened", + "text": "The project adapter passes that actual tool result to Terraphim. A saved learning now contains the command and its error.", + "audio": "voice/ce6d7a2756191e29.wav", + "audioDuration": 7.379333333333333, + "audioSha256": "2a462357d0cc4a02da1c85a1a0fd4ccd4ab3a96c2a5ed22104a4687f21086b28" + }, + { + "start": 18.354, + "end": 31.027, + "title": "Review, then promote", + "text": "I record the correction and explicitly promote it to a tool preference. Compilation produces one rule. This review step matters: a captured failure alone does not activate a replacement.", + "audio": "voice/f3bde5a82c37a918.wav", + "audioDuration": 11.273333333333333, + "audioSha256": "711842fdec9632c84818c1a8e131c1192265c97ff929387c802862f28836b1bf" + }, + { + "start": 31.027, + "end": 41.859, + "title": "Hook output is not execution", + "text": "Without rewriting, the hook keeps the typo and warns. With rewriting enabled, it returns the corrected command. This JSON has not executed anything yet.", + "audio": "voice/d0bd0d86536766b5.wav", + "audioDuration": 9.432, + "audioSha256": "81e7404e468d38f1a937e38486df32316966b08aed962579929b25ccbee03968" + }, + { + "start": 41.859, + "end": 53.945, + "title": "A fresh host run applies it", + "text": "A new coding session tries the same typo. The included OpenCode adapter applies the returned command. The real tool event now records the correct flag and exit code zero.", + "audio": "voice/ec8edda1eb08ffd7.wav", + "audioDuration": 10.686, + "audioSha256": "7287805f718df3e7b839dd9080b0124dd1be598fc3e71f9f7e0819b5942d2758" + }, + { + "start": 53.945, + "end": 64.4, + "title": "Inspect the result on disk", + "text": "The validator writes this result file. Unrelated commands and an unrelated role remain unchanged. These are actual outputs from the released binary.", + "audio": "voice/40574a95452b9fa0.wav", + "audioDuration": 9.054666666666666, + "audioSha256": "c92b32a8b6b7882df5abc86bb28662f4b4d5c473b4a2d88f9f8904c37e964bce" + }, + { + "start": 64.4, + "end": 75, + "title": "Try the public fixture", + "text": "Download the brief, adapter, saved traces and reproduction script. Follow one reviewed correction all the way from failure to a command that actually runs.", + "audio": "voice/2b85beee1296d458.wav", + "audioDuration": 9.054666666666666, + "audioSha256": "89f0f2498ffb3f8a8210012e7528088c47f5fb6cc7d826304570ba812b07162b" + } + ] +} diff --git a/scripts/test_agent_hooks_publication.py b/scripts/test_agent_hooks_publication.py new file mode 100644 index 0000000..95086b1 --- /dev/null +++ b/scripts/test_agent_hooks_publication.py @@ -0,0 +1,70 @@ +"""Bind hook claims to actual captured failures, correction and executed output.""" +import hashlib +import json +from pathlib import Path +import unittest +import zipfile + +ROOT = Path(__file__).resolve().parents[1] +MEDIA = ROOT / 'static/demos/agent-hooks' + + +class AgentHooksPublicationTests(unittest.TestCase): + def test_media_matches_reviewed_manifest(self): + manifest = json.loads((MEDIA / 'manifest.json').read_text()) + self.assertEqual(manifest['qualification'], 'released-binary') + self.assertEqual(manifest['durationSeconds'], 75) + self.assertTrue(manifest['manualPromotion']) + self.assertTrue(manifest['adapterRequired']) + self.assertTrue(manifest['voice']['speechRateUnchanged']) + for entry in manifest['files']: + data = (MEDIA / entry['file']).read_bytes() + self.assertEqual(hashlib.sha256(data).hexdigest(), entry['sha256']) + self.assertEqual(len(data), entry['bytes']) + + def test_bundle_proves_hook_result_and_execution_separately(self): + with zipfile.ZipFile(MEDIA / 'harbour-hooks-example.zip') as z: + self.assertIsNone(z.testzip()) + self.assertTrue(all(not n.startswith('/') and '..' not in Path(n).parts for n in z.namelist())) + load = lambda n: json.loads(z.read('recorded/evidence/' + n)) + result = load('verification.json') + self.assertTrue(result['pass']) + self.assertEqual(result['baseline_exit'], 2) + self.assertEqual(result['corrected_exit'], 0) + self.assertTrue(result['manual_promotion']) + self.assertTrue(result['native_adapter_required']) + off = json.loads(load('rewrite-disabled.json')['stdout']) + on = json.loads(load('rewrite-enabled.json')['stdout']) + self.assertEqual(off['tool_input']['command'], result['host_attempted']) + self.assertEqual(on['tool_input']['command'], result['host_executed']) + events = [json.loads(line) for line in z.read('recorded/evidence/host-events.jsonl').decode().splitlines()] + after = [e['data'] for e in events if e['type'] == 'native-after'] + self.assertEqual(after[0]['result']['metadata']['exit'], 2) + self.assertEqual(after[-1]['input']['command'], result['host_executed']) + self.assertEqual(after[-1]['result']['metadata']['exit'], 0) + self.assertEqual(json.loads(after[-1]['result']['output']['output']), json.loads(z.read('recorded/validation.json'))) + learning = z.read('recorded/evidence/learning-before-correction.md').decode() + self.assertIn(result['host_attempted'], learning) + self.assertIn('exit_code: 2', learning) + self.assertIn('Compiled 0 correction(s)', load('learning-only-compile.json')['stdout']) + self.assertIn('Compiled 1 correction(s)', load('compile-corrections.json')['stdout']) + for row in z.read('SHA256SUMS').decode().splitlines(): + digest, name = row.split(' ', 1) + self.assertEqual(hashlib.sha256(z.read(name)).hexdigest(), digest) + + def test_article_preserves_scope_and_downloads(self): + page = (ROOT / 'content/how-tos/command-rewriting-howto.md').read_text() + self.assertIn('date=2026-04-15', page) + self.assertIn('updated=2026-10-13', page) + for name in ['agent-hooks.mp4','captions.vtt','transcript.txt','harbour-hooks-example.zip']: + self.assertIn('/demos/agent-hooks/' + name, page) + self.assertTrue((MEDIA / name).is_file()) + self.assertIn('explicit human review', page) + self.assertIn('included project-local adapter', page) + self.assertIn('It has not executed Python', page) + self.assertIn('redaction gaps', page) + self.assertIn('synonyms:: python3 validate-brief.py --chek', page) + + +if __name__ == '__main__': + unittest.main() diff --git a/static/demos/agent-hooks/agent-hooks.mp4 b/static/demos/agent-hooks/agent-hooks.mp4 new file mode 100644 index 0000000..306adad Binary files /dev/null and b/static/demos/agent-hooks/agent-hooks.mp4 differ diff --git a/static/demos/agent-hooks/captions.vtt b/static/demos/agent-hooks/captions.vtt new file mode 100644 index 0000000..4251cab --- /dev/null +++ b/static/demos/agent-hooks/captions.vtt @@ -0,0 +1,49 @@ +WEBVTT + +00:00:00.200 --> 00:00:04.110 +The Harbour brief needs checking. In a real coding session, the + +00:00:04.110 --> 00:00:08.375 +agent uses a misspelled flag. The validator fails, with exit code two. + +00:00:09.775 --> 00:00:13.465 +The project adapter passes that actual tool result to Terraphim. + +00:00:13.465 --> 00:00:17.154 +A saved learning now contains the command and its error. + +00:00:18.554 --> 00:00:22.053 +I record the correction and explicitly promote it to + +00:00:22.053 --> 00:00:25.940 +a tool preference. Compilation produces one rule. This review step + +00:00:25.940 --> 00:00:29.827 +matters: a captured failure alone does not activate a replacement. + +00:00:31.227 --> 00:00:35.943 +Without rewriting, the hook keeps the typo and warns. With rewriting enabled, + +00:00:35.943 --> 00:00:40.659 +it returns the corrected command. This JSON has not executed anything yet. + +00:00:42.059 --> 00:00:45.375 +A new coding session tries the same typo. The + +00:00:45.375 --> 00:00:49.060 +included OpenCode adapter applies the returned command. The real tool + +00:00:49.060 --> 00:00:52.745 +event now records the correct flag and exit code zero. + +00:00:54.145 --> 00:00:58.672 +The validator writes this result file. Unrelated commands and an unrelated + +00:00:58.672 --> 00:01:03.200 +role remain unchanged. These are actual outputs from the released binary. + +00:01:04.600 --> 00:01:09.127 +Download the brief, adapter, saved traces and reproduction script. Follow one reviewed + +00:01:09.127 --> 00:01:13.655 +correction all the way from failure to a command that actually runs. diff --git a/static/demos/agent-hooks/harbour-hooks-example.zip b/static/demos/agent-hooks/harbour-hooks-example.zip new file mode 100644 index 0000000..9ac6960 Binary files /dev/null and b/static/demos/agent-hooks/harbour-hooks-example.zip differ diff --git a/static/demos/agent-hooks/manifest.json b/static/demos/agent-hooks/manifest.json new file mode 100644 index 0000000..f1174d6 --- /dev/null +++ b/static/demos/agent-hooks/manifest.json @@ -0,0 +1,68 @@ +{ + "edition": "agent-hooks-20261013", + "qualification": "released-binary", + "durationSeconds": 75, + "release": "1.21.19", + "host": "OpenCode 2.0.20", + "voice": { + "name": "Emma", + "engine": "Kokoro ONNX", + "locale": "en-GB", + "synthetic": true, + "speechRateUnchanged": true + }, + "manualPromotion": true, + "adapterRequired": true, + "render": { + "qualification": "released-binary", + "identities": { + "terraphim_agent": "terraphim-agent 1.21.19", + "binary_sha256": "78542870b76e2a90fa7b2a767ba7fd6126b32304a925feb83e16db01faf0f4d1", + "host": "opencode v2.0.20", + "model": "kimi-code-plan-global/k3", + "note": "Only public fixture sent to the coding model. Hook processing is local; no blanket no-network claim." + }, + "durationSeconds": 75, + "source": "evidence/fresh-run", + "outputPresentation": "Readable excerpts preserving actual source values; no simulated terminal execution.", + "manualPromotion": true, + "adapterRequired": true + }, + "files": [ + { + "file": "agent-hooks.mp4", + "sha256": "2c5cd802c6c0f9223f2df0c0ade39afcf93a496592ac07bf192fb2d2be7cd906", + "bytes": 1670771 + }, + { + "file": "captions.vtt", + "sha256": "9cfb9bc6759539ae8c0c8648c085e7778491b756ab6ebe888904375376b296ad", + "bytes": 1576 + }, + { + "file": "harbour-hooks-example.zip", + "sha256": "f17baaaf504380209e9574d0990336d517a66f974a6f9bf27bbbab704c9acb71", + "bytes": 30326 + }, + { + "file": "screenshots/executed-result.jpg", + "sha256": "c5216ee5e77668289e4f5120fd3d34d51f2ad156c68b5a82c6cd40bfed49a825", + "bytes": 79598 + }, + { + "file": "screenshots/hook-output.jpg", + "sha256": "dee70fc14ce70ae409032211ec4560ed716024b5f0c4cabb40a5dcbc215f6e5e", + "bytes": 89396 + }, + { + "file": "screenshots/poster.jpg", + "sha256": "2994137b31f736bfcf17dbda20ad9f843ceba3b057db312ad7f6bf6f97a67b76", + "bytes": 90467 + }, + { + "file": "transcript.txt", + "sha256": "c0d1fe56b0778d3fdb020c87dca679be9315c9c50a308a80602487a0ad251d42", + "bytes": 1474 + } + ] +} diff --git a/static/demos/agent-hooks/screenshots/executed-result.jpg b/static/demos/agent-hooks/screenshots/executed-result.jpg new file mode 100644 index 0000000..0038154 Binary files /dev/null and b/static/demos/agent-hooks/screenshots/executed-result.jpg differ diff --git a/static/demos/agent-hooks/screenshots/hook-output.jpg b/static/demos/agent-hooks/screenshots/hook-output.jpg new file mode 100644 index 0000000..5cf2d8d Binary files /dev/null and b/static/demos/agent-hooks/screenshots/hook-output.jpg differ diff --git a/static/demos/agent-hooks/screenshots/poster.jpg b/static/demos/agent-hooks/screenshots/poster.jpg new file mode 100644 index 0000000..adfd7d8 Binary files /dev/null and b/static/demos/agent-hooks/screenshots/poster.jpg differ diff --git a/static/demos/agent-hooks/transcript.txt b/static/demos/agent-hooks/transcript.txt new file mode 100644 index 0000000..62dfcf6 --- /dev/null +++ b/static/demos/agent-hooks/transcript.txt @@ -0,0 +1,23 @@ +A failed command, a saved correction, and the next run +Synthetic British female voice: Emma (Kokoro). + +0–9.575s | One wrong flag +The Harbour brief needs checking. In a real coding session, the agent uses a misspelled flag. The validator fails, with exit code two. + +9.575–18.354s | Keep the failure that happened +The project adapter passes that actual tool result to Terraphim. A saved learning now contains the command and its error. + +18.354–31.027s | Review, then promote +I record the correction and explicitly promote it to a tool preference. Compilation produces one rule. This review step matters: a captured failure alone does not activate a replacement. + +31.027–41.859s | Hook output is not execution +Without rewriting, the hook keeps the typo and warns. With rewriting enabled, it returns the corrected command. This JSON has not executed anything yet. + +41.859–53.945s | A fresh host run applies it +A new coding session tries the same typo. The included OpenCode adapter applies the returned command. The real tool event now records the correct flag and exit code zero. + +53.945–64.4s | Inspect the result on disk +The validator writes this result file. Unrelated commands and an unrelated role remain unchanged. These are actual outputs from the released binary. + +64.4–75s | Try the public fixture +Download the brief, adapter, saved traces and reproduction script. Follow one reviewed correction all the way from failure to a command that actually runs.