@crouton-kit/crouter 0.3.81 → 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 (84) 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 +960 -961
  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/front-door.js +19 -6
  56. package/dist/core/runtime/package-health.d.ts +13 -0
  57. package/dist/core/runtime/package-health.js +18 -0
  58. package/dist/core/runtime/pi-vendored.js +15 -1
  59. package/dist/core/runtime/placement.d.ts +1 -10
  60. package/dist/core/runtime/placement.js +43 -26
  61. package/dist/core/runtime/resume.js +72 -4
  62. package/dist/core/runtime/session-list-cache.d.ts +22 -2
  63. package/dist/core/runtime/session-list-cache.js +92 -24
  64. package/dist/core/runtime/spawn.js +9 -0
  65. package/dist/core/spawn.d.ts +0 -59
  66. package/dist/core/spawn.js +5 -163
  67. package/dist/daemon/crtrd.js +7 -6
  68. package/dist/index.d.ts +2 -2
  69. package/dist/index.js +1 -1
  70. package/dist/web-client/assets/index-DZdKS-FB.js +78 -0
  71. package/dist/web-client/index.html +1 -1
  72. package/dist/web-client/sw.js +1 -1
  73. package/package.json +2 -2
  74. package/dist/core/__tests__/full/dead-pane-regression.test.js +0 -154
  75. package/dist/core/__tests__/full/human-new-window-regression.test.js +0 -104
  76. package/dist/core/__tests__/full/review-render-pane-regression.test.js +0 -133
  77. package/dist/core/__tests__/human-stranded-deliver.test.js +0 -145
  78. package/dist/core/__tests__/human-surface-target.test.js +0 -98
  79. package/dist/web-client/assets/index--md2ylfi.js +0 -78
  80. /package/dist/{core/__tests__/full/dead-pane-regression.test.d.ts → clients/attach/__tests__/chat-view-snapshot-ordering.test.d.ts} +0 -0
  81. /package/dist/{core/__tests__/full/human-new-window-regression.test.d.ts → commands/sys/__tests__/setup-front-door.test.d.ts} +0 -0
  82. /package/dist/core/__tests__/{full/review-render-pane-regression.test.d.ts → human-deliver-e2e.test.d.ts} +0 -0
  83. /package/dist/core/__tests__/{human-stranded-deliver.test.d.ts → human-deliver.test.d.ts} +0 -0
  84. /package/dist/core/__tests__/{human-surface-target.test.d.ts → pid-identity-match.test.d.ts} +0 -0
@@ -2,16 +2,22 @@ import { defineLeaf } from '../../core/command.js';
2
2
  import { InputError } from '../../core/io.js';
3
3
  import { spawnNode } from '../../core/runtime/nodes.js';
4
4
  import { interactionDir } from '../../core/artifact.js';
5
- import { tmuxServerReachable } from '../../core/spawn.js';
6
5
  import { mkdirSync, existsSync } from 'node:fs';
7
6
  import { join, resolve } from 'node:path';
8
7
  import { randomBytes } from 'node:crypto';
9
- import { validateDeck, notifyDeck, atomicWriteJson, deckPath, display, } from '@crouton-kit/humanloop';
10
- import { DECK_SCHEMA_HINT, followUpReview, spawnHumanJob, detachHumanTui, resolveMaxPanes, } from './shared.js';
8
+ import { tmuxServerReachable } from '../../core/spawn.js';
9
+ import { validateDeck, notifyDeck, atomicWriteJson, submitDeck, submitReview, display, } from '@crouton-kit/humanloop';
10
+ import { DECK_SCHEMA_HINT, registerCrouterRoot, resolveMaxPanes } from './shared.js';
11
11
  /** The asking node's id, or null when run from a bare shell (no parent to route to). */
12
12
  function askingNode() {
13
13
  return process.env['CRTR_NODE_ID'] ?? null;
14
14
  }
15
+ /** Attribution stamp so C2's attention chrome can bucket the ticket by the
16
+ * agent that raised it. A bare-shell prompt has no node and stays unattributed. */
17
+ function sourceStamp() {
18
+ const nodeId = askingNode();
19
+ return nodeId !== null && nodeId !== '' ? { nodeId } : {};
20
+ }
15
21
  function buildHumanAskDeck(question) {
16
22
  return validateDeck({
17
23
  title: question,
@@ -26,6 +32,7 @@ function buildHumanAskDeck(question) {
26
32
  ],
27
33
  });
28
34
  }
35
+ const QUEUED_FOLLOW_UP = "Queued for the human. Their answer is pushed to your inbox and wakes you when they respond — never poll, verify it opened, or block. Press ⌥I (or run `hl inbox open` at a terminal) to see the queue.";
29
36
  // ---------------------------------------------------------------------------
30
37
  // ask
31
38
  // ---------------------------------------------------------------------------
@@ -48,8 +55,8 @@ export const humanAsk = defineLeaf({
48
55
  ],
49
56
  outputKind: 'object',
50
57
  effects: [
51
- 'Creates a kind:"human" node under you and writes deck.json/run.json to the interaction dir.',
52
- 'Spawns the decision TUI in a detached tmux pane (when in tmux).',
58
+ 'Creates a kind:"human" node under you and enqueues a decision deck in the humanloop inbox.',
59
+ 'Returns immediately; nothing opens on screen. The human answers from the inbox popup (⌥I) on their own time.',
53
60
  ],
54
61
  },
55
62
  run: async (input) => {
@@ -81,15 +88,19 @@ export const humanAsk = defineLeaf({
81
88
  next: DECK_SCHEMA_HINT,
82
89
  });
83
90
  }
91
+ // Spread the stamp LAST so an agent-supplied source can carry labels
92
+ // (sessionName/askedBy/blockedSince) but can never override the authoritative
93
+ // nodeId. A bare shell stamps `{}`, leaving any agent source intact.
94
+ deck = { ...deck, source: { ...(deck.source ?? {}), ...sourceStamp() } };
84
95
  const cwd = process.cwd();
85
96
  const jobId = spawnNode({ kind: 'human', parent: askingNode(), cwd, name: 'human-ask', lifecycle: 'terminal' }).node_id;
86
97
  const idir = interactionDir(jobId, cwd);
87
98
  mkdirSync(idir, { recursive: true });
88
- atomicWriteJson(deckPath(idir), deck);
89
99
  const rc = { mode: 'ask', job_id: jobId };
90
100
  atomicWriteJson(join(idir, 'run.json'), rc);
91
- const { follow_up } = spawnHumanJob(jobId, idir, cwd);
92
- return { job_id: jobId, dir: idir, follow_up };
101
+ const root = registerCrouterRoot(cwd);
102
+ const { dir } = submitDeck({ root, id: jobId, deck });
103
+ return { job_id: jobId, dir, follow_up: QUEUED_FOLLOW_UP };
93
104
  },
94
105
  });
95
106
  // ---------------------------------------------------------------------------
@@ -101,8 +112,8 @@ export const humanReview = defineLeaf({
101
112
  whenToUse: 'a human should comment line-by-line on a document rather than give one overall answer: reviewing a plan or spec before you build it, marking up a draft, flagging specific sections to change. The comments come back anchored to the lines they touch. Use `ask` instead for a single yes/no sign-off on the whole thing, or to pose a discrete choice',
102
113
  help: {
103
114
  name: 'human review',
104
- summary: "put a .md live on the human's screen for anchored, line-by-line comments; returns a job handle instantly (a non-blocking kickoff, like ask). The pane tracks the file and re-renders on every save, so edit in place rather than re-presenting.",
105
- guide: "A kickoff, not a blocking call: it returns at once and the human reviews on their own time. When they submit, the FeedbackResult (anchored comments, plus any line edits they made) is pushed to your inbox — waking you — and autosaved to `output`; you never poll, verify it opened, or background it. The pane is a LIVE view of the file: keep editing the .md in place and it updates, so do not cancel and re-present to show a change. The .md is directive-flavored markdown rendered by termrender (panels, columns, trees, callouts, mermaid) — see `termrender doc -h` for the directive set before authoring one.",
115
+ summary: "queue a .md for anchored, line-by-line human comments; returns a job handle instantly (a non-blocking kickoff, like ask). The human opens it from the inbox popup, which previews the live doc and re-reads it on every save, so edit in place rather than re-presenting.",
116
+ guide: "A kickoff, not a blocking call: it returns at once and the human reviews on their own time from the inbox (⌥I). When they submit, the FeedbackResult (anchored comments, plus any line edits they made) is pushed to your inbox — waking you — and autosaved to `output`; you never poll, verify it opened, or background it. The inbox preview is a LIVE view of the file: keep editing the .md in place and it updates, so do not cancel and re-present to show a change. The .md is directive-flavored markdown rendered by termrender (panels, columns, trees, callouts, mermaid) — see `termrender doc -h` for the directive set before authoring one.",
106
117
  params: [
107
118
  { kind: 'positional', name: 'file', type: 'path', required: true, constraint: 'Absolute path to an existing .md file.' },
108
119
  { kind: 'flag', name: 'output', type: 'path', required: false, constraint: 'Where the FeedbackResult JSON is written. Default: <dir>/feedback.json.' },
@@ -111,13 +122,12 @@ export const humanReview = defineLeaf({
111
122
  { name: 'job_id', type: 'string', required: true, constraint: 'Node id of the kind:"human" node backing this review. Its FeedbackResult is pushed to your inbox when the human submits.' },
112
123
  { name: 'dir', type: 'string', required: true, constraint: 'Interaction directory holding run.json and the autosaved feedback.json.' },
113
124
  { name: 'output', type: 'string', required: true, constraint: 'Path the FeedbackResult JSON is autosaved to when the human submits.' },
114
- { name: 'follow_up', type: 'string', required: true, constraint: 'A non-blocking road sign. The human may take minutes to hours; do not block, poll, or verify the pane — just end your turn and you are woken when they submit.' },
125
+ { name: 'follow_up', type: 'string', required: true, constraint: 'A non-blocking road sign. The human may take minutes to hours; do not block, poll, or verify — just end your turn and you are woken when they submit.' },
115
126
  ],
116
127
  outputKind: 'object',
117
128
  effects: [
118
- 'Creates a kind:"human" node under you and writes run.json to the interaction dir.',
119
- 'Spawns a live review surface (detached tmux panes when in tmux): the raw source in a read-only editor for anchoring comments, beside a termrender-rendered view of the doc; both track the file and re-render on save.',
120
- 'Returns instantly; the anchored comments fan into your inbox when the human submits (off-tmux, drain via `crtr human inbox`).',
129
+ 'Creates a kind:"human" node under you and enqueues an anchored review in the humanloop inbox.',
130
+ 'Returns immediately; nothing opens on screen. The human reviews from the inbox popup (⌥I); comments fan into your inbox when they submit.',
121
131
  ],
122
132
  },
123
133
  run: async (input) => {
@@ -143,19 +153,22 @@ export const humanReview = defineLeaf({
143
153
  const jobId = spawnNode({ kind: 'human', parent: askingNode(), cwd, name: 'human-review', lifecycle: 'terminal' }).node_id;
144
154
  const idir = interactionDir(jobId, cwd);
145
155
  mkdirSync(idir, { recursive: true });
146
- const outputArg = input['output'];
147
- const output = outputArg !== undefined ? outputArg : join(idir, 'feedback.json');
148
- const rc = { mode: 'review', job_id: jobId, file: abs, output };
156
+ const rc = { mode: 'review', job_id: jobId };
149
157
  atomicWriteJson(join(idir, 'run.json'), rc);
150
- // review is a non-blocking kickoff, exactly like ask: the detached
151
- // `_run` worker pushes the FeedbackResult as this node's final report when
152
- // the human submits, which fans into our inbox and wakes us (and is also
153
- // autosaved to `output`). There is nothing to block on — the doc is live on
154
- // the human's screen and tracks the file, so the caller keeps editing in
155
- // place or simply ends its turn.
156
- const { spawned, follow_up: drainFollowUp } = spawnHumanJob(jobId, idir, cwd);
157
- const follow_up = spawned ? followUpReview(jobId) : drainFollowUp;
158
- return { job_id: jobId, dir: idir, output, follow_up };
158
+ const outputArg = input['output'];
159
+ const root = registerCrouterRoot(cwd);
160
+ const { dir } = submitReview({
161
+ root,
162
+ id: jobId,
163
+ review: {
164
+ file: abs,
165
+ ...(outputArg !== undefined ? { output: resolve(outputArg) } : {}),
166
+ title: abs.split('/').pop() ?? abs,
167
+ source: sourceStamp(),
168
+ },
169
+ });
170
+ const output = outputArg !== undefined ? resolve(outputArg) : join(dir, 'feedback.json');
171
+ return { job_id: jobId, dir, output, follow_up: QUEUED_FOLLOW_UP };
159
172
  },
160
173
  });
161
174
  // ---------------------------------------------------------------------------
@@ -167,42 +180,42 @@ export const humanNotify = defineLeaf({
167
180
  whenToUse: 'informing a person without blocking or expecting an answer',
168
181
  help: {
169
182
  name: 'human notify',
170
- summary: 'show a fire-and-forget acknowledgement; creates no job',
183
+ summary: 'enqueue a fire-and-forget acknowledgement in the human inbox; creates no job',
171
184
  guide: 'The body is directive-flavored markdown rendered by termrender (panels, columns, trees, callouts, mermaid) — see `termrender doc -h` for the directive set before authoring one.',
172
185
  params: [
173
186
  { kind: 'positional', name: 'title', type: 'string', required: true, constraint: 'The notification headline.' },
174
187
  { kind: 'flag', name: 'body', type: 'string', required: false, constraint: 'Optional markdown body.' },
175
188
  ],
176
189
  output: [
177
- { name: 'shown', type: 'boolean', required: true, constraint: 'True if the TUI pane was spawned; false when not in tmux (deck surfaces in `human inbox`).' },
190
+ { name: 'queued', type: 'boolean', required: true, constraint: 'Always true the notify deck is durable inbox state the human acknowledges from the popup.' },
178
191
  { name: 'dir', type: 'string', required: true, constraint: 'Interaction directory holding deck.json.' },
179
192
  ],
180
193
  outputKind: 'object',
181
194
  effects: [
182
- 'Writes a notify deck to the per-project interactions root.',
183
- 'Spawns the acknowledgement TUI in a detached tmux pane when in tmux. Creates no node.',
195
+ 'Enqueues a durable notify deck in the per-project human inbox. Creates no node; nothing opens on screen.',
184
196
  ],
185
197
  },
186
198
  run: async (input) => {
187
199
  const title = input['title'];
188
200
  const body = input['body'];
189
- const deck = notifyDeck(title, body !== undefined ? { body } : {});
201
+ let deck = notifyDeck(title, body !== undefined ? { body } : {});
202
+ // Stamp LAST so nodeId cannot be spoofed (see humanAsk).
203
+ deck = { ...deck, source: { ...(deck.source ?? {}), ...sourceStamp() } };
190
204
  const cwd = process.cwd();
191
205
  const id = `nfy-${randomBytes(4).toString('hex')}`;
192
206
  const idir = interactionDir(id, cwd);
193
207
  mkdirSync(idir, { recursive: true });
194
- atomicWriteJson(deckPath(idir), deck);
195
208
  const rc = { mode: 'notify' };
196
209
  atomicWriteJson(join(idir, 'run.json'), rc);
197
- let shown = false;
198
- if (tmuxServerReachable()) {
199
- shown = detachHumanTui(idir, cwd).status === 'spawned';
200
- }
201
- return { shown, dir: idir };
210
+ const root = registerCrouterRoot(cwd);
211
+ const { dir } = submitDeck({ root, id, deck });
212
+ return { queued: true, dir };
202
213
  },
203
214
  });
204
215
  // ---------------------------------------------------------------------------
205
- // show (no job, non-blocking passthrough)
216
+ // show (no job, non-blocking passthrough) — passive live document display, NOT
217
+ // a ticket surface. Retained per the tmux-inbox cutover: `display()` puts a
218
+ // file live in a watch pane and never enqueues anything.
206
219
  // ---------------------------------------------------------------------------
207
220
  export const humanShow = defineLeaf({
208
221
  name: 'show',
@@ -1,32 +1,5 @@
1
- /** Deliver-back + reap an interaction whose answer is on disk but was never
2
- * delivered. The detached `_run` worker's `pushFinal` is normally the SOLE
3
- * deliver-back + reap step; when it never ran (e.g. a broker asker pre-fix, or
4
- * a human draining via `crtr human inbox`, which writes response.json directly
5
- * and never calls pushFinal), the answer strands and the bridge node leaks.
6
- *
7
- * Idempotent and self-gating: a no-op unless the bridge node is still LIVE
8
- * (active|idle) AND its interaction is resolved on disk — so it never
9
- * double-delivers an interaction the `_run` worker already finalized (pushFinal
10
- * flips status=done). Reconstructs the same pushFinal body `_run` would have
11
- * emitted, per mode; a canceled-on-disk response reaps the node without
12
- * delivering a result (mirrors `human cancel`). Returns true iff it acted. */
13
- export declare function finalizeResolvedInteraction(jobId: string, claim?: InteractionClaim): Promise<boolean>;
14
- /** Sweep every interaction under the cwd's interactions root and deliver-back +
15
- * reap any that are answered-but-undelivered (see finalizeResolvedInteraction).
16
- * Interaction dir names ARE the bridge node ids. Returns how many it healed. */
17
- export declare function healStrandedInteractions(cwd: string): Promise<number>;
18
- export declare const humanInbox: import("../../core/command.js").LeafDef;
19
1
  export declare const humanList: import("../../core/command.js").LeafDef;
20
2
  export declare const humanDeck: import("../../core/command.js").LeafDef;
21
- interface InteractionClaim {
22
- release: () => void;
23
- }
24
- export declare function acquireInteractionClaim(dir: string, jobId: string, opts?: {
25
- allowResolved?: boolean;
26
- markProgress?: boolean;
27
- respectProgress?: boolean;
28
- }): InteractionClaim | 'already_resolved' | 'claimed';
29
3
  export declare const humanResolve: import("../../core/command.js").LeafDef;
30
4
  export declare const humanCancel: import("../../core/command.js").LeafDef;
31
- export declare const humanRun: import("../../core/command.js").LeafDef;
32
- export {};
5
+ export declare const humanDeliver: import("../../core/command.js").LeafDef;