@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.
- 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 +7 -3
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +104 -0
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +16 -44
- package/dist/clients/attach/attach-cmd.d.ts +1 -2
- package/dist/clients/attach/attach-cmd.js +685 -685
- 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 +51 -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 +13 -17
- package/dist/commands/node.js +4 -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 +98 -0
- package/dist/commands/sys/setup-core.d.ts +22 -0
- package/dist/commands/sys/setup-core.js +62 -0
- package/dist/commands/sys/setup.js +8 -1
- 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 +83 -0
- 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 +1 -0
- package/dist/core/canvas/boot.js +1 -1
- package/dist/core/canvas/browse/app.js +3 -3
- package/dist/core/canvas/pid.d.ts +43 -17
- package/dist/core/canvas/pid.js +104 -35
- 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__/resolve.test.js +1 -1
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +3 -2
- package/dist/core/preview-registry.js +22 -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 +9 -0
- package/dist/core/spawn.d.ts +0 -59
- package/dist/core/spawn.js +5 -163
- package/dist/daemon/crtrd.js +7 -6
- 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,145 +0,0 @@
|
|
|
1
|
-
// Run with: node --import tsx/esm --test src/core/__tests__/human-stranded-deliver.test.ts
|
|
2
|
-
//
|
|
3
|
-
// BUG REGRESSION (human-ask broker round-trip): a headless-broker asker strips
|
|
4
|
-
// $TMUX, so the detached `crtr human _run` worker — whose `pushFinal` is the SOLE
|
|
5
|
-
// deliver-back + reap step — never spawned. Worse, the `crtr human inbox` drain
|
|
6
|
-
// path writes response.json directly (humanloop's inbox()) and ALSO never calls
|
|
7
|
-
// pushFinal, so an answered deck strands on disk: the asker never learns the
|
|
8
|
-
// answer and the bridge node leaks active forever.
|
|
9
|
-
//
|
|
10
|
-
// This locks the SECONDARY heal: finalizeResolvedInteraction must, for a still-
|
|
11
|
-
// LIVE bridge node whose interaction is resolved on disk, deliver the answer to
|
|
12
|
-
// the asker (parent auto-subscribed) and reap the bridge node — exactly what the
|
|
13
|
-
// _run worker's pushFinal would have done. Idempotent (no double-deliver once
|
|
14
|
-
// done); a canceled-on-disk response reaps without delivering a result.
|
|
15
|
-
import { test, before, after, beforeEach } from 'node:test';
|
|
16
|
-
import assert from 'node:assert/strict';
|
|
17
|
-
import { mkdtempSync, rmSync, mkdirSync } from 'node:fs';
|
|
18
|
-
import { tmpdir } from 'node:os';
|
|
19
|
-
import { join } from 'node:path';
|
|
20
|
-
import { createNode, subscribe, getNode } from '../canvas/canvas.js';
|
|
21
|
-
import { closeDb } from '../canvas/db.js';
|
|
22
|
-
import { readInboxSince } from '../feed/inbox.js';
|
|
23
|
-
import { interactionDir, interactionsRoot } from '../artifact.js';
|
|
24
|
-
import { atomicWriteJson, deckPath } from '@crouton-kit/humanloop';
|
|
25
|
-
import { finalizeResolvedInteraction, acquireInteractionClaim } from '../../commands/human/queue.js';
|
|
26
|
-
let home;
|
|
27
|
-
// A unique cwd so interactionDir (keyed off the REAL homedir + mangled cwd, NOT
|
|
28
|
-
// CRTR_HOME) lands in an isolated, cleanable subtree.
|
|
29
|
-
let workCwd;
|
|
30
|
-
function node(id, parent) {
|
|
31
|
-
return {
|
|
32
|
-
node_id: id,
|
|
33
|
-
name: id,
|
|
34
|
-
created: new Date().toISOString(),
|
|
35
|
-
cwd: workCwd,
|
|
36
|
-
kind: 'developer',
|
|
37
|
-
mode: 'base',
|
|
38
|
-
lifecycle: 'terminal',
|
|
39
|
-
status: 'active',
|
|
40
|
-
parent,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
/** Seed an interaction dir on disk the way the kickoff + a resolution would. */
|
|
44
|
-
function seed(jobId, run, response) {
|
|
45
|
-
const idir = interactionDir(jobId, workCwd);
|
|
46
|
-
mkdirSync(idir, { recursive: true });
|
|
47
|
-
atomicWriteJson(deckPath(idir), { interactions: [] });
|
|
48
|
-
atomicWriteJson(join(idir, 'run.json'), run);
|
|
49
|
-
atomicWriteJson(join(idir, 'response.json'), response);
|
|
50
|
-
}
|
|
51
|
-
before(() => {
|
|
52
|
-
home = mkdtempSync(join(tmpdir(), 'crtr-stranded-'));
|
|
53
|
-
process.env['CRTR_HOME'] = home;
|
|
54
|
-
workCwd = mkdtempSync(join(tmpdir(), 'crtr-stranded-cwd-'));
|
|
55
|
-
});
|
|
56
|
-
beforeEach(() => {
|
|
57
|
-
closeDb();
|
|
58
|
-
rmSync(home, { recursive: true, force: true });
|
|
59
|
-
rmSync(interactionsRoot(workCwd), { recursive: true, force: true });
|
|
60
|
-
});
|
|
61
|
-
after(() => {
|
|
62
|
-
closeDb();
|
|
63
|
-
rmSync(home, { recursive: true, force: true });
|
|
64
|
-
rmSync(interactionsRoot(workCwd), { recursive: true, force: true });
|
|
65
|
-
rmSync(workCwd, { recursive: true, force: true });
|
|
66
|
-
delete process.env['CRTR_HOME'];
|
|
67
|
-
});
|
|
68
|
-
test('answered-but-undelivered ask delivers back to the asker and reaps the bridge', async () => {
|
|
69
|
-
// Asker A, bridge B under A (parent auto-subscribes to its child bridge).
|
|
70
|
-
createNode(node('A', null));
|
|
71
|
-
createNode(node('B', 'A'));
|
|
72
|
-
subscribe('A', 'B');
|
|
73
|
-
seed('B', { mode: 'ask', job_id: 'B' }, { responses: [{ id: 'q', selectedOptionId: 'resync', freetext: 'go ahead' }], completedAt: '2026-06-10T22:27:07.000Z' });
|
|
74
|
-
const acted = await finalizeResolvedInteraction('B');
|
|
75
|
-
assert.equal(acted, true, 'should have delivered + reaped');
|
|
76
|
-
// Bridge reaped.
|
|
77
|
-
assert.equal(getNode('B')?.status, 'done', 'bridge node must be reaped to done');
|
|
78
|
-
// Answer fanned into the asker's inbox, carrying the choice.
|
|
79
|
-
const inbox = readInboxSince('A');
|
|
80
|
-
assert.equal(inbox.length >= 1, true, 'asker inbox must receive the answer pointer');
|
|
81
|
-
const blob = JSON.stringify(inbox);
|
|
82
|
-
assert.match(blob, /resync/, 'delivered answer should carry the human selection');
|
|
83
|
-
// Idempotent: a second sweep is a no-op (node already done — no double-deliver).
|
|
84
|
-
const again = await finalizeResolvedInteraction('B');
|
|
85
|
-
assert.equal(again, false, 'must not re-deliver an already-finalized interaction');
|
|
86
|
-
});
|
|
87
|
-
test('a canceled-on-disk response reaps the bridge without delivering a result', async () => {
|
|
88
|
-
createNode(node('A', null));
|
|
89
|
-
createNode(node('B', 'A'));
|
|
90
|
-
subscribe('A', 'B');
|
|
91
|
-
seed('B', { mode: 'ask', job_id: 'B' }, { canceled: true, canceledAt: '2026-06-10T22:42:04.000Z', reason: 'stale' });
|
|
92
|
-
const acted = await finalizeResolvedInteraction('B');
|
|
93
|
-
assert.equal(acted, true);
|
|
94
|
-
assert.equal(getNode('B')?.status, 'done', 'a canceled stranded bridge must still be reaped');
|
|
95
|
-
// The asker gets a quiet "no answer is coming" note, not a result.
|
|
96
|
-
const blob = JSON.stringify(readInboxSince('A'));
|
|
97
|
-
assert.match(blob, /no answer is coming/);
|
|
98
|
-
});
|
|
99
|
-
test('the _run held-claim path delivers, and finalize without the claim self-conflicts', async () => {
|
|
100
|
-
// BUG REGRESSION (_run ask self-claim): the detached `_run` ask worker holds
|
|
101
|
-
// the exclusive resolve.lock across the blocking ask(), then must deliver the
|
|
102
|
-
// answer. If it calls finalizeResolvedInteraction WITHOUT passing its held
|
|
103
|
-
// claim, finalize re-acquires the same lock, sees this very process's live
|
|
104
|
-
// lock (EEXIST, owner.pid == self, pidAlive true) -> 'claimed' -> returns
|
|
105
|
-
// false, and the human's answer never reaches the asker. Passing the held
|
|
106
|
-
// claim lets finalize run under the worker's own lock.
|
|
107
|
-
createNode(node('A', null));
|
|
108
|
-
createNode(node('B', 'A'));
|
|
109
|
-
subscribe('A', 'B');
|
|
110
|
-
// Set up the deck + run.json but NOT the answer yet, then hold the claim
|
|
111
|
-
// exactly as the _run worker does (no opts) BEFORE ask() resolves the deck.
|
|
112
|
-
const idir = interactionDir('B', workCwd);
|
|
113
|
-
mkdirSync(idir, { recursive: true });
|
|
114
|
-
atomicWriteJson(deckPath(idir), { interactions: [] });
|
|
115
|
-
atomicWriteJson(join(idir, 'run.json'), { mode: 'ask', job_id: 'B' });
|
|
116
|
-
const claim = acquireInteractionClaim(idir, 'B');
|
|
117
|
-
assert.equal(typeof claim === 'object', true, 'claim must be acquired');
|
|
118
|
-
if (typeof claim !== 'object')
|
|
119
|
-
return;
|
|
120
|
-
// ask() then writes the human's answer to disk while the claim is held.
|
|
121
|
-
atomicWriteJson(join(idir, 'response.json'), {
|
|
122
|
-
responses: [{ id: 'q', selectedOptionId: 'resync', freetext: 'go ahead' }],
|
|
123
|
-
completedAt: '2026-06-10T22:27:07.000Z',
|
|
124
|
-
});
|
|
125
|
-
// Without the held claim, finalize from the SAME pid self-conflicts on the
|
|
126
|
-
// live lock and must NOT deliver (this is the stranding regression).
|
|
127
|
-
assert.equal(await finalizeResolvedInteraction('B'), false, 'self-held lock must block claimless finalize');
|
|
128
|
-
assert.equal(getNode('B')?.status, 'active', 'bridge must still be live after the self-conflicting call');
|
|
129
|
-
// Passing the held claim lets finalize run under the worker's own lock and
|
|
130
|
-
// deliver the answer back to the asker, then reap the bridge.
|
|
131
|
-
assert.equal(await finalizeResolvedInteraction('B', claim), true, 'held claim must deliver');
|
|
132
|
-
assert.equal(getNode('B')?.status, 'done', 'bridge reaped after held-claim deliver');
|
|
133
|
-
assert.match(JSON.stringify(readInboxSince('A')), /resync/, 'asker must receive the human selection');
|
|
134
|
-
claim.release();
|
|
135
|
-
});
|
|
136
|
-
test('an unresolved (no response.json) live bridge is left untouched', async () => {
|
|
137
|
-
createNode(node('A', null));
|
|
138
|
-
createNode(node('B', 'A'));
|
|
139
|
-
subscribe('A', 'B');
|
|
140
|
-
const idir = interactionDir('B', workCwd);
|
|
141
|
-
mkdirSync(idir, { recursive: true });
|
|
142
|
-
atomicWriteJson(join(idir, 'run.json'), { mode: 'ask', job_id: 'B' });
|
|
143
|
-
assert.equal(await finalizeResolvedInteraction('B'), false);
|
|
144
|
-
assert.equal(getNode('B')?.status, 'active', 'still-pending bridge must stay live');
|
|
145
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
// Run with: node --import tsx/esm --test src/core/__tests__/human-surface-target.test.ts
|
|
2
|
-
//
|
|
3
|
-
// BUG REGRESSION: `crtr human ask|review|notify` surfaced its humanloop
|
|
4
|
-
// TUI in the backstage `crtr` session (the asking node's own pane) — a session
|
|
5
|
-
// the user never watches — because spawnAndDetach was called with no `-t`
|
|
6
|
-
// target. The fix routes the TUI to the HIGHEST FOCUSED node of the asking
|
|
7
|
-
// node's graph (the viewport the user is actually watching the work in).
|
|
8
|
-
//
|
|
9
|
-
// This locks in the PURE selection (`graphSurfaceTarget`, db-only, no tmux):
|
|
10
|
-
// walk the asking node's spine to its root, enumerate the tree root-first, and
|
|
11
|
-
// return the focus row of the node closest to the root that occupies a viewport.
|
|
12
|
-
import { test, before, after, beforeEach } from 'node:test';
|
|
13
|
-
import assert from 'node:assert/strict';
|
|
14
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
15
|
-
import { tmpdir } from 'node:os';
|
|
16
|
-
import { join } from 'node:path';
|
|
17
|
-
import { createNode, subscribe } from '../canvas/canvas.js';
|
|
18
|
-
import { openFocusRow, closeFocusRow, getFocusByNode } from '../canvas/focuses.js';
|
|
19
|
-
import { closeDb } from '../canvas/db.js';
|
|
20
|
-
import { graphSurfaceTarget } from '../runtime/placement.js';
|
|
21
|
-
let home;
|
|
22
|
-
let savedTmux;
|
|
23
|
-
function node(id, parent) {
|
|
24
|
-
return {
|
|
25
|
-
node_id: id,
|
|
26
|
-
name: id,
|
|
27
|
-
created: new Date().toISOString(),
|
|
28
|
-
cwd: '/tmp/work',
|
|
29
|
-
kind: 'developer',
|
|
30
|
-
mode: 'base',
|
|
31
|
-
lifecycle: 'terminal',
|
|
32
|
-
status: 'active',
|
|
33
|
-
parent,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
before(() => {
|
|
37
|
-
home = mkdtempSync(join(tmpdir(), 'crtr-human-surface-'));
|
|
38
|
-
process.env['CRTR_HOME'] = home;
|
|
39
|
-
savedTmux = process.env['TMUX'];
|
|
40
|
-
delete process.env['TMUX']; // PURE: graphSurfaceTarget never touches tmux
|
|
41
|
-
});
|
|
42
|
-
after(() => {
|
|
43
|
-
closeDb();
|
|
44
|
-
if (savedTmux !== undefined)
|
|
45
|
-
process.env['TMUX'] = savedTmux;
|
|
46
|
-
rmSync(home, { recursive: true, force: true });
|
|
47
|
-
});
|
|
48
|
-
// Graph R → M → W (parent edges + the auto-subscribe spine the runtime builds:
|
|
49
|
-
// a parent subscribes_to its child, so view(R) walks down to M then W).
|
|
50
|
-
beforeEach(() => {
|
|
51
|
-
closeDb();
|
|
52
|
-
rmSync(home, { recursive: true, force: true });
|
|
53
|
-
home = mkdtempSync(join(tmpdir(), 'crtr-human-surface-'));
|
|
54
|
-
process.env['CRTR_HOME'] = home;
|
|
55
|
-
createNode(node('R', null));
|
|
56
|
-
createNode(node('M', 'R'));
|
|
57
|
-
createNode(node('W', 'M'));
|
|
58
|
-
subscribe('R', 'M');
|
|
59
|
-
subscribe('M', 'W');
|
|
60
|
-
});
|
|
61
|
-
test('highest focused = the root when only the root is on screen', () => {
|
|
62
|
-
openFocusRow('f-r', '%10', 'work', 'R');
|
|
63
|
-
const t = graphSurfaceTarget('W');
|
|
64
|
-
assert.equal(t?.node_id, 'R');
|
|
65
|
-
assert.equal(t?.pane, '%10');
|
|
66
|
-
});
|
|
67
|
-
test('falls to the focused mid-orchestrator when the root is NOT on screen', () => {
|
|
68
|
-
openFocusRow('f-m', '%20', 'work', 'M');
|
|
69
|
-
assert.equal(graphSurfaceTarget('W')?.node_id, 'M');
|
|
70
|
-
});
|
|
71
|
-
test('picks the SHALLOWEST focused node when several are on screen', () => {
|
|
72
|
-
openFocusRow('f-m', '%20', 'work', 'M');
|
|
73
|
-
openFocusRow('f-w', '%30', 'work', 'W');
|
|
74
|
-
// M is closer to the root than W → M wins.
|
|
75
|
-
assert.equal(graphSurfaceTarget('W')?.node_id, 'M');
|
|
76
|
-
openFocusRow('f-r', '%10', 'work', 'R');
|
|
77
|
-
// Root trumps everything.
|
|
78
|
-
assert.equal(graphSurfaceTarget('W')?.node_id, 'R');
|
|
79
|
-
});
|
|
80
|
-
test('null when nothing in the graph is on screen (caller falls back)', () => {
|
|
81
|
-
assert.equal(graphSurfaceTarget('W'), null);
|
|
82
|
-
});
|
|
83
|
-
test('a focus row with no pane is skipped, not selected', () => {
|
|
84
|
-
openFocusRow('f-r', null, null, 'R'); // focus exists but not yet placed on a pane
|
|
85
|
-
openFocusRow('f-m', '%20', 'work', 'M');
|
|
86
|
-
assert.equal(graphSurfaceTarget('W')?.node_id, 'M');
|
|
87
|
-
});
|
|
88
|
-
test('the asking node itself, when it is the focused root, is returned', () => {
|
|
89
|
-
openFocusRow('f-r', '%10', 'work', 'R');
|
|
90
|
-
assert.equal(graphSurfaceTarget('R')?.node_id, 'R');
|
|
91
|
-
});
|
|
92
|
-
test('sanity: a closed focus drops out of the selection', () => {
|
|
93
|
-
openFocusRow('f-m', '%20', 'work', 'M');
|
|
94
|
-
assert.equal(graphSurfaceTarget('W')?.node_id, 'M');
|
|
95
|
-
closeFocusRow('f-m');
|
|
96
|
-
assert.equal(getFocusByNode('M'), null);
|
|
97
|
-
assert.equal(graphSurfaceTarget('W'), null);
|
|
98
|
-
});
|