Repository navigation
Expand file tree
/
Copy pathcommand-hooks.jsonc
More file actions
173 lines (169 loc) · 10.2 KB
/
Copy pathcommand-hooks.jsonc
File metadata and controls
173 lines (169 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
{
// Command hooks — declarative shell commands on tool and session events.
//
// Global defaults for every project. A project adds or overrides hooks in
// <project>/.opencode/command-hooks.jsonc; a hook with the same id replaces
// the one here, and `overrideGlobal: true` suppresses every hook below for
// the same phase+tool.
//
// Full reference: skills/command-hooks/SKILL.md
// Documentation: .opencode/context/research/opencode-command-hooks/
//
// ── Why these defaults exist ──────────────────────────────────────────────
//
// Every rule in this configuration that says "verify your own work" relies on
// each agent remembering to do it. That is the same class of problem as
// asking an orchestrator to call a validator: non-deterministic, and it costs
// tokens in the reminder that asks for it. A hook runs on the event, every
// time, without being asked.
//
// `injectOn: "failure"` is the load-bearing option. A passing typecheck
// reports nothing — it costs no tokens — so the hook is silent exactly when
// things are fine and speaks only when the agent needs to know.
// Per-stream capture cap before truncation. Matches OpenCode's bash tool.
"truncationLimit": 30000,
// Cap on the rendered message that actually reaches the model. The character
// limit above bounds each stream; this bounds the whole injection, so a
// chatty command cannot flood the next turn's context.
"injectLimit": 4000,
"tool": [
{
// Type errors after any subagent returns. The single highest-value
// check: a subagent reports success far more often than it reports a
// broken import, and the parent cannot see the difference.
//
// `npx tsc --noEmit` rather than a package script so this works in a
// project that has no `typecheck` script, which is most of them.
"id": "typecheck-after-task",
"when": { "phase": "after", "tool": "task" },
"run": "npx --no-install tsc --noEmit -p tsconfig.json 2>&1 || npx --no-install tsc --noEmit 2>&1",
"injectOn": "failure",
"inject": "<typecheck tool=\"{tool}\" exit=\"{exitCode}\">\nType errors after the subagent completed. Fix these before reporting success:\n\n```\n{stdout}{stderr}\n```\n</typecheck>",
"toast": {
"title": "typecheck",
"message": "failed (exit {exitCode})",
"variant": "error",
"duration": 6000
}
},
{
// A subagent that wrote a file should have produced a test run. This
// injects only on failure, so a green suite stays silent and a red
// one is explained before the parent claims the work is done.
//
// Skipped when the project has no test script at all — `|| true`
// would make a missing suite look like a passing one, so the `test -f`
// gate keeps "no tests configured" out of the reported results.
"id": "tests-after-task",
"when": { "phase": "after", "tool": "task" },
"run": "if [ -f package.json ] && node -e \"const s=require('./package.json').scripts||{};process.exit(s.test?0:1)\" 2>/dev/null; then npm test --silent 2>&1; else exit 0; fi",
"injectOn": "failure",
"inject": "<tests tool=\"{tool}\" exit=\"{exitCode}\">\nThe test suite failed after the subagent completed. Do not report success until this passes:\n\n```\n{stdout}{stderr}\n```\n</tests>",
"toast": {
"title": "tests",
"message": "failed (exit {exitCode})",
"variant": "error",
"duration": 6000
}
},
{
// The graph and the vector store are rebuilt by a sync hook, but the
// graph invariants themselves are not asserted anywhere at runtime.
// After an edit that touches a source file, run the cheap integrity
// check so an inconsistent graph is reported where it was created.
"id": "graph-integrity-after-write",
"when": {
"phase": "after",
"tool": ["write", "edit", "patch"],
"toolArgs": {
"filePath": { "glob": "**/skills/graph-context/scripts/*.ts" }
}
},
"run": "npx --no-install tsc --noEmit -p skills/tsconfig.json 2>&1 | head -40",
"injectOn": "failure",
"inject": "<graph-integrity exit=\"{exitCode}\">\nA file under skills/graph-context/scripts no longer typechecks. The graph invariants in graph-context/SKILL.md are documented as holding; that is now in doubt:\n\n```\n{stdout}{stderr}\n```\n</graph-integrity>"
},
{
// Rebuild the TUI hub menu bundle when a hub spec or a hub SKILL.md
// changes. This is the mechanical half of rules/hub-menu-rebuild.md.
//
// The rule existed because the step was manual and got skipped: the
// dialog rendered from a generated bundle, so a committed spec with a
// stale bundle made a working subcommand invisible with no error
// anywhere. It shipped that way twice (79280b0 — graph, self-improve,
// delegate and insights all missing; 5ae6d27).
//
// Silent on success (`injectOn: "failure"`) and debounced by the hook
// runner's own dedupe, because regenerating on every keystroke-level
// edit would cost more than the staleness it prevents. `dist/tui.js`
// is gitignored and rebuilt locally; `src/generated-hubs.ts` is
// committed, so the pair is what actually has to stay in step.
"id": "hub-menu-rebuild-after-write",
"when": {
"phase": "after",
"tool": ["write", "edit", "patch"],
"toolArgs": {
// One glob, not an array: the matcher takes a single pattern
// (`{glob: string}`) and an array here invalidates the whole
// file — every hook below silently stops firing. picomatch
// expands the braces, so all three sources fit in one string.
"filePath": {
"glob": "{tools/hubs/**/*.ts,tools/hub-*.ts,skills/*/SKILL.md}"
}
}
},
"run": "cd plugins/hubs-tui && bun run generate-menus >/dev/null 2>&1 && bun build src/tui.tsx --outdir dist --target bun --minify >/dev/null 2>&1",
"injectOn": "failure",
"inject": "<hub-menu-rebuild exit=\"{exitCode}\">\nThe hub menu bundle failed to regenerate after a spec change. Until this succeeds, /maintain-hub and its siblings render from a stale bundle and any new or renamed subcommand is INVISIBLE in the dialog. Run: cd plugins/hubs-tui && bun run generate-menus && bun build src/tui.tsx --outdir dist --target bun --minify\n\n```\n{stdout}{stderr}\n```\n</hub-menu-rebuild>",
"toast": {
"title": "hub menu bundle",
"message": "regeneration failed (exit {exitCode})",
"variant": "error",
"duration": 8000
}
},
{
// The mechanical half of rules/auto-commit.md.
//
// The rule states the policy; this makes it happen without a model
// turn. Every write/edit/patch is staged and committed with a UTC
// timestamp message — a message that describes the work costs an
// inference call, and the diff already is the description.
//
// It refuses to commit a change that looks like a secret, which is
// the one case the rule yields on: a false skip costs a review, a
// false commit leaks a credential into history permanently. Silent on
// success (`injectOn: "failure"`), so a normal commit spends nothing.
//
// This is deliberately aggressive — it commits the whole working tree
// per change. That is the requested tradeoff: noisy history in
// exchange for never losing work and never paying to describe it.
"id": "auto-commit-after-write",
"when": { "phase": "after", "tool": ["write", "edit", "patch"] },
"run": "if git rev-parse --git-dir >/dev/null 2>&1; then git add -A 2>/dev/null; if git diff --cached --quiet; then exit 0; fi; if git diff --cached | grep -qiE 'BEGIN [A-Z ]*PRIVATE KEY|sk-[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}'; then echo 'secret-like content staged; auto-commit skipped'; exit 1; fi; git commit -q -m \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"; fi",
"injectOn": "failure",
"inject": "<auto-commit exit=\"{exitCode}\">\nThe mechanical auto-commit (rules/auto-commit.md) did not complete. A staged change looked like it contained a secret, so the commit was skipped rather than committed. Review `git diff --cached`: unstage the secret, or commit deliberately if it is a false positive.\n\n```\n{stdout}{stderr}\n```\n</auto-commit>",
"toast": {
"title": "auto-commit",
"message": "skipped (exit {exitCode})",
"variant": "error",
"duration": 8000
}
}
],
"session": [
{
// A hook on session start is the cheapest place to surface state that
// every turn would otherwise re-derive: whether the previous session
// left an active mode, and whether the test suite is currently green.
//
// This replaces the per-turn efficiency reminder doing the same job by
// hand, and does it once instead of on every cycle.
"id": "session-start-context",
"when": { "event": "session.start" },
"run": "printf 'repo: %s\\nbranch: %s\\n' \"$(basename \"$PWD\")\" \"$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo none)\"",
"injectOn": "always",
"inject": "<session-context>\n{stdout}\n</session-context>"
}
]
}