@crouton-kit/crouter 0.3.82 → 0.3.83

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 (80) hide show
  1. package/dist/builtin-pi-packages/pi-mode-switch/README.md +3 -3
  2. package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +12 -4
  3. package/dist/clients/attach/__tests__/attach-keybindings.test.js +7 -3
  4. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +104 -0
  5. package/dist/clients/attach/__tests__/crtr-output-render.test.js +16 -44
  6. package/dist/clients/attach/attach-cmd.d.ts +1 -2
  7. package/dist/clients/attach/attach-cmd.js +685 -685
  8. package/dist/clients/attach/attach-def.d.ts +2 -0
  9. package/dist/clients/attach/attach-def.js +93 -0
  10. package/dist/clients/attach/canvas-panels.d.ts +10 -3
  11. package/dist/clients/attach/canvas-panels.js +42 -16
  12. package/dist/clients/attach/chat-view.d.ts +20 -1
  13. package/dist/clients/attach/chat-view.js +85 -2
  14. package/dist/commands/__tests__/human.test.js +7 -22
  15. package/dist/commands/attention.js +31 -30
  16. package/dist/commands/human/prompts.js +51 -38
  17. package/dist/commands/human/queue.d.ts +1 -28
  18. package/dist/commands/human/queue.js +156 -520
  19. package/dist/commands/human/shared.d.ts +28 -68
  20. package/dist/commands/human/shared.js +37 -120
  21. package/dist/commands/human.js +13 -17
  22. package/dist/commands/node.js +4 -2
  23. package/dist/commands/surface.js +1 -1
  24. package/dist/commands/sys/__tests__/setup-core.test.js +1 -1
  25. package/dist/commands/sys/__tests__/setup-front-door.test.js +98 -0
  26. package/dist/commands/sys/setup-core.d.ts +22 -0
  27. package/dist/commands/sys/setup-core.js +62 -0
  28. package/dist/commands/sys/setup.js +8 -1
  29. package/dist/core/__tests__/human-deliver-e2e.test.js +95 -0
  30. package/dist/core/__tests__/human-deliver.test.js +332 -0
  31. package/dist/core/__tests__/human-node-not-supervised.test.js +5 -4
  32. package/dist/core/__tests__/pid-identity-match.test.js +83 -0
  33. package/dist/core/canvas/__tests__/attention.test.d.ts +1 -0
  34. package/dist/core/canvas/__tests__/attention.test.js +126 -0
  35. package/dist/core/canvas/__tests__/remote-canvas-source.test.js +4 -4
  36. package/dist/core/canvas/__tests__/render-remote.test.js +1 -1
  37. package/dist/core/canvas/attention.d.ts +16 -14
  38. package/dist/core/canvas/attention.js +34 -29
  39. package/dist/core/canvas/boot.d.ts +1 -0
  40. package/dist/core/canvas/boot.js +1 -1
  41. package/dist/core/canvas/browse/app.js +3 -3
  42. package/dist/core/canvas/pid.d.ts +43 -17
  43. package/dist/core/canvas/pid.js +104 -35
  44. package/dist/core/canvas/remote-canvas-source.d.ts +1 -1
  45. package/dist/core/canvas/remote-canvas-source.js +4 -2
  46. package/dist/core/canvas/render.d.ts +5 -5
  47. package/dist/core/canvas/render.js +14 -14
  48. package/dist/core/canvas/source.d.ts +2 -2
  49. package/dist/core/canvas/source.js +3 -3
  50. package/dist/core/keybindings/__tests__/resolve.test.js +1 -1
  51. package/dist/core/keybindings/catalog.d.ts +2 -2
  52. package/dist/core/keybindings/catalog.js +3 -2
  53. package/dist/core/preview-registry.js +22 -29
  54. package/dist/core/profiles/select.js +4 -4
  55. package/dist/core/runtime/pi-vendored.js +15 -1
  56. package/dist/core/runtime/placement.d.ts +1 -10
  57. package/dist/core/runtime/placement.js +43 -26
  58. package/dist/core/runtime/session-list-cache.d.ts +22 -2
  59. package/dist/core/runtime/session-list-cache.js +92 -24
  60. package/dist/core/runtime/spawn.js +9 -0
  61. package/dist/core/spawn.d.ts +0 -59
  62. package/dist/core/spawn.js +5 -163
  63. package/dist/daemon/crtrd.js +7 -6
  64. package/dist/index.d.ts +2 -2
  65. package/dist/index.js +1 -1
  66. package/dist/web-client/assets/index-DZdKS-FB.js +78 -0
  67. package/dist/web-client/index.html +1 -1
  68. package/dist/web-client/sw.js +1 -1
  69. package/package.json +2 -2
  70. package/dist/core/__tests__/full/dead-pane-regression.test.js +0 -154
  71. package/dist/core/__tests__/full/human-new-window-regression.test.js +0 -104
  72. package/dist/core/__tests__/full/review-render-pane-regression.test.js +0 -133
  73. package/dist/core/__tests__/human-stranded-deliver.test.js +0 -145
  74. package/dist/core/__tests__/human-surface-target.test.js +0 -98
  75. package/dist/web-client/assets/index--md2ylfi.js +0 -78
  76. /package/dist/{core/__tests__/full/dead-pane-regression.test.d.ts → clients/attach/__tests__/chat-view-snapshot-ordering.test.d.ts} +0 -0
  77. /package/dist/{core/__tests__/full/human-new-window-regression.test.d.ts → commands/sys/__tests__/setup-front-door.test.d.ts} +0 -0
  78. /package/dist/core/__tests__/{full/review-render-pane-regression.test.d.ts → human-deliver-e2e.test.d.ts} +0 -0
  79. /package/dist/core/__tests__/{human-stranded-deliver.test.d.ts → human-deliver.test.d.ts} +0 -0
  80. /package/dist/core/__tests__/{human-surface-target.test.d.ts → pid-identity-match.test.d.ts} +0 -0
@@ -9,6 +9,7 @@
9
9
  // placement.ts, and crtrd.ts into one (Phase 3 review reuse MINOR-1).
10
10
  import { spawnSync } from 'node:child_process';
11
11
  import { readFileSync } from 'node:fs';
12
+ import { readKernelBootId } from './boot.js';
12
13
  /** Is `pid` a ZOMBIE (exited, awaiting reap by its parent)? `kill(pid, 0)`
13
14
  * reports a zombie as a perfectly normal alive process — its PID is still
14
15
  * occupied in the process table — so callers relying on that signal-0 probe
@@ -60,13 +61,29 @@ export function isPidAlive(pid) {
60
61
  }
61
62
  return !isZombie(pid);
62
63
  }
63
- /** Split a `composeIdentity` fingerprint (`lstart` or `lstart#ticks`) into its
64
- * coarse (`lstart`, second-granular, portable) and fine (`ticks`, Linux-only
65
- * jiffies, best-effort) parts. `#` never appears in `lstart` itself (a `ps`
66
- * timestamp), so the first `#` unambiguously separates them. */
64
+ /** Matches the kernel `boot_id` UUID shape (`/proc/sys/kernel/random/boot_id`),
65
+ * used to tell a NEW-format identity base (a per-boot UUID) apart from a
66
+ * LEGACY one (a `ps lstart` wall-clock timestamp). A boot_id is a lowercase
67
+ * hex UUID; an `lstart` string is a human-readable date — the two never
68
+ * collide, so this cleanly classifies which comparison rule an identity's
69
+ * base obeys (see `identitiesMatch`). */
70
+ const BOOT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
71
+ /** Split a `composeIdentity` fingerprint (`<base>` or `<base>#<ticks>`, where
72
+ * `base` is a per-boot `boot_id` UUID on Linux-with-procfs or the portable
73
+ * `ps lstart` timestamp otherwise) into its coarse base and fine `ticks`
74
+ * (Linux-only jiffies) parts. `#` never appears in a `boot_id` or an `lstart`
75
+ * timestamp, so the first `#` unambiguously separates them. Only a NON-EMPTY,
76
+ * purely-numeric suffix counts as ticks (the shape `procStartTicks` and
77
+ * `composeIdentity` guarantee); a malformed or empty suffix (`A#`, `A#bad`,
78
+ * `A#1#2`) is treated as ABSENT ticks, so the base compare then applies — a
79
+ * garbage suffix must never be trusted as a fine discriminator (review
80
+ * Minor). */
67
81
  function splitIdentity(identity) {
68
82
  const i = identity.indexOf('#');
69
- return i === -1 ? { base: identity, ticks: undefined } : { base: identity.slice(0, i), ticks: identity.slice(i + 1) };
83
+ if (i === -1)
84
+ return { base: identity, ticks: undefined };
85
+ const suffix = identity.slice(i + 1);
86
+ return { base: identity.slice(0, i), ticks: /^\d+$/.test(suffix) ? suffix : undefined };
70
87
  }
71
88
  /** THE single precision-aware identity compare, used EVERYWHERE two
72
89
  * `composeIdentity` fingerprints are compared (this module's own
@@ -75,32 +92,60 @@ function splitIdentity(identity) {
75
92
  * drift from this rule (review Major #1, Round 2 of the pid-reuse-liveness
76
93
  * fix).
77
94
  *
78
- * The hazard this closes: `composeIdentity` appends the `#<ticks>` Linux
79
- * discriminator only when `/proc/<pid>/stat` happens to be readable at
80
- * capture time so the SAME live process can be recorded plain `lstart` on
81
- * one capture and `lstart#ticks` on another (a transient `/proc` read
82
- * failure, or capturing on a platform/container without procfs). A naive
83
- * `===` then reads that as a reuse (different string) and reports `dead` for
84
- * a broker that never actually died — the false-DEAD direction, which can
85
- * make `reviveNode`'s double-launch guard wrongly relaunch a SECOND broker
86
- * onto a still-live session.
95
+ * Two hazards this closes, both in the false-DEAD direction (the dangerous
96
+ * one a false DEAD lets `reviveNode`'s double-launch guard relaunch a
97
+ * SECOND broker onto a still-live session):
87
98
  *
88
- * Rule: split both on `#`. Bases (`lstart`) differ definitely NOT a match
89
- * (different process, or the same pid reused in a different second). Bases
90
- * match and BOTH carry a `#ticks` suffix match iff the suffixes are equal
91
- * (this still catches genuine same-second reuse, the case `procStartTicks`
92
- * exists for). Bases match and EITHER lacks the suffix → coarse-vs-fine,
93
- * can't distinguish MATCH (fail-open to same-process, matching this
94
- * module's universal discipline: only a POSITIVE mismatch counts as reuse,
95
- * never an absence of evidence). */
99
+ * 1. `composeIdentity` appends the `#<ticks>` Linux discriminator only when
100
+ * `/proc/<pid>/stat` happens to be readable at capture time so the SAME
101
+ * live process can be recorded plain `lstart` on one capture and
102
+ * `lstart#ticks` on another (a transient `/proc` read failure, or
103
+ * capturing on a platform/container without procfs). A naive `===` reads
104
+ * that as a reuse (different string) and reports `dead` for a broker that
105
+ * never died.
106
+ * 2. `lstart` is NOT stable for a live process on every host. It is derived
107
+ * from `/proc/stat` btime (wall-clock boot epoch) + starttime jiffies, so
108
+ * a clock correction / suspend-resume shifts btime and re-anchors the
109
+ * SAME live process's `lstart` by seconds between two captures. Observed
110
+ * on Blaxel unikraft guests: pid+ticks identical across probes, `lstart`
111
+ * drifting +81s — a base-first compare then reads the live home-node
112
+ * broker as reused/dead every cycle, driving an uncapped revive loop.
113
+ *
114
+ * A third hazard the base itself once carried: `ticks` (jiffies-since-boot)
115
+ * RESET on every boot and can repeat across boots, so ticks-alone is only
116
+ * safe WITHIN one boot. `composeIdentity` therefore boot-SCOPES the base on
117
+ * Linux-with-procfs — the base IS the kernel `boot_id` UUID — so a
118
+ * new-format identity is `<bootId>#<ticks>` and equality means same boot AND
119
+ * same process. This closes the cross-boot false match at the root, without
120
+ * plumbing boot reconciliation ahead of every caller.
121
+ *
122
+ * Rule: split both on `#`.
123
+ * - BOTH sides carry `#ticks` AND both bases are `boot_id` UUIDs
124
+ * (new-format vs new-format): match iff base AND ticks are equal. A
125
+ * different boot_id with equal ticks is NOT a match (the cross-boot
126
+ * collision).
127
+ * - BOTH sides carry `#ticks` but a base is NOT a boot_id UUID on EITHER
128
+ * side — a LEGACY `lstart#ticks` row recorded before this repo boot-scoped
129
+ * the base, or a Linux host that has procfs ticks but no readable boot_id:
130
+ * ticks DECIDE it alone (the prior landed semantics). This is a NARROW
131
+ * format-migration path so an in-place upgrade never false-kills a live
132
+ * broker whose row still holds `lstart#ticks`; it is NOT a general
133
+ * fallback — new-format vs new-format always demands base+ticks above.
134
+ * - EITHER side lacks a valid `#ticks` suffix (macOS / no procfs / transient
135
+ * `/proc` miss / malformed suffix): fall back to the coarse base —
136
+ * matching iff the bases agree (fail-open to same-process, matching this
137
+ * module's universal discipline: only a POSITIVE mismatch counts as reuse,
138
+ * never an absence of evidence). */
96
139
  export function identitiesMatch(a, b) {
97
140
  const ia = splitIdentity(a);
98
141
  const ib = splitIdentity(b);
99
- if (ia.base !== ib.base)
100
- return false;
101
- if (ia.ticks !== undefined && ib.ticks !== undefined)
102
- return ia.ticks === ib.ticks;
103
- return true;
142
+ if (ia.ticks !== undefined && ib.ticks !== undefined) {
143
+ if (BOOT_ID_RE.test(ia.base) && BOOT_ID_RE.test(ib.base)) {
144
+ return ia.base === ib.base && ia.ticks === ib.ticks;
145
+ }
146
+ return ia.ticks === ib.ticks; // legacy lstart#ticks (either side) → ticks-alone migration
147
+ }
148
+ return ia.base === ib.base;
104
149
  }
105
150
  export function recordedPidLiveness(pid, expectedIdentity) {
106
151
  const debug = Boolean(process.env.CRTR_DEBUG_PID_LIVENESS);
@@ -226,16 +271,40 @@ function procStartTicks(pid) {
226
271
  return null;
227
272
  }
228
273
  }
229
- /** Compose the stable process-identity fingerprint used everywhere in this
230
- * module: `ps lstart` (portable, BSD + Linux) plus the Linux-only jiffies
231
- * discriminator when available (best-effortsee `procStartTicks`). ALWAYS
232
- * used to build an identity string, never `lstart` alone directly, so every
233
- * identity captured anywhere (launch-time `recordPid`, the teardown
234
- * snapshot, and the escalation-window re-check) is comparable apples-to-
235
- * apples. */
274
+ /** The kernel `boot_id` (`/proc/sys/kernel/random/boot_id`) is stable for the
275
+ * whole lifetime of this process it only changes on a kernel boot, which
276
+ * necessarily kills this processso it is read ONCE and cached, rather than
277
+ * re-read for every row of a whole-process-table `ps` scan. `undefined` = not
278
+ * yet read; `null` = read and absent (non-Linux / no procfs). */
279
+ let cachedBootId;
280
+ function bootIdBase() {
281
+ if (cachedBootId === undefined)
282
+ cachedBootId = readKernelBootId();
283
+ return cachedBootId;
284
+ }
285
+ /** Compose the stable, BOOT-SCOPED process-identity fingerprint used
286
+ * everywhere in this module. ALWAYS used to build an identity string, never a
287
+ * raw field directly, so every identity captured anywhere (launch-time
288
+ * `recordPid`, the teardown snapshot, and the escalation-window re-check) is
289
+ * comparable apples-to-apples.
290
+ *
291
+ * Platform capability, not layered fallbacks — the base is the finest STABLE
292
+ * per-boot anchor available:
293
+ * - Linux-with-procfs (the production guest shape): `<bootId>#<ticks>` — the
294
+ * per-boot `boot_id` UUID plus the per-process jiffies-since-boot. Ticks
295
+ * reset per boot and can repeat, so scoping the base to the boot is what
296
+ * makes an equal-ticks compare safe ACROSS boots (see `identitiesMatch`).
297
+ * - Linux-with-procfs but no readable `boot_id` (if that combination
298
+ * exists): `<lstart>#<ticks>` — the prior landed behavior.
299
+ * - No procfs ticks (macOS / containers without procfs): `<lstart>` alone —
300
+ * unchanged coarse behavior. The base is only ever a `boot_id` when a
301
+ * `ticks` discriminator is ALSO present; a bare `boot_id` (shared by every
302
+ * process on the boot) would be a useless per-process identity. */
236
303
  function composeIdentity(pid, lstart) {
237
304
  const ticks = procStartTicks(pid);
238
- return ticks !== null ? `${lstart}#${ticks}` : lstart;
305
+ if (ticks === null)
306
+ return lstart;
307
+ return `${bootIdBase() ?? lstart}#${ticks}`;
239
308
  }
240
309
  /** One combined `ps` probe over the WHOLE process table — `pid`, `ppid`, AND
241
310
  * `lstart` per row — parsed into BOTH a ppid → children-pid[] map (for
@@ -57,6 +57,6 @@ export declare class RemoteCanvasSource implements CanvasSource {
57
57
  subscriptionsOf(id: string): Promise<SubscriptionRef[]>;
58
58
  subscribersOf(id: string): Promise<SubscriptionRef[]>;
59
59
  view(root: string): Promise<string[]>;
60
- askCountsForView(root: string): Promise<Record<string, number>>;
60
+ ticketCountsForView(root: string): Promise<Record<string, number>>;
61
61
  hasActiveLiveSubscription(id: string): Promise<boolean>;
62
62
  }
@@ -12,7 +12,7 @@ const CACHE_TTL_MS = 1000;
12
12
  // `execJson<T>` parses stdout and casts it to T with NO structural check — a
13
13
  // remote subprocess that exits 0 with a wrong shape (e.g. `{}`, `{node:null}`)
14
14
  // would otherwise pass through as a "valid" T. `fetchShow`/`listNodes`/
15
- // `askCountsForView` validate before caching/returning; on a shape mismatch
15
+ // `ticketCountsForView` validate before caching/returning; on a shape mismatch
16
16
  // they degrade exactly like the existing network-failure path (null/[]/{}),
17
17
  // never a malformed object a caller might dereference.
18
18
  // ---------------------------------------------------------------------------
@@ -206,7 +206,9 @@ export class RemoteCanvasSource {
206
206
  this.viewCache.set(root, { value: out, at: Date.now() });
207
207
  return out;
208
208
  }
209
- async askCountsForView(root) {
209
+ async ticketCountsForView(root) {
210
+ // The `canvas attention map` transport is unchanged; its `counts` values now
211
+ // mean all pending human tickets (deck/review/notify), not just asks.
210
212
  const res = await this.execJson(['canvas', 'attention', 'map', '--view', root]);
211
213
  return isPlainObject(res) && isPlainObject(res['counts']) ? res['counts'] : {};
212
214
  }
@@ -126,16 +126,16 @@ export declare function dashboardRowsAll(): DashboardRow[];
126
126
  export declare function dashboardRowsAllFromSource(source: CanvasSource): Promise<DashboardRow[]>;
127
127
  /** Fold the cheap-boot row's deferred fields in for ONE row: the full label
128
128
  * (meta.description → fullName), ctx tokens (telemetry.json), and ⚑ asks. One
129
- * meta read + one telemetry read + one ask scan. Idempotent (no-op once enriched).
130
- * For a batch, prefer {@link enrichRows} — it scans each cwd's ask inbox once. */
129
+ * meta read + one telemetry read + one ticket scan. Idempotent (no-op once enriched).
130
+ * For a batch, prefer {@link enrichRows} — it scans each cwd's ticket inbox once. */
131
131
  export declare function enrichRow(row: DashboardRow): DashboardRow;
132
132
  /** Batch {@link enrichRow}: enrich every not-yet-enriched row, scanning each
133
- * distinct cwd's ask inbox exactly ONCE (via `asksForNodes`) instead of per row.
134
- * Use this for a whole viewport / the full forest; mutates each row in place. */
133
+ * distinct cwd's ticket inbox exactly ONCE (via `ticketCountsForNodes`) instead
134
+ * of per row. Use this for a whole viewport / the full forest; mutates each row in place. */
135
135
  export declare function enrichRows(rows: DashboardRow[]): void;
136
136
  /** Telemetry (ctx tokens) is a local disk read, so it is suppressed to 0 for a
137
137
  * remote source. Ask counts are suppressed to 0 UNLESS the caller supplies
138
- * `remoteAsks` — a pre-fetched `askCountsForView` map (one call per forest
138
+ * `remoteAsks` — a pre-fetched `ticketCountsForView` map (one call per forest
139
139
  * root; the caller's job, since the right root set is a browse/snapshot
140
140
  * concern, not this per-row enrichment loop's). See `runBrowse`'s
141
141
  * `remoteAskCounts`, fetched once per snapshot rebuild, not per flush. */
@@ -18,7 +18,7 @@ import { getNode, listNodes, subscriptionsOf, view } from './canvas.js';
18
18
  import { RemoteCanvasSource } from './remote-canvas-source.js';
19
19
  import { fullName } from './labels.js';
20
20
  import { jobDir, contextDir } from './paths.js';
21
- import { countAsks, asksForNodes } from './attention.js';
21
+ import { countTickets, ticketCountsForNodes } from './attention.js';
22
22
  import { isPidAlive } from './pid.js';
23
23
  import { listFocuses } from './focuses.js';
24
24
  import { resolveNodeVisual, faultSummary } from './status-glyph.js';
@@ -68,7 +68,7 @@ function nodeLine(nodeId, indent, connector) {
68
68
  const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
69
69
  const tel = readNodeTelemetry(nodeId);
70
70
  const ctx = fmtCtx(tel.tokens_in);
71
- const asks = countAsks(nodeId);
71
+ const asks = countTickets(nodeId);
72
72
  const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
73
73
  const faultSuffix = hanging !== null ? ` · ${faultSummary(hanging)}` : '';
74
74
  return `${indent}${connector}${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}${faultSuffix}`;
@@ -119,7 +119,7 @@ export function renderTree(rootId) {
119
119
  return `? <missing node: ${rootId}>`;
120
120
  const tel = readNodeTelemetry(rootId);
121
121
  const ctx = fmtCtx(tel.tokens_in);
122
- const asks = countAsks(rootId);
122
+ const asks = countTickets(rootId);
123
123
  const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
124
124
  const hanging = hangingFor(node);
125
125
  const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
@@ -182,7 +182,7 @@ async function nodeLineFromSource(source, nodeId, indent, connector, remote) {
182
182
  const hanging = remote ? null : hangingFor(node);
183
183
  const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
184
184
  const ctx = remote ? '0k' : fmtCtx(readNodeTelemetry(nodeId).tokens_in);
185
- const asks = remote ? 0 : countAsks(nodeId);
185
+ const asks = remote ? 0 : countTickets(nodeId);
186
186
  const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
187
187
  const faultSuffix = hanging !== null ? ` · ${faultSummary(hanging)}` : '';
188
188
  return `${indent}${connector}${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}${faultSuffix}`;
@@ -212,7 +212,7 @@ export async function renderTreeFromSource(source, rootId) {
212
212
  if (node === null)
213
213
  return `? <missing node: ${rootId}>`;
214
214
  const ctx = remote ? '0k' : fmtCtx(readNodeTelemetry(rootId).tokens_in);
215
- const asks = remote ? 0 : countAsks(rootId);
215
+ const asks = remote ? 0 : countTickets(rootId);
216
216
  const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
217
217
  const hanging = remote ? null : hangingFor(node);
218
218
  const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
@@ -457,7 +457,7 @@ export function dashboardRows(rootId) {
457
457
  kind: node.kind,
458
458
  mode: node.mode,
459
459
  ctx_tokens: tel.tokens_in ?? 0,
460
- asks: countAsks(id),
460
+ asks: countTickets(id),
461
461
  cwd: node.cwd,
462
462
  created: node.created,
463
463
  }];
@@ -542,8 +542,8 @@ export async function dashboardRowsAllFromSource(source) {
542
542
  // ---------------------------------------------------------------------------
543
543
  /** Fold the cheap-boot row's deferred fields in for ONE row: the full label
544
544
  * (meta.description → fullName), ctx tokens (telemetry.json), and ⚑ asks. One
545
- * meta read + one telemetry read + one ask scan. Idempotent (no-op once enriched).
546
- * For a batch, prefer {@link enrichRows} — it scans each cwd's ask inbox once. */
545
+ * meta read + one telemetry read + one ticket scan. Idempotent (no-op once enriched).
546
+ * For a batch, prefer {@link enrichRows} — it scans each cwd's ticket inbox once. */
547
547
  export function enrichRow(row) {
548
548
  if (row.enriched === true)
549
549
  return row;
@@ -551,18 +551,18 @@ export function enrichRow(row) {
551
551
  if (meta !== null)
552
552
  row.name = fullName(meta);
553
553
  row.ctx_tokens = readNodeTelemetry(row.node_id).tokens_in ?? 0;
554
- row.asks = countAsks(row.node_id);
554
+ row.asks = countTickets(row.node_id);
555
555
  row.enriched = true;
556
556
  return row;
557
557
  }
558
558
  /** Batch {@link enrichRow}: enrich every not-yet-enriched row, scanning each
559
- * distinct cwd's ask inbox exactly ONCE (via `asksForNodes`) instead of per row.
560
- * Use this for a whole viewport / the full forest; mutates each row in place. */
559
+ * distinct cwd's ticket inbox exactly ONCE (via `ticketCountsForNodes`) instead
560
+ * of per row. Use this for a whole viewport / the full forest; mutates each row in place. */
561
561
  export function enrichRows(rows) {
562
562
  const todo = rows.filter((r) => r.enriched !== true);
563
563
  if (todo.length === 0)
564
564
  return;
565
- const asks = asksForNodes(todo.map((r) => r.node_id));
565
+ const asks = ticketCountsForNodes(todo.map((r) => r.node_id));
566
566
  for (const row of todo) {
567
567
  const meta = getNode(row.node_id);
568
568
  if (meta !== null)
@@ -574,7 +574,7 @@ export function enrichRows(rows) {
574
574
  }
575
575
  /** Telemetry (ctx tokens) is a local disk read, so it is suppressed to 0 for a
576
576
  * remote source. Ask counts are suppressed to 0 UNLESS the caller supplies
577
- * `remoteAsks` — a pre-fetched `askCountsForView` map (one call per forest
577
+ * `remoteAsks` — a pre-fetched `ticketCountsForView` map (one call per forest
578
578
  * root; the caller's job, since the right root set is a browse/snapshot
579
579
  * concern, not this per-row enrichment loop's). See `runBrowse`'s
580
580
  * `remoteAskCounts`, fetched once per snapshot rebuild, not per flush. */
@@ -583,7 +583,7 @@ export async function enrichRowsFromSource(source, rows, remoteAsks) {
583
583
  if (todo.length === 0)
584
584
  return;
585
585
  const remote = source instanceof RemoteCanvasSource;
586
- const asks = remote ? (remoteAsks ?? {}) : asksForNodes(todo.map((r) => r.node_id));
586
+ const asks = remote ? (remoteAsks ?? {}) : ticketCountsForNodes(todo.map((r) => r.node_id));
587
587
  await Promise.all(todo.map(async (row) => {
588
588
  const meta = await source.getNode(row.node_id);
589
589
  if (meta !== null)
@@ -9,7 +9,7 @@ export interface CanvasSource {
9
9
  subscriptionsOf(subscriber: string): Promise<SubscriptionRef[]>;
10
10
  subscribersOf(publisher: string): Promise<SubscriptionRef[]>;
11
11
  view(root: string): Promise<string[]>;
12
- askCountsForView(root: string): Promise<Record<string, number>>;
12
+ ticketCountsForView(root: string): Promise<Record<string, number>>;
13
13
  hasActiveLiveSubscription(nodeId: string): Promise<boolean>;
14
14
  }
15
15
  export declare class LocalCanvasSource implements CanvasSource {
@@ -21,7 +21,7 @@ export declare class LocalCanvasSource implements CanvasSource {
21
21
  subscriptionsOf(subscriber: string): Promise<SubscriptionRef[]>;
22
22
  subscribersOf(publisher: string): Promise<SubscriptionRef[]>;
23
23
  view(root: string): Promise<string[]>;
24
- askCountsForView(root: string): Promise<Record<string, number>>;
24
+ ticketCountsForView(root: string): Promise<Record<string, number>>;
25
25
  hasActiveLiveSubscription(nodeId: string): Promise<boolean>;
26
26
  }
27
27
  export declare const localCanvasSource: CanvasSource;
@@ -1,5 +1,5 @@
1
1
  import { getNode, getRow, listNodes, subscriptionsOf, subscribersOf, view, hasActiveLiveSubscription, } from './canvas.js';
2
- import { asksForNodes } from './attention.js';
2
+ import { ticketCountsForNodes } from './attention.js';
3
3
  import { readState } from '../config.js';
4
4
  import { resolveRemoteCanvasTarget } from '../view/remote-canvas-target.js';
5
5
  import { RemoteCanvasSource } from './remote-canvas-source.js';
@@ -22,8 +22,8 @@ export class LocalCanvasSource {
22
22
  async view(root) {
23
23
  return view(root);
24
24
  }
25
- async askCountsForView(root) {
26
- return asksForNodes([root, ...view(root)]);
25
+ async ticketCountsForView(root) {
26
+ return ticketCountsForNodes([root, ...view(root)]);
27
27
  }
28
28
  async hasActiveLiveSubscription(nodeId) {
29
29
  return hasActiveLiveSubscription(nodeId);
@@ -18,7 +18,7 @@ const REQUIRED_IDS = [
18
18
  ...ids('crtr.tmux.menu.issue', ['idea', 'bug']),
19
19
  ...ids('crtr.tmux.node', ['next', 'previous']),
20
20
  ...ids('crtr.tmux.view', ['next', 'previous']),
21
- ...ids('crtr.attach', ['detach', 'clear-or-detach', 'graph.toggle', 'model-ladder.next']),
21
+ ...ids('crtr.attach', ['detach', 'clear-or-detach', 'graph.toggle', 'model-ladder.next', 'model-ladder.previous']),
22
22
  'crtr.mode.cycle',
23
23
  ...ids('crtr.graph', ['down', 'up', 'first', 'last', 'collapse-or-manager', 'expand-or-child', 'focus', 'focus-manager', 'close-subtree', 'dismiss']),
24
24
  ...ids('crtr.graph.confirm', ['accept', 'cancel']),
@@ -7,8 +7,8 @@ type BrowseAction = 'quit' | 'down' | 'up' | 'first' | 'last' | 'expand-or-child
7
7
  type ProfileMenuAction = 'up' | 'down' | 'select' | 'accept-default' | 'abort' | `slot.${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}` | 'create' | 'search-existing' | 'use-root' | 'set-default' | 'add-directory' | 'unpin';
8
8
  type SetupAction = 'cancel' | 'tab-next' | 'tab-previous' | 'open' | 'list-down' | 'list-up' | 'toggle' | 'details' | 'advance';
9
9
  type SetupKeybindingAction = 'edit' | 'commit-edit' | 'cancel-edit' | 'unbind' | 'reset-row' | 'reset-all' | 'save' | 'discard' | 'page-up' | 'page-down' | 'filter' | 'filter-delete' | 'filter-clear';
10
- export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | `crtr.tmux.node.${'next' | 'previous'}` | `crtr.tmux.view.${'next' | 'previous'}` | `crtr.attach.${'detach' | 'clear-or-detach' | 'graph.toggle' | 'model-ladder.next'}` | 'crtr.mode.cycle' | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.view.host.${'force-quit' | 'editor.backspace'}` | `crtr.view.canvas.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.chat.${'submit' | 'contextual-cancel' | 'reconnect' | 'request-control' | 'release-control' | 'quit'}` | `crtr.view.git-pr.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.inbox.${InboxAction}` | `crtr.view.linkedin.${LinkedinAction}` | `crtr.view.prompt-review.${'down' | 'up' | 'compare-previous' | 'compare-next' | 'toggle-mode' | 'expand' | 'export' | 'refresh' | 'quit'}` | `crtr.view.settings.${'down' | 'up' | 'cell-previous' | 'cell-next' | 'refresh' | 'quit' | 'edit-or-submit' | 'cancel-edit'}` | `crtr.view.workspace-sidebar.${'down' | 'up' | 'open' | 'refresh' | 'quit'}` | `crtr.view-picker.${'down' | 'up' | 'run' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}`;
11
- export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.promote">, BindingDefinition<"crtr.tmux.menu.resume">, BindingDefinition<"crtr.tmux.menu.demote">, BindingDefinition<"crtr.tmux.menu.detach">, BindingDefinition<"crtr.tmux.menu.close-subtree">, BindingDefinition<"crtr.tmux.menu.context">, BindingDefinition<"crtr.tmux.menu.providers">, BindingDefinition<"crtr.tmux.menu.graph">, BindingDefinition<"crtr.tmux.menu.focus-manager">, BindingDefinition<"crtr.tmux.menu.issues">, BindingDefinition<"crtr.tmux.menu.issue.idea">, BindingDefinition<"crtr.tmux.menu.issue.bug">, BindingDefinition<"crtr.tmux.node.next">, BindingDefinition<"crtr.tmux.node.previous">, BindingDefinition<"crtr.tmux.view.next">, BindingDefinition<"crtr.tmux.view.previous">, BindingDefinition<"crtr.attach.detach">, BindingDefinition<"crtr.attach.clear-or-detach">, BindingDefinition<"crtr.attach.graph.toggle">, BindingDefinition<"crtr.attach.model-ladder.next">, BindingDefinition<"crtr.mode.cycle">, BindingDefinition<"crtr.graph.down">, BindingDefinition<"crtr.graph.up">, BindingDefinition<"crtr.graph.first">, BindingDefinition<"crtr.graph.last">, BindingDefinition<"crtr.graph.collapse-or-manager">, BindingDefinition<"crtr.graph.expand-or-child">, BindingDefinition<"crtr.graph.focus">, BindingDefinition<"crtr.graph.focus-manager">, BindingDefinition<"crtr.graph.close-subtree">, BindingDefinition<"crtr.graph.dismiss">, BindingDefinition<"crtr.graph.confirm.accept">, BindingDefinition<"crtr.graph.confirm.cancel">, BindingDefinition<"crtr.view.host.force-quit">, BindingDefinition<"crtr.view.host.editor.backspace">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<"crtr.view.chat.submit">, BindingDefinition<"crtr.view.chat.contextual-cancel">, BindingDefinition<"crtr.view.chat.reconnect">, BindingDefinition<"crtr.view.chat.request-control">, BindingDefinition<"crtr.view.chat.release-control">, BindingDefinition<"crtr.view.chat.quit">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.view.prompt-review.down"> | BindingDefinition<"crtr.view.prompt-review.up"> | BindingDefinition<"crtr.view.prompt-review.compare-previous"> | BindingDefinition<"crtr.view.prompt-review.compare-next"> | BindingDefinition<"crtr.view.prompt-review.toggle-mode"> | BindingDefinition<"crtr.view.prompt-review.expand"> | BindingDefinition<"crtr.view.prompt-review.export"> | BindingDefinition<"crtr.view.prompt-review.refresh"> | BindingDefinition<"crtr.view.prompt-review.quit"> | BindingDefinition<"crtr.view.settings.down"> | BindingDefinition<"crtr.view.settings.up"> | BindingDefinition<"crtr.view.settings.cell-previous"> | BindingDefinition<"crtr.view.settings.cell-next"> | BindingDefinition<"crtr.view.settings.refresh"> | BindingDefinition<"crtr.view.settings.quit"> | BindingDefinition<"crtr.view.settings.edit-or-submit"> | BindingDefinition<"crtr.view.settings.cancel-edit"> | BindingDefinition<"crtr.view.workspace-sidebar.down"> | BindingDefinition<"crtr.view.workspace-sidebar.up"> | BindingDefinition<"crtr.view.workspace-sidebar.open"> | BindingDefinition<"crtr.view.workspace-sidebar.refresh"> | BindingDefinition<"crtr.view.workspace-sidebar.quit"> | BindingDefinition<"crtr.view-picker.down"> | BindingDefinition<"crtr.view-picker.up"> | BindingDefinition<"crtr.view-picker.run"> | BindingDefinition<"crtr.view-picker.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">];
10
+ export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | `crtr.tmux.node.${'next' | 'previous'}` | `crtr.tmux.view.${'next' | 'previous'}` | `crtr.attach.${'detach' | 'clear-or-detach' | 'graph.toggle' | 'model-ladder.next' | 'model-ladder.previous'}` | 'crtr.mode.cycle' | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.view.host.${'force-quit' | 'editor.backspace'}` | `crtr.view.canvas.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.chat.${'submit' | 'contextual-cancel' | 'reconnect' | 'request-control' | 'release-control' | 'quit'}` | `crtr.view.git-pr.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.inbox.${InboxAction}` | `crtr.view.linkedin.${LinkedinAction}` | `crtr.view.prompt-review.${'down' | 'up' | 'compare-previous' | 'compare-next' | 'toggle-mode' | 'expand' | 'export' | 'refresh' | 'quit'}` | `crtr.view.settings.${'down' | 'up' | 'cell-previous' | 'cell-next' | 'refresh' | 'quit' | 'edit-or-submit' | 'cancel-edit'}` | `crtr.view.workspace-sidebar.${'down' | 'up' | 'open' | 'refresh' | 'quit'}` | `crtr.view-picker.${'down' | 'up' | 'run' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}`;
11
+ export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.promote">, BindingDefinition<"crtr.tmux.menu.resume">, BindingDefinition<"crtr.tmux.menu.demote">, BindingDefinition<"crtr.tmux.menu.detach">, BindingDefinition<"crtr.tmux.menu.close-subtree">, BindingDefinition<"crtr.tmux.menu.context">, BindingDefinition<"crtr.tmux.menu.providers">, BindingDefinition<"crtr.tmux.menu.graph">, BindingDefinition<"crtr.tmux.menu.focus-manager">, BindingDefinition<"crtr.tmux.menu.issues">, BindingDefinition<"crtr.tmux.menu.issue.idea">, BindingDefinition<"crtr.tmux.menu.issue.bug">, BindingDefinition<"crtr.tmux.node.next">, BindingDefinition<"crtr.tmux.node.previous">, BindingDefinition<"crtr.tmux.view.next">, BindingDefinition<"crtr.tmux.view.previous">, BindingDefinition<"crtr.attach.detach">, BindingDefinition<"crtr.attach.clear-or-detach">, BindingDefinition<"crtr.attach.graph.toggle">, BindingDefinition<"crtr.attach.model-ladder.next">, BindingDefinition<"crtr.attach.model-ladder.previous">, BindingDefinition<"crtr.mode.cycle">, BindingDefinition<"crtr.graph.down">, BindingDefinition<"crtr.graph.up">, BindingDefinition<"crtr.graph.first">, BindingDefinition<"crtr.graph.last">, BindingDefinition<"crtr.graph.collapse-or-manager">, BindingDefinition<"crtr.graph.expand-or-child">, BindingDefinition<"crtr.graph.focus">, BindingDefinition<"crtr.graph.focus-manager">, BindingDefinition<"crtr.graph.close-subtree">, BindingDefinition<"crtr.graph.dismiss">, BindingDefinition<"crtr.graph.confirm.accept">, BindingDefinition<"crtr.graph.confirm.cancel">, BindingDefinition<"crtr.view.host.force-quit">, BindingDefinition<"crtr.view.host.editor.backspace">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<"crtr.view.chat.submit">, BindingDefinition<"crtr.view.chat.contextual-cancel">, BindingDefinition<"crtr.view.chat.reconnect">, BindingDefinition<"crtr.view.chat.request-control">, BindingDefinition<"crtr.view.chat.release-control">, BindingDefinition<"crtr.view.chat.quit">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.view.prompt-review.down"> | BindingDefinition<"crtr.view.prompt-review.up"> | BindingDefinition<"crtr.view.prompt-review.compare-previous"> | BindingDefinition<"crtr.view.prompt-review.compare-next"> | BindingDefinition<"crtr.view.prompt-review.toggle-mode"> | BindingDefinition<"crtr.view.prompt-review.expand"> | BindingDefinition<"crtr.view.prompt-review.export"> | BindingDefinition<"crtr.view.prompt-review.refresh"> | BindingDefinition<"crtr.view.prompt-review.quit"> | BindingDefinition<"crtr.view.settings.down"> | BindingDefinition<"crtr.view.settings.up"> | BindingDefinition<"crtr.view.settings.cell-previous"> | BindingDefinition<"crtr.view.settings.cell-next"> | BindingDefinition<"crtr.view.settings.refresh"> | BindingDefinition<"crtr.view.settings.quit"> | BindingDefinition<"crtr.view.settings.edit-or-submit"> | BindingDefinition<"crtr.view.settings.cancel-edit"> | BindingDefinition<"crtr.view.workspace-sidebar.down"> | BindingDefinition<"crtr.view.workspace-sidebar.up"> | BindingDefinition<"crtr.view.workspace-sidebar.open"> | BindingDefinition<"crtr.view.workspace-sidebar.refresh"> | BindingDefinition<"crtr.view.workspace-sidebar.quit"> | BindingDefinition<"crtr.view-picker.down"> | BindingDefinition<"crtr.view-picker.up"> | BindingDefinition<"crtr.view-picker.run"> | BindingDefinition<"crtr.view-picker.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">];
12
12
  export declare const BINDING_IDS: ReadonlySet<string>;
13
13
  export declare const SAFETY_REQUIREMENTS: readonly SafetyRequirement[];
14
14
  export {};
@@ -45,8 +45,9 @@ export const BINDING_CATALOG = Object.freeze([
45
45
  b('crtr.attach.detach', 'Attach', 'Detach', 'Detach from the node.', ['ctrl+d'], 'host-global', ATTACH_CONTEXTS),
46
46
  b('crtr.attach.clear-or-detach', 'Attach', 'Clear or detach', 'Clear input or detach when input is empty.', ['ctrl+c'], 'host-global', ATTACH_CONTEXTS),
47
47
  b('crtr.attach.graph.toggle', 'Attach', 'Toggle graph', 'Open or close the graph overlay.', ['alt+g'], 'host-global', ATTACH_CONTEXTS),
48
- b('crtr.attach.model-ladder.next', 'Attach', 'Next model ladder', 'Move to the next model ladder rung.', ['alt+shift+m'], 'host-global', ATTACH_CONTEXTS),
49
- b('crtr.mode.cycle', 'Attach', 'Cycle mode', 'Cycle the active agent mode.', ['alt+m'], 'host-global', ATTACH_CONTEXTS),
48
+ b('crtr.attach.model-ladder.next', 'Attach', 'Next model ladder', 'Move to the next model ladder rung.', ['alt+m'], 'host-global', ATTACH_CONTEXTS),
49
+ b('crtr.attach.model-ladder.previous', 'Attach', 'Previous model ladder', 'Move to the previous model ladder rung.', ['alt+shift+m'], 'host-global', ATTACH_CONTEXTS),
50
+ b('crtr.mode.cycle', 'Attach', 'Cycle mode', 'Cycle the active agent mode.', ['alt+shift+p'], 'host-global', ATTACH_CONTEXTS),
50
51
  b('crtr.graph.down', 'GRAPH', 'Move down', 'Move to the next graph row.', ['j'], 'terminal', ['graph']),
51
52
  b('crtr.graph.up', 'GRAPH', 'Move up', 'Move to the previous graph row.', ['k'], 'terminal', ['graph']),
52
53
  b('crtr.graph.first', 'GRAPH', 'First row', 'Move to the first graph row.', ['g'], 'terminal', ['graph']),
@@ -93,7 +93,6 @@ const LEAF_ICON_CP = {
93
93
  'human notify': 0xf0f3,
94
94
  'human show': 0xf108,
95
95
  'human cancel': 0xf00d,
96
- 'human inbox': 0xf01c,
97
96
  'human list': 0xf03a,
98
97
  'human deck': 0xf0db,
99
98
  'human resolve': 0xf00c,
@@ -1083,29 +1082,27 @@ function truncateLine(line, max) {
1083
1082
  return line.length > max ? `${line.slice(0, max).trimEnd()}…` : line;
1084
1083
  }
1085
1084
  const ASK_TEASER_CHARS = 160;
1086
- /** `crtr human ask` (humanAsk.run, src/commands/human/prompts.ts:97) returns
1085
+ /** `crtr human ask` (humanAsk.run, src/commands/human/prompts.ts) returns
1087
1086
  * `{job_id, dir, follow_up}` — `job_id` is always short (a node id) so it
1088
1087
  * always bullets; `dir`/`follow_up` cross the prose threshold depending on
1089
- * path length, so the shape is recognized by `job_id` plus one of the two
1090
- * FIXED follow_up sentences (followUpResult/followUpDrain, shared.ts) being
1091
- * present anywhere in the text — never by assuming dir/follow_up's bullet
1092
- * vs. prose form. The question text isn't in the output at all (only the
1093
- * bare-question invocation echoes it), so it's recovered from argv: the
1094
- * first non-flag token after `human ask`, skipping `--context-file`'s value
1095
- * absent for a `--context-file` deck invocation, which still gets the
1096
- * road-sign line with no question teaser. Returns null when neither
1097
- * follow_up sentence is present (an error/unrecognized shape). */
1088
+ * path length, so the shape is recognized by `job_id` plus the single FIXED
1089
+ * QUEUED_FOLLOW_UP road-sign (`Queued for the human`, prompts.ts) appearing
1090
+ * anywhere in the text — never by assuming dir/follow_up's bullet vs. prose
1091
+ * form. Arrival is now always just durable queue state (no tmux/non-tmux
1092
+ * drained-vs-delivered fork), so the card collapses to one posted line. The
1093
+ * question text isn't in the output at all (only the bare-question invocation
1094
+ * echoes it), so it's recovered from argv: the first non-flag token after
1095
+ * `human ask`, skipping `--context-file`'s value absent for a
1096
+ * `--context-file` deck invocation, which still gets the road-sign line with
1097
+ * no question teaser. Returns null when the road-sign is absent (an
1098
+ * error/unrecognized shape). */
1098
1099
  function summarizeHumanAsk(text, tokens) {
1099
1100
  const jobId = /^- job_id:\s*(\S+)\s*$/m.exec(text)?.[1];
1100
1101
  if (jobId === undefined)
1101
1102
  return null;
1102
- const drained = text.includes('Not in tmux: a human must drain it');
1103
- const delivered = text.includes('no need to poll');
1104
- if (!drained && !delivered)
1103
+ if (!text.includes('Queued for the human'))
1105
1104
  return null;
1106
- const lines = drained
1107
- ? [{ text: 'ask posted — drain via `human inbox`', tone: 'warning' }]
1108
- : [{ text: 'ask posted → waiting on human', tone: 'accent' }];
1105
+ const lines = [{ text: 'ask posted → waiting on human', tone: 'accent' }];
1109
1106
  const question = firstPositionalArg(tokens, 2, new Set(['--context-file']));
1110
1107
  if (question !== undefined) {
1111
1108
  const firstLine = truncateLine(question.split('\n')[0].trim(), ASK_TEASER_CHARS);
@@ -1114,26 +1111,22 @@ function summarizeHumanAsk(text, tokens) {
1114
1111
  }
1115
1112
  return lines;
1116
1113
  }
1117
- /** `crtr human review` (humanReview.run, src/commands/human/prompts.ts:169)
1114
+ /** `crtr human review` (humanReview.run, src/commands/human/prompts.ts)
1118
1115
  * returns `{job_id, dir, output, follow_up}` — same recognition strategy as
1119
- * summarizeHumanAsk (job_id bullet + one of the two fixed follow_up
1120
- * sentences, this time followUpReview/followUpDrain). The reviewed file
1121
- * isn't in the output either, so its basename is recovered from argv (the
1122
- * file positional, skipping `--output`'s value). Returns null when neither
1123
- * follow_up sentence is present. */
1116
+ * summarizeHumanAsk (job_id bullet + the single fixed QUEUED_FOLLOW_UP
1117
+ * road-sign, `Queued for the human`, which review shares with ask). The
1118
+ * reviewed file isn't in the output either, so its basename is recovered
1119
+ * from argv (the file positional, skipping `--output`'s value). Returns null
1120
+ * when the road-sign is absent. */
1124
1121
  function summarizeHumanReview(text, tokens) {
1125
1122
  const jobId = /^- job_id:\s*(\S+)\s*$/m.exec(text)?.[1];
1126
1123
  if (jobId === undefined)
1127
1124
  return null;
1128
- const drained = text.includes('Not in tmux: a human must drain it');
1129
- const live = text.includes("live on the human's screen for anchored, line-by-line review");
1130
- if (!drained && !live)
1125
+ if (!text.includes('Queued for the human'))
1131
1126
  return null;
1132
1127
  const file = firstPositionalArg(tokens, 2, new Set(['--output']));
1133
1128
  const label = file !== undefined ? `review posted: ${basename(file)}` : 'review posted';
1134
- return drained
1135
- ? [{ text: `${label} — drain via \`human inbox\``, tone: 'warning' }]
1136
- : [{ text: `${label} → waiting on human`, tone: 'accent' }];
1129
+ return [{ text: `${label} → waiting on human`, tone: 'accent' }];
1137
1130
  }
1138
1131
  /** `crtr human list` (humanList.run, src/commands/human/queue.ts:283) has no
1139
1132
  * custom `render`; the generic renderer bullets `- total: N` then an
@@ -297,10 +297,10 @@ function rootProfileMessage(cwd) {
297
297
  * the session IS interactive. Ask on the controlling terminal — create a
298
298
  * profile pointing at cwd, or proceed as root — and block until answered.
299
299
  * Deliberately a minimal `readline` prompt, not `crtr human ask`: that bridge
300
- * is asynchronous (writes a deck, waits for a separate process to answer it)
301
- * and cannot gate pi's boot; the codebase's other humanloop entry points
302
- * (`launchReview`, the `human _run` bridge) are built for reviewing an
303
- * artifact or a queued interaction, not a two-choice gate on the same TTY pi
300
+ * is asynchronous (registers an interaction with humanloop, then waits for the
301
+ * human to answer it from a separate inbox) and cannot gate pi's boot; the
302
+ * humanloop entry points (`human ask`/`review`) are built for a queued,
303
+ * asynchronously-answered interaction, not a two-choice gate on the same TTY pi
304
304
  * is about to take over — a raw `readline` question is the actual minimal
305
305
  * fit here. */
306
306
  /** Explicit `--profile` names a profile whose manifest does NOT cover cwd.
@@ -21,9 +21,23 @@ import { join } from 'node:path';
21
21
  * the sessions dir — same vendoring rationale as the slash-command list below. */
22
22
  export function piSessionsRoot() {
23
23
  const env = process.env['PI_CODING_AGENT_DIR'];
24
- const agentDir = env !== undefined && env !== '' ? env : join(homedir(), '.pi', 'agent');
24
+ const agentDir = env !== undefined && env !== '' ? expandTilde(env) : join(homedir(), '.pi', 'agent');
25
25
  return join(agentDir, 'sessions');
26
26
  }
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
+ }
27
41
  /**
28
42
  * pi's builtin slash commands — vendored verbatim from pi `core/slash-commands.js`
29
43
  * `BUILTIN_SLASH_COMMANDS` (review C1). **21 entries at 0.78.1** (review n1 — NOT 23).