predicate-skill 1.2.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.claude-plugin/plugin.json +6 -5
  2. package/README.md +88 -17
  3. package/cli.bundle.mjs +29269 -234
  4. package/dashboard/index.html +180 -0
  5. package/hooks/codex-cli/README.md +30 -0
  6. package/hooks/codex-cli/config.toml.template +10 -0
  7. package/hooks/codex-cli/pre-compact.sh +4 -0
  8. package/hooks/codex-cli/session-start.sh +7 -0
  9. package/hooks/codex-cli/stop.sh +4 -0
  10. package/hooks/cursor/README.md +46 -0
  11. package/hooks/cursor/mcp.json.template +12 -0
  12. package/hooks/cursor/pre-compact.sh +7 -0
  13. package/hooks/cursor/session-start.sh +7 -0
  14. package/hooks/cursor/stop.sh +5 -0
  15. package/hooks/gemini-cli/README.md +38 -0
  16. package/hooks/gemini-cli/pre-compact.sh +5 -0
  17. package/hooks/gemini-cli/session-start.sh +6 -0
  18. package/hooks/gemini-cli/settings.json.template +17 -0
  19. package/hooks/gemini-cli/stop.sh +20 -0
  20. package/hooks/hooks.json +25 -7
  21. package/hooks/opencode/README.md +40 -0
  22. package/hooks/opencode/opencode.json.template +18 -0
  23. package/hooks/opencode/pre-compact.sh +5 -0
  24. package/hooks/opencode/session-start.sh +6 -0
  25. package/hooks/opencode/stop.sh +20 -0
  26. package/hooks/post-tool-use.sh +10 -0
  27. package/hooks/pre-tool-use.sh +11 -0
  28. package/hooks/session-start.sh +7 -19
  29. package/hooks/stop.sh +20 -0
  30. package/hooks/vscode-copilot/README.md +43 -0
  31. package/hooks/vscode-copilot/pre-compact.sh +4 -0
  32. package/hooks/vscode-copilot/session-start.sh +7 -0
  33. package/hooks/vscode-copilot/settings.json.template +12 -0
  34. package/hooks/vscode-copilot/stop.sh +5 -0
  35. package/package.json +2 -1
  36. package/server.bundle.mjs +28422 -18694
  37. package/skills/predicate/SKILL.md +193 -1
@@ -0,0 +1,180 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Predicate Dashboard</title>
6
+ <style>
7
+ :root { --bg:#0e0e10; --fg:#e8e8e8; --muted:#888; --accent:#2dd4bf; --warn:#f59e0b; --bad:#ef4444; --row:#1a1a1d; }
8
+ * { box-sizing: border-box; }
9
+ body { background:var(--bg); color:var(--fg); font:14px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif; margin:0; padding:24px; }
10
+ h1 { margin:0 0 4px; font-size:22px; }
11
+ .sub { color:var(--muted); margin-bottom:24px; }
12
+ .grid { display:grid; gap:24px; grid-template-columns:repeat(auto-fit,minmax(420px,1fr)); }
13
+ .card { background:var(--row); border-radius:8px; padding:16px; }
14
+ .card h2 { margin:0 0 8px; font-size:14px; color:var(--accent); text-transform:uppercase; letter-spacing:.05em; }
15
+ .card .empty { color:var(--muted); font-style:italic; }
16
+ table { width:100%; border-collapse:collapse; font-size:13px; }
17
+ th { text-align:left; color:var(--muted); padding:4px 8px 4px 0; font-weight:500; }
18
+ td { padding:4px 8px 4px 0; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; vertical-align:top; word-break:break-all; }
19
+ td.num { text-align:right; font-variant-numeric:tabular-nums; }
20
+ .warn { color:var(--warn); }
21
+ .bad { color:var(--bad); }
22
+ .stats { display:grid; gap:8px; grid-template-columns:1fr 1fr; }
23
+ .stat { display:flex; justify-content:space-between; padding:6px 0; border-bottom:1px solid #2a2a2e; }
24
+ .stat:last-child { border-bottom:none; }
25
+ .stat .k { color:var(--muted); }
26
+ .stat .v { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; }
27
+ .err { background:#3a1212; color:#f5c2c7; padding:12px; border-radius:6px; margin-bottom:16px; }
28
+ </style>
29
+ </head>
30
+ <body>
31
+ <h1>Predicate Dashboard</h1>
32
+ <div class="sub">Session-history + reasoning over Fuseki @ <span id="ep"></span></div>
33
+ <div id="err" class="err" style="display:none"></div>
34
+ <div class="grid">
35
+ <div class="card"><h2>Stats</h2><div class="stats" id="stats"></div></div>
36
+ <div class="card"><h2>Recent Sessions</h2><div id="sessions"></div></div>
37
+ <div class="card"><h2>Hotspots <span class="sub">(files modified in &ge;3 sessions)</span></h2><div id="hotspots"></div></div>
38
+ <div class="card"><h2>Flaky Commands <span class="sub">(failed in &ge;2 sessions)</span></h2><div id="flaky"></div></div>
39
+ <div class="card"><h2>Active Files <span class="sub">(modified in latest session)</span></h2><div id="active"></div></div>
40
+ </div>
41
+ <script>
42
+ const EP = '/api/query';
43
+ document.getElementById('ep').textContent = location.host;
44
+
45
+ async function ask(sparql) {
46
+ const r = await fetch(EP, {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/sparql-results+json' },
49
+ body: 'query=' + encodeURIComponent(sparql),
50
+ });
51
+ if (!r.ok) throw new Error('SPARQL ' + r.status + ': ' + (await r.text()));
52
+ return (await r.json()).results.bindings;
53
+ }
54
+
55
+ function tbl(headers, rows, opts = {}) {
56
+ if (!rows.length) return '<div class="empty">' + (opts.empty || 'no data') + '</div>';
57
+ return '<table><thead><tr>'
58
+ + headers.map(h => '<th>' + h + '</th>').join('')
59
+ + '</tr></thead><tbody>'
60
+ + rows.map(r => '<tr>' + r.map((c, i) => '<td' + (opts.numCols && opts.numCols.includes(i) ? ' class="num"' : '') + '>' + c + '</td>').join('') + '</tr>').join('')
61
+ + '</tbody></table>';
62
+ }
63
+
64
+ function trim(s, n) { return s.length > n ? s.slice(0, n) + '…' : s; }
65
+ function shortFile(f) { return f.replace(/^file:\/\//, '').split('/').slice(-3).join('/'); }
66
+
67
+ async function loadStats() {
68
+ const q = `
69
+ PREFIX pred: <https://predicate.dev/meta#>
70
+ PREFIX cb: <https://predicate.dev/codebase#>
71
+ SELECT
72
+ (COUNT(DISTINCT ?session) AS ?sessions)
73
+ (COUNT(DISTINCT ?file) AS ?files)
74
+ (COUNT(DISTINCT ?cmd) AS ?cmds)
75
+ WHERE {
76
+ GRAPH <kg:abox> {
77
+ OPTIONAL { ?session a pred:Session }
78
+ OPTIONAL { ?file cb:modifiedIn ?_s1 }
79
+ OPTIONAL { ?cmd a cb:Command }
80
+ }
81
+ }
82
+ `;
83
+ const b = (await ask(q))[0];
84
+ const ti = await ask('SELECT (COUNT(*) AS ?n) WHERE { GRAPH <kg:abox> { ?s ?p ?o } }');
85
+ const ii = await ask('SELECT (COUNT(*) AS ?n) WHERE { GRAPH <kg:inferred> { ?s ?p ?o } }');
86
+ const stats = [
87
+ ['sessions', b.sessions.value],
88
+ ['files modified', b.files.value],
89
+ ['commands', b.cmds.value],
90
+ ['abox triples', ti[0].n.value],
91
+ ['inferred triples', ii[0].n.value],
92
+ ];
93
+ document.getElementById('stats').innerHTML = stats.map(([k, v]) => `<div class="stat"><span class="k">${k}</span><span class="v">${v}</span></div>`).join('');
94
+
95
+ // v2.0 ontology + schema-learning badges
96
+ const cfg = await ask(`
97
+ PREFIX pred: <https://predicate.dev/meta#>
98
+ SELECT ?ontology ?learning WHERE {
99
+ GRAPH <kg:meta> {
100
+ OPTIONAL { <urn:predicate:config> pred:initOntology ?ontology }
101
+ OPTIONAL { <urn:predicate:config> pred:schemaLearningEnabled ?learning }
102
+ }
103
+ }
104
+ `);
105
+ const c = cfg[0] ?? {};
106
+ const ontology = c.ontology?.value ?? '(uninitialized)';
107
+ const learning = c.learning?.value === 'true' ? 'on' : (c.learning?.value === 'false' ? 'off' : 'on');
108
+ document.title = `Predicate Dashboard — ${ontology}`;
109
+ const subEl = document.querySelector('.sub');
110
+ if (subEl) subEl.innerHTML = `Session-history + reasoning over Fuseki @ <span id="ep">${location.host}</span> · ontology: <b>${ontology}</b> · learning: <b>${learning}</b>`;
111
+ }
112
+
113
+ async function loadSessions() {
114
+ const q = `
115
+ PREFIX pred: <https://predicate.dev/meta#>
116
+ PREFIX cb: <https://predicate.dev/codebase#>
117
+ SELECT ?sid ?at
118
+ (COUNT(DISTINCT ?file) AS ?nFiles)
119
+ (COUNT(DISTINCT ?ok) AS ?nOk)
120
+ (COUNT(DISTINCT ?bad) AS ?nBad)
121
+ WHERE {
122
+ GRAPH <kg:abox> {
123
+ ?s a pred:Session ; pred:sessionId ?sid ; pred:at ?at .
124
+ OPTIONAL { ?file cb:modifiedIn ?s }
125
+ OPTIONAL { ?ok cb:succeededIn ?s }
126
+ OPTIONAL { ?bad cb:failedIn ?s }
127
+ }
128
+ }
129
+ GROUP BY ?sid ?at
130
+ ORDER BY DESC(?at)
131
+ LIMIT 20
132
+ `;
133
+ const rows = (await ask(q)).map(b => [
134
+ trim(b.sid.value, 32),
135
+ b.at.value.slice(0, 19).replace('T', ' '),
136
+ b.nFiles.value,
137
+ b.nOk.value,
138
+ `<span class="${parseInt(b.nBad.value) > 0 ? 'bad' : ''}">${b.nBad.value}</span>`,
139
+ ]);
140
+ document.getElementById('sessions').innerHTML = tbl(['session', 'at', 'files', 'ok', 'fail'], rows, { numCols: [2, 3, 4], empty: 'no sessions extracted yet' });
141
+ }
142
+
143
+ async function loadDerived(varName, klass, predicate, divId, label, empty) {
144
+ const q = `
145
+ PREFIX cb: <https://predicate.dev/codebase#>
146
+ SELECT ?${varName} (COUNT(DISTINCT ?session) AS ?n)
147
+ WHERE {
148
+ { GRAPH <kg:inferred> { ?${varName} a cb:${klass} } }
149
+ OPTIONAL { GRAPH <kg:abox> { ?${varName} cb:${predicate} ?session } }
150
+ }
151
+ GROUP BY ?${varName}
152
+ ORDER BY DESC(?n)
153
+ LIMIT 15
154
+ `;
155
+ const bindings = await ask(q);
156
+ const rows = bindings.map(b => {
157
+ const v = b[varName].value;
158
+ const display = klass === 'FlakyCommand' ? trim(v, 80) : shortFile(v);
159
+ return [display, b.n.value];
160
+ });
161
+ document.getElementById(divId).innerHTML = tbl([label, 'n'], rows, { numCols: [1], empty });
162
+ }
163
+
164
+ async function load() {
165
+ try {
166
+ await loadStats();
167
+ await loadSessions();
168
+ await loadDerived('file', 'Hotspot', 'modifiedIn', 'hotspots', 'file', 'no hotspots — need ≥3 sessions touching the same file');
169
+ await loadDerived('cmd', 'FlakyCommand', 'failedIn', 'flaky', 'command', 'no flaky commands — need ≥2 sessions with a failed command');
170
+ await loadDerived('file', 'ActiveFile', 'modifiedIn', 'active', 'file', 'no active files — run a session, then `predicate maintain`');
171
+ } catch (e) {
172
+ document.getElementById('err').textContent = 'Failed to load: ' + e.message;
173
+ document.getElementById('err').style.display = 'block';
174
+ }
175
+ }
176
+ load();
177
+ setInterval(load, 30000); // refresh every 30s
178
+ </script>
179
+ </body>
180
+ </html>
@@ -0,0 +1,30 @@
1
+ # Codex CLI adapter
2
+
3
+ ## Install MCP server
4
+
5
+ Merge `config.toml.template` into `~/.codex/config.toml`, replacing
6
+ `__PLUGIN_DIR__` with the absolute path to this package. The 8 `kg_*`
7
+ tools will be available the next time you launch `codex`.
8
+
9
+ ## Hooks
10
+
11
+ Codex CLI does not expose SessionStart, PreCompact, or Stop lifecycle
12
+ events as of writing. The three scripts in this directory are provided
13
+ so you can:
14
+
15
+ 1. Run `session-start.sh` manually and paste output into your initial
16
+ Codex prompt. Or alias:
17
+
18
+ ```sh
19
+ # in ~/.zshrc or ~/.bashrc
20
+ codex() { command codex --context "$(predicate sessionstart 2>/dev/null)" "$@"; }
21
+ ```
22
+
23
+ 2. Wire `pre-compact.sh` and `stop.sh` to cron for periodic maintenance:
24
+
25
+ ```cron
26
+ */30 * * * * /absolute/path/hooks/codex-cli/pre-compact.sh >/dev/null 2>&1
27
+ ```
28
+
29
+ If Codex CLI adds lifecycle hooks in the future, this adapter is ready
30
+ to wire them — script logic is unchanged.
@@ -0,0 +1,10 @@
1
+ # Merge into ~/.codex/config.toml — replace __PLUGIN_DIR__ with the
2
+ # absolute path to packages/predicate-skill.
3
+
4
+ [mcp_servers.predicate]
5
+ command = "node"
6
+ args = ["__PLUGIN_DIR__/server.bundle.mjs"]
7
+
8
+ [mcp_servers.predicate.env]
9
+ FUSEKI_URL = "http://localhost:3030"
10
+ PREDICATE_DATASET = "predicate"
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # Codex CLI pre-compact adapter: run via cron to keep the KG tidy.
3
+ set -euo pipefail
4
+ predicate maintain
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ # Codex CLI session-start adapter. Codex has no native SessionStart event;
3
+ # run this manually before a session and paste the output as initial context,
4
+ # or alias it to `codex` in your shell rc.
5
+ set -euo pipefail
6
+ predicate sessionstart 2>/dev/null || \
7
+ echo "Predicate: Fuseki not reachable; run \`predicate up\` first."
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # Codex CLI stop adapter: run manually after long sessions.
3
+ set -euo pipefail
4
+ predicate maintain
@@ -0,0 +1,46 @@
1
+ # Cursor adapter
2
+
3
+ Predicate exposes its 8 `kg_*` tools to Cursor over MCP, plus three optional
4
+ maintenance scripts you can wire into cron.
5
+
6
+ ## 1. MCP server
7
+
8
+ Copy `mcp.json.template` to `.cursor/mcp.json` (project-local) or
9
+ `~/.cursor/mcp.json` (global), replacing `__PLUGIN_DIR__` with the absolute
10
+ path to your local clone, e.g. `/Users/you/code/predicate/packages/predicate-skill`.
11
+
12
+ Then in Cursor restart MCP (Cmd-Shift-P → "Reload MCP servers") and the 8
13
+ `kg_*` tools will be available.
14
+
15
+ ## 2. Optional: SessionStart context
16
+
17
+ Cursor has no native SessionStart event. Two options:
18
+
19
+ **a. Manual:** Run `bash session-start.sh` in your terminal; paste the
20
+ output into `.cursor/rules/predicate.md`.
21
+
22
+ **b. Cron:** Refresh the rule file periodically:
23
+
24
+ ```cron
25
+ */10 * * * * bash /absolute/path/hooks/cursor/session-start.sh > /project/.cursor/rules/predicate.md
26
+ ```
27
+
28
+ ## 3. Optional: PreCompact maintenance
29
+
30
+ Cursor has no native PreCompact event. Wire `pre-compact.sh` to cron so the
31
+ KG stays tidy between sessions:
32
+
33
+ ```cron
34
+ */30 * * * * /absolute/path/hooks/cursor/pre-compact.sh >/dev/null 2>&1
35
+ ```
36
+
37
+ ## 4. Optional: Stop maintenance
38
+
39
+ Run `bash stop.sh` manually after a long session, or wire it into a shell
40
+ shutdown alias.
41
+
42
+ ## Notes
43
+
44
+ All scripts require `predicate` on `$PATH`. Install with
45
+ `npm install -g predicate-skill`, or use the absolute path:
46
+ `/abs/path/to/predicate/packages/predicate-skill/cli.bundle.mjs`.
@@ -0,0 +1,12 @@
1
+ {
2
+ "mcpServers": {
3
+ "predicate": {
4
+ "command": "node",
5
+ "args": ["__PLUGIN_DIR__/server.bundle.mjs"],
6
+ "env": {
7
+ "FUSEKI_URL": "http://localhost:3030",
8
+ "PREDICATE_DATASET": "predicate"
9
+ }
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ # Cursor pre-compact adapter: trims low-confidence stale facts and
3
+ # promotes any matured staged TBox proposals before context compaction.
4
+ # Cursor has no native PreCompact event — run manually or via cron, e.g.:
5
+ # */30 * * * * /path/to/hooks/cursor/pre-compact.sh
6
+ set -euo pipefail
7
+ predicate maintain
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ # Cursor session-start adapter: emits a plain text status line.
3
+ # Cursor reads stdout when invoked from a custom rule script;
4
+ # can also be run manually and pasted into .cursor/rules/predicate.md.
5
+ set -euo pipefail
6
+ predicate sessionstart 2>/dev/null || \
7
+ echo "Predicate: Fuseki not reachable; run \`predicate up\` first."
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ # Cursor session-end adapter: runs maintenance on session close.
3
+ # Cursor has no native Stop event — run manually after each session.
4
+ set -euo pipefail
5
+ predicate maintain
@@ -0,0 +1,38 @@
1
+ # Gemini CLI adapter
2
+
3
+ ## Install
4
+
5
+ Merge `settings.json.template` into `~/.gemini/settings.json`, replacing
6
+ `__PLUGIN_DIR__` with the absolute path to this package
7
+ (e.g. `/Users/you/code/predicate/packages/predicate-skill`).
8
+
9
+ Restart Gemini CLI. The 8 `kg_*` tools will be available; the three hook
10
+ scripts will fire on `sessionStart`, `preCompress`, and `stop`.
11
+
12
+ ## Hooks reference
13
+
14
+ | Event | Script | What it does |
15
+ |---|---|---|
16
+ | `sessionStart` | `session-start.sh` | Prints KG status line; Gemini reads stdout as context. |
17
+ | `preCompress` | `pre-compact.sh` | Runs `predicate maintain` before context compression. |
18
+ | `stop` | `stop.sh` | Reads the Stop-hook JSON payload from stdin, pipes it to `predicate extract --from-stdin --platform gemini` to assert typed triples for the turn, then runs `predicate maintain`. Fail-open: any error exits 0. |
19
+
20
+ > **Stop-hook extraction (v1.8.0+):** `stop.sh` now invokes
21
+ > `predicate extract --from-stdin --platform gemini` before maintenance.
22
+ > The `--platform gemini` flag selects the Gemini-specific transcript
23
+ > adapter that maps Gemini's `{type:"tool_call", toolUse:{...}}` /
24
+ > `{type:"tool_result", toolResult:{...}}` events into the canonical
25
+ > shape the deterministic extractor understands. The adapter is
26
+ > permissive and falls through silently on unrecognized shapes, so it
27
+ > never blocks your next prompt.
28
+
29
+ ## If your Gemini version doesn't expose hooks
30
+
31
+ The `hooks` block is harmless if unsupported. You can still run each script
32
+ manually or via cron — see `../cursor/README.md` for cron examples; the
33
+ syntax is identical.
34
+
35
+ ## Verify wiring
36
+
37
+ Run `gemini --debug` and start a fresh session; you should see Predicate's
38
+ KG status line printed in the debug output before your first prompt.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ # Gemini CLI pre-compress adapter: runs maintenance before Gemini compacts
3
+ # the chat context. Wire to the `preCompress` event in settings.json.
4
+ set -euo pipefail
5
+ predicate maintain
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ # Gemini CLI session-start adapter. Gemini reads stdout as additional context
3
+ # when wired via the `hooks` block in ~/.gemini/settings.json (event: "sessionStart").
4
+ set -euo pipefail
5
+ predicate sessionstart 2>/dev/null || \
6
+ echo "Predicate: Fuseki not reachable; run \`predicate up\` first."
@@ -0,0 +1,17 @@
1
+ {
2
+ "mcpServers": {
3
+ "predicate": {
4
+ "command": "node",
5
+ "args": ["__PLUGIN_DIR__/server.bundle.mjs"],
6
+ "env": {
7
+ "FUSEKI_URL": "http://localhost:3030",
8
+ "PREDICATE_DATASET": "predicate"
9
+ }
10
+ }
11
+ },
12
+ "hooks": [
13
+ { "event": "sessionStart", "command": "bash __PLUGIN_DIR__/hooks/gemini-cli/session-start.sh" },
14
+ { "event": "preCompress", "command": "bash __PLUGIN_DIR__/hooks/gemini-cli/pre-compact.sh" },
15
+ { "event": "stop", "command": "bash __PLUGIN_DIR__/hooks/gemini-cli/stop.sh" }
16
+ ]
17
+ }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ # Gemini CLI stop hook: reads the Stop-hook JSON payload from stdin,
3
+ # runs structured turn extraction (predicate extract --platform gemini),
4
+ # then a maintenance sweep. Fail-open: any error returns exit 0 so
5
+ # capture never blocks the user's next prompt.
6
+ set -uo pipefail
7
+
8
+ if ! command -v predicate >/dev/null 2>&1; then
9
+ exit 0
10
+ fi
11
+
12
+ # Buffer stdin so we can pipe it into extract.
13
+ payload="$(cat || true)"
14
+
15
+ if [ -n "$payload" ]; then
16
+ printf '%s' "$payload" | predicate extract --from-stdin --platform gemini >/dev/null 2>&1 || true
17
+ fi
18
+
19
+ predicate maintain >/dev/null 2>&1 || true
20
+ exit 0
package/hooks/hooks.json CHANGED
@@ -1,9 +1,27 @@
1
1
  {
2
- "hooks": [
3
- {
4
- "event": "SessionStart",
5
- "matcher": "startup|clear|compact",
6
- "command": "bash ${PLUGIN_DIR}/hooks/session-start.sh"
7
- }
8
- ]
2
+ "description": "Predicate hooks: SessionStart context banner, Stop-hook turn extraction → kg:abox via kg_assert. PreToolUse/PostToolUse raw-capture scripts are shipped but not wired by default; enable them manually if you set PREDICATE_RAW_CAPTURE=1.",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "matcher": "",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh\""
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "Stop": [
16
+ {
17
+ "matcher": "",
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/stop.sh\""
22
+ }
23
+ ]
24
+ }
25
+ ]
26
+ }
9
27
  }
@@ -0,0 +1,40 @@
1
+ # OpenCode adapter
2
+
3
+ ## Install
4
+
5
+ Merge `opencode.json.template` into `~/.config/opencode/opencode.json`
6
+ (or your project-local `opencode.json`), replacing `__PLUGIN_DIR__` with
7
+ the absolute path to this package.
8
+
9
+ Restart OpenCode. The 8 `kg_*` tools will be available, and the three
10
+ hook scripts will fire on `session.started`, `session.compacted`, and
11
+ `session.stopped`.
12
+
13
+ ## Hooks reference
14
+
15
+ | Event | Script | What it does |
16
+ |---|---|---|
17
+ | `session.started` | `session-start.sh` | Prints KG status line; OpenCode reads stdout as context. |
18
+ | `session.compacted` | `pre-compact.sh` | Runs `predicate maintain` before context compression. |
19
+ | `session.stopped` | `stop.sh` | Reads the Stop-hook JSON payload from stdin, pipes it to `predicate extract --from-stdin --platform opencode` to assert typed triples for the turn, then runs `predicate maintain`. Fail-open: any error exits 0. |
20
+
21
+ > **Stop-hook extraction (v1.8.0+):** `stop.sh` now invokes
22
+ > `predicate extract --from-stdin --platform opencode` before
23
+ > maintenance. The `--platform opencode` flag selects the OpenCode
24
+ > transcript adapter that maps `{event:"tool.before", tool:{...}}` /
25
+ > `{event:"tool.after", result|error}` events into the canonical shape
26
+ > the deterministic extractor understands. The adapter is permissive
27
+ > and falls through silently on unrecognized shapes, so it never blocks
28
+ > your next prompt.
29
+
30
+ ## Verify wiring
31
+
32
+ Start an OpenCode session and check the debug log; you should see
33
+ Predicate's KG status line in the initial context, and `predicate maintain`
34
+ output when the session compacts or stops.
35
+
36
+ ## If event names changed in your OpenCode version
37
+
38
+ Consult `opencode --help events` (or the OpenCode docs) for the current
39
+ event names. The scripts are event-agnostic — only the template's `on:`
40
+ keys need to match.
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "mcp": {
4
+ "predicate": {
5
+ "type": "local",
6
+ "command": ["node", "__PLUGIN_DIR__/server.bundle.mjs"],
7
+ "environment": {
8
+ "FUSEKI_URL": "http://localhost:3030",
9
+ "PREDICATE_DATASET": "predicate"
10
+ }
11
+ }
12
+ },
13
+ "events": [
14
+ { "on": "session.started", "run": "bash __PLUGIN_DIR__/hooks/opencode/session-start.sh" },
15
+ { "on": "session.compacted", "run": "bash __PLUGIN_DIR__/hooks/opencode/pre-compact.sh" },
16
+ { "on": "session.stopped", "run": "bash __PLUGIN_DIR__/hooks/opencode/stop.sh" }
17
+ ]
18
+ }
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ # OpenCode pre-compact adapter. Wire to the session.compacted event
3
+ # (fires immediately before OpenCode compresses chat history).
4
+ set -euo pipefail
5
+ predicate maintain
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ # OpenCode session-start adapter. OpenCode reads stdout as additional context
3
+ # when wired to the session.started event.
4
+ set -euo pipefail
5
+ predicate sessionstart 2>/dev/null || \
6
+ echo "Predicate: Fuseki not reachable; run \`predicate up\` first."
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ # OpenCode stop hook: reads the Stop-hook JSON payload from stdin,
3
+ # runs structured turn extraction (predicate extract --platform opencode),
4
+ # then a maintenance sweep. Fail-open: any error returns exit 0 so
5
+ # capture never blocks the user's next prompt.
6
+ set -uo pipefail
7
+
8
+ if ! command -v predicate >/dev/null 2>&1; then
9
+ exit 0
10
+ fi
11
+
12
+ # Buffer stdin so we can pipe it into extract.
13
+ payload="$(cat || true)"
14
+
15
+ if [ -n "$payload" ]; then
16
+ printf '%s' "$payload" | predicate extract --from-stdin --platform opencode >/dev/null 2>&1 || true
17
+ fi
18
+
19
+ predicate maintain >/dev/null 2>&1 || true
20
+ exit 0
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code PostToolUse hook: records {toolName, input, output, sessionId,
3
+ # phase:"post"} in kg:usage. Reads Claude Code's hook payload JSON from stdin
4
+ # and delegates to `predicate capture --from-stdin`. Fails open.
5
+ set -uo pipefail
6
+
7
+ if command -v predicate >/dev/null 2>&1; then
8
+ predicate capture --from-stdin --phase post >/dev/null 2>&1 || true
9
+ fi
10
+ exit 0
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code PreToolUse hook: records {toolName, input, sessionId, phase:"pre"}
3
+ # in kg:usage. Reads Claude Code's hook payload JSON from stdin and delegates
4
+ # to `predicate capture --from-stdin`. Fails open: any error returns exit 0
5
+ # so the user's tool invocation is never blocked by capture logic.
6
+ set -uo pipefail
7
+
8
+ if command -v predicate >/dev/null 2>&1; then
9
+ predicate capture --from-stdin --phase pre >/dev/null 2>&1 || true
10
+ fi
11
+ exit 0
@@ -1,25 +1,13 @@
1
1
  #!/usr/bin/env bash
2
- # SessionStart hook: emits a short context block telling the agent
3
- # how many open goals and active concepts Predicate is tracking.
2
+ # SessionStart hook for Claude Code: emits a short context block telling
3
+ # the agent what's in the KG. Delegates to `predicate sessionstart` so the
4
+ # message format stays in one place.
4
5
  set -euo pipefail
5
- FUSEKI="${FUSEKI_URL:-http://localhost:3030}"
6
- DS="${PREDICATE_DATASET:-predicate}"
7
6
 
8
- if ! curl -fsS "$FUSEKI/$/ping" >/dev/null 2>&1; then
9
- jq -n '{ additional_context: "Predicate: Fuseki not reachable; KG tools may fail. Start it with `pnpm fuseki:up`." }'
10
- exit 0
7
+ if MSG="$(predicate sessionstart 2>/dev/null)"; then
8
+ :
9
+ else
10
+ MSG="Predicate: Fuseki not reachable; KG tools may fail. Start it with \`predicate up\`."
11
11
  fi
12
12
 
13
- GOALS=$(curl -fsS "$FUSEKI/$DS/query" \
14
- --data-urlencode "query=PREFIX pred: <https://predicate.dev/meta#>
15
- SELECT (COUNT(*) AS ?n) WHERE { GRAPH <kg:goals> { ?g pred:status \"active\" } }" \
16
- --header "Accept: application/sparql-results+json" \
17
- | jq -r '.results.bindings[0].n.value // "0"')
18
-
19
- CONCEPTS=$(curl -fsS "$FUSEKI/$DS/query" \
20
- --data-urlencode "query=SELECT (COUNT(DISTINCT ?c) AS ?n) WHERE { GRAPH <kg:tbox> { ?c a <http://www.w3.org/2002/07/owl#Class> } }" \
21
- --header "Accept: application/sparql-results+json" \
22
- | jq -r '.results.bindings[0].n.value // "0"')
23
-
24
- MSG="Predicate ready: ${GOALS} active goals, ${CONCEPTS} TBox classes. Use kg_explore_schema before drafting SPARQL."
25
13
  jq -n --arg m "$MSG" '{ additional_context: $m }'
package/hooks/stop.sh ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code Stop hook: reads the Stop-hook JSON payload from stdin,
3
+ # runs structured turn extraction (predicate extract), then a
4
+ # maintenance sweep. Fail-open: any error returns exit 0 so capture
5
+ # never blocks the user's next prompt.
6
+ set -uo pipefail
7
+
8
+ if ! command -v predicate >/dev/null 2>&1; then
9
+ exit 0
10
+ fi
11
+
12
+ # Buffer stdin so we can tee it into extract.
13
+ payload="$(cat || true)"
14
+
15
+ if [ -n "$payload" ]; then
16
+ printf '%s' "$payload" | predicate extract --from-stdin >/dev/null 2>&1 || true
17
+ fi
18
+
19
+ predicate maintain >/dev/null 2>&1 || true
20
+ exit 0