@a-company/gnomon 0.0.0-stage → 8.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.
Files changed (71) hide show
  1. package/README.md +126 -2
  2. package/dist/agent.d.ts +28 -0
  3. package/dist/agent.js +45708 -0
  4. package/dist/build-id.json +1 -0
  5. package/dist/cli/gnomon.js +53946 -0
  6. package/dist/cli/gnomond.js +47744 -0
  7. package/dist/gnomon-local/cli.js +897 -0
  8. package/dist/index.d.ts +3477 -0
  9. package/dist/index.js +13698 -0
  10. package/dist/loops.d.ts +440 -0
  11. package/dist/loops.js +4706 -0
  12. package/dist/signer-CQ3Bpe7G.d.ts +158 -0
  13. package/dist/transport/gnomon-web.js +688 -0
  14. package/gnomon-mods/.purpose +143 -0
  15. package/gnomon-mods/constellation/README.md +27 -0
  16. package/gnomon-mods/constellation/fixtures/map-the-idea.golden.json +19 -0
  17. package/gnomon-mods/constellation/fixtures/map-the-idea.json +3 -0
  18. package/gnomon-mods/constellation/fixtures/summarize-the-session.golden.json +20 -0
  19. package/gnomon-mods/constellation/fixtures/summarize-the-session.json +3 -0
  20. package/gnomon-mods/constellation/fixtures/two-blocks.golden.json +4 -0
  21. package/gnomon-mods/constellation/fixtures/two-blocks.json +3 -0
  22. package/gnomon-mods/constellation/gnomon-manifest.json +68 -0
  23. package/gnomon-mods/constellation/skills/constellation-desk/SKILL.md +35 -0
  24. package/gnomon-mods/ledger/README.md +10 -0
  25. package/gnomon-mods/ledger/fixtures/breaker.golden.json +28 -0
  26. package/gnomon-mods/ledger/fixtures/breaker.json +80 -0
  27. package/gnomon-mods/ledger/fixtures/compacted.golden.json +42 -0
  28. package/gnomon-mods/ledger/fixtures/compacted.json +103 -0
  29. package/gnomon-mods/ledger/fixtures/over-budget.golden.json +24 -0
  30. package/gnomon-mods/ledger/fixtures/over-budget.json +86 -0
  31. package/gnomon-mods/ledger/fixtures/strip-reduced.golden.json +23 -0
  32. package/gnomon-mods/ledger/fixtures/strip-reduced.json +76 -0
  33. package/gnomon-mods/ledger/fixtures/strip.golden.json +18 -0
  34. package/gnomon-mods/ledger/fixtures/strip.json +68 -0
  35. package/gnomon-mods/ledger/gnomon-manifest.json +16 -0
  36. package/gnomon-mods/ledger/index.mjs +77 -0
  37. package/gnomon-mods/ledger/model.mjs +237 -0
  38. package/gnomon-mods/neurons/README.md +31 -0
  39. package/gnomon-mods/neurons/fixtures/capture-a-decision.golden.json +20 -0
  40. package/gnomon-mods/neurons/fixtures/capture-a-decision.json +3 -0
  41. package/gnomon-mods/neurons/fixtures/capture-a-learning.golden.json +19 -0
  42. package/gnomon-mods/neurons/fixtures/capture-a-learning.json +3 -0
  43. package/gnomon-mods/neurons/fixtures/search-the-brain.golden.json +19 -0
  44. package/gnomon-mods/neurons/fixtures/search-the-brain.json +3 -0
  45. package/gnomon-mods/neurons/fixtures/undeclared-kind.golden.json +9 -0
  46. package/gnomon-mods/neurons/fixtures/undeclared-kind.json +3 -0
  47. package/gnomon-mods/neurons/gnomon-manifest.json +87 -0
  48. package/gnomon-mods/neurons/skills/neurons-desk/SKILL.md +44 -0
  49. package/gnomon-mods/paradigm-context/README.md +9 -0
  50. package/gnomon-mods/paradigm-context/fixtures/empty.golden.json +28 -0
  51. package/gnomon-mods/paradigm-context/fixtures/empty.json +48 -0
  52. package/gnomon-mods/paradigm-context/fixtures/full-dark.golden.json +497 -0
  53. package/gnomon-mods/paradigm-context/fixtures/full-dark.json +134 -0
  54. package/gnomon-mods/paradigm-context/fixtures/full.golden.json +497 -0
  55. package/gnomon-mods/paradigm-context/fixtures/full.json +134 -0
  56. package/gnomon-mods/paradigm-context/fixtures/stale.golden.json +205 -0
  57. package/gnomon-mods/paradigm-context/fixtures/stale.json +69 -0
  58. package/gnomon-mods/paradigm-context/fixtures/unfocused.golden.json +28 -0
  59. package/gnomon-mods/paradigm-context/fixtures/unfocused.json +14 -0
  60. package/gnomon-mods/paradigm-context/gnomon-manifest.json +15 -0
  61. package/gnomon-mods/paradigm-context/index.mjs +64 -0
  62. package/gnomon-mods/paradigm-context/model.mjs +137 -0
  63. package/gnomon-mods/workshop/README.md +12 -0
  64. package/gnomon-mods/workshop/fixtures/strip-reduced.golden.json +56 -0
  65. package/gnomon-mods/workshop/fixtures/strip-reduced.json +16 -0
  66. package/gnomon-mods/workshop/fixtures/strip.golden.json +56 -0
  67. package/gnomon-mods/workshop/fixtures/strip.json +16 -0
  68. package/gnomon-mods/workshop/gnomon-manifest.json +14 -0
  69. package/gnomon-mods/workshop/index.mjs +73 -0
  70. package/gnomon-mods/workshop/model.mjs +152 -0
  71. package/package.json +80 -4
@@ -0,0 +1,86 @@
1
+ {
2
+ "view": "ledger.strip",
3
+ "input": {
4
+ "fixtureEvents": [
5
+ {
6
+ "kind": "session.open",
7
+ "payload": {
8
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
9
+ "backend": "claude",
10
+ "project": 2
11
+ }
12
+ },
13
+ {
14
+ "kind": "turn.usage",
15
+ "payload": {
16
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
17
+ "tokens": 40000
18
+ }
19
+ },
20
+ {
21
+ "kind": "turn.result",
22
+ "payload": {
23
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
24
+ "tokens": 0,
25
+ "tokensIn": 12000,
26
+ "tokensOut": 4000,
27
+ "cacheRead": 90000,
28
+ "cacheWrite": 8000,
29
+ "costMicros": 210000
30
+ }
31
+ },
32
+ {
33
+ "kind": "session.compacted",
34
+ "payload": {
35
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
36
+ "tokensBefore": 180000,
37
+ "tokensAfter": 52000
38
+ }
39
+ },
40
+ {
41
+ "kind": "turn.result",
42
+ "payload": {
43
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
44
+ "tokens": 0,
45
+ "tokensIn": 15000,
46
+ "tokensOut": 5000,
47
+ "cacheRead": 39000,
48
+ "cacheWrite": 18000,
49
+ "costMicros": 210000
50
+ }
51
+ },
52
+ {
53
+ "kind": "session.compacted",
54
+ "payload": {
55
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
56
+ "tokensBefore": 190000,
57
+ "tokensAfter": 41000
58
+ }
59
+ },
60
+ {
61
+ "kind": "budget.ladder",
62
+ "payload": {
63
+ "dimension": "tokens",
64
+ "scope": "session",
65
+ "rung": 90
66
+ }
67
+ },
68
+ {
69
+ "kind": "budget.usage",
70
+ "payload": {
71
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
72
+ "dimension": "tokens",
73
+ "n": 182000,
74
+ "of": 150000,
75
+ "over": true
76
+ }
77
+ }
78
+ ],
79
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA"
80
+ },
81
+ "width": 80,
82
+ "prefs": {
83
+ "motion": "full",
84
+ "theme": "day"
85
+ }
86
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "schemaVersion": "display-block:v1",
3
+ "type": "Box",
4
+ "title": "Ledger · this session",
5
+ "direction": "row",
6
+ "children": [
7
+ {
8
+ "type": "Text",
9
+ "text": "●",
10
+ "tone": "hue-2"
11
+ },
12
+ {
13
+ "type": "Text",
14
+ "text": "182k in · 9k out · 71% cache · $0.42 · ⇣ 2 compactions (190k → 41k)",
15
+ "tone": "warn"
16
+ },
17
+ {
18
+ "type": "Text",
19
+ "text": "▮ tokens 75 of 100% (session daily cap)",
20
+ "tone": "warn"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,76 @@
1
+ {
2
+ "view": "ledger.strip",
3
+ "input": {
4
+ "fixtureEvents": [
5
+ {
6
+ "kind": "session.open",
7
+ "payload": {
8
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
9
+ "backend": "claude",
10
+ "project": 2
11
+ }
12
+ },
13
+ {
14
+ "kind": "turn.usage",
15
+ "payload": {
16
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
17
+ "tokens": 40000
18
+ }
19
+ },
20
+ {
21
+ "kind": "turn.result",
22
+ "payload": {
23
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
24
+ "tokens": 0,
25
+ "tokensIn": 12000,
26
+ "tokensOut": 4000,
27
+ "cacheRead": 90000,
28
+ "cacheWrite": 8000,
29
+ "costMicros": 210000
30
+ }
31
+ },
32
+ {
33
+ "kind": "session.compacted",
34
+ "payload": {
35
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
36
+ "tokensBefore": 180000,
37
+ "tokensAfter": 52000
38
+ }
39
+ },
40
+ {
41
+ "kind": "turn.result",
42
+ "payload": {
43
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
44
+ "tokens": 0,
45
+ "tokensIn": 15000,
46
+ "tokensOut": 5000,
47
+ "cacheRead": 39000,
48
+ "cacheWrite": 18000,
49
+ "costMicros": 210000
50
+ }
51
+ },
52
+ {
53
+ "kind": "session.compacted",
54
+ "payload": {
55
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
56
+ "tokensBefore": 190000,
57
+ "tokensAfter": 41000
58
+ }
59
+ },
60
+ {
61
+ "kind": "budget.ladder",
62
+ "payload": {
63
+ "dimension": "tokens",
64
+ "scope": "session",
65
+ "rung": 75
66
+ }
67
+ }
68
+ ],
69
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA"
70
+ },
71
+ "width": 80,
72
+ "prefs": {
73
+ "motion": "reduced",
74
+ "theme": "day"
75
+ }
76
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "schemaVersion": "display-block:v1",
3
+ "type": "Box",
4
+ "title": "Ledger · this session",
5
+ "direction": "row",
6
+ "children": [
7
+ {
8
+ "type": "Text",
9
+ "text": "●",
10
+ "tone": "hue-2"
11
+ },
12
+ {
13
+ "type": "Text",
14
+ "text": "182k in · 9k out · 71% cache · $0.42 · ⇣ 2 compactions (190k → 41k)",
15
+ "tone": "quiet"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "view": "ledger.strip",
3
+ "input": {
4
+ "fixtureEvents": [
5
+ {
6
+ "kind": "session.open",
7
+ "payload": {
8
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
9
+ "backend": "claude",
10
+ "project": 2
11
+ }
12
+ },
13
+ {
14
+ "kind": "turn.usage",
15
+ "payload": {
16
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
17
+ "tokens": 40000
18
+ }
19
+ },
20
+ {
21
+ "kind": "turn.result",
22
+ "payload": {
23
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
24
+ "tokens": 0,
25
+ "tokensIn": 12000,
26
+ "tokensOut": 4000,
27
+ "cacheRead": 90000,
28
+ "cacheWrite": 8000,
29
+ "costMicros": 210000
30
+ }
31
+ },
32
+ {
33
+ "kind": "session.compacted",
34
+ "payload": {
35
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
36
+ "tokensBefore": 180000,
37
+ "tokensAfter": 52000
38
+ }
39
+ },
40
+ {
41
+ "kind": "turn.result",
42
+ "payload": {
43
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
44
+ "tokens": 0,
45
+ "tokensIn": 15000,
46
+ "tokensOut": 5000,
47
+ "cacheRead": 39000,
48
+ "cacheWrite": 18000,
49
+ "costMicros": 210000
50
+ }
51
+ },
52
+ {
53
+ "kind": "session.compacted",
54
+ "payload": {
55
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA",
56
+ "tokensBefore": 190000,
57
+ "tokensAfter": 41000
58
+ }
59
+ }
60
+ ],
61
+ "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA"
62
+ },
63
+ "width": 80,
64
+ "prefs": {
65
+ "motion": "full",
66
+ "theme": "day"
67
+ }
68
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "manifest": 2,
3
+ "kind": "mod",
4
+ "publisher": "gnomon",
5
+ "name": "ledger",
6
+ "version": "1.1.0",
7
+ "abi": "mod-abi:v1",
8
+ "description": "The Ledger: a context and budget meter (tokens in and out, cache share, cost, compactions, budget rungs and breakers), every number from the kernel's numbers-only feed.",
9
+ "provides": {
10
+ "views": [
11
+ { "id": "ledger.strip", "slot": "composer.footer", "placement": "append", "fixtures": ["fixtures/strip.json", "fixtures/strip-reduced.json", "fixtures/over-budget.json", "fixtures/breaker.json"] },
12
+ { "id": "ledger.sheet", "slot": "sidebar", "placement": "append", "fixtures": ["fixtures/compacted.json"] }
13
+ ]
14
+ },
15
+ "requires": { "capabilities": ["log.read", "log.read:usage"], "egress": "none" }
16
+ }
@@ -0,0 +1,77 @@
1
+ // #ledger-mod (the context and budget meter; first-party, mod-abi:v1, no
2
+ // effects beyond `read`). The #workshop-mod stdio loop, verbatim but for its
3
+ // two views. Runs under #mod-host from its pinned snapshot. Self-contained on
4
+ // purpose: it imports nothing outside this folder, so its packageHash (pinned
5
+ // in the kernel's first-party allowlist) never depends on a build of the SDK.
6
+ // State comes ONLY from the host's projected kernel-event feed (`event`
7
+ // notifications, granted by `log.read`); a fixture render (host-initiated, no
8
+ // relayedFor) may replay `input.fixtureEvents` — a relayed render never can.
9
+ import { apply, emptyState, VIEWS } from './model.mjs';
10
+
11
+ const ABI = 'mod-abi:v1';
12
+ // Echoed at init; the host refuses any other id (a mod-ABI change needs a new Ledger build).
13
+ const MOD_ABI_ID = 'sha256:7aaf92f46dc35fc35141557c1f89088958f220e6bf53ea31a630bb06430ba082';
14
+ const MAX_FIXTURE_EVENTS = 1024;
15
+
16
+ const live = emptyState();
17
+ const send = (msg) => process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', ...msg })}\n`);
18
+
19
+ function handle(method, p) {
20
+ switch (method) {
21
+ case 'init':
22
+ if (p.abi !== ABI) throw Object.assign(new Error(`unsupported abi ${String(p.abi)}`), { code: -32602 });
23
+ return { abi: ABI, features: [], modAbiId: MOD_ABI_ID };
24
+ case 'health':
25
+ return { ok: true, abi: ABI };
26
+ case 'shutdown':
27
+ setImmediate(() => process.exit(0));
28
+ return {};
29
+ case 'view.render': {
30
+ const render = Object.hasOwn(VIEWS, p.viewId) ? VIEWS[p.viewId] : null;
31
+ if (!render) throw Object.assign(new Error(`no view ${String(p.viewId)}`), { code: -32601 });
32
+ const prefs = p.prefs && typeof p.prefs === 'object' ? p.prefs : {};
33
+ const fx = !p.relayedFor && p.input && Array.isArray(p.input.fixtureEvents) ? p.input.fixtureEvents.slice(0, MAX_FIXTURE_EVENTS) : null;
34
+ let state = live;
35
+ if (fx) {
36
+ state = emptyState();
37
+ for (const e of fx) apply(state, e);
38
+ }
39
+ // a relayed input may name the focused session (display only); it never carries events
40
+ const input = p.input && typeof p.input === 'object' && typeof p.input.hsid === 'string' ? { hsid: p.input.hsid } : {};
41
+ return { block: render(state, prefs, input) };
42
+ }
43
+ default:
44
+ throw Object.assign(new Error(`unknown method ${method}`), { code: -32601 });
45
+ }
46
+ }
47
+
48
+ let buf = '';
49
+ process.stdin.setEncoding('utf8');
50
+ process.stdin.on('data', (chunk) => {
51
+ buf += chunk;
52
+ let nl;
53
+ while ((nl = buf.indexOf('\n')) >= 0) {
54
+ const line = buf.slice(0, nl).trim();
55
+ buf = buf.slice(nl + 1);
56
+ if (!line) continue;
57
+ let msg;
58
+ try {
59
+ msg = JSON.parse(line);
60
+ } catch {
61
+ continue;
62
+ }
63
+ if (typeof msg.method !== 'string') continue; // the Ledger makes no mod → host requests
64
+ const params = msg.params && typeof msg.params === 'object' ? msg.params : {};
65
+ if (msg.method === 'event') {
66
+ apply(live, params.event);
67
+ continue;
68
+ }
69
+ if (msg.method === 'cancel' || msg.id === undefined) continue;
70
+ try {
71
+ send({ id: msg.id, result: handle(msg.method, params) });
72
+ } catch (e) {
73
+ send({ id: msg.id, error: { code: typeof e.code === 'number' ? e.code : -32000, message: String(e.message) } });
74
+ }
75
+ }
76
+ });
77
+ process.stdin.on('end', () => process.exit(0));
@@ -0,0 +1,237 @@
1
+ // #ledger-mod model: the context and budget meter. A pure reducer over the
2
+ // host's NUMBERS-ONLY kernel-event feed (#mod-host projection v2) and a pure
3
+ // render to display-block:v1 (kit:v1 blocks only: Box, Text, Table, Progress).
4
+ // Inputs: turn.usage/turn.result {hsid, tokens, tokensIn?, tokensOut?,
5
+ // cacheRead?, cacheWrite?, costMicros?}, session.compacted {hsid,
6
+ // tokensBefore, tokensAfter}, session.open {hsid, backend, project},
7
+ // session.exited, budget.usage {dimension, n, of?, over?}, budget.ladder
8
+ // {dimension, scope, rung}, budget.breaker.open/close {dimension},
9
+ // permit.deny {dimension}. Unknown kinds and malformed fields are ignored;
10
+ // every number is re-clamped here (a mod never trusts a count).
11
+
12
+ export const MAX_SESSIONS = 256;
13
+ export const MAX_DIMENSIONS = 32;
14
+ export const HUES = 8;
15
+ export const WARN_RUNG = 75;
16
+ const ID = /^[A-Za-z0-9:._-]{1,128}$/;
17
+ const TOKEN = /^[a-z][a-z0-9-]{0,31}$/;
18
+
19
+ /** A finite non-negative safe integer (anything else is 0). */
20
+ export function count(v) {
21
+ if (typeof v !== 'number' || !Number.isFinite(v) || v <= 0) return 0;
22
+ return Math.min(Math.floor(v), Number.MAX_SAFE_INTEGER);
23
+ }
24
+ const token = (v, d = 'unknown') => (typeof v === 'string' && TOKEN.test(v) ? v : d);
25
+ const isNum = (v) => typeof v === 'number' && Number.isFinite(v);
26
+
27
+ export function emptyState() {
28
+ return { sessions: new Map(), permit: null, ladder: new Map(), breakers: new Set(), denies: new Map() };
29
+ }
30
+
31
+ function session(state, hsid) {
32
+ let s = state.sessions.get(hsid);
33
+ if (s) return s;
34
+ if (state.sessions.size >= MAX_SESSIONS) state.sessions.delete(state.sessions.keys().next().value);
35
+ s = { backend: 'claude', project: 0, open: true, tokens: 0, tokensIn: 0, tokensOut: 0, cacheRead: 0, cacheWrite: 0, split: false, costMicros: 0, cost: false, compactions: 0, lastBefore: 0, lastAfter: 0 };
36
+ state.sessions.set(hsid, s);
37
+ return s;
38
+ }
39
+
40
+ const boundedSet = (m, k, v) => {
41
+ if (!m.has(k) && m.size >= MAX_DIMENSIONS) return;
42
+ m.set(k, v);
43
+ };
44
+
45
+ /** Apply one projected kernel event. */
46
+ export function apply(state, event) {
47
+ if (!event || typeof event !== 'object' || typeof event.kind !== 'string') return state;
48
+ const p = event.payload && typeof event.payload === 'object' ? event.payload : {};
49
+ const hsid = typeof p.hsid === 'string' && ID.test(p.hsid) ? p.hsid : null;
50
+ switch (event.kind) {
51
+ case 'session.open': {
52
+ if (!hsid) break;
53
+ const s = session(state, hsid);
54
+ s.backend = p.backend === 'local' ? 'local' : 'claude';
55
+ s.project = Number.isInteger(p.project) && p.project >= 0 && p.project < HUES ? p.project : 0;
56
+ s.open = true;
57
+ break;
58
+ }
59
+ case 'session.exited': {
60
+ const s = hsid && state.sessions.get(hsid);
61
+ if (s) s.open = false;
62
+ break;
63
+ }
64
+ case 'turn.usage':
65
+ case 'turn.result': {
66
+ if (!hsid) break;
67
+ const s = session(state, hsid);
68
+ s.tokens += count(p.tokens);
69
+ if (event.kind === 'turn.result') {
70
+ for (const k of ['tokensIn', 'tokensOut', 'cacheRead', 'cacheWrite']) {
71
+ if (isNum(p[k])) {
72
+ s[k] += count(p[k]);
73
+ s.split = true;
74
+ }
75
+ }
76
+ if (isNum(p.costMicros)) {
77
+ s.costMicros += count(p.costMicros);
78
+ s.cost = true;
79
+ }
80
+ }
81
+ break;
82
+ }
83
+ case 'session.compacted': {
84
+ if (!hsid) break;
85
+ const s = session(state, hsid);
86
+ s.compactions++;
87
+ s.lastBefore = count(p.tokensBefore);
88
+ s.lastAfter = count(p.tokensAfter);
89
+ break;
90
+ }
91
+ case 'budget.usage':
92
+ if (p.over === true) state.permit = { dimension: token(p.dimension, 'tokens'), n: count(p.n), of: count(p.of) };
93
+ break;
94
+ case 'budget.ladder': {
95
+ const key = `${token(p.dimension)}|${token(p.scope, 'session')}`;
96
+ const rung = Math.min(count(p.rung), 1000);
97
+ if (rung > (state.ladder.get(key) ?? -1)) boundedSet(state.ladder, key, rung);
98
+ break;
99
+ }
100
+ case 'budget.breaker.open':
101
+ if (state.breakers.size < MAX_DIMENSIONS) state.breakers.add(token(p.dimension));
102
+ break;
103
+ case 'budget.breaker.close':
104
+ state.breakers.delete(token(p.dimension));
105
+ break;
106
+ case 'permit.deny': {
107
+ const d = token(p.dimension);
108
+ boundedSet(state.denies, d, (state.denies.get(d) ?? 0) + 1);
109
+ break;
110
+ }
111
+ default:
112
+ break;
113
+ }
114
+ return state;
115
+ }
116
+
117
+ /** 182000 → 182k, 9400 → 9k, 1250000 → 1.3M, 950 → 950. */
118
+ export function short(n) {
119
+ if (n < 1000) return String(n);
120
+ if (n < 1_000_000) return `${Math.round(n / 1000)}k`;
121
+ return `${(Math.round(n / 100_000) / 10).toFixed(1)}M`;
122
+ }
123
+ const dollars = (micros) => `$${(micros / 1e6).toFixed(2)}`;
124
+
125
+ /** The totals of one session, or of every session when hsid is null. */
126
+ export function totals(state, hsid) {
127
+ const list = hsid ? [state.sessions.get(hsid)].filter(Boolean) : [...state.sessions.values()];
128
+ const t = { tokens: 0, tokensIn: 0, tokensOut: 0, cacheRead: 0, cacheWrite: 0, split: false, unsplit: 0, costMicros: 0, cost: false, compactions: 0, lastBefore: 0, lastAfter: 0 };
129
+ for (const s of list) {
130
+ for (const k of ['tokens', 'tokensIn', 'tokensOut', 'cacheRead', 'cacheWrite', 'costMicros', 'compactions']) t[k] = Math.min(t[k] + s[k], Number.MAX_SAFE_INTEGER);
131
+ t.split ||= s.split;
132
+ if (!s.split) t.unsplit = Math.min(t.unsplit + s.tokens, Number.MAX_SAFE_INTEGER);
133
+ t.cost ||= s.cost;
134
+ if (s.compactions) {
135
+ t.lastBefore = s.lastBefore;
136
+ t.lastAfter = s.lastAfter;
137
+ }
138
+ }
139
+ return t;
140
+ }
141
+
142
+ /** `182k in · 9k out · 71% cache · $0.42 · ⇣ 2 compactions (190k → 41k)`; `in` counts every input token (fresh + cache). */
143
+ export function summaryLine(t) {
144
+ const parts = [];
145
+ if (t.split) {
146
+ const input = t.tokensIn + t.cacheRead + t.cacheWrite;
147
+ parts.push(`${short(input)} in`, `${short(t.tokensOut)} out`, `${input ? Math.round((t.cacheRead / input) * 100) : 0}% cache`);
148
+ if (t.unsplit) parts.push(`${short(t.unsplit)} local`);
149
+ } else parts.push(`${short(t.tokens)} tokens`);
150
+ if (t.cost) parts.push(dollars(t.costMicros));
151
+ if (t.compactions) parts.push(`⇣ ${t.compactions} compaction${t.compactions === 1 ? '' : 's'} (${short(t.lastBefore)} → ${short(t.lastAfter)})`);
152
+ return parts.join(' · ');
153
+ }
154
+
155
+ /** The worst budget signal: deny (vermilion) > warn (amber) > none. */
156
+ export function budgetTone(state) {
157
+ if (state.breakers.size || state.denies.size) return 'deny';
158
+ if (state.permit) return 'warn';
159
+ for (const r of state.ladder.values()) if (r >= WARN_RUNG) return 'warn';
160
+ return 'quiet';
161
+ }
162
+
163
+ /** The budget meter: the over-permit row (n of m), else the highest ladder rung (percent of the daily cap). */
164
+ function meter(state, reduced) {
165
+ let m = null;
166
+ if (state.permit && state.permit.of > 0) {
167
+ const fmt = state.permit.dimension === 'usd' ? dollars : short;
168
+ m = { value: state.permit.n, max: state.permit.of, label: `${state.permit.dimension} ${fmt(state.permit.n)} of ${fmt(state.permit.of)} (permit)` };
169
+ } else {
170
+ let best = null;
171
+ for (const [k, r] of [...state.ladder.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1))) if (!best || r > best[1]) best = [k, r];
172
+ if (best) {
173
+ const [dim, scope] = best[0].split('|');
174
+ m = { value: best[1], max: 100, label: `${dim} ${best[1]} of 100% (${scope} daily cap)` };
175
+ }
176
+ }
177
+ if (!m) return [];
178
+ // reduced motion: no fill element at all (nothing can animate), the same numbers as text
179
+ return reduced ? [{ type: 'Text', text: `▮ ${m.label}`, tone: budgetTone(state) }] : [{ type: 'Progress', value: Math.min(m.value, m.max), max: m.max, label: m.label }];
180
+ }
181
+
182
+ function alarms(state) {
183
+ const out = [];
184
+ for (const d of [...state.breakers].sort()) out.push({ type: 'Text', text: `✕ breaker open · ${d}`, tone: 'deny' });
185
+ for (const [d, n] of [...state.denies.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1))) out.push({ type: 'Text', text: `✕ permit denied · ${d}${n > 1 ? ` ×${n}` : ''}`, tone: 'deny' });
186
+ return out;
187
+ }
188
+
189
+ /** The strip (composer.footer): the cap carries the session's project hue (never as text). */
190
+ export function renderStrip(state, prefs = {}, input = {}) {
191
+ const reduced = prefs.motion === 'reduced';
192
+ const hsid = input && typeof input.hsid === 'string' && ID.test(input.hsid) && state.sessions.has(input.hsid) ? input.hsid : null;
193
+ const s = hsid ? state.sessions.get(hsid) : null;
194
+ const t = totals(state, hsid);
195
+ const tone = budgetTone(state);
196
+ const cap = s ? { type: 'Text', text: s.backend === 'local' ? '○' : '●', tone: `hue-${s.project}` } : { type: 'Text', text: '◌', tone: 'quiet' };
197
+ return {
198
+ schemaVersion: 'display-block:v1',
199
+ type: 'Box',
200
+ title: hsid ? 'Ledger · this session' : `Ledger · ${state.sessions.size} session${state.sessions.size === 1 ? '' : 's'}`,
201
+ direction: 'row',
202
+ children: [cap, { type: 'Text', text: state.sessions.size ? summaryLine(t) : 'no usage yet', tone: tone === 'deny' ? 'deny' : tone === 'warn' ? 'warn' : 'quiet' }, ...meter(state, reduced), ...alarms(state)],
203
+ };
204
+ }
205
+
206
+ /** The sheet (sidebar): one row per session, then the budget meter and alarms. */
207
+ export function renderSheet(state, prefs = {}) {
208
+ const reduced = prefs.motion === 'reduced';
209
+ const rows = [...state.sessions.entries()]
210
+ .sort((a, b) => (a[0] < b[0] ? -1 : 1))
211
+ .map(([id, s]) => {
212
+ const input = s.tokensIn + s.cacheRead + s.cacheWrite;
213
+ return [
214
+ `…${id.slice(-6)}${s.open ? '' : ' (ended)'}`,
215
+ s.split ? short(input) : short(s.tokens),
216
+ s.split ? short(s.tokensOut) : '-',
217
+ s.split ? `${input ? Math.round((s.cacheRead / input) * 100) : 0}%` : '-',
218
+ s.cost ? dollars(s.costMicros) : '-',
219
+ s.compactions ? `${s.compactions} (${short(s.lastBefore)} → ${short(s.lastAfter)})` : '0',
220
+ ];
221
+ });
222
+ const t = totals(state, null);
223
+ return {
224
+ schemaVersion: 'display-block:v1',
225
+ type: 'Box',
226
+ title: 'Ledger',
227
+ direction: 'column',
228
+ children: [
229
+ rows.length ? { type: 'Table', columns: ['session', 'in', 'out', 'cache', 'cost', '⇣ compactions'], rows } : { type: 'Text', text: 'no sessions metered yet', tone: 'quiet' },
230
+ ...(rows.length ? [{ type: 'Text', text: `total · ${summaryLine(t)}`, tone: 'quiet' }] : []),
231
+ ...meter(state, reduced),
232
+ ...alarms(state),
233
+ ],
234
+ };
235
+ }
236
+
237
+ export const VIEWS = Object.freeze({ 'ledger.strip': renderStrip, 'ledger.sheet': renderSheet });
@@ -0,0 +1,31 @@
1
+ # gnomon.neurons (first-party Gnomon mod)
2
+
3
+ Neurons puts the user's personal knowledge brain on the Desk. It has no view and no process of its own: it declares the Neurons MCP server, three offers and a playbook skill.
4
+
5
+ - **search-the-brain** (diverge, `neuron_search`): check what is already known before researching a topic from scratch.
6
+ - **capture-a-learning** (synthesize, `source_note`): save something verified first-hand in this session as a citable source note.
7
+ - **capture-a-decision** (commit, `neuron_write`): write a decision with its reasons as neurons that cite a source.
8
+
9
+ The mod id is `gnomon.neurons` (manifest: 2, publisher `gnomon`, slice S0b), which is what the model writes in an offer block. Before S0b it was `neurons-brain`, because the bare name `neurons` is a kernel namespace token (the §9.1 `neurons.event` kind); under a publisher its events live in `modns.gnomon.neurons.*`, so the first-party name is allowed for publisher `gnomon` only. The kernel names its MCP server `gm_gnomon_neurons` (manifest: 2 entries carry no name). `NEURONS_VAULT` is the upstream server's own variable, so it keeps its name (first-party env names are exempt from the `<PUB>__<NAME>__` own-prefix).
10
+
11
+ Each offer's restraint text says when not to offer it. `skills/neurons-desk/SKILL.md` is the playbook. Outside Gnomon it falls back to plain one-sentence suggestions.
12
+
13
+ It requires no capability and lists no egress. The MCP server reads and writes the vault on the user's own machine.
14
+
15
+ ## Install
16
+
17
+ ```
18
+ gnomon mod install packages/gnomon-mods/neurons
19
+ ```
20
+
21
+ Approve the card in the GUI. The manifest's default launch, `neurons mcp`, is generic and is not an absolute path, so the server fails closed (it is never launched) until you set a per-user override. The manifest declares `config: {command, args, env: [NEURONS_VAULT]}`. Set the override with `gnomon mod config` (slice S1):
22
+
23
+ - the command: the absolute path of your node (`which node`)
24
+ - the args: `<your a-neurons checkout>/dist/cli/main.js mcp`
25
+ - `NEURONS_VAULT`: your vault folder
26
+
27
+ An override command must be an absolute path; a bare `node` is refused. The override belongs to you and is never committed.
28
+
29
+ Before the model saves anything with `source_note` or `neuron_write`, the playbook has it show you the exact text and wait for you to confirm or edit it. It also flags any part that came from fetched pages, tool output or unverified files.
30
+
31
+ The packageHash is pinned in the kernel's first-party list, so editing any file here needs a new pin: run `npm run repin:first-party` in packages/gnomon (it also regenerates the offer goldens in `fixtures/`).
@@ -0,0 +1,20 @@
1
+ {
2
+ "offer": {
3
+ "mod": "gnomon.neurons",
4
+ "kind": "capture-a-decision",
5
+ "question": "Want me to save this decision and its reasons to the brain?"
6
+ },
7
+ "body": "So we go with SQLite over Postgres for the local store: one file, no daemon, and the write volume fits.",
8
+ "shown": {
9
+ "mod": "gnomon.neurons",
10
+ "kind": "capture-a-decision",
11
+ "phase": "commit",
12
+ "title": "Save this decision to the brain",
13
+ "contextIn": [
14
+ "recent-messages",
15
+ "session-summary"
16
+ ],
17
+ "question": "Want me to save this decision and its reasons to the brain?",
18
+ "handoffTool": "neuron_write"
19
+ }
20
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "reply": "So we go with SQLite over Postgres for the local store: one file, no daemon, and the write volume fits.\n\n```offer\n{\"mod\":\"gnomon.neurons\",\"kind\":\"capture-a-decision\",\"question\":\"Want me to save this decision and its reasons to the brain?\"}\n```\n"
3
+ }