@north-light/crouter 0.3.176 → 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 (140) 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/reports.d.ts +1 -1
  5. package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
  6. package/dist/clients/attach/config.js +5 -5
  7. package/dist/clients/attach/input/clipboard-image.js +2 -2
  8. package/dist/clients/attach/input/ref-autocomplete.js +1 -1
  9. package/dist/clients/attach/input/titled-editor.d.ts +12 -13
  10. package/dist/clients/attach/input/titled-editor.js +14 -15
  11. package/dist/clients/attach/overlays/dialogs.d.ts +1 -1
  12. package/dist/clients/attach/overlays/dialogs.js +2 -2
  13. package/dist/clients/attach/overlays/mcp.js +5 -12
  14. package/dist/clients/attach/render/chat-view.d.ts +1 -1
  15. package/dist/clients/attach/render/chat-view.js +2 -2
  16. package/dist/clients/attach/render/group-recap.js +7 -0
  17. package/dist/clients/attach/session/context.js +3 -3
  18. package/dist/clients/attach/session/editor-inventory.js +6 -6
  19. package/dist/clients/attach/session/input-wiring.js +1 -1
  20. package/dist/clients/attach/session/teardown.js +7 -9
  21. package/dist/clients/attach/slash/dispatch.d.ts +1 -2
  22. package/dist/clients/attach/slash/dispatch.js +4 -8
  23. package/dist/clients/attach/viewer.js +394 -394
  24. package/dist/clients/inbox/controller.js +3 -2
  25. package/dist/clients/inbox/deck-adapter.js +3 -2
  26. package/dist/clients/surfaces/host.d.ts +1 -1
  27. package/dist/commands/api-client.d.ts +12 -13
  28. package/dist/commands/api-client.js +15 -16
  29. package/dist/commands/chord.js +1 -2
  30. package/dist/commands/human/queue.js +6 -5
  31. package/dist/commands/memory/find.js +3 -3
  32. package/dist/commands/node/lifecycle.js +1 -1
  33. package/dist/commands/node-lifecycle-revive.d.ts +1 -1
  34. package/dist/commands/node-lifecycle-revive.js +3 -3
  35. package/dist/commands/push.js +1 -1
  36. package/dist/commands/surface/node/placement.d.ts +1 -1
  37. package/dist/commands/surface/node/placement.js +1 -1
  38. package/dist/commands/surface-edit.js +1 -4
  39. package/dist/commands/sys/panels/panel.d.ts +3 -3
  40. package/dist/commands/sys/panels/panel.js +3 -3
  41. package/dist/commands/sys/setup-core.js +1 -7
  42. package/dist/commands/sys/sync-deps.js +1 -3
  43. package/dist/commands/sys/sync-project-guidance.js +2 -11
  44. package/dist/commands/sys/sysprompt.js +1 -3
  45. package/dist/core/__tests__/fixtures/fake-engine.js +1 -1
  46. package/dist/core/__tests__/helpers/harness.d.ts +1 -1
  47. package/dist/core/__tests__/helpers/harness.js +1 -1
  48. package/dist/core/canvas/boot.js +1 -1
  49. package/dist/core/canvas/browse/app.js +2 -2
  50. package/dist/core/canvas/canvas.d.ts +3 -3
  51. package/dist/core/canvas/canvas.js +3 -3
  52. package/dist/core/canvas/daemon-owner.js +1 -2
  53. package/dist/core/canvas/db.js +7 -8
  54. package/dist/core/canvas/index.js +2 -2
  55. package/dist/core/canvas/pid.d.ts +12 -14
  56. package/dist/core/canvas/pid.js +16 -20
  57. package/dist/core/canvas/remote-transport.js +2 -2
  58. package/dist/core/canvas/status-glyph.d.ts +1 -1
  59. package/dist/core/canvas/status-glyph.js +1 -1
  60. package/dist/core/canvas/types.d.ts +1 -1
  61. package/dist/core/command-manifests/manifest.js +4 -6
  62. package/dist/core/command-manifests/schema.js +10 -12
  63. package/dist/core/command-plugins/transport/exec-invoke.js +5 -7
  64. package/dist/core/command-plugins/transport/http-invoke.js +4 -6
  65. package/dist/core/config.d.ts +1 -1
  66. package/dist/core/config.js +2 -2
  67. package/dist/core/events/read.js +1 -3
  68. package/dist/core/fault-classifier.js +1 -3
  69. package/dist/core/fs-utils.d.ts +7 -0
  70. package/dist/core/fs-utils.js +29 -2
  71. package/dist/core/host-exports/export.d.ts +4 -5
  72. package/dist/core/host-exports/export.js +4 -5
  73. package/dist/core/human/claim.js +3 -2
  74. package/dist/core/human/convention.d.ts +0 -1
  75. package/dist/core/human/convention.js +1 -9
  76. package/dist/core/human/scan.js +5 -4
  77. package/dist/core/profiles/manifest.d.ts +1 -1
  78. package/dist/core/profiles/manifest.js +1 -1
  79. package/dist/core/review/types.d.ts +1 -1
  80. package/dist/core/review/types.js +1 -1
  81. package/dist/core/runtime/broker/frame-dispatch.js +4 -4
  82. package/dist/core/runtime/broker-cli.js +1 -1
  83. package/dist/core/runtime/broker-protocol.d.ts +9 -10
  84. package/dist/core/runtime/broker-protocol.js +9 -10
  85. package/dist/core/runtime/broker-sdk.d.ts +1 -1
  86. package/dist/core/runtime/broker.js +6 -6
  87. package/dist/core/runtime/host.js +14 -14
  88. package/dist/core/runtime/launch.d.ts +1 -1
  89. package/dist/core/runtime/launch.js +1 -1
  90. package/dist/core/runtime/lifecycle.js +16 -21
  91. package/dist/core/runtime/node-read.js +2 -3
  92. package/dist/core/runtime/package-health.js +1 -8
  93. package/dist/core/runtime/persona.js +2 -2
  94. package/dist/core/runtime/pi-vendored.d.ts +4 -2
  95. package/dist/core/runtime/pi-vendored.js +5 -16
  96. package/dist/core/runtime/promote.js +1 -1
  97. package/dist/core/runtime/reopen.js +2 -2
  98. package/dist/core/runtime/revive-all.d.ts +3 -3
  99. package/dist/core/runtime/revive-all.js +6 -6
  100. package/dist/core/runtime/revive.js +1 -1
  101. package/dist/core/runtime/spawn.d.ts +1 -2
  102. package/dist/core/runtime/spawn.js +2 -3
  103. package/dist/core/runtime/structured-output.js +4 -6
  104. package/dist/core/runtime/tmux-driver.d.ts +2 -2
  105. package/dist/core/runtime/tmux-driver.js +3 -5
  106. package/dist/core/runtime/tool-group-summary.js +5 -4
  107. package/dist/core/runtime/warm-pool.js +1 -1
  108. package/dist/core/scope.js +2 -9
  109. package/dist/core/self-update.js +3 -3
  110. package/dist/core/spawn.d.ts +0 -1
  111. package/dist/core/spawn.js +1 -4
  112. package/dist/core/substrate/on-read.js +1 -9
  113. package/dist/core/wake.js +2 -2
  114. package/dist/core/worktree.d.ts +3 -3
  115. package/dist/core/worktree.js +5 -7
  116. package/dist/daemon/api/bridge.js +1 -1
  117. package/dist/daemon/api/handlers/broker-ops.js +1 -2
  118. package/dist/daemon/api/handlers/messages.js +2 -2
  119. package/dist/daemon/api/handlers/nodes.js +1 -1
  120. package/dist/daemon/api/handlers/reports.js +1 -1
  121. package/dist/daemon/api/handlers/validate.d.ts +2 -1
  122. package/dist/daemon/api/handlers/validate.js +2 -3
  123. package/dist/daemon/cron-run.js +7 -7
  124. package/dist/daemon/human/finish.js +3 -2
  125. package/dist/daemon/manage.d.ts +6 -6
  126. package/dist/daemon/manage.js +15 -16
  127. package/dist/pi-extensions/canvas-context-intro.js +1 -14
  128. package/dist/pi-extensions/canvas-doc-substrate.js +1 -1
  129. package/dist/pi-extensions/canvas-review-boundary.js +1 -14
  130. package/dist/pi-extensions/truncate.d.ts +5 -0
  131. package/dist/pi-extensions/truncate.js +14 -0
  132. package/dist/shared/predicates.d.ts +2 -0
  133. package/dist/shared/predicates.js +4 -0
  134. package/dist/shared/shell-quote.d.ts +3 -0
  135. package/dist/shared/shell-quote.js +5 -0
  136. package/dist/shared/tool-groups.d.ts +4 -2
  137. package/dist/shared/tool-groups.js +3 -3
  138. package/dist/types.d.ts +4 -5
  139. package/package.json +1 -1
  140. package/runtime.lock.json +2 -2
@@ -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;
@@ -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
@@ -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
@@ -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
  },
@@ -14,6 +14,7 @@ import { detectNerdFont, nerdFontInstallCommand, NERD_FONT_SELECT_HINT } from '.
14
14
  import { ensureRenderer, isRendererReady } from '../../core/termrender/termrender.js';
15
15
  import { installFromMarketplace } from '../pkg/plugin-manage.js';
16
16
  import { findPluginByName } from '../../core/resolver.js';
17
+ import { expandTilde } from '../../core/fs-utils.js';
17
18
  function isKeybindingsObject(value) {
18
19
  return value !== null && typeof value === 'object' && !Array.isArray(value);
19
20
  }
@@ -214,13 +215,6 @@ export function buildSetupManifest(resolveBuiltinPackageDir = builtinPiPackageDi
214
215
  };
215
216
  });
216
217
  }
217
- function expandTilde(pathValue, homeDir) {
218
- if (pathValue === '~')
219
- return homeDir;
220
- if (pathValue.startsWith('~/'))
221
- return join(homeDir, pathValue.slice(2));
222
- return pathValue;
223
- }
224
218
  function npmPackageName(source) {
225
219
  if (!source.startsWith('npm:'))
226
220
  return null;
@@ -32,13 +32,11 @@ import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
32
32
  import { pathExists, readJsonIfExists, readText, walkFiles, writeText } from '../../core/fs-utils.js';
33
33
  import { serializeMemoryDoc } from '../memory/shared.js';
34
34
  import { routeFromDescription, scalarString } from './sync-shared.js';
35
+ import { isRecord } from '../../shared/predicates.js';
35
36
  /** Marker value written to the generated top INDEX's `generated-by` field —
36
37
  * the wipe guard: `deps` only ever deletes a deps/ dir whose INDEX carries
37
38
  * this marker. */
38
39
  export const DEPS_GENERATED_MARKER = 'crtr sys sync deps';
39
- function isRecord(value) {
40
- return typeof value === 'object' && value !== null && !Array.isArray(value);
41
- }
42
40
  function invalidDepsConfig(message) {
43
41
  throw usage(`--deps: invalid package.json crtr.sync.deps.exclude: ${message}`);
44
42
  }
@@ -3,7 +3,7 @@
3
3
  // `.crouter/memory/`. Each project guide becomes the root INDEX front door;
4
4
  // rules retain their explicit path routes, or use the workspace-open `.` route
5
5
  // when they are pathless.
6
- import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs';
6
+ import { existsSync, readdirSync, statSync } from 'node:fs';
7
7
  import { basename, dirname, join, relative, resolve, sep } from 'node:path';
8
8
  import { defineLeaf } from '../../core/command.js';
9
9
  import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
@@ -13,6 +13,7 @@ import { loadProfileManifest } from '../../core/profiles/manifest.js';
13
13
  import { CRTR_DIR_NAME } from '../../types.js';
14
14
  import { memoryFilePath, serializeMemoryDoc } from '../memory/shared.js';
15
15
  import { normalizedName, routeFromDescription, scalarString } from './sync-shared.js';
16
+ import { realpathOrSelf } from '../../core/fs-utils.js';
16
17
  // Directories a project-doc walk never descends into — build/dependency trees
17
18
  // plus crouter's own state dir.
18
19
  const PROJECT_DOC_JUNK_DIRS = new Set([
@@ -136,16 +137,6 @@ function gitWorktreeRoot(dir) {
136
137
  const top = res.stdout.trim();
137
138
  return top === '' ? undefined : resolve(top);
138
139
  }
139
- /** `realpathSync`, tolerant of a path that doesn't exist or can't be resolved
140
- * (falls back to the path as given). */
141
- function realpathOrSelf(p) {
142
- try {
143
- return realpathSync(p);
144
- }
145
- catch {
146
- return p;
147
- }
148
- }
149
140
  /** First Markdown H1 heading in the doc body, heading marker stripped. */
150
141
  function h1Title(body) {
151
142
  for (const raw of body.split('\n')) {
@@ -7,9 +7,7 @@ import { jobDir } from '../../core/canvas/paths.js';
7
7
  import { openNodeWindow } from '../../core/runtime/placement-tmux.js';
8
8
  import { ApiError } from '../../api/index.js';
9
9
  import { cliClient } from '../api-client.js';
10
- function shellQuote(s) {
11
- return `'${s.replace(/'/g, `'\\''`)}'`;
12
- }
10
+ import { shellQuote } from '../../shared/shell-quote.js';
13
11
  export const sysSyspromptLeaf = defineLeaf({
14
12
  name: 'sysprompt',
15
13
  description: 'print a node\'s assembled system prompt',
@@ -139,7 +139,7 @@ export class SessionManager {
139
139
  }
140
140
  }
141
141
  // ---------------------------------------------------------------------------
142
- // AgentSessionEvent stream synthesis (T8 / G1, G3, G4, G8). The fake emits a
142
+ // AgentSessionEvent stream synthesis. The fake emits a
143
143
  // REALISTIC streaming assistant turn through session.subscribe — the SAME channel
144
144
  // the real pi engine feeds the broker's fan-out — so the frames a `crtr surface attach`
145
145
  // client receives are byte-for-byte real AgentSessionEvents. The EMITTED event
@@ -37,7 +37,7 @@ export interface HarnessOpts {
37
37
  * harness does NOT gate on tmux, does NOT eagerly create the isolated tmux
38
38
  * session, and skips the per-pane `set-environment` seeding — nothing in a
39
39
  * headless run opens a window. CRTR_NODE_SESSION/CRTR_ROOT_SESSION are still
40
- * scrubbed/restored for env hygiene, but no longer route placement: a managed
40
+ * scrubbed/restored for env hygiene, but do not route placement: a managed
41
41
  * child opens NO viewer (it boots a detached broker, supervised by pid), and a
42
42
  * --root opens one viewer in the caller's current session. Headless tests must
43
43
  * use `spawnHeadlessChild`, never `spawnChild`. See createHeadlessHarness(). */
@@ -312,7 +312,7 @@ export async function createHarness(opts = {}) {
312
312
  }
313
313
  // Pre-create the isolated session on the DEFAULT server so teardown always
314
314
  // has a kill-sessions target.
315
- // ISOLATION ASSUMPTION (see header + MINOR-6): isolation is by SESSION NAME on
315
+ // ISOLATION ASSUMPTION (see header): isolation is by SESSION NAME on
316
316
  // the DEFAULT tmux server only. The runtime CLI shells `tmux` with no `-L`, so
317
317
  // a custom-socket server (`tmux -L foo`) would be invisible to it; this harness
318
318
  // therefore assumes the default socket and only ever kill-sessions, never the
@@ -148,7 +148,7 @@ export function reconcileBootLiveness(current = currentBootIdentity()) {
148
148
  const rows = db
149
149
  .prepare('SELECT node_id FROM nodes WHERE pi_pid IS NOT NULL')
150
150
  .all();
151
- // Clear the launch-time identity baseline (crouter#98 review finding 1)
151
+ // Clear the launch-time identity baseline
152
152
  // alongside `pi_pid` — a boot resets the OS process table, so any recorded
153
153
  // identity is as meaningless as the pid it was captured for; leaving it
154
154
  // behind would let a coincidentally-reused pid on the NEW boot compare
@@ -358,8 +358,8 @@ export async function runBrowse(opts) {
358
358
  // Coalesced search repaint. The expensive work per keystroke is recompute()
359
359
  // (a fuzzyMatch flatten over the whole corpus) + a full renderFrame. Doing it
360
360
  // synchronously on every key blocks the event loop, so fast typing batches into
361
- // the stdin reads — and batched chunks used to be dropped (see onKeySearch).
362
- // Instead, each key only mutates state.query (cheap) and marks the view dirty;
361
+ // the stdin reads, where a naive per-key handler drops batched chunks (see
362
+ // onKeySearch). Instead, each key only mutates state.query (cheap) and marks the view dirty;
363
363
  // a single setImmediate drains AFTER every queued stdin 'data' callback (poll
364
364
  // phase) has appended its chars, so N rapid keys collapse to ONE recompute+paint
365
365
  // and no keystroke is ignored. The sort may still take a moment — that's fine.
@@ -33,7 +33,7 @@ export declare function setPresence(nodeId: string, presence: {
33
33
  pane?: string | null;
34
34
  }): void;
35
35
  /** Record the live pi pid (daemon liveness signal) PLUS its launch-time
36
- * identity fingerprint, in one atomic write (crouter#98 review finding 1):
36
+ * identity fingerprint, in one atomic write:
37
37
  * `capturePidIdentities([pid])` is a signal-0-adjacent `ps -p <pid>` probe
38
38
  * taken RIGHT NOW, while `pid` is the process we just spawned/bound and
39
39
  * therefore definitely still exists — this is the LAUNCH-time baseline
@@ -242,7 +242,7 @@ export type TerminalGuardResult<T> = {
242
242
  } | {
243
243
  kind: 'missing';
244
244
  };
245
- /** #343: close the TOCTOU between checking a target has a natural cycle ahead
245
+ /** Close the TOCTOU between checking a target has a natural cycle ahead
246
246
  * of it and the write that depends on that (for example, a deferred inbox
247
247
  * append). Fresh-reads `nodeId`'s
248
248
  * (status, final_report) and either runs `body` (a write) or short-circuits
@@ -254,7 +254,7 @@ export type TerminalGuardResult<T> = {
254
254
  * write left open. Pass the shared `hasNoNaturalCycle` predicate rather than
255
255
  * a bespoke one so the two never drift.
256
256
  *
257
- * #343 review (Major): a dangling `nodeId` — the row deleted after the
257
+ * A dangling `nodeId` — the row deleted after the
258
258
  * caller's own selection but before this guard's fresh read — MUST NEVER run
259
259
  * `body`. Reporting `'missing'` distinctly from `'terminal'` (rather than
260
260
  * folding it into either the terminal branch or a fabricated "safe" empty
@@ -243,7 +243,7 @@ export function setPresence(nodeId, presence) {
243
243
  .run(presence.tmux_session ?? null, presence.window ?? null, presence.pane ?? null, nodeId);
244
244
  }
245
245
  /** Record the live pi pid (daemon liveness signal) PLUS its launch-time
246
- * identity fingerprint, in one atomic write (crouter#98 review finding 1):
246
+ * identity fingerprint, in one atomic write:
247
247
  * `capturePidIdentities([pid])` is a signal-0-adjacent `ps -p <pid>` probe
248
248
  * taken RIGHT NOW, while `pid` is the process we just spawned/bound and
249
249
  * therefore definitely still exists — this is the LAUNCH-time baseline
@@ -633,7 +633,7 @@ export function settleDeadMessageWait(nodeId, controller, deliver) {
633
633
  return result;
634
634
  });
635
635
  }
636
- /** #343: close the TOCTOU between checking a target has a natural cycle ahead
636
+ /** Close the TOCTOU between checking a target has a natural cycle ahead
637
637
  * of it and the write that depends on that (for example, a deferred inbox
638
638
  * append). Fresh-reads `nodeId`'s
639
639
  * (status, final_report) and either runs `body` (a write) or short-circuits
@@ -645,7 +645,7 @@ export function settleDeadMessageWait(nodeId, controller, deliver) {
645
645
  * write left open. Pass the shared `hasNoNaturalCycle` predicate rather than
646
646
  * a bespoke one so the two never drift.
647
647
  *
648
- * #343 review (Major): a dangling `nodeId` — the row deleted after the
648
+ * A dangling `nodeId` — the row deleted after the
649
649
  * caller's own selection but before this guard's fresh read — MUST NEVER run
650
650
  * `body`. Reporting `'missing'` distinctly from `'terminal'` (rather than
651
651
  * folding it into either the terminal branch or a fabricated "safe" empty
@@ -1,5 +1,4 @@
1
- // daemon-owner.ts — the canvas-layer authoritative daemon ownership primitive
2
- // (Phase 1 runtime-boundary, task D0).
1
+ // daemon-owner.ts — the canvas-layer authoritative daemon ownership primitive.
3
2
  //
4
3
  // A pidfile check followed by a later write is not singleton ownership: two
5
4
  // `crtrd` invocations racing a cold start can both read "no pidfile", both
@@ -58,10 +58,9 @@ function addNodeColumnIfMissing(db, columns, name, ddl) {
58
58
  db.exec(ddl);
59
59
  columns.add(name);
60
60
  }
61
- /** v2 — additive runtime columns the keystone (Phase 2) will make
62
- * authoritative: `intent, pi_pid, window, tmux_session`. `status` already
63
- * lives in the baseline row, so it is NOT re-added here. All four default to
64
- * NULL, so nothing observes a behavior change until a later phase reads them. */
61
+ /** v2 — the additive runtime columns: `intent, pi_pid, window, tmux_session`.
62
+ * `status` already lives in the baseline row, so it is NOT re-added here. All
63
+ * four default to NULL; v3 backfills them. */
65
64
  function addRuntimeColumns(db) {
66
65
  const columns = nodeColumns(db);
67
66
  addNodeColumnIfMissing(db, columns, 'intent', `ALTER TABLE nodes ADD COLUMN intent TEXT;`);
@@ -69,14 +68,14 @@ function addRuntimeColumns(db) {
69
68
  addNodeColumnIfMissing(db, columns, 'window', `ALTER TABLE nodes ADD COLUMN window TEXT;`);
70
69
  addNodeColumnIfMissing(db, columns, 'tmux_session', `ALTER TABLE nodes ADD COLUMN tmux_session TEXT;`);
71
70
  }
72
- /** v3 — DATA backfill (keystone, Phase 2). The runtime fields
71
+ /** v3 — DATA backfill. The runtime fields
73
72
  * (`intent, pi_pid, window, tmux_session`) become authoritative in the row;
74
73
  * copy each existing node's values out of its meta.json into the row columns
75
74
  * once, so the version boundary loses no live state. `status` already mirrors
76
75
  * the row, so it is not re-copied.
77
76
  *
78
- * LAYERING NOTE (explicitly sanctioned by the runtime-fix plan): a *data*
79
- * migration must read meta.json, which db.ts normally would not. Reading it
77
+ * LAYERING NOTE — a sanctioned exception: a *data* migration must read
78
+ * meta.json, which db.ts normally would not. Reading it
80
79
  * directly here — via paths.ts, a one-time, clearly-labeled boot-time data
81
80
  * migration — is the deliberate choice over splitting the `user_version`
82
81
  * counter across two modules. Idempotent and gated: it runs exactly once at the
@@ -367,7 +366,7 @@ function addProfileIdColumn(db) {
367
366
  * identity fingerprint (`pid.ts`'s `composeIdentity`) recorded alongside
368
367
  * `pi_pid` (v2) by `recordPid`, so `headlessBrokerHost.teardown()` can refuse
369
368
  * to signal a `pi_pid` the OS has since recycled for an unrelated process
370
- * (crouter#98 final review, Fix 1 — see `NodeRuntime.pi_pid_identity`).
369
+ * (see `NodeRuntime.pi_pid_identity`).
371
370
  * Defaults NULL ⇒ no baseline ⇒ the guard fails open, exactly like a node
372
371
  * booted before this column existed. No data backfill: an existing `pi_pid`
373
372
  * with no recorded identity simply has no launch-time baseline to compare
@@ -1,5 +1,5 @@
1
- // The canvas: one global graph of nodes + edges. Phase 0 of the pi-native
2
- // agent runtime. Topology in sqlite (WAL), node flesh on disk.
1
+ // The canvas: one global graph of nodes + edges.
2
+ // Topology in sqlite (WAL), node flesh on disk.
3
3
  export * from './types.js';
4
4
  export * from './labels.js';
5
5
  export * from './paths.js';