@north-light/crouter 0.3.236 → 0.3.238

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 (177) hide show
  1. package/dist/api/client.d.ts +2 -1
  2. package/dist/api/client.js +3 -0
  3. package/dist/api/dto/broker-ops.d.ts +1 -1
  4. package/dist/api/dto/common.d.ts +2 -0
  5. package/dist/api/dto/lifecycle.d.ts +9 -2
  6. package/dist/api/dto/messages.d.ts +5 -0
  7. package/dist/api/dto/nodes.d.ts +10 -2
  8. package/dist/api/dto/worktree.d.ts +10 -0
  9. package/dist/api/routes.d.ts +1 -0
  10. package/dist/api/routes.js +1 -0
  11. package/dist/builtin-memory/00-runtime-base/00-authoring.md +1 -1
  12. package/dist/builtin-memory/internal/agent-shaping.md +1 -1
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +1 -1
  14. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/sysprompt-window.ts +1 -1
  15. package/dist/clients/attach/render/tool-calls.js +18 -3
  16. package/dist/clients/attach/viewer.js +442 -442
  17. package/dist/clients/inbox/review/__tests__/editor-roundtrip.test.js +6 -0
  18. package/dist/clients/inbox/review/roundtrip.js +1 -1
  19. package/dist/commands/memory/lint.js +22 -13
  20. package/dist/commands/memory/shared.js +1 -1
  21. package/dist/commands/node/create.d.ts +1 -1
  22. package/dist/commands/node/create.js +1 -1
  23. package/dist/commands/node/lifecycle.js +1 -1
  24. package/dist/commands/node-worktree.js +39 -8
  25. package/dist/commands/revive.js +11 -2
  26. package/dist/commands/sys/context/admin/actions.d.ts +48 -0
  27. package/dist/commands/sys/context/admin/actions.js +246 -0
  28. package/dist/commands/sys/context/admin/axes-panel.d.ts +21 -0
  29. package/dist/commands/sys/context/admin/axes-panel.js +117 -0
  30. package/dist/commands/sys/context/admin/detail-panel.d.ts +53 -0
  31. package/dist/commands/sys/context/admin/detail-panel.js +178 -0
  32. package/dist/commands/sys/context/admin/docs-panel.d.ts +45 -0
  33. package/dist/commands/sys/context/admin/docs-panel.js +186 -0
  34. package/dist/commands/sys/context/admin/model.d.ts +75 -0
  35. package/dist/commands/sys/context/admin/model.js +209 -0
  36. package/dist/commands/sys/context/admin/shell.d.ts +82 -0
  37. package/dist/commands/sys/context/admin/shell.js +669 -0
  38. package/dist/commands/sys/context/admin.d.ts +1 -0
  39. package/dist/commands/sys/context/admin.js +124 -0
  40. package/dist/commands/sys/context/doc.d.ts +1 -0
  41. package/dist/commands/sys/context/doc.js +196 -0
  42. package/dist/commands/sys/context/prompt-review.d.ts +1 -0
  43. package/dist/commands/sys/{prompt-review.js → context/prompt-review.js} +10 -10
  44. package/dist/commands/sys/context/resolve.d.ts +55 -0
  45. package/dist/commands/sys/context/resolve.js +428 -0
  46. package/dist/commands/sys/context/sysprompt.d.ts +1 -0
  47. package/dist/commands/sys/{sysprompt.js → context/sysprompt.js} +11 -11
  48. package/dist/commands/sys/context.d.ts +2 -0
  49. package/dist/commands/sys/context.js +20 -0
  50. package/dist/commands/sys.js +3 -4
  51. package/dist/core/__tests__/child-death-wake.test.js +38 -22
  52. package/dist/core/__tests__/cron-self-cancel-delivery.test.js +72 -0
  53. package/dist/core/__tests__/daemon-boot.test.js +40 -28
  54. package/dist/core/__tests__/human-deliver.test.js +1 -1
  55. package/dist/core/__tests__/human-node-not-supervised.test.js +10 -15
  56. package/dist/core/__tests__/integration/revive.test.js +12 -12
  57. package/dist/core/__tests__/integration/spawn-root.test.js +6 -0
  58. package/dist/core/__tests__/integration/worktree-reap.test.js +32 -0
  59. package/dist/core/__tests__/lifecycle-terminal-reasons.test.d.ts +1 -0
  60. package/dist/core/__tests__/lifecycle-terminal-reasons.test.js +61 -0
  61. package/dist/core/__tests__/migration.test.js +38 -1
  62. package/dist/core/__tests__/push-final-guard.test.js +1 -1
  63. package/dist/core/__tests__/revive-capacity.test.d.ts +1 -0
  64. package/dist/core/__tests__/revive-capacity.test.js +127 -0
  65. package/dist/core/__tests__/revive-parked-fresh.test.js +7 -7
  66. package/dist/core/__tests__/seam/broker-cap-freeze.test.d.ts +1 -0
  67. package/dist/core/__tests__/seam/broker-cap-freeze.test.js +81 -0
  68. package/dist/core/__tests__/seam/dormancy-release.test.js +7 -7
  69. package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +1 -1
  70. package/dist/core/canvas/browse/render.js +11 -3
  71. package/dist/core/canvas/canvas.d.ts +15 -3
  72. package/dist/core/canvas/canvas.js +52 -16
  73. package/dist/core/canvas/migrations.js +21 -0
  74. package/dist/core/canvas/nav-render.js +3 -1
  75. package/dist/core/canvas/remote-canvas-source.js +2 -0
  76. package/dist/core/canvas/render-source.d.ts +3 -1
  77. package/dist/core/canvas/render-source.js +7 -3
  78. package/dist/core/canvas/status-glyph.d.ts +10 -1
  79. package/dist/core/canvas/status-glyph.js +19 -3
  80. package/dist/core/canvas/types.d.ts +20 -1
  81. package/dist/core/feed/feed.js +1 -1
  82. package/dist/core/human/feedback-companion.js +5 -2
  83. package/dist/core/keybindings/catalog.d.ts +1 -1
  84. package/dist/core/keybindings/catalog.js +6 -2
  85. package/dist/core/memory/lint.d.ts +19 -0
  86. package/dist/core/memory/lint.js +97 -0
  87. package/dist/core/memory-resolver.d.ts +4 -0
  88. package/dist/core/memory-resolver.js +7 -7
  89. package/dist/core/preview-registry.js +4 -4
  90. package/dist/core/review/realize.js +4 -2
  91. package/dist/core/runtime/close.js +1 -1
  92. package/dist/core/runtime/fleet.d.ts +29 -1
  93. package/dist/core/runtime/fleet.js +23 -0
  94. package/dist/core/runtime/host.js +32 -11
  95. package/dist/core/runtime/launch-prompt.d.ts +1 -1
  96. package/dist/core/runtime/launch-prompt.js +2 -2
  97. package/dist/core/runtime/lifecycle.d.ts +4 -2
  98. package/dist/core/runtime/lifecycle.js +12 -1
  99. package/dist/core/runtime/memory.js +1 -1
  100. package/dist/core/runtime/reopen.js +10 -1
  101. package/dist/core/runtime/reset.js +1 -1
  102. package/dist/core/runtime/revive-all.d.ts +3 -0
  103. package/dist/core/runtime/revive-all.js +10 -4
  104. package/dist/core/runtime/revive.d.ts +12 -0
  105. package/dist/core/runtime/revive.js +62 -3
  106. package/dist/core/runtime/spawn.d.ts +4 -0
  107. package/dist/core/runtime/spawn.js +36 -7
  108. package/dist/core/runtime/tmux-bindings.js +4 -0
  109. package/dist/core/substrate/frontmatter-validation.js +1 -1
  110. package/dist/core/substrate/index.d.ts +6 -3
  111. package/dist/core/substrate/index.js +4 -3
  112. package/dist/core/substrate/on-read.d.ts +0 -8
  113. package/dist/core/substrate/on-read.js +45 -124
  114. package/dist/core/substrate/plan.d.ts +94 -0
  115. package/dist/core/substrate/plan.js +266 -0
  116. package/dist/core/substrate/render.d.ts +5 -6
  117. package/dist/core/substrate/render.js +76 -129
  118. package/dist/core/substrate/schema.d.ts +1 -6
  119. package/dist/core/substrate/schema.js +1 -15
  120. package/dist/core/substrate/session-cache.d.ts +2 -10
  121. package/dist/core/substrate/session-cache.js +9 -47
  122. package/dist/core/substrate/subject-fields.d.ts +1 -4
  123. package/dist/core/substrate/subject-fields.js +2 -5
  124. package/dist/core/substrate/surface-match.d.ts +37 -17
  125. package/dist/core/substrate/surface-match.js +44 -53
  126. package/dist/core/tui/page-host.d.ts +6 -0
  127. package/dist/core/tui/page-host.js +12 -3
  128. package/dist/core/worktree.d.ts +8 -0
  129. package/dist/core/worktree.js +65 -0
  130. package/dist/daemon/__tests__/reconciler-signature.test.js +3 -2
  131. package/dist/daemon/__tests__/thaw-order.test.d.ts +1 -0
  132. package/dist/daemon/__tests__/thaw-order.test.js +55 -0
  133. package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +6 -0
  134. package/dist/daemon/api/__tests__/node-create-description.test.js +6 -0
  135. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +1 -1
  136. package/dist/daemon/api/bridge.js +5 -1
  137. package/dist/daemon/api/handlers/attach.js +5 -1
  138. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  139. package/dist/daemon/api/handlers/messages.js +10 -2
  140. package/dist/daemon/api/handlers/nodes.js +8 -1
  141. package/dist/daemon/api/handlers/worktree.js +10 -1
  142. package/dist/daemon/api/map.js +7 -0
  143. package/dist/daemon/companion-retire.js +1 -1
  144. package/dist/daemon/cron/passes.d.ts +0 -5
  145. package/dist/daemon/cron/passes.js +0 -20
  146. package/dist/daemon/cron/sinks.js +1 -1
  147. package/dist/daemon/cron-run.d.ts +5 -6
  148. package/dist/daemon/cron-run.js +7 -16
  149. package/dist/daemon/crtrd.d.ts +0 -10
  150. package/dist/daemon/crtrd.js +23 -61
  151. package/dist/daemon/fleet.d.ts +10 -7
  152. package/dist/daemon/fleet.js +71 -12
  153. package/dist/daemon/messaging/node-message.js +3 -1
  154. package/dist/daemon/profile-delete.js +1 -1
  155. package/dist/daemon/reconcilers/broker-supervision.d.ts +26 -9
  156. package/dist/daemon/reconcilers/broker-supervision.js +39 -52
  157. package/dist/daemon/reconcilers/controller-death.d.ts +7 -0
  158. package/dist/daemon/reconcilers/controller-death.js +26 -0
  159. package/dist/daemon/reconcilers/cron-lane.d.ts +0 -3
  160. package/dist/daemon/reconcilers/cron-lane.js +0 -3
  161. package/dist/daemon/reconcilers/live-obligation.js +3 -1
  162. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.d.ts +53 -0
  163. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +170 -0
  164. package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.d.ts +5 -0
  165. package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.js +16 -0
  166. package/dist/daemon/reconcilers/node-lifecycle/terminating.d.ts +4 -0
  167. package/dist/daemon/reconcilers/node-lifecycle/terminating.js +13 -0
  168. package/dist/daemon/reconcilers/node-lifecycle/tick.d.ts +26 -0
  169. package/dist/daemon/reconcilers/node-lifecycle/tick.js +140 -0
  170. package/package.json +1 -1
  171. package/runtime.lock.json +2 -2
  172. package/dist/commands/sys/prompt-review.d.ts +0 -1
  173. package/dist/commands/sys/sysprompt.d.ts +0 -1
  174. package/dist/core/__tests__/cron-broker-capacity.test.js +0 -119
  175. package/dist/daemon/reconcilers/dormant-inbox.d.ts +0 -18
  176. package/dist/daemon/reconcilers/dormant-inbox.js +0 -157
  177. /package/dist/core/__tests__/{cron-broker-capacity.test.d.ts → cron-self-cancel-delivery.test.d.ts} +0 -0
@@ -0,0 +1,117 @@
1
+ // The snapshot rail: the configuration axes the page plans against. Every row
2
+ // is a live control — an enum cycles in place, a free-text axis opens a field —
3
+ // and every change re-plans the whole surface.
4
+ import { truncateToWidth } from '@earendil-works/pi-tui';
5
+ import { padAnsi, theme, visibleRange } from '../../../../core/tui/panel.js';
6
+ import { SURFACE_EVENTS } from '../../../../core/substrate/schema.js';
7
+ import { listProfiles } from '../../../../core/profiles/manifest.js';
8
+ import { payloadAxisFor } from './model.js';
9
+ function tildify(path) {
10
+ const home = process.env['HOME'];
11
+ return home !== undefined && home !== '' && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
12
+ }
13
+ /** The rail's rows for a snapshot. The payload row appears only for the events
14
+ * that read one, because on boot there is nothing it could change. */
15
+ export function axesOf(snapshot, profileName) {
16
+ const payload = payloadAxisFor(snapshot.event);
17
+ const payloadValue = payload === 'file' ? snapshot.file : payload === 'docName' ? snapshot.docName : payload === 'command' ? snapshot.command : '';
18
+ return [
19
+ { id: 'event', label: 'event', value: snapshot.event, typed: false },
20
+ ...(payload === null
21
+ ? []
22
+ : [{
23
+ id: 'payload',
24
+ label: payload === 'file' ? 'file' : payload === 'docName' ? 'doc' : 'command',
25
+ value: payloadValue === '' ? '(none — nothing matches)' : payloadValue,
26
+ typed: true,
27
+ }]),
28
+ { id: 'kind', label: 'kind', value: snapshot.kind, typed: true },
29
+ { id: 'mode', label: 'mode', value: snapshot.mode, typed: false },
30
+ { id: 'lifecycle', label: 'lifecycle', value: snapshot.lifecycle, typed: false },
31
+ { id: 'manager', label: 'manager', value: snapshot.hasManager ? 'yes' : 'no', typed: false },
32
+ { id: 'depth', label: 'depth', value: String(snapshot.depth), typed: false },
33
+ { id: 'profile', label: 'profile', value: profileName, typed: false },
34
+ { id: 'cwd', label: 'cwd', value: tildify(snapshot.cwd), typed: true },
35
+ ];
36
+ }
37
+ /** Cycle one enum axis, returning the changed snapshot. A typed axis is never
38
+ * cycled — the shell opens a field for it instead. */
39
+ export function cycleAxis(snapshot, id, delta) {
40
+ const step = (values, current) => {
41
+ const at = values.indexOf(current);
42
+ return values[(Math.max(0, at) + delta + values.length) % values.length];
43
+ };
44
+ switch (id) {
45
+ case 'event':
46
+ return { ...snapshot, event: step(SURFACE_EVENTS, snapshot.event) };
47
+ case 'mode': {
48
+ const mode = step(['base', 'orchestrator'], snapshot.mode);
49
+ // Lifecycle follows mode the way node creation defaults it, so cycling to
50
+ // orchestrator does not leave a resident-only persona reading as terminal.
51
+ return { ...snapshot, mode, lifecycle: mode === 'orchestrator' ? 'resident' : 'terminal' };
52
+ }
53
+ case 'lifecycle':
54
+ return { ...snapshot, lifecycle: step(['terminal', 'resident'], snapshot.lifecycle) };
55
+ case 'manager':
56
+ return { ...snapshot, hasManager: !snapshot.hasManager };
57
+ case 'depth':
58
+ return { ...snapshot, depth: Math.max(0, snapshot.depth + delta) };
59
+ case 'profile': {
60
+ const ids = [null, ...listProfiles().map((entry) => entry.profileId)];
61
+ return { ...snapshot, profileId: step(ids, snapshot.profileId) };
62
+ }
63
+ default:
64
+ return snapshot;
65
+ }
66
+ }
67
+ /** Apply a typed axis's submitted value. */
68
+ export function setTypedAxis(snapshot, id, value) {
69
+ const trimmed = value.trim();
70
+ if (id === 'kind')
71
+ return trimmed === '' ? snapshot : { ...snapshot, kind: trimmed };
72
+ if (id === 'cwd')
73
+ return trimmed === '' ? snapshot : { ...snapshot, cwd: trimmed };
74
+ if (id !== 'payload')
75
+ return snapshot;
76
+ const axis = payloadAxisFor(snapshot.event);
77
+ if (axis === 'file')
78
+ return { ...snapshot, file: trimmed };
79
+ if (axis === 'docName')
80
+ return { ...snapshot, docName: trimmed };
81
+ if (axis === 'command')
82
+ return { ...snapshot, command: trimmed };
83
+ return snapshot;
84
+ }
85
+ /** The value a typed axis's field is seeded with. */
86
+ export function typedAxisValue(snapshot, id) {
87
+ if (id === 'kind')
88
+ return snapshot.kind;
89
+ if (id === 'cwd')
90
+ return snapshot.cwd;
91
+ const axis = payloadAxisFor(snapshot.event);
92
+ return axis === 'file' ? snapshot.file : axis === 'docName' ? snapshot.docName : axis === 'command' ? snapshot.command : '';
93
+ }
94
+ const LABEL_WIDTH = 10;
95
+ export function renderAxes(axes, index, width, height, focused) {
96
+ const out = [theme.fg('dim', ' SNAPSHOT'), ''];
97
+ const headerRows = out.length;
98
+ const rows = [];
99
+ for (const [at, axis] of axes.entries()) {
100
+ const room = Math.max(1, width - LABEL_WIDTH - 2);
101
+ const label = padAnsi(truncateToWidth(axis.label, LABEL_WIDTH), LABEL_WIDTH);
102
+ const value = truncateToWidth(axis.value, room);
103
+ const body = ` ${label} ${value}`;
104
+ if (at === index && focused)
105
+ rows.push(theme.bg('selectedBg', padAnsi(body, width)));
106
+ else if (at === index)
107
+ rows.push(`${theme.fg('accent', '▌')}${padAnsi(theme.fg('accent', body.slice(1)), width - 1)}`);
108
+ else
109
+ rows.push(padAnsi(` ${theme.fg('dim', label)} ${value}`, width));
110
+ }
111
+ const capacity = Math.max(1, height - headerRows);
112
+ const range = visibleRange(rows.length, index, capacity);
113
+ out.push(...rows.slice(range.start, range.start + capacity));
114
+ while (out.length < height)
115
+ out.push('');
116
+ return out.slice(0, height);
117
+ }
@@ -0,0 +1,53 @@
1
+ import { type Rung, type SurfaceEvent } from '../../../../core/substrate/schema.js';
2
+ import type { DeliveryRecord } from '../../../../core/substrate/plan.js';
3
+ import type { DocRow } from './docs-panel.js';
4
+ /** A field of the focused document the shell can write. `entry` carries an
5
+ * index into the document's whole surfaces list, not the focused event's
6
+ * subset, because that list is what a write replaces. */
7
+ export type DetailRow = {
8
+ kind: 'entry';
9
+ at: number;
10
+ } | {
11
+ kind: 'add-entry';
12
+ } | {
13
+ kind: 'gate';
14
+ } | {
15
+ kind: 'unlisted';
16
+ } | {
17
+ kind: 'slash';
18
+ } | {
19
+ kind: 'routing';
20
+ } | {
21
+ kind: 'short-form';
22
+ } | {
23
+ kind: 'body';
24
+ } | {
25
+ kind: 'cap';
26
+ } | {
27
+ kind: 'move';
28
+ };
29
+ export interface CapOwner {
30
+ path: string;
31
+ memory: Rung;
32
+ }
33
+ export declare class ContextDetailPanel {
34
+ private row;
35
+ private event;
36
+ private capOwner;
37
+ private readOnly;
38
+ private rows;
39
+ private index;
40
+ /** Lines scrolled past the cursor's own window, so the verdict below the last
41
+ * editable row stays reachable. */
42
+ private tail;
43
+ setContext(row: DocRow | undefined, event: SurfaceEvent, capOwner: CapOwner | null, readOnly: string | null): void;
44
+ get selected(): DetailRow | undefined;
45
+ /** The entry index a rung cycle or a removal would act on. */
46
+ get selectedEntry(): number | undefined;
47
+ move(delta: number): void;
48
+ render(width: number, height: number, focused: boolean): string[];
49
+ private verdict;
50
+ private acrossEvents;
51
+ }
52
+ /** The editable rows for one document, in the order the panel draws them. */
53
+ export declare function buildDetailRows(record: DeliveryRecord): DetailRow[];
@@ -0,0 +1,178 @@
1
+ // The dossier: one document's editable configuration above the read-only
2
+ // verdict that configuration produces. Every selectable row here is a field the
3
+ // shell can write; everything below the rule is what the planner decided.
4
+ import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
5
+ import { padAnsi, theme, wrapText } from '../../../../core/tui/panel.js';
6
+ import { SURFACE_EVENTS } from '../../../../core/substrate/schema.js';
7
+ import { EVENT_INITIALS, cellStateOf, describeSurfaceEntry, plural } from './model.js';
8
+ const STATE_WORD = {
9
+ pending: 'planning…',
10
+ delivers: 'delivers',
11
+ capped: 'capped',
12
+ gated: 'gated out',
13
+ shadowed: 'shadowed',
14
+ silent: 'silent',
15
+ };
16
+ export class ContextDetailPanel {
17
+ row;
18
+ event = 'boot';
19
+ capOwner = null;
20
+ readOnly = null;
21
+ rows = [];
22
+ index = 0;
23
+ /** Lines scrolled past the cursor's own window, so the verdict below the last
24
+ * editable row stays reachable. */
25
+ tail = 0;
26
+ setContext(row, event, capOwner, readOnly) {
27
+ const changed = row?.key !== this.row?.key;
28
+ this.row = row;
29
+ this.event = event;
30
+ this.capOwner = capOwner;
31
+ this.readOnly = readOnly;
32
+ this.rows = row === undefined ? [] : buildDetailRows(row.anchor);
33
+ if (changed) {
34
+ this.index = 0;
35
+ this.tail = 0;
36
+ }
37
+ this.index = Math.max(0, Math.min(this.index, this.rows.length - 1));
38
+ }
39
+ get selected() {
40
+ return this.rows[this.index];
41
+ }
42
+ /** The entry index a rung cycle or a removal would act on. */
43
+ get selectedEntry() {
44
+ const row = this.selected;
45
+ return row?.kind === 'entry' ? row.at : undefined;
46
+ }
47
+ move(delta) {
48
+ const next = this.index + delta;
49
+ if (next < 0 || next >= this.rows.length) {
50
+ this.tail = Math.max(0, this.tail + delta);
51
+ return;
52
+ }
53
+ this.index = next;
54
+ this.tail = 0;
55
+ }
56
+ render(width, height, focused) {
57
+ const row = this.row;
58
+ if (row === undefined) {
59
+ return fit([theme.fg('dim', ' Select a document.')], height);
60
+ }
61
+ const record = row.records.get(this.event) ?? row.anchor;
62
+ const lines = [];
63
+ const rowLines = new Map();
64
+ lines.push(truncateToWidth(`${theme.fg('accent', theme.bold(record.name))}${this.readOnly === null ? '' : theme.fg('warning', ' read-only')}`, width));
65
+ lines.push(truncateToWidth(theme.fg('dim', `${record.kind} · ${record.scope}${record.nodeLocal ? ' · node-local' : ''} · ${record.source.representation}`), width));
66
+ lines.push(truncateToWidth(theme.fg('dim', record.storePath), width));
67
+ if (this.readOnly !== null)
68
+ lines.push(...wrapText(this.readOnly, width).map((line) => theme.fg('warning', line)));
69
+ lines.push('');
70
+ const push = (at, body) => {
71
+ rowLines.set(at, lines.length);
72
+ lines.push(at === this.index && focused
73
+ ? theme.bg('selectedBg', padAnsi(truncateToWidth(body, width), width))
74
+ : at === this.index
75
+ ? `${theme.fg('accent', '▌')}${truncateToWidth(body, Math.max(0, width - 1))}`
76
+ : ` ${truncateToWidth(body, Math.max(0, width - 1))}`);
77
+ };
78
+ let at = 0;
79
+ lines.push(theme.fg('dim', ' SURFACES'));
80
+ const surfaces = record.doc.surfaces;
81
+ if (surfaces.length === 0)
82
+ push(at++, theme.fg('dim', 'no entries — this document only appears in its listing'));
83
+ else {
84
+ for (const entry of surfaces) {
85
+ const text = describeSurfaceEntry(entry);
86
+ push(at++, entry.on === this.event ? text : theme.fg('dim', text));
87
+ }
88
+ }
89
+ push(at++, theme.fg('success', '+ add a surface entry'));
90
+ lines.push('');
91
+ lines.push(theme.fg('dim', ' DOCUMENT'));
92
+ push(at++, field('gate', record.doc.gate === undefined ? theme.fg('dim', 'none') : JSON.stringify(record.doc.gate), width));
93
+ push(at++, field('unlisted', record.doc.unlisted ? 'yes' : theme.fg('dim', 'no'), width));
94
+ push(at++, field('slash', record.doc.slash ? 'yes' : theme.fg('dim', 'no'), width));
95
+ push(at++, field('routing', record.doc.whenAndWhyToRead, width));
96
+ push(at++, field('short form', record.doc.shortForm === '' ? theme.fg('dim', 'none') : record.doc.shortForm, width));
97
+ push(at++, field('body', theme.fg('dim', `${plural(bodyLines(record.doc.body), 'line', 'lines')} \u2014 opens in $EDITOR`), width));
98
+ push(at++, field('profile cap', this.capOwner === null
99
+ ? theme.fg('dim', 'no profile project governs this store — uncapped')
100
+ : `${this.capOwner.memory} ${theme.fg('dim', this.capOwner.path)}`, width));
101
+ push(at++, field('canonical name', record.name, width));
102
+ lines.push('');
103
+ lines.push(theme.fg('border', '─'.repeat(Math.max(0, width))));
104
+ lines.push(theme.fg('dim', ` VERDICT · ${this.event}`));
105
+ lines.push(...this.verdict(record, width));
106
+ lines.push('');
107
+ lines.push(theme.fg('dim', ' ACROSS EVENTS'));
108
+ lines.push(...this.acrossEvents(row, width));
109
+ const cursorLine = rowLines.get(this.index) ?? 0;
110
+ const maxStart = Math.max(0, lines.length - height);
111
+ let start = Math.min(Math.max(0, cursorLine - Math.floor(height / 3)), maxStart);
112
+ if (this.tail > 0)
113
+ start = Math.min(maxStart, start + this.tail);
114
+ return fit(lines.slice(start, start + height), height);
115
+ }
116
+ verdict(record, width) {
117
+ const out = [];
118
+ const cap = record.projectMemoryCap === null ? 'uncapped' : record.projectMemoryCap;
119
+ out.push(truncateToWidth(` authored ${theme.bold(record.authoredRung)} → cap ${cap} → ${theme.fg(record.finalRung === 'none' ? 'dim' : 'success', theme.bold(record.finalRung))}`, width));
120
+ if (!record.docGate.pass)
121
+ out.push(...wrapText(` document gate fails: ${record.docGate.reason}`, width).map((line) => theme.fg('warning', line)));
122
+ if (record.entryGate !== null && !record.entryGate.pass) {
123
+ out.push(...wrapText(` entry gate fails: ${record.entryGate.reason}`, width).map((line) => theme.fg('warning', line)));
124
+ }
125
+ if (record.matchedEntry === null && record.authoredEntries.length > 0) {
126
+ out.push(theme.fg('dim', ` ${plural(record.authoredEntries.length, 'entry', 'entries')} for this event, none matched`));
127
+ }
128
+ if (record.shadowedBy !== null) {
129
+ out.push(...wrapText(` shadowed by the ${record.shadowedBy.scope} copy at ${record.shadowedBy.path}`, width).map((line) => theme.fg('muted', line)));
130
+ }
131
+ else if (record.winner) {
132
+ out.push(theme.fg('dim', ' wins this canonical name'));
133
+ }
134
+ return out;
135
+ }
136
+ acrossEvents(row, width) {
137
+ return SURFACE_EVENTS.map((event) => {
138
+ const record = row.records.get(event);
139
+ const state = record === undefined ? 'silent' : cellStateOf(record);
140
+ const rung = record === undefined ? 'none' : record.finalRung;
141
+ const label = padAnsi(` ${EVENT_INITIALS[event]} ${event}`, 20);
142
+ const word = STATE_WORD[state] ?? state;
143
+ const body = `${label} ${padAnsi(word, 11)} ${rung}`;
144
+ return truncateToWidth(state === 'delivers' || state === 'capped' ? body : theme.fg('dim', body), width);
145
+ });
146
+ }
147
+ }
148
+ /** The editable rows for one document, in the order the panel draws them. */
149
+ export function buildDetailRows(record) {
150
+ const entries = record.doc.surfaces.map((_entry, at) => ({ kind: 'entry', at }));
151
+ return [
152
+ ...entries,
153
+ { kind: 'add-entry' },
154
+ { kind: 'gate' },
155
+ { kind: 'unlisted' },
156
+ { kind: 'slash' },
157
+ { kind: 'routing' },
158
+ { kind: 'short-form' },
159
+ { kind: 'body' },
160
+ { kind: 'cap' },
161
+ { kind: 'move' },
162
+ ];
163
+ }
164
+ function bodyLines(body) {
165
+ const trimmed = body.replace(/\n+$/, '');
166
+ return trimmed === '' ? 0 : trimmed.split('\n').length;
167
+ }
168
+ const FIELD_WIDTH = 15;
169
+ function field(label, value, width) {
170
+ const room = Math.max(1, width - FIELD_WIDTH - 2);
171
+ return `${padAnsi(theme.fg('dim', label), FIELD_WIDTH)} ${truncateToWidth(value, room)}`;
172
+ }
173
+ function fit(lines, height) {
174
+ const out = [...lines];
175
+ while (out.length < height)
176
+ out.push('');
177
+ return out.slice(0, height).map((line) => (visibleWidth(line) === 0 ? '' : line));
178
+ }
@@ -0,0 +1,45 @@
1
+ import { type SurfaceEvent } from '../../../../core/substrate/schema.js';
2
+ import type { DeliveryRecord } from '../../../../core/substrate/plan.js';
3
+ import { PlanSet } from './model.js';
4
+ /** One physical document across every event. `records` is sparse: an event
5
+ * whose corpus never reaches this document has no record for it, and an event
6
+ * still being planned has none yet either. */
7
+ export interface DocRow {
8
+ key: string;
9
+ name: string;
10
+ /** The record for the focused event, or the first one any event carries —
11
+ * every panel needs a document to describe even when the focused event's
12
+ * corpus excludes it. */
13
+ anchor: DeliveryRecord;
14
+ records: Map<SurfaceEvent, DeliveryRecord>;
15
+ /** Present only in an event other than the focused one. */
16
+ offEvent: boolean;
17
+ }
18
+ /** The union of every planned event's corpus, ordered by the focused event's
19
+ * precedence order with the documents only other events see appended. */
20
+ export declare function buildRows(plans: PlanSet, event: SurfaceEvent): DocRow[];
21
+ export declare function rowDelivers(row: DocRow): boolean;
22
+ export declare class ContextDocsPanel {
23
+ private readonly onChange;
24
+ private rows;
25
+ private index;
26
+ private filter;
27
+ private filtering;
28
+ /** Off by default: the surface is legible only when the silent documents are
29
+ * visible beside the delivering ones. */
30
+ private deliveringOnly;
31
+ constructor(onChange: () => void);
32
+ get selected(): DocRow | undefined;
33
+ get ownsKeyboard(): boolean;
34
+ get filteringDelivering(): boolean;
35
+ setRows(rows: DocRow[], selectedKey?: string): void;
36
+ move(delta: number): void;
37
+ toggleDeliveringOnly(): void;
38
+ /** True only when a live filter consumed the input. */
39
+ handleInput(data: string): boolean;
40
+ private visible;
41
+ render(width: number, height: number, focused: boolean, plans: PlanSet): string[];
42
+ /** Pad rather than trim: this column sets the length of the divider drawn
43
+ * beside it. */
44
+ private fit;
45
+ }
@@ -0,0 +1,186 @@
1
+ // The middle column: every document the snapshot can see, each carrying its own
2
+ // six-event matrix. One row is one physical document, so two stores answering
3
+ // to the same canonical name stay two rows and the shadowed one is visible.
4
+ import { matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
5
+ import { padAnsi, theme, visibleRange } from '../../../../core/tui/panel.js';
6
+ import { SURFACE_EVENTS } from '../../../../core/substrate/schema.js';
7
+ import { EVENT_INITIALS, cellGlyph, cellStateOf, plural } from './model.js';
8
+ /** The union of every planned event's corpus, ordered by the focused event's
9
+ * precedence order with the documents only other events see appended. */
10
+ export function buildRows(plans, event) {
11
+ const rows = new Map();
12
+ const order = [];
13
+ const absorb = (planEvent) => {
14
+ const plan = plans.peek(planEvent);
15
+ if (plan === undefined)
16
+ return;
17
+ for (const record of plan.docs) {
18
+ const key = record.storePath;
19
+ const held = rows.get(key);
20
+ if (held === undefined) {
21
+ rows.set(key, {
22
+ key,
23
+ name: record.name,
24
+ anchor: record,
25
+ records: new Map([[planEvent, record]]),
26
+ offEvent: planEvent !== event,
27
+ });
28
+ order.push(key);
29
+ continue;
30
+ }
31
+ held.records.set(planEvent, record);
32
+ if (planEvent === event) {
33
+ held.anchor = record;
34
+ held.offEvent = false;
35
+ }
36
+ }
37
+ };
38
+ absorb(event);
39
+ for (const other of SURFACE_EVENTS)
40
+ if (other !== event)
41
+ absorb(other);
42
+ return order.map((key) => rows.get(key));
43
+ }
44
+ export function rowDelivers(row) {
45
+ for (const record of row.records.values())
46
+ if (record.finalRung !== 'none')
47
+ return true;
48
+ return false;
49
+ }
50
+ /** Cells and their width, so the shell's legend and the list agree. */
51
+ const CELL_COUNT = SURFACE_EVENTS.length;
52
+ const MATRIX_WIDTH = CELL_COUNT * 2 - 1;
53
+ export class ContextDocsPanel {
54
+ onChange;
55
+ rows = [];
56
+ index = 0;
57
+ filter = '';
58
+ filtering = false;
59
+ /** Off by default: the surface is legible only when the silent documents are
60
+ * visible beside the delivering ones. */
61
+ deliveringOnly = false;
62
+ constructor(onChange) {
63
+ this.onChange = onChange;
64
+ }
65
+ get selected() {
66
+ const rows = this.visible();
67
+ this.index = Math.max(0, Math.min(this.index, rows.length - 1));
68
+ return rows[this.index];
69
+ }
70
+ get ownsKeyboard() {
71
+ return this.filtering;
72
+ }
73
+ get filteringDelivering() {
74
+ return this.deliveringOnly;
75
+ }
76
+ setRows(rows, selectedKey) {
77
+ this.rows = rows;
78
+ const visible = this.visible();
79
+ const found = selectedKey === undefined ? -1 : visible.findIndex((row) => row.key === selectedKey);
80
+ this.index = found >= 0 ? found : Math.min(this.index, Math.max(0, visible.length - 1));
81
+ }
82
+ move(delta) {
83
+ const rows = this.visible();
84
+ if (rows.length === 0)
85
+ return;
86
+ this.index = Math.max(0, Math.min(rows.length - 1, this.index + delta));
87
+ this.onChange();
88
+ }
89
+ toggleDeliveringOnly() {
90
+ this.deliveringOnly = !this.deliveringOnly;
91
+ const held = this.selected?.key;
92
+ this.index = 0;
93
+ if (held !== undefined) {
94
+ const at = this.visible().findIndex((row) => row.key === held);
95
+ if (at >= 0)
96
+ this.index = at;
97
+ }
98
+ this.onChange();
99
+ }
100
+ /** True only when a live filter consumed the input. */
101
+ handleInput(data) {
102
+ if (this.filtering) {
103
+ if (matchesKey(data, 'escape')) {
104
+ this.filter = '';
105
+ this.filtering = false;
106
+ }
107
+ else if (matchesKey(data, 'enter')) {
108
+ this.filtering = false;
109
+ }
110
+ else if (matchesKey(data, 'backspace')) {
111
+ this.filter = this.filter.slice(0, -1);
112
+ this.index = 0;
113
+ }
114
+ else if (data.length === 1 && data.charCodeAt(0) >= 32) {
115
+ this.filter += data;
116
+ this.index = 0;
117
+ }
118
+ this.onChange();
119
+ return true;
120
+ }
121
+ if (data === '/' || matchesKey(data, '/')) {
122
+ this.filtering = true;
123
+ this.onChange();
124
+ return true;
125
+ }
126
+ if (matchesKey(data, 'escape') && this.filter !== '') {
127
+ this.filter = '';
128
+ this.index = 0;
129
+ this.onChange();
130
+ return true;
131
+ }
132
+ return false;
133
+ }
134
+ visible() {
135
+ const needle = this.filter.trim().toLowerCase();
136
+ return this.rows.filter((row) => {
137
+ if (this.deliveringOnly && !rowDelivers(row))
138
+ return false;
139
+ if (needle === '')
140
+ return true;
141
+ return `${row.name}\n${row.anchor.scope}\n${row.anchor.storePath}`.toLowerCase().includes(needle);
142
+ });
143
+ }
144
+ render(width, height, focused, plans) {
145
+ const rows = this.visible();
146
+ const out = [];
147
+ const heading = this.filtering
148
+ ? `/${this.filter}`
149
+ : this.deliveringOnly
150
+ ? 'DELIVERING'
151
+ : 'DOCUMENTS';
152
+ const count = this.filter === '' ? plural(rows.length, 'doc', 'docs') : plural(rows.length, 'match', 'matches');
153
+ const matrixHead = SURFACE_EVENTS.map((event) => EVENT_INITIALS[event]).join(' ');
154
+ const nameRoom = Math.max(1, width - MATRIX_WIDTH - 2);
155
+ out.push(truncateToWidth(` ${padAnsi(`${theme.fg('accent', theme.bold(heading))} ${theme.fg('dim', count)}`, nameRoom - 1)} ${theme.fg('dim', matrixHead)}`, width));
156
+ if (rows.length === 0) {
157
+ out.push('', theme.fg('dim', this.filter === '' ? ' No documents in this snapshot\u2019s corpus.' : ' Nothing matches that filter.'));
158
+ return this.fit(out, height);
159
+ }
160
+ const capacity = Math.max(1, height - out.length);
161
+ const range = visibleRange(rows.length, this.index, capacity);
162
+ for (let at = range.start; at < range.end; at += 1) {
163
+ const row = rows[at];
164
+ const matrix = SURFACE_EVENTS.map((event) => {
165
+ const state = plans.peek(event) === undefined ? 'pending' : cellStateOf(row.records.get(event));
166
+ return cellGlyph(row.records.get(event), state);
167
+ }).join(' ');
168
+ const scope = theme.fg('dim', ` ${row.anchor.scope}`);
169
+ const label = truncateToWidth(`${row.name}${scope}`, nameRoom - 1);
170
+ const body = ` ${padAnsi(label, nameRoom - 1)} ${matrix}`;
171
+ out.push(at === this.index && focused
172
+ ? theme.bg('selectedBg', padAnsi(body, width))
173
+ : at === this.index
174
+ ? `${theme.fg('accent', '▌')}${truncateToWidth(body.slice(1), Math.max(0, width - 1))}`
175
+ : body);
176
+ }
177
+ return this.fit(out, height);
178
+ }
179
+ /** Pad rather than trim: this column sets the length of the divider drawn
180
+ * beside it. */
181
+ fit(lines, height) {
182
+ while (lines.length < height)
183
+ lines.push('');
184
+ return lines.slice(0, height).map((line) => (visibleWidth(line) === 0 ? '' : line));
185
+ }
186
+ }
@@ -0,0 +1,75 @@
1
+ import { type DeliveryPlan, type DeliveryRecord } from '../../../../core/substrate/plan.js';
2
+ import { type Rung, type SurfaceEntry, type SurfaceEvent } from '../../../../core/substrate/schema.js';
3
+ import { type NodeConfigSubject } from '../../../../core/substrate/subject-fields.js';
4
+ import { type MemoryTarget } from '../../../../core/memory-resolver.js';
5
+ /** The state an agent-engineer dials. Every axis feeds either the gate subject
6
+ * or the store/cap resolution, and changing one re-plans every event. */
7
+ export interface Snapshot {
8
+ kind: string;
9
+ mode: 'base' | 'orchestrator';
10
+ lifecycle: 'terminal' | 'resident';
11
+ hasManager: boolean;
12
+ profileId: string | null;
13
+ cwd: string;
14
+ nodeId: string | null;
15
+ depth: number;
16
+ event: SurfaceEvent;
17
+ /** `read` payload — the file that was read. */
18
+ file: string;
19
+ /** `memory-read` payload — the canonical name the read resolved to. */
20
+ docName: string;
21
+ /** `command` / `pre-command` payload. */
22
+ command: string;
23
+ }
24
+ export declare function subjectOf(snapshot: Snapshot): NodeConfigSubject;
25
+ export declare function targetOf(snapshot: Snapshot): MemoryTarget;
26
+ /** Which payload axis an event reads, so the rail offers exactly the one field
27
+ * that can make the current event match anything. */
28
+ export declare function payloadAxisFor(event: SurfaceEvent): 'file' | 'docName' | 'command' | null;
29
+ /** Every plan for one snapshot, computed on demand.
30
+ *
31
+ * Each event's corpus is its own — workspace-open sees project stores only,
32
+ * boot orders node-local documents last, read pulls in the stores enclosing
33
+ * the read file — so a shared corpus would report a delivery the runtime would
34
+ * never make. The planner therefore loads per event, and this cache keeps the
35
+ * page responsive by resolving the focused event first and warming the rest
36
+ * one macrotask at a time. */
37
+ export declare class PlanSet {
38
+ private snapshot;
39
+ private readonly plans;
40
+ private warming;
41
+ constructor(snapshot: Snapshot);
42
+ setSnapshot(snapshot: Snapshot): void;
43
+ /** Drop every plan — after a snapshot change, or after a mutation rewrote a
44
+ * document the corpus was read from. */
45
+ invalidate(): void;
46
+ dispose(): void;
47
+ /** The plan for one event, computing it now. */
48
+ plan(event: SurfaceEvent): DeliveryPlan;
49
+ /** The plan for one event only if it is already computed — what the matrix
50
+ * reads, so an uncomputed column draws as pending instead of stalling the
51
+ * frame on five extra corpus loads. */
52
+ peek(event: SurfaceEvent): DeliveryPlan | undefined;
53
+ /** Compute one pending event per macrotask, calling back after each so the
54
+ * matrix fills in column by column. */
55
+ warm(onProgress: () => void): void;
56
+ private payload;
57
+ }
58
+ /** How one document reads in one event's cell. */
59
+ export type CellState = 'pending' | 'delivers' | 'capped' | 'gated' | 'shadowed' | 'silent';
60
+ export declare function cellStateOf(record: DeliveryRecord | undefined): CellState;
61
+ /** One matrix cell: the delivered rung as a filled bar, or the mark that says
62
+ * why nothing is delivered. Colour separates a clean delivery from one the
63
+ * profile cap trimmed, so a capped `content` never reads as an authored one. */
64
+ export declare function cellGlyph(record: DeliveryRecord | undefined, state: CellState): string;
65
+ /** The one-letter column heads over the matrix, in `SURFACE_EVENTS` order. */
66
+ export declare const EVENT_INITIALS: Record<SurfaceEvent, string>;
67
+ export declare function nextRung(rung: Rung, delta: number): Rung;
68
+ /** A parsed entry back in the shape `memory edit --surface` accepts. The parsed
69
+ * form always carries `match` as a list and spells its frontmatter key
70
+ * `matchFrontmatter`; `coerceSurface` wants a bare string for a single glob
71
+ * and the hyphenated key, and rejects anything else. */
72
+ export declare function surfaceEntryToFlag(entry: SurfaceEntry): string;
73
+ /** One authored entry as a single readable row. */
74
+ export declare function describeSurfaceEntry(entry: SurfaceEntry): string;
75
+ export declare function plural(count: number, one: string, many: string): string;