@rune-kit/rune 2.18.1 → 2.21.0
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/README.md +31 -12
- package/compiler/__tests__/comprehension.test.js +916 -0
- package/compiler/__tests__/governance-collector.test.js +376 -0
- package/compiler/__tests__/setup.test.js +5 -0
- package/compiler/analytics.js +5 -0
- package/compiler/bin/rune.js +165 -1
- package/compiler/comprehension-client.js +2348 -0
- package/compiler/comprehension.js +1254 -0
- package/compiler/governance-collector.js +382 -0
- package/compiler/schemas/comprehension.schema.json +87 -0
- package/compiler/schemas/governance.schema.json +78 -0
- package/compiler/transforms/branding.js +1 -1
- package/hooks/context-watch/index.cjs +24 -13
- package/hooks/hooks.json +2 -2
- package/hooks/lib/context-key.cjs +37 -0
- package/hooks/metrics-collector/index.cjs +37 -8
- package/hooks/pre-tool-guard/index.cjs +44 -0
- package/hooks/session-start/index.cjs +4 -10
- package/package.json +1 -1
- package/skills/adversary/SKILL.md +36 -3
- package/skills/adversary/references/cross-model-escalation.md +85 -0
- package/skills/adversary/references/reasoning-modes.md +95 -0
- package/skills/autopsy/SKILL.md +41 -0
- package/skills/ba/SKILL.md +106 -27
- package/skills/ba/references/ears-format.md +91 -0
- package/skills/brainstorm/SKILL.md +32 -7
- package/skills/completion-gate/SKILL.md +21 -10
- package/skills/converge/SKILL.md +178 -0
- package/skills/converge/references/eval-fixtures.md +59 -0
- package/skills/cook/SKILL.md +41 -6
- package/skills/debug/SKILL.md +4 -2
- package/skills/deploy/SKILL.md +23 -2
- package/skills/deploy/references/observability.md +146 -0
- package/skills/design/SKILL.md +15 -1
- package/skills/graft/SKILL.md +24 -8
- package/skills/onboard/SKILL.md +45 -0
- package/skills/perf/SKILL.md +4 -1
- package/skills/plan/SKILL.md +70 -7
- package/skills/plan/references/boundary-artifacts.md +127 -0
- package/skills/plan/references/plan-templates.md +28 -9
- package/skills/plan/references/vertical-slice.md +8 -6
- package/skills/preflight/SKILL.md +9 -3
- package/skills/review/SKILL.md +4 -2
- package/skills/skill-router/SKILL.md +2 -0
- package/skills/test/SKILL.md +15 -8
- package/skills/verification/SKILL.md +39 -7
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governance Collector
|
|
3
|
+
*
|
|
4
|
+
* Assembles .rune/governance.json from three best-effort sources:
|
|
5
|
+
* 1. Gate fires — skill-frequency counts from analytics (.rune/metrics/)
|
|
6
|
+
* 2. Signals — static mesh signal definitions from the visualizer
|
|
7
|
+
* 3. Compliance — obligations declared in Business pack PACK.md files
|
|
8
|
+
*
|
|
9
|
+
* Phase-3 known capture gaps (updated from Phase 1):
|
|
10
|
+
*
|
|
11
|
+
* GAP-1 Gate outcomes (passed / bypassed / blocked) — PARTIALLY FILLED in Phase 3.
|
|
12
|
+
* pre-tool-guard now appends a structured record to
|
|
13
|
+
* .rune/metrics/gate-outcomes.jsonl when it decides to BLOCK (exit code 2).
|
|
14
|
+
* This file is read here and populates the `blocked` count + most-recent `ts`
|
|
15
|
+
* for the "privacy-mesh" gate.
|
|
16
|
+
* STILL uncaptured: `passed` and `bypassed` outcomes remain 0 because they
|
|
17
|
+
* are not observable at the hook layer without instrumenting every allow/pass
|
|
18
|
+
* path — which would add noise without signal value.
|
|
19
|
+
*
|
|
20
|
+
* GAP-2 Gate timestamps — no PER-INVOCATION timestamp is captured. Best-effort:
|
|
21
|
+
* the `ts` field carries the date-only `last_used` from skills.json when
|
|
22
|
+
* available, else null. A gate-outcome hook would provide real per-fire ts.
|
|
23
|
+
*
|
|
24
|
+
* GAP-6 `fired` is an INVOCATION count (cumulative total_invocations from
|
|
25
|
+
* skills.json, or windowed session-PRESENCE as fallback) — NOT a count of
|
|
26
|
+
* gate pass/fail events, and not strictly windowed when sourced from the
|
|
27
|
+
* cumulative totals file. Pair with GAP-1: outcomes remain uncaptured.
|
|
28
|
+
*
|
|
29
|
+
* GAP-3 Signal runtime counts — collectGraphData() returns static mesh
|
|
30
|
+
* definitions (emit / listen pairs) from the INSTALLED RUNE mesh
|
|
31
|
+
* (runeRoot/skills/**), not the buyer's app, and not runtime emission
|
|
32
|
+
* counts. Signal `count` is always 0 until a runtime signal log is wired.
|
|
33
|
+
*
|
|
34
|
+
* GAP-4 Compliance status — Business pack PACK.md files declare obligations
|
|
35
|
+
* (Constraints + Done-When items) but there is no automated outcome
|
|
36
|
+
* capture. All compliance entries are marked status:"unknown".
|
|
37
|
+
*
|
|
38
|
+
* GAP-5 Decision provenance — no provenance capture source exists in Phase 1.
|
|
39
|
+
* The decisions[] array is always []. A future journal/ADR hook or
|
|
40
|
+
* xlabs_log_decision integration would feed this field.
|
|
41
|
+
*
|
|
42
|
+
* None of the above gaps cause this function to throw — they degrade gracefully.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { existsSync } from 'node:fs';
|
|
46
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
47
|
+
import path from 'node:path';
|
|
48
|
+
import { collectGraphData } from './visualizer.js';
|
|
49
|
+
|
|
50
|
+
// ─── Gate skill list ───────────────────────────────────────────────────────────
|
|
51
|
+
// These are the governance / quality-gate skills tracked in the mesh.
|
|
52
|
+
// Add new gate skills here as they are introduced.
|
|
53
|
+
const GATE_SKILLS = [
|
|
54
|
+
'sentinel',
|
|
55
|
+
'sentinel-env',
|
|
56
|
+
'preflight',
|
|
57
|
+
'completion-gate',
|
|
58
|
+
'logic-guardian',
|
|
59
|
+
'constraint-check',
|
|
60
|
+
'hallucination-guard',
|
|
61
|
+
'integrity-check',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function readJsonl(content) {
|
|
67
|
+
return content
|
|
68
|
+
.trim()
|
|
69
|
+
.split('\n')
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.map((line) => {
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(line);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Load sessions.jsonl + skills.json from .rune/metrics/ in runeRoot.
|
|
83
|
+
* Returns empty structures on any error — never throws.
|
|
84
|
+
*/
|
|
85
|
+
async function loadMetrics(runeRoot) {
|
|
86
|
+
const metricsDir = path.join(runeRoot, '.rune', 'metrics');
|
|
87
|
+
|
|
88
|
+
let sessions = [];
|
|
89
|
+
try {
|
|
90
|
+
const sessionsPath = path.join(metricsDir, 'sessions.jsonl');
|
|
91
|
+
if (existsSync(sessionsPath)) {
|
|
92
|
+
sessions = readJsonl(await readFile(sessionsPath, 'utf-8'));
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
/* degrade silently */
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let skillTotals = {};
|
|
99
|
+
try {
|
|
100
|
+
const skillsPath = path.join(metricsDir, 'skills.json');
|
|
101
|
+
if (existsSync(skillsPath)) {
|
|
102
|
+
const raw = JSON.parse(await readFile(skillsPath, 'utf-8'));
|
|
103
|
+
skillTotals = raw.skills || {};
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
/* degrade silently */
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { sessions, skillTotals };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Filter sessions to the lookback window (inclusive).
|
|
114
|
+
*/
|
|
115
|
+
function filterByDays(sessions, days) {
|
|
116
|
+
if (!days || days <= 0) return sessions;
|
|
117
|
+
const cutoff = new Date();
|
|
118
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
119
|
+
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
|
120
|
+
return sessions.filter((s) => s.date >= cutoffStr);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Return the body of a "## <heading>" markdown section up to the next "## "
|
|
125
|
+
* heading or end-of-file. Index-based (no fragile regex end-anchor — JS has no \Z).
|
|
126
|
+
*/
|
|
127
|
+
function extractSection(content, heading) {
|
|
128
|
+
const start = content.search(new RegExp(`^##\\s+${heading}\\s*$`, 'm'));
|
|
129
|
+
if (start === -1) return '';
|
|
130
|
+
const body = content.slice(start).replace(/^##[^\n]*\n/, ''); // drop heading line
|
|
131
|
+
const next = body.search(/^##\s/m);
|
|
132
|
+
return next === -1 ? body : body.slice(0, next);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Gate-outcome capture reader ─────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Read .rune/metrics/gate-outcomes.jsonl and return a map of
|
|
139
|
+
* gate → { blocked: number, latestTs: string|null }
|
|
140
|
+
*
|
|
141
|
+
* Only "blocked" outcomes are currently captured (pre-tool-guard Phase 3).
|
|
142
|
+
* passed/bypassed remain uncaptured — see GAP-1.
|
|
143
|
+
*
|
|
144
|
+
* Never throws — degrades to empty map on any error.
|
|
145
|
+
*/
|
|
146
|
+
async function loadGateOutcomes(runeRoot) {
|
|
147
|
+
const outFile = path.join(runeRoot, '.rune', 'metrics', 'gate-outcomes.jsonl');
|
|
148
|
+
const result = new Map(); // gate → { blocked, latestTs }
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
if (!existsSync(outFile)) return result;
|
|
152
|
+
const raw = await readFile(outFile, 'utf-8');
|
|
153
|
+
for (const record of readJsonl(raw)) {
|
|
154
|
+
if (!record || typeof record.gate !== 'string') continue;
|
|
155
|
+
const gate = record.gate;
|
|
156
|
+
const existing = result.get(gate) || { blocked: 0, latestTs: null };
|
|
157
|
+
|
|
158
|
+
if (record.outcome === 'blocked') {
|
|
159
|
+
existing.blocked += 1;
|
|
160
|
+
// Keep the most-recent ts (ISO string compare works lexicographically)
|
|
161
|
+
if (record.ts && (!existing.latestTs || record.ts > existing.latestTs)) {
|
|
162
|
+
existing.latestTs = record.ts;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
result.set(gate, existing);
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
/* degrade silently */
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Gate assembly ────────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Assemble gates[] — one entry per governance gate skill with activity.
|
|
178
|
+
*
|
|
179
|
+
* `fired` = cumulative invocation count from skills.json `total_invocations`
|
|
180
|
+
* (the TRUE fire count the producer accumulates), falling back to windowed
|
|
181
|
+
* session-PRESENCE when skills.json is absent. See GAP-6 — this is an
|
|
182
|
+
* invocation count, NOT a pass/fail outcome.
|
|
183
|
+
*/
|
|
184
|
+
async function assembleGates(runeRoot, days) {
|
|
185
|
+
const [{ sessions, skillTotals }, gateOutcomes] = await Promise.all([
|
|
186
|
+
loadMetrics(runeRoot),
|
|
187
|
+
loadGateOutcomes(runeRoot),
|
|
188
|
+
]);
|
|
189
|
+
const filtered = filterByDays(sessions, days);
|
|
190
|
+
|
|
191
|
+
// Windowed session-PRESENCE: number of sessions in the window that invoked the
|
|
192
|
+
// gate. NOTE: sessions.skills_used is DEDUPED per session by the producer
|
|
193
|
+
// (post-session-reflect writes Object.keys(skillCounts)), so this counts
|
|
194
|
+
// "sessions that used the gate", not individual fires. Used only as a fallback.
|
|
195
|
+
const sessionPresence = {};
|
|
196
|
+
for (const session of filtered) {
|
|
197
|
+
if (!Array.isArray(session.skills_used)) continue;
|
|
198
|
+
for (const skill of session.skills_used) {
|
|
199
|
+
if (GATE_SKILLS.includes(skill)) {
|
|
200
|
+
sessionPresence[skill] = (sessionPresence[skill] || 0) + 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// skills.json shape (from post-session-reflect): { <skill>: { total_invocations, last_used } }
|
|
206
|
+
const totalFires = (name) => {
|
|
207
|
+
const rec = skillTotals[name];
|
|
208
|
+
return rec && typeof rec === 'object' ? rec.total_invocations || 0 : 0;
|
|
209
|
+
};
|
|
210
|
+
const lastUsedTs = (name) => {
|
|
211
|
+
const rec = skillTotals[name];
|
|
212
|
+
return rec && typeof rec === 'object' && rec.last_used ? `${rec.last_used}T00:00:00.000Z` : null;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const allGateNames = new Set([
|
|
216
|
+
...Object.keys(sessionPresence),
|
|
217
|
+
...GATE_SKILLS.filter((g) => totalFires(g) > 0),
|
|
218
|
+
...gateOutcomes.keys(),
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
if (allGateNames.size === 0) return [];
|
|
222
|
+
|
|
223
|
+
return Array.from(allGateNames).map((name) => {
|
|
224
|
+
const outcomes = gateOutcomes.get(name) || { blocked: 0, latestTs: null };
|
|
225
|
+
// Prefer outcome ts over skills.json date when a real block event was recorded
|
|
226
|
+
const ts = outcomes.latestTs || lastUsedTs(name);
|
|
227
|
+
return {
|
|
228
|
+
name,
|
|
229
|
+
// Cumulative true fire count; windowed session-presence as fallback (GAP-6).
|
|
230
|
+
fired: totalFires(name) || sessionPresence[name] || 0,
|
|
231
|
+
// GAP-1 (Phase 3 partial fill): blocked IS now captured from gate-outcomes.jsonl.
|
|
232
|
+
// passed/bypassed remain 0 — not observable at the hook layer.
|
|
233
|
+
passed: 0,
|
|
234
|
+
bypassed: 0,
|
|
235
|
+
blocked: outcomes.blocked,
|
|
236
|
+
// Best-effort ts: real block event ts when available, else last_used from skills.json.
|
|
237
|
+
ts,
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ─── Signal assembly ──────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Extract signal definitions from the static mesh via collectGraphData.
|
|
246
|
+
* Returns signals[] with from/to derived from signalEdges.
|
|
247
|
+
*
|
|
248
|
+
* GAP-3: runtime counts are always 0 until signal emission is logged.
|
|
249
|
+
*/
|
|
250
|
+
async function assembleSignals(runeRoot) {
|
|
251
|
+
let graphData;
|
|
252
|
+
try {
|
|
253
|
+
graphData = await collectGraphData(runeRoot);
|
|
254
|
+
} catch {
|
|
255
|
+
return []; // degrade if skills dir is missing or parse fails
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const { signalEdges } = graphData;
|
|
259
|
+
if (!Array.isArray(signalEdges) || signalEdges.length === 0) return [];
|
|
260
|
+
|
|
261
|
+
// Deduplicate: one entry per (signal name, from, to) triple.
|
|
262
|
+
const seen = new Map();
|
|
263
|
+
for (const edge of signalEdges) {
|
|
264
|
+
const key = `${edge.signal}::${edge.source}::${edge.target}`;
|
|
265
|
+
if (!seen.has(key)) {
|
|
266
|
+
seen.set(key, {
|
|
267
|
+
name: edge.signal,
|
|
268
|
+
from: edge.source || null,
|
|
269
|
+
to: edge.target || null,
|
|
270
|
+
// GAP-3: no runtime count available in Phase 1
|
|
271
|
+
count: 0,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return Array.from(seen.values());
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ─── Compliance assembly ──────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Extract compliance obligations from Business PACK.md files.
|
|
283
|
+
* Looks for Business pack at <runeRoot>/../Business/extensions/pro-*.
|
|
284
|
+
*
|
|
285
|
+
* Obligations are extracted from the ## Constraints and ## Done When sections —
|
|
286
|
+
* the same sections Business PACK.md authors already maintain.
|
|
287
|
+
*
|
|
288
|
+
* GAP-4: outcomes are always "unknown" until automated verification exists.
|
|
289
|
+
*/
|
|
290
|
+
async function assembleCompliance(runeRoot) {
|
|
291
|
+
const bizExtDir = path.join(runeRoot, '..', 'Business', 'extensions');
|
|
292
|
+
if (!existsSync(bizExtDir)) return [];
|
|
293
|
+
|
|
294
|
+
let packDirs;
|
|
295
|
+
try {
|
|
296
|
+
const entries = await readdir(bizExtDir, { withFileTypes: true });
|
|
297
|
+
packDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
298
|
+
} catch {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const obligations = [];
|
|
303
|
+
|
|
304
|
+
for (const packDir of packDirs) {
|
|
305
|
+
const packFile = path.join(bizExtDir, packDir, 'PACK.md');
|
|
306
|
+
if (!existsSync(packFile)) continue;
|
|
307
|
+
|
|
308
|
+
let content;
|
|
309
|
+
try {
|
|
310
|
+
content = await readFile(packFile, 'utf-8');
|
|
311
|
+
} catch {
|
|
312
|
+
continue; // skip unreadable pack
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Derive a human-readable pack name from the directory name.
|
|
316
|
+
const packName = packDir.replace(/^pro-/, '@rune-business/');
|
|
317
|
+
|
|
318
|
+
// Extract obligations from ## Constraints. Real Business PACK.md files use
|
|
319
|
+
// numbered ("1. MUST …") OR bulleted ("- MUST …") lists, so accept both.
|
|
320
|
+
const constraintsBody = extractSection(content, 'Constraints');
|
|
321
|
+
const listItem = /^(?:[-*]|\d+\.)\s+(.*)$/;
|
|
322
|
+
for (const raw of constraintsBody.split('\n')) {
|
|
323
|
+
const m = raw.trim().match(listItem);
|
|
324
|
+
if (!m) continue;
|
|
325
|
+
const text = m[1].trim();
|
|
326
|
+
if (!/^(MUST|NEVER)\b/.test(text)) continue;
|
|
327
|
+
obligations.push({
|
|
328
|
+
pack: packName,
|
|
329
|
+
obligation: text,
|
|
330
|
+
// GAP-4: no automated outcome verification yet
|
|
331
|
+
status: 'unknown',
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Extract checkbox items from ## Done When (only checked boxes are "met").
|
|
336
|
+
const doneWhenBody = extractSection(content, 'Done When');
|
|
337
|
+
for (const raw of doneWhenBody.split('\n')) {
|
|
338
|
+
const line = raw.trim();
|
|
339
|
+
const cb = line.match(/^[-*]\s+\[([ xX])\]\s+(.*)$/);
|
|
340
|
+
if (!cb) continue;
|
|
341
|
+
const met = cb[1] === 'x' || cb[1] === 'X';
|
|
342
|
+
obligations.push({
|
|
343
|
+
pack: packName,
|
|
344
|
+
obligation: cb[2].trim(),
|
|
345
|
+
// Only mark "met" if the checkbox is checked in the PACK.md itself;
|
|
346
|
+
// otherwise unknown because we cannot verify runtime outcomes.
|
|
347
|
+
status: met ? 'met' : 'unknown',
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return obligations;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Assemble a governance.json object conforming to governance.schema.json.
|
|
359
|
+
*
|
|
360
|
+
* @param {string} runeRoot - Absolute path to the Free repo root (the directory
|
|
361
|
+
* that contains the `skills/` and `.rune/` directories).
|
|
362
|
+
* @param {number} [days=30] - Lookback window in days for gate fire counts.
|
|
363
|
+
* @returns {Promise<object>} Governance record (never throws — degrades gracefully).
|
|
364
|
+
*/
|
|
365
|
+
export async function assembleGovernance(runeRoot, days = 30) {
|
|
366
|
+
const [gates, signals, compliance] = await Promise.all([
|
|
367
|
+
assembleGates(runeRoot, days).catch(() => []),
|
|
368
|
+
assembleSignals(runeRoot).catch(() => []),
|
|
369
|
+
assembleCompliance(runeRoot).catch(() => []),
|
|
370
|
+
]);
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
generated_at: new Date().toISOString(),
|
|
374
|
+
window_days: days,
|
|
375
|
+
gates,
|
|
376
|
+
signals,
|
|
377
|
+
compliance,
|
|
378
|
+
// GAP-5: decision provenance has no capture source in Phase 1.
|
|
379
|
+
// A future journal ADR hook or xlabs_log_decision integration would feed this.
|
|
380
|
+
decisions: [],
|
|
381
|
+
};
|
|
382
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://rune-kit.dev/schemas/comprehension.schema.json",
|
|
4
|
+
"title": "Rune Comprehension Graph",
|
|
5
|
+
"description": "Buyer-codebase comprehension graph emitted by onboard/autopsy. Powers the dashboard Understand tab (modules, domains, flows, steps) and the Verdict health score. Written to .rune/comprehension.json.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["project", "generated_at", "modules", "edges"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"project": { "type": "string", "description": "Project name" },
|
|
11
|
+
"generated_at": { "type": "string", "format": "date-time" },
|
|
12
|
+
"source": { "type": "string", "enum": ["onboard", "autopsy"], "description": "Which skill produced this emit" },
|
|
13
|
+
"health_score": { "type": ["integer", "null"], "minimum": 0, "maximum": 100, "description": "0-100, or null when the producer (e.g. onboard) does not compute a score" },
|
|
14
|
+
"layers": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"description": "Architectural layers detected (e.g. API, Service, Data, UI, Utility)",
|
|
17
|
+
"items": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"required": ["id", "name"],
|
|
20
|
+
"additionalProperties": false,
|
|
21
|
+
"properties": {
|
|
22
|
+
"id": { "type": "string" },
|
|
23
|
+
"name": { "type": "string" },
|
|
24
|
+
"color": { "type": "string", "description": "Optional palette key (code/service/data/domain/docs/infra/concept)" }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"domains": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"description": "Business domains → flows → steps (non-tech readable process view)",
|
|
31
|
+
"items": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"required": ["name", "flows"],
|
|
34
|
+
"additionalProperties": false,
|
|
35
|
+
"properties": {
|
|
36
|
+
"name": { "type": "string" },
|
|
37
|
+
"summary": { "type": "string" },
|
|
38
|
+
"flows": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"items": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"required": ["name", "steps"],
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"properties": {
|
|
45
|
+
"name": { "type": "string" },
|
|
46
|
+
"steps": { "type": "array", "items": { "type": "string" } }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"modules": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"description": "Graph nodes — files/modules/services/etc.",
|
|
56
|
+
"items": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"required": ["id", "name"],
|
|
59
|
+
"additionalProperties": false,
|
|
60
|
+
"properties": {
|
|
61
|
+
"id": { "type": "string" },
|
|
62
|
+
"name": { "type": "string" },
|
|
63
|
+
"layer": { "type": "string" },
|
|
64
|
+
"type": { "type": "string", "description": "file | function | class | module | service | table | endpoint | domain | flow | step | doc | config" },
|
|
65
|
+
"complexity": { "type": "string", "enum": ["simple", "moderate", "complex"] },
|
|
66
|
+
"files": { "type": "integer", "minimum": 0 },
|
|
67
|
+
"summary": { "type": "string" }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"edges": {
|
|
72
|
+
"type": "array",
|
|
73
|
+
"description": "Graph edges between modules",
|
|
74
|
+
"items": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"required": ["from", "to", "category"],
|
|
77
|
+
"additionalProperties": false,
|
|
78
|
+
"properties": {
|
|
79
|
+
"from": { "type": "string" },
|
|
80
|
+
"to": { "type": "string" },
|
|
81
|
+
"category": { "type": "string", "enum": ["structural", "behavioral", "data-flow", "dependency", "semantic", "infrastructure", "domain", "knowledge"] },
|
|
82
|
+
"weight": { "type": "number" }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://rune-kit.dev/schemas/governance.schema.json",
|
|
4
|
+
"title": "Rune Governance Record",
|
|
5
|
+
"description": "Governance + value evidence assembled by governance-collector from metrics, session-bridge state, journal ADRs, and the signal mesh. Powers the dashboard Verdict + Govern tabs (gates, signals, compliance, decision provenance). Written to .rune/governance.json.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["generated_at", "gates", "signals"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"generated_at": { "type": "string", "format": "date-time" },
|
|
11
|
+
"window_days": { "type": "integer", "minimum": 1, "description": "Lookback window for the assembled record" },
|
|
12
|
+
"gates": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"description": "HARD-GATE fire events (sentinel, preflight, completion-gate, logic-guardian, …)",
|
|
15
|
+
"items": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"required": ["name", "fired"],
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"properties": {
|
|
20
|
+
"name": { "type": "string" },
|
|
21
|
+
"fired": { "type": "integer", "minimum": 0, "description": "INVOCATION count of the gate skill (cumulative total_invocations from skills.json; windowed session-presence as fallback). NOT a pass/fail outcome count — see passed/bypassed/blocked." },
|
|
22
|
+
"passed": { "type": "integer", "minimum": 0 },
|
|
23
|
+
"bypassed": { "type": "integer", "minimum": 0 },
|
|
24
|
+
"blocked": { "type": "integer", "minimum": 0 },
|
|
25
|
+
"ts": { "type": "string", "format": "date-time", "description": "Most recent fire" }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"signals": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"description": "Mesh signal emissions captured (cause→effect across skills/packs)",
|
|
32
|
+
"items": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"required": ["name"],
|
|
35
|
+
"additionalProperties": false,
|
|
36
|
+
"properties": {
|
|
37
|
+
"name": { "type": "string" },
|
|
38
|
+
"from": { "type": "string" },
|
|
39
|
+
"to": { "type": "string" },
|
|
40
|
+
"count": { "type": "integer", "minimum": 0 },
|
|
41
|
+
"ts": { "type": "string", "format": "date-time" }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"compliance": {
|
|
46
|
+
"type": "array",
|
|
47
|
+
"description": "Obligations declared by installed Business packs vs met/gap status",
|
|
48
|
+
"items": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"required": ["pack", "obligation", "status"],
|
|
51
|
+
"additionalProperties": false,
|
|
52
|
+
"properties": {
|
|
53
|
+
"pack": { "type": "string" },
|
|
54
|
+
"obligation": { "type": "string" },
|
|
55
|
+
"status": { "type": "string", "enum": ["met", "partial", "gap", "unknown"] },
|
|
56
|
+
"evidence": { "type": "string" }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"decisions": {
|
|
61
|
+
"type": "array",
|
|
62
|
+
"description": "Decision provenance — trace an output back through signal chain → gate → model → cost",
|
|
63
|
+
"items": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"required": ["output"],
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"properties": {
|
|
68
|
+
"output": { "type": "string", "description": "Commit, file, or artifact" },
|
|
69
|
+
"signal_chain": { "type": "array", "items": { "type": "string" } },
|
|
70
|
+
"gate": { "type": "string" },
|
|
71
|
+
"model": { "type": "string" },
|
|
72
|
+
"cost": { "type": "number", "description": "Estimated USD" },
|
|
73
|
+
"ts": { "type": "string", "format": "date-time" }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
export const BRANDING_FOOTER = [
|
|
13
13
|
'',
|
|
14
14
|
'---',
|
|
15
|
-
'> **Rune Skill Mesh** — 64 skills,
|
|
15
|
+
'> **Rune Skill Mesh** — 64 skills, 204 connections + 40 signals, 14 extension packs',
|
|
16
16
|
'> [Landing Page](https://rune-kit.github.io/rune) · [Source](https://github.com/rune-kit/rune) (MIT)',
|
|
17
17
|
'> **Rune Pro** ($49 lifetime) — product, sales, data-science, support packs → [rune-kit/rune-pro](https://github.com/rune-kit/rune-pro)',
|
|
18
18
|
'> **Rune Business** ($149 lifetime) — finance, legal, HR, enterprise-search packs → [rune-kit/rune-business](https://github.com/rune-kit/rune-business)',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Rune Context Watch Hook
|
|
2
2
|
// Lightweight tool call counter — detects context pressure and suggests rune:context-engine
|
|
3
|
-
// Runs as PreToolUse hook on
|
|
3
|
+
// Runs as PreToolUse hook on ALL tools (matcher ".*") — total tool volume is the
|
|
4
|
+
// context-pressure proxy and feeds tool_distribution metrics at session end.
|
|
4
5
|
//
|
|
5
6
|
// H3 Intelligence: also tracks tool type distribution and session start timestamp
|
|
6
7
|
// for metrics aggregation at session end.
|
|
@@ -11,18 +12,19 @@
|
|
|
11
12
|
// Data source: stdin JSON from Claude Code (not env vars)
|
|
12
13
|
|
|
13
14
|
const fs = require('fs');
|
|
14
|
-
const
|
|
15
|
-
const os = require('os');
|
|
15
|
+
const { stateFile } = require('../lib/context-key.cjs');
|
|
16
16
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const counterFile = path.join(os.tmpdir(), `rune-context-watch-${hash}.json`);
|
|
17
|
+
// The counter file is keyed by Claude Code session_id (parsed from stdin in the
|
|
18
|
+
// handler below) so it resets per session and never bleeds across sessions or
|
|
19
|
+
// projects. See lib/context-key.cjs.
|
|
21
20
|
|
|
22
|
-
// Thresholds
|
|
23
|
-
|
|
21
|
+
// Thresholds — counts ALL tool calls (matcher ".*"), not just Edit/Write, so the
|
|
22
|
+
// totals reflect true context pressure AND feed accurate tool_distribution metrics.
|
|
23
|
+
// Aligned with the context-engine skill's own model (ORANGE ~80, RED ~120 tool calls)
|
|
24
|
+
// so the hook and the skill it delegates to agree.
|
|
25
|
+
const FIRST_WARNING = 80;
|
|
24
26
|
const REPEAT_INTERVAL = 20;
|
|
25
|
-
const CRITICAL_THRESHOLD =
|
|
27
|
+
const CRITICAL_THRESHOLD = 120;
|
|
26
28
|
|
|
27
29
|
// Read stdin JSON to get tool name (Claude Code passes hook data via stdin)
|
|
28
30
|
let stdinData = '';
|
|
@@ -30,14 +32,19 @@ process.stdin.setEncoding('utf-8');
|
|
|
30
32
|
process.stdin.on('data', chunk => { stdinData += chunk; });
|
|
31
33
|
process.stdin.on('end', () => {
|
|
32
34
|
let toolName = 'unknown';
|
|
35
|
+
let sessionId;
|
|
33
36
|
try {
|
|
34
37
|
const parsed = JSON.parse(stdinData);
|
|
35
38
|
toolName = parsed.tool || parsed.tool_name || 'unknown';
|
|
39
|
+
sessionId = parsed.session_id;
|
|
36
40
|
} catch {
|
|
37
41
|
// Fallback to env var for backwards compatibility
|
|
38
42
|
toolName = process.env.CLAUDE_TOOL_NAME || 'unknown';
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
// Session-keyed counter (resets per session, no cross-session/project bleed).
|
|
46
|
+
const counterFile = stateFile('rune-context-watch', sessionId);
|
|
47
|
+
|
|
41
48
|
// Read current state
|
|
42
49
|
let state = { count: 0, lastWarning: 0, sessionStart: null, sessionId: null, toolCounts: {} };
|
|
43
50
|
try {
|
|
@@ -57,16 +64,20 @@ process.stdin.on('end', () => {
|
|
|
57
64
|
state.sessionId = `s-${now.slice(0, 10).replace(/-/g, '')}-${now.slice(11, 19).replace(/:/g, '')}`;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
// Increment total and per-tool
|
|
67
|
+
// Increment total (drives context-pressure warnings) and per-tool distribution.
|
|
68
|
+
// Skip the per-tool bucket when the tool name is unattributable, so a parse
|
|
69
|
+
// hiccup doesn't pollute tool_distribution with an "unknown" entry.
|
|
61
70
|
state.count += 1;
|
|
62
|
-
|
|
71
|
+
if (toolName !== 'unknown') {
|
|
72
|
+
state.toolCounts[toolName] = (state.toolCounts[toolName] || 0) + 1;
|
|
73
|
+
}
|
|
63
74
|
|
|
64
75
|
// Check thresholds
|
|
65
76
|
const count = state.count;
|
|
66
77
|
const sinceLast = count - state.lastWarning;
|
|
67
78
|
|
|
68
79
|
if (count >= CRITICAL_THRESHOLD && sinceLast >= REPEAT_INTERVAL) {
|
|
69
|
-
console.log(`\n🔴 [Rune context-watch] ${count} tool calls — context likely RED (
|
|
80
|
+
console.log(`\n🔴 [Rune context-watch] ${count} tool calls — context likely RED (tool count is directional, not a %).`);
|
|
70
81
|
console.log(' RECOMMENDED: Invoke rune:context-engine for state save + /compact.');
|
|
71
82
|
console.log(' Risk: auto-compaction may lose critical decisions without state save.\n');
|
|
72
83
|
state.lastWarning = count;
|
package/hooks/hooks.json
CHANGED