@north-light/crouter 0.3.175 → 0.3.177

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 (185) hide show
  1. package/dist/api/client.d.ts +4 -4
  2. package/dist/api/client.js +2 -2
  3. package/dist/api/dto/lifecycle.d.ts +1 -1
  4. package/dist/api/dto/nodes.d.ts +7 -1
  5. package/dist/api/dto/profiles.d.ts +3 -0
  6. package/dist/api/dto/reports.d.ts +1 -1
  7. package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
  8. package/dist/clients/attach/config.js +5 -5
  9. package/dist/clients/attach/input/clipboard-image.js +2 -2
  10. package/dist/clients/attach/input/ref-autocomplete.js +1 -1
  11. package/dist/clients/attach/input/titled-editor.d.ts +12 -13
  12. package/dist/clients/attach/input/titled-editor.js +14 -15
  13. package/dist/clients/attach/overlays/dialogs.d.ts +1 -1
  14. package/dist/clients/attach/overlays/dialogs.js +2 -2
  15. package/dist/clients/attach/overlays/mcp.js +5 -12
  16. package/dist/clients/attach/render/chat-view.d.ts +1 -1
  17. package/dist/clients/attach/render/chat-view.js +2 -2
  18. package/dist/clients/attach/render/crtr-output.d.ts +3 -3
  19. package/dist/clients/attach/render/crtr-output.js +25 -13
  20. package/dist/clients/attach/render/group-recap.js +7 -0
  21. package/dist/clients/attach/session/context.js +3 -3
  22. package/dist/clients/attach/session/editor-inventory.js +6 -6
  23. package/dist/clients/attach/session/input-wiring.js +1 -1
  24. package/dist/clients/attach/session/teardown.js +7 -9
  25. package/dist/clients/attach/slash/dispatch.d.ts +1 -2
  26. package/dist/clients/attach/slash/dispatch.js +4 -8
  27. package/dist/clients/attach/viewer.js +536 -566
  28. package/dist/clients/inbox/controller.js +3 -2
  29. package/dist/clients/inbox/deck-adapter.js +3 -2
  30. package/dist/clients/surfaces/host.d.ts +1 -1
  31. package/dist/commands/api-client.d.ts +12 -13
  32. package/dist/commands/api-client.js +15 -16
  33. package/dist/commands/chord.js +1 -2
  34. package/dist/commands/human/queue.js +6 -5
  35. package/dist/commands/memory/find.js +3 -3
  36. package/dist/commands/node/create.js +7 -3
  37. package/dist/commands/node/lifecycle.js +1 -1
  38. package/dist/commands/node-lifecycle-revive.d.ts +1 -1
  39. package/dist/commands/node-lifecycle-revive.js +3 -3
  40. package/dist/commands/profile/list.js +3 -1
  41. package/dist/commands/profile/new.js +4 -1
  42. package/dist/commands/profile/project.js +23 -9
  43. package/dist/commands/profile/show.js +3 -1
  44. package/dist/commands/profile.js +2 -2
  45. package/dist/commands/push.js +1 -1
  46. package/dist/commands/surface/node/placement.d.ts +1 -1
  47. package/dist/commands/surface/node/placement.js +1 -1
  48. package/dist/commands/surface-edit.js +1 -4
  49. package/dist/commands/sys/logs.js +4 -2
  50. package/dist/commands/sys/panels/panel.d.ts +3 -3
  51. package/dist/commands/sys/panels/panel.js +3 -3
  52. package/dist/commands/sys/setup-core.js +1 -7
  53. package/dist/commands/sys/sync-deps.js +1 -3
  54. package/dist/commands/sys/sync-project-guidance.js +2 -11
  55. package/dist/commands/sys/sysprompt.js +1 -3
  56. package/dist/core/__tests__/branded-host-concurrency.test.js +1 -1
  57. package/dist/core/__tests__/fixtures/fake-engine.js +1 -1
  58. package/dist/core/__tests__/helpers/harness.d.ts +1 -1
  59. package/dist/core/__tests__/helpers/harness.js +1 -1
  60. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +1 -1
  61. package/dist/core/__tests__/on-read-dedup-resume.test.js +1 -1
  62. package/dist/core/canvas/boot.js +1 -1
  63. package/dist/core/canvas/browse/app.js +2 -2
  64. package/dist/core/canvas/canvas.d.ts +3 -3
  65. package/dist/core/canvas/canvas.js +3 -3
  66. package/dist/core/canvas/daemon-owner.js +1 -2
  67. package/dist/core/canvas/db.js +7 -8
  68. package/dist/core/canvas/extensions.d.ts +1 -1
  69. package/dist/core/canvas/extensions.js +7 -1
  70. package/dist/core/canvas/index.js +2 -2
  71. package/dist/core/canvas/pid.d.ts +12 -14
  72. package/dist/core/canvas/pid.js +16 -20
  73. package/dist/core/canvas/remote-transport.js +2 -2
  74. package/dist/core/canvas/status-glyph.d.ts +1 -1
  75. package/dist/core/canvas/status-glyph.js +1 -1
  76. package/dist/core/canvas/types.d.ts +1 -1
  77. package/dist/core/command-manifests/manifest.js +4 -6
  78. package/dist/core/command-manifests/schema.js +10 -12
  79. package/dist/core/command-plugins/transport/exec-invoke.js +5 -7
  80. package/dist/core/command-plugins/transport/http-invoke.js +14 -8
  81. package/dist/core/command.js +3 -1
  82. package/dist/core/config.d.ts +1 -1
  83. package/dist/core/config.js +6 -29
  84. package/dist/core/events/read.js +1 -3
  85. package/dist/core/exclusive-lock.d.ts +14 -0
  86. package/dist/core/exclusive-lock.js +124 -0
  87. package/dist/core/fault-classifier.js +1 -3
  88. package/dist/core/fs-utils.d.ts +7 -0
  89. package/dist/core/fs-utils.js +29 -2
  90. package/dist/core/host-exports/export.d.ts +4 -5
  91. package/dist/core/host-exports/export.js +4 -5
  92. package/dist/core/human/claim.js +4 -2
  93. package/dist/core/human/convention.d.ts +0 -11
  94. package/dist/core/human/convention.js +1 -131
  95. package/dist/core/human/scan.js +5 -4
  96. package/dist/core/io.d.ts +23 -0
  97. package/dist/core/io.js +60 -0
  98. package/dist/core/preview-registry.d.ts +10 -5
  99. package/dist/core/preview-registry.js +136 -2537
  100. package/dist/core/profiles/manifest.d.ts +13 -2
  101. package/dist/core/profiles/manifest.js +58 -49
  102. package/dist/core/review/store.d.ts +4 -1
  103. package/dist/core/review/store.js +10 -0
  104. package/dist/core/review/types.d.ts +1 -1
  105. package/dist/core/review/types.js +1 -1
  106. package/dist/core/runtime/branded-host.js +6 -84
  107. package/dist/core/runtime/broker/frame-dispatch.js +4 -4
  108. package/dist/core/runtime/broker-cli.js +1 -1
  109. package/dist/core/runtime/broker-extension-render.js +1 -1
  110. package/dist/core/runtime/broker-protocol.d.ts +9 -10
  111. package/dist/core/runtime/broker-protocol.js +9 -10
  112. package/dist/core/runtime/broker-sdk.d.ts +1 -1
  113. package/dist/core/runtime/broker.js +6 -6
  114. package/dist/core/runtime/canvas-extensions.d.ts +1 -0
  115. package/dist/core/runtime/canvas-extensions.js +2 -0
  116. package/dist/core/runtime/host.js +14 -14
  117. package/dist/core/runtime/launch.d.ts +1 -1
  118. package/dist/core/runtime/launch.js +1 -1
  119. package/dist/core/runtime/lifecycle.js +16 -21
  120. package/dist/core/runtime/node-read.js +2 -3
  121. package/dist/core/runtime/package-health.js +1 -8
  122. package/dist/core/runtime/persona.js +2 -2
  123. package/dist/core/runtime/pi-vendored.d.ts +4 -2
  124. package/dist/core/runtime/pi-vendored.js +5 -16
  125. package/dist/core/runtime/promote.js +1 -1
  126. package/dist/core/runtime/reopen.js +2 -2
  127. package/dist/core/runtime/revive-all.d.ts +3 -3
  128. package/dist/core/runtime/revive-all.js +6 -6
  129. package/dist/core/runtime/revive.js +1 -1
  130. package/dist/core/runtime/spawn.d.ts +1 -2
  131. package/dist/core/runtime/spawn.js +2 -3
  132. package/dist/core/runtime/structured-output.js +4 -6
  133. package/dist/core/runtime/tmux-bindings.js +1 -1
  134. package/dist/core/runtime/tmux-driver.d.ts +2 -2
  135. package/dist/core/runtime/tmux-driver.js +3 -5
  136. package/dist/core/runtime/tool-group-summary.js +5 -4
  137. package/dist/core/runtime/warm-pool.d.ts +9 -8
  138. package/dist/core/runtime/warm-pool.js +58 -62
  139. package/dist/core/scope.js +2 -9
  140. package/dist/core/self-update.js +3 -3
  141. package/dist/core/spawn.d.ts +0 -1
  142. package/dist/core/spawn.js +1 -4
  143. package/dist/core/substrate/index.d.ts +2 -1
  144. package/dist/core/substrate/index.js +2 -1
  145. package/dist/core/substrate/on-read-node.d.ts +7 -0
  146. package/dist/core/substrate/on-read-node.js +37 -0
  147. package/dist/core/substrate/on-read.d.ts +7 -5
  148. package/dist/core/substrate/on-read.js +16 -32
  149. package/dist/core/termrender/termrender.js +16 -96
  150. package/dist/core/wake.js +2 -2
  151. package/dist/core/worktree.d.ts +3 -3
  152. package/dist/core/worktree.js +16 -107
  153. package/dist/daemon/api/bridge.js +1 -1
  154. package/dist/daemon/api/handlers/broker-ops.js +1 -2
  155. package/dist/daemon/api/handlers/messages.js +2 -2
  156. package/dist/daemon/api/handlers/nodes.js +33 -10
  157. package/dist/daemon/api/handlers/reports.js +1 -1
  158. package/dist/daemon/api/handlers/review-comments.js +5 -44
  159. package/dist/daemon/api/handlers/reviews.js +5 -53
  160. package/dist/daemon/api/handlers/validate.d.ts +23 -0
  161. package/dist/daemon/api/handlers/validate.js +58 -0
  162. package/dist/daemon/api/map.js +1 -0
  163. package/dist/daemon/cron-run.js +7 -7
  164. package/dist/daemon/human/finish.js +3 -2
  165. package/dist/daemon/manage.d.ts +6 -6
  166. package/dist/daemon/manage.js +15 -16
  167. package/dist/daemon/review/finish.js +1 -10
  168. package/dist/pi-extensions/canvas-context-intro.js +1 -14
  169. package/dist/pi-extensions/canvas-doc-substrate.js +2 -2
  170. package/dist/pi-extensions/canvas-preview-result.d.ts +22 -0
  171. package/dist/pi-extensions/canvas-preview-result.js +60 -0
  172. package/dist/pi-extensions/canvas-review-boundary.js +1 -14
  173. package/dist/pi-extensions/truncate.d.ts +5 -0
  174. package/dist/pi-extensions/truncate.js +14 -0
  175. package/dist/shared/predicates.d.ts +2 -0
  176. package/dist/shared/predicates.js +4 -0
  177. package/dist/shared/shell-quote.d.ts +3 -0
  178. package/dist/shared/shell-quote.js +5 -0
  179. package/dist/shared/tool-groups.d.ts +4 -2
  180. package/dist/shared/tool-groups.js +3 -3
  181. package/dist/types.d.ts +8 -5
  182. package/package.json +1 -1
  183. package/runtime.lock.json +2 -2
  184. package/dist/core/substrate/on-read-pure.d.ts +0 -7
  185. package/dist/core/substrate/on-read-pure.js +0 -272
@@ -78,12 +78,12 @@ export declare function waitForDaemonExit(pid: number, timeoutMs?: number, deps?
78
78
  /** Stop the RECORDED daemon: SIGTERM, and if it does not exit within the wait
79
79
  * window, escalate to SIGKILL exactly as `sweepStrayDaemons` does for a stray.
80
80
  *
81
- * The escalation is the point. `stop` used to send SIGTERM, wait, and throw on
82
- * timeout — before ever reaching the stray sweep — so the one daemon `stop` is
83
- * actually aimed at was the only crtrd process exempt from the SIGKILL backstop.
84
- * A daemon whose shutdown hung (or outran the window) then became permanently
85
- * unstoppable from the CLI: every retry re-sent a SIGTERM it was already
86
- * ignoring, and `start` kept refusing as "already running".
81
+ * The escalation is the point. Without it — SIGTERM, wait, throw on timeout,
82
+ * never reaching the stray sweep — the one daemon `stop` is actually aimed at
83
+ * would be the only crtrd process exempt from the SIGKILL backstop, and a
84
+ * daemon whose shutdown hangs (or outruns the window) becomes permanently
85
+ * unstoppable from the CLI: every retry re-sends a SIGTERM it is already
86
+ * ignoring, and `start` keeps refusing as "already running".
87
87
  *
88
88
  * A SIGKILL'd daemon runs no exit handler, so it leaves its pidfile behind
89
89
  * pointing at a dead pid. That is a stale record, not an owner: `readPidfile`
@@ -193,15 +193,14 @@ export function sweepStrayDaemons(keepPid) {
193
193
  // Daemon lifecycle transitions are bounded so callers never claim a state
194
194
  // change before the pidfile and process liveness agree.
195
195
  //
196
- // #508: a cold boot right after `install-runtime` (empty module cache, fresh
197
- // generation, possible pending db migration) can take several seconds to
198
- // reach writePidfile() — a fixed 5s window produced false "did not become
199
- // ready" failures on a daemon that was in fact starting up fine. Widened to
200
- // tolerate that slow-but-valid cold start; a process exit (signal/non-zero
201
- // code) still fails immediately regardless of this window.
196
+ // A cold boot right after `install-runtime` (empty module cache, fresh
197
+ // generation, possible pending db migration) can take several seconds to reach
198
+ // writePidfile(), so the window is wide enough to tolerate that slow-but-valid
199
+ // cold start rather than report a false "did not become ready"; a process exit
200
+ // (signal/non-zero code) still fails immediately regardless of this window.
202
201
  // Exported so `commands/api-client.ts` can align the CLI-side `CrtrClient`'s
203
202
  // cold-start `/healthz` poll window with this authoritative valid-startup bound
204
- // (issue #508 follow-up) instead of the two windows drifting independently.
203
+ // instead of the two windows drifting independently.
205
204
  export const DAEMON_VERIFY_WINDOW_MS = 20_000;
206
205
  const DAEMON_VERIFY_POLL_MS = 10;
207
206
  function sleepMs(ms) {
@@ -272,12 +271,12 @@ const DAEMON_KILL_WINDOW_MS = 2_000;
272
271
  /** Stop the RECORDED daemon: SIGTERM, and if it does not exit within the wait
273
272
  * window, escalate to SIGKILL exactly as `sweepStrayDaemons` does for a stray.
274
273
  *
275
- * The escalation is the point. `stop` used to send SIGTERM, wait, and throw on
276
- * timeout — before ever reaching the stray sweep — so the one daemon `stop` is
277
- * actually aimed at was the only crtrd process exempt from the SIGKILL backstop.
278
- * A daemon whose shutdown hung (or outran the window) then became permanently
279
- * unstoppable from the CLI: every retry re-sent a SIGTERM it was already
280
- * ignoring, and `start` kept refusing as "already running".
274
+ * The escalation is the point. Without it — SIGTERM, wait, throw on timeout,
275
+ * never reaching the stray sweep — the one daemon `stop` is actually aimed at
276
+ * would be the only crtrd process exempt from the SIGKILL backstop, and a
277
+ * daemon whose shutdown hangs (or outruns the window) becomes permanently
278
+ * unstoppable from the CLI: every retry re-sends a SIGTERM it is already
279
+ * ignoring, and `start` keeps refusing as "already running".
281
280
  *
282
281
  * A SIGKILL'd daemon runs no exit handler, so it leaves its pidfile behind
283
282
  * pointing at a dead pid. That is a stale record, not an owner: `readPidfile`
@@ -332,8 +331,8 @@ export async function spawnDaemon() {
332
331
  // output the daemon doesn't control, plus the narrow set of authorized raw
333
332
  // `[crtrd] …` bootstrap/fatal lines that by construction predate or escape
334
333
  // canonical event emission (the pre-bind losing-claim line and a cleanup-
335
- // fatal write in crtrd.ts): all used to go to /dev/null for any manually-
336
- // started daemon, the common case (only a launchd-owned daemon had a
334
+ // fatal write in crtrd.ts). Without this fd all of it is discarded for any
335
+ // manually-started daemon, the common case (only a launchd-owned daemon has a
337
336
  // logging plist). Every other daemon diagnostic — including per-node
338
337
  // supervise/relaunch failures — is a canonical `emitEvent` write, not raw
339
338
  // stderr; this file is not where those live. Mirrors host.ts's broker.log
@@ -344,7 +343,7 @@ export async function spawnDaemon() {
344
343
  bindDaemonEventSource();
345
344
  // `--canvas-home` declares this daemon's canvas ownership in argv so
346
345
  // findDaemonPids()/sweepStrayDaemons (above) can match its own daemon
347
- // exactly instead of scanning the whole machine (#518). crtrd-cli.ts's argv
346
+ // exactly instead of scanning the whole machine. crtrd-cli.ts's argv
348
347
  // parsing only looks for `--tcp`, so this extra pair of tokens is inert
349
348
  // there — it exists purely for `ps` visibility.
350
349
  const child = spawn(hostExecPath(), [...launchArgs, '--canvas-home', crtrHome()], {
@@ -8,7 +8,7 @@ import { reviewResultPath } from '../../core/canvas/paths.js';
8
8
  import { snapshotCommentsLocked } from '../../core/review/comments.js';
9
9
  import { readReviewSource } from '../../core/review/document.js';
10
10
  import { signalReviewActivity } from '../../core/review/signal.js';
11
- import { approveReviewLocked, cancelReviewRow, getReview, markDelivered, markDeliveryError, markProjected, markProjectionError, } from '../../core/review/store.js';
11
+ import { approveReviewLocked, cancelReviewRow, markDelivered, markDeliveryError, markProjected, markProjectionError, requireVisibleReview, } from '../../core/review/store.js';
12
12
  import { ReviewOperationError } from '../../core/review/types.js';
13
13
  import { transition } from '../../core/runtime/lifecycle.js';
14
14
  import { headlessBrokerHost } from '../../core/runtime/host.js';
@@ -22,15 +22,6 @@ function now() {
22
22
  function errorText(error) {
23
23
  return error instanceof Error ? error.message : String(error);
24
24
  }
25
- function unknownReview(reviewId) {
26
- throw new ReviewOperationError('unknown_review', 'review was not found', { received: reviewId });
27
- }
28
- function requireVisibleReview(reviewId) {
29
- const review = getReview(reviewId);
30
- if (review === null || review.state === 'binding')
31
- return unknownReview(reviewId);
32
- return review;
33
- }
34
25
  function terminalConflict(review, requested) {
35
26
  throw new ReviewOperationError('review_terminal', 'review already has a terminal outcome', {
36
27
  state: review.state,
@@ -30,6 +30,7 @@ import { CONTEXT_INTRO_CUSTOM_TYPE } from '../shared/generated-context.js';
30
30
  import { brokerExtensionState } from '../core/runtime/broker/daemon-ops.js';
31
31
  import { buildContextBearingsFromState, buildForkBearingsFromState, buildProjectContextBlockForBroker } from '../core/runtime/broker-extension-render.js';
32
32
  import { situationalContextBlock } from '../core/runtime/situational-context.js';
33
+ import { truncateToWidth } from './truncate.js';
33
34
  /** The `customType` stamped on the injected session message. Used both to write
34
35
  * the entry and to detect it on resume (the idempotency guard). Defined beside
35
36
  * the block builder in `core/runtime/bearings.ts` because the warm-pool claim's
@@ -91,20 +92,6 @@ function wrapLine(line, width) {
91
92
  out.push(chars.slice(i, i + width).join(''));
92
93
  return out;
93
94
  }
94
- /** Truncate plain text to at most `width` columns, appending an ellipsis when it
95
- * would overflow. Content here is ANSI-free plain text (label + prose), so a
96
- * code-point count stands in for visible width. The renderer MUST keep every
97
- * emitted line within the terminal width or pi's TUI aborts the whole render. */
98
- function truncateToWidth(text, width) {
99
- if (width <= 0)
100
- return '';
101
- const chars = Array.from(text);
102
- if (chars.length <= width)
103
- return text;
104
- if (width === 1)
105
- return '…';
106
- return chars.slice(0, width - 1).join('') + '…';
107
- }
108
95
  /**
109
96
  * Renderer for `crtr-context` messages. Collapsed (default) shows a one-line
110
97
  * stub; expanded (Ctrl+O) shows the label + full body. Returns a plain object
@@ -34,7 +34,7 @@ import { homedir } from 'node:os';
34
34
  import { join, resolve } from 'node:path';
35
35
  import { brokerExtensionState } from '../core/runtime/broker/daemon-ops.js';
36
36
  import { renderPreferencesFromState } from '../core/runtime/broker-extension-render.js';
37
- import { renderOnReadDocsForSubject } from '../core/substrate/on-read-pure.js';
37
+ import { renderOnReadDocsForSubject } from '../core/substrate/on-read.js';
38
38
  import { clearSessionCache } from '../core/substrate/session-cache.js';
39
39
  import { loadInjectedDocs, saveInjectedDocs } from '../core/substrate/injected-store.js';
40
40
  import { autoLoadedContextInner, mergeAutoLoadedContext } from './envelope-merge.js';
@@ -76,7 +76,7 @@ export function registerCanvasDocSubstrate(pi) {
76
76
  // (memory-usage guidance + preferences). The guidance frame is always
77
77
  // present for a canvas node (the memory system always exists), so the block
78
78
  // is never empty even when no preference is eligible. (The knowledge catalog
79
- // no longer enters the system prompt — it rides the bearings message via
79
+ // does not enter the system prompt — it rides the bearings message via
80
80
  // renderKnowledgeBlock.)
81
81
  pi.on('before_agent_start', async (event) => {
82
82
  try {
@@ -0,0 +1,22 @@
1
+ interface ToolCallEvent {
2
+ toolName: string;
3
+ toolCallId: string;
4
+ input: {
5
+ command?: string;
6
+ };
7
+ }
8
+ interface ToolResultEvent {
9
+ toolName: string;
10
+ toolCallId: string;
11
+ details?: Record<string, unknown>;
12
+ }
13
+ interface PiLike {
14
+ on(event: 'tool_call', handler: (event: ToolCallEvent) => void): void;
15
+ on(event: 'tool_result', handler: (event: ToolResultEvent) => {
16
+ details: Record<string, unknown>;
17
+ } | void): void;
18
+ }
19
+ /** Loaded into every canvas broker. Plain Pi sessions have no node id and
20
+ * deliberately receive no filesystem transport. */
21
+ export declare function registerCanvasPreviewResult(pi: PiLike): void;
22
+ export default registerCanvasPreviewResult;
@@ -0,0 +1,60 @@
1
+ // canvas-preview-result.ts — private structured result transport for crtr bash calls.
2
+ //
3
+ // A crtr leaf returns a record, but the CLI normally renders that record to
4
+ // stdout for the agent. stdout is model context, so this hook assigns every
5
+ // bash call a private file, lets crtr atomically mirror its record there, then
6
+ // attaches the record to Pi's ToolResult.details. The attach viewer can render
7
+ // a structured preview without parsing the agent-facing text.
8
+ import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ function isPreviewRecord(value) {
12
+ if (value === null || typeof value !== 'object' || typeof value['path'] !== 'string')
13
+ return false;
14
+ const record = value;
15
+ return (record.result !== undefined && record.result !== null && typeof record.result === 'object' && !Array.isArray(record.result))
16
+ || (Array.isArray(record.jsonl) && record.jsonl.every((item) => item !== null && typeof item === 'object' && !Array.isArray(item)))
17
+ || (record.error !== undefined && record.error !== null && typeof record.error === 'object' && !Array.isArray(record.error));
18
+ }
19
+ /** Loaded into every canvas broker. Plain Pi sessions have no node id and
20
+ * deliberately receive no filesystem transport. */
21
+ export function registerCanvasPreviewResult(pi) {
22
+ if (process.env['CRTR_NODE_ID'] === undefined || process.env['CRTR_NODE_ID'] === '')
23
+ return;
24
+ const directory = join(tmpdir(), 'crouter-preview-results');
25
+ const paths = new Map();
26
+ pi.on('tool_call', (event) => {
27
+ if (event.toolName !== 'bash' || typeof event.input.command !== 'string')
28
+ return;
29
+ mkdirSync(directory, { recursive: true });
30
+ const path = join(directory, `${encodeURIComponent(event.toolCallId)}.json`);
31
+ paths.set(event.toolCallId, path);
32
+ // Export before the complete script rather than prefixing its first simple
33
+ // command: agent bash calls routinely start with `cd … &&`, a compound
34
+ // statement, or setup commands before crtr itself.
35
+ event.input.command = `export CRTR_PREVIEW_RESULT_PATH=${JSON.stringify(path)}\n${event.input.command}`;
36
+ });
37
+ pi.on('tool_result', (event) => {
38
+ const path = paths.get(event.toolCallId);
39
+ if (path === undefined)
40
+ return;
41
+ paths.delete(event.toolCallId);
42
+ if (!existsSync(path))
43
+ return;
44
+ try {
45
+ const record = JSON.parse(readFileSync(path, 'utf8'));
46
+ if (!isPreviewRecord(record))
47
+ return;
48
+ return { details: { ...(event.details ?? {}), crtrPreview: record } };
49
+ }
50
+ catch {
51
+ // A malformed side-channel record is not agent-visible output and must
52
+ // not change the bash call's actual result.
53
+ return;
54
+ }
55
+ finally {
56
+ rmSync(path, { force: true });
57
+ }
58
+ });
59
+ }
60
+ export default registerCanvasPreviewResult;
@@ -6,23 +6,10 @@
6
6
  // inherited context.
7
7
  import { brokerExtensionState } from '../core/runtime/broker/daemon-ops.js';
8
8
  import { isReviewBoundary, REVIEW_BOUNDARY_CUSTOM_TYPE } from '../core/runtime/session-visibility.js';
9
+ import { truncateToWidth } from './truncate.js';
9
10
  // ---------------------------------------------------------------------------
10
11
  // Collapsed-by-default rendering
11
12
  // ---------------------------------------------------------------------------
12
- /** Truncate plain text to at most `width` columns, appending an ellipsis when it
13
- * would overflow. Content here is ANSI-free plain text, so code-point count
14
- * stands in for visible width. The renderer MUST keep every emitted line within
15
- * the terminal width or pi's TUI aborts the whole render. */
16
- function truncateToWidth(text, width) {
17
- if (width <= 0)
18
- return '';
19
- const chars = Array.from(text);
20
- if (chars.length <= width)
21
- return text;
22
- if (width === 1)
23
- return '…';
24
- return chars.slice(0, width - 1).join('') + '…';
25
- }
26
13
  /** Render the marker as a single compact divider; its full content remains in
27
14
  * the message list for the model and generated-context presenters. */
28
15
  export function renderReviewBoundary(_message, _options, theme) {
@@ -0,0 +1,5 @@
1
+ /** Hard-truncate to `width` columns with an ellipsis when the text would
2
+ * overflow. Extension-rendered content is ANSI-free plain text, so a
3
+ * code-point count stands in for visible width. Every renderer MUST keep its
4
+ * emitted lines within the terminal width or pi's TUI aborts the whole render. */
5
+ export declare function truncateToWidth(text: string, width: number): string;
@@ -0,0 +1,14 @@
1
+ /** Hard-truncate to `width` columns with an ellipsis when the text would
2
+ * overflow. Extension-rendered content is ANSI-free plain text, so a
3
+ * code-point count stands in for visible width. Every renderer MUST keep its
4
+ * emitted lines within the terminal width or pi's TUI aborts the whole render. */
5
+ export function truncateToWidth(text, width) {
6
+ if (width <= 0)
7
+ return '';
8
+ const chars = Array.from(text);
9
+ if (chars.length <= width)
10
+ return text;
11
+ if (width === 1)
12
+ return '…';
13
+ return chars.slice(0, width - 1).join('') + '…';
14
+ }
@@ -0,0 +1,2 @@
1
+ /** A plain object with named properties: null and arrays are not records. */
2
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
@@ -0,0 +1,4 @@
1
+ /** A plain object with named properties: null and arrays are not records. */
2
+ export function isRecord(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
@@ -0,0 +1,3 @@
1
+ /** POSIX single-quote escaping for one shell word, so paths and arguments
2
+ * survive spaces and shell metacharacters when interpolated into `sh -c`. */
3
+ export declare function shellQuote(s: string): string;
@@ -0,0 +1,5 @@
1
+ /** POSIX single-quote escaping for one shell word, so paths and arguments
2
+ * survive spaces and shell metacharacters when interpolated into `sh -c`. */
3
+ export function shellQuote(s) {
4
+ return `'${s.replace(/'/g, `'\\''`)}'`;
5
+ }
@@ -1,8 +1,10 @@
1
- /** Broker-generated activity recap for one completed tool-call group. */
1
+ /** Broker-generated activity recap for one completed tool-call group. Changed
2
+ * files are counted deterministically by the viewer's GroupActivityRecorder;
3
+ * deletions have no tool of their own, so only the model can evidence them. */
2
4
  export interface ToolGroupSummary {
3
5
  bullets: string[];
4
6
  nodesSpawned: number;
5
- filesEdited: number;
7
+ filesDeleted: number;
6
8
  }
7
9
  export interface ToolGroupMessageLike {
8
10
  role?: string;
@@ -2,7 +2,7 @@
2
2
  // Keep this independent of renderer state: both live event tracking and snapshot
3
3
  // reconstruction must agree on which prose closes a run of tools.
4
4
  import { generatedContextPresentation, generatedContextText } from './generated-context.js';
5
- const TOOL_GROUP_SUMMARY_KEYS = new Set(['bullets', 'nodesSpawned', 'filesEdited']);
5
+ const TOOL_GROUP_SUMMARY_KEYS = new Set(['bullets', 'nodesSpawned', 'filesDeleted']);
6
6
  const MAX_TOOL_GROUP_BULLETS = 5;
7
7
  const MAX_TOOL_GROUP_BULLET_LENGTH = 1_000;
8
8
  /** Validate the one durable summary shape and cap its display-sized fields.
@@ -21,14 +21,14 @@ export function normalizeToolGroupSummary(value) {
21
21
  return null;
22
22
  if (typeof record.nodesSpawned !== 'number' || !Number.isSafeInteger(record.nodesSpawned) || record.nodesSpawned < 0)
23
23
  return null;
24
- if (typeof record.filesEdited !== 'number' || !Number.isSafeInteger(record.filesEdited) || record.filesEdited < 0)
24
+ if (typeof record.filesDeleted !== 'number' || !Number.isSafeInteger(record.filesDeleted) || record.filesDeleted < 0)
25
25
  return null;
26
26
  return {
27
27
  bullets: record.bullets
28
28
  .slice(0, MAX_TOOL_GROUP_BULLETS)
29
29
  .map((bullet) => bullet.trim().slice(0, MAX_TOOL_GROUP_BULLET_LENGTH).trimEnd()),
30
30
  nodesSpawned: record.nodesSpawned,
31
- filesEdited: record.filesEdited,
31
+ filesDeleted: record.filesDeleted,
32
32
  };
33
33
  }
34
34
  /** Validate sidecar/snapshot entries independently so one malformed or legacy
package/dist/types.d.ts CHANGED
@@ -128,13 +128,12 @@ export interface ModelRoutingConfig {
128
128
  /** Launch metadata for one kind (a top-level kind like `developer`, or a full
129
129
  * sub-kind string like `plan/reviewers/security`) — the settings-file
130
130
  * replacement for per-kind persona frontmatter. `model`/`tools`/`extensions`
131
- * are launch knobs consumed by `buildLaunchSpec` (Phase 3); `whenToUse` is the
131
+ * are launch knobs consumed by `buildLaunchSpec`; `whenToUse` is the
132
132
  * one-line gloss shown in `node new -h` / `node promote -h`. `availableTo` is
133
133
  * VISIBILITY-ONLY (never launch validation): the list of top-level kind names
134
134
  * whose spawn menus surface this sub-kind, `'*'` meaning every kind; omitted
135
135
  * defaults to the sub-kind's own top-level ancestor (e.g.
136
- * `plan/reviewers/security` defaults to `['plan']`) — this reproduces the
137
- * original sub-persona `availableTo` semantics. A direct launch by full kind
136
+ * `plan/reviewers/security` defaults to `['plan']`). A direct launch by full kind
138
137
  * string is always valid regardless of `availableTo`. */
139
138
  export interface KindConfig {
140
139
  model?: string;
@@ -313,9 +312,9 @@ export interface InstalledMarketplace {
313
312
  * (spec §2.1). A profile is an agent identity defined at the user root: it
314
313
  * names N project directories and owns its own `memory/` store. `projects`
315
314
  * are absolute, real-path-resolved directories in manifest order — the
316
- * pointer set `findProjectScopeRoots` walks (Phase 5), after the node's own
315
+ * pointer set `findProjectScopeRoots` walks, after the node's own
317
316
  * cwd. `name` is the mutable display name (and the gateable `profile` subject
318
- * field, Phase 6); `<slug>-<id>` in the directory name is the stable id and
317
+ * field); `<slug>-<id>` in the directory name is the stable id and
319
318
  * never changes on rename. See `src/core/profiles/manifest.ts` for every
320
319
  * read/write of this file — it is the sole owner of path resolution and
321
320
  * mutation. */
@@ -323,6 +322,10 @@ export interface ProfileManifest {
323
322
  schema_version: number;
324
323
  name: string;
325
324
  projects: string[];
325
+ /** The directory nodes under this profile are pinned to (see
326
+ * `profileHome`). Normalized on read to the first project when the manifest
327
+ * carries none, so a profile written before homes existed still has one. */
328
+ home: string | null;
326
329
  created_at: string;
327
330
  last_used_at: string | null;
328
331
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.175",
3
+ "version": "0.3.177",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.175",
3
+ "version": "0.3.177",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.175",
9
+ "version": "0.3.177",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
@@ -1,7 +0,0 @@
1
- import type { NodeConfigSubject } from './subject-fields.js';
2
- /** Surface docs matched by a successful read tool call using the caller-supplied
3
- * daemon snapshot subject. */
4
- export declare function renderOnReadDocsForSubject(subject: NodeConfigSubject, readFilePath: string, seen?: Set<string>): string;
5
- /** Workspace boot docs using the snapshot's cwd/profile instead of a canvas
6
- * lookup. */
7
- export declare function renderWorkspaceOpenDocsForSubject(subject: NodeConfigSubject, cwd: string, profileId: string | null): string;