@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
@@ -8,8 +8,7 @@ export declare function isPidAlive(pid: number | null | undefined): boolean;
8
8
  * `composeIdentity` fingerprints are compared (this module's own
9
9
  * `recordedPidLiveness`, `captureTeardownSnapshot`'s reuse check, and
10
10
  * `killProcessTreePids`'s reuse check) — centralized so no call site can
11
- * drift from this rule (review Major #1, Round 2 of the pid-reuse-liveness
12
- * fix).
11
+ * drift from this rule.
13
12
  *
14
13
  * Two hazards this closes, both in the false-DEAD direction (the dangerous
15
14
  * one — a false DEAD lets `reviveNode`'s double-launch guard relaunch a
@@ -94,7 +93,7 @@ export declare function recordedPidLiveness(pid: number | null | undefined, expe
94
93
  * swallowing ESRCH (already gone). Used by cron cancellation, timeout, and
95
94
  * stale-lease recovery. Lives beside isPidAlive: canvas/ is
96
95
  * the lowest shared layer every process-liveness/-teardown primitive sits at.
97
- * Guards `pid` before signaling (review nit, crouter#98): never hit pid 0
96
+ * Guards `pid` before signaling: never hit pid 0
98
97
  * (which means "my own group") or a negative/non-integer value. */
99
98
  export declare function killProcessGroup(pid: number, signal?: NodeJS.Signals): void;
100
99
  /** The kernel `boot_id` (`/proc/sys/kernel/random/boot_id`) is stable for the
@@ -155,8 +154,8 @@ export interface PsLivenessSnapshot {
155
154
  }
156
155
  export declare function captureLivenessSnapshot(): PsLivenessSnapshot | null;
157
156
  /** Every transitive descendant of `rootPid`, discovered by walking `ps`'s
158
- * pid/ppid table (BFS) — NOT process-GROUP membership. This is the fix for
159
- * crouton-labs/crouter#98: the pi SDK's bash tool spawns its shell child with
157
+ * pid/ppid table (BFS) — NOT process-GROUP membership. The pi SDK's bash tool
158
+ * spawns its shell child with
160
159
  * `detached: true` (verified in `@earendil-works/pi-agent-core`'s
161
160
  * `harness/env/nodejs.js`), which calls `setsid()` and puts that child in a
162
161
  * brand-new process GROUP of its own — so `kill(-rootPid)` never reaches it.
@@ -173,7 +172,7 @@ export declare function descendantPids(rootPid: number): number[];
173
172
  * batched `ps -p <list>` call. `lstart` is a standard format keyword on BOTH
174
173
  * BSD ps (macOS) and procps-ng (Linux), so this needs no `/proc` parsing or
175
174
  * other Linux-only machinery. Used to guard against PID REUSE during the
176
- * multi-second teardown escalation ladder (crouter#98 review finding 3): a
175
+ * multi-second teardown escalation ladder: a
177
176
  * captured pid can exit and the OS can recycle it for an unrelated process
178
177
  * before the SIGKILL rung fires, and a bare `kill(pid, 0)` / `kill(pid, sig)`
179
178
  * cannot tell the difference.
@@ -182,9 +181,9 @@ export declare function descendantPids(rootPid: number): number[];
182
181
  * could not be spawned, or was killed by the timeout): a probe failure means
183
182
  * we have NO information, which is a fundamentally different outcome from a
184
183
  * probe that SUCCEEDED and simply found no row for a given pid (that pid is
185
- * genuinely gone). Collapsing these two into the same empty map was the
186
- * final-review bug (crouter#98 signal-safety finding): `killProcessTreePids`
187
- * cannot tell "pid confirmed gone" from "we don't know" without this
184
+ * genuinely gone). Collapsing these two into the same empty map is unsafe:
185
+ * `killProcessTreePids` cannot tell "pid confirmed gone" from "we don't know"
186
+ * without this
188
187
  * distinction, and misreading a failed probe as positive proof of
189
188
  * absence/reuse either signals a reused stranger or — worse — silently skips
190
189
  * reaping a real live orphan. A successful call may still return a map with
@@ -210,7 +209,7 @@ export declare function capturePidIdentities(pids: readonly number[]): Map<numbe
210
209
  * signal. */
211
210
  export declare function capturePidCommand(pid: number): string | null;
212
211
  /** The full pre-signal snapshot `teardown()` needs, taken ONCE, from ONE `ps`
213
- * table (crouter#98 final review, Fix 1): the process TREE rooted at `rootPid`
212
+ * table: the process TREE rooted at `rootPid`
214
213
  * (itself + every transitive descendant) alongside each member's identity
215
214
  * fingerprint — PLUS, load-bearing, a launch-time-identity REUSE check on
216
215
  * `rootPid` itself before the tree is even walked.
@@ -259,21 +258,20 @@ export declare function captureTeardownSnapshot(rootPid: number, expectedIdentit
259
258
  * caller before sending any signal — see the doc on `isProcessTreeAlive` for
260
259
  * why re-deriving via `ps` after signaling starts is unsafe). Signals EACH
261
260
  * pid's OWN process group (a detached SDK-spawned descendant is its own
262
- * group leader — the pi bash tool's shell child, crouton-labs/crouter#98)
261
+ * group leader — the pi bash tool's shell child)
263
262
  * PLUS a plain `kill(pid)` on each (belt-and-suspenders for a pid that is
264
263
  * NOT its own group leader). Every signal is best-effort — `killProcessGroup`
265
264
  * swallows ESRCH and the plain `kill` here does too — so firing this against
266
265
  * an already-fully-dead snapshot is a harmless no-op.
267
266
  *
268
- * `identities`, when supplied (crouter#98 review finding 3), is a
267
+ * `identities`, when supplied, is a
269
268
  * `capturePidIdentities` snapshot taken alongside the pid list at the SAME
270
269
  * moment — `null` if that INITIAL capture's `ps` probe itself failed (see
271
270
  * `capturePidIdentities`'s doc). Before signaling EITHER the group or the
272
271
  * plain kill for a pid, this re-derives CURRENT identities in one fresh
273
272
  * batched call and compares — but ONLY skips on POSITIVE proof of reuse; any
274
273
  * failure or absence of evidence falls through to the normal signal path
275
- * (fail-open, matching this module's pre-guard behavior — final review,
276
- * crouter#98 signal-safety finding):
274
+ * (fail-open, matching this module's unguarded behavior):
277
275
  *
278
276
  * - baseline identity present AND the current probe SUCCEEDED AND the
279
277
  * current identity for this pid is present AND DIFFERENT — the OS has
@@ -4,9 +4,8 @@
4
4
  // LOWEST shared layer — so canvas/, runtime/, AND daemon/ can all import it
5
5
  // "down" without a reverse-layer violation (runtime/ sits above canvas/, daemon/
6
6
  // above runtime/). It is a pure process-existence utility, NOT data-model access,
7
- // so it is exempt from the "only canvas.ts touches the db" rule. Collapses the
8
- // four near-identical copies that used to live in canvas.ts, revive.ts,
9
- // placement.ts, and crtrd.ts into one (Phase 3 review reuse MINOR-1).
7
+ // so it is exempt from the "only canvas.ts touches the db" rule. canvas.ts,
8
+ // revive.ts, placement.ts, and crtrd.ts all probe liveness through it.
10
9
  import { spawnSync } from 'node:child_process';
11
10
  import { readFileSync } from 'node:fs';
12
11
  import { readKernelBootId } from './boot-id.js';
@@ -62,8 +61,7 @@ const BOOT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12
62
61
  * purely-numeric suffix counts as ticks (the shape `procStartTicks` and
63
62
  * `composeIdentity` guarantee); a malformed or empty suffix (`A#`, `A#bad`,
64
63
  * `A#1#2`) is treated as ABSENT ticks, so the base compare then applies — a
65
- * garbage suffix must never be trusted as a fine discriminator (review
66
- * Minor). */
64
+ * garbage suffix must never be trusted as a fine discriminator. */
67
65
  function splitIdentity(identity) {
68
66
  const i = identity.indexOf('#');
69
67
  if (i === -1)
@@ -75,8 +73,7 @@ function splitIdentity(identity) {
75
73
  * `composeIdentity` fingerprints are compared (this module's own
76
74
  * `recordedPidLiveness`, `captureTeardownSnapshot`'s reuse check, and
77
75
  * `killProcessTreePids`'s reuse check) — centralized so no call site can
78
- * drift from this rule (review Major #1, Round 2 of the pid-reuse-liveness
79
- * fix).
76
+ * drift from this rule.
80
77
  *
81
78
  * Two hazards this closes, both in the false-DEAD direction (the dangerous
82
79
  * one — a false DEAD lets `reviveNode`'s double-launch guard relaunch a
@@ -208,7 +205,7 @@ function logPidLivenessDecision(pid, isPidAliveResult, expectedIdentity, probeOu
208
205
  * swallowing ESRCH (already gone). Used by cron cancellation, timeout, and
209
206
  * stale-lease recovery. Lives beside isPidAlive: canvas/ is
210
207
  * the lowest shared layer every process-liveness/-teardown primitive sits at.
211
- * Guards `pid` before signaling (review nit, crouter#98): never hit pid 0
208
+ * Guards `pid` before signaling: never hit pid 0
212
209
  * (which means "my own group") or a negative/non-integer value. */
213
210
  export function killProcessGroup(pid, signal = 'SIGTERM') {
214
211
  if (!Number.isInteger(pid) || pid <= 0)
@@ -220,8 +217,8 @@ export function killProcessGroup(pid, signal = 'SIGTERM') {
220
217
  /* already gone */
221
218
  }
222
219
  }
223
- /** Linux-only higher-resolution discriminator layered onto `lstart` (crouter#98
224
- * review finding, minor): `ps lstart` is only second-granular, so a PID reused
220
+ /** Linux-only higher-resolution discriminator layered onto `lstart`:
221
+ * `ps lstart` is only second-granular, so a PID reused
225
222
  * within the same wall-clock second compares equal and slips past the reuse
226
223
  * guard. `/proc/<pid>/stat`'s `starttime` field is the kernel's own jiffies-
227
224
  * since-boot counter for that process — effectively unique per pid even for
@@ -395,8 +392,8 @@ function descendantPidsFrom(rootPid, childrenOf) {
395
392
  return out;
396
393
  }
397
394
  /** Every transitive descendant of `rootPid`, discovered by walking `ps`'s
398
- * pid/ppid table (BFS) — NOT process-GROUP membership. This is the fix for
399
- * crouton-labs/crouter#98: the pi SDK's bash tool spawns its shell child with
395
+ * pid/ppid table (BFS) — NOT process-GROUP membership. The pi SDK's bash tool
396
+ * spawns its shell child with
400
397
  * `detached: true` (verified in `@earendil-works/pi-agent-core`'s
401
398
  * `harness/env/nodejs.js`), which calls `setsid()` and puts that child in a
402
399
  * brand-new process GROUP of its own — so `kill(-rootPid)` never reaches it.
@@ -420,7 +417,7 @@ export function descendantPids(rootPid) {
420
417
  * batched `ps -p <list>` call. `lstart` is a standard format keyword on BOTH
421
418
  * BSD ps (macOS) and procps-ng (Linux), so this needs no `/proc` parsing or
422
419
  * other Linux-only machinery. Used to guard against PID REUSE during the
423
- * multi-second teardown escalation ladder (crouter#98 review finding 3): a
420
+ * multi-second teardown escalation ladder: a
424
421
  * captured pid can exit and the OS can recycle it for an unrelated process
425
422
  * before the SIGKILL rung fires, and a bare `kill(pid, 0)` / `kill(pid, sig)`
426
423
  * cannot tell the difference.
@@ -429,9 +426,9 @@ export function descendantPids(rootPid) {
429
426
  * could not be spawned, or was killed by the timeout): a probe failure means
430
427
  * we have NO information, which is a fundamentally different outcome from a
431
428
  * probe that SUCCEEDED and simply found no row for a given pid (that pid is
432
- * genuinely gone). Collapsing these two into the same empty map was the
433
- * final-review bug (crouter#98 signal-safety finding): `killProcessTreePids`
434
- * cannot tell "pid confirmed gone" from "we don't know" without this
429
+ * genuinely gone). Collapsing these two into the same empty map is unsafe:
430
+ * `killProcessTreePids` cannot tell "pid confirmed gone" from "we don't know"
431
+ * without this
435
432
  * distinction, and misreading a failed probe as positive proof of
436
433
  * absence/reuse either signals a reused stranger or — worse — silently skips
437
434
  * reaping a real live orphan. A successful call may still return a map with
@@ -529,21 +526,20 @@ export function captureTeardownSnapshot(rootPid, expectedIdentity) {
529
526
  * caller before sending any signal — see the doc on `isProcessTreeAlive` for
530
527
  * why re-deriving via `ps` after signaling starts is unsafe). Signals EACH
531
528
  * pid's OWN process group (a detached SDK-spawned descendant is its own
532
- * group leader — the pi bash tool's shell child, crouton-labs/crouter#98)
529
+ * group leader — the pi bash tool's shell child)
533
530
  * PLUS a plain `kill(pid)` on each (belt-and-suspenders for a pid that is
534
531
  * NOT its own group leader). Every signal is best-effort — `killProcessGroup`
535
532
  * swallows ESRCH and the plain `kill` here does too — so firing this against
536
533
  * an already-fully-dead snapshot is a harmless no-op.
537
534
  *
538
- * `identities`, when supplied (crouter#98 review finding 3), is a
535
+ * `identities`, when supplied, is a
539
536
  * `capturePidIdentities` snapshot taken alongside the pid list at the SAME
540
537
  * moment — `null` if that INITIAL capture's `ps` probe itself failed (see
541
538
  * `capturePidIdentities`'s doc). Before signaling EITHER the group or the
542
539
  * plain kill for a pid, this re-derives CURRENT identities in one fresh
543
540
  * batched call and compares — but ONLY skips on POSITIVE proof of reuse; any
544
541
  * failure or absence of evidence falls through to the normal signal path
545
- * (fail-open, matching this module's pre-guard behavior — final review,
546
- * crouter#98 signal-safety finding):
542
+ * (fail-open, matching this module's unguarded behavior):
547
543
  *
548
544
  * - baseline identity present AND the current probe SUCCEEDED AND the
549
545
  * current identity for this pid is present AND DIFFERENT — the OS has
@@ -21,8 +21,8 @@ export function createRemoteExec(previewEndpoint, relayToken, timeoutMs = DEFAUL
21
21
  // A malformed stored token (control chars / non-Latin1 bytes) would throw
22
22
  // from the Headers constructor with the invalid value IN the thrown
23
23
  // message — validate up front so that never happens, and fail with a
24
- // GENERIC message instead (Phase 3 review Minor 1: no token bytes may ever
25
- // reach returned stderr). HTTP header values are ISO-8859-1; reject
24
+ // GENERIC message instead: no token bytes may ever reach returned stderr.
25
+ // HTTP header values are ISO-8859-1; reject
26
26
  // anything outside `/^[\x20-\x7e]+$/` (printable ASCII, no CR/LF/control).
27
27
  const tokenValid = /^[\x20-\x7e]+$/.test(relayToken);
28
28
  return async function exec(bin, args) {
@@ -28,7 +28,7 @@ export declare function resolveNodeVisual(status: NodeStatus, opts: {
28
28
  * dashboard flags the node for the human. Exported so a recovery verb (e.g.
29
29
  * `node lifecycle revive --now`, src/commands/node-lifecycle-revive.ts) can gate on the SAME
30
30
  * predicate the dashboard advertises instead of drifting out of sync with it
31
- * (issue #115 — `--now` used to reject exactly the nodes shown as "needs you"). */
31
+ * and rejecting exactly the nodes shown as "needs you". */
32
32
  export declare function faultNeedsYou(fault: Pick<Fault, 'retry'>): boolean;
33
33
  /** Human-friendly active-fault disposition text. `auto` faults show the current
34
34
  * retry attempt plus the next scheduled retry; client-owned redials say
@@ -69,7 +69,7 @@ function retryCountdown(iso, now) {
69
69
  * dashboard flags the node for the human. Exported so a recovery verb (e.g.
70
70
  * `node lifecycle revive --now`, src/commands/node-lifecycle-revive.ts) can gate on the SAME
71
71
  * predicate the dashboard advertises instead of drifting out of sync with it
72
- * (issue #115 — `--now` used to reject exactly the nodes shown as "needs you"). */
72
+ * and rejecting exactly the nodes shown as "needs you". */
73
73
  export function faultNeedsYou(fault) {
74
74
  return fault.retry.disposition !== 'auto';
75
75
  }
@@ -191,7 +191,7 @@ export interface NodeRuntime {
191
191
  /** Stable process-identity fingerprint (`pid.ts`'s `composeIdentity`) captured
192
192
  * at the SAME moment `pi_pid` is recorded (`recordPid`) — the LAUNCH-time
193
193
  * baseline `headlessBrokerHost.teardown()` compares `pi_pid`'s CURRENT
194
- * identity against before signaling anything (crouter#98 review finding 1):
194
+ * identity against before signaling anything:
195
195
  * a mismatch proves the OS has already recycled `pi_pid` for an unrelated
196
196
  * process since this node launched it, and teardown must refuse to signal
197
197
  * that stranger. `null` for a node booted before this field existed, or if
@@ -8,14 +8,12 @@
8
8
  // This is pure validation: no fetch, no registration lookup, no file I/O beyond
9
9
  // the raw manifest bytes passed in.
10
10
  import { validateCommandNode } from './schema.js';
11
+ import { isRecord } from '../../shared/predicates.js';
11
12
  // ---------------------------------------------------------------------------
12
13
  // Validation helpers
13
14
  // ---------------------------------------------------------------------------
14
15
  const KEBAB = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
15
16
  const REST_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
16
- function isObject(v) {
17
- return typeof v === 'object' && v !== null && !Array.isArray(v);
18
- }
19
17
  function typeName(v) {
20
18
  if (v === null)
21
19
  return 'null';
@@ -38,7 +36,7 @@ export function validateCommandManifest(raw, options) {
38
36
  issues.push({ code, message, received, expected, next, ...(path ? { path } : {}) });
39
37
  };
40
38
  // Top-level structure check
41
- if (!isObject(raw)) {
39
+ if (!isRecord(raw)) {
42
40
  issue('command_manifest_invalid', 'manifest must be an object', typeName(raw), options.transport === 'http' ? '{ schemaVersion, baseUrl?, timeouts?, mounts }' : '{ schemaVersion, mounts }', 'Provide a valid JSON manifest object.');
43
41
  return { issues };
44
42
  }
@@ -126,7 +124,7 @@ export function validateCommandManifest(raw, options) {
126
124
  // Timeouts validation
127
125
  // ---------------------------------------------------------------------------
128
126
  function validateTimeouts(raw, issue) {
129
- if (!isObject(raw)) {
127
+ if (!isRecord(raw)) {
130
128
  issue('command_manifest_invalid', 'timeouts must be an object', typeName(raw), '{ connectMs?, requestMs?, streamIdleMs? }', 'Fix timeouts.', 'timeouts');
131
129
  return null;
132
130
  }
@@ -150,7 +148,7 @@ function validateTimeouts(raw, issue) {
150
148
  }
151
149
  function validateMount(raw, index, transport, issue) {
152
150
  const path = `mounts[${index}]`;
153
- if (!isObject(raw)) {
151
+ if (!isRecord(raw)) {
154
152
  issue('command_manifest_invalid', 'mount must be an object', typeName(raw), '{ parent, node }', 'Fix the mount.', path);
155
153
  return null;
156
154
  }
@@ -1,4 +1,5 @@
1
1
  // Unified command-manifest schema. Transport is the sole leaf-dialect discriminator.
2
+ import { isRecord } from '../../shared/predicates.js';
2
3
  // ---------------------------------------------------------------------------
3
4
  // Validation helpers
4
5
  // ---------------------------------------------------------------------------
@@ -41,9 +42,6 @@ const OUTPUT_KEYS = new Set(['name', 'type', 'required', 'constraint']);
41
42
  * may name, so a typo'd lowercase key fails at authoring time rather than
42
43
  * silently never resolving. */
43
44
  const ENV_VAR_NAME = /^[A-Z][A-Z0-9_]*$/;
44
- function isObject(v) {
45
- return typeof v === 'object' && v !== null && !Array.isArray(v);
46
- }
47
45
  function typeName(v) {
48
46
  if (v === null)
49
47
  return 'null';
@@ -60,7 +58,7 @@ function checkKeys(raw, allowed, path, issue) {
60
58
  return true;
61
59
  }
62
60
  function validateRootEntry(raw, path, issue) {
63
- if (!isObject(raw)) {
61
+ if (!isRecord(raw)) {
64
62
  issue('command_node_invalid', `rootEntry must be an object`, typeName(raw), 'a { concept, description, whenToUse } object', 'Fix rootEntry.', path);
65
63
  return null;
66
64
  }
@@ -91,7 +89,7 @@ function validateParams(raw, path, issue) {
91
89
  for (let i = 0; i < raw.length; i++) {
92
90
  const p = raw[i];
93
91
  const pp = `${path}[${i}]`;
94
- if (!isObject(p)) {
92
+ if (!isRecord(p)) {
95
93
  issue('command_node_invalid', `param must be an object`, typeName(p), 'a param object', 'Fix the param.', pp);
96
94
  return null;
97
95
  }
@@ -289,7 +287,7 @@ function validateOutput(raw, path, issue) {
289
287
  for (let i = 0; i < raw.length; i++) {
290
288
  const f = raw[i];
291
289
  const fp = `${path}[${i}]`;
292
- if (!isObject(f)) {
290
+ if (!isRecord(f)) {
293
291
  issue('command_node_invalid', `output field must be an object`, typeName(f), 'a { name, type, required, constraint } object', 'Fix the output field.', fp);
294
292
  return null;
295
293
  }
@@ -337,7 +335,7 @@ function checkCommon(raw, path, issue) {
337
335
  }
338
336
  export function validateCommandNode(raw, path, topLevel, transport, issue) {
339
337
  const pathString = path.join('.');
340
- if (!isObject(raw)) {
338
+ if (!isRecord(raw)) {
341
339
  issue('command_node_invalid', 'node must be an object', typeName(raw), 'a branch or leaf node object', 'Fix the node.', pathString);
342
340
  return null;
343
341
  }
@@ -424,7 +422,7 @@ function validateBranch(raw, path, topLevel, transport, issue) {
424
422
  }
425
423
  /** `passthrough: { bin, installHint }` — both required non-empty strings. */
426
424
  function validatePassthrough(raw, path, issue) {
427
- if (!isObject(raw)) {
425
+ if (!isRecord(raw)) {
428
426
  issue('command_node_invalid', 'passthrough must be an object', typeName(raw), 'an object { bin, installHint }', 'Fix or remove passthrough.', path);
429
427
  return null;
430
428
  }
@@ -482,7 +480,7 @@ const HTTP_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
482
480
  const REST_PARAM_PLACEMENT_SET = new Set(['path', 'query', 'body', 'header']);
483
481
  function validateRestMapping(raw, basePath, params, issue) {
484
482
  const path = `${basePath}.rest`;
485
- if (!isObject(raw)) {
483
+ if (!isRecord(raw)) {
486
484
  issue('command_rest_invalid', `rest must be an object`, typeName(raw), '{ method, path, streaming?, params }', 'Provide a valid rest mapping.', path);
487
485
  return null;
488
486
  }
@@ -523,7 +521,7 @@ function validateRestMapping(raw, basePath, params, issue) {
523
521
  }
524
522
  // Validate params mapping (required object)
525
523
  const restParams = raw['params'];
526
- if (!isObject(restParams)) {
524
+ if (!isRecord(restParams)) {
527
525
  issue('command_rest_invalid', `params must be an object`, typeName(restParams), 'a mapping of param names to placement specs', 'Provide params.', `${path}.params`);
528
526
  return null;
529
527
  }
@@ -542,7 +540,7 @@ function validateRestMapping(raw, basePath, params, issue) {
542
540
  return null;
543
541
  }
544
542
  declaredNames.add(paramName);
545
- if (!isObject(mapping)) {
543
+ if (!isRecord(mapping)) {
546
544
  issue('command_rest_invalid', `param mapping must be an object`, typeName(mapping), '{ in, as? }', 'Fix the param mapping.', `${path}.params.${paramName}`);
547
545
  return null;
548
546
  }
@@ -644,7 +642,7 @@ function validateRestMapping(raw, basePath, params, issue) {
644
642
  return null;
645
643
  }
646
644
  const rawBody = raw['body'];
647
- if (!isObject(rawBody)) {
645
+ if (!isRecord(rawBody)) {
648
646
  issue('command_rest_invalid', `body must be an object`, typeName(rawBody), 'a map of constant literal body fields (string | number | boolean)', 'Fix body.', `${path}.body`);
649
647
  return null;
650
648
  }
@@ -11,6 +11,7 @@ import { spawnSync } from 'node:child_process';
11
11
  import { CrtrError } from '../../errors.js';
12
12
  import { diag } from '../../io.js';
13
13
  import { ExitCode } from '../../../types.js';
14
+ import { isRecord } from '../../../shared/predicates.js';
14
15
  const MAX_STDOUT = 10 * 1024 * 1024; // 10 MiB hard cap on plugin stdout.
15
16
  const RESERVED_CODES = new Set(['internal', 'unknown_path', 'command_collision', 'plugin_protocol_error']);
16
17
  // Strict snake_case: no leading/trailing underscore, no consecutive underscores.
@@ -77,7 +78,7 @@ export function executeExternalLeaf(spec, input) {
77
78
  catch {
78
79
  throw protocolError(spec, `stdout was not a single JSON envelope`, 'exactly one JSON envelope on stdout');
79
80
  }
80
- if (!isObject(envelope)) {
81
+ if (!isRecord(envelope)) {
81
82
  throw protocolError(spec, `stdout was not a JSON object`, 'a { protocolVersion: 1, ok: boolean, ... } envelope');
82
83
  }
83
84
  if (envelope['protocolVersion'] !== 1) {
@@ -96,7 +97,7 @@ export function executeExternalLeaf(spec, input) {
96
97
  }
97
98
  if (ok) {
98
99
  const result = envelope['result'];
99
- if (!isObject(result)) {
100
+ if (!isRecord(result)) {
100
101
  throw protocolError(spec, `success envelope has no result object`, 'result: an object matching the declared output');
101
102
  }
102
103
  validateResult(spec, result);
@@ -104,7 +105,7 @@ export function executeExternalLeaf(spec, input) {
104
105
  }
105
106
  // Recoverable command failure → structured CrtrError with the plugin's code.
106
107
  const error = envelope['error'];
107
- if (!isObject(error) || typeof error['code'] !== 'string' || typeof error['message'] !== 'string') {
108
+ if (!isRecord(error) || typeof error['code'] !== 'string' || typeof error['message'] !== 'string') {
108
109
  throw protocolError(spec, `error envelope is missing a { code, message }`, 'error: { code, message, ... }');
109
110
  }
110
111
  const code = error['code'];
@@ -146,7 +147,7 @@ function typeMatches(type, v) {
146
147
  if (t === 'array' || t.endsWith('[]'))
147
148
  return Array.isArray(v);
148
149
  if (t === 'object')
149
- return isObject(v);
150
+ return isRecord(v);
150
151
  return true;
151
152
  }
152
153
  function protocolError(spec, received, expected) {
@@ -155,6 +156,3 @@ function protocolError(spec, received, expected) {
155
156
  function label(spec) {
156
157
  return `plugin "${spec.plugin.name}" command \`${spec.commandPath.join(' ')}\``;
157
158
  }
158
- function isObject(v) {
159
- return typeof v === 'object' && v !== null && !Array.isArray(v);
160
- }
@@ -19,6 +19,7 @@ import { validateDeclaredResult } from './exec-invoke.js';
19
19
  import { CrtrError } from '../../errors.js';
20
20
  import { diag, recordPreviewError, recordPreviewJsonLine, writeStdout } from '../../io.js';
21
21
  import { ExitCode } from '../../../types.js';
22
+ import { isRecord } from '../../../shared/predicates.js';
22
23
  // ---------------------------------------------------------------------------
23
24
  // Constants
24
25
  // ---------------------------------------------------------------------------
@@ -320,7 +321,7 @@ function mapUnary2xx(spec, body) {
320
321
  catch {
321
322
  throw protocolError(spec, 'response body was not valid JSON', 'a JSON object matching the declared output');
322
323
  }
323
- if (!isObject(parsed)) {
324
+ if (!isRecord(parsed)) {
324
325
  throw protocolError(spec, `response body was not a JSON object (got ${typeName(parsed)})`, 'a JSON object matching the declared output');
325
326
  }
326
327
  const issue = validateDeclaredResult(spec.output, parsed);
@@ -482,9 +483,9 @@ function throwNon2xx(spec, status, body) {
482
483
  catch {
483
484
  parsed = undefined;
484
485
  }
485
- if (isObject(parsed)) {
486
+ if (isRecord(parsed)) {
486
487
  const err = parsed['error'];
487
- if (isObject(err) && typeof err['code'] === 'string' && typeof err['message'] === 'string') {
488
+ if (isRecord(err) && typeof err['code'] === 'string' && typeof err['message'] === 'string') {
488
489
  const code = err['code'];
489
490
  const message = err['message'];
490
491
  const field = typeof err['field'] === 'string' ? err['field'] : undefined;
@@ -543,9 +544,6 @@ function readAll(res) {
543
544
  res.on('error', () => resolve(Buffer.concat(chunks).toString('utf8'))); // best-effort
544
545
  });
545
546
  }
546
- function isObject(v) {
547
- return typeof v === 'object' && v !== null && !Array.isArray(v);
548
- }
549
547
  function typeName(v) {
550
548
  if (v === null)
551
549
  return 'null';
@@ -81,7 +81,7 @@ export declare function readMergedLaunchConfig(targetCwd?: string, targetProfile
81
81
  * `developer`, or sub-kind, e.g. `plan/reviewers/security`), across
82
82
  * project > user > builtin precedence. Returns `undefined` for a kind no
83
83
  * scope registers — existence is deliberately NOT validated here (kind
84
- * existence/launch-menu enumeration is a Phase 3 caller concern); this only
84
+ * existence/launch-menu enumeration is a caller concern); this only
85
85
  * resolves the config for a kind the caller already knows about. */
86
86
  export declare function resolveKindConfig(kind: string): KindConfig | undefined;
87
87
  /** The sub-kinds available to spawn FROM a given top-level kind — every
@@ -560,7 +560,7 @@ export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileI
560
560
  * `developer`, or sub-kind, e.g. `plan/reviewers/security`), across
561
561
  * project > user > builtin precedence. Returns `undefined` for a kind no
562
562
  * scope registers — existence is deliberately NOT validated here (kind
563
- * existence/launch-menu enumeration is a Phase 3 caller concern); this only
563
+ * existence/launch-menu enumeration is a caller concern); this only
564
564
  * resolves the config for a kind the caller already knows about. */
565
565
  export function resolveKindConfig(kind) {
566
566
  return readMergedLaunchConfig().kinds[kind];
@@ -622,7 +622,7 @@ function sparseModelLaddersToPersist(rawLadders, baseline, mutated) {
622
622
  * sparse override stays sparse), and only keys the mutation actually changed
623
623
  * are written. `modelLadders` gets sub-field-granular sparse handling via
624
624
  * `sparseModelLaddersToPersist`. Result: a fresh bootstrap or any unrelated
625
- * `sys config set` no longer freezes a full concrete `modelLadders` to disk,
625
+ * `sys config set` never freezes a full concrete `modelLadders` to disk,
626
626
  * and a direct ladder-rung set still persists only the touched rung -- all
627
627
  * through one lock-protected atomic update path. */
628
628
  function mutateConfigAtPath(path, mutate) {
@@ -4,6 +4,7 @@ import { crtrHome, isSafeNodeId, nodesRoot } from '../canvas/paths.js';
4
4
  import { validateOperationId } from './operation-id.js';
5
5
  import { MAX_EVENT_RECORD_BYTES } from './serialize.js';
6
6
  import { errorClassCodes, } from './types.js';
7
+ import { isRecord } from '../../shared/predicates.js';
7
8
  const SNAPSHOT_ITEMS = 200;
8
9
  const MAX_CANONICAL_SOURCE_BYTES = 6 * 1024 * 1024;
9
10
  const MAX_RAW_SOURCE_BYTES = 256 * 1024;
@@ -41,9 +42,6 @@ const RETRY_DISPOSITIONS = new Set(['auto', 'manual', 'fatal']);
41
42
  const ERROR_CLASSES = new Set(errorClassCodes);
42
43
  const LEVEL_RANK = { debug: 0, info: 1, warn: 2, error: 3 };
43
44
  const GENERATION_RANK = { 2: 0, 1: 1, 0: 2 };
44
- function isRecord(value) {
45
- return typeof value === 'object' && value !== null && !Array.isArray(value);
46
- }
47
45
  function hasOwn(value, key) {
48
46
  return Object.prototype.hasOwnProperty.call(value, key);
49
47
  }
@@ -1,4 +1,5 @@
1
1
  import { hasManagedProviderCoolingDiagnostic } from './runtime/managed-provider-cooling.js';
2
+ import { isRecord } from '../shared/predicates.js';
2
3
  const errorClassByFaultKind = {
3
4
  'rate-limit': 'rate_limit',
4
5
  overloaded: 'overloaded',
@@ -16,9 +17,6 @@ export function errorClassFromFault(fault) {
16
17
  retry_disposition: fault.retry.disposition,
17
18
  };
18
19
  }
19
- function isRecord(value) {
20
- return typeof value === 'object' && value !== null;
21
- }
22
20
  function text(value) {
23
21
  return typeof value === 'string' ? value : '';
24
22
  }
@@ -1,9 +1,16 @@
1
+ /** `realpathSync`, tolerant of a path that doesn't exist or can't be resolved. */
2
+ export declare function realpathOrSelf(p: string): string;
3
+ /** Expand a leading `~` against `homeDir` (pi's own convention). Other forms
4
+ * — absolute, relative, `~user` — pass through as pi leaves them. */
5
+ export declare function expandTilde(p: string, homeDir?: string): string;
1
6
  export declare function ensureDir(dir: string): void;
2
7
  export declare function writeJson(path: string, data: unknown): void;
3
8
  export declare function atomicWriteJson(path: string, data: unknown): void;
4
9
  export declare function atomicWriteText(path: string, content: string | Uint8Array): void;
5
10
  export declare function readJson<T = unknown>(path: string): T;
6
11
  export declare function readJsonIfExists<T = unknown>(path: string): T | null;
12
+ /** Missing or corrupt reads as absent, so a crash artifact never throws. */
13
+ export declare function readJsonOrNull<T = unknown>(path: string): T | null;
7
14
  export declare function readTextIfExists(path: string): string | null;
8
15
  export declare function readText(path: string): string;
9
16
  export declare function writeText(path: string, content: string): void;
@@ -1,6 +1,24 @@
1
- import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync, cpSync, readlinkSync, renameSync, chmodSync, } from 'node:fs';
1
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync, cpSync, readlinkSync, renameSync, chmodSync, realpathSync, } from 'node:fs';
2
2
  import { dirname, join, relative } from 'node:path';
3
- import { platform } from 'node:os';
3
+ import { homedir, platform } from 'node:os';
4
+ /** `realpathSync`, tolerant of a path that doesn't exist or can't be resolved. */
5
+ export function realpathOrSelf(p) {
6
+ try {
7
+ return realpathSync(p);
8
+ }
9
+ catch {
10
+ return p;
11
+ }
12
+ }
13
+ /** Expand a leading `~` against `homeDir` (pi's own convention). Other forms
14
+ * — absolute, relative, `~user` — pass through as pi leaves them. */
15
+ export function expandTilde(p, homeDir = homedir()) {
16
+ if (p === '~')
17
+ return homeDir;
18
+ if (p.startsWith('~/') || (platform() === 'win32' && p.startsWith('~\\')))
19
+ return join(homeDir, p.slice(2));
20
+ return p;
21
+ }
4
22
  export function ensureDir(dir) {
5
23
  mkdirSync(dir, { recursive: true });
6
24
  }
@@ -44,6 +62,15 @@ export function readJsonIfExists(path) {
44
62
  return null;
45
63
  return readJson(path);
46
64
  }
65
+ /** Missing or corrupt reads as absent, so a crash artifact never throws. */
66
+ export function readJsonOrNull(path) {
67
+ try {
68
+ return JSON.parse(readFileSync(path, 'utf8'));
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
47
74
  export function readTextIfExists(path) {
48
75
  if (!existsSync(path))
49
76
  return null;
@@ -3,12 +3,11 @@
3
3
  * generated slash-command templates to `~/.claude/commands` and
4
4
  * `~/.pi/agent/prompts`.
5
5
  *
6
- * crouter no longer exports Agent Skills / SKILL.md bundles. The export pass
7
- * still prunes old marker-bearing `crtr-skills` bundles so hosts stop surfacing
8
- * pi/Claude native skills after the memory cutover. Markerless files are
9
- * user-owned and never removed.
6
+ * crouter exports no Agent Skills / SKILL.md bundles. The export pass prunes
7
+ * marker-bearing `crtr-skills` bundles so hosts stop surfacing pi/Claude native
8
+ * skills. Markerless files are user-owned and never removed.
10
9
  *
11
- * Triggered per-run by cli.ts (argv-gated, exactly as the old writers were) so
10
+ * Triggered per-run by cli.ts (argv-gated) so
12
11
  * first-run provisioning is preserved. Best-effort: a provisioning fault must
13
12
  * never break an unrelated `crtr` command (the deliberate, pre-existing
14
13
  * behavior of the deleted writers).
@@ -3,12 +3,11 @@
3
3
  * generated slash-command templates to `~/.claude/commands` and
4
4
  * `~/.pi/agent/prompts`.
5
5
  *
6
- * crouter no longer exports Agent Skills / SKILL.md bundles. The export pass
7
- * still prunes old marker-bearing `crtr-skills` bundles so hosts stop surfacing
8
- * pi/Claude native skills after the memory cutover. Markerless files are
9
- * user-owned and never removed.
6
+ * crouter exports no Agent Skills / SKILL.md bundles. The export pass prunes
7
+ * marker-bearing `crtr-skills` bundles so hosts stop surfacing pi/Claude native
8
+ * skills. Markerless files are user-owned and never removed.
10
9
  *
11
- * Triggered per-run by cli.ts (argv-gated, exactly as the old writers were) so
10
+ * Triggered per-run by cli.ts (argv-gated) so
12
11
  * first-run provisioning is preserved. Best-effort: a provisioning fault must
13
12
  * never break an unrelated `crtr` command (the deliberate, pre-existing
14
13
  * behavior of the deleted writers).