@north-light/crouter 0.3.263 → 0.3.270

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 (171) hide show
  1. package/dist/api/client.d.ts +5 -1
  2. package/dist/api/client.js +6 -0
  3. package/dist/api/dto/broker.d.ts +11 -2
  4. package/dist/api/dto/broker.js +2 -2
  5. package/dist/api/dto/canvas.d.ts +53 -2
  6. package/dist/api/dto/nodes.d.ts +10 -2
  7. package/dist/api/routes.d.ts +1 -0
  8. package/dist/api/routes.js +1 -0
  9. package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +2 -2
  10. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +50 -0
  11. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +46 -6
  12. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +6 -2
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +7 -2
  14. package/dist/clients/attach/session/whip-achievements.d.ts +4 -0
  15. package/dist/clients/attach/session/whip-achievements.js +145 -0
  16. package/dist/clients/attach/slash/dispatch.js +5 -3
  17. package/dist/clients/attach/viewer.js +607 -606
  18. package/dist/commands/canvas-history/grep.js +3 -22
  19. package/dist/commands/canvas-history/read.js +5 -3
  20. package/dist/commands/canvas-history/search.js +3 -22
  21. package/dist/commands/canvas-history/shared.d.ts +8 -0
  22. package/dist/commands/canvas-history/shared.js +64 -1
  23. package/dist/commands/canvas-history/stats.d.ts +1 -0
  24. package/dist/commands/canvas-history/stats.js +68 -0
  25. package/dist/commands/canvas-history.js +4 -3
  26. package/dist/commands/node/create.js +2 -2
  27. package/dist/commands/pkg/plugin-inspect.js +2 -2
  28. package/dist/commands/pkg/plugin-manage.js +3 -3
  29. package/dist/commands/sys/config.js +19 -2
  30. package/dist/commands/sys/doctor.js +4 -3
  31. package/dist/commands/sys/panels/models-panel.js +3 -1
  32. package/dist/core/__tests__/human-deliver.test.js +1 -1
  33. package/dist/core/__tests__/integration/lifecycle-hooks.test.d.ts +1 -0
  34. package/dist/core/__tests__/integration/lifecycle-hooks.test.js +182 -0
  35. package/dist/core/__tests__/integration/revive.test.js +19 -17
  36. package/dist/core/__tests__/model-pin-durability.test.js +25 -1
  37. package/dist/core/__tests__/push-final-guard.test.js +1 -1
  38. package/dist/core/__tests__/relaunch-root.test.js +17 -0
  39. package/dist/core/__tests__/revive-capacity.test.js +20 -20
  40. package/dist/core/__tests__/revive-parked-fresh.test.js +14 -14
  41. package/dist/core/__tests__/seam/broker-cap-freeze.test.js +2 -2
  42. package/dist/core/__tests__/seam/broker-provider-retry.test.js +19 -4
  43. package/dist/core/__tests__/seam/dormancy-release.test.js +7 -7
  44. package/dist/core/__tests__/seam/held-deferred-human-prompt.test.js +1 -1
  45. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +19 -2
  46. package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +2 -2
  47. package/dist/core/broker-client/__tests__/transport-relay.test.js +3 -1
  48. package/dist/core/broker-client/transport-relay.js +5 -1
  49. package/dist/core/canvas/__tests__/history-transcript.test.d.ts +1 -0
  50. package/dist/core/canvas/__tests__/history-transcript.test.js +102 -0
  51. package/dist/core/canvas/browse/app.d.ts +1 -1
  52. package/dist/core/canvas/browse/app.js +13 -10
  53. package/dist/core/canvas/browse/model.js +16 -1
  54. package/dist/core/canvas/browse/render.d.ts +23 -3
  55. package/dist/core/canvas/browse/render.js +429 -64
  56. package/dist/core/canvas/canvas.js +12 -8
  57. package/dist/core/canvas/extensions.d.ts +1 -1
  58. package/dist/core/canvas/extensions.js +7 -1
  59. package/dist/core/canvas/history.d.ts +36 -1
  60. package/dist/core/canvas/history.js +328 -7
  61. package/dist/core/canvas/labels.d.ts +2 -2
  62. package/dist/core/canvas/migrations.js +32 -0
  63. package/dist/core/canvas/node-recap.d.ts +23 -0
  64. package/dist/core/canvas/node-recap.js +67 -0
  65. package/dist/core/canvas/render-source.d.ts +33 -1
  66. package/dist/core/canvas/render-source.js +80 -22
  67. package/dist/core/canvas/types.d.ts +9 -0
  68. package/dist/core/command-hooks/discovery.d.ts +27 -1
  69. package/dist/core/command-hooks/discovery.js +52 -0
  70. package/dist/core/command-hooks/index.d.ts +4 -3
  71. package/dist/core/command-hooks/index.js +2 -1
  72. package/dist/core/command-hooks/lifecycle-catalog.d.ts +3 -0
  73. package/dist/core/command-hooks/lifecycle-catalog.js +4 -0
  74. package/dist/core/command-hooks/report.d.ts +11 -2
  75. package/dist/core/command-hooks/report.js +10 -1
  76. package/dist/core/command-hooks/schema.d.ts +16 -1
  77. package/dist/core/command-hooks/schema.js +128 -40
  78. package/dist/core/command-hooks/transport/exec-lifecycle.d.ts +12 -0
  79. package/dist/core/command-hooks/transport/exec-lifecycle.js +152 -0
  80. package/dist/core/config.js +13 -5
  81. package/dist/core/human/feedback-companion.js +1 -1
  82. package/dist/core/keybindings/catalog.d.ts +2 -2
  83. package/dist/core/keybindings/catalog.js +2 -1
  84. package/dist/core/model-routes.js +1 -1
  85. package/dist/core/review/realize.js +1 -1
  86. package/dist/core/runtime/broker/auth-reload.js +4 -1
  87. package/dist/core/runtime/broker/engine-drive.d.ts +14 -3
  88. package/dist/core/runtime/broker/engine-drive.js +29 -1
  89. package/dist/core/runtime/broker/fault-retry.js +7 -2
  90. package/dist/core/runtime/broker/frame-client.js +3 -0
  91. package/dist/core/runtime/broker/frame-dispatch.js +7 -0
  92. package/dist/core/runtime/broker/inbox.d.ts +12 -2
  93. package/dist/core/runtime/broker/inbox.js +68 -38
  94. package/dist/core/runtime/broker/rebind.js +7 -0
  95. package/dist/core/runtime/broker-protocol.d.ts +16 -1
  96. package/dist/core/runtime/broker.d.ts +1 -1
  97. package/dist/core/runtime/broker.js +27 -3
  98. package/dist/core/runtime/canvas-extensions.d.ts +1 -0
  99. package/dist/core/runtime/canvas-extensions.js +2 -0
  100. package/dist/core/runtime/interactive-deliver.d.ts +8 -2
  101. package/dist/core/runtime/interactive-deliver.js +12 -3
  102. package/dist/core/runtime/launch-target.js +1 -1
  103. package/dist/core/runtime/launch.d.ts +12 -6
  104. package/dist/core/runtime/launch.js +19 -17
  105. package/dist/core/runtime/model-swap.d.ts +5 -4
  106. package/dist/core/runtime/model-swap.js +7 -5
  107. package/dist/core/runtime/node-read.d.ts +2 -0
  108. package/dist/core/runtime/node-read.js +18 -9
  109. package/dist/core/runtime/promote.js +2 -1
  110. package/dist/core/runtime/reset.js +4 -2
  111. package/dist/core/runtime/revive-all.d.ts +1 -1
  112. package/dist/core/runtime/revive-all.js +2 -2
  113. package/dist/core/runtime/revive.d.ts +2 -2
  114. package/dist/core/runtime/revive.js +71 -13
  115. package/dist/core/runtime/session-visibility.d.ts +10 -14
  116. package/dist/core/runtime/session-visibility.js +43 -30
  117. package/dist/core/runtime/spawn.d.ts +3 -3
  118. package/dist/core/runtime/spawn.js +20 -3
  119. package/dist/core/runtime/stamp/channel.d.ts +22 -0
  120. package/dist/core/runtime/stamp/channel.js +83 -0
  121. package/dist/core/runtime/stamp/protocol.d.ts +90 -0
  122. package/dist/core/runtime/stamp/protocol.js +113 -0
  123. package/dist/core/runtime/tmux-driver.d.ts +11 -1
  124. package/dist/core/runtime/tmux-driver.js +27 -11
  125. package/dist/core/runtime/turn-visibility.d.ts +10 -0
  126. package/dist/core/runtime/turn-visibility.js +31 -0
  127. package/dist/core/self-update.js +2 -5
  128. package/dist/core/tui/draw.d.ts +1 -0
  129. package/dist/core/tui/draw.js +5 -1
  130. package/dist/core/tui/markdown.d.ts +17 -0
  131. package/dist/core/tui/markdown.js +407 -0
  132. package/dist/core/user-settings.d.ts +4 -0
  133. package/dist/core/user-settings.js +1 -0
  134. package/dist/daemon/api/__tests__/node-model-validation.test.js +21 -0
  135. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +2 -2
  136. package/dist/daemon/api/bridge.d.ts +3 -2
  137. package/dist/daemon/api/bridge.js +34 -9
  138. package/dist/daemon/api/handlers/attach.js +4 -4
  139. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  140. package/dist/daemon/api/handlers/canvas.js +62 -16
  141. package/dist/daemon/api/handlers/messages.js +14 -3
  142. package/dist/daemon/api/handlers/nodes.js +17 -7
  143. package/dist/daemon/api/map.js +2 -0
  144. package/dist/daemon/cron/sinks.js +3 -3
  145. package/dist/daemon/fleet.js +1 -1
  146. package/dist/daemon/messaging/node-message.js +16 -2
  147. package/dist/daemon/profile-delete.js +1 -1
  148. package/dist/daemon/reconcilers/broker-supervision.js +26 -9
  149. package/dist/daemon/reconcilers/live-obligation.d.ts +8 -1
  150. package/dist/daemon/reconcilers/live-obligation.js +17 -0
  151. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +6 -6
  152. package/dist/hook-authoring.d.ts +31 -1
  153. package/dist/hook-authoring.js +42 -5
  154. package/dist/index.d.ts +1 -0
  155. package/dist/index.js +3 -0
  156. package/dist/pi-extensions/canvas-inbox-watcher.js +27 -2
  157. package/dist/pi-extensions/canvas-recap.js +4 -0
  158. package/dist/pi-extensions/canvas-stamp.d.ts +14 -0
  159. package/dist/pi-extensions/canvas-stamp.js +77 -0
  160. package/dist/pi-extensions/canvas-stophook.js +3 -0
  161. package/dist/shared/__tests__/env-boundary.test.js +4 -2
  162. package/dist/shared/crtr-version.d.ts +2 -0
  163. package/dist/shared/crtr-version.js +18 -0
  164. package/dist/shared/env.d.ts +10 -0
  165. package/dist/shared/env.js +19 -4
  166. package/dist/shared/generated-context.d.ts +1 -2
  167. package/dist/shared/generated-context.js +3 -4
  168. package/dist/types.d.ts +10 -0
  169. package/dist/types.js +4 -0
  170. package/package.json +1 -1
  171. package/runtime.lock.json +2 -2
@@ -0,0 +1,67 @@
1
+ // node-recap.ts — the node's last Goal/State briefing, persisted.
2
+ //
3
+ // The recap itself is generated in the broker (`pi-extensions/canvas-recap.ts`)
4
+ // and painted above the editor as throwaway chrome. That widget lives in broker
5
+ // memory, so the moment a node goes dormant its briefing is gone — which is
6
+ // exactly when a developer scanning `crtr canvas browse` needs it most ("what was
7
+ // this one for, and where did it stop?"). Persisting the two lines the viewer
8
+ // already produced makes them readable without a broker and without a model call.
9
+ //
10
+ // Stored beside telemetry in `job/` because it is machine-generated runtime
11
+ // state, not a document the node authored: safe to delete, rebuilt on the node's
12
+ // next idle recap. `context/` stays the agent's own document space.
13
+ import { join } from 'node:path';
14
+ import { mkdirSync } from 'node:fs';
15
+ import { jobDir } from './paths.js';
16
+ import { readJsonIfExists, atomicWriteJson } from '../fs-utils.js';
17
+ function recapPath(nodeId) {
18
+ return join(jobDir(nodeId), 'recap.json');
19
+ }
20
+ /** Strip one leading `Goal:` / `State:` label from a briefing line. */
21
+ function stripLabel(line) {
22
+ return line.replace(/^\s*(goal|state)\s*:\s*/i, '').trim();
23
+ }
24
+ /** Split the model's two-line briefing into its labelled halves. Returns null for
25
+ * anything that doesn't yield at least one non-empty line — a garbled recap is
26
+ * worse than none, so it is never stored. Missing labels fall back to line order
27
+ * (the prompt asks for Goal first, State second). */
28
+ export function parseRecapBriefing(briefing) {
29
+ const lines = briefing.split('\n').map((l) => l.trim()).filter((l) => l !== '');
30
+ if (lines.length === 0)
31
+ return null;
32
+ const labelled = (prefix) => lines.find((l) => new RegExp(`^${prefix}\\s*:`, 'i').test(l));
33
+ const goalLine = labelled('goal') ?? lines[0];
34
+ const stateLine = labelled('state') ?? (lines[1] !== goalLine ? lines[1] : undefined);
35
+ const goal = goalLine === undefined ? '' : stripLabel(goalLine);
36
+ const state = stateLine === undefined ? '' : stripLabel(stateLine);
37
+ if (goal === '' && state === '')
38
+ return null;
39
+ return { goal, state };
40
+ }
41
+ /** Persist a freshly generated briefing. Best-effort — a write failure only costs
42
+ * the browse preview one stale panel, so it must never disturb the broker. */
43
+ export function writeNodeRecap(nodeId, briefing) {
44
+ const parsed = parseRecapBriefing(briefing);
45
+ if (parsed === null)
46
+ return;
47
+ try {
48
+ mkdirSync(jobDir(nodeId), { recursive: true });
49
+ atomicWriteJson(recapPath(nodeId), { ...parsed, at: new Date().toISOString() });
50
+ }
51
+ catch {
52
+ /* best-effort: the recap is regenerated on the node's next idle window */
53
+ }
54
+ }
55
+ /** The node's stored briefing, or undefined when it never generated one (a node
56
+ * that finished before its first idle window, or one last run before this file
57
+ * existed). Never throws. */
58
+ export function readNodeRecap(nodeId) {
59
+ const raw = readJsonIfExists(recapPath(nodeId));
60
+ if (raw === null || typeof raw !== 'object')
61
+ return undefined;
62
+ const goal = typeof raw.goal === 'string' ? raw.goal : '';
63
+ const state = typeof raw.state === 'string' ? raw.state : '';
64
+ if (goal === '' && state === '')
65
+ return undefined;
66
+ return { goal, state, at: typeof raw.at === 'string' ? raw.at : '' };
67
+ }
@@ -1,4 +1,5 @@
1
1
  import type { CanvasSource } from './source.js';
2
+ import { type NodeRecap } from './node-recap.js';
2
3
  import { type Fault } from '../runtime/fault.js';
3
4
  import type { NodeStatus, Lifecycle, NodeMeta, NodeRow, TerminalReason } from './types.js';
4
5
  export declare function activeFaultForDisplay<T extends {
@@ -69,6 +70,15 @@ export interface DashboardRow {
69
70
  * by loadPreview (same single session read as `prompts`); undefined for a node whose
70
71
  * session has no assistant reply yet. Shown in the preview's reply block. */
71
72
  lastAssistant?: string;
73
+ /** The node's last persisted Goal/State briefing (`job/recap.json`, written by the
74
+ * broker's recap extension). Populated LAZILY by loadPreview; undefined for a node
75
+ * that never generated one. The preview's fastest "what is this / where is it" line. */
76
+ recap?: NodeRecap;
77
+ /** The newest report the node pushed (`reports/<ts>-<id>-<kind>.md`). Populated
78
+ * LAZILY by loadPreview; undefined when the node has pushed nothing. Residents push
79
+ * updates as they work, so this is the last thing the node deliberately DELIVERED —
80
+ * usually crisper than its last assistant message. */
81
+ report?: NodeReportPreview;
72
82
  /** True when the node is GENUINELY mid-turn right now — its `busy` marker exists
73
83
  * AND its broker pid is alive (not merely an `active`, between-turns node). The
74
84
  * live "is it generating?" cue. Computed on the cheap boot path for LIVE nodes
@@ -93,8 +103,30 @@ export interface DashboardRow {
93
103
  /** Lazy-enrichment guard (internal): true once loadPreview has read goal + the
94
104
  * session (prompts + lastAssistant). Idempotency marker for the selected row. */
95
105
  previewLoaded?: boolean;
106
+ /** The source `NodeRow` this row was built from (dashboardRowFromNodeRow only).
107
+ * Carries every column listNodes already fetched — the runtime fields
108
+ * (host_kind/parent/waiting_for/final_report/finalized_at/pi_pid) plus the
109
+ * mirrored identity fields (description/cycles) — so the snapshot handler
110
+ * reads them here instead of opening meta.json.
111
+ * Undefined for rows built by other paths (dashboardRowsFromSource). */
112
+ srcRow?: NodeRow;
96
113
  }
97
114
  export declare function readGoalText(nodeId: string): string | undefined;
115
+ /** The newest pushed report, reduced to what a preview panel can use. */
116
+ export interface NodeReportPreview {
117
+ /** final | update | urgent (frontmatter, else the filename suffix). */
118
+ kind: string;
119
+ /** ISO 8601 push time (frontmatter, else the filename stamp). */
120
+ ts: string;
121
+ /** The report's first markdown heading, or its first non-empty line. */
122
+ heading: string;
123
+ /** The body after that heading, whitespace-normalized + capped. */
124
+ body: string;
125
+ }
126
+ /** The node's newest report, read straight off disk. Report filenames lead with a
127
+ * compact timestamp, so the lexicographic maximum is the latest one — no stat of
128
+ * the whole directory. Never throws; undefined when the node pushed nothing. */
129
+ export declare function readNewestReport(nodeId: string): NodeReportPreview | undefined;
98
130
  interface SessionParts {
99
131
  prompts?: string;
100
132
  lastAssistant?: string;
@@ -138,7 +170,7 @@ export declare function dashboardRowsFromSource(source: CanvasSource, rootId: st
138
170
  * coupling the canvas source to local viewer state. Remote sources ignore that
139
171
  * set. Local disk telemetry/fault/streaming are likewise suppressed remotely. */
140
172
  export declare function dashboardRowsAllFromSource(source: CanvasSource, localFocusedNodeIds?: ReadonlySet<string>, liveOnly?: boolean): Promise<DashboardRow[]>;
141
- export declare function enrichRowsFromSource(source: CanvasSource, rows: DashboardRow[], asks?: Record<string, number>): Promise<void>;
173
+ export declare function enrichRowsFromSource(_source: CanvasSource, rows: DashboardRow[], asks?: Record<string, number>): Promise<void>;
142
174
  /** goal (initial-prompt.md) and session parts are local disk reads — suppressed
143
175
  * to undefined for a remote source. */
144
176
  export declare function loadPreviewFromSource(source: CanvasSource, row: DashboardRow): Promise<DashboardRow>;
@@ -9,14 +9,16 @@
9
9
  // migrated onto these `*FromSource` variants directly.
10
10
  //
11
11
  // ALLOWED imports (verified openDb-free): node:fs/path, ./pid.js, ./labels.js,
12
- // ./status-glyph.js, ../runtime/fault.js, ./paths.js, ./remote-canvas-source.js,
13
- // and TYPE-ONLY ./source.js + ./types.js. Do NOT import ./canvas.js, ./attention.js,
12
+ // ./status-glyph.js, ./node-recap.js, ../runtime/fault.js, ../frontmatter.js,
13
+ // ./paths.js, ./remote-canvas-source.js, and TYPE-ONLY ./source.js + ./types.js. Do NOT import ./canvas.js, ./attention.js,
14
14
  // ./focuses.js, or ./db.js here — that is the whole point of the split.
15
- import { existsSync, readFileSync, statSync } from 'node:fs';
15
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
16
16
  import { join } from 'node:path';
17
17
  import { RemoteCanvasSource } from './remote-canvas-source.js';
18
18
  import { fullName } from './labels.js';
19
- import { jobDir, contextDir } from './paths.js';
19
+ import { jobDir, contextDir, reportsDir } from './paths.js';
20
+ import { readNodeRecap } from './node-recap.js';
21
+ import { parseFrontmatterGeneric } from '../frontmatter.js';
20
22
  import { isPidAlive } from './pid.js';
21
23
  import { isFrozenRow, resolveNodeVisual, faultSummary } from './status-glyph.js';
22
24
  import { readFault } from '../runtime/fault.js';
@@ -168,6 +170,63 @@ export function readGoalText(nodeId) {
168
170
  return undefined;
169
171
  }
170
172
  }
173
+ const REPORT_BODY_CAP = 1024;
174
+ /** Compact report stamp `20260810T190533286` → an ISO 8601 instant. */
175
+ function reportStampToIso(stamp) {
176
+ const m = stamp.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(\d{3})?$/);
177
+ if (m === null)
178
+ return undefined;
179
+ return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}.${m[7] ?? '000'}Z`;
180
+ }
181
+ /** The node's newest report, read straight off disk. Report filenames lead with a
182
+ * compact timestamp, so the lexicographic maximum is the latest one — no stat of
183
+ * the whole directory. Never throws; undefined when the node pushed nothing. */
184
+ export function readNewestReport(nodeId) {
185
+ let newest;
186
+ try {
187
+ for (const f of readdirSync(reportsDir(nodeId))) {
188
+ if (!f.endsWith('.md'))
189
+ continue;
190
+ if (newest === undefined || f > newest)
191
+ newest = f;
192
+ }
193
+ }
194
+ catch {
195
+ return undefined; // no reports dir
196
+ }
197
+ if (newest === undefined)
198
+ return undefined;
199
+ let raw;
200
+ try {
201
+ raw = readFileSync(join(reportsDir(nodeId), newest), 'utf8');
202
+ }
203
+ catch {
204
+ return undefined;
205
+ }
206
+ const { data, body } = parseFrontmatterGeneric(raw);
207
+ const name = newest.replace(/\.md$/, '');
208
+ const kind = typeof data?.['kind'] === 'string'
209
+ ? data['kind']
210
+ : (name.split('-').pop() ?? 'report');
211
+ const ts = typeof data?.['ts'] === 'string'
212
+ ? data['ts']
213
+ : (reportStampToIso(name.split('-')[0] ?? '') ?? '');
214
+ // The heading is the report's own headline; everything after it is the detail the
215
+ // preview shows next, so a panel never repeats the heading twice.
216
+ const lines = body.split('\n');
217
+ let heading = '';
218
+ let rest = body;
219
+ for (let i = 0; i < lines.length; i++) {
220
+ const t = (lines[i] ?? '').trim();
221
+ if (t === '')
222
+ continue;
223
+ heading = t.startsWith('#') ? t.replace(/^#+\s*/, '').trim() : t;
224
+ rest = lines.slice(i + 1).join('\n');
225
+ break;
226
+ }
227
+ const text = rest.replace(/\s+/g, ' ').trim();
228
+ return { kind, ts, heading, body: text.length > REPORT_BODY_CAP ? text.slice(0, REPORT_BODY_CAP) : text };
229
+ }
171
230
  /** The node's session-derived preview text in ONE file read (today's two reads,
172
231
  * folded): EVERY user prompt across the pi session (`prompts` — the whole
173
232
  * conversation, not just the spawn prompt) AND the LAST assistant message text
@@ -385,8 +444,9 @@ export function dashboardRowFromNodeRow(row, focusedNodeIds, remote) {
385
444
  const hanging = remote ? null : activeFaultForDisplay(row, readFault(row.node_id));
386
445
  return {
387
446
  node_id: row.node_id,
388
- name: row.name, // handle only; enrichRow upgrades to fullName (meta.description)
447
+ name: fullName(row), // description is a row column (v38) — no meta read needed
389
448
  handle: row.name,
449
+ description: row.description,
390
450
  status: row.status,
391
451
  frozen_at: row.frozen_at ?? null,
392
452
  terminal_reason: row.terminal_reason ?? null,
@@ -403,6 +463,7 @@ export function dashboardRowFromNodeRow(row, focusedNodeIds, remote) {
403
463
  streaming: !remote && hanging === null && live ? isStreaming(row.node_id, row.pi_pid) : false,
404
464
  hanging,
405
465
  viewed: remote ? false : focusedNodeIds.has(row.node_id) || hasLiveBrokerViewer(row),
466
+ srcRow: row,
406
467
  };
407
468
  }
408
469
  /** One row per node visible in the sub-DAG of `rootId` (including root), read
@@ -449,28 +510,23 @@ export async function dashboardRowsAllFromSource(source, localFocusedNodeIds = n
449
510
  const rows = await source.listNodes(liveOnly ? { status: ['active', 'idle'] } : undefined);
450
511
  return rows.map((row) => dashboardRowFromNodeRow(row, focusedNodeIds, remote));
451
512
  }
452
- // Lazy enrichment (source-driven) — fold expensive per-node reads into rows on
453
- // demand. Ask counts are supplied by the caller (`asks` — a pre-fetched
454
- // `ticketCountsForView` map merged across the visible roots), NOT scanned here
455
- // (`ticketCountsForNodes` reaches openDb). Telemetry (ctx tokens) + goal/session
456
- // are LOCAL disk reads, suppressed for a `RemoteCanvasSource`.
457
- export async function enrichRowsFromSource(source, rows, asks) {
513
+ // Lazy enrichment (source-driven) — fold the per-node reads the cheap boot skips
514
+ // into rows on demand. Ask counts are supplied by the caller (`asks` — a
515
+ // pre-fetched `ticketCountsForView` map merged across the visible roots), NOT
516
+ // scanned here (`ticketCountsForNodes` reaches openDb). Telemetry (ctx tokens) is
517
+ // a LOCAL disk read, suppressed for a `RemoteCanvasSource`.
518
+ //
519
+ // This does NO source read. The label fields it used to re-fetch per node
520
+ // (name/handle/description) are set by the row builders: `description` became a
521
+ // nodes-table column in v38, so both builders already hold the full label and a
522
+ // per-node getNode here would re-read what the caller's single listNodes returned.
523
+ export async function enrichRowsFromSource(_source, rows, asks) {
458
524
  const todo = rows.filter((r) => r.enriched !== true);
459
525
  if (todo.length === 0)
460
526
  return;
461
- const remote = source instanceof RemoteCanvasSource;
527
+ const remote = _source instanceof RemoteCanvasSource;
462
528
  const askMap = asks ?? {};
463
- // A full dashboard may contain thousands of rows. Keep source reads serial:
464
- // ApiCanvasSource turns each into a unix-socket request, and an unbounded
465
- // Promise.all can overflow crtrd's accept backlog then spuriously trigger
466
- // the client's cold-daemon path even though the daemon is still running.
467
529
  for (const row of todo) {
468
- const meta = await source.getNode(row.node_id);
469
- if (meta !== null) {
470
- row.name = fullName(meta);
471
- row.handle = meta.name;
472
- row.description = meta.description;
473
- }
474
530
  row.ctx_tokens = remote ? 0 : (readNodeTelemetry(row.node_id).tokens_in ?? 0);
475
531
  row.asks = askMap[row.node_id] ?? 0;
476
532
  row.enriched = true;
@@ -488,6 +544,8 @@ export async function loadPreviewFromSource(source, row) {
488
544
  const { prompts, lastAssistant } = readSessionParts(meta?.pi_session_file);
489
545
  row.prompts = prompts;
490
546
  row.lastAssistant = lastAssistant;
547
+ row.recap = readNodeRecap(row.node_id);
548
+ row.report = readNewestReport(row.node_id);
491
549
  }
492
550
  row.previewLoaded = true;
493
551
  return row;
@@ -292,6 +292,15 @@ export type NodeMeta = NodeIdentity & NodeRuntime;
292
292
  export interface NodeRow {
293
293
  node_id: string;
294
294
  name: string;
295
+ /** The pi-generated first-task description; see NodeIdentity.description.
296
+ * Durable IDENTITY mirrored to the row (like `cwd`/`host_kind`) so a roster
297
+ * read can build the full display label without opening meta.json. Absent for
298
+ * a node not yet named — the column's NULL is mapped to undefined at `rowFrom`
299
+ * so identity, row, and DTO all spell "unnamed" the same way. */
300
+ description?: string;
301
+ /** Relaunch count; see NodeIdentity.cycles. Mirrored to the row for the same
302
+ * reason as `description`; absent on a row written before this column. */
303
+ cycles?: number;
295
304
  kind: string;
296
305
  mode: Mode;
297
306
  lifecycle: Lifecycle;
@@ -1,6 +1,7 @@
1
1
  import type { InstalledPlugin } from '../../types.js';
2
2
  import type { CoreHookCatalog } from './catalog.js';
3
- import type { HookManifestIssue, HookPhase, ValidatedHookManifest } from './schema.js';
3
+ import type { HookManifestIssue, HookPhase, LifecycleHookPhase, ValidatedHookManifest } from './schema.js';
4
+ import type { LifecycleEvent } from './lifecycle-catalog.js';
4
5
  export type HookDiscoveryIssueCode = HookManifestIssue['code'] | 'command_hook_target_invalid' | 'command_hook_collision';
5
6
  export interface HookDiscoveryIssue {
6
7
  code: HookDiscoveryIssueCode;
@@ -38,6 +39,27 @@ export interface EffectiveHookPlan {
38
39
  after: readonly EffectiveHook[];
39
40
  replacements: readonly EffectiveHook[];
40
41
  }
42
+ /** One lifecycle declaration resolved to the local executable that receives it. */
43
+ export interface EffectiveLifecycleHook {
44
+ executable: string;
45
+ plugin: HookPluginAttribution;
46
+ event: LifecycleEvent;
47
+ phase: LifecycleHookPhase;
48
+ op: string;
49
+ description: string;
50
+ effects: readonly string[];
51
+ /** Index in this plugin's lifecycle declaration array. */
52
+ declarationIndex: number;
53
+ /** Stable discovery order: scope, lexical plugin name, declaration index. */
54
+ order: number;
55
+ }
56
+ export interface LifecycleHookRegistry {
57
+ hooks: readonly EffectiveLifecycleHook[];
58
+ plans: ReadonlyMap<LifecycleEvent, readonly EffectiveLifecycleHook[]>;
59
+ validations: readonly HookPluginValidation[];
60
+ issues: readonly HookDiscoveryIssue[];
61
+ valid: boolean;
62
+ }
41
63
  export interface HookPluginValidation {
42
64
  plugin: InstalledPlugin;
43
65
  manifestPath?: string;
@@ -74,6 +96,10 @@ export interface HookPluginCandidate {
74
96
  * manifest files to validate them but never executes a plugin executable.
75
97
  */
76
98
  export declare function compileHookRegistry(catalog: CoreHookCatalog, plugins: readonly InstalledPlugin[]): HookRegistry;
99
+ /** Compiles lifecycle declarations from resolved plugins without loading the command tree. */
100
+ export declare function compileLifecycleHookRegistry(plugins: readonly InstalledPlugin[]): LifecycleHookRegistry;
101
+ /** Discovers lifecycle hooks for one node's resolved cwd and profile. */
102
+ export declare function discoverLifecycleHookRegistry(startDir: string, profileId?: string | null): LifecycleHookRegistry;
77
103
  /** Discovers and compiles the invocation-local effective registry. */
78
104
  export declare function discoverHookRegistry(catalog: CoreHookCatalog, startDir?: string, profileId?: string | null): HookRegistry;
79
105
  /** Compile the exact effective set a source candidate would create before its
@@ -153,6 +153,58 @@ export function compileHookRegistry(catalog, plugins) {
153
153
  valid: issues.length === 0,
154
154
  });
155
155
  }
156
+ /** Compiles lifecycle declarations from resolved plugins without loading the command tree. */
157
+ export function compileLifecycleHookRegistry(plugins) {
158
+ const validations = [];
159
+ const hooks = [];
160
+ const issues = [];
161
+ let order = 0;
162
+ for (const plugin of plugins) {
163
+ const artifact = validatePluginHookArtifact(plugin.root, plugin.manifest);
164
+ const pluginIssues = artifactIssues(plugin, artifact.issues);
165
+ let compiled = [];
166
+ if (pluginIssues.length === 0 && artifact.manifest !== undefined && artifact.executablePath !== undefined) {
167
+ const lifecycle = artifact.manifest.schemaVersion === 2 ? artifact.manifest.lifecycle : [];
168
+ compiled = lifecycle.map((hook, declarationIndex) => Object.freeze({
169
+ executable: artifact.executablePath,
170
+ plugin: Object.freeze({ name: plugin.name, scope: plugin.scope, root: plugin.root }),
171
+ event: hook.event,
172
+ phase: hook.phase,
173
+ op: hook.op,
174
+ description: hook.description,
175
+ effects: Object.freeze([...hook.effects]),
176
+ declarationIndex,
177
+ order: order++,
178
+ }));
179
+ hooks.push(...compiled);
180
+ }
181
+ validations.push(Object.freeze({
182
+ plugin,
183
+ ...(artifact.manifestPath === undefined ? {} : { manifestPath: artifact.manifestPath }),
184
+ ...(artifact.executablePath === undefined ? {} : { executablePath: artifact.executablePath }),
185
+ ...(artifact.manifest === undefined ? {} : { manifest: artifact.manifest }),
186
+ hooks: Object.freeze([]),
187
+ issues: Object.freeze(pluginIssues),
188
+ }));
189
+ issues.push(...pluginIssues);
190
+ }
191
+ const plans = new Map();
192
+ for (const hook of hooks) {
193
+ const entries = plans.get(hook.event) ?? [];
194
+ plans.set(hook.event, Object.freeze([...entries, hook]));
195
+ }
196
+ return Object.freeze({
197
+ hooks: Object.freeze(hooks),
198
+ plans,
199
+ validations: Object.freeze(validations),
200
+ issues: Object.freeze(issues),
201
+ valid: issues.length === 0,
202
+ });
203
+ }
204
+ /** Discovers lifecycle hooks for one node's resolved cwd and profile. */
205
+ export function discoverLifecycleHookRegistry(startDir, profileId) {
206
+ return compileLifecycleHookRegistry(effectiveHookPlugins(startDir, profileId));
207
+ }
156
208
  /** Discovers and compiles the invocation-local effective registry. */
157
209
  export function discoverHookRegistry(catalog, startDir = process.cwd(), profileId) {
158
210
  return compileHookRegistry(catalog, effectiveHookPlugins(startDir, profileId));
@@ -1,6 +1,7 @@
1
1
  export { createCoreHookCatalog, type CoreHookCatalog, type CoreHookTarget } from './catalog.js';
2
2
  export { projectEffectiveLeafHelp } from './help.js';
3
- export { compileHookRegistry, discoverCandidateHookRegistry, discoverHookRegistry, discoverHookRegistryInScopes, effectiveHookPlugins, hookDiscoveryScopes, type EffectiveHook, type EffectiveHookPlan, type HookDiscoveryIssue, type HookPluginAttribution, type HookPluginValidation, type HookRegistry, type HookScope, } from './discovery.js';
3
+ export { compileHookRegistry, compileLifecycleHookRegistry, discoverCandidateHookRegistry, discoverHookRegistry, discoverHookRegistryInScopes, discoverLifecycleHookRegistry, effectiveHookPlugins, hookDiscoveryScopes, type EffectiveHook, type EffectiveHookPlan, type EffectiveLifecycleHook, type HookDiscoveryIssue, type HookPluginAttribution, type HookPluginValidation, type HookRegistry, type HookScope, type LifecycleHookRegistry, } from './discovery.js';
4
4
  export { validatePluginHookArtifact, type HookArtifactValidation } from './artifact.js';
5
- export { hasDeclaredHooks, hookReport, HOOK_TRUST_WARNING, type HookDeclarationReport, type HookReport } from './report.js';
6
- export { validateHookManifest, type DeclaredHook, type HookManifestIssue, type HookManifestValidation, type HookPhase, type ValidatedHookManifest, } from './schema.js';
5
+ export { hasDeclaredHooks, hookReport, HOOK_TRUST_WARNING, type HookDeclarationReport, type HookReport, type LifecycleHookDeclarationReport } from './report.js';
6
+ export { validateHookManifest, type DeclaredHook, type DeclaredLifecycleHook, type HookManifestIssue, type HookManifestValidation, type HookPhase, type LifecycleHookPhase, type ValidatedHookManifest, } from './schema.js';
7
+ export { LIFECYCLE_EVENTS, isLifecycleEvent, type LifecycleEvent } from './lifecycle-catalog.js';
@@ -1,6 +1,7 @@
1
1
  export { createCoreHookCatalog } from './catalog.js';
2
2
  export { projectEffectiveLeafHelp } from './help.js';
3
- export { compileHookRegistry, discoverCandidateHookRegistry, discoverHookRegistry, discoverHookRegistryInScopes, effectiveHookPlugins, hookDiscoveryScopes, } from './discovery.js';
3
+ export { compileHookRegistry, compileLifecycleHookRegistry, discoverCandidateHookRegistry, discoverHookRegistry, discoverHookRegistryInScopes, discoverLifecycleHookRegistry, effectiveHookPlugins, hookDiscoveryScopes, } from './discovery.js';
4
4
  export { validatePluginHookArtifact } from './artifact.js';
5
5
  export { hasDeclaredHooks, hookReport, HOOK_TRUST_WARNING } from './report.js';
6
6
  export { validateHookManifest, } from './schema.js';
7
+ export { LIFECYCLE_EVENTS, isLifecycleEvent } from './lifecycle-catalog.js';
@@ -0,0 +1,3 @@
1
+ export declare const LIFECYCLE_EVENTS: readonly ["node:start"];
2
+ export type LifecycleEvent = typeof LIFECYCLE_EVENTS[number];
3
+ export declare function isLifecycleEvent(value: string): value is LifecycleEvent;
@@ -0,0 +1,4 @@
1
+ export const LIFECYCLE_EVENTS = ['node:start'];
2
+ export function isLifecycleEvent(value) {
3
+ return LIFECYCLE_EVENTS.includes(value);
4
+ }
@@ -1,7 +1,7 @@
1
1
  import type { PluginManifest } from '../../types.js';
2
2
  import type { HookDiscoveryIssue, HookPluginValidation } from './discovery.js';
3
- import type { DeclaredHook } from './schema.js';
4
- export declare const HOOK_TRUST_WARNING = "Hooks receive normalized inputs; after hooks receive results. They run implicitly and may block or replace declared targets. The local executable runs with caller authority and can transmit received data.";
3
+ import type { DeclaredHook, DeclaredLifecycleHook } from './schema.js';
4
+ export declare const HOOK_TRUST_WARNING = "Command hooks receive normalized inputs and may block or replace declared targets. Lifecycle hooks run unprompted at every declared event before node launch. Both run local executables with runtime authority and may transmit received data.";
5
5
  export interface HookDeclarationReport {
6
6
  target: string;
7
7
  phase: DeclaredHook['phase'];
@@ -9,6 +9,13 @@ export interface HookDeclarationReport {
9
9
  description: string;
10
10
  effects: readonly string[];
11
11
  }
12
+ export interface LifecycleHookDeclarationReport {
13
+ event: DeclaredLifecycleHook['event'];
14
+ phase: DeclaredLifecycleHook['phase'];
15
+ op: string;
16
+ description: string;
17
+ effects: readonly string[];
18
+ }
12
19
  /** One static hook artifact report shared by plugin inspection and lifecycle
13
20
  * output. It only reads declarations and filesystem metadata; it never runs
14
21
  * the hook executable. */
@@ -16,6 +23,8 @@ export interface HookReport {
16
23
  manifestPath?: string;
17
24
  executablePath?: string;
18
25
  declarations: readonly HookDeclarationReport[];
26
+ /** Present only for schemaVersion 2 manifests. */
27
+ lifecycle?: readonly LifecycleHookDeclarationReport[];
19
28
  issues: readonly HookDiscoveryIssue[];
20
29
  trust: typeof HOOK_TRUST_WARNING;
21
30
  }
@@ -1,4 +1,4 @@
1
- export const HOOK_TRUST_WARNING = 'Hooks receive normalized inputs; after hooks receive results. They run implicitly and may block or replace declared targets. The local executable runs with caller authority and can transmit received data.';
1
+ export const HOOK_TRUST_WARNING = 'Command hooks receive normalized inputs and may block or replace declared targets. Lifecycle hooks run unprompted at every declared event before node launch. Both run local executables with runtime authority and may transmit received data.';
2
2
  export function hasDeclaredHooks(manifest) {
3
3
  return manifest.hooks !== undefined || manifest.hookExecutable !== undefined;
4
4
  }
@@ -13,6 +13,15 @@ export function hookReport(validation, issues = validation.issues) {
13
13
  description: hook.description,
14
14
  effects: Object.freeze([...hook.effects]),
15
15
  }))),
16
+ ...(validation.manifest?.schemaVersion === 2 ? {
17
+ lifecycle: Object.freeze(validation.manifest.lifecycle.map((hook) => Object.freeze({
18
+ event: hook.event,
19
+ phase: hook.phase,
20
+ op: hook.op,
21
+ description: hook.description,
22
+ effects: Object.freeze([...hook.effects]),
23
+ }))),
24
+ } : {}),
16
25
  issues: Object.freeze([...issues]),
17
26
  trust: HOOK_TRUST_WARNING,
18
27
  });
@@ -1,4 +1,6 @@
1
+ import { type LifecycleEvent } from './lifecycle-catalog.js';
1
2
  export type HookPhase = 'before' | 'after' | 'replace';
3
+ export type LifecycleHookPhase = 'on';
2
4
  export interface DeclaredHook {
3
5
  target: string;
4
6
  phase: HookPhase;
@@ -6,10 +8,23 @@ export interface DeclaredHook {
6
8
  description: string;
7
9
  effects: string[];
8
10
  }
9
- export interface ValidatedHookManifest {
11
+ export interface DeclaredLifecycleHook {
12
+ event: LifecycleEvent;
13
+ phase: LifecycleHookPhase;
14
+ op: string;
15
+ description: string;
16
+ effects: string[];
17
+ }
18
+ export interface ValidatedV1HookManifest {
10
19
  schemaVersion: 1;
11
20
  hooks: DeclaredHook[];
12
21
  }
22
+ export interface ValidatedV2HookManifest {
23
+ schemaVersion: 2;
24
+ hooks: DeclaredHook[];
25
+ lifecycle: DeclaredLifecycleHook[];
26
+ }
27
+ export type ValidatedHookManifest = ValidatedV1HookManifest | ValidatedV2HookManifest;
13
28
  export type HookIssueCode = 'hook_manifest_unreadable' | 'hook_manifest_invalid' | 'hook_schema_version' | 'hook_path_unsafe' | 'hook_not_executable';
14
29
  export interface HookManifestIssue {
15
30
  code: HookIssueCode;