predicate-skill 1.6.0 → 2.0.2
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.
- package/.claude-plugin/plugin.json +6 -5
- package/README.md +6 -4
- package/cli.bundle.mjs +28254 -26871
- package/dashboard/index.html +180 -0
- package/hooks/gemini-cli/README.md +10 -1
- package/hooks/gemini-cli/stop.sh +19 -4
- package/hooks/hooks.json +25 -22
- package/hooks/opencode/README.md +10 -1
- package/hooks/opencode/stop.sh +19 -3
- package/package.json +2 -1
- package/server.bundle.mjs +28371 -18709
- 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 ≥3 sessions)</span></h2><div id="hotspots"></div></div>
|
|
38
|
+
<div class="card"><h2>Flaky Commands <span class="sub">(failed in ≥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` |
|
|
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
|
|
package/hooks/gemini-cli/stop.sh
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Gemini CLI stop
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
}
|
package/hooks/opencode/README.md
CHANGED
|
@@ -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` |
|
|
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
|
|
package/hooks/opencode/stop.sh
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# OpenCode stop
|
|
3
|
-
|
|
4
|
-
|
|
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": "
|
|
3
|
+
"version": "2.0.2",
|
|
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
|
],
|