@crouton-kit/crouter 0.3.14 → 0.3.15
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/build-root.d.ts +8 -0
- package/dist/build-root.js +30 -0
- package/dist/builtin-personas/design/base.md +3 -7
- package/dist/builtin-personas/design/orchestrator.md +4 -3
- package/dist/builtin-personas/developer/base.md +3 -7
- package/dist/builtin-personas/developer/orchestrator.md +5 -4
- package/dist/builtin-personas/explore/base.md +3 -7
- package/dist/builtin-personas/explore/orchestrator.md +1 -5
- package/dist/builtin-personas/general/base.md +2 -4
- package/dist/builtin-personas/general/orchestrator.md +2 -4
- package/dist/builtin-personas/lifecycle/resident.md +2 -0
- package/dist/builtin-personas/lifecycle/terminal.md +6 -0
- package/dist/builtin-personas/orchestration-kernel.md +42 -3
- package/dist/builtin-personas/plan/base.md +3 -5
- package/dist/builtin-personas/plan/orchestrator.md +5 -4
- package/dist/builtin-personas/plan/reviewers/architecture-fit/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/code-smells/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/security/base.md +9 -0
- package/dist/builtin-personas/review/base.md +3 -5
- package/dist/builtin-personas/review/orchestrator.md +2 -6
- package/dist/builtin-personas/runtime-base.md +3 -19
- package/dist/builtin-personas/spec/base.md +3 -5
- package/dist/builtin-personas/spec/orchestrator.md +4 -3
- package/dist/builtin-personas/spine/has-manager.md +10 -0
- package/dist/builtin-personas/spine/no-manager.md +2 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +96 -0
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +1 -1
- package/dist/builtin-skills/skills/spec/SKILL.md +2 -2
- package/dist/cli.js +6 -29
- package/dist/commands/attention.js +76 -7
- package/dist/commands/canvas-prune.d.ts +2 -0
- package/dist/commands/canvas-prune.js +66 -0
- package/dist/commands/canvas.js +5 -8
- package/dist/commands/chord.d.ts +2 -0
- package/dist/commands/chord.js +143 -0
- package/dist/commands/daemon.js +8 -5
- package/dist/commands/dashboard.js +2 -0
- package/dist/commands/human/prompts.js +28 -27
- package/dist/commands/human/queue.js +30 -14
- package/dist/commands/human/shared.d.ts +26 -21
- package/dist/commands/human/shared.js +44 -66
- package/dist/commands/human.js +4 -14
- package/dist/commands/node.d.ts +11 -0
- package/dist/commands/node.js +221 -98
- package/dist/commands/pkg/market-inspect.js +6 -4
- package/dist/commands/pkg/market-manage.js +10 -6
- package/dist/commands/pkg/market.js +2 -4
- package/dist/commands/pkg/plugin-inspect.js +6 -4
- package/dist/commands/pkg/plugin-manage.js +12 -7
- package/dist/commands/pkg/plugin.js +2 -4
- package/dist/commands/pkg.js +0 -4
- package/dist/commands/push.js +178 -15
- package/dist/commands/revive.js +5 -3
- package/dist/commands/skill/author.js +6 -4
- package/dist/commands/skill/find.js +8 -5
- package/dist/commands/skill/read.js +2 -0
- package/dist/commands/skill/state.js +6 -4
- package/dist/commands/skill.js +0 -6
- package/dist/commands/sys/config.js +21 -7
- package/dist/commands/sys/doctor.js +2 -0
- package/dist/commands/sys/update.js +4 -0
- package/dist/commands/sys.js +0 -6
- package/dist/commands/tmux-spread.d.ts +2 -0
- package/dist/commands/tmux-spread.js +130 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
- package/dist/core/__tests__/child-followup.test.d.ts +1 -0
- package/dist/core/__tests__/child-followup.test.js +83 -0
- package/dist/core/__tests__/close.test.d.ts +1 -0
- package/dist/core/__tests__/close.test.js +148 -0
- package/dist/core/__tests__/context-intro.test.d.ts +1 -0
- package/dist/core/__tests__/context-intro.test.js +196 -0
- package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-boot.test.js +93 -0
- package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-liveness.test.js +223 -0
- package/dist/core/__tests__/focuses.test.d.ts +1 -0
- package/dist/core/__tests__/focuses.test.js +259 -0
- package/dist/core/__tests__/fork.test.d.ts +1 -0
- package/dist/core/__tests__/fork.test.js +91 -0
- package/dist/core/__tests__/home-session.test.d.ts +1 -0
- package/dist/core/__tests__/home-session.test.js +153 -0
- package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
- package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
- package/dist/core/__tests__/keystone.test.d.ts +1 -0
- package/dist/core/__tests__/keystone.test.js +185 -0
- package/dist/core/__tests__/kickoff.test.d.ts +1 -0
- package/dist/core/__tests__/kickoff.test.js +89 -0
- package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/lifecycle.test.js +178 -0
- package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
- package/dist/core/__tests__/listing-completeness.test.js +31 -0
- package/dist/core/__tests__/memory.test.d.ts +1 -0
- package/dist/core/__tests__/memory.test.js +152 -0
- package/dist/core/__tests__/migration.test.d.ts +1 -0
- package/dist/core/__tests__/migration.test.js +238 -0
- package/dist/core/__tests__/pane-column.test.d.ts +1 -0
- package/dist/core/__tests__/pane-column.test.js +153 -0
- package/dist/core/__tests__/passive-subscription.test.js +24 -1
- package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
- package/dist/core/__tests__/persona-compose.test.js +53 -0
- package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
- package/dist/core/__tests__/persona-subkind.test.js +62 -0
- package/dist/core/__tests__/persona.test.d.ts +1 -0
- package/dist/core/__tests__/persona.test.js +107 -0
- package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
- package/dist/core/__tests__/placement-focus.test.js +244 -0
- package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
- package/dist/core/__tests__/placement-reconcile.test.js +212 -0
- package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
- package/dist/core/__tests__/placement-revive.test.js +238 -0
- package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/placement-teardown.test.js +183 -0
- package/dist/core/__tests__/prune.test.d.ts +1 -0
- package/dist/core/__tests__/prune.test.js +116 -0
- package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
- package/dist/core/__tests__/push-final-guard.test.js +71 -0
- package/dist/core/__tests__/relaunch.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch.test.js +328 -0
- package/dist/core/__tests__/reset.test.js +26 -7
- package/dist/core/__tests__/revive.test.d.ts +1 -0
- package/dist/core/__tests__/revive.test.js +217 -0
- package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
- package/dist/core/__tests__/spawn-root.test.js +73 -0
- package/dist/core/__tests__/steer-note.test.d.ts +1 -0
- package/dist/core/__tests__/steer-note.test.js +39 -0
- package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
- package/dist/core/__tests__/stop-guard.test.js +82 -0
- package/dist/core/__tests__/subcommand-tier.test.js +35 -33
- package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/tmux-surface.test.js +106 -0
- package/dist/core/__tests__/unknown-path.test.js +8 -2
- package/dist/core/canvas/attention.d.ts +10 -0
- package/dist/core/canvas/attention.js +40 -0
- package/dist/core/canvas/canvas.d.ts +66 -7
- package/dist/core/canvas/canvas.js +209 -21
- package/dist/core/canvas/db.d.ts +8 -0
- package/dist/core/canvas/db.js +206 -4
- package/dist/core/canvas/focuses.d.ts +22 -0
- package/dist/core/canvas/focuses.js +80 -0
- package/dist/core/canvas/index.d.ts +3 -0
- package/dist/core/canvas/index.js +3 -0
- package/dist/core/canvas/labels.d.ts +27 -0
- package/dist/core/canvas/labels.js +36 -0
- package/dist/core/canvas/render.js +25 -10
- package/dist/core/canvas/telemetry.d.ts +14 -0
- package/dist/core/canvas/telemetry.js +35 -0
- package/dist/core/canvas/types.d.ts +115 -12
- package/dist/core/command.d.ts +25 -1
- package/dist/core/command.js +23 -15
- package/dist/core/config.js +36 -2
- package/dist/core/feed/feed.js +3 -3
- package/dist/core/feed/inbox.d.ts +3 -1
- package/dist/core/feed/inbox.js +45 -5
- package/dist/core/feed/passive.js +24 -11
- package/dist/core/help.d.ts +26 -13
- package/dist/core/help.js +44 -37
- package/dist/core/personas/index.d.ts +1 -1
- package/dist/core/personas/index.js +1 -1
- package/dist/core/personas/loader.d.ts +40 -1
- package/dist/core/personas/loader.js +63 -1
- package/dist/core/personas/resolve.d.ts +13 -6
- package/dist/core/personas/resolve.js +46 -34
- package/dist/core/runtime/bearings.d.ts +20 -0
- package/dist/core/runtime/bearings.js +92 -0
- package/dist/core/runtime/close.d.ts +14 -0
- package/dist/core/runtime/close.js +151 -0
- package/dist/core/runtime/demote.js +27 -10
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/kickoff.d.ts +23 -6
- package/dist/core/runtime/kickoff.js +92 -36
- package/dist/core/runtime/launch.d.ts +24 -12
- package/dist/core/runtime/launch.js +75 -19
- package/dist/core/runtime/lifecycle.d.ts +13 -0
- package/dist/core/runtime/lifecycle.js +86 -0
- package/dist/core/runtime/memory.d.ts +43 -0
- package/dist/core/runtime/memory.js +165 -0
- package/dist/core/runtime/naming.d.ts +22 -0
- package/dist/core/runtime/naming.js +166 -0
- package/dist/core/runtime/nodes.d.ts +32 -1
- package/dist/core/runtime/nodes.js +60 -10
- package/dist/core/runtime/persona.d.ts +25 -0
- package/dist/core/runtime/persona.js +139 -0
- package/dist/core/runtime/placement.d.ts +287 -0
- package/dist/core/runtime/placement.js +663 -0
- package/dist/core/runtime/presence.d.ts +7 -15
- package/dist/core/runtime/presence.js +90 -66
- package/dist/core/runtime/promote.d.ts +14 -7
- package/dist/core/runtime/promote.js +57 -67
- package/dist/core/runtime/reset.d.ts +47 -4
- package/dist/core/runtime/reset.js +223 -52
- package/dist/core/runtime/revive.d.ts +26 -2
- package/dist/core/runtime/revive.js +166 -39
- package/dist/core/runtime/spawn.d.ts +20 -5
- package/dist/core/runtime/spawn.js +163 -43
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +18 -8
- package/dist/core/runtime/tmux.d.ts +100 -14
- package/dist/core/runtime/tmux.js +201 -28
- package/dist/core/spawn.js +15 -0
- package/dist/daemon/crtrd.d.ts +12 -1
- package/dist/daemon/crtrd.js +152 -34
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
- package/dist/pi-extensions/canvas-commands.js +16 -13
- package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
- package/dist/pi-extensions/canvas-context-intro.js +164 -0
- package/dist/pi-extensions/canvas-goal-capture.d.ts +3 -0
- package/dist/pi-extensions/canvas-goal-capture.js +15 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
- package/dist/pi-extensions/canvas-nav.d.ts +12 -4
- package/dist/pi-extensions/canvas-nav.js +586 -262
- package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
- package/dist/pi-extensions/canvas-stophook.js +344 -228
- package/dist/types.d.ts +28 -0
- package/dist/types.js +16 -0
- package/package.json +1 -1
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { NodeMeta } from '../canvas/index.js';
|
|
2
|
-
/** Persist `nodeId` as the currently focused node. Best-effort; never throws.
|
|
2
|
+
/** Persist `nodeId` as the currently focused node. Best-effort; never throws.
|
|
3
|
+
* Also maintains the transitional focus.ptr↔focuses-table dual-write bridge
|
|
4
|
+
* (see below) so Step 6 can flip reads to the table with no data gap. */
|
|
3
5
|
export declare function setFocus(nodeId: string): void;
|
|
4
|
-
/** Read the currently focused node id, or null if
|
|
5
|
-
*
|
|
6
|
+
/** Read the currently focused node id, or null if there is no active focus.
|
|
7
|
+
* Reads `focus.ptr` first; FALLS BACK to the canonical focuses row (the bridge,
|
|
8
|
+
* below) when the pointer is absent/empty — so a reader sees the same focus
|
|
9
|
+
* whichever store a writer reached. Best-effort; never throws. */
|
|
6
10
|
export declare function getFocus(): string | null;
|
|
7
11
|
/** True when the node's tmux window is alive. A falsy tmux_session/window
|
|
8
12
|
* always returns false so callers don't need to null-guard. */
|
|
@@ -24,15 +28,3 @@ export declare function focusNode(nodeId: string): {
|
|
|
24
28
|
focused: boolean;
|
|
25
29
|
session: string | null;
|
|
26
30
|
};
|
|
27
|
-
/** Focus a node IN PLACE: bring its pane into the caller's current pane slot
|
|
28
|
-
* (swap-pane) instead of navigating the client to the node's own window. This
|
|
29
|
-
* is the default for `crtr node focus` and the nav-chrome spine jump — the
|
|
30
|
-
* agent appears where you are.
|
|
31
|
-
*
|
|
32
|
-
* Falls back to window focus when there is no caller pane (not inside tmux) or
|
|
33
|
-
* the target pane can't be resolved. `inPlace` reports which path ran. */
|
|
34
|
-
export declare function focusNodeInPlace(nodeId: string, callerPane?: string, callerNodeId?: string): {
|
|
35
|
-
focused: boolean;
|
|
36
|
-
session: string | null;
|
|
37
|
-
inPlace: boolean;
|
|
38
|
-
};
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
19
19
|
import { dirname } from 'node:path';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
|
-
import { crtrHome, getNode,
|
|
22
|
-
import { selectWindow, switchClient, windowAlive, currentTmux,
|
|
21
|
+
import { crtrHome, getNode, getRow, openFocusRow, closeFocusRow, getFocusById, getFocusByNode, } from '../canvas/index.js';
|
|
22
|
+
import { selectWindow, switchClient, windowAlive, currentTmux, inTmux } from './tmux.js';
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
// Focus pointer
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
@@ -27,7 +27,9 @@ import { selectWindow, switchClient, windowAlive, currentTmux, paneOfWindow, swa
|
|
|
27
27
|
function focusPtrPath() {
|
|
28
28
|
return join(crtrHome(), 'focus.ptr');
|
|
29
29
|
}
|
|
30
|
-
/** Persist `nodeId` as the currently focused node. Best-effort; never throws.
|
|
30
|
+
/** Persist `nodeId` as the currently focused node. Best-effort; never throws.
|
|
31
|
+
* Also maintains the transitional focus.ptr↔focuses-table dual-write bridge
|
|
32
|
+
* (see below) so Step 6 can flip reads to the table with no data gap. */
|
|
31
33
|
export function setFocus(nodeId) {
|
|
32
34
|
try {
|
|
33
35
|
const p = focusPtrPath();
|
|
@@ -37,19 +39,101 @@ export function setFocus(nodeId) {
|
|
|
37
39
|
catch {
|
|
38
40
|
/* focus pointer is best-effort; never surface */
|
|
39
41
|
}
|
|
42
|
+
syncBridgeFocusRow(nodeId); // Step-4 dual-write bridge (REMOVED in Step 8)
|
|
40
43
|
}
|
|
41
|
-
/** Read the currently focused node id, or null if
|
|
42
|
-
*
|
|
44
|
+
/** Read the currently focused node id, or null if there is no active focus.
|
|
45
|
+
* Reads `focus.ptr` first; FALLS BACK to the canonical focuses row (the bridge,
|
|
46
|
+
* below) when the pointer is absent/empty — so a reader sees the same focus
|
|
47
|
+
* whichever store a writer reached. Best-effort; never throws. */
|
|
43
48
|
export function getFocus() {
|
|
44
49
|
try {
|
|
45
50
|
const raw = readFileSync(focusPtrPath(), 'utf8').trim();
|
|
46
|
-
|
|
51
|
+
if (raw !== '')
|
|
52
|
+
return raw;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* pointer absent — fall through to the table */
|
|
56
|
+
}
|
|
57
|
+
// Bridge fallback: the canonical focus row's occupant (Step-8 removal).
|
|
58
|
+
try {
|
|
59
|
+
return getFocusById(BRIDGE_FOCUS_ID)?.node_id ?? null;
|
|
47
60
|
}
|
|
48
61
|
catch {
|
|
49
62
|
return null;
|
|
50
63
|
}
|
|
51
64
|
}
|
|
52
65
|
// ---------------------------------------------------------------------------
|
|
66
|
+
// Transitional focus.ptr ↔ focuses-table dual-write bridge.
|
|
67
|
+
//
|
|
68
|
+
// THROWAWAY — DELETED IN STEP 8. Today `focus.ptr` owns the single "current"
|
|
69
|
+
// focus. Step 4 stands up the plural `focuses` table but nothing reads it as
|
|
70
|
+
// authority yet (that switch is Step 6). To populate it in lockstep WITHOUT a
|
|
71
|
+
// behavior change, every `setFocus` ALSO writes one canonical focus row that
|
|
72
|
+
// mirrors `focus.ptr`, and `getFocus` falls back to it. Step 6 replaces
|
|
73
|
+
// focusNodeInPlace with retargetFocus/openFocus, which write pane-correct focus
|
|
74
|
+
// rows directly — then this bridge (and focus.ptr) is removed.
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
/** The fixed focus_id of the one canonical row that mirrors `focus.ptr`. */
|
|
77
|
+
const BRIDGE_FOCUS_ID = '__focus_ptr__';
|
|
78
|
+
/** Best-effort pane/session for the canonical focus row. A bare `setFocus(id)`
|
|
79
|
+
* only carries a node id, but a focus row wants pane+session. Resolve them
|
|
80
|
+
* READ-ONLY from the node's already-stored LOCATION (`row.pane`/`tmux_session`),
|
|
81
|
+
* else from the caller's current tmux pane (`currentTmux`).
|
|
82
|
+
*
|
|
83
|
+
* DELIBERATE DEVIATION from the design's "run reconcile(nodeId) first": reconcile
|
|
84
|
+
* WRITES node presence via setPresence, and `setFocus` has many non-focus callers
|
|
85
|
+
* (reset/close/demote/tmux-spread). Reconciling on every setFocus would mutate
|
|
86
|
+
* their nodes' LOCATION as an invisible side-effect of a dual-write that is
|
|
87
|
+
* supposed to change NOTHING this step. So the bridge reads, never reconciles;
|
|
88
|
+
* best-effort is fine THIS step (nothing reads the row as authority until Step 6,
|
|
89
|
+
* which replaces these writers with pane-correct retargetFocus/openFocus). */
|
|
90
|
+
function resolveBridgePaneSession(nodeId) {
|
|
91
|
+
try {
|
|
92
|
+
const row = getRow(nodeId);
|
|
93
|
+
if (row?.pane != null && row.pane !== '') {
|
|
94
|
+
return { pane: row.pane, session: row.tmux_session ?? null };
|
|
95
|
+
}
|
|
96
|
+
if (inTmux()) {
|
|
97
|
+
const cur = currentTmux();
|
|
98
|
+
if (cur)
|
|
99
|
+
return { pane: cur.pane, session: cur.session };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
/* best-effort */
|
|
104
|
+
}
|
|
105
|
+
return { pane: null, session: null };
|
|
106
|
+
}
|
|
107
|
+
/** Mirror the current focus into the single canonical focuses row. `''` closes
|
|
108
|
+
* it (focus cleared). Otherwise re-point the row at `nodeId`: drop the prior
|
|
109
|
+
* canonical row and any row already holding `nodeId` (UNIQUE(node_id) safety)
|
|
110
|
+
* before re-inserting. All best-effort — a failure here must never break a
|
|
111
|
+
* setFocus caller or the build. */
|
|
112
|
+
function syncBridgeFocusRow(nodeId) {
|
|
113
|
+
try {
|
|
114
|
+
if (nodeId === '') {
|
|
115
|
+
closeFocusRow(BRIDGE_FOCUS_ID);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Step 6: retargetFocus/openFocus now write REAL (pane-correct) focus rows.
|
|
119
|
+
// If one already shows this node, the table is already authoritative —
|
|
120
|
+
// focus.ptr (the file, written above) names the node and getFocus's fallback
|
|
121
|
+
// reads the real row. Drop any stale bridge row and PIGGYBACK on the real
|
|
122
|
+
// one; never duplicate-insert (UNIQUE node_id) or clobber it.
|
|
123
|
+
const real = getFocusByNode(nodeId);
|
|
124
|
+
if (real !== null && real.focus_id !== BRIDGE_FOCUS_ID) {
|
|
125
|
+
closeFocusRow(BRIDGE_FOCUS_ID);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const { pane, session } = resolveBridgePaneSession(nodeId);
|
|
129
|
+
closeFocusRow(BRIDGE_FOCUS_ID);
|
|
130
|
+
openFocusRow(BRIDGE_FOCUS_ID, pane, session, nodeId);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
/* dual-write is best-effort; never surface */
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
53
137
|
// Liveness
|
|
54
138
|
// ---------------------------------------------------------------------------
|
|
55
139
|
/** True when the node's tmux window is alive. A falsy tmux_session/window
|
|
@@ -92,63 +176,3 @@ export function focusNode(nodeId) {
|
|
|
92
176
|
const windowOk = selectWindow(session, window);
|
|
93
177
|
return { focused: clientOk && windowOk, session };
|
|
94
178
|
}
|
|
95
|
-
/** Focus a node IN PLACE: bring its pane into the caller's current pane slot
|
|
96
|
-
* (swap-pane) instead of navigating the client to the node's own window. This
|
|
97
|
-
* is the default for `crtr node focus` and the nav-chrome spine jump — the
|
|
98
|
-
* agent appears where you are.
|
|
99
|
-
*
|
|
100
|
-
* Falls back to window focus when there is no caller pane (not inside tmux) or
|
|
101
|
-
* the target pane can't be resolved. `inPlace` reports which path ran. */
|
|
102
|
-
export function focusNodeInPlace(nodeId, callerPane, callerNodeId) {
|
|
103
|
-
const meta = getNode(nodeId);
|
|
104
|
-
// Always write the pointer so the dashboard reflects intent even on failure.
|
|
105
|
-
setFocus(nodeId);
|
|
106
|
-
if (meta === null || !nodeLive(meta)) {
|
|
107
|
-
return { focused: false, session: meta?.tmux_session ?? null, inPlace: false };
|
|
108
|
-
}
|
|
109
|
-
const session = meta.tmux_session;
|
|
110
|
-
const window = meta.window;
|
|
111
|
-
const pane = callerPane ?? process.env['TMUX_PANE'] ?? currentTmux()?.pane;
|
|
112
|
-
// No caller pane (not in tmux) — best we can do is bring the window forefront.
|
|
113
|
-
if (pane === undefined || pane === '') {
|
|
114
|
-
const ok = switchClient(session) && selectWindow(session, window);
|
|
115
|
-
return { focused: ok, session, inPlace: false };
|
|
116
|
-
}
|
|
117
|
-
const targetPane = paneOfWindow(session, window);
|
|
118
|
-
if (targetPane === null) {
|
|
119
|
-
const ok = switchClient(session) && selectWindow(session, window);
|
|
120
|
-
return { focused: ok, session, inPlace: false };
|
|
121
|
-
}
|
|
122
|
-
if (targetPane === pane)
|
|
123
|
-
return { focused: true, session, inPlace: true }; // already here
|
|
124
|
-
// The window the caller's pane currently sits in — the slot the target's pane
|
|
125
|
-
// is about to be swapped INTO.
|
|
126
|
-
const callerWindow = windowOfPane(pane);
|
|
127
|
-
const ok = swapPaneInPlace(targetPane, pane);
|
|
128
|
-
// Keep the canvas window mapping in sync with the physical swap. swap-pane
|
|
129
|
-
// exchanges the two PANES between their windows (pane ids are stable, windows
|
|
130
|
-
// are slots): after the swap the target's pane occupies the caller's window
|
|
131
|
-
// and the caller's pane occupies the target's old window. Without this update
|
|
132
|
-
// meta.window goes stale, and a later paneOfWindow(session, meta.window)
|
|
133
|
-
// resolves the WRONG pane — the bug that made focusing back to a manager a
|
|
134
|
-
// no-op (it kept resolving the pane already in view) and made a focused node's
|
|
135
|
-
// exit collapse the visible window instead of its background one.
|
|
136
|
-
if (ok && callerWindow !== null && callerWindow !== window) {
|
|
137
|
-
try {
|
|
138
|
-
updateNode(nodeId, { window: callerWindow });
|
|
139
|
-
}
|
|
140
|
-
catch { /* best-effort */ }
|
|
141
|
-
// The caller is the node running this focus (its pi process owns callerPane).
|
|
142
|
-
// Its pane moved to the target's old window, so re-point its window there.
|
|
143
|
-
// Prefer an explicit id (the `node cycle` tmux binding runs outside any pi,
|
|
144
|
-
// so CRTR_NODE_ID is unset there) and fall back to the env for `node focus`.
|
|
145
|
-
const cnid = callerNodeId ?? process.env['CRTR_NODE_ID'];
|
|
146
|
-
if (cnid !== undefined && cnid.trim() !== '' && cnid !== nodeId) {
|
|
147
|
-
try {
|
|
148
|
-
updateNode(cnid, { window });
|
|
149
|
-
}
|
|
150
|
-
catch { /* best-effort */ }
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return { focused: ok, session, inPlace: true };
|
|
154
|
-
}
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { type NodeMeta } from '../canvas/index.js';
|
|
2
2
|
export interface PromoteResult {
|
|
3
3
|
meta: NodeMeta;
|
|
4
|
-
/** Orchestration guidance to surface into the node's current context now. */
|
|
5
|
-
guidance: string;
|
|
6
4
|
roadmapWritten: boolean;
|
|
7
5
|
/** Absolute path to the node's roadmap doc (context/roadmap.md). */
|
|
8
6
|
roadmapPath: string;
|
|
9
7
|
/** Absolute path to the node's goal doc (context/initial-prompt.md). */
|
|
10
8
|
goalPath: string;
|
|
9
|
+
/** Absolute path to the node-local memory index (context/memory/MEMORY.md). */
|
|
10
|
+
memoryPath: string;
|
|
11
|
+
/** Absolute path to the user-global memory index (<crtrHome>/memory/MEMORY.md). */
|
|
12
|
+
userMemoryPath: string;
|
|
13
|
+
/** Absolute path to the project memory index (<crtrHome>/projects/<key>/memory/MEMORY.md). */
|
|
14
|
+
projectMemoryPath: string;
|
|
11
15
|
}
|
|
12
|
-
/** Promote a node to
|
|
13
|
-
* (e.g. a `general` worker becoming a
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* is
|
|
16
|
+
/** Promote a node to an orchestrator (mode→orchestrator), optionally
|
|
17
|
+
* specializing its kind (e.g. a `general` worker becoming a
|
|
18
|
+
* `developer.orchestrator`) and optionally also making it resident. Idempotent:
|
|
19
|
+
* re-promoting just rewrites the spec. Seeds a roadmap SCAFFOLD if absent (a
|
|
20
|
+
* boss with no map is a failure mode) — no goal is forced here; authoring the
|
|
21
|
+
* goal + roadmap is the node's next act. The transition guidance is injected
|
|
22
|
+
* centrally by the persona injector at the next turn boundary, not returned. */
|
|
17
23
|
export declare function promote(nodeId: string, opts?: {
|
|
18
24
|
kind?: string;
|
|
25
|
+
resident?: boolean;
|
|
19
26
|
}): PromoteResult;
|
|
20
27
|
export interface YieldResult {
|
|
21
28
|
meta: NodeMeta;
|
|
@@ -1,71 +1,36 @@
|
|
|
1
|
-
// Promotion —
|
|
1
|
+
// Promotion — the worker→orchestrator polymorph (mode→orchestrator).
|
|
2
2
|
//
|
|
3
3
|
// Two stages (the pi-mode-switch pattern):
|
|
4
|
-
// 1. Promotion →
|
|
5
|
-
// node's mode
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// 1. Promotion → mode flips to orchestrator (mid-turn). This call flips the
|
|
5
|
+
// node's mode and (optionally) its KIND, REWRITES its launch spec to that
|
|
6
|
+
// kind's orchestrator persona (so the next revive comes back as that
|
|
7
|
+
// orchestrator), and seeds a roadmap scaffold + the three memory stores.
|
|
8
|
+
// The transition guidance the node needs is injected CENTRALLY by the
|
|
9
|
+
// persona injector (runtime/persona.ts) at the turn boundary — promote()
|
|
10
|
+
// itself no longer returns or hand-emits guidance.
|
|
10
11
|
// 2. Refresh → persona swap (permanent). On the next fresh revive the node
|
|
11
12
|
// starts with the orchestrator system prompt baked in (because the launch
|
|
12
|
-
// spec now says orchestrator). The guidance
|
|
13
|
+
// spec now says orchestrator). The injected guidance bridges until then.
|
|
14
|
+
//
|
|
15
|
+
// Mode and lifecycle are ORTHOGONAL: promotion flips mode only. Lifecycle stays
|
|
16
|
+
// whatever it was (a promoted child is terminal/orchestrator — still reports up
|
|
17
|
+
// + reaps) unless the caller passes `resident:true` to also make it resident.
|
|
13
18
|
//
|
|
14
19
|
// Trigger is persistence-need (deliberate, or a refresh-yield with open work),
|
|
15
20
|
// never the mere act of spawning a child.
|
|
16
|
-
import { getNode, updateNode
|
|
21
|
+
import { getNode, updateNode } from '../canvas/index.js';
|
|
22
|
+
import { transition } from './lifecycle.js';
|
|
17
23
|
import { buildLaunchSpec } from './launch.js';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { readText } from '../fs-utils.js';
|
|
21
|
-
import { parseFrontmatter } from '../frontmatter.js';
|
|
22
|
-
import { hasRoadmap, seedRoadmap, readRoadmap, roadmapPath } from './roadmap.js';
|
|
24
|
+
import { hasRoadmap, seedRoadmap, roadmapPath } from './roadmap.js';
|
|
25
|
+
import { seedMemory, memoryPath, seedUserMemory, userMemoryPath, seedProjectMemory, projectMemoryPath, } from './memory.js';
|
|
23
26
|
import { readGoal, goalPath } from './kickoff.js';
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
catch {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
/** Build the mid-turn guidance dump, specialized to the node's (possibly
|
|
36
|
-
* just-chosen) kind: the shared kernel + that kind's roadmap-shaping skill
|
|
37
|
-
* (auto-loaded now, before the persona swap bakes in on revive) + the roadmap
|
|
38
|
-
* scaffold the node must author. No goal is assumed — writing it is step one. */
|
|
39
|
-
function orchestrationGuidance(nodeId, kind) {
|
|
40
|
-
const kernel = loadKernel();
|
|
41
|
-
const orch = loadPersona(kind, 'orchestrator');
|
|
42
|
-
const roadmapSkill = typeof orch?.frontmatter?.['roadmapSkill'] === 'string'
|
|
43
|
-
? orch.frontmatter['roadmapSkill']
|
|
44
|
-
: undefined;
|
|
45
|
-
const skillBody = roadmapSkill ? loadSkillBody(roadmapSkill) : null;
|
|
46
|
-
const roadmap = readRoadmap(nodeId) ?? '(no roadmap yet)';
|
|
47
|
-
const rmPath = roadmapPath(nodeId);
|
|
48
|
-
const goal = readGoal(nodeId);
|
|
49
|
-
const parts = [
|
|
50
|
-
`You are now a RESIDENT ${kind.toUpperCase()} ORCHESTRATOR. Your scarce resource is your own context window.`,
|
|
51
|
-
'Your job is to manage context and delegate — not to do the goal yourself.',
|
|
52
|
-
'',
|
|
53
|
-
kernel,
|
|
54
|
-
];
|
|
55
|
-
if (goal !== null && goal.trim() !== '') {
|
|
56
|
-
parts.push('', `--- Your goal (${goalPath(nodeId)}) ---`, '', goal.trim());
|
|
57
|
-
}
|
|
58
|
-
if (skillBody) {
|
|
59
|
-
parts.push('', `--- How to shape a ${kind} roadmap (skill: ${roadmapSkill}) ---`, '', skillBody);
|
|
60
|
-
}
|
|
61
|
-
parts.push('', `Your roadmap scaffold (\`${rmPath}\`) — author it now: state the goal, exit criteria, and the phase skeleton, using the approach above. Current contents:`, '', roadmap, '', 'Then delegate each phase with `crtr node new --kind <kind>`. When your context fills, run `crtr node yield` to refresh against this roadmap.');
|
|
62
|
-
return parts.join('\n');
|
|
63
|
-
}
|
|
64
|
-
/** Promote a node to resident orchestrator, optionally specializing its kind
|
|
65
|
-
* (e.g. a `general` worker becoming a `developer.orchestrator`). Idempotent:
|
|
66
|
-
* re-promoting just rewrites the spec + returns fresh guidance. Seeds a
|
|
67
|
-
* roadmap SCAFFOLD if absent (a boss with no map is a failure mode) — no goal
|
|
68
|
-
* is forced here; authoring the goal + roadmap is the node's next act. */
|
|
27
|
+
/** Promote a node to an orchestrator (mode→orchestrator), optionally
|
|
28
|
+
* specializing its kind (e.g. a `general` worker becoming a
|
|
29
|
+
* `developer.orchestrator`) and optionally also making it resident. Idempotent:
|
|
30
|
+
* re-promoting just rewrites the spec. Seeds a roadmap SCAFFOLD if absent (a
|
|
31
|
+
* boss with no map is a failure mode) — no goal is forced here; authoring the
|
|
32
|
+
* goal + roadmap is the node's next act. The transition guidance is injected
|
|
33
|
+
* centrally by the persona injector at the next turn boundary, not returned. */
|
|
69
34
|
export function promote(nodeId, opts = {}) {
|
|
70
35
|
const node = getNode(nodeId);
|
|
71
36
|
if (node === null)
|
|
@@ -76,7 +41,13 @@ export function promote(nodeId, opts = {}) {
|
|
|
76
41
|
// *next* revive comes back orchestrating in that kind (polymorph stage 2).
|
|
77
42
|
// nodeEnv reads meta.{kind,mode}, so CRTR_KIND/CRTR_MODE flip immediately for
|
|
78
43
|
// the live process's children too.
|
|
79
|
-
|
|
44
|
+
// Bake the node's post-promote lifecycle + spine into the rebuilt prompt:
|
|
45
|
+
// lifecycle becomes resident only when the caller asked (else it keeps its
|
|
46
|
+
// current value); spine is fixed by parent-ness (immutable).
|
|
47
|
+
const { launch } = buildLaunchSpec(targetKind, 'orchestrator', {
|
|
48
|
+
lifecycle: opts.resident === true ? 'resident' : node.lifecycle,
|
|
49
|
+
hasManager: node.parent !== null,
|
|
50
|
+
});
|
|
80
51
|
// Seed a barebones roadmap scaffold if absent so the file exists for a
|
|
81
52
|
// refresh. Pre-fill its Goal from the node's goal doc when present (set at
|
|
82
53
|
// spawn, or captured from the first user message); the node fleshes out the
|
|
@@ -87,13 +58,30 @@ export function promote(nodeId, opts = {}) {
|
|
|
87
58
|
seedRoadmap(nodeId, goal !== null && goal.trim() !== '' ? { goal: goal.trim() } : {});
|
|
88
59
|
roadmapWritten = true;
|
|
89
60
|
}
|
|
90
|
-
|
|
61
|
+
// Seed all three scoped memory stores alongside the roadmap — user-global,
|
|
62
|
+
// project (keyed off this node's cwd), and node-local. Each is a durable,
|
|
63
|
+
// refresh-surviving artifact; each guarded so a re-seed never clobbers an
|
|
64
|
+
// evolved memory.
|
|
65
|
+
seedUserMemory();
|
|
66
|
+
seedProjectMemory(node.cwd);
|
|
67
|
+
seedMemory(nodeId);
|
|
68
|
+
// Flip mode→orchestrator + kind + launch spec. Lifecycle is independent:
|
|
69
|
+
// only set resident when the caller asked for it (the common self-promotion
|
|
70
|
+
// stays terminal/orchestrator — it still reports up + reaps).
|
|
71
|
+
const meta = updateNode(nodeId, {
|
|
72
|
+
kind: targetKind,
|
|
73
|
+
mode: 'orchestrator',
|
|
74
|
+
launch,
|
|
75
|
+
...(opts.resident === true ? { lifecycle: 'resident' } : {}),
|
|
76
|
+
});
|
|
91
77
|
return {
|
|
92
78
|
meta,
|
|
93
|
-
guidance: orchestrationGuidance(nodeId, targetKind),
|
|
94
79
|
roadmapWritten,
|
|
95
80
|
roadmapPath: roadmapPath(nodeId),
|
|
96
81
|
goalPath: goalPath(nodeId),
|
|
82
|
+
memoryPath: memoryPath(nodeId),
|
|
83
|
+
userMemoryPath: userMemoryPath(),
|
|
84
|
+
projectMemoryPath: projectMemoryPath(node.cwd),
|
|
97
85
|
};
|
|
98
86
|
}
|
|
99
87
|
/** Request a refresh-yield: discard in-memory context and revive fresh against
|
|
@@ -107,14 +95,16 @@ export function requestYield(nodeId, opts = {}) {
|
|
|
107
95
|
if (node === null)
|
|
108
96
|
throw new Error(`unknown node: ${nodeId}`);
|
|
109
97
|
let promoted = false;
|
|
110
|
-
if (node.
|
|
111
|
-
//
|
|
112
|
-
// (
|
|
98
|
+
if (node.mode !== 'orchestrator') {
|
|
99
|
+
// A yield needs a ROADMAP to refresh against — i.e. orchestrator mode, not
|
|
100
|
+
// resident lifecycle. Ensure orchestrator (which seeds the roadmap + memory)
|
|
101
|
+
// WITHOUT forcing resident: a terminal/orchestrator yields fine, since the
|
|
102
|
+
// daemon's refresh-revive keys on intent='refresh', not lifecycle.
|
|
113
103
|
promote(nodeId, opts.kind !== undefined ? { kind: opts.kind } : {});
|
|
114
104
|
promoted = true;
|
|
115
105
|
}
|
|
116
106
|
// Mark the intent; the stophook enacts the shutdown, the daemon the revive.
|
|
117
|
-
|
|
118
|
-
|
|
107
|
+
transition(nodeId, 'yield');
|
|
108
|
+
const meta = getNode(nodeId);
|
|
119
109
|
return { meta, promoted, willRefresh: true };
|
|
120
110
|
}
|
|
@@ -1,13 +1,56 @@
|
|
|
1
|
+
/** Reap the descendant sub-DAG of `rootId`: mark each **done** (the user moved
|
|
2
|
+
* on — a clean teardown, NOT a fault) + clear intent FIRST, then kill its
|
|
3
|
+
* window (closes the daemon revive race). Edges are LEFT INTACT — descendants
|
|
4
|
+
* keep parent=rootId. No wipe. Returns the reaped ids.
|
|
5
|
+
*
|
|
6
|
+
* Why `done`, and why marking is STILL explicit: a `closeWindow`/`respawn-pane
|
|
7
|
+
* -k` kill is abrupt and fires NO clean `session_shutdown`, so the general
|
|
8
|
+
* quit→done rule does NOT auto-resolve a force-killed descendant — we mark it
|
|
9
|
+
* `done` here. Shared by relaunchRoot (option C) and resetRoot's in-place
|
|
10
|
+
* fallback, so both leave their descendants `done`. */
|
|
11
|
+
export declare function reapDescendants(rootId: string): string[];
|
|
1
12
|
export interface ResetRootResult {
|
|
2
|
-
/** Descendant node ids torn down (window killed + marked
|
|
13
|
+
/** Descendant node ids torn down (window killed + marked done). */
|
|
3
14
|
reaped: string[];
|
|
4
15
|
/** Direct subscriptions dropped off the root. */
|
|
5
16
|
detached: string[];
|
|
6
17
|
/** True when the node was a root and a full reset ran. */
|
|
7
18
|
reset: boolean;
|
|
8
19
|
}
|
|
9
|
-
/** Reset a root node to a pristine, empty graph (the `/new` semantics
|
|
20
|
+
/** Reset a root node to a pristine, empty graph (the legacy `/new` semantics —
|
|
21
|
+
* now used as the no-pane fallback and the non-root session-id refresh).
|
|
10
22
|
*
|
|
11
23
|
* For a non-root (spawned child), a `/new` is not a graph reset — we only
|
|
12
|
-
* refresh its session id so a later `--
|
|
13
|
-
export declare function resetRoot(nodeId: string, newSessionId?: string): ResetRootResult;
|
|
24
|
+
* refresh its session id so a later `--session <id>` wakes the right conversation. */
|
|
25
|
+
export declare function resetRoot(nodeId: string, newSessionId?: string, newSessionFile?: string | null): ResetRootResult;
|
|
26
|
+
/** Injectable respawn seam — tests pass a double since tmux isn't available. */
|
|
27
|
+
export interface RelaunchDeps {
|
|
28
|
+
relaunchRootInPane?: (nodeId: string, pane: string) => void;
|
|
29
|
+
}
|
|
30
|
+
export type HandleNewSessionPath = 'relaunch' | 'reset-root' | 'reset-child' | 'noop';
|
|
31
|
+
export interface HandleNewSessionResult {
|
|
32
|
+
path: HandleNewSessionPath;
|
|
33
|
+
newNodeId?: string;
|
|
34
|
+
}
|
|
35
|
+
/** The single entry the stophook calls on a detected `/new` (session id change).
|
|
36
|
+
* Policy lives here so the stophook stays thin and this stays unit-testable:
|
|
37
|
+
* - non-root child → resetRoot(nodeId, newSessionId) (session-id refresh only)
|
|
38
|
+
* - root + pane present → relaunchRoot(nodeId, pane) (option C)
|
|
39
|
+
* - root + no pane (no tmux) → resetRoot(nodeId, newSessionId) (in-place fallback)
|
|
40
|
+
* On a respawn-dispatch failure the live pi never died, so we degrade to the
|
|
41
|
+
* legacy in-place reset. */
|
|
42
|
+
export declare function handleNewSession(nodeId: string, newSessionId: string, pane: string | undefined, deps?: RelaunchDeps, newSessionFile?: string | null): HandleNewSessionResult;
|
|
43
|
+
/** Park the old root + create+launch a fresh root in `pane` (option C). All DB
|
|
44
|
+
* writes are synchronous and happen BEFORE the respawn (the respawn kills the
|
|
45
|
+
* caller). Returns the new node id, or null on a defensive guard (not a root /
|
|
46
|
+
* already parked). Throws only if the respawn dispatch fails — and self-rolls-
|
|
47
|
+
* back its writes first so the caller can degrade to resetRoot. */
|
|
48
|
+
export declare function relaunchRoot(oldId: string, pane: string, deps?: RelaunchDeps): {
|
|
49
|
+
newNodeId: string;
|
|
50
|
+
} | null;
|
|
51
|
+
/** Resolve a cleanly-exiting node to `done`. Returns true iff it transitioned.
|
|
52
|
+
* Guard: only a real quit, and only a node still active|idle with no pending
|
|
53
|
+
* intent — so it never clobbers a node already routed by agent_end to done
|
|
54
|
+
* (push final), refresh (yield), or idle-release. Pure/DB-only (no pi/tmux) so
|
|
55
|
+
* the guard is unit-testable without a live pi. */
|
|
56
|
+
export declare function markCleanExitDone(nodeId: string, reason: unknown): boolean;
|