@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,205 @@
1
+ {
2
+ "schemaVersion": "display-block:v1",
3
+ "type": "Box",
4
+ "title": "Paradigm",
5
+ "direction": "column",
6
+ "children": [
7
+ {
8
+ "type": "Box",
9
+ "direction": "row",
10
+ "children": [
11
+ {
12
+ "type": "Text",
13
+ "text": "●",
14
+ "tone": "hue-0"
15
+ },
16
+ {
17
+ "type": "Text",
18
+ "text": "ledgerette"
19
+ }
20
+ ]
21
+ },
22
+ {
23
+ "type": "Text",
24
+ "text": "stale · last entry 2026-08-30 (51 days ago)",
25
+ "tone": "warn"
26
+ },
27
+ {
28
+ "type": "Box",
29
+ "direction": "column",
30
+ "children": [
31
+ {
32
+ "type": "Box",
33
+ "direction": "row",
34
+ "children": [
35
+ {
36
+ "type": "Text",
37
+ "text": "tasks",
38
+ "tone": "head"
39
+ },
40
+ {
41
+ "type": "Text",
42
+ "text": "1 of 1 open",
43
+ "tone": "quiet"
44
+ }
45
+ ]
46
+ },
47
+ {
48
+ "type": "Box",
49
+ "direction": "row",
50
+ "children": [
51
+ {
52
+ "type": "Text",
53
+ "text": "task",
54
+ "tone": "quiet"
55
+ },
56
+ {
57
+ "type": "Text",
58
+ "text": "T-2026-08-12-002",
59
+ "tone": "quiet"
60
+ },
61
+ {
62
+ "type": "Text",
63
+ "text": "“Typecheck packages/ledger after the Cents migration”",
64
+ "tone": "quiet"
65
+ }
66
+ ]
67
+ }
68
+ ]
69
+ },
70
+ {
71
+ "type": "Box",
72
+ "direction": "column",
73
+ "children": [
74
+ {
75
+ "type": "Box",
76
+ "direction": "row",
77
+ "children": [
78
+ {
79
+ "type": "Text",
80
+ "text": "decisions",
81
+ "tone": "head"
82
+ },
83
+ {
84
+ "type": "Text",
85
+ "text": "1 of 3",
86
+ "tone": "quiet"
87
+ }
88
+ ]
89
+ },
90
+ {
91
+ "type": "Box",
92
+ "direction": "row",
93
+ "children": [
94
+ {
95
+ "type": "Text",
96
+ "text": "decision",
97
+ "tone": "quiet"
98
+ },
99
+ {
100
+ "type": "Text",
101
+ "text": "TD-2026-08-30-118",
102
+ "tone": "quiet"
103
+ },
104
+ {
105
+ "type": "Text",
106
+ "text": "2026-08-30",
107
+ "tone": "quiet"
108
+ },
109
+ {
110
+ "type": "Text",
111
+ "text": "“Money is integer cents end to end”",
112
+ "tone": "quiet"
113
+ }
114
+ ]
115
+ }
116
+ ]
117
+ },
118
+ {
119
+ "type": "Box",
120
+ "direction": "column",
121
+ "children": [
122
+ {
123
+ "type": "Box",
124
+ "direction": "row",
125
+ "children": [
126
+ {
127
+ "type": "Text",
128
+ "text": "lore",
129
+ "tone": "head"
130
+ },
131
+ {
132
+ "type": "Text",
133
+ "text": "1 of 4",
134
+ "tone": "quiet"
135
+ }
136
+ ]
137
+ },
138
+ {
139
+ "type": "Box",
140
+ "direction": "row",
141
+ "children": [
142
+ {
143
+ "type": "Text",
144
+ "text": "lore milestone",
145
+ "tone": "quiet"
146
+ },
147
+ {
148
+ "type": "Text",
149
+ "text": "L-2026-08-29-matt-101500-001",
150
+ "tone": "quiet"
151
+ },
152
+ {
153
+ "type": "Text",
154
+ "text": "“Ledger import lands”",
155
+ "tone": "quiet"
156
+ }
157
+ ]
158
+ }
159
+ ]
160
+ },
161
+ {
162
+ "type": "Box",
163
+ "direction": "column",
164
+ "children": [
165
+ {
166
+ "type": "Box",
167
+ "direction": "row",
168
+ "children": [
169
+ {
170
+ "type": "Text",
171
+ "text": "symbols",
172
+ "tone": "head"
173
+ },
174
+ {
175
+ "type": "Text",
176
+ "text": "2",
177
+ "tone": "quiet"
178
+ }
179
+ ]
180
+ },
181
+ {
182
+ "type": "Box",
183
+ "direction": "row",
184
+ "children": [
185
+ {
186
+ "type": "Text",
187
+ "text": "#ledger-post",
188
+ "tone": "chip"
189
+ },
190
+ {
191
+ "type": "Text",
192
+ "text": "$import-flow",
193
+ "tone": "chip"
194
+ }
195
+ ]
196
+ }
197
+ ]
198
+ },
199
+ {
200
+ "type": "Text",
201
+ "text": "skipped · 1 refused · 2 secret",
202
+ "tone": "quiet"
203
+ }
204
+ ]
205
+ }
@@ -0,0 +1,69 @@
1
+ {
2
+ "view": "paradigm-context.sheet",
3
+ "input": {
4
+ "paradigm": {
5
+ "v": "paradigm-slice:v1",
6
+ "state": "ok",
7
+ "project": 0,
8
+ "name": "ledgerette",
9
+ "at": "2026-10-20",
10
+ "newest": "2026-08-30",
11
+ "tasks": {
12
+ "open": 1,
13
+ "high": 0,
14
+ "items": [
15
+ {
16
+ "id": "T-2026-08-12-002",
17
+ "title": "Typecheck packages/ledger after the Cents migration",
18
+ "priority": "medium"
19
+ }
20
+ ],
21
+ "count": {
22
+ "n": 1,
23
+ "of": 1
24
+ }
25
+ },
26
+ "decisions": {
27
+ "items": [
28
+ {
29
+ "id": "TD-2026-08-30-118",
30
+ "title": "Money is integer cents end to end",
31
+ "date": "2026-08-30"
32
+ }
33
+ ],
34
+ "count": {
35
+ "n": 1,
36
+ "of": 3
37
+ }
38
+ },
39
+ "lore": {
40
+ "items": [
41
+ {
42
+ "id": "L-2026-08-29-matt-101500-001",
43
+ "title": "Ledger import lands",
44
+ "type": "milestone"
45
+ }
46
+ ],
47
+ "count": {
48
+ "n": 1,
49
+ "of": 4
50
+ }
51
+ },
52
+ "symbols": [
53
+ "#ledger-post",
54
+ "$import-flow"
55
+ ],
56
+ "skipped": {
57
+ "malformed": 0,
58
+ "refused": 1,
59
+ "secret": 2,
60
+ "truncated": false
61
+ }
62
+ }
63
+ },
64
+ "width": 40,
65
+ "prefs": {
66
+ "motion": "full",
67
+ "theme": "day"
68
+ }
69
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "schemaVersion": "display-block:v1",
3
+ "type": "Box",
4
+ "title": "Paradigm",
5
+ "direction": "column",
6
+ "children": [
7
+ {
8
+ "type": "Box",
9
+ "direction": "row",
10
+ "children": [
11
+ {
12
+ "type": "Text",
13
+ "text": "◌",
14
+ "tone": "quiet"
15
+ },
16
+ {
17
+ "type": "Text",
18
+ "text": "paradigm"
19
+ }
20
+ ]
21
+ },
22
+ {
23
+ "type": "Text",
24
+ "text": "focus a live session to see its project context",
25
+ "tone": "quiet"
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "view": "paradigm-context.sheet",
3
+ "input": {
4
+ "paradigm": {
5
+ "v": "paradigm-slice:v1",
6
+ "state": "unfocused"
7
+ }
8
+ },
9
+ "width": 40,
10
+ "prefs": {
11
+ "motion": "full",
12
+ "theme": "day"
13
+ }
14
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "manifest": 2,
3
+ "kind": "mod",
4
+ "publisher": "gnomon",
5
+ "name": "paradigm-context",
6
+ "version": "1.1.1",
7
+ "abi": "mod-abi:v1",
8
+ "description": "Paradigm Context: the focused session's project knowledge (open tasks, recent decisions, recent lore and the symbols they touch), read-only, from the kernel's bounded paradigm-slice.",
9
+ "provides": {
10
+ "views": [
11
+ { "id": "paradigm-context.sheet", "slot": "sidebar", "placement": "append", "fixtures": ["fixtures/empty.json", "fixtures/full.json", "fixtures/full-dark.json", "fixtures/stale.json", "fixtures/unfocused.json"] }
12
+ ]
13
+ },
14
+ "requires": { "capabilities": ["paradigm.read:summary"], "egress": "none" }
15
+ }
@@ -0,0 +1,64 @@
1
+ // #paradigm-context-mod (Paradigm Context; first-party, mod-abi:v1, no effects
2
+ // beyond `read`, egress none). The #ledger-mod stdio loop, minus the feed:
3
+ // this mod requires paradigm.read:summary only, so it gets no kernel events.
4
+ // Its ONLY input is the host-composed view input: `input.paradigm`, the K1
5
+ // paradigm-slice:v1 the KERNEL computed for the focused LIVE session's repo
6
+ // (the host joins {focus:{hsid}} server-side, K3). It reads no file and
7
+ // never sees a path. Self-contained on purpose: it imports nothing outside
8
+ // this folder, so its packageHash (pinned in the kernel's first-party
9
+ // allowlist) never depends on a build of the SDK.
10
+ import { VIEWS } from './model.mjs';
11
+
12
+ const ABI = 'mod-abi:v1';
13
+ // Echoed at init; the host refuses any other id (a mod-ABI change needs a new build).
14
+ const MOD_ABI_ID = 'sha256:7aaf92f46dc35fc35141557c1f89088958f220e6bf53ea31a630bb06430ba082';
15
+
16
+ const send = (msg) => process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', ...msg })}\n`);
17
+
18
+ function handle(method, p) {
19
+ switch (method) {
20
+ case 'init':
21
+ if (p.abi !== ABI) throw Object.assign(new Error(`unsupported abi ${String(p.abi)}`), { code: -32602 });
22
+ return { abi: ABI, features: [], modAbiId: MOD_ABI_ID };
23
+ case 'health':
24
+ return { ok: true, abi: ABI };
25
+ case 'shutdown':
26
+ setImmediate(() => process.exit(0));
27
+ return {};
28
+ case 'view.render': {
29
+ const render = Object.hasOwn(VIEWS, p.viewId) ? VIEWS[p.viewId] : null;
30
+ if (!render) throw Object.assign(new Error(`no view ${String(p.viewId)}`), { code: -32601 });
31
+ const prefs = p.prefs && typeof p.prefs === 'object' ? p.prefs : {};
32
+ return { block: render(p.input && typeof p.input === 'object' ? p.input : {}, prefs) };
33
+ }
34
+ default:
35
+ throw Object.assign(new Error(`unknown method ${method}`), { code: -32601 });
36
+ }
37
+ }
38
+
39
+ let buf = '';
40
+ process.stdin.setEncoding('utf8');
41
+ process.stdin.on('data', (chunk) => {
42
+ buf += chunk;
43
+ let nl;
44
+ while ((nl = buf.indexOf('\n')) >= 0) {
45
+ const line = buf.slice(0, nl).trim();
46
+ buf = buf.slice(nl + 1);
47
+ if (!line) continue;
48
+ let msg;
49
+ try {
50
+ msg = JSON.parse(line);
51
+ } catch {
52
+ continue;
53
+ }
54
+ if (typeof msg.method !== 'string') continue; // the mod makes no mod → host requests
55
+ if (msg.method === 'event' || msg.method === 'cancel' || msg.id === undefined) continue;
56
+ const params = msg.params && typeof msg.params === 'object' ? msg.params : {};
57
+ try {
58
+ send({ id: msg.id, result: handle(msg.method, params) });
59
+ } catch (e) {
60
+ send({ id: msg.id, error: { code: typeof e.code === 'number' ? e.code : -32000, message: String(e.message) } });
61
+ }
62
+ }
63
+ });
64
+ process.stdin.on('end', () => process.exit(0));
@@ -0,0 +1,137 @@
1
+ // #paradigm-context-mod model: a PURE render of the kernel's paradigm-slice:v1
2
+ // (K1, host-composed view input; granted by paradigm.read:summary) to
3
+ // display-block:v1 (kit:v1 only). Read-only: no Button, no command, no
4
+ // effect. Nothing here reads a file or a path; the slice is the only input
5
+ // and it is re-validated here (a mod never trusts a count or a string).
6
+
7
+ export const SLICE_V1 = 'paradigm-slice:v1';
8
+ export const HUES = 8;
9
+ export const TOP = 5;
10
+ export const TITLE_MAX = 80;
11
+ export const MAX_SYMBOLS = 24;
12
+ /** A slice whose newest decision or lore entry is older than this (days, against its own `at`) is shown stale. */
13
+ export const STALE_DAYS = 14;
14
+
15
+ const ID = /^[A-Za-z0-9._-]{1,64}$/;
16
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
17
+ const TOKEN = /^[a-z][a-z0-9-]{0,31}$/;
18
+ const SYMBOL = /^[#$^!~][a-z0-9][a-z0-9-]{0,63}$/;
19
+ const PRIORITIES = ['high', 'medium', 'low'];
20
+
21
+ /** A finite non-negative safe integer (anything else is 0). */
22
+ export function count(v) {
23
+ if (typeof v !== 'number' || !Number.isFinite(v) || v <= 0) return 0;
24
+ return Math.min(Math.floor(v), Number.MAX_SAFE_INTEGER);
25
+ }
26
+
27
+ /** One display line: no newline or tab, capped at `max` code points (… when cut). */
28
+ export function clip(s, max = TITLE_MAX) {
29
+ const one = String(typeof s === 'string' ? s : '').replace(/[\n\t\r]+/g, ' ').replace(/ {2,}/g, ' ').trim();
30
+ const cps = [...one];
31
+ return cps.length > max ? `${cps.slice(0, max - 1).join('').trimEnd()}…` : one;
32
+ }
33
+
34
+ const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
35
+ const arr = (v) => (Array.isArray(v) ? v : []);
36
+ const items = (zone, extra) =>
37
+ arr(obj(zone).items)
38
+ .filter((it) => it && typeof it === 'object' && typeof it.id === 'string' && ID.test(it.id) && typeof it.title === 'string')
39
+ .slice(0, TOP)
40
+ .map((it) => ({ id: it.id, title: clip(it.title), ...extra(it) }));
41
+
42
+ /** The slice, re-validated and bounded; null when it is not a paradigm-slice:v1. */
43
+ export function readSlice(input) {
44
+ const s = obj(obj(input).paradigm);
45
+ if (s.v !== SLICE_V1) return null;
46
+ const state = s.state === 'ok' || s.state === 'absent' ? s.state : 'unfocused';
47
+ const project = Number.isInteger(s.project) && s.project >= 0 && s.project < HUES ? s.project : null;
48
+ const name = typeof s.name === 'string' ? clip(s.name, 64) : '';
49
+ const at = typeof s.at === 'string' && DATE.test(s.at) ? s.at : null;
50
+ const newest = typeof s.newest === 'string' && DATE.test(s.newest) ? s.newest : null;
51
+ const t = obj(s.tasks);
52
+ const d = obj(s.decisions);
53
+ const l = obj(s.lore);
54
+ const sk = obj(s.skipped);
55
+ const of = (z, n) => Math.max(count(obj(obj(z).count).of), n);
56
+ const tasks = items(t, (it) => ({ priority: PRIORITIES.includes(it.priority) ? it.priority : 'medium' }));
57
+ const decisions = items(d, (it) => ({ date: typeof it.date === 'string' && DATE.test(it.date) ? it.date : null }));
58
+ const lore = items(l, (it) => ({ type: typeof it.type === 'string' && TOKEN.test(it.type) ? it.type : 'note' }));
59
+ const open = Math.max(count(t.open), tasks.length);
60
+ return {
61
+ state,
62
+ project,
63
+ name,
64
+ at,
65
+ newest,
66
+ tasks: { open, high: Math.min(count(t.high), open), items: tasks, of: of(t, open) },
67
+ decisions: { items: decisions, of: of(d, decisions.length) },
68
+ lore: { items: lore, of: of(l, lore.length) },
69
+ symbols: arr(s.symbols).filter((x) => typeof x === 'string' && SYMBOL.test(x)).filter((x, i, a) => a.indexOf(x) === i).slice(0, MAX_SYMBOLS),
70
+ skipped: { malformed: count(sk.malformed), refused: count(sk.refused), secret: count(sk.secret), truncated: sk.truncated === true },
71
+ };
72
+ }
73
+
74
+ /** Days from `newest` to `at` (both YYYY-MM-DD), or null. */
75
+ export function ageDays(newest, at) {
76
+ if (!newest || !at) return null;
77
+ return Math.max(0, Math.round((Date.parse(`${at}T00:00:00Z`) - Date.parse(`${newest}T00:00:00Z`)) / 86_400_000));
78
+ }
79
+
80
+ const text = (t, tone) => ({ type: 'Text', text: t, ...(tone ? { tone } : {}) });
81
+ /** The {n, of} count as the zone title reads it. */
82
+ const nOf = (n, of) => `${n} of ${of}`;
83
+
84
+ function head(sl) {
85
+ // the project swatch: a hue-toned mark (the GUI draws the project's ■ in its hue); the name is ink
86
+ const cap = sl && sl.project !== null ? text('●', `hue-${sl.project}`) : text('◌', 'quiet');
87
+ return { type: 'Box', direction: 'row', children: [cap, text(sl && sl.name ? sl.name : 'paradigm')] };
88
+ }
89
+
90
+ /**
91
+ * Aegis L2: one item line. A FIXED type prefix (task / decision / lore) and the id come from the model; the
92
+ * repo-authored title follows QUOTED and dim, so a title can never pass for a status glyph, a kernel line or a
93
+ * card (any status mark, like the ▲ of a high task, is the model's own, in the prefix). Curly quotes inside the
94
+ * title are flattened so it cannot close its own quote.
95
+ */
96
+ function line(kind, id, title, date = null) {
97
+ const q = String(title).replace(/[\u201c\u201d]/g, '"');
98
+ // the id and its date are separate words, so the GUI can truncate the id on one line and never break it mid-token
99
+ return { type: 'Box', direction: 'row', children: [text(kind, 'quiet'), text(id, 'quiet'), ...(date ? [text(date, 'quiet')] : []), text(`\u201c${q}\u201d`, 'quiet')] };
100
+ }
101
+
102
+ // a zone heading is a row: the zone's name (tone head, the label style) and its {n, of} count as quiet meta beside it
103
+ function zone(title, meta, lines, empty) {
104
+ return { type: 'Box', direction: 'column', children: [{ type: 'Box', direction: 'row', children: [text(title, 'head'), text(meta, 'quiet')] }, ...(lines.length ? lines : [text(empty, 'quiet')])] };
105
+ }
106
+
107
+ /** The one quiet line of a project with no open tasks, decisions or lore. */
108
+ export const EMPTY_LINE = 'nothing yet: no tasks, decisions or lore';
109
+
110
+ /** The sheet (sidebar): the project swatch, then tasks, decisions and lore, each with its {n, of} count. */
111
+ export function renderSheet(input, prefs = {}) {
112
+ void prefs; // the theme and motion change nothing here: no fill animates, every tone is a token
113
+ const sl = readSlice(input);
114
+ const base = { schemaVersion: 'display-block:v1', type: 'Box', title: 'Paradigm', direction: 'column' };
115
+ if (!sl || sl.state === 'unfocused') return { ...base, children: [head(null), text('focus a live session to see its project context', 'quiet')] };
116
+ if (sl.state === 'absent') return { ...base, children: [head(sl), text('no .paradigm in this repo', 'quiet')] };
117
+ const age = ageDays(sl.newest, sl.at);
118
+ const children = [head(sl)];
119
+ const t = sl.tasks;
120
+ // an empty project says so once (Mika 2026-10-23), not once per zone; each zone's own empty line shows only beside a zone with items
121
+ const nothing = !t.items.length && !sl.decisions.items.length && !sl.lore.items.length;
122
+ if (nothing) children.push(text(EMPTY_LINE, 'quiet'));
123
+ else if (sl.newest !== null && age !== null && age > STALE_DAYS) children.push(text(`stale · last entry ${sl.newest} (${age} days ago)`, 'warn'));
124
+ if (!nothing) children.push(
125
+ zone('tasks', `${nOf(t.items.length, t.of)} open${t.high ? ` · ${t.high} high` : ''}`, t.items.map((it) => line(`task${it.priority === 'high' ? ' ▲' : ''}`, it.id, it.title)), 'no open tasks'),
126
+ zone('decisions', nOf(sl.decisions.items.length, sl.decisions.of), sl.decisions.items.map((it) => line('decision', it.id, it.title, it.date)), 'no decisions'),
127
+ zone('lore', nOf(sl.lore.items.length, sl.lore.of), sl.lore.items.map((it) => line(`lore ${it.type}`, it.id, it.title)), 'no lore'),
128
+ );
129
+ // symbol chips: mono ink text (tone chip), never a hue
130
+ if (sl.symbols.length) children.push({ type: 'Box', direction: 'column', children: [{ type: 'Box', direction: 'row', children: [text('symbols', 'head'), text(String(sl.symbols.length), 'quiet')] }, { type: 'Box', direction: 'row', children: sl.symbols.map((s) => text(s, 'chip')) }] });
131
+ const sk = sl.skipped;
132
+ const notes = [sk.malformed ? `${sk.malformed} malformed` : '', sk.refused ? `${sk.refused} refused` : '', sk.secret ? `${sk.secret} secret` : '', sk.truncated ? 'truncated' : ''].filter(Boolean);
133
+ if (notes.length) children.push(text(`skipped · ${notes.join(' · ')}`, 'quiet'));
134
+ return { ...base, children };
135
+ }
136
+
137
+ export const VIEWS = Object.freeze({ 'paradigm-context.sheet': renderSheet });
@@ -0,0 +1,12 @@
1
+ # workshop (first-party Gnomon mod)
2
+
3
+ The Workshop view (spec §11.6): a sidebar strip with two zones.
4
+
5
+ - **needs you**: one cap per pending card, labelled with the card's kind.
6
+ - **working**: one cap per open session. A cap bobs only while its session streams (`turn.partial`) and stands still otherwise. With reduced motion it shows the word `working` instead.
7
+
8
+ Each cap is a kit `Text` node: its `text` is the accessible form, and its `cap` is the closed shape `{shape, hue, filled, state, label}` (gnomon-contracts `checkCap`). The kernel's mod-block gate refuses any other cap field, and the GUI draws the hat from these five fields alone. Where the strip shows (a footer strip, a left or right column, or hidden) is the GUI Layout's choice; the manifest slot stays `sidebar`.
9
+
10
+ Every attribute comes from kernel events that the host projects to the mod (`#mod-host` feed, granted by `log.read`). Nothing comes from model text. The hat shape shows the work type, a filled hat means Claude and an outlined hat means local, the hue shows the project, and violet is used only for a KNOT.
11
+
12
+ Install it with `gnomon mod install packages/gnomon-mods/workshop`, then approve the card in the GUI. Its packageHash is pinned in the kernel's first-party list, so editing any file here needs a new pin.
@@ -0,0 +1,56 @@
1
+ {
2
+ "schemaVersion": "display-block:v1",
3
+ "type": "Box",
4
+ "title": "Workshop",
5
+ "direction": "column",
6
+ "children": [
7
+ {
8
+ "type": "Box",
9
+ "title": "needs you 1",
10
+ "children": [
11
+ {
12
+ "type": "Text",
13
+ "text": "▲ permission",
14
+ "tone": "cap",
15
+ "cap": {
16
+ "shape": "tall",
17
+ "hue": 2,
18
+ "filled": true,
19
+ "state": "needs",
20
+ "label": "? permission"
21
+ }
22
+ }
23
+ ]
24
+ },
25
+ {
26
+ "type": "Box",
27
+ "title": "working 1 of 2",
28
+ "children": [
29
+ {
30
+ "type": "Text",
31
+ "text": "▲ builder filled working",
32
+ "tone": "hue-2",
33
+ "cap": {
34
+ "shape": "tall",
35
+ "hue": 2,
36
+ "filled": true,
37
+ "state": "working",
38
+ "label": "builder"
39
+ }
40
+ },
41
+ {
42
+ "type": "Text",
43
+ "text": "▲ builder outlined still",
44
+ "tone": "violet",
45
+ "cap": {
46
+ "shape": "tall",
47
+ "hue": 5,
48
+ "filled": false,
49
+ "state": "knot",
50
+ "label": "builder"
51
+ }
52
+ }
53
+ ]
54
+ }
55
+ ]
56
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "input": {
3
+ "fixtureEvents": [
4
+ { "kind": "session.open", "payload": { "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA", "backend": "claude", "project": 2 } },
5
+ { "kind": "session.open", "payload": { "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZB", "backend": "local", "project": 5 } },
6
+ { "kind": "turn.partial", "payload": { "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA" } },
7
+ { "kind": "approval.request", "payload": { "cardId": "card:01HZZZZZZZZZZZZZZZZZZZZZZC", "kind": "permission", "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZA" } },
8
+ { "kind": "approval.request", "payload": { "cardId": "card:01HZZZZZZZZZZZZZZZZZZZZZZD", "kind": "plan", "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZB" } },
9
+ { "kind": "approval.decide", "payload": { "cardId": "card:01HZZZZZZZZZZZZZZZZZZZZZZD" } },
10
+ { "kind": "warpline.knot", "payload": { "hsid": "hsid:v1:01HZZZZZZZZZZZZZZZZZZZZZZB" } },
11
+ { "kind": "mod.paused", "payload": { "name": "hello", "reason": "user" } }
12
+ ]
13
+ },
14
+ "width": 40,
15
+ "prefs": { "motion": "reduced", "theme": "day" }
16
+ }