Spike: a tool.call hook that holds a call on a long-poll through the mods API

<h1>nixie</h1>
A personal assistant platform that its owner runs and controls: every rule readable and editable, every decision recorded, and personal data kept on the owner's own hosts.
<a href="./docs/README.md">Documentation</a> • <a href="./docs/README.md#decisions">Decisions</a> • <a href="./AGENTS.md">Agent Guidelines</a>
nixie is in Phase 2, Research, and has no product code. The work runs in 4 phases: brainstorm, research, design, and decide. Decisions are recorded as research settles each question.
bun install
bun run check # format, lint, and the prose check
hooks/register.js 33 lines1// Holds MARK_HOLD calls on one long-poll to the owner's decision service.
2// The wait sits inside $.http.fetch, a mods API call, so it does not count against the hook's limit.
3async function readDecision($, url, started) {
4 try {
5 const response = await $.http.fetch(url);
6 return JSON.parse(response.text).decision;
7 } catch (error) {
8 await $.ui.log(`nixie fetch failed after ${Date.now() - started} ms: ${error.message}`, {
9 to: 'debug',
10 });
11 throw error;
12 }
13}
14
15async function handleToolCall($, e, next) {
16 if (!e.command.includes('MARK_HOLD')) {
17 return next(e);
18 }
19 const base = await $.env.get('NIXIE_DECIDER_URL'),
20 started = Date.now();
21 await $.ui.log(`nixie hold start budget=${JSON.stringify(next.budget)}`, { to: 'debug' });
22 if ((await readDecision($, `${base}/decide?tool_use_id=${e.tool_use_id}`, started)) === 'allow') {
23 return next(e);
24 }
25 return {
26 deny: `nixie-hold-http: the owner refused this command after ${Date.now() - started} ms.`,
27 };
28}
29
30export function register(on) {
31 on('tool.call', { tool: 'Bash' }, handleToolCall);
32}
33