predicate-skill 1.6.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.
@@ -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>
@@ -15,7 +15,16 @@ scripts will fire on `sessionStart`, `preCompress`, and `stop`.
15
15
  |---|---|---|
16
16
  | `sessionStart` | `session-start.sh` | Prints KG status line; Gemini reads stdout as context. |
17
17
  | `preCompress` | `pre-compact.sh` | Runs `predicate maintain` before context compression. |
18
- | `stop` | `stop.sh` | Runs `predicate maintain` on session close. |
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.
19
28
 
20
29
  ## If your Gemini version doesn't expose hooks
21
30
 
@@ -1,5 +1,20 @@
1
1
  #!/usr/bin/env bash
2
- # Gemini CLI stop adapter: runs maintenance on session close.
3
- # Wire to the `stop` event in settings.json.
4
- set -euo pipefail
5
- predicate maintain
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,24 +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
- {
9
- "event": "PreToolUse",
10
- "matcher": "*",
11
- "command": "bash ${PLUGIN_DIR}/hooks/pre-tool-use.sh"
12
- },
13
- {
14
- "event": "PostToolUse",
15
- "matcher": "*",
16
- "command": "bash ${PLUGIN_DIR}/hooks/post-tool-use.sh"
17
- },
18
- {
19
- "event": "Stop",
20
- "matcher": "*",
21
- "command": "bash ${PLUGIN_DIR}/hooks/stop.sh"
22
- }
23
- ]
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
+ }
24
27
  }
@@ -16,7 +16,16 @@ hook scripts will fire on `session.started`, `session.compacted`, and
16
16
  |---|---|---|
17
17
  | `session.started` | `session-start.sh` | Prints KG status line; OpenCode reads stdout as context. |
18
18
  | `session.compacted` | `pre-compact.sh` | Runs `predicate maintain` before context compression. |
19
- | `session.stopped` | `stop.sh` | Runs `predicate maintain` on session close. |
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.
20
29
 
21
30
  ## Verify wiring
22
31
 
@@ -1,4 +1,20 @@
1
1
  #!/usr/bin/env bash
2
- # OpenCode stop adapter. Wire to the session.stopped event.
3
- set -euo pipefail
4
- predicate maintain
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "predicate-skill",
3
- "version": "1.6.0",
3
+ "version": "2.0.1",
4
4
  "description": "Local reasoning knowledge graph (RDF/OWL) for AI agents — Claude Code plugin + MCP server + predicate CLI.",
5
5
  "author": {
6
6
  "name": "Nordic Agents Research",
@@ -30,6 +30,7 @@
30
30
  "commands",
31
31
  "hooks",
32
32
  "compose",
33
+ "dashboard",
33
34
  "LICENSE",
34
35
  "README.md"
35
36
  ],