Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
298 changes: 89 additions & 209 deletions content/how-tos/command-rewriting-howto.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions content/posts/native-hook-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

<!-- more -->
Expand Down
66 changes: 66 additions & 0 deletions publication/agent-memory-hooks/hooks/render_demo.py
Original file line number Diff line number Diff line change
@@ -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'<section class="box {kind}"><h2>{esc(label)}</h2><pre>{esc(value)}</pre></section>'

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') + '<p class="hero">A mistake worth<br><em>remembering.</em></p>',
box('Saved learning · Markdown excerpt', learning_excerpt) + box('Error retained in the learning', baseline['result']['output']['output']) + '<p class="note">Actual after-tool event → project adapter → released learning hook</p>',
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') + '<p class="note">Manual promotion to a tool preference · selected role loads compiled JSON</p>',
box('Rewrite disabled · returned command', disabled['tool_input']['command'], 'command') + box('Rewrite enabled · actual returned JSON', json.dumps(enabled, indent=2), 'result') + '<p class="note">Standalone hook result. No validator has executed in this comparison.</p>',
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') + '<p class="note">Included OpenCode V2 adapter applies the command through event.input.</p>',
box('validation.json · actual saved file', json.dumps(saved, indent=2), 'result') + '<p class="note">Unrelated command: unchanged<br>Unrelated role: unchanged<br>Baseline: exit 2 → corrected execution: exit 0</p>',
'<p class="hero">A reviewed correction.<br><em>A result you can check.</em></p><div class="download">Public brief + local adapter + saved traces<br>Release download + reproduction script</div><p class="url">terraphim.ai/how-tos/command-rewriting-howto/</p>'
]
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'<!doctype html><meta charset="utf-8"><style>{css}</style><div class="frame"><div class="top"><span>TERRAPHIM · LEARNING AND HOOKS</span><span class="badge">AGENT 1.21.19 · OPENCODE 2.0.20</span></div><h1 class="title">{esc(beat["title"])}</h1><div class="content">{body}</div><div class="footer"><span>Actual saved outputs · readable excerpt view · synthetic narration</span><span>{i+1:02d} / 07</span></div></div>'
(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')
58 changes: 58 additions & 0 deletions publication/agent-memory-hooks/hooks/render_voice.py
Original file line number Diff line number Diff line change
@@ -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('<i2').tobytes())
with wave.open(str(path)) as f:
assert f.getframerate() == 24000 and f.getnchannels() == 1
duration = f.getnframes() / f.getframerate()
samples = np.frombuffer(f.readframes(f.getnframes()), dtype='<i2').astype(float) / 32768
assert duration <= beat['end'] - beat['start'] - .25, (i, duration, beat)
offset = round((beat['start'] + .2) * 24000)
audio[offset:offset + len(samples)] += samples
beat.update(audio=str(path.relative_to(ROOT)), audioDuration=duration,
audioSha256=hashlib.sha256(path.read_bytes()).hexdigest())
print(i, round(duration, 2), beat['title'], flush=True)
peak = float(np.max(np.abs(audio)))
assert peak > .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('<i2').tobytes())
plan['voice']['processing'] = 'Fixed gain and scheduled silence; no time stretching or pitch change'
(ROOT / 'storyboard.json').write_text(json.dumps(plan, indent=2) + '\n')

def stamp(seconds):
ms = round(seconds * 1000)
return f'{ms//3600000:02d}:{ms//60000%60:02d}:{ms//1000%60:02d}.{ms%1000:03d}'

vtt = ['WEBVTT', '']
for beat in plan['beats']:
words = beat['text'].split()
count = (len(words) + 13) // 14
for cue in range(count):
start = cue * len(words) // count
end = (cue + 1) * len(words) // count
t0 = beat['start'] + .2 + beat['audioDuration'] * start / len(words)
t1 = beat['start'] + .2 + beat['audioDuration'] * end / len(words)
vtt += [f'{stamp(t0)} --> {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')
77 changes: 77 additions & 0 deletions publication/agent-memory-hooks/hooks/storyboard.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
70 changes: 70 additions & 0 deletions scripts/test_agent_hooks_publication.py
Original file line number Diff line number Diff line change
@@ -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()
Binary file added static/demos/agent-hooks/agent-hooks.mp4
Binary file not shown.
49 changes: 49 additions & 0 deletions static/demos/agent-hooks/captions.vtt
Original file line number Diff line number Diff line change
@@ -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.
Binary file not shown.
Loading
Loading