@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
@@ -7,7 +7,8 @@ import { scanInbox } from '../../core/human/scan.js';
7
7
  import { watchInboxActivity } from '../../core/human/root.js';
8
8
  import { claimTicket, heartbeatClaim, releaseClaim } from '../../core/human/claim.js';
9
9
  import { readTicketResult } from '../../core/human/tickets.js';
10
- import { clearProgress, deckPath, readJson } from '../../core/human/convention.js';
10
+ import { clearProgress, deckPath } from '../../core/human/convention.js';
11
+ import { readJsonOrNull } from '../../core/fs-utils.js';
11
12
  import { DeckAdapter } from './deck-adapter.js';
12
13
  import { validateDeck } from '../../core/human/deck-schema.js';
13
14
  import { ReviewAdapter } from './review-adapter.js';
@@ -258,7 +259,7 @@ export class InboxController {
258
259
  this.markDirty();
259
260
  }
260
261
  readDeck(dir) {
261
- const deck = readJson(deckPath(dir));
262
+ const deck = readJsonOrNull(deckPath(dir));
262
263
  if (deck === null)
263
264
  return undefined;
264
265
  try {
@@ -1,4 +1,5 @@
1
- import { progressPath, readJson } from '../../core/human/convention.js';
1
+ import { progressPath } from '../../core/human/convention.js';
2
+ import { readJsonOrNull } from '../../core/fs-utils.js';
2
3
  import { mountPanel } from './tui/panel.js';
3
4
  /** Embeds the single deck renderer in a controller-owned rectangle. */
4
5
  export class DeckAdapter {
@@ -50,7 +51,7 @@ function notificationsAcknowledged(deck, responses) {
50
51
  });
51
52
  }
52
53
  function initialResponses(deck, dir) {
53
- const saved = readJson(progressPath(dir))?.responses;
54
+ const saved = readJsonOrNull(progressPath(dir))?.responses;
54
55
  if (Array.isArray(saved))
55
56
  return saved;
56
57
  return deck.interactions.flatMap((interaction) => {
@@ -12,7 +12,7 @@ export interface SurfaceComponent extends Component {
12
12
  /** Consume wheel input when applicable. */
13
13
  handleWheel?(direction: 'up' | 'down', lines: number): boolean;
14
14
  }
15
- /** The identities used for mounted Phase 3 surfaces. */
15
+ /** The identities used for mounted surfaces. */
16
16
  export type SurfaceId = 'inbox' | 'review';
17
17
  /** How long a notice stays up. */
18
18
  export interface SurfaceNoticeOptions {
@@ -20,14 +20,14 @@ export declare function rethrowAsCliError(err: unknown, next?: string): never;
20
20
  * 404-only→null mapping as `ApiCanvasSource.orNull`, but returning the raw DTO
21
21
  * and rethrowing non-404s as CLI errors rather than swallowing them. */
22
22
  export declare function getNodeOrNull(nodeId: string): Promise<NodeDetailDTO | null>;
23
- /** Bounded tail of crtrd.err surfaced on a cold-start `/healthz` timeout
24
- * (issue #516). Large enough to catch a real startup failure/stack trace,
23
+ /** Bounded tail of crtrd.err surfaced on a cold-start `/healthz` timeout.
24
+ * Large enough to catch a real startup failure/stack trace,
25
25
  * small enough that a runaway daemon log can never leak unbounded into a CLI
26
26
  * error message. */
27
27
  export declare const COLD_START_DIAGNOSTIC_TAIL_BYTES = 4096;
28
28
  /** Read a bounded tail of crtrd's stderr log to enrich a `daemon_unavailable`
29
- * cold-start timeout (issue #516: the prior bare message discarded the actual
30
- * startup failure). Bounds the I/O itself — opens the file and reads only the
29
+ * cold-start timeout, so the actual startup failure is not discarded behind a
30
+ * bare message. Bounds the I/O itself — opens the file and reads only the
31
31
  * final `COLD_START_DIAGNOSTIC_TAIL_BYTES` bytes via `fstatSync`+`readSync`,
32
32
  * never `readFileSync`-ing the whole append-only log — then decodes/filters
33
33
  * that bounded slice. Best-effort and synchronous per the `coldStartDiagnostic`
@@ -42,15 +42,14 @@ export declare function readColdStartDiagnostic(): string | undefined;
42
42
  * autostart is disabled — the client throws `daemon_unavailable` (spec §7.1,
43
43
  * §8). This is the ONLY CLI reference to `ensureDaemon`.
44
44
  *
45
- * #508 follow-up: `onColdSocket` is fire-and-forget — `ensureDaemon()` spawns
46
- * crtrd and returns immediately, so the client's OWN `/healthz` poll is the
47
- * only deadline the initiating CLI invocation actually waits on. That poll
48
- * used to default to a fixed 10s window while `spawnDaemon`'s
49
- * `verifyDaemonStartup` (which `ensureDaemon`'s spawn transitively awaits, in
50
- * the detached child, on the SAME cold-start path) was widened to 20s for a
51
- * slow-but-valid cold boot: a daemon that took 12s was a valid, verified
52
- * startup, but the CLI process that triggered it still gave up and reported
53
- * `daemon_unavailable` at 10s. Passing the SAME `DAEMON_VERIFY_WINDOW_MS`
45
+ * `onColdSocket` is fire-and-forget — `ensureDaemon()` spawns crtrd and
46
+ * returns immediately, so the client's OWN `/healthz` poll is the only
47
+ * deadline the initiating CLI invocation actually waits on. It must never be
48
+ * narrower than the window `spawnDaemon`'s `verifyDaemonStartup` allows (which
49
+ * `ensureDaemon`'s spawn transitively awaits, in the detached child, on the
50
+ * SAME cold-start path): a daemon that takes 12s is a valid, verified startup,
51
+ * and a 10s client poll would still give up and report `daemon_unavailable`
52
+ * against it. Passing the SAME `DAEMON_VERIFY_WINDOW_MS`
54
53
  * here keeps the two in lockstep — one authoritative window, not two that can
55
54
  * drift apart — without adding any retry/fallback poll loop. */
56
55
  export declare function cliClient(opts?: {
@@ -65,15 +65,15 @@ export async function getNodeOrNull(nodeId) {
65
65
  function autostartDisabled() {
66
66
  return process.env['CRTR_NO_DAEMON_AUTOSTART'] === '1' || process.argv.includes('--no-autostart');
67
67
  }
68
- /** Bounded tail of crtrd.err surfaced on a cold-start `/healthz` timeout
69
- * (issue #516). Large enough to catch a real startup failure/stack trace,
68
+ /** Bounded tail of crtrd.err surfaced on a cold-start `/healthz` timeout.
69
+ * Large enough to catch a real startup failure/stack trace,
70
70
  * small enough that a runaway daemon log can never leak unbounded into a CLI
71
71
  * error message. */
72
72
  export const COLD_START_DIAGNOSTIC_TAIL_BYTES = 4_096;
73
73
  const COLD_START_DIAGNOSTIC_TAIL_LINES = 20;
74
74
  /** Read a bounded tail of crtrd's stderr log to enrich a `daemon_unavailable`
75
- * cold-start timeout (issue #516: the prior bare message discarded the actual
76
- * startup failure). Bounds the I/O itself — opens the file and reads only the
75
+ * cold-start timeout, so the actual startup failure is not discarded behind a
76
+ * bare message. Bounds the I/O itself — opens the file and reads only the
77
77
  * final `COLD_START_DIAGNOSTIC_TAIL_BYTES` bytes via `fstatSync`+`readSync`,
78
78
  * never `readFileSync`-ing the whole append-only log — then decodes/filters
79
79
  * that bounded slice. Best-effort and synchronous per the `coldStartDiagnostic`
@@ -116,15 +116,14 @@ export function readColdStartDiagnostic() {
116
116
  * autostart is disabled — the client throws `daemon_unavailable` (spec §7.1,
117
117
  * §8). This is the ONLY CLI reference to `ensureDaemon`.
118
118
  *
119
- * #508 follow-up: `onColdSocket` is fire-and-forget — `ensureDaemon()` spawns
120
- * crtrd and returns immediately, so the client's OWN `/healthz` poll is the
121
- * only deadline the initiating CLI invocation actually waits on. That poll
122
- * used to default to a fixed 10s window while `spawnDaemon`'s
123
- * `verifyDaemonStartup` (which `ensureDaemon`'s spawn transitively awaits, in
124
- * the detached child, on the SAME cold-start path) was widened to 20s for a
125
- * slow-but-valid cold boot: a daemon that took 12s was a valid, verified
126
- * startup, but the CLI process that triggered it still gave up and reported
127
- * `daemon_unavailable` at 10s. Passing the SAME `DAEMON_VERIFY_WINDOW_MS`
119
+ * `onColdSocket` is fire-and-forget — `ensureDaemon()` spawns crtrd and
120
+ * returns immediately, so the client's OWN `/healthz` poll is the only
121
+ * deadline the initiating CLI invocation actually waits on. It must never be
122
+ * narrower than the window `spawnDaemon`'s `verifyDaemonStartup` allows (which
123
+ * `ensureDaemon`'s spawn transitively awaits, in the detached child, on the
124
+ * SAME cold-start path): a daemon that takes 12s is a valid, verified startup,
125
+ * and a 10s client poll would still give up and report `daemon_unavailable`
126
+ * against it. Passing the SAME `DAEMON_VERIFY_WINDOW_MS`
128
127
  * here keeps the two in lockstep — one authoritative window, not two that can
129
128
  * drift apart — without adding any retry/fallback poll loop. */
130
129
  export function cliClient(opts) {
@@ -140,7 +139,7 @@ export function cliClient(opts) {
140
139
  ensureDaemon();
141
140
  },
142
141
  // On a cold-start timeout, enrich the bare `daemon_unavailable` with the
143
- // tail of crtrd's own stderr log (issue #516) instead of discarding it.
142
+ // tail of crtrd's own stderr log instead of discarding it.
144
143
  coldStartDiagnostic: readColdStartDiagnostic,
145
144
  coldStartPollWindowMs: DAEMON_VERIFY_WINDOW_MS,
146
145
  });
@@ -153,8 +152,8 @@ export function cliClient(opts) {
153
152
  // performs no lossy legacy reconstruction or invented local-presence values.
154
153
  // The attach viewer's recurring subtree walk (chrome/canvas-panels.ts + overlays/graph.ts
155
154
  // computeActivity/visitActivity) calls subscriptionsOf/subscribersOf once per
156
- // visited node on every poll tick; each call was previously its own
157
- // `GET /v1/nodes/:id` round trip just to read the edge list off the detail DTO.
155
+ // visited node on every poll tick, and a `GET /v1/nodes/:id` per call just to
156
+ // read the edge list off the detail DTO costs one round trip per visited node.
158
157
  // This short-TTL cache backs those two methods with the lean roster
159
158
  // (`GET /v1/canvas/roster`) instead — one shared fetch per TTL window covers
160
159
  // every topology walk in that window, comfortably under the attach viewer's 5s
@@ -19,8 +19,7 @@ const pexec = promisify(execFile);
19
19
  * here rather than importing node.ts's `nodeInPane` (which reaches openDb via the
20
20
  * canvas accessors) so this dispatcher's import graph stays db-free. Under the
21
21
  * headless-broker model every engine runs DETACHED (window=null), so the pane tag
22
- * is the ONLY node handle — the window→node fallback in node.ts's variant only ever
23
- * matched legacy in-pane engines, which no longer exist. A stale tag is ignored
22
+ * is the ONLY node handle. A stale tag is ignored
24
23
  * (the tagged node must still be live), validated through the daemon API. */
25
24
  async function nodeInPane(pane) {
26
25
  if (pane === undefined || pane === '')
@@ -5,7 +5,8 @@ import { defineLeaf } from '../../core/command.js';
5
5
  import { ticketDir } from '../../core/human/root.js';
6
6
  import { scanInbox } from '../../core/human/scan.js';
7
7
  import { parseDeck } from '../../core/human/deck-schema.js';
8
- import { deckPath, isResolved, readJson, reviewPath } from '../../core/human/convention.js';
8
+ import { deckPath, isResolved, reviewPath } from '../../core/human/convention.js';
9
+ import { readJsonOrNull } from '../../core/fs-utils.js';
9
10
  import { InputError } from '../../core/io.js';
10
11
  import { paginate } from '../../core/pagination.js';
11
12
  import { cliClient, rethrowAsCliError } from '../api-client.js';
@@ -14,9 +15,9 @@ function toRef(n) {
14
15
  return { node_id: n.node_id, name: n.name, cwd: n.cwd, parent: n.parent, status: n.status };
15
16
  }
16
17
  /** The canvas roster, fetched ONCE per leaf run through the API and threaded
17
- * into the sync provenance helpers below (they used to call `listNodes()`/
18
- * `getNode()` synchronously; the API surface is async, so the fetch is hoisted
19
- * to the leaf and the helpers read the pre-fetched array/map). */
18
+ * into the sync provenance helpers below (the API surface is async, so the
19
+ * fetch is hoisted to the leaf and the helpers read the pre-fetched
20
+ * array/map). */
20
21
  async function fetchNodeRefs() {
21
22
  return (await cliClient().listNodes()).map(toRef);
22
23
  }
@@ -67,7 +68,7 @@ function summarizeTicket(summary, nodes, byId) {
67
68
  return null;
68
69
  const asking = askingNodeFor(jobId, byId);
69
70
  const conversation = resolveConversation(asking, nodes);
70
- const interactionCount = summary.kind === 'deck' ? readJson(deckPath(summary.dir))?.interactions.length : undefined;
71
+ const interactionCount = summary.kind === 'deck' ? readJsonOrNull(deckPath(summary.dir))?.interactions.length : undefined;
71
72
  const sourceLabel = readableSourceLabel(summary.source);
72
73
  return {
73
74
  id: summary.id,
@@ -155,9 +155,9 @@ export const findLeaf = defineLeaf({
155
155
  // Sort comparator MUST match keyOf exactly (score desc, then scope asc,
156
156
  // then name asc as the final deterministic identity component — the
157
157
  // candidate dedup keys on scope+name, so this pair is always unique).
158
- // Equal-score hits from different scopes previously sorted by name alone
159
- // while the cursor key ordered by scope first, so the array could disagree
160
- // with its own keys and paginate() would silently skip a later item.
158
+ // Sorting equal-score hits from different scopes by name alone while the
159
+ // cursor key orders by scope first would let the array disagree with its
160
+ // own keys, and paginate() would silently skip a later item.
161
161
  hits.sort((a, b) => {
162
162
  if (b.score !== a.score)
163
163
  return b.score - a.score;
@@ -71,7 +71,7 @@ function nodeNewParams() {
71
71
  { kind: 'stdin', name: 'prompt', required: false, constraint: 'First user message. Required unless --no-kickoff is given.' },
72
72
  { kind: 'flag', name: 'kind', type: 'string', required: false, default: 'general', constraint: 'Persona kind. The <kinds> list below names every top-level installable kind.' },
73
73
  { kind: 'flag', name: 'mode', type: 'enum', choices: ['base', 'orchestrator'], required: false, default: 'base', constraint: 'Persona mode. base for hands-on work; orchestrator when the unit itself needs decomposition across children.' },
74
- { kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Directory the node is pinned to. Defaults to the caller cwd.' },
74
+ { kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Pin the node to this directory. Defaults to the spawner\u2019s directory for a managed child, and to the profile home for a root (`crtr profile show <profile>`).' },
75
75
  { kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Display name. Defaults to the kind.' },
76
76
  { kind: 'flag', name: 'parent', type: 'string', required: false, constraint: 'Parent node id. Defaults to the calling node.' },
77
77
  { kind: 'flag', name: 'node-id', type: 'string', required: false, constraint: 'Exact id for the new node; lowercase letters, digits, and hyphens only, at most 128 characters. Rejected if already present.' },
@@ -106,7 +106,7 @@ async function runNodeCreation(input) {
106
106
  }
107
107
  const kind = input['kind'] ?? 'general';
108
108
  const mode = (input['mode'] ?? 'base');
109
- const cwd = input['cwd'] ?? process.cwd();
109
+ const pinCwd = input['cwd']?.trim();
110
110
  const name = input['name'];
111
111
  const parent = input['parent'];
112
112
  const root = input['root'] === true;
@@ -140,7 +140,11 @@ async function runNodeCreation(input) {
140
140
  const detail = await cliClient().createNode({
141
141
  kind,
142
142
  mode,
143
- cwd,
143
+ // The caller's directory is the profile-SELECTION context; where the node
144
+ // actually lands is resolved server-side (pin > spawner's cwd > profile
145
+ // home). Only `--cwd` pins.
146
+ cwd: process.cwd(),
147
+ ...(pinCwd !== undefined && pinCwd !== '' ? { pin_cwd: pinCwd } : {}),
144
148
  ...(name !== undefined ? { name } : {}),
145
149
  prompt,
146
150
  ...(root ? { root: true } : { parent: parentId ?? null }),
@@ -375,7 +375,7 @@ export const nodeYield = defineLeaf({
375
375
  { kind: 'flag', name: 'promote', type: 'bool', required: false, constraint: 'Also become an orchestrator as you refresh. Pass only when (1) the remaining work contains independent units that can run in parallel and (2) the task is large enough that parallel execution will materially improve intelligence, productivity, or elapsed throughput after coordination and synthesis. Task length, context exhaustion, sequential phases, or one helper are not enough; keep hands-on work in base mode and yield again as needed. Promote when coordinating and integrating children becomes your primary job. Already an orchestrator? This is not an error: your mode stays orchestrator and a missing roadmap is seeded.' },
376
376
  { kind: 'flag', name: 'kind', type: 'string', required: false, constraint: 'Respecialize as you refresh. The <kinds> list below names every top-level available kind and when to choose it; a sub-kind (e.g. plan/reviewers/security) is valid too by exact path but not listed here. Defaults to your current kind. With --promote it sets the orchestrator kind you become; without --promote it changes the kind while preserving your current mode.' },
377
377
  { kind: 'flag', name: 'model', type: 'enum', choices: ['ultra', 'strong'], required: false, constraint: 'Change the model your fresh revive runs on, by capability tier: `ultra` (frontier — reserve for high-taste judgment or enormous work: speccing, planning something large, e2e-testing something hard to test) or `strong` (opus — regular dev work). Omit to keep your current model. A node steering work is never weaker than opus, so these are the only two choices; the change is durable across future revives.' },
378
- // Deliberate fail-first contract (#395): builtin orchestration guidance shows bare
378
+ // Deliberate fail-first contract: builtin orchestration guidance shows bare
379
379
  // `crtr node yield`; that invocation fails fast and surfaces yield's full help just in
380
380
  // time, at the point of use. Keep the note required rather than pre-documenting stdin
381
381
  // in kernel guidance.
@@ -3,7 +3,7 @@ import type { Fault } from '../core/runtime/fault.js';
3
3
  /** --now eligibility for a live, non-busy fault: a daemon-owned AUTO retry
4
4
  * (kick it early instead of waiting out the schedule), or any fault the
5
5
  * canvas dashboard flags "needs you" (`faultNeedsYou`, status-glyph.ts) — the
6
- * disposition that otherwise has NO sanctioned recovery verb (issue #115).
6
+ * disposition that otherwise has NO sanctioned recovery verb.
7
7
  * Both are scoped to `pi→provider`: refuses a client-owned auto retry
8
8
  * (redialing) already in flight, and refuses any OTHER link — notably a
9
9
  * `daemon→node` wedge fault, which --now can't clear (SIGTERM doesn't touch
@@ -40,7 +40,7 @@ import { waitForViewSocketReady } from '../core/runtime/view-socket.js';
40
40
  /** --now eligibility for a live, non-busy fault: a daemon-owned AUTO retry
41
41
  * (kick it early instead of waiting out the schedule), or any fault the
42
42
  * canvas dashboard flags "needs you" (`faultNeedsYou`, status-glyph.ts) — the
43
- * disposition that otherwise has NO sanctioned recovery verb (issue #115).
43
+ * disposition that otherwise has NO sanctioned recovery verb.
44
44
  * Both are scoped to `pi→provider`: refuses a client-owned auto retry
45
45
  * (redialing) already in flight, and refuses any OTHER link — notably a
46
46
  * `daemon→node` wedge fault, which --now can't clear (SIGTERM doesn't touch
@@ -143,7 +143,7 @@ export const nodeReviveLeaf = defineLeaf({
143
143
  // above) so it can't be used to nuke a healthy node. This whole path stays
144
144
  // LOCAL: the kick is a direct SIGTERM to the broker pid (there is no revive
145
145
  // API call), and the node detail comes from the daemon read (`getNode`). The
146
- // reopen gate (#339) is NOT invoked here — --now targets a LIVE broker, and a
146
+ // reopen gate is NOT invoked here — --now targets a LIVE broker, and a
147
147
  // finalized node has none (its own live-pid check below refuses it anyway).
148
148
  if (now) {
149
149
  // The detail DTO projects `pi_pid_identity` (the launch-time process-identity
@@ -201,7 +201,7 @@ export const nodeReviveLeaf = defineLeaf({
201
201
  };
202
202
  }
203
203
  // Ordinary revive: the daemon relaunches the broker (the ONLY sanctioned
204
- // launcher). Node-existence validation (`not_found`) and the #339 reopen gate
204
+ // launcher). Node-existence validation (`not_found`) and the reopen gate
205
205
  // are both enforced server-side in `handleRevive`; a thrown gate/not-found
206
206
  // error round-trips through `ApiError.details` and is reconstructed into the
207
207
  // identical `InputError` by the shared `apiErrorToCliError` (via
@@ -4,7 +4,7 @@ import { listProfiles } from '../../core/profiles/manifest.js';
4
4
  export const listLeaf = defineLeaf({
5
5
  name: 'list',
6
6
  description: 'inventory every profile',
7
- whenToUse: 'browse every profile that exists — one line each with its id, name, project count, and last-used time. Reach for `profile show` once you have picked one, for its full project list.',
7
+ whenToUse: 'browse every profile that exists — one line each with its id, name, project count, home directory, and last-used time. Reach for `profile show` once you have picked one, for its full project list.',
8
8
  help: {
9
9
  name: 'profile list',
10
10
  summary: 'every profile as JSONL, one line each, sorted by name then profile_id',
@@ -12,6 +12,7 @@ export const listLeaf = defineLeaf({
12
12
  { name: 'profile_id', type: 'string', required: true, constraint: 'Stable directory id.' },
13
13
  { name: 'name', type: 'string', required: true, constraint: 'Manifest name.' },
14
14
  { name: 'projects_count', type: 'int', required: true, constraint: 'Number of project directories on the manifest.' },
15
+ { name: 'home', type: 'string | null', required: true, constraint: 'Directory the profile\u2019s nodes are pinned to, or null when it owns no projects.' },
15
16
  { name: 'last_used_at', type: 'string | null', required: true, constraint: 'ISO 8601 timestamp of the most recent selection, or null if never selected.' },
16
17
  ],
17
18
  outputKind: 'jsonl',
@@ -27,6 +28,7 @@ export const listLeaf = defineLeaf({
27
28
  profile_id: profileId,
28
29
  name: manifest.name,
29
30
  projects_count: manifest.projects.length,
31
+ home: manifest.home,
30
32
  last_used_at: manifest.last_used_at,
31
33
  });
32
34
  }
@@ -28,7 +28,7 @@ export const newLeaf = defineLeaf({
28
28
  name: 'project',
29
29
  type: 'path',
30
30
  required: false,
31
- constraint: 'One initial project directory. Must already exist and be a directory; resolved to its absolute real path before storing. Singular — add more with `crtr profile project add` after creation.',
31
+ constraint: "One initial project directory. Must already exist and be a directory; resolved to its absolute real path before storing. Also becomes the profile's home — where its nodes are pinned. Singular — add more with `crtr profile project add` after creation.",
32
32
  },
33
33
  ],
34
34
  output: [
@@ -36,6 +36,7 @@ export const newLeaf = defineLeaf({
36
36
  { name: 'name', type: 'string', required: true, constraint: 'The manifest name as given.' },
37
37
  { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the profile directory.' },
38
38
  { name: 'projects', type: 'string[]', required: true, constraint: 'Resolved absolute project directories, in manifest order. Empty when --project was omitted.' },
39
+ { name: 'home', type: 'string | null', required: true, constraint: 'Directory the profile\u2019s nodes are pinned to — the initial project, or null when none was given.' },
39
40
  { name: 'created_at', type: 'string', required: true, constraint: 'ISO 8601 creation timestamp.' },
40
41
  { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands.' },
41
42
  ],
@@ -56,6 +57,7 @@ export const newLeaf = defineLeaf({
56
57
  name: manifest.name,
57
58
  path: profileRoot(profileId),
58
59
  projects: manifest.projects,
60
+ home: manifest.home,
59
61
  created_at: manifest.created_at,
60
62
  follow_up: `Root profile ensured. Spawn nodes under it with \`--profile ${profileId}\`, or widen its purview with \`crtr profile project add ${profileId} --dir <path>\`.`,
61
63
  };
@@ -68,6 +70,7 @@ export const newLeaf = defineLeaf({
68
70
  name: manifest.name,
69
71
  path: profileRoot(profileId),
70
72
  projects: manifest.projects,
73
+ home: manifest.home,
71
74
  created_at: manifest.created_at,
72
75
  follow_up: `Show it with \`crtr profile show ${profileId}\`, add another project with \`crtr profile project add ${profileId} --dir <path>\`, or list every profile with \`crtr profile list\`.`,
73
76
  };
@@ -1,13 +1,14 @@
1
1
  // `crtr profile project` — the project-purview sub-noun of `profile`. A
2
- // profile's `projects` list is what it can SEE (memory + config resolution).
3
- // These two verbs widen or narrow that list after creation; `crtr profile
4
- // show` reads it back.
2
+ // profile's `projects` list is what it can SEE (memory + config resolution),
3
+ // and its `home` — one of those dirs — is where its nodes actually run. These
4
+ // two verbs widen or narrow that list after creation and re-point the home;
5
+ // `crtr profile show` reads both back.
5
6
  import { defineLeaf, defineBranch } from '../../core/command.js';
6
7
  import { addProfileProject, removeProfileProject, loadProfileManifest } from '../../core/profiles/manifest.js';
7
8
  const addLeaf = defineLeaf({
8
9
  name: 'add',
9
10
  description: "add a project directory to a profile's purview",
10
- whenToUse: "a profile needs to see (memory + config from) another project directory — extend its purview after creation, including mid-session from a node already running under that profile.",
11
+ whenToUse: "a profile needs to see (memory + config from) another project directory — extend its purview after creation, including mid-session from a node already running under that profile. Also the way to re-point the profile's home, with --home.",
11
12
  help: {
12
13
  name: 'profile project add',
13
14
  summary: "append a project directory to a profile's manifest, deduped by real path",
@@ -25,21 +26,32 @@ const addLeaf = defineLeaf({
25
26
  required: true,
26
27
  constraint: 'Directory to add. Must exist and be a directory; resolved to its absolute real path before storing. A path already on the manifest is a no-op, not an error.',
27
28
  },
29
+ {
30
+ kind: 'flag',
31
+ name: 'home',
32
+ type: 'bool',
33
+ required: false,
34
+ constraint: "Also make this directory the profile's home — where every node under it is pinned unless the create passes an explicit --cwd. Defaults to the first project dir; pass this on an already-listed dir to re-point it.",
35
+ },
28
36
  ],
29
37
  output: [
30
38
  { name: 'profile_id', type: 'string', required: true, constraint: 'Stable directory id.' },
31
39
  { name: 'projects', type: 'string[]', required: true, constraint: 'The updated, deduped project list in manifest order.' },
40
+ { name: 'home', type: 'string | null', required: true, constraint: "The profile's home directory after the change." },
32
41
  { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands.' },
33
42
  ],
34
43
  outputKind: 'object',
35
- effects: ["Appends to the profile manifest's `projects` array. Invalidates the process scope cache."],
44
+ effects: ["Appends to the profile manifest's `projects` array, and rewrites `home` when --home is given. Invalidates the process scope cache."],
36
45
  },
37
46
  run: async (input) => {
38
47
  const { profileId } = loadProfileManifest(input['profile']);
39
- const { manifest } = addProfileProject(profileId, input['dir']);
48
+ const { manifest } = addProfileProject(profileId, input['dir'], {
49
+ home: input['home'] === true,
50
+ });
40
51
  return {
41
52
  profile_id: profileId,
42
53
  projects: manifest.projects,
54
+ home: manifest.home,
43
55
  follow_up: `Show the full manifest with \`crtr profile show ${profileId}\`.`,
44
56
  };
45
57
  },
@@ -63,16 +75,17 @@ const removeLeaf = defineLeaf({
63
75
  name: 'dir',
64
76
  type: 'path',
65
77
  required: true,
66
- constraint: 'Directory to remove, matched by its resolved real path against the manifest. Does not require the directory to still exist on disk.',
78
+ constraint: 'Directory to remove, matched by its resolved real path against the manifest. Does not require the directory to still exist on disk. Removing the home dir re-points the home at the first remaining project.',
67
79
  },
68
80
  ],
69
81
  output: [
70
82
  { name: 'profile_id', type: 'string', required: true, constraint: 'Stable directory id.' },
71
83
  { name: 'projects', type: 'string[]', required: true, constraint: 'The updated project list in manifest order.' },
84
+ { name: 'home', type: 'string | null', required: true, constraint: "The profile's home directory after the change." },
72
85
  { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands.' },
73
86
  ],
74
87
  outputKind: 'object',
75
- effects: ["Removes an entry from the profile manifest's `projects` array. Invalidates the process scope cache."],
88
+ effects: ["Removes an entry from the profile manifest's `projects` array, re-pointing `home` when that entry was it. Invalidates the process scope cache."],
76
89
  },
77
90
  run: async (input) => {
78
91
  const { profileId } = loadProfileManifest(input['profile']);
@@ -80,6 +93,7 @@ const removeLeaf = defineLeaf({
80
93
  return {
81
94
  profile_id: profileId,
82
95
  projects: manifest.projects,
96
+ home: manifest.home,
83
97
  follow_up: `Show the full manifest with \`crtr profile show ${profileId}\`.`,
84
98
  };
85
99
  },
@@ -91,7 +105,7 @@ export const projectBranch = defineBranch({
91
105
  help: {
92
106
  name: 'profile project',
93
107
  summary: "manage a profile's project purview — the directories it resolves memory and config from",
94
- model: "A profile's `projects` list is the set of directories it inherits memory and config from. `add` widens that purview by one directory (deduped by real path); `remove` narrows it. Read the current list with `crtr profile show`. Purview is independent of a per-cwd default pin (`crtr profile default`) — widening purview never changes which profile a directory defaults to.",
108
+ model: "A profile's `projects` list is the set of directories it inherits memory and config from, and its `home` — the first project unless `add --home` says otherwise — is the directory its nodes are pinned to. `add` widens that purview by one directory (deduped by real path) and optionally re-points the home; `remove` narrows it, re-pointing the home when it removed it. Read both back with `crtr profile show`. Purview is independent of a per-cwd default pin (`crtr profile default`) — widening purview never changes which profile a directory defaults to.",
95
109
  },
96
110
  children: [addLeaf, removeLeaf],
97
111
  });
@@ -6,7 +6,7 @@ import { CONFIG_FILE } from '../../types.js';
6
6
  export const showLeaf = defineLeaf({
7
7
  name: 'show',
8
8
  description: "one profile's manifest in full",
9
- whenToUse: "you need a profile's full project list, id, and timestamps — after `profile list` narrows to a name, or to inspect what a node's bearings named as its profile.",
9
+ whenToUse: "you need a profile's full project list, home directory, id, and timestamps — after `profile list` narrows to a name, to find where its nodes will run, or to inspect what a node's bearings named as its profile.",
10
10
  help: {
11
11
  name: 'profile show',
12
12
  summary: "print one profile's manifest plus its on-disk path and config.json location",
@@ -22,6 +22,7 @@ export const showLeaf = defineLeaf({
22
22
  { name: 'profile_id', type: 'string', required: true, constraint: 'Stable directory id.' },
23
23
  { name: 'name', type: 'string', required: true, constraint: 'Manifest name.' },
24
24
  { name: 'projects', type: 'string[]', required: true, constraint: 'Absolute project directories, in manifest order.' },
25
+ { name: 'home', type: 'string | null', required: true, constraint: 'The directory every node under this profile is pinned to unless its create passes an explicit --cwd. Defaults to the first project; re-point it with `profile project add --home`. Null when the profile owns no projects (the root profile), in which case nodes fall back to the creating directory.' },
25
26
  { name: 'created_at', type: 'string', required: true, constraint: 'ISO 8601 creation timestamp.' },
26
27
  { name: 'last_used_at', type: 'string | null', required: true, constraint: 'ISO 8601 timestamp of the most recent selection, or null if never selected.' },
27
28
  { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the profile directory.' },
@@ -39,6 +40,7 @@ export const showLeaf = defineLeaf({
39
40
  profile_id: profileId,
40
41
  name: manifest.name,
41
42
  projects: manifest.projects,
43
+ home: manifest.home,
42
44
  created_at: manifest.created_at,
43
45
  last_used_at: manifest.last_used_at,
44
46
  path: profileRoot(profileId),
@@ -23,14 +23,14 @@ export function registerProfile() {
23
23
  return defineBranch({
24
24
  name: 'profile',
25
25
  rootEntry: {
26
- concept: 'an agent identity defined at the user root — a manifest naming project directories plus its own memory store',
26
+ concept: 'an agent identity defined at the user root — a manifest naming project directories and the home its nodes run in, plus its own memory store',
27
27
  desc: 'create, inspect, and manage profiles',
28
28
  useWhen: 'creating a new agent identity, inspecting or listing existing ones, changing a profile\u2019s project purview, pinning which profile a directory defaults to at startup, or managing a profile\u2019s stored env vars. Selecting a profile for a one-off run (`--profile`) lives elsewhere.',
29
29
  },
30
30
  help: {
31
31
  name: 'profile',
32
32
  summary: 'create, inspect, and manage profiles \u2014 agent identities with their own project purview and memory store',
33
- model: '`new` creates one from a name (+ at most one initial `--project`). `list` inventories every profile as JSONL. `show` prints one manifest in full. `rename` changes only the manifest name \u2014 the profile_id is stable across a rename. `delete` removes a profile\u2019s directory (manifest + its own memory store) entirely and does not touch the project directories it pointed at. `project` widens/narrows a profile\u2019s project purview (the dirs it resolves memory + config from). `default` pins which profile a directory defaults to at startup so the chooser stops asking \u2014 directory-scoped, independent of purview. `env` manages a profile-scoped 0600 store of env vars injected directly into every broker launched under that profile \u2014 set on stdin, list names only (never values), or remove one; no `spawnEnv.allow` entry needed. Every manifest verb but `new`/`list` takes <profile> as an exact id or a unique manifest name; an ambiguous name fails listing every matching id.',
33
+ model: 'A profile owns a `home` — the first of its project dirs unless `project add --home` re-points it — and every node created under the profile is pinned there, so cwd selects the profile and the profile then supplies where its nodes run. Only `node new --cwd` (or inheriting a spawner already elsewhere, notably inside a managed worktree) overrides that; the root profile has no home, so nodes under it stay in the directory that created them. `new` creates one from a name (+ at most one initial `--project`, which becomes its home). `list` inventories every profile as JSONL. `show` prints one manifest in full. `rename` changes only the manifest name \u2014 the profile_id is stable across a rename. `delete` removes a profile\u2019s directory (manifest + its own memory store) entirely and does not touch the project directories it pointed at. `project` widens/narrows a profile\u2019s project purview (the dirs it resolves memory + config from). `default` pins which profile a directory defaults to at startup so the chooser stops asking \u2014 directory-scoped, independent of purview. `env` manages a profile-scoped 0600 store of env vars injected directly into every broker launched under that profile \u2014 set on stdin, list names only (never values), or remove one; no `spawnEnv.allow` entry needed. Every manifest verb but `new`/`list` takes <profile> as an exact id or a unique manifest name; an ambiguous name fails listing every matching id.',
34
34
  },
35
35
  children: [newLeaf, listLeaf, showLeaf, renameLeaf, deleteLeaf, projectBranch, defaultBranch, envBranch],
36
36
  });
@@ -73,7 +73,7 @@ function makeTierLeaf(tier) {
73
73
  ...(tier === 'final'
74
74
  ? [
75
75
  'Marks the node done (status + intent) server-side; its engine shuts down on next stop.',
76
- 'When this node owns a managed worktree with zero commits ahead of its base and no uncommitted changes, the server auto-drops it instead of blocking on `node worktree close` (#327/#333); a worktree with real commits or real uncommitted changes still blocks with open_managed_worktree.',
76
+ 'When this node owns a managed worktree with zero commits ahead of its base and no uncommitted changes, the server auto-drops it instead of blocking on `node worktree close`; a worktree with real commits or real uncommitted changes still blocks with open_managed_worktree.',
77
77
  ]
78
78
  : []),
79
79
  ],
@@ -39,7 +39,7 @@ export declare function openSpawnViewer(nodeId: string, cwd: string, name: strin
39
39
  * A `crtr surface attach` viewer self-tags its pane with the node it views
40
40
  * (`@crtr_node`) — the tag IS the handle (a broker engine runs DETACHED with
41
41
  * window=null, so there is no window→node fallback). Kept SYNC + tag-only: the
42
- * re-plumbed CLI no longer reads canvas state to confirm the tagged node is
42
+ * CLI does not read canvas state to confirm the tagged node is
43
43
  * still live, so a stale tag (attach SIGKILLed without clearing) resolves until
44
44
  * the pane is respawned. Callers that mutate then re-validate the id via the
45
45
  * API (getNode 404 → clean error). */
@@ -111,7 +111,7 @@ export function openSpawnViewer(nodeId, cwd, name) {
111
111
  * A `crtr surface attach` viewer self-tags its pane with the node it views
112
112
  * (`@crtr_node`) — the tag IS the handle (a broker engine runs DETACHED with
113
113
  * window=null, so there is no window→node fallback). Kept SYNC + tag-only: the
114
- * re-plumbed CLI no longer reads canvas state to confirm the tagged node is
114
+ * CLI does not read canvas state to confirm the tagged node is
115
115
  * still live, so a stale tag (attach SIGKILLed without clearing) resolves until
116
116
  * the pane is respawned. Callers that mutate then re-validate the id via the
117
117
  * API (getNode 404 → clean error). */
@@ -33,6 +33,7 @@ import { isAbsolute, resolve } from 'node:path';
33
33
  import { defineLeaf } from '../core/command.js';
34
34
  import { InputError } from '../core/io.js';
35
35
  import { currentTmux, splitWindow, paneLocation, paneOfNode, } from '../core/runtime/placement-tmux.js';
36
+ import { shellQuote } from '../shared/shell-quote.js';
36
37
  /** Resolve the pane the editor should open BESIDE — the pane that opened this
37
38
  * command: explicit --pane, the caller's own $TMUX_PANE, the pane where THIS
38
39
  * node is being watched (its `@crtr_node`-tagged pane, whatever viewer put it
@@ -57,10 +58,6 @@ function resolveTargetPane(explicit) {
57
58
  }
58
59
  return currentTmux()?.pane ?? null;
59
60
  }
60
- /** POSIX single-quote a shell word so paths survive spaces/specials. */
61
- function shellQuote(s) {
62
- return `'${s.replace(/'/g, `'\\''`)}'`;
63
- }
64
61
  /** The command the new pane runs. The pane INHERITS the tmux server environment
65
62
  * (like every crtr chrome pane — the subagent viewer panes get theirs the same
66
63
  * way), and a tmux server started from the human's interactive shell captured
@@ -3,7 +3,7 @@ import { isSafeNodeId } from '../../core/canvas/paths.js';
3
3
  import { usage } from '../../core/errors.js';
4
4
  import { validateOperationId } from '../../core/events/operation-id.js';
5
5
  import { readLogSnapshot } from '../../core/events/read.js';
6
- import { emitLine, isJsonOutput } from '../../core/io.js';
6
+ import { emitLine, isJsonOutput, recordPreviewLine } from '../../core/io.js';
7
7
  const ISO_TIMESTAMP = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/u;
8
8
  const MAX_PATTERN_BYTES = 4_096;
9
9
  function invalidFlag(flag, received, expected) {
@@ -116,8 +116,10 @@ export const sysLogsLeaf = defineLeaf({
116
116
  if (item.kind === 'event') {
117
117
  if (json)
118
118
  emitLine({ ...item.envelope });
119
- else
119
+ else {
120
+ recordPreviewLine({ ...item.envelope });
120
121
  process.stdout.write(`${renderEvent(item.envelope)}\n`);
122
+ }
121
123
  }
122
124
  else if (!json) {
123
125
  process.stdout.write(`RAW ${item.source} ${new Date(item.effectiveTsMs).toISOString()} ${JSON.stringify(item.content)}\n`);
@@ -7,9 +7,9 @@ export interface SetupPanel {
7
7
  type ThemeFgRole = "border" | "accent" | "dim" | "success" | "warning" | "muted" | "mdLink" | "mdLinkUrl";
8
8
  export declare const theme: {
9
9
  fg(role: ThemeFgRole, text: string): string;
10
- /** A real filled selection band. `selectedBg` was previously an identity
11
- * function, which left the active tab and the active row with no background
12
- * at all — reverse video is the one background every terminal theme honours. */
10
+ /** A real filled selection band. An identity `selectedBg` would leave the
11
+ * active tab and the active row with no background at all — reverse video is
12
+ * the one background every terminal theme honours. */
13
13
  bg(_role: "selectedBg", text: string): string;
14
14
  bold(text: string): string;
15
15
  };
@@ -23,9 +23,9 @@ export const theme = {
23
23
  return ansiYellow(text);
24
24
  }
25
25
  },
26
- /** A real filled selection band. `selectedBg` was previously an identity
27
- * function, which left the active tab and the active row with no background
28
- * at all — reverse video is the one background every terminal theme honours. */
26
+ /** A real filled selection band. An identity `selectedBg` would leave the
27
+ * active tab and the active row with no background at all — reverse video is
28
+ * the one background every terminal theme honours. */
29
29
  bg(_role, text) {
30
30
  return `\x1b[7m${text}\x1b[27m`;
31
31
  },