@crouton-kit/crouter 0.3.15 → 0.3.16
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-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/commands/chord.js +1 -1
- package/dist/commands/human/shared.js +1 -1
- package/dist/commands/node.js +1 -2
- package/dist/commands/tmux-spread.js +2 -3
- package/dist/core/__tests__/close.test.js +2 -2
- package/dist/core/__tests__/focuses.test.js +5 -68
- package/dist/core/__tests__/home-session.test.js +1 -1
- package/dist/core/__tests__/placement-focus.test.js +54 -32
- package/dist/core/__tests__/placement-teardown.test.js +4 -9
- package/dist/core/__tests__/relaunch.test.js +10 -4
- package/dist/core/__tests__/tmux-surface.test.js +8 -9
- package/dist/core/canvas/db.js +2 -3
- package/dist/core/canvas/focuses.d.ts +2 -2
- package/dist/core/canvas/focuses.js +4 -3
- package/dist/core/canvas/types.d.ts +1 -1
- package/dist/core/runtime/close.js +2 -2
- package/dist/core/runtime/demote.js +2 -7
- package/dist/core/runtime/launch.d.ts +3 -1
- package/dist/core/runtime/launch.js +4 -1
- package/dist/core/runtime/nodes.d.ts +7 -0
- package/dist/core/runtime/nodes.js +10 -1
- package/dist/core/runtime/placement.d.ts +17 -5
- package/dist/core/runtime/placement.js +56 -31
- package/dist/core/runtime/reset.js +13 -13
- package/dist/core/runtime/revive.d.ts +1 -1
- package/dist/core/runtime/revive.js +2 -2
- package/dist/core/runtime/spawn.js +3 -3
- package/dist/core/runtime/tmux-chrome.d.ts +1 -0
- package/dist/core/runtime/tmux-chrome.js +4 -0
- package/dist/core/runtime/tmux.d.ts +13 -6
- package/dist/core/runtime/tmux.js +21 -12
- package/dist/pi-extensions/canvas-nav.js +11 -3
- package/dist/pi-extensions/canvas-resume.d.ts +22 -0
- package/dist/pi-extensions/canvas-resume.js +173 -0
- package/dist/pi-extensions/canvas-stophook.js +5 -9
- package/package.json +2 -2
- package/dist/core/runtime/presence.d.ts +0 -30
- package/dist/core/runtime/presence.js +0 -178
|
@@ -4,7 +4,7 @@ roadmapSkill: development
|
|
|
4
4
|
|
|
5
5
|
You are a **developer orchestrator** — a senior engineer who owns a feature-sized goal and delivers it by driving specialist children, never by writing the code yourself. Your children are `explore` (to map), `spec` (to specify), `plan` (to decompose), `developer` (to implement), and `review` (to validate). Keep them pointed at the right work with the right context, integrate what they return, and advance the goal phase by phase until it is genuinely done.
|
|
6
6
|
|
|
7
|
-
Run the build as a delegation pipeline — spec → plan → implement → review → fix → validate — parallel wherever tasks are file-independent. Each phase clears a non-negotiable exit criterion before anything builds on it: implementation is done when it is **provably correct against the spec's acceptance criteria**, not when it compiles; review is done when an agent *other than the implementer* has read the diff and every Major and Critical finding is resolved; validation is done when the thing works end-to-end in the real runtime, exercised by something other than the code that produced it. Not every change earns the full pipeline — a one-line wrapper goes straight to implementation — but whatever phase you do run, it clears its bar.
|
|
7
|
+
Before you shape the roadmap, read `crtr skill read development` for the roadmap shapes, development styles, and exit-criteria patterns for software goals. Run the build as a delegation pipeline — spec → plan → implement → review → fix → validate — parallel wherever tasks are file-independent. Each phase clears a non-negotiable exit criterion before anything builds on it: implementation is done when it is **provably correct against the spec's acceptance criteria**, not when it compiles; review is done when an agent *other than the implementer* has read the diff and every Major and Critical finding is resolved; validation is done when the thing works end-to-end in the real runtime, exercised by something other than the code that produced it. Not every change earns the full pipeline — a one-line wrapper goes straight to implementation — but whatever phase you do run, it clears its bar.
|
|
8
8
|
|
|
9
9
|
Stay flexible, not waterfall. When a review exposes a flaw in the spec, re-delegate the **spec** phase — don't patch the implementation forward on a bad foundation. When an implementer reports unexpected complexity or a dependency the plan missed, fix the **plan** and re-delegate the affected tasks rather than asking the implementer to improvise. The bad phase is the one you re-run; patching downstream of a wrong upstream phase buries the flaw instead of removing it.
|
|
10
10
|
|
|
@@ -8,6 +8,6 @@ Decompose by **domain seam, not raw size** — what forces a split is a boundary
|
|
|
8
8
|
|
|
9
9
|
When you split, **synthesis is the load-bearing step — not the splitting.** As the only agent holding the whole picture, edit the part-plans into one coherent voice: resolve file-ownership conflicts, align naming and shared types across slices, and stress-test the seams no single sub-planner could see. Keep the master a small navigable index — a dependency task table over linked part-plans — because that is what forces the decomposition to be real instead of a flat dump.
|
|
10
10
|
|
|
11
|
-
No consequential plan leaves your hands unreviewed. Fan out your plan-reviewer sub-kinds — the **requirements-coverage**, **pattern-consistency**, **code-smells**, **security**, and **architecture-fit** lenses in your spawnable menu — in parallel, then fold their findings back before you advance: a light plan folds one pass inside a single wake, a load-bearing one loops review → yield → revise → re-review across cycles until it is sound. Calibrate the roster to the stakes — a one-file wrapper change does not summon five lenses.
|
|
11
|
+
No consequential plan leaves your hands unreviewed. Fan out your plan-reviewer sub-kinds — the **requirements-coverage**, **pattern-consistency**, **code-smells**, **security**, and **architecture-fit** lenses in your spawnable menu — in parallel, then fold their findings back before you advance: a light plan folds one pass inside a single wake, a load-bearing one loops review → yield → revise → re-review across cycles until it is sound. Calibrate the roster to the stakes — a one-file wrapper change does not summon five lenses. Each reviewer reports findings, not verdicts; you decide what blocks, and a clean review is a valid and expected result.
|
|
12
12
|
|
|
13
13
|
@include orchestration-kernel.md
|
package/dist/commands/chord.js
CHANGED
|
@@ -21,7 +21,7 @@ import { execFile } from 'node:child_process';
|
|
|
21
21
|
import { defineLeaf } from '../core/command.js';
|
|
22
22
|
import { InputError } from '../core/io.js';
|
|
23
23
|
import { readConfig } from '../core/config.js';
|
|
24
|
-
import { sendKeysEnter } from '../core/runtime/tmux.js';
|
|
24
|
+
import { sendKeysEnter } from '../core/runtime/tmux-chrome.js';
|
|
25
25
|
import { nodeInPane } from './node.js';
|
|
26
26
|
import { getNode, subscribersOf, subscriptionsOf, view, fullName, } from '../core/canvas/index.js';
|
|
27
27
|
const pexec = promisify(execFile);
|
|
@@ -6,7 +6,7 @@ import { countPanesInCurrentWindow, spawnAndDetach, shellQuote } from '../../cor
|
|
|
6
6
|
export const DECK_SCHEMA_HINT = 'Deck must match the humanloop deck schema: {title?, ' +
|
|
7
7
|
'source?:{sessionName?,askedBy?,blockedSince?}, ' +
|
|
8
8
|
'interactions:[{id,title,subtitle?,(body?|bodyPath?),options:[{id,label,' +
|
|
9
|
-
'description
|
|
9
|
+
'description?}],multiSelect?,allowFreetext?,freetextLabel?,' +
|
|
10
10
|
"kind?:'notify'|'validation'|'decision'|'context'|'error'}]}.";
|
|
11
11
|
export function resolveMaxPanes() {
|
|
12
12
|
return readConfig('user').max_panes_per_window;
|
package/dist/commands/node.js
CHANGED
|
@@ -11,10 +11,9 @@ import { promote, requestYield } from '../core/runtime/promote.js';
|
|
|
11
11
|
import { writeYieldMessage } from '../core/runtime/kickoff.js';
|
|
12
12
|
import { reviveNode } from '../core/runtime/revive.js';
|
|
13
13
|
import { demoteNode } from '../core/runtime/demote.js';
|
|
14
|
-
import { detachToBackground, focus as placementFocus } from '../core/runtime/placement.js';
|
|
14
|
+
import { detachToBackground, focus as placementFocus, windowAlive, windowOfPane, currentTmux } from '../core/runtime/placement.js';
|
|
15
15
|
import { buildLaunchSpec } from '../core/runtime/launch.js';
|
|
16
16
|
import { closeNode } from '../core/runtime/close.js';
|
|
17
|
-
import { windowAlive, windowOfPane, currentTmux } from '../core/runtime/tmux.js';
|
|
18
17
|
import { appendInbox } from '../core/feed/inbox.js';
|
|
19
18
|
import { availableKinds } from '../core/personas/index.js';
|
|
20
19
|
import { getNode, updateNode, listNodes, subscribe, unsubscribe, subscriptionsOf, subscribersOf, readContextTokens, } from '../core/canvas/index.js';
|
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
// meta.{tmux_session,window} goes stale — `windowAlive` would then report it
|
|
11
11
|
// dormant and the daemon would spuriously revive it. After each join we
|
|
12
12
|
// re-derive the child's location from its (stable) pane id and updateNode it,
|
|
13
|
-
// mirroring the swap fix-up in
|
|
13
|
+
// mirroring the swap fix-up in placement.ts.
|
|
14
14
|
import { defineLeaf } from '../core/command.js';
|
|
15
15
|
import { InputError } from '../core/io.js';
|
|
16
16
|
import { readConfig } from '../core/config.js';
|
|
17
17
|
import { reviveNode } from '../core/runtime/revive.js';
|
|
18
|
-
import { isNodePaneAlive, spreadNode } from '../core/runtime/placement.js';
|
|
19
|
-
import { inTmux } from '../core/runtime/tmux.js';
|
|
18
|
+
import { isNodePaneAlive, spreadNode, inTmux } from '../core/runtime/placement.js';
|
|
20
19
|
import { nodeInPane } from './node.js';
|
|
21
20
|
import { getNode, subscriptionsOf } from '../core/canvas/index.js';
|
|
22
21
|
export const tmuxSpreadLeaf = defineLeaf({
|
|
@@ -140,8 +140,8 @@ test('Step 7: closing a FOCUSED node closes its focus row + nulls its pane (tear
|
|
|
140
140
|
openFocusRow('fN', '%x', 'Sa', 'N');
|
|
141
141
|
closeNode('N');
|
|
142
142
|
// tearDownNode closes the focus row the node occupied and nulls its LOCATION.
|
|
143
|
-
// Non-vacuous: pre-Step-7 close used closeWindow
|
|
144
|
-
//
|
|
143
|
+
// Non-vacuous: pre-Step-7 close used closeWindow and never touched the focuses
|
|
144
|
+
// table, so getFocusByNode('N') would still return fN.
|
|
145
145
|
assert.equal(getFocusByNode('N'), null, 'focus row closed by tearDownNode');
|
|
146
146
|
assert.equal(getNode('N').pane ?? null, null, 'pane nulled (pane-keyed teardown)');
|
|
147
147
|
assert.equal(getNode('N').status, 'canceled', 'node canceled as before');
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// Run with: node --import tsx/esm --test src/core/__tests__/focuses.test.ts
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// nothing reads it as authority yet (that switch is Step 6). Covers:
|
|
3
|
+
// The `focuses` table (canvas.db, migration v6) + its canvas setters + the
|
|
4
|
+
// placement reads that compose over them. The table is the CANONICAL focus store
|
|
5
|
+
// — there is no focus.ptr file and no dual-write bridge. Covers:
|
|
7
6
|
// - migration v6 adds `focuses` to a fresh db (and a legacy v5 db migrates up);
|
|
8
7
|
// idempotent / forward-only on re-run + re-open
|
|
9
8
|
// - canvas setters/reads round-trip: open / setOccupant / setPane / close;
|
|
@@ -13,23 +12,17 @@
|
|
|
13
12
|
// - independent focus rows don't contend
|
|
14
13
|
// - placement focusOf / isFocused / focusByPane / focusedNodes / listFocuses
|
|
15
14
|
// agree with the rows
|
|
16
|
-
// - dual-write: setFocus populates the table; getFocus falls back to the table
|
|
17
|
-
// when focus.ptr is absent; setFocus('') clears both
|
|
18
15
|
import { test, before, beforeEach, after } from 'node:test';
|
|
19
16
|
import assert from 'node:assert/strict';
|
|
20
|
-
import { mkdtempSync, rmSync
|
|
17
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
21
18
|
import { tmpdir } from 'node:os';
|
|
22
19
|
import { join } from 'node:path';
|
|
23
20
|
import { DatabaseSync } from 'node:sqlite';
|
|
24
21
|
import { openFocusRow, setFocusOccupant, setFocusPane, closeFocusRow, getFocusByNode, getFocusByPane, getFocusById, listFocuses, } from '../canvas/focuses.js';
|
|
25
22
|
import { openDb, closeDb, migrate, MIGRATIONS } from '../canvas/db.js';
|
|
26
|
-
import { canvasDbPath, ensureHome
|
|
23
|
+
import { canvasDbPath, ensureHome } from '../canvas/paths.js';
|
|
27
24
|
import { focusOf, isFocused, focusByPane, focusedNodes, listFocuses as placementListFocuses, } from '../runtime/placement.js';
|
|
28
|
-
import { setFocus, getFocus } from '../runtime/presence.js';
|
|
29
25
|
let home;
|
|
30
|
-
// Saved/restored so the bridge always exercises its deterministic no-tmux path
|
|
31
|
-
// regardless of whether the suite is run from inside a tmux session.
|
|
32
|
-
let savedTmux;
|
|
33
26
|
function userVersion(db) {
|
|
34
27
|
return db.prepare('PRAGMA user_version').get().user_version;
|
|
35
28
|
}
|
|
@@ -39,8 +32,6 @@ function tableNames(db) {
|
|
|
39
32
|
before(() => {
|
|
40
33
|
home = mkdtempSync(join(tmpdir(), 'crtr-focuses-'));
|
|
41
34
|
process.env['CRTR_HOME'] = home;
|
|
42
|
-
savedTmux = process.env['TMUX'];
|
|
43
|
-
delete process.env['TMUX'];
|
|
44
35
|
});
|
|
45
36
|
beforeEach(() => {
|
|
46
37
|
closeDb();
|
|
@@ -50,8 +41,6 @@ after(() => {
|
|
|
50
41
|
closeDb();
|
|
51
42
|
rmSync(home, { recursive: true, force: true });
|
|
52
43
|
delete process.env['CRTR_HOME'];
|
|
53
|
-
if (savedTmux !== undefined)
|
|
54
|
-
process.env['TMUX'] = savedTmux;
|
|
55
44
|
});
|
|
56
45
|
// ---------------------------------------------------------------------------
|
|
57
46
|
// Migration v6 — the additive `focuses` table.
|
|
@@ -205,55 +194,3 @@ test('placement focus reads agree with the focus rows', () => {
|
|
|
205
194
|
assert.deepEqual(focusedNodes(), new Set(['A', 'B']));
|
|
206
195
|
assert.deepEqual(placementListFocuses().map((f) => f.node_id), ['A', 'B']);
|
|
207
196
|
});
|
|
208
|
-
// ---------------------------------------------------------------------------
|
|
209
|
-
// Dual-write bridge — setFocus populates the table; getFocus falls back to the
|
|
210
|
-
// table when focus.ptr is absent; setFocus('') clears both.
|
|
211
|
-
// ---------------------------------------------------------------------------
|
|
212
|
-
function focusPtrPath() {
|
|
213
|
-
return join(crtrHome(), 'focus.ptr');
|
|
214
|
-
}
|
|
215
|
-
test('setFocus populates the focuses table in lockstep with focus.ptr', () => {
|
|
216
|
-
openDb();
|
|
217
|
-
setFocus('A');
|
|
218
|
-
assert.equal(getFocus(), 'A', 'focus.ptr reads back');
|
|
219
|
-
const row = getFocusByNode('A');
|
|
220
|
-
assert.ok(row, 'a canonical focus row mirrors the current focus');
|
|
221
|
-
assert.equal(row?.node_id, 'A');
|
|
222
|
-
assert.equal(isFocused('A'), true, 'placement.isFocused agrees');
|
|
223
|
-
assert.deepEqual(focusOf('A')?.node_id, 'A', 'placement.focusOf agrees with getFocus');
|
|
224
|
-
// Re-focusing a different node re-points the SAME canonical row (no stray rows,
|
|
225
|
-
// UNIQUE(node_id) upheld).
|
|
226
|
-
setFocus('B');
|
|
227
|
-
assert.equal(getFocus(), 'B');
|
|
228
|
-
assert.equal(getFocusByNode('A'), null, 'the old occupant is dropped');
|
|
229
|
-
assert.equal(getFocusByNode('B')?.node_id, 'B');
|
|
230
|
-
assert.equal(listFocuses().length, 1, 'still exactly one canonical row');
|
|
231
|
-
});
|
|
232
|
-
test('getFocus falls back to the table when focus.ptr is absent', () => {
|
|
233
|
-
openDb();
|
|
234
|
-
setFocus('A'); // writes both focus.ptr and the canonical row
|
|
235
|
-
// Simulate a missing pointer (a writer that reached only the table, or a lost
|
|
236
|
-
// file): delete focus.ptr and confirm getFocus recovers the focus from the row.
|
|
237
|
-
if (existsSync(focusPtrPath()))
|
|
238
|
-
unlinkSync(focusPtrPath());
|
|
239
|
-
assert.equal(getFocus(), 'A', 'getFocus recovers the focus from the table');
|
|
240
|
-
});
|
|
241
|
-
test("setFocus('') clears both the pointer and the canonical focus row", () => {
|
|
242
|
-
openDb();
|
|
243
|
-
setFocus('A');
|
|
244
|
-
assert.equal(getFocus(), 'A');
|
|
245
|
-
assert.ok(getFocusByNode('A'), 'precondition: row present');
|
|
246
|
-
setFocus('');
|
|
247
|
-
assert.equal(getFocus(), null, 'getFocus is null after clear (ptr empty, no row)');
|
|
248
|
-
assert.equal(getFocusByNode('A'), null, 'the canonical row was closed');
|
|
249
|
-
assert.deepEqual(listFocuses(), [], 'no focus rows remain');
|
|
250
|
-
});
|
|
251
|
-
test('a focus row written directly (no focus.ptr) is visible through getFocus + placement', () => {
|
|
252
|
-
openDb();
|
|
253
|
-
// A writer that reached only the table (the canonical bridge row), with no
|
|
254
|
-
// focus.ptr on disk at all.
|
|
255
|
-
openFocusRow('__focus_ptr__', null, null, 'X');
|
|
256
|
-
assert.ok(!existsSync(focusPtrPath()), 'precondition: no focus.ptr file');
|
|
257
|
-
assert.equal(getFocus(), 'X', 'getFocus falls back to the canonical row');
|
|
258
|
-
assert.equal(isFocused('X'), true);
|
|
259
|
-
});
|
|
@@ -18,7 +18,7 @@ import { createNode, getNode, updateNode } from '../canvas/canvas.js';
|
|
|
18
18
|
import { nodeMetaPath } from '../canvas/paths.js';
|
|
19
19
|
import { closeDb } from '../canvas/db.js';
|
|
20
20
|
import { resolveBirthSession, homeSessionOf } from '../runtime/nodes.js';
|
|
21
|
-
import { nodeSession } from '../runtime/
|
|
21
|
+
import { nodeSession } from '../runtime/nodes.js';
|
|
22
22
|
import { relaunchRoot } from '../runtime/reset.js';
|
|
23
23
|
import { demoteNode } from '../runtime/demote.js';
|
|
24
24
|
let home;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// Run with: node --import tsx/esm --test src/core/__tests__/placement-focus.test.ts
|
|
2
2
|
//
|
|
3
3
|
// STEP 6 of the placement/focus migration: retargetFocus / openFocus / focus +
|
|
4
|
-
// remain-on-exit + root-boot focus #1
|
|
4
|
+
// remain-on-exit + root-boot focus #1.
|
|
5
5
|
//
|
|
6
6
|
// Two proof tiers (mirrors placement-revive.test.ts):
|
|
7
|
-
// 1. PURE (no tmux): outgoingDisposition (backstage-vs-kill)
|
|
8
|
-
//
|
|
9
|
-
// (the Step-6 bridge fix). Each is provably non-vacuous (a wrong impl fails).
|
|
7
|
+
// 1. PURE (no tmux): outgoingDisposition (backstage-vs-kill). Provably
|
|
8
|
+
// non-vacuous (a wrong impl fails).
|
|
10
9
|
// 2. Gated real-tmux: the hot-swap itself — screen position invariant (ZERO new
|
|
11
10
|
// user windows), the two post-swap LOCATIONs, outgoing backstaged (still
|
|
12
11
|
// generating) vs reaped (dormant), the Q5 vacate-old-focus path, openFocus
|
|
@@ -15,16 +14,14 @@
|
|
|
15
14
|
// a live server; gated {skip:!hasTmux()} like §5.2.
|
|
16
15
|
import { test, before, after, beforeEach } from 'node:test';
|
|
17
16
|
import assert from 'node:assert/strict';
|
|
18
|
-
import { mkdtempSync, rmSync
|
|
17
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
19
18
|
import { tmpdir } from 'node:os';
|
|
20
19
|
import { join } from 'node:path';
|
|
21
20
|
import { spawnSync } from 'node:child_process';
|
|
22
21
|
import { createNode, getNode, setPresence } from '../canvas/canvas.js';
|
|
23
22
|
import { openFocusRow, getFocusByNode, getFocusById, listFocuses, } from '../canvas/focuses.js';
|
|
24
23
|
import { closeDb } from '../canvas/db.js';
|
|
25
|
-
import {
|
|
26
|
-
import { outgoingDisposition, retargetFocus, openFocus, focus as placementFocus, registerRootFocus, focusByPane, } from '../runtime/placement.js';
|
|
27
|
-
import { setFocus, getFocus } from '../runtime/presence.js';
|
|
24
|
+
import { outgoingDisposition, retargetFocus, openFocus, focus as placementFocus, registerRootFocus, focusByPane, detachToBackground, } from '../runtime/placement.js';
|
|
28
25
|
let home;
|
|
29
26
|
let savedTmux;
|
|
30
27
|
function node(id, over = {}) {
|
|
@@ -75,29 +72,6 @@ test('outgoingDisposition: a HOLDER / vanished node (no row) → KILL (never bac
|
|
|
75
72
|
assert.deepEqual(outgoingDisposition({ exists: false, generating: false }), { kind: 'kill' });
|
|
76
73
|
});
|
|
77
74
|
// ---------------------------------------------------------------------------
|
|
78
|
-
// 1b. PURE: the focus.ptr dual-write bridge PIGGYBACKS on a real focus row
|
|
79
|
-
// (Step-6 fix) — setFocus must NOT clobber the pane-correct row retargetFocus
|
|
80
|
-
// wrote. The OLD bridge closed `existing` unconditionally, replacing the real
|
|
81
|
-
// row with a `__focus_ptr__` row; asserting the focus_id survives fails it.
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
test('focus.ptr bridge: setFocus piggybacks on a REAL focus row (never clobbers it)', () => {
|
|
84
|
-
// Simulate a real row written by retargetFocus/openFocus (a non-bridge id).
|
|
85
|
-
openFocusRow('real-f', '%a', 'Suser', 'A');
|
|
86
|
-
setFocus('A'); // the consistency mirror retargetFocus calls at the end
|
|
87
|
-
assert.equal(getFocus(), 'A', 'focus.ptr names the node');
|
|
88
|
-
const row = getFocusByNode('A');
|
|
89
|
-
assert.ok(row, 'A still occupies a focus');
|
|
90
|
-
assert.equal(row?.focus_id, 'real-f', 'the REAL row survived (not replaced by a bridge row)');
|
|
91
|
-
assert.equal(row?.pane, '%a', 'the pane-correct row is intact');
|
|
92
|
-
assert.equal(listFocuses().length, 1, 'no duplicate bridge row was inserted (UNIQUE node_id)');
|
|
93
|
-
});
|
|
94
|
-
test('focus.ptr bridge: a plain setFocus (no real row) still creates the bridge row + getFocus reads it', () => {
|
|
95
|
-
setFocus('Z');
|
|
96
|
-
if (existsSync(join(crtrHome(), 'focus.ptr')))
|
|
97
|
-
unlinkSync(join(crtrHome(), 'focus.ptr'));
|
|
98
|
-
assert.equal(getFocus(), 'Z', 'getFocus falls back to the canonical bridge row when the ptr is gone');
|
|
99
|
-
});
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
75
|
// 2. Gated real-tmux: the hot-swap. Two isolated sessions: `user` (the user's
|
|
102
76
|
// terminal) + `back` (stand-in for the backstage `crtr`). Panes run `sleep`,
|
|
103
77
|
// never a real pi; node pi_pid is set explicitly to control "generating".
|
|
@@ -168,7 +142,7 @@ test('retargetFocus: outgoing GENERATING → backstaged; the viewport stays put
|
|
|
168
142
|
assert.equal(paneExistsReal(focusPane), true, 'R\'s pane is alive (NOT reaped — it is still generating)');
|
|
169
143
|
assert.equal(paneSession(focusPane), back, 'R\'s pane physically moved to the backstage');
|
|
170
144
|
assert.equal(getNode('R').tmux_session, back, 'R\'s LOCATION session is the backstage');
|
|
171
|
-
assert.equal(
|
|
145
|
+
assert.equal(focusByPane(backPane)?.node_id, 'A', 'the focus row tracks A after the retarget');
|
|
172
146
|
});
|
|
173
147
|
});
|
|
174
148
|
test('retargetFocus: outgoing DORMANT (no live pi) → its now-backstage pane is REAPED (Invariant P)', { skip: !hasTmux() }, async () => {
|
|
@@ -242,3 +216,51 @@ test('focus front-door: round-trip open(register #1) → retarget in place → t
|
|
|
242
216
|
assert.equal(focusByPane(aPane)?.node_id, 'A', 'the focus row tracks A\'s pane');
|
|
243
217
|
});
|
|
244
218
|
});
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Regression guards (review findings on HEAD=ccc3ee2): a detach/failed-open must
|
|
221
|
+
// not leave a dangling focus row or an orphan viewport pane (Invariant P / F4).
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
test('detachToBackground: a FOCUSED node sent to the backstage CLOSES its focus row (Invariant P — no phantom viewport)', { skip: !hasTmux() }, async () => {
|
|
224
|
+
await withSessions('detach', async ({ user, back, userWindow }) => {
|
|
225
|
+
const prev = process.env['CRTR_NODE_SESSION'];
|
|
226
|
+
process.env['CRTR_NODE_SESSION'] = back; // detach relocates into THIS backstage session
|
|
227
|
+
try {
|
|
228
|
+
const focusPane = livePane(user, userWindow); // the node's foreground viewport pane
|
|
229
|
+
createNode(node('N', { pane: focusPane, tmux_session: user, window: userWindow, status: 'active', pi_pid: process.pid, home_session: back }));
|
|
230
|
+
openFocusRow('f1', focusPane, user, 'N'); // N is the focus occupant
|
|
231
|
+
assert.equal(getFocusByNode('N')?.focus_id, 'f1', 'precondition: N is focused');
|
|
232
|
+
const ok = detachToBackground('N', focusPane);
|
|
233
|
+
assert.equal(ok, true, 'the break to the backstage succeeded');
|
|
234
|
+
// The fix: N is now generating-but-UNFOCUSED, so its focus row is CLOSED.
|
|
235
|
+
assert.equal(getFocusByNode('N'), null, 'N no longer occupies any focus (row CLOSED — Invariant P)');
|
|
236
|
+
assert.equal(focusByPane(focusPane), null, 'NO phantom focus resolves on the relocated pane (%id survives the break)');
|
|
237
|
+
assert.equal(listFocuses().length, 0, 'no dangling focus rows remain');
|
|
238
|
+
// The pi keeps running — the pane is alive, just moved off-screen.
|
|
239
|
+
assert.equal(paneExistsReal(focusPane), true, 'N\'s pi keeps generating (pane alive, relocated not killed)');
|
|
240
|
+
assert.equal(paneSession(focusPane), back, 'N\'s pane physically moved to the backstage session');
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
if (prev === undefined)
|
|
244
|
+
delete process.env['CRTR_NODE_SESSION'];
|
|
245
|
+
else
|
|
246
|
+
process.env['CRTR_NODE_SESSION'] = prev;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
test('focus --new-pane: a FAILED retarget reaps the holder pane + focus row (no orphan viewport — F4/Invariant P)', { skip: !hasTmux() }, async () => {
|
|
251
|
+
await withSessions('newpane-fail', async ({ user, userWindow }) => {
|
|
252
|
+
const callerPane = livePane(user, userWindow); // the caller's pane we split beside
|
|
253
|
+
createNode(node('D', { status: 'active', pi_pid: null })); // D is DORMANT — no live pane
|
|
254
|
+
const winBefore = windowIds(user).length;
|
|
255
|
+
const panesIn = (s, w) => tmuxOut(['list-panes', '-t', `${s}:${w}`, '-F', '#{pane_id}']).split('\n').filter((x) => x !== '').length;
|
|
256
|
+
const panesBefore = panesIn(user, userWindow);
|
|
257
|
+
// A reviver that does NOTHING → D stays dormant, retargetFocus finds no live
|
|
258
|
+
// pin and returns focused:false. The just-opened holder must be reaped.
|
|
259
|
+
const noopRevive = () => { };
|
|
260
|
+
const res = placementFocus('D', { pane: callerPane, newPane: true, revive: noopRevive });
|
|
261
|
+
assert.equal(res.focused, false, 'D could not be placed → the --new-pane focus failed');
|
|
262
|
+
assert.equal(listFocuses().length, 0, 'the just-opened focus row was REAPED (no phantom)');
|
|
263
|
+
assert.equal(panesIn(user, userWindow), panesBefore, 'the holder split pane was REAPED (no leaked sleep pane)');
|
|
264
|
+
assert.equal(windowIds(user).length, winBefore, 'no window leaked');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
// not-already-focused manager; false (caller closes the focus) in each of the
|
|
10
10
|
// three guard cases. Each guard is asserted distinctly.
|
|
11
11
|
// • tearDownNode(nodeId) — close/reset teardown: close the focus row it
|
|
12
|
-
// occupies
|
|
13
|
-
// focus.
|
|
12
|
+
// occupies and null its LOCATION.
|
|
14
13
|
import { test, before, after, beforeEach } from 'node:test';
|
|
15
14
|
import assert from 'node:assert/strict';
|
|
16
15
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
@@ -21,7 +20,6 @@ import { createNode, getNode } from '../canvas/canvas.js';
|
|
|
21
20
|
import { openFocusRow, getFocusByNode, getFocusById } from '../canvas/focuses.js';
|
|
22
21
|
import { closeDb } from '../canvas/db.js';
|
|
23
22
|
import { handFocusToManager, tearDownNode } from '../runtime/placement.js';
|
|
24
|
-
import { setFocus, getFocus } from '../runtime/presence.js';
|
|
25
23
|
let home;
|
|
26
24
|
function node(id, over = {}) {
|
|
27
25
|
return {
|
|
@@ -166,18 +164,15 @@ test('handFocusToManager: DORMANT manager (dead pane) → occupant repointed, NO
|
|
|
166
164
|
// ---------------------------------------------------------------------------
|
|
167
165
|
// tearDownNode (pure DB; no tmux — pane is null so closePane never runs).
|
|
168
166
|
// ---------------------------------------------------------------------------
|
|
169
|
-
test('tearDownNode: closes the focus row
|
|
167
|
+
test('tearDownNode: closes the focus row M occupied and nulls its LOCATION', () => {
|
|
170
168
|
createNode(node('M', { pane: null, window: null }));
|
|
171
|
-
openFocusRow('fM', null, 'Sa', 'M');
|
|
172
|
-
setFocus('M'); // M is the current focus.ptr
|
|
169
|
+
openFocusRow('fM', null, 'Sa', 'M'); // M occupies a focus row
|
|
173
170
|
tearDownNode('M');
|
|
174
171
|
assert.equal(getFocusByNode('M'), null, 'the focus row M occupied is closed');
|
|
175
172
|
const m = getNode('M');
|
|
176
173
|
assert.equal(m.pane ?? null, null, 'pane nulled');
|
|
177
174
|
assert.equal(m.window ?? null, null, 'window nulled');
|
|
178
175
|
assert.equal(m.tmux_session ?? null, null, 'session nulled');
|
|
179
|
-
const cur = getFocus();
|
|
180
|
-
assert.ok(cur === null || cur === '', 'focus.ptr cleared (M was the current focus)');
|
|
181
176
|
// Non-vacuous: an impl that skips closeFocusRow leaves fM → getFocusByNode('M')
|
|
182
|
-
// is non-null
|
|
177
|
+
// is non-null.
|
|
183
178
|
});
|
|
@@ -11,10 +11,10 @@ import { tmpdir } from 'node:os';
|
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { createNode, getNode, subscribe, subscriptionsOf, view, listNodes, } from '../canvas/canvas.js';
|
|
13
13
|
import { closeDb } from '../canvas/db.js';
|
|
14
|
+
import { getFocusByNode, openFocusRow } from '../canvas/focuses.js';
|
|
14
15
|
import { reportsDir, inboxPath, contextDir } from '../canvas/paths.js';
|
|
15
16
|
import { roadmapPath } from '../runtime/roadmap.js';
|
|
16
17
|
import { relaunchRoot, handleNewSession, markCleanExitDone, reapDescendants, } from '../runtime/reset.js';
|
|
17
|
-
import { getFocus } from '../runtime/presence.js';
|
|
18
18
|
import { renderForest } from '../canvas/render.js';
|
|
19
19
|
let home;
|
|
20
20
|
function node(id, over = {}) {
|
|
@@ -79,6 +79,7 @@ test('relaunchRoot parks the old root (done, edges intact, no wipe) and mints a
|
|
|
79
79
|
createNode(node('grand', { parent: 'child' }));
|
|
80
80
|
subscribe('root', 'child', true);
|
|
81
81
|
subscribe('child', 'grand', true);
|
|
82
|
+
openFocusRow('fRoot', '%root', 'crtr', 'root'); // the old root occupies focus #1
|
|
82
83
|
// Working state on the old root that parking must PRESERVE (no wipe).
|
|
83
84
|
writeFileSync(roadmapPath('root'), '# Roadmap\nold goal\n');
|
|
84
85
|
writeFileSync(inboxPath('root'), '{"ts":"x","from":"child","tier":"normal","kind":"update","label":"hi"}\n');
|
|
@@ -122,7 +123,9 @@ test('relaunchRoot parks the old root (done, edges intact, no wipe) and mints a
|
|
|
122
123
|
assert.equal(fresh?.window, '@7');
|
|
123
124
|
assert.ok(fresh?.launch, 'a fresh base launch spec was written');
|
|
124
125
|
assert.equal(readdirSync(contextDir(newId)).length, 0, 'fresh empty context dir');
|
|
125
|
-
|
|
126
|
+
// Focus follows content: the focus row the old root held now shows the new root.
|
|
127
|
+
assert.equal(getFocusByNode(newId)?.focus_id, 'fRoot', 'focus row repointed to the new root');
|
|
128
|
+
assert.equal(getFocusByNode('root'), null, 'old root no longer occupies the focus');
|
|
126
129
|
});
|
|
127
130
|
// ---------------------------------------------------------------------------
|
|
128
131
|
// #1b — handleNewSession success branch: root WITH a pane routes to relaunch
|
|
@@ -138,6 +141,7 @@ test('handleNewSession on a root with a pane returns path:relaunch + parks old,
|
|
|
138
141
|
}));
|
|
139
142
|
createNode(node('child', { parent: 'root' }));
|
|
140
143
|
subscribe('root', 'child', true);
|
|
144
|
+
openFocusRow('fRoot', '%root', 'crtr', 'root'); // the old root occupies focus #1
|
|
141
145
|
const respawn = okRespawn();
|
|
142
146
|
const res = handleNewSession('root', 'newsess', 'test-pane', { relaunchRootInPane: respawn.fn });
|
|
143
147
|
// The policy router's success return shape: relaunch + a fresh new node id.
|
|
@@ -156,7 +160,7 @@ test('handleNewSession on a root with a pane returns path:relaunch + parks old,
|
|
|
156
160
|
assert.equal(fresh?.parent, null, 'new node is a root');
|
|
157
161
|
assert.equal(fresh?.status, 'active', 'fresh root active');
|
|
158
162
|
assert.equal(fresh?.spawned_by, 'root', 'audit-only successor link to old root');
|
|
159
|
-
assert.equal(
|
|
163
|
+
assert.equal(getFocusByNode(res.newNodeId)?.focus_id, 'fRoot', 'focus row repointed to the fresh root');
|
|
160
164
|
});
|
|
161
165
|
// ---------------------------------------------------------------------------
|
|
162
166
|
// #2 — handleNewSession on a non-root → session-id refresh only
|
|
@@ -238,6 +242,7 @@ test('a respawn dispatch failure rolls the whole transaction back and degrades t
|
|
|
238
242
|
tmux_session: 'crtr',
|
|
239
243
|
window: '@3',
|
|
240
244
|
}));
|
|
245
|
+
openFocusRow('fRoot', '%root', 'crtr', 'root'); // the old root occupies focus #1
|
|
241
246
|
const respawn = throwingRespawn();
|
|
242
247
|
const res = handleNewSession('root', 'newsess', 'test-pane', { relaunchRootInPane: respawn.fn });
|
|
243
248
|
assert.equal(res.path, 'reset-root', 'degraded to in-place reset');
|
|
@@ -256,7 +261,8 @@ test('a respawn dispatch failure rolls the whole transaction back and degrades t
|
|
|
256
261
|
assert.deepEqual(actives, ['root'], 'only the old root is active — no zombie');
|
|
257
262
|
assert.equal(listNodes({ status: ['dead'] }).length, 0, 'no dead zombie — new node row rolled back');
|
|
258
263
|
assert.equal(listNodes().length, 1, 'only the old root row exists — the mint was fully undone');
|
|
259
|
-
|
|
264
|
+
// The focus repoint was INSIDE the txn, so ROLLBACK restored the old occupant.
|
|
265
|
+
assert.equal(getFocusByNode('root')?.focus_id, 'fRoot', 'focus row restored to the old root (rollback undid the repoint)');
|
|
260
266
|
});
|
|
261
267
|
// ---------------------------------------------------------------------------
|
|
262
268
|
// #7 — markCleanExitDone guard table (termination rule)
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
// redesign kills. This guards the bug's blast radius and should pass today.
|
|
11
11
|
//
|
|
12
12
|
// 2. The §5.1 "only placement.ts / tmux-chrome.ts import tmux.ts" lint guard
|
|
13
|
-
// (
|
|
14
|
-
//
|
|
15
|
-
//
|
|
13
|
+
// (ENFORCED as of Step 8): every other module reaches the driver through
|
|
14
|
+
// placement's re-exports or the tmux-chrome seam, so the only direct
|
|
15
|
+
// importers are placement.ts + tmux-chrome.ts (tmux.ts itself excluded).
|
|
16
16
|
import { test } from 'node:test';
|
|
17
17
|
import assert from 'node:assert/strict';
|
|
18
18
|
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
@@ -71,11 +71,10 @@ test('§2.2 driver invariant: every placement verb in tmux.ts passes an explicit
|
|
|
71
71
|
assert.ok(found >= 4, `expected to scan ≥4 placement verbs, saw ${found}`);
|
|
72
72
|
});
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
// Lint guard — §5.1.
|
|
75
|
-
// placement.ts (the
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
// turns the body into the enforced boundary.
|
|
74
|
+
// Lint guard — §5.1, ENFORCED. The driver (tmux.ts) is imported ONLY by
|
|
75
|
+
// placement.ts (the sanctioned model-over-driver, which re-exports the verbs
|
|
76
|
+
// other modules need) and the tmux-chrome.ts chrome seam. Every other module —
|
|
77
|
+
// runtime, daemon, commands, stophook, AND tests — must route through those.
|
|
79
78
|
// ---------------------------------------------------------------------------
|
|
80
79
|
/** Every `.ts` file under `src` (recursively), excluding nothing. */
|
|
81
80
|
function allTsFiles(dir) {
|
|
@@ -97,7 +96,7 @@ function importsDriver(file) {
|
|
|
97
96
|
// `tmux-spread.js` are NOT matched). Covers `from '...'` and `import('...')`.
|
|
98
97
|
return [...src.matchAll(/(?:from|import\s*\()\s*'([^']+)'/g)].some((m) => basename(m[1]) === 'tmux.js');
|
|
99
98
|
}
|
|
100
|
-
test('§5.1 lint: only placement.ts / tmux-chrome.ts import the tmux driver',
|
|
99
|
+
test('§5.1 lint: only placement.ts / tmux-chrome.ts import the tmux driver', () => {
|
|
101
100
|
const offenders = allTsFiles(SRC_ROOT)
|
|
102
101
|
.filter((f) => !ALLOWED_IMPORTERS.has(basename(f)))
|
|
103
102
|
.filter(importsDriver)
|
package/dist/core/canvas/db.js
CHANGED
|
@@ -196,9 +196,8 @@ function addPaneColumn(db) {
|
|
|
196
196
|
* viewport (Q7 widens canvas.db from "topology" to "topology + focuses"). Each
|
|
197
197
|
* row is anchored on the durable tmux `%pane_id`; `session` is a derived cache
|
|
198
198
|
* reconciled from the pane; `node_id` is UNIQUE so a node occupies at most one
|
|
199
|
-
* focus (Q5).
|
|
200
|
-
*
|
|
201
|
-
* dual-write; the switch to table-as-authority lands in Step 6). */
|
|
199
|
+
* focus (Q5). The focuses table is the CANONICAL focus store — there is no
|
|
200
|
+
* focus.ptr file and no dual-write bridge; placement writes focus rows directly. */
|
|
202
201
|
function addFocusesTable(db) {
|
|
203
202
|
db.exec(`
|
|
204
203
|
CREATE TABLE IF NOT EXISTS focuses (
|
|
@@ -15,8 +15,8 @@ export declare function closeFocusRow(focus_id: string): void;
|
|
|
15
15
|
export declare function getFocusByNode(node_id: string): FocusRow | null;
|
|
16
16
|
/** The focus realized by a given pane (`%id`), or null. */
|
|
17
17
|
export declare function getFocusByPane(pane: string): FocusRow | null;
|
|
18
|
-
/** A focus by its stable id, or null.
|
|
19
|
-
*
|
|
18
|
+
/** A focus by its stable id, or null. Used by placement to read a row back by id
|
|
19
|
+
* (handFocusToManager / retargetFocus / registerRootFocus). */
|
|
20
20
|
export declare function getFocusById(focus_id: string): FocusRow | null;
|
|
21
21
|
/** Every focus row, ordered by id. */
|
|
22
22
|
export declare function listFocuses(): FocusRow[];
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// "topology + focuses", so the focus-row SQL lives here beside the node+edge
|
|
5
5
|
// model, never in the runtime layer. A FOCUS is one durable on-screen viewport
|
|
6
6
|
// bound to one node; the table is PLURAL (many focuses across windows/sessions),
|
|
7
|
-
// the generalization of the old single
|
|
7
|
+
// the generalization of the old single focus pointer. It is the CANONICAL focus
|
|
8
|
+
// store — there is no focus.ptr file and no dual-write bridge.
|
|
8
9
|
//
|
|
9
10
|
// placement.ts COMPOSES over these atomic setters/reads (the same way it calls
|
|
10
11
|
// setPresence) — it never runs raw focus SQL itself.
|
|
@@ -66,8 +67,8 @@ export function getFocusByPane(pane) {
|
|
|
66
67
|
.get(pane);
|
|
67
68
|
return r ? focusFrom(r) : null;
|
|
68
69
|
}
|
|
69
|
-
/** A focus by its stable id, or null.
|
|
70
|
-
*
|
|
70
|
+
/** A focus by its stable id, or null. Used by placement to read a row back by id
|
|
71
|
+
* (handFocusToManager / retargetFocus / registerRootFocus). */
|
|
71
72
|
export function getFocusById(focus_id) {
|
|
72
73
|
const r = openDb()
|
|
73
74
|
.prepare('SELECT * FROM focuses WHERE focus_id = ?')
|
|
@@ -168,7 +168,7 @@ export interface Edge {
|
|
|
168
168
|
/** A FOCUS row as stored in the `focuses` table (canvas.db, migration v6): one
|
|
169
169
|
* durable on-screen viewport the user looks at, bound to one node. Plural —
|
|
170
170
|
* many focuses live at once across windows and sessions (the plural
|
|
171
|
-
* generalization of the old single
|
|
171
|
+
* generalization of the old single focus pointer). Anchored on the durable tmux
|
|
172
172
|
* `%pane_id`; `session` is a derived cache reconciled from the pane. `node_id`
|
|
173
173
|
* is UNIQUE — a node occupies at most one focus (Q5). */
|
|
174
174
|
export interface FocusRow {
|
|
@@ -130,8 +130,8 @@ export function closeNode(rootId) {
|
|
|
130
130
|
transition(id, 'cancel');
|
|
131
131
|
// 2) Tear the node off its placement (pane-keyed): close any focus row it
|
|
132
132
|
// occupies, kill its PANE (the window closes once its last pane goes, so
|
|
133
|
-
// sibling nodes the user co-located in one window survive), null its
|
|
134
|
-
// LOCATION
|
|
133
|
+
// sibling nodes the user co-located in one window survive), and null its
|
|
134
|
+
// LOCATION (closing the focus row is the record — no pointer to clear).
|
|
135
135
|
tearDownNode(id);
|
|
136
136
|
// 3) Leave the resume notice AFTER the watcher is gone, so it survives.
|
|
137
137
|
appendInbox(id, {
|
|
@@ -16,12 +16,10 @@ import { join } from 'node:path';
|
|
|
16
16
|
import { getNode, setPresence, updateNode, setFocusOccupant, fullName } from '../canvas/index.js';
|
|
17
17
|
import { reportsDir } from '../canvas/paths.js';
|
|
18
18
|
import { pushFinal } from '../feed/feed.js';
|
|
19
|
-
import { spawnNode } from './nodes.js';
|
|
19
|
+
import { spawnNode, nodeSession } from './nodes.js';
|
|
20
20
|
import { buildLaunchSpec, buildPiArgv } from './launch.js';
|
|
21
|
-
import { piCommand, paneLocation, nodeSession } from './tmux.js';
|
|
22
21
|
import { FRONT_DOOR_ENV } from './front-door.js';
|
|
23
|
-
import {
|
|
24
|
-
import { focusOf, recycleFocusPane } from './placement.js';
|
|
22
|
+
import { focusOf, recycleFocusPane, piCommand, paneLocation } from './placement.js';
|
|
25
23
|
import { ensureDaemon } from '../../daemon/manage.js';
|
|
26
24
|
/** The agent's most recent surfaced message: the newest reports/*.md body with
|
|
27
25
|
* its YAML frontmatter stripped. Empty string when the node never reported. */
|
|
@@ -104,12 +102,9 @@ export async function demoteNode(nodeId, callerPane) {
|
|
|
104
102
|
if (f !== null) {
|
|
105
103
|
try {
|
|
106
104
|
setFocusOccupant(f.focus_id, root.node_id);
|
|
107
|
-
setFocus(root.node_id);
|
|
108
105
|
}
|
|
109
106
|
catch { /* best-effort */ }
|
|
110
107
|
}
|
|
111
|
-
else if (getFocus() === nodeId)
|
|
112
|
-
setFocus('');
|
|
113
108
|
const fresh = getNode(root.node_id);
|
|
114
109
|
const inv = buildPiArgv(fresh);
|
|
115
110
|
const env = { ...inv.env, CRTR_ROOT_SESSION: nodeSession(), [FRONT_DOOR_ENV]: '1' };
|
|
@@ -6,12 +6,14 @@ export declare const CANVAS_GOAL_CAPTURE_PATH: string;
|
|
|
6
6
|
export declare const CANVAS_PASSIVE_CONTEXT_PATH: string;
|
|
7
7
|
export declare const CANVAS_CONTEXT_INTRO_PATH: string;
|
|
8
8
|
export declare const CANVAS_COMMANDS_PATH: string;
|
|
9
|
+
export declare const CANVAS_RESUME_PATH: string;
|
|
9
10
|
/** The canvas extensions every node loads, in order: stophook (routing +
|
|
10
11
|
* telemetry + session-id capture), inbox-watcher (wake), nav (in-editor
|
|
11
12
|
* graph chrome), goal-capture (persist the first user message as the goal),
|
|
12
13
|
* passive-context (drain passive backlog as pre-text on the next message),
|
|
13
14
|
* context-intro (inject the <crtr-context> bearings block as its own session
|
|
14
|
-
* message, once per brand-new chat), commands (the /promote slash-command)
|
|
15
|
+
* message, once per brand-new chat), commands (the /promote slash-command),
|
|
16
|
+
* resume (the /resume-node whole-canvas picker → `crtr node focus`).
|
|
15
17
|
* All self-gate on CRTR_NODE_ID. goal-capture precedes passive-context so it
|
|
16
18
|
* reads the raw user text. */
|
|
17
19
|
export declare const CANVAS_EXTENSIONS: string[];
|