@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
@@ -127,7 +127,7 @@ const BROKER_SIGKILL_GRACE_MS = 2_000;
127
127
  /** How often `pollUntilTreeDead` re-checks the process TREE. Short enough that
128
128
  * the common/happy path (tree already dead) pays only a couple of ticks; the
129
129
  * full grace ceilings above are reserved for the genuinely-wedged case,
130
- * where the added latency is the whole point (crouter#98 review finding 3). */
130
+ * where the added latency is the whole point. */
131
131
  const TEARDOWN_POLL_MS = 150;
132
132
  /** Poll `isAlive` every `TEARDOWN_POLL_MS` until it reports dead. If it is
133
133
  * STILL alive once `timeoutMs` elapses, fire `onTimeout` exactly once (the
@@ -136,7 +136,7 @@ const TEARDOWN_POLL_MS = 150;
136
136
  * up (an unkillable/D-state descendant is out of scope for this primitive).
137
137
  *
138
138
  * Deliberately a recursive, NEVER-`.unref()`'d `setTimeout` rather than a
139
- * fixed unref'd wait (crouter#98 review finding 3): most teardown callers
139
+ * fixed unref'd wait: most teardown callers
140
140
  * are short-lived CLI subprocesses (`node lifecycle close`, `canvas prune`)
141
141
  * that would otherwise exit before an unref'd SIGKILL timer ever fires,
142
142
  * silently skipping the last-resort rung. Polling (instead of a blind fixed
@@ -167,7 +167,7 @@ function pollUntil(isAlive, timeoutMs, onTimeout, hardCeilingMs = timeoutMs + 5_
167
167
  * `captureTeardownSnapshot` identity map over that same list) — both taken
168
168
  * ONCE by `teardown()` synchronously at its very top, before EITHER
169
169
  * escalation path (connect-error or post-shutdown-frame) can run, and
170
- * threaded through here unchanged (crouter#98 review finding 1). This
170
+ * threaded through here unchanged. This
171
171
  * function must NEVER re-walk the process tree itself: `launch()` spawns the
172
172
  * broker `detached: true`, so
173
173
  * signaling the broker's OWN group (the first thing `killProcessTreePids`
@@ -180,8 +180,8 @@ function pollUntil(isAlive, timeoutMs, onTimeout, hardCeilingMs = timeoutMs + 5_
180
180
  * "lose" a still-alive detached descendant — e.g. the pi bash tool's shell
181
181
  * child, itself `detached: true` and thus its own group leader, distinct
182
182
  * from the broker's group, so a plain `kill(-brokerPid)` never reaches it
183
- * either (crouton-labs/crouter#98). `identities` guards each signal against
184
- * PID REUSE across the multi-second escalation window (review finding 3) —
183
+ * either. `identities` guards each signal against
184
+ * PID REUSE across the multi-second escalation window —
185
185
  * see `killProcessTreePids`. `null` means the initial `captureTeardownSnapshot`
186
186
  * probe itself failed (no baseline to compare against) — `killProcessTreePids`
187
187
  * falls through to its normal unguarded signal path in that case, never
@@ -312,19 +312,19 @@ export const headlessBrokerHost = {
312
312
  // won't revive. On connect failure (broker dead/crashed/wedged) fall back
313
313
  // to escalateBrokerTeardown — a tree-wide SIGTERM now, a tree-wide SIGKILL
314
314
  // as the last resort — so no orphaned descendant of a dead/wedged broker
315
- // survives the node (crouton-labs/crouter#98) — + unlink the stale socket.
315
+ // survives the node — + unlink the stale socket.
316
316
  //
317
317
  // Capture the broker pid ONCE, synchronously, right here — never re-query
318
318
  // `getNode(nodeId)` inside the async callbacks/timers below. Callers such
319
319
  // as placement.ts's `reapIfEmpty()` call `teardown()` then immediately
320
320
  // `deleteNode()` in the same tick; by the time an async callback fires the
321
321
  // row can already be gone, so a re-query would silently resolve to
322
- // `undefined` and skip escalation entirely (crouter#98 review finding 2).
322
+ // `undefined` and skip escalation entirely.
323
323
  const node = getNode(nodeId);
324
324
  const pid = node?.pi_pid;
325
- // Snapshot the full descendant TREE (crouter#98 review finding 1) — the
325
+ // Snapshot the full descendant TREE — the
326
326
  // root pid plus every transitive descendant — and a stable identity
327
- // fingerprint for every pid in it (review finding 3), BOTH from ONE `ps`
327
+ // fingerprint for every pid in it, BOTH from ONE `ps`
328
328
  // table read (`captureTeardownSnapshot`, final review: closes the small
329
329
  // race between two separate probes), exactly ONCE, right here, while the
330
330
  // broker (if alive) still holds parentage over any detached descendant.
@@ -393,10 +393,10 @@ export const headlessBrokerHost = {
393
393
  // Bounded exit confirmation: a broker that connects fine but then HANGS
394
394
  // inside session.dispose() (disposeAndExit catches a throw, not a hang)
395
395
  // would leak the process holding the sole .jsonl writer — and any
396
- // bash→test→app descendants it spawned along the way (crouter#98). Poll
396
+ // bash→test→app descendants it spawned along the way. Poll
397
397
  // the whole process TREE (not just the broker's own pid) for up to
398
- // `BROKER_SHUTDOWN_GRACE_MS`, UNCONDITIONAL on the broker's own liveness
399
- // (crouter#98 review finding 1): a broker that exits cleanly can still
398
+ // `BROKER_SHUTDOWN_GRACE_MS`, UNCONDITIONAL on the broker's own liveness:
399
+ // a broker that exits cleanly can still
400
400
  // leave a live descendant behind (dispose() partially failed, or never
401
401
  // reached a detached SDK child), which is exactly the shape this
402
402
  // closes. `pollUntil` is ref'd — not `.unref()`'d — so a short-lived
@@ -405,8 +405,8 @@ export const headlessBrokerHost = {
405
405
  // couple of poll ticks.
406
406
  //
407
407
  // Checks `isAnyPidAlive(tree)` against the FIXED pre-signal snapshot,
408
- // NEVER a fresh `isProcessTreeAlive`/`descendantPids` re-walk (crouter#98
409
- // review finding 1, fixed): the broker can exit at any point during this
408
+ // NEVER a fresh `isProcessTreeAlive`/`descendantPids` re-walk: the broker
409
+ // can exit at any point during this
410
410
  // grace window while leaving a detached descendant alive, and the
411
411
  // instant it does, the kernel reparents that descendant away from `pid`
412
412
  // — a re-walk-by-ppid would then find nothing and silently skip
@@ -56,7 +56,7 @@ export declare function nextLadderModel(currentSpec: string, direction?: 1 | -1,
56
56
  * 3. Unset → pi default.
57
57
  * Callers pass the authoritative lifecycle + hasManager (`parent !== null`) so
58
58
  * a polymorph/flip rebuilds the recipe faithfully; `lifecycle` is returned
59
- * as-given — it is no longer persona-frontmatter-derived. The two canvas
59
+ * as-given, never derived from persona frontmatter. The two canvas
60
60
  * extensions are always first; kind-declared extensions follow. */
61
61
  export declare function buildLaunchSpec(kind: string, mode: Mode, opts: {
62
62
  lifecycle: Lifecycle;
@@ -249,7 +249,7 @@ export function nextLadderModel(currentSpec, direction = 1, providers) {
249
249
  * 3. Unset → pi default.
250
250
  * Callers pass the authoritative lifecycle + hasManager (`parent !== null`) so
251
251
  * a polymorph/flip rebuilds the recipe faithfully; `lifecycle` is returned
252
- * as-given — it is no longer persona-frontmatter-derived. The two canvas
252
+ * as-given, never derived from persona frontmatter. The two canvas
253
253
  * extensions are always first; kind-declared extensions follow. */
254
254
  export function buildLaunchSpec(kind, mode, opts) {
255
255
  const merged = readMergedLaunchConfig(opts.cwd ?? process.cwd(), opts.profileId !== undefined ? opts.profileId : (process.env['CRTR_PROFILE_ID'] || null));
@@ -1,33 +1,28 @@
1
1
  // lifecycle.ts — the node status×intent state machine.
2
2
  //
3
3
  // ONE place defines which (status, intent) moves are legal and enacts them.
4
- // Before this, ~a dozen scattered setStatus()/setIntent() pairs across
5
- // reset/close/revive/feed/daemon/stophook/queue/promote re-derived the lifecycle
6
- // by hand, with no shared definition of "what move is legal." Here the legal
7
- // transition TABLE is the definition, and `transition(id, event)` is the single
8
- // writer of status+intent: it validates the from-status, then writes both fields
9
- // in ONE atomic statement (built on Phase 2's WAL'd row setters) so the two can
10
- // never disagree.
4
+ // The legal transition TABLE is the definition, and `transition(id, event)` is
5
+ // the single writer of status+intent for every caller (reset, close, revive,
6
+ // feed, daemon, stophook, queue, promote): it validates the from-status, then
7
+ // writes both fields in ONE atomic statement (on the WAL'd row setters) so the
8
+ // two can never disagree.
11
9
  //
12
10
  // This mirrors persona.ts: persona.ts is the single source of transition PROSE;
13
11
  // lifecycle.ts is the single source of which status/intent move is LEGAL. Two
14
12
  // parallel, legible state machines instead of scattered enactment.
15
13
  //
16
- // Crash-safety invariant (was a comment repeated in close/reapDescendants):
17
- // "flip status to a non-supervised value + clear intent BEFORE killing the
18
- // host" (tmux pane or headless broker) — the daemon only ever revives
19
- // active|idle nodes, so a teardown must
20
- // leave the node done/canceled first to close the revive race. That invariant is
21
- // now the DEFINITION of the `cancel` event: callers flip via transition()
22
- // and only THEN tear the host down.
14
+ // Crash-safety invariant: flip status to a non-supervised value + clear intent
15
+ // BEFORE killing the host (tmux pane or headless broker) — the daemon only ever
16
+ // revives active|idle nodes, so a teardown must leave the node done/canceled
17
+ // first to close the revive race. That invariant is the DEFINITION of the
18
+ // `cancel` event: callers flip via transition() and only THEN tear the host down.
23
19
  //
24
- // Unification (A5, human-confirmed 2026-06-06): an externally-reaped node — torn
25
- // down because the user moved on (close cascade) OR because a root relaunch
26
- // superseded its workers — ends `canceled`, NOT `done`. `done` is reserved for a
27
- // node that finished its OWN work (finish) — including a relaunched root, which
28
- // is parked `done` as history, not canceled. The old `reap` event (→ done) was
29
- // identical to `cancel` in every field and side effect once unified on status, so
30
- // it was COLLAPSED into `cancel`; reset.ts's reapDescendants routes through
20
+ // An externally-reaped node — torn down because the user moved on (close
21
+ // cascade) OR because a root relaunch superseded its workers — ends `canceled`,
22
+ // NOT `done`. `done` is reserved for a node that finished its OWN work (finish)
23
+ // — including a relaunched root, which is parked `done` as history, not
24
+ // canceled. There is no separate reap event: it would be identical to `cancel`
25
+ // in every field and side effect, so reset.ts's reapDescendants routes through
31
26
  // `cancel`.
32
27
  //
33
28
  // Layering note: lifecycle.ts is runtime, but it is the canvas write surface's
@@ -4,9 +4,8 @@
4
4
  // These are canvas/runtime reads (getNode + SessionManager + session-cycles):
5
5
  // they belong SERVER-SIDE. crtrd's node read handlers (daemon/api/handlers/
6
6
  // nodes.ts) call them behind `GET /v1/nodes/{id}/{snapshot,transcript}`. They
7
- // used to live in the CLI leaves (commands/node-snapshot.ts, node-transcript.ts)
8
- // and were imported into the handlers from there; Stage B-3 relocated them here
9
- // so the CLI leaves' import graph reaches no canvas state store (spec §1/§10).
7
+ // live here rather than in the CLI leaves so the leaves' import graph reaches
8
+ // no canvas state store (spec §1/§10).
10
9
  import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
11
10
  import { readJsonIfExists } from '../fs-utils.js';
12
11
  import { tmpdir } from 'node:os';
@@ -14,6 +14,7 @@ import { existsSync, readFileSync } from 'node:fs';
14
14
  import { homedir } from 'node:os';
15
15
  import { join, resolve } from 'node:path';
16
16
  import { isRendererReady } from '../termrender/termrender.js';
17
+ import { expandTilde } from '../fs-utils.js';
17
18
  /** A non-fatal, action-oriented diagnostic for an unprovisioned managed
18
19
  * renderer — the one remaining out-of-tree dependency. Deliberately calls
19
20
  * `isRendererReady()`, NOT `ensureRenderer()`: this runs on the front-door boot
@@ -25,14 +26,6 @@ export function rendererWarning() {
25
26
  return 'crtr: the pinned termrender renderer is not provisioned — decks, reviews, and inline diagrams render as plaintext.\n' +
26
27
  ' Fix: run `crtr sys setup` (requires `uv`: curl -LsSf https://astral.sh/uv/install.sh | sh).';
27
28
  }
28
- /** Expand a leading `~` against `homeDir` (pi's own convention). */
29
- function expandTilde(pathValue, homeDir) {
30
- if (pathValue === '~')
31
- return homeDir;
32
- if (pathValue.startsWith('~/'))
33
- return join(homeDir, pathValue.slice(2));
34
- return pathValue;
35
- }
36
29
  /**
37
30
  * The pi agent config dir (`~/.pi/agent`, or `$PI_CODING_AGENT_DIR`) — a local
38
31
  * reimplementation of pi's `getAgentDir()`. Reimplemented (not imported) ON
@@ -30,7 +30,7 @@ import { orchestratorContextNote } from './bearings.js';
30
30
  /** Load a builtin/user/project memory doc's body by its normalized name, or ''
31
31
  * if it can't be resolved. The single source for the lifecycle/spine/
32
32
  * orchestration-kernel fragments this injector re-delivers on a transition —
33
- * the SAME docs Phase 2 authored under `src/builtin-memory/`, so static
33
+ * the SAME docs under `src/builtin-memory/`, so static
34
34
  * (system-prompt splice) and transition prose can never drift. */
35
35
  function loadMemoryBody(name) {
36
36
  try {
@@ -48,7 +48,7 @@ function loadMemoryBody(name) {
48
48
  * resource is its own context window. Roadmap-shaping guidance is just another orchestrator-gated memory
49
49
  * doc the node already gets via the boot-render splice (kinds/<kind>/orchestrator
50
50
  * gated `{kind, mode: orchestrator}`). (Lifecycle is left to its own section —
51
- * promotion no longer forces resident, so this never asserts residency.) */
51
+ * promotion does not force resident, so this never asserts residency.) */
52
52
  function orchestrationGuidance(nodeId, kind) {
53
53
  const kernel = loadMemoryBody('orchestration-kernel');
54
54
  const roadmap = readRoadmap(nodeId) ?? '(no roadmap yet)';
@@ -4,7 +4,9 @@
4
4
  * heavy pi SDK index on crtr's front-door hot path (the reason broker-sdk.ts
5
5
  * dynamic-imports the engine). Mirrors pi: `PI_CODING_AGENT_DIR` env, else
6
6
  * `~/.pi/agent` (APP_NAME defaults to 'pi'). Re-sync on a pi SDK bump that moves
7
- * the sessions dir — same vendoring rationale as the slash-command list below. */
7
+ * the sessions dir — same vendoring rationale as the slash-command list below.
8
+ * `PI_CODING_AGENT_DIR` goes through the same tilde expansion pi's
9
+ * `getAgentDir()` applies, or `~/custom-pi` yields a broken relative path. */
8
10
  export declare function piSessionsRoot(): string;
9
11
  /**
10
12
  * pi's builtin slash commands — vendored verbatim from pi `core/slash-commands.js`
@@ -13,7 +15,7 @@ export declare function piSessionsRoot(): string;
13
15
  * Builtins are NOT engine-interpreted (`session.prompt('/model')` ships `/model`
14
16
  * to the LLM as literal text), so the broker's `get_commands` op MERGES this list
15
17
  * with the engine's registered commands/templates/skills, and the viewer
16
- * autocomplete parses these locally. Both the broker (T3) and the viewer (T6)
18
+ * autocomplete parses these locally. Both the broker and the viewer
17
19
  * import this single copy.
18
20
  *
19
21
  * Upstream interpolates `${APP_NAME}` into the `quit` description; `APP_NAME`
@@ -12,32 +12,21 @@
12
12
  // below to match and adjust the count note.
13
13
  import { homedir } from 'node:os';
14
14
  import { join } from 'node:path';
15
+ import { expandTilde } from '../fs-utils.js';
15
16
  /** pi's sessions root, VENDORED from pi `config.getSessionsDir()` (= `<agentDir>/
16
17
  * sessions`). pi's package `exports` map is `.`-only, so config.js can't be
17
18
  * deep-imported, and a ROOT import of `getAgentDir` would eager-load the entire
18
19
  * heavy pi SDK index on crtr's front-door hot path (the reason broker-sdk.ts
19
20
  * dynamic-imports the engine). Mirrors pi: `PI_CODING_AGENT_DIR` env, else
20
21
  * `~/.pi/agent` (APP_NAME defaults to 'pi'). Re-sync on a pi SDK bump that moves
21
- * the sessions dir — same vendoring rationale as the slash-command list below. */
22
+ * the sessions dir — same vendoring rationale as the slash-command list below.
23
+ * `PI_CODING_AGENT_DIR` goes through the same tilde expansion pi's
24
+ * `getAgentDir()` applies, or `~/custom-pi` yields a broken relative path. */
22
25
  export function piSessionsRoot() {
23
26
  const env = process.env['PI_CODING_AGENT_DIR'];
24
27
  const agentDir = env !== undefined && env !== '' ? expandTilde(env) : join(homedir(), '.pi', 'agent');
25
28
  return join(agentDir, 'sessions');
26
29
  }
27
- /** Mirror pi's `expandTildePath` (= `normalizePath` with the default expandTilde
28
- * on): a bare `~` is the home dir, and a leading `~/` (or `~\` on win32) joins
29
- * the remainder under home. pi's `getAgentDir()` runs `PI_CODING_AGENT_DIR`
30
- * through this, so the vendored root must too or `~/custom-pi` yields a broken
31
- * relative path. Other forms (absolute, relative, `~user`) pass through as pi
32
- * leaves them. */
33
- function expandTilde(p) {
34
- if (p === '~')
35
- return homedir();
36
- if (p.startsWith('~/') || (process.platform === 'win32' && p.startsWith('~\\'))) {
37
- return join(homedir(), p.slice(2));
38
- }
39
- return p;
40
- }
41
30
  /**
42
31
  * pi's builtin slash commands — vendored verbatim from pi `core/slash-commands.js`
43
32
  * `BUILTIN_SLASH_COMMANDS` (review C1). **21 entries at 0.78.1** (review n1 — NOT 23).
@@ -45,7 +34,7 @@ function expandTilde(p) {
45
34
  * Builtins are NOT engine-interpreted (`session.prompt('/model')` ships `/model`
46
35
  * to the LLM as literal text), so the broker's `get_commands` op MERGES this list
47
36
  * with the engine's registered commands/templates/skills, and the viewer
48
- * autocomplete parses these locally. Both the broker (T3) and the viewer (T6)
37
+ * autocomplete parses these locally. Both the broker and the viewer
49
38
  * import this single copy.
50
39
  *
51
40
  * Upstream interpolates `${APP_NAME}` into the `quit` description; `APP_NAME`
@@ -7,7 +7,7 @@
7
7
  // orchestrator), and seeds a roadmap scaffold.
8
8
  // The transition guidance the node needs is injected CENTRALLY by the
9
9
  // persona injector (runtime/persona.ts) at the turn boundary — promote()
10
- // itself no longer returns or hand-emits guidance.
10
+ // itself returns no guidance and emits none by hand.
11
11
  // 2. Refresh → persona swap (permanent). On the next fresh revive the node
12
12
  // starts with the orchestrator system prompt baked in (because the launch
13
13
  // spec now says orchestrator). The injected guidance bridges until then.
@@ -1,4 +1,4 @@
1
- // The re-task/reopen gate (#339) — enforced at every doorway that can revive a
1
+ // The re-task/reopen gate — enforced at every doorway that can revive a
2
2
  // node: `node message send` immediate delivery, `node wait deadline`, and
3
3
  // `node lifecycle revive`.
4
4
  //
@@ -12,7 +12,7 @@
12
12
  // so the node's next real `push final` re-latches through the normal
13
13
  // `WHERE final_report IS NULL` guard.
14
14
  //
15
- // Concurrency (#340 review): every check here reads `final_report` FRESH from
15
+ // Concurrency: every check here reads `final_report` FRESH from
16
16
  // the DB — never a caller-cached snapshot from an earlier point in the
17
17
  // command — and the actual clear is a compare-and-swap (`UPDATE ... WHERE
18
18
  // final_report = <the exact value just observed>`), mirroring the CAS
@@ -13,18 +13,18 @@ export declare const TERMINAL_BY_CHOICE: readonly NodeStatus[];
13
13
  * inbox watcher's own hold-and-flush) ever brings such a node back on its
14
14
  * own — only an explicit revive/reopen does. Shared by `isDisconnected`
15
15
  * (mass-revive scope, below) and `node message send --tier deferred`'s terminal-edge
16
- * rejection (gh #341) — a deferred entry appended to such a target would
16
+ * rejection — a deferred entry appended to such a target would
17
17
  * never be delivered, so that doorway rejects instead of silently holding
18
18
  * it forever. */
19
19
  export declare function hasNoNaturalCycle(meta: Pick<NodeMeta, 'status' | 'final_report'>): boolean;
20
20
  /** True when `meta`'s engine is NOT running but it has a resumable saved session
21
- * — the precise "disconnected" predicate (gh #9).
21
+ * — the precise "disconnected" predicate.
22
22
  *
23
23
  * - engine not running: no live `pi_pid` (a dead/absent broker pid).
24
24
  * - reconnectable: a captured pi session marker (`pi_session_file` or
25
25
  * `pi_session_id`) — without one there is nothing to reopen. reviveNode
26
26
  * resumes by the `.jsonl` path when it still exists, else relaunches fresh (a
27
- * bare `pi_session_id` alone no longer resumes — the broker preflight rejects
27
+ * bare `pi_session_id` alone does not resume — the broker preflight rejects
28
28
  * it — so such a node comes back fresh).
29
29
  * - `human`-kind rows are the `crtr human` bridge, never a pi engine, so they
30
30
  * are never "disconnected" (mirrors the daemon's superviseTick carve-out).
@@ -1,4 +1,4 @@
1
- // revive-all — resume EVERY disconnected node in one shot (gh issue #9).
1
+ // revive-all — resume EVERY disconnected node in one shot.
2
2
  //
3
3
  // After a mass-disconnect event (a reboot, a killed login/tmux session, a mass
4
4
  // crash, or the daemon being down a while) many nodes end up with their
@@ -30,20 +30,20 @@ export const TERMINAL_BY_CHOICE = ['done', 'canceled'];
30
30
  * inbox watcher's own hold-and-flush) ever brings such a node back on its
31
31
  * own — only an explicit revive/reopen does. Shared by `isDisconnected`
32
32
  * (mass-revive scope, below) and `node message send --tier deferred`'s terminal-edge
33
- * rejection (gh #341) — a deferred entry appended to such a target would
33
+ * rejection — a deferred entry appended to such a target would
34
34
  * never be delivered, so that doorway rejects instead of silently holding
35
35
  * it forever. */
36
36
  export function hasNoNaturalCycle(meta) {
37
37
  return meta.final_report != null || TERMINAL_BY_CHOICE.includes(meta.status);
38
38
  }
39
39
  /** True when `meta`'s engine is NOT running but it has a resumable saved session
40
- * — the precise "disconnected" predicate (gh #9).
40
+ * — the precise "disconnected" predicate.
41
41
  *
42
42
  * - engine not running: no live `pi_pid` (a dead/absent broker pid).
43
43
  * - reconnectable: a captured pi session marker (`pi_session_file` or
44
44
  * `pi_session_id`) — without one there is nothing to reopen. reviveNode
45
45
  * resumes by the `.jsonl` path when it still exists, else relaunches fresh (a
46
- * bare `pi_session_id` alone no longer resumes — the broker preflight rejects
46
+ * bare `pi_session_id` alone does not resume — the broker preflight rejects
47
47
  * it — so such a node comes back fresh).
48
48
  * - `human`-kind rows are the `crtr human` bridge, never a pi engine, so they
49
49
  * are never "disconnected" (mirrors the daemon's superviseTick carve-out).
@@ -78,8 +78,8 @@ export function listDisconnected() {
78
78
  // IDENTITY-aware AND requires CONFIRMED alive: a bare isPidAlive would
79
79
  // misread a recycled pi_pid as "engine running" and SKIP the node, so it
80
80
  // never reaches reviveNode's own identity-aware double-launch guard
81
- // (Round 2 of the pid-reuse-liveness fix, fold-in Major). `dead` OR
82
- // `indeterminate` both count as disconnected/eligible here — only a
81
+ // guard. `dead` OR `indeterminate` both count as disconnected/eligible
82
+ // here — only a
83
83
  // positively confirmed-alive read is treated as "still connected".
84
84
  if (meta !== null && isDisconnected(meta, (pid) => recordedPidLiveness(pid, meta.pi_pid_identity) === 'alive'))
85
85
  out.push(meta);
@@ -100,7 +100,7 @@ export function reviveNode(nodeId, opts) {
100
100
  }
101
101
  // Fresh durable snapshot immediately before the lifecycle guard.
102
102
  meta = getNode(nodeId) ?? meta;
103
- // #339/#340: refuse to revive a LATCHED node — the reopen gate at every
103
+ // Refuse to revive a LATCHED node — the reopen gate at every
104
104
  // legitimate caller (node message send, node lifecycle revive) already checked this
105
105
  // before calling in, but a target can finalize in the gap between that
106
106
  // check and this call. This authoritative snapshot closes that race;
@@ -109,8 +109,7 @@ export interface SpawnChildResult {
109
109
  * (explicit `--parent` or the calling node's CRTR_NODE_ID). A --root spawn
110
110
  * does not: it is top-level by definition and the spawner identity is
111
111
  * provenance only — a human shell with no CRTR_NODE_ID is a legitimate root
112
- * spawner (regression: `crtr node new --root` from outside a node used to
113
- * throw here). */
112
+ * spawner, so `crtr node new --root` from outside a node must not throw. */
114
113
  export declare function resolveSpawner(parent: string | undefined, ctxNodeId: string | null, root: boolean): string | null;
115
114
  /** Spawn a node from a live node. By default a managed terminal worker in a
116
115
  * background window, with the spawner auto-subscribed (active) via spawnNode.
@@ -187,8 +187,7 @@ export async function assertLaunchModelRegistered(request, options, registry) {
187
187
  * (explicit `--parent` or the calling node's CRTR_NODE_ID). A --root spawn
188
188
  * does not: it is top-level by definition and the spawner identity is
189
189
  * provenance only — a human shell with no CRTR_NODE_ID is a legitimate root
190
- * spawner (regression: `crtr node new --root` from outside a node used to
191
- * throw here). */
190
+ * spawner, so `crtr node new --root` from outside a node must not throw. */
192
191
  export function resolveSpawner(parent, ctxNodeId, root) {
193
192
  const spawner = parent ?? ctxNodeId ?? null;
194
193
  if (!root && spawner === null) {
@@ -218,7 +217,7 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
218
217
  const attendedRoot = root && lifecycle === 'resident';
219
218
  // Born WITHOUT a name — the canvas-goal-capture extension names it async from
220
219
  // its first message (the kickoff task) inside its own pi process, so spawn
221
- // never blocks on the LLM naming round-trip (the 2-3s freeze it used to cost).
220
+ // never blocks on the LLM naming round-trip (a 2-3s freeze).
222
221
  const wantsWorktree = opts.worktree === true;
223
222
  const nodeId = wantsWorktree ? (opts.nodeId ?? newNodeId()) : opts.nodeId;
224
223
  // Preflight: resolve every fallible input BEFORE creating the worktree or the
@@ -3,15 +3,13 @@ import { dirname, resolve } from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { nodeDir, contextDir } from '../canvas/paths.js';
5
5
  import { InputError } from '../io.js';
6
+ import { isRecord } from '../../shared/predicates.js';
6
7
  export function outputSchemaPath(nodeId) {
7
8
  return `${nodeDir(nodeId)}/output-schema.json`;
8
9
  }
9
10
  export function outputResultPath(nodeId) {
10
11
  return `${contextDir(nodeId)}/result.json`;
11
12
  }
12
- function isObject(value) {
13
- return value !== null && typeof value === 'object' && !Array.isArray(value);
14
- }
15
13
  export function parseOutputSchemaValue(value) {
16
14
  const raw = value?.trim();
17
15
  if (raw === undefined || raw === '')
@@ -49,7 +47,7 @@ export function parseOutputSchemaValue(value) {
49
47
  next: 'Pass valid JSON whose top-level value is an object.',
50
48
  });
51
49
  }
52
- if (!isObject(parsed)) {
50
+ if (!isRecord(parsed)) {
53
51
  throw new InputError({
54
52
  error: 'bad_output_schema',
55
53
  message: `output schema from ${source} must be a JSON object`,
@@ -91,7 +89,7 @@ export function readOutputRequest(nodeId) {
91
89
  catch (err) {
92
90
  return { state: 'invalid', error: `request file is not valid JSON: ${err.message}` };
93
91
  }
94
- if (!isObject(parsed)) {
92
+ if (!isRecord(parsed)) {
95
93
  return { state: 'invalid', error: 'request file top-level value must be an object' };
96
94
  }
97
95
  const mode = parsed['mode'];
@@ -99,7 +97,7 @@ export function readOutputRequest(nodeId) {
99
97
  if (mode !== 'terminal' && mode !== 'oneoff') {
100
98
  return { state: 'invalid', error: `mode must be 'terminal' or 'oneoff', got ${JSON.stringify(mode)}` };
101
99
  }
102
- if (!isObject(schema)) {
100
+ if (!isRecord(schema)) {
103
101
  return { state: 'invalid', error: `schema must be an object, got ${typeof schema}` };
104
102
  }
105
103
  return { state: 'valid', request: { mode, schema } };
@@ -7,7 +7,7 @@ import { tmpdir } from 'node:os';
7
7
  import { join } from 'node:path';
8
8
  import { ATTACH_CONTROL_BINDINGS, BINDING_CATALOG, BINDING_IDS, encodeAttachControlInput, formatGesture, isAttachPaneBinding, resolveUserKeybindingSettings, } from '../keybindings/index.js';
9
9
  import { normalizeTmuxPassthrough } from '../config.js';
10
- import { withExclusiveDirectoryLock } from '../human/convention.js';
10
+ import { withExclusiveDirectoryLock } from '../exclusive-lock.js';
11
11
  import { surfaceTmuxStyleArgs } from './surface-bg.js';
12
12
  import { runTmux as tmux, shellQuote } from './tmux-driver.js';
13
13
  // ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  import { type AttachBindingId } from '../keybindings/index.js';
2
- /** POSIX single-quote escaping for one shell word. */
3
- export declare function shellQuote(s: string): string;
2
+ import { shellQuote } from '../../shared/shell-quote.js';
3
+ export { shellQuote };
4
4
  declare function tmux(args: string[]): {
5
5
  ok: boolean;
6
6
  stdout: string;
@@ -6,13 +6,11 @@
6
6
  import { execFile, spawnSync } from 'node:child_process';
7
7
  import { encodeAttachControlInput, } from '../keybindings/index.js';
8
8
  import { bundledPiCommandForShell } from './pi-cli.js';
9
+ import { shellQuote } from '../../shared/shell-quote.js';
9
10
  // ---------------------------------------------------------------------------
10
- // Shell quoting + tmux invocation
11
+ // tmux invocation
11
12
  // ---------------------------------------------------------------------------
12
- /** POSIX single-quote escaping for one shell word. */
13
- export function shellQuote(s) {
14
- return `'${s.replace(/'/g, `'\\''`)}'`;
15
- }
13
+ export { shellQuote };
16
14
  function tmux(args) {
17
15
  const r = spawnSync('tmux', args, { encoding: 'utf8' });
18
16
  return {
@@ -49,9 +49,10 @@ const SUMMARY_SYSTEM_PROMPT = 'Create a structured recap of this coding-agent to
49
49
  'Omit inbox receipt, delivery, and queueing; node-response and report bookkeeping; help lookups; status checks; and summary-generation activity. ' +
50
50
  'A node response may support a substantive outcome, but its arrival or resummarization is never itself an outcome. ' +
51
51
  'If no meaningful outcome remains after those omissions, submit the single bullet "No meaningful changes." ' +
52
- 'Count nodes created and distinct files created, modified, or deleted only when evidenced by the supplied tool arguments or results; otherwise use zero, even when the corresponding mechanics are omitted from bullets. ' +
52
+ 'Count nodes created, and separately count distinct files removed from disk, only when the supplied tool arguments or results evidence them; otherwise use zero, even when the corresponding mechanics are omitted from bullets. ' +
53
+ 'A deleted file is one that no longer exists after the segment, never a file that was merely edited or overwritten, and a recursive or wildcard removal of an unknown number of files counts as one. ' +
53
54
  'Do not invent success when a result shows failure. ' +
54
- 'Call submit_summary exactly once with bullets, nodesSpawned, and filesEdited, then stop.';
55
+ 'Call submit_summary exactly once with bullets, nodesSpawned, and filesDeleted, then stop.';
55
56
  const SUBMIT_SUMMARY_SCHEMA = Type.Object({
56
57
  bullets: Type.Array(Type.String({
57
58
  minLength: 1,
@@ -62,9 +63,9 @@ const SUBMIT_SUMMARY_SCHEMA = Type.Object({
62
63
  minimum: 0,
63
64
  description: 'Count of nodes created during the supplied segment, or zero when none are evidenced.',
64
65
  }),
65
- filesEdited: Type.Integer({
66
+ filesDeleted: Type.Integer({
66
67
  minimum: 0,
67
- description: 'Count of distinct files created, modified, or deleted during the supplied segment, or zero when none are evidenced.',
68
+ description: 'Count of distinct files removed from disk during the supplied segment, or zero when none are evidenced.',
68
69
  }),
69
70
  }, { additionalProperties: false });
70
71
  /** Generate a structured summary object, or null on every failure. */
@@ -16,10 +16,11 @@ export declare function warmPoolEnabled(): boolean;
16
16
  export interface WarmRequest {
17
17
  kind: string;
18
18
  mode: Mode;
19
+ /** Where the node runs — already resolved by the create handler (pin >
20
+ * spawner > profile home), never a raw caller directory. */
19
21
  cwd: string;
20
- /** The RAW `--profile` operand: a string to pin, `null` for explicitly no
21
- * profile, `undefined` to let the daemon resolve one for the cwd. */
22
- profile: string | null | undefined;
22
+ /** The RESOLVED profile id, or null for explicitly no profile. */
23
+ profileId: string | null;
23
24
  model: string | null;
24
25
  situationalContext: string | null;
25
26
  name: string | null;
@@ -43,9 +44,9 @@ export declare function reapStaleSpares(): string[];
43
44
  * throws and never blocks a caller: a failed mint just means the next create
44
45
  * for this key is cold again. */
45
46
  export declare function refillWarmPool(request: WarmRequest): void;
46
- /** Mint a spare for the most recently used project recipes so the FIRST create
47
- * after crtrd boots is warm too, not only the second. The recipe is sourced
48
- * from real recent roots — (cwd, profile) is the whole of it, since everything
49
- * else a claim reshapes live. Fire-and-forget: returns as soon as the mints
50
- * are scheduled, never blocking the daemon's work lane. */
47
+ /** Mint a spare for the most recently used profiles so the FIRST create after
48
+ * crtrd boots is warm too, not only the second. The profile is the whole
49
+ * recipe: it supplies the cwd, and everything else a claim reshapes live.
50
+ * Fire-and-forget — returns as soon as the mints are scheduled, never blocking
51
+ * the daemon's work lane. */
51
52
  export declare function prewarmRecentRecipes(): void;