@crouton-kit/crouter 0.3.82 → 0.3.84
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.
- package/dist/builtin-pi-packages/pi-mode-switch/README.md +3 -3
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +12 -4
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +21 -4
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +104 -0
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +17 -44
- package/dist/clients/attach/attach-cmd.d.ts +2 -3
- package/dist/clients/attach/attach-cmd.js +689 -689
- package/dist/clients/attach/attach-def.d.ts +2 -0
- package/dist/clients/attach/attach-def.js +93 -0
- package/dist/clients/attach/canvas-panels.d.ts +10 -3
- package/dist/clients/attach/canvas-panels.js +42 -16
- package/dist/clients/attach/chat-view.d.ts +20 -1
- package/dist/clients/attach/chat-view.js +85 -2
- package/dist/commands/__tests__/human.test.js +7 -22
- package/dist/commands/attention.js +31 -30
- package/dist/commands/human/prompts.js +57 -38
- package/dist/commands/human/queue.d.ts +1 -28
- package/dist/commands/human/queue.js +156 -520
- package/dist/commands/human/shared.d.ts +28 -68
- package/dist/commands/human/shared.js +37 -120
- package/dist/commands/human.js +15 -17
- package/dist/commands/node.js +5 -2
- package/dist/commands/surface.js +1 -1
- package/dist/commands/sys/__tests__/setup-core.test.js +1 -1
- package/dist/commands/sys/__tests__/setup-front-door.test.js +85 -0
- package/dist/commands/sys/setup-core.d.ts +10 -0
- package/dist/commands/sys/setup-core.js +39 -1
- package/dist/commands/sys/setup.js +10 -1
- package/dist/core/__tests__/boot.test.js +141 -6
- package/dist/core/__tests__/human-deliver-e2e.test.js +95 -0
- package/dist/core/__tests__/human-deliver.test.js +332 -0
- package/dist/core/__tests__/human-node-not-supervised.test.js +5 -4
- package/dist/core/__tests__/pid-identity-match.test.js +145 -0
- package/dist/core/__tests__/tmux-surface.test.js +180 -2
- package/dist/core/canvas/__tests__/attention.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/attention.test.js +126 -0
- package/dist/core/canvas/__tests__/remote-canvas-source.test.js +4 -4
- package/dist/core/canvas/__tests__/render-remote.test.js +1 -1
- package/dist/core/canvas/attention.d.ts +16 -14
- package/dist/core/canvas/attention.js +34 -29
- package/dist/core/canvas/boot.d.ts +45 -0
- package/dist/core/canvas/boot.js +36 -3
- package/dist/core/canvas/browse/app.js +3 -3
- package/dist/core/canvas/canvas.d.ts +40 -0
- package/dist/core/canvas/canvas.js +65 -1
- package/dist/core/canvas/pid.d.ts +86 -17
- package/dist/core/canvas/pid.js +152 -37
- package/dist/core/canvas/remote-canvas-source.d.ts +1 -1
- package/dist/core/canvas/remote-canvas-source.js +4 -2
- package/dist/core/canvas/render.d.ts +5 -5
- package/dist/core/canvas/render.js +14 -14
- package/dist/core/canvas/source.d.ts +2 -2
- package/dist/core/canvas/source.js +3 -3
- package/dist/core/keybindings/__tests__/inbox-affordance.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/inbox-affordance.test.js +38 -0
- package/dist/core/keybindings/__tests__/resolve.test.js +2 -1
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +4 -2
- package/dist/core/keybindings/inbox.d.ts +21 -0
- package/dist/core/keybindings/inbox.js +35 -0
- package/dist/core/keybindings/index.d.ts +1 -0
- package/dist/core/keybindings/index.js +1 -0
- package/dist/core/preview-registry.js +23 -29
- package/dist/core/profiles/select.js +4 -4
- package/dist/core/runtime/pi-vendored.js +15 -1
- package/dist/core/runtime/placement.d.ts +1 -10
- package/dist/core/runtime/placement.js +43 -26
- package/dist/core/runtime/session-list-cache.d.ts +22 -2
- package/dist/core/runtime/session-list-cache.js +92 -24
- package/dist/core/runtime/spawn.js +2 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +1 -0
- package/dist/core/runtime/tmux.js +55 -1
- package/dist/core/spawn.d.ts +0 -59
- package/dist/core/spawn.js +5 -163
- package/dist/daemon/crtrd.js +36 -11
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/web-client/assets/index-DZdKS-FB.js +78 -0
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +2 -2
- package/dist/core/__tests__/full/dead-pane-regression.test.js +0 -154
- package/dist/core/__tests__/full/human-new-window-regression.test.js +0 -104
- package/dist/core/__tests__/full/review-render-pane-regression.test.js +0 -133
- package/dist/core/__tests__/human-stranded-deliver.test.js +0 -145
- package/dist/core/__tests__/human-surface-target.test.js +0 -98
- package/dist/web-client/assets/index--md2ylfi.js +0 -78
- /package/dist/{core/__tests__/full/dead-pane-regression.test.d.ts → clients/attach/__tests__/chat-view-snapshot-ordering.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/human-new-window-regression.test.d.ts → commands/sys/__tests__/setup-front-door.test.d.ts} +0 -0
- /package/dist/core/__tests__/{full/review-render-pane-regression.test.d.ts → human-deliver-e2e.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-stranded-deliver.test.d.ts → human-deliver.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-surface-target.test.d.ts → pid-identity-match.test.d.ts} +0 -0
|
@@ -1,79 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { CompletionHandler } from '@crouton-kit/humanloop';
|
|
2
2
|
export declare const DECK_SCHEMA_HINT: string;
|
|
3
|
+
/**
|
|
4
|
+
* Crouter's private per-interaction record. Reduced to the delivery essentials:
|
|
5
|
+
* the mode `_deliver` dispatches on and the bridge node id it delivers back to.
|
|
6
|
+
* Humanloop owns every public request/result/output path (deck.json,
|
|
7
|
+
* response.json, review.json); crouter never records pane ids or output paths.
|
|
8
|
+
*/
|
|
3
9
|
export interface RunRecord {
|
|
4
10
|
mode: 'ask' | 'notify' | 'review';
|
|
5
11
|
job_id?: string;
|
|
6
|
-
file?: string;
|
|
7
|
-
output?: string;
|
|
8
|
-
/** tmux pane id of the detached TUI, recorded so `human cancel` can kill it. */
|
|
9
|
-
pane_id?: string;
|
|
10
|
-
/** tmux pane id of a review's live termrender render pane, so cancel/finish can clear it. */
|
|
11
|
-
render_pane_id?: string;
|
|
12
12
|
}
|
|
13
13
|
export declare function resolveMaxPanes(): number;
|
|
14
|
-
export declare function pickPlacement(targetPane?: string): 'split-h' | 'new-window';
|
|
15
|
-
/**
|
|
16
|
-
* The tmux pane the humanloop TUI should open BESIDE so a PERSON actually sees
|
|
17
|
-
* it. A prompt raised by a canvas node must NOT land in the backstage `crtr`
|
|
18
|
-
* session (the holding ground for un-watched node panes) — it lands in the
|
|
19
|
-
* session the user is watching that node's graph in:
|
|
20
|
-
* 1. node prompt (CRTR_NODE_ID set) → the HIGHEST FOCUSED node of its graph
|
|
21
|
-
* (`graphSurfaceTarget`, the focused node closest to the graph root): split
|
|
22
|
-
* beside it, in the session/window the user already has it open in.
|
|
23
|
-
* 2. nothing in the graph on screen (or a dead focus pane), or a bare-shell
|
|
24
|
-
* prompt → the user's currently-attached pane (`attachedClientPane`).
|
|
25
|
-
* 3. neither resolvable → undefined (tmux falls back to the caller pane).
|
|
26
|
-
* The TUI lands in the right place but NEVER switches the user's session/window
|
|
27
|
-
* (no switch-client / select-window; new-window opens `-d`). They see it when
|
|
28
|
-
* they look at the node they are already watching.
|
|
29
|
-
*/
|
|
30
|
-
export declare function resolveHumanTarget(): string | undefined;
|
|
31
|
-
/**
|
|
32
|
-
* Open the detached humanloop `_run` pane for an interaction dir, ROUTED to the
|
|
33
|
-
* session the user is watching (`resolveHumanTarget`). The single spawn path
|
|
34
|
-
* behind ask/review (`spawnHumanJob`) and notify. `detached: true` keeps
|
|
35
|
-
* the user's view put — the prompt lands beside the watched node without
|
|
36
|
-
* jumping their session/window. `jobId`, when given, is injected as CRTR_JOB_ID.
|
|
37
|
-
*/
|
|
38
|
-
export declare function detachHumanTui(idir: string, cwd: string, jobId?: string): DetachResult;
|
|
39
|
-
export declare function runCmd(dir: string): string;
|
|
40
|
-
export declare function followUpResult(_jobId: string): string;
|
|
41
|
-
export declare function followUpDrain(_jobId: string): string;
|
|
42
14
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
15
|
+
* The direct-exec completion handler humanloop runs when a crouter ticket
|
|
16
|
+
* resolves, fed the completion event on stdin. The argv is absolute and
|
|
17
|
+
* PATH/cwd-independent — never a `crtr` shell string — and, critically, must
|
|
18
|
+
* actually EXECUTE in both crouter contexts:
|
|
19
|
+
*
|
|
20
|
+
* - Built (`dist/cli.js` present): `node <abs>/dist/cli.js human _deliver`.
|
|
21
|
+
* - Dev/tsx (only `src/cli.ts` on disk): plain `node` cannot run the `.ts`
|
|
22
|
+
* entry — its mandatory `.js`-extension ESM imports have no `.js` sibling on
|
|
23
|
+
* disk, so Node's type-stripping fails at the first relative import. So we
|
|
24
|
+
* register tsx's ESM loader first (`node --import <abs tsx/esm> src/cli.ts …`),
|
|
25
|
+
* with the tsx entry resolved to an absolute file URL so the handler stays
|
|
26
|
+
* cwd-independent when humanloop invokes it. tsx is necessarily present here
|
|
27
|
+
* because crouter itself is running under it.
|
|
54
28
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* so the `pushFinal` the `_run` worker emits — for ask AND review —
|
|
58
|
-
* fans the answer straight into the asking node's inbox. The pane id is recorded
|
|
59
|
-
* on run.json (not returned) so `human cancel` can later kill the TUI.
|
|
29
|
+
* Throws loudly when neither entry exists rather than registering an argv that
|
|
30
|
+
* humanloop would retry forever.
|
|
60
31
|
*/
|
|
61
|
-
export declare function
|
|
62
|
-
spawned: boolean;
|
|
63
|
-
follow_up: string;
|
|
64
|
-
};
|
|
32
|
+
export declare function crouterDeliverHandler(): CompletionHandler;
|
|
65
33
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* 1. paneId must be a real tmux pane id (`%<n>`) — never an empty/odd string.
|
|
72
|
-
* 2. The pane's start command must contain `verify` (the interaction dir, which
|
|
73
|
-
* humanloop bakes into the worker's `CRTR_HUMAN_DIR=... crtr human _run`
|
|
74
|
-
* launch). A shell (`zsh -l`) or the agent's pi never matches, so we can
|
|
75
|
-
* only ever kill the exact worker we spawned for this job.
|
|
76
|
-
*
|
|
77
|
-
* Returns true only when a matching pane was found and killed. Never throws.
|
|
34
|
+
* Register the per-cwd interaction root with humanloop as owner `crouter`,
|
|
35
|
+
* attaching the `_deliver` completion handler. Idempotent for owner `crouter`
|
|
36
|
+
* (refreshes the handler), and creates/canonicalizes the root. Returns the
|
|
37
|
+
* canonical root path humanloop submissions must be rooted at.
|
|
78
38
|
*/
|
|
79
|
-
export declare function
|
|
39
|
+
export declare function registerCrouterRoot(cwd: string): string;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
1
5
|
import { readConfig } from '../../core/config.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { atomicWriteJson, readJson } from '@crouton-kit/humanloop';
|
|
5
|
-
import { countPanesInWindow, spawnAndDetach, shellQuote, attachedClientPane, paneAlive, } from '../../core/spawn.js';
|
|
6
|
-
import { graphSurfaceTarget } from '../../core/runtime/placement.js';
|
|
6
|
+
import { interactionsRoot } from '../../core/artifact.js';
|
|
7
|
+
import { registerInboxRoot } from '@crouton-kit/humanloop';
|
|
7
8
|
export const DECK_SCHEMA_HINT = 'Deck must match the humanloop deck schema: {title?, ' +
|
|
8
9
|
'source?:{sessionName?,askedBy?,blockedSince?}, interactions:[{id, ' +
|
|
9
10
|
'title (short ≤~4-word topic — the thing being decided, not the decision), ' +
|
|
@@ -16,127 +17,43 @@ export const DECK_SCHEMA_HINT = 'Deck must match the humanloop deck schema: {tit
|
|
|
16
17
|
export function resolveMaxPanes() {
|
|
17
18
|
return readConfig('user').max_panes_per_window;
|
|
18
19
|
}
|
|
19
|
-
export function pickPlacement(targetPane) {
|
|
20
|
-
return countPanesInWindow(targetPane) >= resolveMaxPanes() ? 'new-window' : 'split-h';
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* The tmux pane the humanloop TUI should open BESIDE so a PERSON actually sees
|
|
24
|
-
* it. A prompt raised by a canvas node must NOT land in the backstage `crtr`
|
|
25
|
-
* session (the holding ground for un-watched node panes) — it lands in the
|
|
26
|
-
* session the user is watching that node's graph in:
|
|
27
|
-
* 1. node prompt (CRTR_NODE_ID set) → the HIGHEST FOCUSED node of its graph
|
|
28
|
-
* (`graphSurfaceTarget`, the focused node closest to the graph root): split
|
|
29
|
-
* beside it, in the session/window the user already has it open in.
|
|
30
|
-
* 2. nothing in the graph on screen (or a dead focus pane), or a bare-shell
|
|
31
|
-
* prompt → the user's currently-attached pane (`attachedClientPane`).
|
|
32
|
-
* 3. neither resolvable → undefined (tmux falls back to the caller pane).
|
|
33
|
-
* The TUI lands in the right place but NEVER switches the user's session/window
|
|
34
|
-
* (no switch-client / select-window; new-window opens `-d`). They see it when
|
|
35
|
-
* they look at the node they are already watching.
|
|
36
|
-
*/
|
|
37
|
-
export function resolveHumanTarget() {
|
|
38
|
-
const nodeId = process.env['CRTR_NODE_ID'];
|
|
39
|
-
if (nodeId !== undefined && nodeId !== '') {
|
|
40
|
-
const f = graphSurfaceTarget(nodeId);
|
|
41
|
-
if (f !== null && f.pane !== null && paneAlive(f.pane))
|
|
42
|
-
return f.pane;
|
|
43
|
-
}
|
|
44
|
-
return attachedClientPane() ?? undefined;
|
|
45
|
-
}
|
|
46
20
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
killAfterSeconds: 0,
|
|
61
|
-
detached: true,
|
|
62
|
-
...(targetPane !== undefined ? { targetPane } : {}),
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
export function runCmd(dir) {
|
|
66
|
-
return `CRTR_HUMAN_DIR=${shellQuote(dir)} crtr human _run`;
|
|
67
|
-
}
|
|
68
|
-
export function followUpResult(_jobId) {
|
|
69
|
-
return "The human's answer is delivered to your inbox when they respond — no need to poll.";
|
|
70
|
-
}
|
|
71
|
-
export function followUpDrain(_jobId) {
|
|
72
|
-
return ('Not in tmux: a human must drain it — run `crtr human inbox` (or re-run ' +
|
|
73
|
-
'inside tmux). The answer then arrives in your inbox.');
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Road sign for a spawned `human review`. It is a non-blocking kickoff, so the
|
|
77
|
-
* text steers the caller to stop rather than wait, verify, or re-present: the
|
|
78
|
-
* pane is already live and tracks the file, and the comments arrive via the
|
|
79
|
-
* inbox/wake when the human submits.
|
|
80
|
-
*/
|
|
81
|
-
export function followUpReview(_jobId) {
|
|
82
|
-
return ("The document is live on the human's screen for anchored, line-by-line " +
|
|
83
|
-
'review. The pane tracks the file — edit the .md in place and it re-renders ' +
|
|
84
|
-
'on save, so never cancel and re-present just to show a change. Do not poll, ' +
|
|
85
|
-
'verify it opened, or background this call; end your turn. The human reviews ' +
|
|
86
|
-
'on their own time, and their comments (with any line edits) arrive in your ' +
|
|
87
|
-
'inbox and wake you when they submit.');
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Spawn the detached `_run` pane that drives the humanloop TUI for this node.
|
|
91
|
-
* Returns whether the pane spawned and the follow_up road sign. Degrades to the
|
|
92
|
-
* inbox-drain follow_up when not in tmux / spawn fails — kickoffs are
|
|
93
|
-
* intentionally non-fatal off-tmux.
|
|
21
|
+
* The direct-exec completion handler humanloop runs when a crouter ticket
|
|
22
|
+
* resolves, fed the completion event on stdin. The argv is absolute and
|
|
23
|
+
* PATH/cwd-independent — never a `crtr` shell string — and, critically, must
|
|
24
|
+
* actually EXECUTE in both crouter contexts:
|
|
25
|
+
*
|
|
26
|
+
* - Built (`dist/cli.js` present): `node <abs>/dist/cli.js human _deliver`.
|
|
27
|
+
* - Dev/tsx (only `src/cli.ts` on disk): plain `node` cannot run the `.ts`
|
|
28
|
+
* entry — its mandatory `.js`-extension ESM imports have no `.js` sibling on
|
|
29
|
+
* disk, so Node's type-stripping fails at the first relative import. So we
|
|
30
|
+
* register tsx's ESM loader first (`node --import <abs tsx/esm> src/cli.ts …`),
|
|
31
|
+
* with the tsx entry resolved to an absolute file URL so the handler stays
|
|
32
|
+
* cwd-independent when humanloop invokes it. tsx is necessarily present here
|
|
33
|
+
* because crouter itself is running under it.
|
|
94
34
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* so the `pushFinal` the `_run` worker emits — for ask AND review —
|
|
98
|
-
* fans the answer straight into the asking node's inbox. The pane id is recorded
|
|
99
|
-
* on run.json (not returned) so `human cancel` can later kill the TUI.
|
|
35
|
+
* Throws loudly when neither entry exists rather than registering an argv that
|
|
36
|
+
* humanloop would retry forever.
|
|
100
37
|
*/
|
|
101
|
-
export function
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
38
|
+
export function crouterDeliverHandler() {
|
|
39
|
+
const here = dirname(fileURLToPath(import.meta.url)); // dist/commands/human | src/commands/human
|
|
40
|
+
const distEntry = join(here, '..', '..', 'cli.js');
|
|
41
|
+
if (existsSync(distEntry)) {
|
|
42
|
+
return { command: process.execPath, args: [distEntry, 'human', '_deliver'] };
|
|
105
43
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const rc = readJson(rcPath);
|
|
111
|
-
if (rc !== null)
|
|
112
|
-
atomicWriteJson(rcPath, { ...rc, pane_id: spawn.paneId });
|
|
44
|
+
const tsEntry = join(here, '..', '..', 'cli.ts');
|
|
45
|
+
if (existsSync(tsEntry)) {
|
|
46
|
+
const tsxLoader = pathToFileURL(createRequire(import.meta.url).resolve('tsx/esm')).href;
|
|
47
|
+
return { command: process.execPath, args: ['--import', tsxLoader, tsEntry, 'human', '_deliver'] };
|
|
113
48
|
}
|
|
114
|
-
|
|
49
|
+
throw new Error(`crouter completion handler: no runnable CLI entry near ${here} (neither cli.js nor cli.ts exists)`);
|
|
115
50
|
}
|
|
116
51
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* 1. paneId must be a real tmux pane id (`%<n>`) — never an empty/odd string.
|
|
123
|
-
* 2. The pane's start command must contain `verify` (the interaction dir, which
|
|
124
|
-
* humanloop bakes into the worker's `CRTR_HUMAN_DIR=... crtr human _run`
|
|
125
|
-
* launch). A shell (`zsh -l`) or the agent's pi never matches, so we can
|
|
126
|
-
* only ever kill the exact worker we spawned for this job.
|
|
127
|
-
*
|
|
128
|
-
* Returns true only when a matching pane was found and killed. Never throws.
|
|
52
|
+
* Register the per-cwd interaction root with humanloop as owner `crouter`,
|
|
53
|
+
* attaching the `_deliver` completion handler. Idempotent for owner `crouter`
|
|
54
|
+
* (refreshes the handler), and creates/canonicalizes the root. Returns the
|
|
55
|
+
* canonical root path humanloop submissions must be rooted at.
|
|
129
56
|
*/
|
|
130
|
-
export function
|
|
131
|
-
|
|
132
|
-
return false;
|
|
133
|
-
const probe = spawnSync('tmux', ['display-message', '-p', '-t', paneId, '#{pane_start_command}'], {
|
|
134
|
-
encoding: 'utf8',
|
|
135
|
-
});
|
|
136
|
-
// Pane is gone (status !== 0) → nothing to kill. Pane exists but its launch
|
|
137
|
-
// command doesn't carry our interaction dir → it is NOT our worker; refuse.
|
|
138
|
-
if (probe.status !== 0 || !probe.stdout.includes(verify))
|
|
139
|
-
return false;
|
|
140
|
-
const r = spawnSync('tmux', ['kill-pane', '-t', paneId], { encoding: 'utf8' });
|
|
141
|
-
return r.status === 0;
|
|
57
|
+
export function registerCrouterRoot(cwd) {
|
|
58
|
+
return registerInboxRoot({ root: interactionsRoot(cwd), owner: 'crouter', handler: crouterDeliverHandler() }).root;
|
|
142
59
|
}
|
package/dist/commands/human.js
CHANGED
|
@@ -1,31 +1,30 @@
|
|
|
1
|
-
// `crtr human` subtree —
|
|
1
|
+
// `crtr human` subtree — humanloop producer + completion bridge.
|
|
2
2
|
//
|
|
3
|
-
// Kickoff leaves (ask/review) create a kind:'human' node under
|
|
4
|
-
// asking node,
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// inbox
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// (set inline in the spawned command) + run.json, never stdin. The noninteractive
|
|
13
|
-
// resolve leaf is the only stdin-taking child and does not spawn a TUI pane.
|
|
3
|
+
// Kickoff leaves (ask/review) create a terminal kind:'human' bridge node under
|
|
4
|
+
// the asking node, register the per-cwd interaction root with humanloop (owner
|
|
5
|
+
// 'crouter', `_deliver` handler), submit the deck/review, and return at once —
|
|
6
|
+
// nothing opens on screen. When the human answers from humanloop's inbox popup
|
|
7
|
+
// (the inbox-toggle keybinding, or `hl inbox open`), humanloop invokes the
|
|
8
|
+
// registered `_deliver` handler, which pushFinals
|
|
9
|
+
// the answer as the bridge node's final report, fanning it into the asking
|
|
10
|
+
// node's inbox. notify enqueues a durable deck with no node; show is passive
|
|
11
|
+
// display. Humanloop owns the queue UI, claims, and atomic finalization.
|
|
14
12
|
import { defineBranch } from '../core/command.js';
|
|
15
13
|
import { humanAsk, humanReview, humanNotify, humanShow } from './human/prompts.js';
|
|
16
|
-
import {
|
|
14
|
+
import { humanList, humanDeck, humanResolve, humanCancel, humanDeliver } from './human/queue.js';
|
|
15
|
+
import { inboxOpenInstruction } from '../core/keybindings/index.js';
|
|
17
16
|
export function registerHuman() {
|
|
18
17
|
return defineBranch({
|
|
19
18
|
name: 'human',
|
|
20
19
|
rootEntry: {
|
|
21
20
|
concept: 'human-in-the-loop decisions, document review, and live display',
|
|
22
|
-
desc: 'ask, review, notify, show, cancel,
|
|
21
|
+
desc: 'ask, review, notify, show, cancel, list',
|
|
23
22
|
useWhen: 'you have a question for the user, want their feedback, or are presenting them with options or a choice — always reach for human instead of guessing or assuming, or laying a choice out as prose, when a person can decide; and whenever the user wants to review a spec, plan, or requirements document, this is the command for anchored line-by-line comments.'
|
|
24
23
|
},
|
|
25
24
|
help: {
|
|
26
25
|
name: 'human',
|
|
27
26
|
summary: 'human-in-the-loop decisions, document review, and live display',
|
|
28
|
-
model:
|
|
27
|
+
model: `Every body and displayed file is directive-flavored markdown rendered by termrender (panels, columns, trees, callouts, mermaid) — see \`termrender doc -h\` for the directive set before authoring one. ask and review are kickoffs: they create a kind:'human' bridge node under you and return instantly, never blocking. Nothing opens on screen — the ticket is queued in the humanloop inbox; the human opens it — ${inboxOpenInstruction()} — on their own time, and their response is pushed to your inbox when they answer, so keep working (or just end your turn) and you'll be woken with it. ask covers everything from a yes/no sign-off gate (two options) to an open-ended judgment call; review queues a .md for anchored comments and the inbox previews the live file, so edit in place rather than canceling and re-presenting. notify enqueues a durable acknowledgement with no node; show is passive live display.`,
|
|
29
28
|
},
|
|
30
29
|
children: [
|
|
31
30
|
humanAsk,
|
|
@@ -33,11 +32,10 @@ export function registerHuman() {
|
|
|
33
32
|
humanNotify,
|
|
34
33
|
humanShow,
|
|
35
34
|
humanCancel,
|
|
36
|
-
humanInbox,
|
|
37
35
|
humanList,
|
|
38
36
|
humanDeck,
|
|
39
37
|
humanResolve,
|
|
40
|
-
|
|
38
|
+
humanDeliver,
|
|
41
39
|
],
|
|
42
40
|
});
|
|
43
41
|
}
|
package/dist/commands/node.js
CHANGED
|
@@ -15,6 +15,7 @@ import { writeYieldMessage, readGoal } from '../core/runtime/kickoff.js';
|
|
|
15
15
|
import { reviveNode } from '../core/runtime/revive.js';
|
|
16
16
|
import { parseOutputSchemaValue, writeOutputSchema } from '../core/runtime/structured-output.js';
|
|
17
17
|
import { newNodeId } from '../core/runtime/nodes.js';
|
|
18
|
+
import { inboxPopupHint } from '../core/keybindings/index.js';
|
|
18
19
|
import { readRoadmap, hasRoadmap, seedRoadmap } from '../core/runtime/roadmap.js';
|
|
19
20
|
import { parseWhen, parseCadence, cadenceDisplay } from '../core/wake.js';
|
|
20
21
|
import { nodeSnapshotLeaf } from './node-snapshot.js';
|
|
@@ -617,6 +618,7 @@ const nodeFocus = defineLeaf({
|
|
|
617
618
|
params: [
|
|
618
619
|
{ kind: 'positional', name: 'node', required: true, constraint: 'Node id to focus.' },
|
|
619
620
|
{ kind: 'flag', name: 'new-pane', type: 'bool', required: false, constraint: 'Always open a NEW viewer SIDE-BY-SIDE with your current pane instead of moving/reusing the node\'s existing viewer. Two agents on screen at once (F4).' },
|
|
621
|
+
{ kind: 'flag', name: 'in-place', type: 'bool', required: false, constraint: 'Re-exec the --pane pane ONTO this node, REPLACING whatever viewer it showed (a swap in place, not a split beside). Backs the attach viewer\'s subagent scroll+swap (↓ into the reports roster, Enter dives into the child right in your pane). MUST be driven from OUTSIDE that pane\'s process tree (e.g. `tmux run-shell`) — respawn-pane -k kills whatever runs in it. Mutually exclusive with --new-pane.' },
|
|
620
622
|
{ kind: 'flag', name: 'pane', type: 'string', required: false, constraint: 'tmux pane id to focus beside (default: caller TMUX_PANE). Used by the canvas browser popup to focus back into the originating pane.' },
|
|
621
623
|
],
|
|
622
624
|
output: [
|
|
@@ -626,7 +628,7 @@ const nodeFocus = defineLeaf({
|
|
|
626
628
|
{ name: 'in_place', type: 'boolean', required: true, constraint: 'True when an existing viewer was navigated to (no new pane); false when a fresh viewer pane was opened or moved beside you.' },
|
|
627
629
|
],
|
|
628
630
|
outputKind: 'object',
|
|
629
|
-
effects: ['Ensures the node\'s broker engine is alive (revives it if dormant) and opens/moves its one viewer pane (a surface attach view) beside the caller, or navigates to it if a live viewer already sits in the caller\'s session.', 'With --new-pane: always splits a fresh viewer beside the caller (two agents on screen) instead of reusing the existing viewer.'],
|
|
631
|
+
effects: ['Ensures the node\'s broker engine is alive (revives it if dormant) and opens/moves its one viewer pane (a surface attach view) beside the caller, or navigates to it if a live viewer already sits in the caller\'s session.', 'With --new-pane: always splits a fresh viewer beside the caller (two agents on screen) instead of reusing the existing viewer.', 'With --in-place: re-execs the --pane pane onto the node, replacing its current viewer (a swap in place) rather than opening beside; the replaced node\'s broker keeps running headless.'],
|
|
630
632
|
},
|
|
631
633
|
run: async (input) => {
|
|
632
634
|
const id = input['node'];
|
|
@@ -641,7 +643,7 @@ const nodeFocus = defineLeaf({
|
|
|
641
643
|
throw new InputError({
|
|
642
644
|
error: 'not_focusable',
|
|
643
645
|
message: `node ${id} is a human-ask (kind:human), not a conversation — it has no pi session to focus.`,
|
|
644
|
-
next: `The pending question is
|
|
646
|
+
next: `The pending question is queued for the human (${inboxPopupHint()} opens the inbox popup); see it with \`crtr human list\`, or retract it with \`crtr human cancel ${id}\`.`,
|
|
645
647
|
});
|
|
646
648
|
}
|
|
647
649
|
// Placement owns the whole act: ensure the target's broker engine is alive
|
|
@@ -651,6 +653,7 @@ const nodeFocus = defineLeaf({
|
|
|
651
653
|
const res = await placementFocus(id, {
|
|
652
654
|
pane: input['pane'],
|
|
653
655
|
newPane: input['newPane'] === true,
|
|
656
|
+
inPlace: input['inPlace'] === true,
|
|
654
657
|
revive: (nid) => reviveNode(nid, { resume: true }),
|
|
655
658
|
});
|
|
656
659
|
return { focused: res.focused, session: res.session, revived: res.revived, in_place: res.inPlace };
|
package/dist/commands/surface.js
CHANGED
|
@@ -7,7 +7,7 @@ import { defineBranch } from '../core/command.js';
|
|
|
7
7
|
import { registerView } from './view.js';
|
|
8
8
|
import { registerWorkspace } from './workspace.js';
|
|
9
9
|
import { registerWeb } from '../clients/web/web-cmd.js';
|
|
10
|
-
import { registerAttach } from '../clients/attach/attach-
|
|
10
|
+
import { registerAttach } from '../clients/attach/attach-def.js';
|
|
11
11
|
import { tmuxSpreadLeaf } from './surface-tmux-spread.js';
|
|
12
12
|
function namespaceForSurface(node) {
|
|
13
13
|
node.help.name = `surface ${node.help.name}`;
|
|
@@ -55,7 +55,7 @@ test('setup keybinding draft keeps the last valid cell value on syntax, collisio
|
|
|
55
55
|
assert.deepEqual(draft.overrides(), {});
|
|
56
56
|
const collision = draft.edit('crtr.attach.graph.toggle', 'alt+m');
|
|
57
57
|
assert.equal(collision.ok, false);
|
|
58
|
-
assert.match(collision.diagnostics[0].message, /crtr\.
|
|
58
|
+
assert.match(collision.diagnostics[0].message, /crtr\.attach\.model-ladder\.next/);
|
|
59
59
|
assert.deepEqual(draft.overrides(), {});
|
|
60
60
|
const unsafe = draft.unbind('crtr.setup.keybindings.save');
|
|
61
61
|
assert.equal(unsafe.ok, false);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/commands/sys/__tests__/setup-front-door.test.ts
|
|
2
|
+
//
|
|
3
|
+
// M1 — `crtr sys setup` front-door reconciliation. Setup registers known nodes'
|
|
4
|
+
// interaction roots to owner 'crouter' so a resolved humanloop ticket can call
|
|
5
|
+
// back into the asking node (best-effort). The inbox-toggle tmux key is a
|
|
6
|
+
// crouter keybinding installed by `installTmuxBindings()`, not reconciled here.
|
|
7
|
+
// The humanloop registry is isolated with a temp XDG_STATE_HOME (see
|
|
8
|
+
// human-deliver.test.ts); the canvas is isolated with a temp CRTR_HOME + HOME.
|
|
9
|
+
import { test, before, beforeEach, after } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { mkdtempSync, realpathSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { listInboxRoots } from '@crouton-kit/humanloop';
|
|
15
|
+
import { spawnNode } from '../../../core/runtime/nodes.js';
|
|
16
|
+
import { closeDb } from '../../../core/canvas/index.js';
|
|
17
|
+
import { reconcileInboxFrontDoor } from '../setup-core.js';
|
|
18
|
+
import { sysSetupLeaf } from '../setup.js';
|
|
19
|
+
let home;
|
|
20
|
+
let stateHome;
|
|
21
|
+
let priorHome;
|
|
22
|
+
before(() => {
|
|
23
|
+
priorHome = process.env['HOME'];
|
|
24
|
+
});
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
closeDb();
|
|
27
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-setup-home-'));
|
|
28
|
+
stateHome = mkdtempSync(join(tmpdir(), 'crtr-setup-state-'));
|
|
29
|
+
process.env['CRTR_HOME'] = home;
|
|
30
|
+
process.env['HOME'] = home;
|
|
31
|
+
process.env['XDG_STATE_HOME'] = stateHome;
|
|
32
|
+
delete process.env['CRTR_NODE_ID'];
|
|
33
|
+
});
|
|
34
|
+
after(() => {
|
|
35
|
+
closeDb();
|
|
36
|
+
if (priorHome !== undefined)
|
|
37
|
+
process.env['HOME'] = priorHome;
|
|
38
|
+
else
|
|
39
|
+
delete process.env['HOME'];
|
|
40
|
+
delete process.env['CRTR_HOME'];
|
|
41
|
+
delete process.env['XDG_STATE_HOME'];
|
|
42
|
+
});
|
|
43
|
+
test('reconcile registers each known node cwd as an owner-crouter interaction root', () => {
|
|
44
|
+
// Two nodes sharing one cwd + one on another cwd → two distinct roots.
|
|
45
|
+
const otherCwd = mkdtempSync(join(tmpdir(), 'crtr-setup-cwd-'));
|
|
46
|
+
spawnNode({ kind: 'general', parent: null, cwd: home, name: 'a' });
|
|
47
|
+
spawnNode({ kind: 'general', parent: null, cwd: home, name: 'b' });
|
|
48
|
+
spawnNode({ kind: 'general', parent: null, cwd: otherCwd, name: 'c' });
|
|
49
|
+
const { interactionRootsRegistered } = reconcileInboxFrontDoor();
|
|
50
|
+
// Deduped by cwd: two cwds → two registrations.
|
|
51
|
+
assert.equal(interactionRootsRegistered, 2, 'one registration per distinct node cwd');
|
|
52
|
+
const crouterRoots = listInboxRoots().filter((r) => r.owner === 'crouter');
|
|
53
|
+
assert.ok(crouterRoots.length >= 2, 'both interaction roots landed in the humanloop registry as owner crouter');
|
|
54
|
+
});
|
|
55
|
+
test('invoking `crtr sys setup` (non-TTY leaf run) reconciles the front door', async () => {
|
|
56
|
+
// Exercise the real control flow — sysSetupLeaf.run() → runSetup() →
|
|
57
|
+
// reconcileInboxFrontDoor — not the helper directly. With no TTY the leaf
|
|
58
|
+
// prints static instructions and returns, but reconciliation must still have
|
|
59
|
+
// happened on the way through.
|
|
60
|
+
spawnNode({ kind: 'general', parent: null, cwd: home, name: 'a' });
|
|
61
|
+
const originalIsTTY = process.stdout.isTTY;
|
|
62
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
63
|
+
// Force the non-TTY (instructions-print) branch and swallow its output.
|
|
64
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: false, configurable: true });
|
|
65
|
+
process.stdout.write = () => true;
|
|
66
|
+
try {
|
|
67
|
+
await sysSetupLeaf.run({});
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
process.stdout.write = originalWrite;
|
|
71
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true });
|
|
72
|
+
}
|
|
73
|
+
const homeReal = realpathSync(home);
|
|
74
|
+
const crouterRoots = listInboxRoots().filter((r) => r.owner === 'crouter');
|
|
75
|
+
// registerCrouterRoot maps the node cwd to its workspace interactions dir
|
|
76
|
+
// under that cwd, so the registered root is rooted at the node's home.
|
|
77
|
+
assert.ok(crouterRoots.some((r) => r.root.startsWith(homeReal)), 'the leaf invocation registered the node cwd as an owner-crouter interaction root');
|
|
78
|
+
});
|
|
79
|
+
test('reconcile is idempotent for owner crouter — re-running does not error and keeps the roots', () => {
|
|
80
|
+
spawnNode({ kind: 'general', parent: null, cwd: home, name: 'a' });
|
|
81
|
+
reconcileInboxFrontDoor();
|
|
82
|
+
const { interactionRootsRegistered } = reconcileInboxFrontDoor();
|
|
83
|
+
assert.equal(interactionRootsRegistered, 1);
|
|
84
|
+
assert.ok(listInboxRoots().some((r) => r.owner === 'crouter'));
|
|
85
|
+
});
|
|
@@ -51,6 +51,7 @@ export interface SetupExecutionSummary {
|
|
|
51
51
|
exaKeyPath: string;
|
|
52
52
|
composioKeyWritten: boolean;
|
|
53
53
|
composioKeyPath: string;
|
|
54
|
+
interactionRootsRegistered: number;
|
|
54
55
|
}
|
|
55
56
|
export interface SetupInitialState {
|
|
56
57
|
manifest: SetupPackageManifestEntry[];
|
|
@@ -122,6 +123,14 @@ export declare function buildSetupStaticInstructions(opts: {
|
|
|
122
123
|
export declare function renderSetupStaticInstructions(state: Pick<SetupInitialState, 'manifest' | 'installedPackageIds' | 'systemTools' | 'exaKeyPath' | 'composioKeyPath'>): string;
|
|
123
124
|
export declare function writeExaKey(exaKeyPath: string, exaKey: string): Promise<void>;
|
|
124
125
|
export declare function writeComposioKey(composioKeyPath: string, composioKey: string): Promise<void>;
|
|
126
|
+
/** Register each known node's interaction root to owner `crouter` so a resolved
|
|
127
|
+
* ticket can call back into `human _deliver`. Best-effort: setup must never
|
|
128
|
+
* fail because the canvas is unreadable. The inbox-toggle tmux key is NOT
|
|
129
|
+
* reconciled here — it is a crouter keybinding installed by `installTmuxBindings()`
|
|
130
|
+
* alongside every other binding. */
|
|
131
|
+
export declare function reconcileInboxFrontDoor(): {
|
|
132
|
+
interactionRootsRegistered: number;
|
|
133
|
+
};
|
|
125
134
|
export declare function summarizeSetupExecution(result: {
|
|
126
135
|
installedPackages: string[];
|
|
127
136
|
skippedPackages: string[];
|
|
@@ -131,6 +140,7 @@ export declare function summarizeSetupExecution(result: {
|
|
|
131
140
|
exaKeyPath: string;
|
|
132
141
|
composioKeyWritten: boolean;
|
|
133
142
|
composioKeyPath: string;
|
|
143
|
+
interactionRootsRegistered: number;
|
|
134
144
|
}): SetupExecutionSummary;
|
|
135
145
|
export declare function formatSetupExecutionSummary(summary: SetupExecutionSummary): string;
|
|
136
146
|
export declare function executeSetupSubmission(submission: SetupSubmission): Promise<SetupExecutionSummary>;
|
|
@@ -4,10 +4,12 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { isDeepStrictEqual } from 'node:util';
|
|
7
|
-
import { BINDING_CATALOG, normalizeGesture, resolveKeybindings, } from '../../core/keybindings/index.js';
|
|
7
|
+
import { BINDING_CATALOG, normalizeGesture, inboxShortcut, resolveKeybindings, } from '../../core/keybindings/index.js';
|
|
8
8
|
import { builtinPiPackageDir } from '../../core/scope.js';
|
|
9
9
|
import { readJsonIfExists } from '../../core/fs-utils.js';
|
|
10
10
|
import { resolveBundledPiCliPath } from '../../core/runtime/pi-cli.js';
|
|
11
|
+
import { listNodes } from '../../core/canvas/index.js';
|
|
12
|
+
import { registerCrouterRoot } from '../human/shared.js';
|
|
11
13
|
function isKeybindingsObject(value) {
|
|
12
14
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
13
15
|
}
|
|
@@ -422,6 +424,8 @@ export function renderSetupStaticInstructions(state) {
|
|
|
422
424
|
'',
|
|
423
425
|
instructions.exaLine,
|
|
424
426
|
instructions.composioLine,
|
|
427
|
+
'',
|
|
428
|
+
`Inbox popup: the inbox-toggle key (${inboxShortcut() ?? 'disabled'}) is a crouter keybinding — view or rebind it in the \`crtr sys setup\` keybindings tab; it is installed on the same tmux server as every other crouter binding. \`crtr sys setup\` also registers known nodes' interaction roots (best-effort).`,
|
|
425
429
|
instructions.footer,
|
|
426
430
|
].join('\n') + '\n';
|
|
427
431
|
}
|
|
@@ -447,6 +451,33 @@ export async function writeComposioKey(composioKeyPath, composioKey) {
|
|
|
447
451
|
await writeFile(composioKeyPath, `${composioKey}\n`, 'utf8');
|
|
448
452
|
await chmod(composioKeyPath, 0o600).catch(() => undefined);
|
|
449
453
|
}
|
|
454
|
+
/** Register each known node's interaction root to owner `crouter` so a resolved
|
|
455
|
+
* ticket can call back into `human _deliver`. Best-effort: setup must never
|
|
456
|
+
* fail because the canvas is unreadable. The inbox-toggle tmux key is NOT
|
|
457
|
+
* reconciled here — it is a crouter keybinding installed by `installTmuxBindings()`
|
|
458
|
+
* alongside every other binding. */
|
|
459
|
+
export function reconcileInboxFrontDoor() {
|
|
460
|
+
let interactionRootsRegistered = 0;
|
|
461
|
+
try {
|
|
462
|
+
const seen = new Set();
|
|
463
|
+
for (const node of listNodes()) {
|
|
464
|
+
if (seen.has(node.cwd))
|
|
465
|
+
continue;
|
|
466
|
+
seen.add(node.cwd);
|
|
467
|
+
try {
|
|
468
|
+
registerCrouterRoot(node.cwd);
|
|
469
|
+
interactionRootsRegistered += 1;
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
/* skip a cwd humanloop can't canonicalize/create */
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
/* best-effort: canvas unavailable */
|
|
478
|
+
}
|
|
479
|
+
return { interactionRootsRegistered };
|
|
480
|
+
}
|
|
450
481
|
export function summarizeSetupExecution(result) {
|
|
451
482
|
return {
|
|
452
483
|
installed: [...result.installedPackages, ...result.installedSystemDeps],
|
|
@@ -455,6 +486,7 @@ export function summarizeSetupExecution(result) {
|
|
|
455
486
|
exaKeyPath: result.exaKeyPath,
|
|
456
487
|
composioKeyWritten: result.composioKeyWritten,
|
|
457
488
|
composioKeyPath: result.composioKeyPath,
|
|
489
|
+
interactionRootsRegistered: result.interactionRootsRegistered,
|
|
458
490
|
};
|
|
459
491
|
}
|
|
460
492
|
export function formatSetupExecutionSummary(summary) {
|
|
@@ -464,6 +496,7 @@ export function formatSetupExecutionSummary(summary) {
|
|
|
464
496
|
summary.skipped.length > 0 ? `skipped: ${summary.skipped.join(', ')}` : 'skipped: none',
|
|
465
497
|
summary.exaKeyWritten ? `exa key written: ${summary.exaKeyPath}` : 'exa key written: no',
|
|
466
498
|
summary.composioKeyWritten ? `composio key written: ${summary.composioKeyPath}` : 'composio key written: no',
|
|
499
|
+
`interaction roots registered: ${summary.interactionRootsRegistered}`,
|
|
467
500
|
].join('\n') + '\n';
|
|
468
501
|
}
|
|
469
502
|
export async function executeSetupSubmission(submission) {
|
|
@@ -499,6 +532,10 @@ export async function executeSetupSubmission(submission) {
|
|
|
499
532
|
if (composioKeyWritten) {
|
|
500
533
|
await writeComposioKey(submission.composioKeyPath, submission.composioKey);
|
|
501
534
|
}
|
|
535
|
+
// Front-door reconciliation: register known nodes' interaction roots for
|
|
536
|
+
// `human _deliver` callback. Best-effort. The inbox-toggle key is a crouter
|
|
537
|
+
// keybinding installed by `installTmuxBindings()`, not reconciled here.
|
|
538
|
+
const { interactionRootsRegistered } = reconcileInboxFrontDoor();
|
|
502
539
|
return summarizeSetupExecution({
|
|
503
540
|
installedPackages,
|
|
504
541
|
skippedPackages,
|
|
@@ -508,5 +545,6 @@ export async function executeSetupSubmission(submission) {
|
|
|
508
545
|
exaKeyPath: submission.exaKeyPath,
|
|
509
546
|
composioKeyWritten,
|
|
510
547
|
composioKeyPath: submission.composioKeyPath,
|
|
548
|
+
interactionRootsRegistered,
|
|
511
549
|
});
|
|
512
550
|
}
|