@crouton-kit/crouter 0.3.40 → 0.3.42
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.js +1 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +472 -9
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +284 -76
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +31 -24
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +52 -15
- package/dist/clients/attach/attach-cmd.js +720 -747
- package/dist/clients/attach/chat-view.js +11 -0
- package/dist/clients/attach/view-socket.d.ts +8 -1
- package/dist/clients/attach/view-socket.js +15 -1
- package/dist/clients/web/server.js +5 -28
- package/dist/clients/web/web-cmd.js +1 -1
- package/dist/commands/__tests__/revive-now-gate.test.js +60 -0
- package/dist/commands/canvas-rebuild-index.js +5 -5
- package/dist/commands/memory/shared.d.ts +7 -3
- package/dist/commands/memory/shared.js +35 -5
- package/dist/commands/memory/write.js +5 -3
- package/dist/commands/node-snapshot.js +9 -1
- package/dist/commands/node.js +37 -17
- package/dist/commands/push.js +8 -0
- package/dist/commands/revive.d.ts +10 -0
- package/dist/commands/revive.js +28 -10
- package/dist/commands/sys/__tests__/setup-core.test.js +19 -0
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/setup-core.js +3 -2
- package/dist/commands/sys/setup.js +1 -1
- package/dist/commands/worktree.d.ts +2 -0
- package/dist/commands/worktree.js +94 -0
- package/dist/core/__tests__/boot.test.js +4 -4
- package/dist/core/__tests__/canvas.test.js +19 -7
- package/dist/core/__tests__/child-followup.test.js +15 -5
- package/dist/core/__tests__/daemon-boot.test.js +6 -1
- package/dist/core/__tests__/daemon-wedge.test.js +18 -1
- package/dist/core/__tests__/fault-classifier.test.js +30 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +20 -0
- package/dist/core/__tests__/full/spike-harness.test.js +8 -5
- package/dist/core/__tests__/grace-clock.test.js +18 -2
- package/dist/core/__tests__/host-teardown-process-group.test.js +465 -0
- package/dist/core/__tests__/prune-to-limit.test.js +14 -0
- package/dist/core/__tests__/review-model-floor.test.js +32 -0
- package/dist/core/__tests__/revive.test.js +18 -4
- package/dist/core/__tests__/session-cycles.test.js +77 -0
- package/dist/core/__tests__/worktree.test.js +85 -0
- package/dist/core/canvas/boot.js +12 -7
- package/dist/core/canvas/canvas.d.ts +27 -8
- package/dist/core/canvas/canvas.js +54 -26
- package/dist/core/canvas/db.js +14 -0
- package/dist/core/canvas/history.js +1 -0
- package/dist/core/canvas/paths.d.ts +10 -9
- package/dist/core/canvas/paths.js +10 -9
- package/dist/core/canvas/pid.d.ts +155 -1
- package/dist/core/canvas/pid.js +306 -1
- package/dist/core/canvas/status-glyph.d.ts +7 -0
- package/dist/core/canvas/status-glyph.js +10 -1
- package/dist/core/canvas/types.d.ts +34 -0
- package/dist/core/fault-classifier.js +5 -1
- package/dist/core/profiles/select.d.ts +4 -2
- package/dist/core/profiles/select.js +30 -4
- package/dist/core/runtime/bearings.js +4 -0
- package/dist/core/runtime/branded-host.d.ts +7 -0
- package/dist/core/runtime/branded-host.js +44 -17
- package/dist/core/runtime/broker-sdk.js +28 -68
- package/dist/core/runtime/broker.js +35 -4
- package/dist/core/runtime/host.d.ts +3 -3
- package/dist/core/runtime/host.js +148 -33
- package/dist/core/runtime/launch.d.ts +15 -15
- package/dist/core/runtime/launch.js +58 -4
- package/dist/core/runtime/naming.js +3 -2
- package/dist/core/runtime/nodes.d.ts +3 -1
- package/dist/core/runtime/nodes.js +1 -0
- package/dist/core/runtime/pi-cli.d.ts +2 -0
- package/dist/core/runtime/pi-cli.js +51 -0
- package/dist/core/runtime/placement.d.ts +1 -1
- package/dist/core/runtime/placement.js +5 -2
- package/dist/core/runtime/recap.js +2 -1
- package/dist/core/runtime/revive.d.ts +21 -15
- package/dist/core/runtime/revive.js +87 -52
- package/dist/core/runtime/session-cycles.d.ts +30 -0
- package/dist/core/runtime/session-cycles.js +77 -0
- package/dist/core/runtime/spawn.d.ts +4 -0
- package/dist/core/runtime/spawn.js +152 -100
- package/dist/core/runtime/tmux.d.ts +7 -10
- package/dist/core/runtime/tmux.js +9 -11
- package/dist/core/worktree.d.ts +35 -0
- package/dist/core/worktree.js +158 -0
- package/dist/daemon/crtrd.d.ts +17 -8
- package/dist/daemon/crtrd.js +191 -40
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +1 -0
- package/dist/pi-extensions/canvas-context-intro.js +34 -23
- package/dist/pi-extensions/canvas-stophook.js +11 -5
- package/dist/web-client/assets/{index-CbO8L0mN.js → index-B00YpRQ1.js} +20 -20
- package/dist/web-client/assets/index-DrkcvANq.css +2 -0
- package/dist/web-client/index.html +2 -2
- package/docs/compat/hearth-crtr-v1.md +191 -0
- package/docs/public-api.md +75 -0
- package/package.json +3 -4
- package/dist/core/__tests__/hearth-bootstrap.test.js +0 -136
- package/dist/core/hearth/__tests__/model-auth-guest.test.js +0 -151
- package/dist/core/hearth/config.d.ts +0 -3
- package/dist/core/hearth/config.js +0 -108
- package/dist/core/hearth/guest-env.d.ts +0 -9
- package/dist/core/hearth/guest-env.js +0 -27
- package/dist/core/hearth/index.d.ts +0 -4
- package/dist/core/hearth/index.js +0 -4
- package/dist/core/hearth/model-auth-guest.d.ts +0 -8
- package/dist/core/hearth/model-auth-guest.js +0 -430
- package/dist/core/hearth/provider.d.ts +0 -36
- package/dist/core/hearth/provider.js +0 -10
- package/dist/core/hearth/providers/__tests__/sweep-and-release.test.js +0 -362
- package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +0 -12
- package/dist/core/hearth/providers/blaxel-bootstrap.js +0 -147
- package/dist/core/hearth/providers/blaxel-home.d.ts +0 -60
- package/dist/core/hearth/providers/blaxel-home.js +0 -405
- package/dist/core/hearth/providers/blaxel.d.ts +0 -36
- package/dist/core/hearth/providers/blaxel.js +0 -364
- package/dist/core/hearth/providers/types.d.ts +0 -93
- package/dist/core/hearth/types.d.ts +0 -155
- package/dist/hearth/control-plane/__tests__/error-serialization.test.js +0 -29
- package/dist/hearth/control-plane/__tests__/node-message.test.js +0 -60
- package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.js +0 -44
- package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.js +0 -49
- package/dist/hearth/control-plane/__tests__/relay-security.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/relay-security.test.js +0 -314
- package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.js +0 -133
- package/dist/hearth/control-plane/__tests__/trigger-delivery.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/trigger-delivery.test.js +0 -170
- package/dist/hearth/control-plane/__tests__/wake-roll.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/wake-roll.test.js +0 -230
- package/dist/hearth/control-plane/__tests__/webhook-ingress.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/webhook-ingress.test.js +0 -167
- package/dist/hearth/control-plane/config.d.ts +0 -21
- package/dist/hearth/control-plane/config.js +0 -77
- package/dist/hearth/control-plane/db.d.ts +0 -30
- package/dist/hearth/control-plane/db.js +0 -561
- package/dist/hearth/control-plane/hearth-target.d.ts +0 -23
- package/dist/hearth/control-plane/hearth-target.js +0 -68
- package/dist/hearth/control-plane/ingress/rate-limit.d.ts +0 -24
- package/dist/hearth/control-plane/ingress/rate-limit.js +0 -100
- package/dist/hearth/control-plane/ingress/route-store.d.ts +0 -31
- package/dist/hearth/control-plane/ingress/route-store.js +0 -61
- package/dist/hearth/control-plane/ingress/webhook-delivery-store.d.ts +0 -41
- package/dist/hearth/control-plane/ingress/webhook-delivery-store.js +0 -69
- package/dist/hearth/control-plane/ingress/webhook-route.d.ts +0 -55
- package/dist/hearth/control-plane/ingress/webhook-route.js +0 -285
- package/dist/hearth/control-plane/main.d.ts +0 -1
- package/dist/hearth/control-plane/main.js +0 -88
- package/dist/hearth/control-plane/node-message.d.ts +0 -31
- package/dist/hearth/control-plane/node-message.js +0 -98
- package/dist/hearth/control-plane/register.d.ts +0 -15
- package/dist/hearth/control-plane/register.js +0 -34
- package/dist/hearth/control-plane/registry.d.ts +0 -22
- package/dist/hearth/control-plane/registry.js +0 -168
- package/dist/hearth/control-plane/relay.d.ts +0 -44
- package/dist/hearth/control-plane/relay.js +0 -711
- package/dist/hearth/control-plane/scheduler/fire-store.d.ts +0 -36
- package/dist/hearth/control-plane/scheduler/fire-store.js +0 -73
- package/dist/hearth/control-plane/scheduler/recurrence.d.ts +0 -7
- package/dist/hearth/control-plane/scheduler/recurrence.js +0 -58
- package/dist/hearth/control-plane/scheduler/scan-loop.d.ts +0 -38
- package/dist/hearth/control-plane/scheduler/scan-loop.js +0 -138
- package/dist/hearth/control-plane/scheduler/schedule-store.d.ts +0 -32
- package/dist/hearth/control-plane/scheduler/schedule-store.js +0 -66
- package/dist/hearth/control-plane/secrets.d.ts +0 -31
- package/dist/hearth/control-plane/secrets.js +0 -134
- package/dist/hearth/control-plane/server.d.ts +0 -27
- package/dist/hearth/control-plane/server.js +0 -482
- package/dist/hearth/control-plane/serving.d.ts +0 -15
- package/dist/hearth/control-plane/serving.js +0 -106
- package/dist/hearth/control-plane/session.d.ts +0 -68
- package/dist/hearth/control-plane/session.js +0 -273
- package/dist/hearth/control-plane/triggers/acl.d.ts +0 -14
- package/dist/hearth/control-plane/triggers/acl.js +0 -52
- package/dist/hearth/control-plane/triggers/audit-store.d.ts +0 -38
- package/dist/hearth/control-plane/triggers/audit-store.js +0 -79
- package/dist/hearth/control-plane/triggers/deliver.d.ts +0 -43
- package/dist/hearth/control-plane/triggers/deliver.js +0 -76
- package/dist/hearth/control-plane/triggers/envelope.d.ts +0 -29
- package/dist/hearth/control-plane/triggers/envelope.js +0 -38
- package/dist/hearth/control-plane/types.d.ts +0 -86
- package/dist/hearth/control-plane/types.js +0 -1
- package/dist/hearth/control-plane/wake.d.ts +0 -86
- package/dist/hearth/control-plane/wake.js +0 -550
- package/dist/web-client/assets/index-DwO46Cs5.css +0 -2
- /package/dist/{core/__tests__/hearth-bootstrap.test.d.ts → commands/__tests__/revive-now-gate.test.d.ts} +0 -0
- /package/dist/{core/hearth/__tests__/model-auth-guest.test.d.ts → commands/sys/__tests__/setup-core.test.d.ts} +0 -0
- /package/dist/core/{hearth/providers/__tests__/sweep-and-release.test.d.ts → __tests__/fault-classifier.test.d.ts} +0 -0
- /package/dist/core/{hearth/providers/types.js → __tests__/host-teardown-process-group.test.d.ts} +0 -0
- /package/dist/core/{hearth/types.js → __tests__/review-model-floor.test.d.ts} +0 -0
- /package/dist/{hearth/control-plane/__tests__/error-serialization.test.d.ts → core/__tests__/session-cycles.test.d.ts} +0 -0
- /package/dist/{hearth/control-plane/__tests__/node-message.test.d.ts → core/__tests__/worktree.test.d.ts} +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Cycle-aware session history — one node, one `.jsonl`, many cycles.
|
|
2
|
+
//
|
|
3
|
+
// A refresh-yield does not start a new session file: reviveNode reuses the
|
|
4
|
+
// node's existing `.jsonl` (buildPiArgv's `--crtr-new-cycle`), and the broker
|
|
5
|
+
// resets the session-tree leaf, appending a `crtr-cycle` custom entry as the
|
|
6
|
+
// new cycle's ROOT. Custom entries never participate in LLM context, so the
|
|
7
|
+
// refreshed window is exactly as clean as a brand-new session — but every
|
|
8
|
+
// cycle lives in the same tree as a sibling root branch, so the /tree
|
|
9
|
+
// navigator shows previous cycles natively.
|
|
10
|
+
//
|
|
11
|
+
// `cycleAwareMessages()` is the display-side counterpart: it rebuilds the FULL
|
|
12
|
+
// node history — each prior cycle's messages (via pi's own `buildSessionContext`,
|
|
13
|
+
// so a compacted prior cycle renders correctly), a synthetic divider message per
|
|
14
|
+
// boundary, then the live cycle's context — so the broker welcome snapshot
|
|
15
|
+
// (broker.ts) and the dormant snapshot (node-snapshot.ts) both render one
|
|
16
|
+
// continuous conversation.
|
|
17
|
+
import { buildSessionContext } from '@earendil-works/pi-coding-agent';
|
|
18
|
+
/** customType of the persisted tree entry that ROOTS each post-yield cycle.
|
|
19
|
+
* Data payload: `{ cycle, fromLeaf }` — `cycle` is the node's cycle counter at
|
|
20
|
+
* relaunch, `fromLeaf` the previous cycle's leaf entry id. */
|
|
21
|
+
export const CRTR_CYCLE_CUSTOM_TYPE = 'crtr-cycle';
|
|
22
|
+
/** customType of the SYNTHETIC divider message spliced between cycles in
|
|
23
|
+
* snapshot rendering. Never persisted to the session file. */
|
|
24
|
+
export const CRTR_CYCLE_DIVIDER_CUSTOM_TYPE = 'crtr-cycle-divider';
|
|
25
|
+
function cycleDivider(cycle, timestamp) {
|
|
26
|
+
const label = cycle !== undefined ? `cycle ${cycle}` : 'new cycle';
|
|
27
|
+
return {
|
|
28
|
+
role: 'custom',
|
|
29
|
+
customType: CRTR_CYCLE_DIVIDER_CUSTOM_TYPE,
|
|
30
|
+
content: `yielded — ${label} begins with a fresh context window`,
|
|
31
|
+
display: true,
|
|
32
|
+
timestamp: new Date(timestamp).getTime(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** The full multi-cycle message history for display: prior cycles (oldest
|
|
36
|
+
* first, each closed by a divider), then the live cycle's context. Falls back
|
|
37
|
+
* to plain `buildSessionContext()` when the session has no cycle markers, when
|
|
38
|
+
* the manager lacks tree accessors (fake engine), or when the current leaf was
|
|
39
|
+
* tree-navigated back into a pre-yield branch (pi's active-branch semantics
|
|
40
|
+
* then apply unchanged). */
|
|
41
|
+
export function cycleAwareMessages(sm) {
|
|
42
|
+
const current = sm.buildSessionContext().messages;
|
|
43
|
+
if (typeof sm.getEntries !== 'function' || typeof sm.getBranch !== 'function')
|
|
44
|
+
return current;
|
|
45
|
+
// File order = append order = chronological cycle order.
|
|
46
|
+
const entries = sm.getEntries();
|
|
47
|
+
const markers = entries.filter((e) => e.type === 'custom' && e.customType === CRTR_CYCLE_CUSTOM_TYPE);
|
|
48
|
+
if (markers.length === 0)
|
|
49
|
+
return current;
|
|
50
|
+
// The current cycle's root: first entry on the leaf→root path. When it is
|
|
51
|
+
// not a cycle marker, the leaf sits in the ORIGINAL (pre-yield) branch —
|
|
52
|
+
// render just that branch. Otherwise replay every cycle up to and including
|
|
53
|
+
// the one the marker closes.
|
|
54
|
+
const currentRootId = sm.getBranch()[0]?.id;
|
|
55
|
+
const rootMarkerIndex = markers.findIndex((m) => m.id === currentRootId);
|
|
56
|
+
if (rootMarkerIndex === -1)
|
|
57
|
+
return current;
|
|
58
|
+
// Shared byId map — buildSessionContext would otherwise rebuild one from
|
|
59
|
+
// `entries` on every call; one pass here amortizes it across every replayed
|
|
60
|
+
// marker.
|
|
61
|
+
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
62
|
+
const out = [];
|
|
63
|
+
for (let i = 0; i <= rootMarkerIndex; i++) {
|
|
64
|
+
const marker = markers[i];
|
|
65
|
+
const fromLeaf = marker.data?.fromLeaf;
|
|
66
|
+
if (typeof fromLeaf === 'string' && fromLeaf !== '') {
|
|
67
|
+
// Replay this prior cycle's own branch through pi's canonical context
|
|
68
|
+
// builder — preserving compaction semantics (one compactionSummary +
|
|
69
|
+
// kept + post-compaction messages only) exactly like the live cycle
|
|
70
|
+
// below.
|
|
71
|
+
out.push(...buildSessionContext(entries, fromLeaf, byId).messages);
|
|
72
|
+
}
|
|
73
|
+
out.push(cycleDivider(marker.data?.cycle, marker.timestamp));
|
|
74
|
+
}
|
|
75
|
+
out.push(...current);
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
@@ -54,6 +54,10 @@ export interface SpawnChildOpts {
|
|
|
54
54
|
* choice at fire time (an explicit value re-validates; inheritance re-reads
|
|
55
55
|
* the spawner's THEN-current profile). */
|
|
56
56
|
profile?: string | null;
|
|
57
|
+
/** Create a managed worktree for this node and pin its cwd to it. */
|
|
58
|
+
worktree?: boolean;
|
|
59
|
+
/** Preallocated node id, used when a managed worktree must be named before birth. */
|
|
60
|
+
nodeId?: string;
|
|
57
61
|
}
|
|
58
62
|
/** Resolve a `--fork-from` value to an ABSOLUTE `.jsonl` source path for the
|
|
59
63
|
* broker fork (`SessionManager.forkFrom`, which loads a file — never a bare id).
|
|
@@ -13,15 +13,18 @@ import { spawnSync } from 'node:child_process';
|
|
|
13
13
|
import { readdirSync, existsSync } from 'node:fs';
|
|
14
14
|
import { isAbsolute, resolve, join } from 'node:path';
|
|
15
15
|
import { homedir } from 'node:os';
|
|
16
|
-
import { spawnNode, currentNodeContext, rootOfSpine } from './nodes.js';
|
|
16
|
+
import { spawnNode, currentNodeContext, rootOfSpine, newNodeId } from './nodes.js';
|
|
17
17
|
import { loadProfileManifest } from '../profiles/manifest.js';
|
|
18
18
|
import { selectProfileForCwd } from '../profiles/select.js';
|
|
19
19
|
import { buildLaunchSpec, buildPiArgv } from './launch.js';
|
|
20
|
+
import { gitSync } from '../git.js';
|
|
21
|
+
import { createManagedWorktree } from '../worktree.js';
|
|
22
|
+
import { usage } from '../errors.js';
|
|
20
23
|
import { writeGoal } from './kickoff.js';
|
|
21
24
|
import { hasRoadmap, seedRoadmap } from './roadmap.js';
|
|
22
25
|
import { buildIdentityAssertion, buildWakeBearings } from './bearings.js';
|
|
23
26
|
import { installMenuBinding, installNavBindings, installViewNavBindings } from './tmux-chrome.js';
|
|
24
|
-
import { getNode, fullName } from '../canvas/index.js';
|
|
27
|
+
import { getNode, fullName, recordPid } from '../canvas/index.js';
|
|
25
28
|
import { registerViewerFocus, openViewerWindow, waitForBrokerViewSocket, viewerSplitEnv, windowOfPane, currentTmux, inTmux, focusWindow, } from './placement.js';
|
|
26
29
|
import { transition } from './lifecycle.js';
|
|
27
30
|
import { headlessBrokerHost } from './host.js';
|
|
@@ -134,6 +137,7 @@ export async function bootRoot(opts) {
|
|
|
134
137
|
transition(meta.node_id, 'crash');
|
|
135
138
|
throw new Error(`failed to launch the broker engine for the front-door root ${meta.node_id} — nothing to attach to.`);
|
|
136
139
|
}
|
|
140
|
+
recordPid(meta.node_id, placed.pid);
|
|
137
141
|
// The terminal has nothing to attach to until view.sock accepts. The root is
|
|
138
142
|
// useless without its engine, so a socket timeout IS fatal here (unlike a
|
|
139
143
|
// background child's optional viewer, which is skipped on timeout).
|
|
@@ -170,7 +174,9 @@ function piSessionsRoot() {
|
|
|
170
174
|
function resolveSessionUuid(uuid) {
|
|
171
175
|
const sessionsRoot = piSessionsRoot();
|
|
172
176
|
if (!existsSync(sessionsRoot)) {
|
|
173
|
-
throw
|
|
177
|
+
throw usage(`--fork-from '${uuid}': no pi sessions store at ${sessionsRoot} to resolve it from — pass a node id or an absolute .jsonl path.`, {
|
|
178
|
+
next: 'Pass a live node id, an absolute .jsonl session path, or drop --fork-from.',
|
|
179
|
+
});
|
|
174
180
|
}
|
|
175
181
|
const matches = [];
|
|
176
182
|
for (const dir of readdirSync(sessionsRoot, { withFileTypes: true })) {
|
|
@@ -187,10 +193,15 @@ function resolveSessionUuid(uuid) {
|
|
|
187
193
|
}
|
|
188
194
|
const exact = matches.filter((m) => m.id === uuid);
|
|
189
195
|
const hit = exact.length > 0 ? exact : matches.filter((m) => m.id.startsWith(uuid));
|
|
190
|
-
if (hit.length === 0)
|
|
191
|
-
throw
|
|
196
|
+
if (hit.length === 0) {
|
|
197
|
+
throw usage(`--fork-from '${uuid}': no pi session with that id found.`, {
|
|
198
|
+
next: 'Pass a live node id, a real session uuid/prefix, or an absolute .jsonl session path.',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
192
201
|
if (hit.length > 1) {
|
|
193
|
-
throw
|
|
202
|
+
throw usage(`--fork-from '${uuid}': ambiguous — ${hit.length} sessions match that id prefix; pass the full id or the .jsonl path.`, {
|
|
203
|
+
next: 'Pass the full session uuid or an absolute .jsonl path to disambiguate.',
|
|
204
|
+
});
|
|
194
205
|
}
|
|
195
206
|
return hit[0].path;
|
|
196
207
|
}
|
|
@@ -203,7 +214,7 @@ function resolveSessionUuid(uuid) {
|
|
|
203
214
|
export function resolveForkSource(value) {
|
|
204
215
|
const v = value.trim();
|
|
205
216
|
if (v === '')
|
|
206
|
-
throw
|
|
217
|
+
throw usage('--fork-from requires a node id, session file, or session uuid.', { next: 'Pass a live node id, an absolute .jsonl session path, or a session uuid/prefix.' });
|
|
207
218
|
// A path (contains `/` or ends `.jsonl`) is a session file — ensure it is
|
|
208
219
|
// ABSOLUTE so the broker, launched in a different cwd, still resolves it.
|
|
209
220
|
if (v.includes('/') || v.endsWith('.jsonl')) {
|
|
@@ -215,7 +226,9 @@ export function resolveForkSource(value) {
|
|
|
215
226
|
if (n !== null) {
|
|
216
227
|
const src = n.pi_session_file;
|
|
217
228
|
if (src === undefined || src === null || src === '') {
|
|
218
|
-
throw
|
|
229
|
+
throw usage(`node ${v} has no pi session yet — it has not captured a session FILE to fork from (the broker forks a .jsonl path, not a bare id).`, {
|
|
230
|
+
next: 'Pick a node that has captured a session, or drop --fork-from.',
|
|
231
|
+
});
|
|
219
232
|
}
|
|
220
233
|
return src;
|
|
221
234
|
}
|
|
@@ -279,102 +292,141 @@ export async function spawnChild(opts) {
|
|
|
279
292
|
// Born WITHOUT a name — the canvas-goal-capture extension names it async from
|
|
280
293
|
// its first message (the kickoff task) inside its own pi process, so spawn
|
|
281
294
|
// never blocks on the LLM naming round-trip (the 2-3s freeze it used to cost).
|
|
295
|
+
const wantsWorktree = opts.worktree === true;
|
|
296
|
+
const nodeId = wantsWorktree ? (opts.nodeId ?? newNodeId()) : opts.nodeId;
|
|
297
|
+
// Preflight: resolve every fallible input BEFORE creating the worktree or the
|
|
298
|
+
// node. `--fork-from <bad>` (resolveForkSource) and an invalid `--profile`
|
|
299
|
+
// (resolveProfileId) must fail HERE, before anything exists to clean up or
|
|
300
|
+
// strand — catching them later (after the worktree/node are created) is what
|
|
301
|
+
// let `--worktree --fork-from <bad>` leave an active node pinned to a deleted
|
|
302
|
+
// cwd with an open managed_worktree.
|
|
282
303
|
const profileId = await resolveProfileId(opts.profile, spawner, opts.cwd);
|
|
283
|
-
const meta = spawnNode({
|
|
284
|
-
kind: opts.kind,
|
|
285
|
-
mode,
|
|
286
|
-
lifecycle,
|
|
287
|
-
cwd: opts.cwd,
|
|
288
|
-
name: opts.name ?? opts.kind,
|
|
289
|
-
// A root has no spine parent (top-level, nobody subscribes); it still
|
|
290
|
-
// records spawned_by=spawner when a node (not a human shell) spawned it.
|
|
291
|
-
// A child's parent IS its manager.
|
|
292
|
-
parent: root ? null : spawner,
|
|
293
|
-
spawnedBy: root ? (spawner ?? undefined) : undefined,
|
|
294
|
-
profile_id: profileId,
|
|
295
|
-
// Persist the RAW fork reference (not the resolved path) as provenance, so
|
|
296
|
-
// the boot intro can detect this is a fork and re-assert the node's own
|
|
297
|
-
// identity over the source's copied-in conversation.
|
|
298
|
-
forkFrom: opts.forkFrom,
|
|
299
|
-
modelOverride: opts.model,
|
|
300
|
-
launch,
|
|
301
|
-
});
|
|
302
|
-
// Persist the task as the child's goal for a fresh revive to re-read.
|
|
303
|
-
writeGoal(meta.node_id, opts.prompt);
|
|
304
|
-
// A fork copies an existing conversation into this child's first session
|
|
305
|
-
// (resolved to an absolute file path when forking from a node). Resolved here
|
|
306
|
-
// — not in buildPiArgv — so a bad reference fails the spawn loudly before any
|
|
307
|
-
// window opens, rather than after pi is already booting.
|
|
308
304
|
const forkFrom = opts.forkFrom !== undefined ? resolveForkSource(opts.forkFrom) : undefined;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
305
|
+
let managedWorktree;
|
|
306
|
+
let spawnCwd = opts.cwd;
|
|
307
|
+
try {
|
|
308
|
+
if (wantsWorktree) {
|
|
309
|
+
managedWorktree = createManagedWorktree(opts.cwd, nodeId);
|
|
310
|
+
spawnCwd = managedWorktree.path;
|
|
311
|
+
}
|
|
312
|
+
const meta = spawnNode({
|
|
313
|
+
kind: opts.kind,
|
|
314
|
+
mode,
|
|
315
|
+
lifecycle,
|
|
316
|
+
cwd: spawnCwd,
|
|
317
|
+
name: opts.name ?? opts.kind,
|
|
318
|
+
// A root has no spine parent (top-level, nobody subscribes); it still
|
|
319
|
+
// records spawned_by=spawner when a node (not a human shell) spawned it.
|
|
320
|
+
// A child's parent IS its manager.
|
|
321
|
+
parent: root ? null : spawner,
|
|
322
|
+
spawnedBy: root ? (spawner ?? undefined) : undefined,
|
|
323
|
+
profile_id: profileId,
|
|
324
|
+
// Persist the RAW fork reference (not the resolved path) as provenance, so
|
|
325
|
+
// the boot intro can detect this is a fork and re-assert the node's own
|
|
326
|
+
// identity over the source's copied-in conversation.
|
|
327
|
+
forkFrom: opts.forkFrom,
|
|
328
|
+
modelOverride: opts.model,
|
|
329
|
+
...(managedWorktree !== undefined ? { managedWorktree } : {}),
|
|
330
|
+
...(nodeId !== undefined ? { nodeId } : {}),
|
|
331
|
+
launch,
|
|
332
|
+
});
|
|
333
|
+
// Persist the task as the child's goal for a fresh revive to re-read.
|
|
334
|
+
writeGoal(meta.node_id, opts.prompt);
|
|
335
|
+
// A fork inherits the SOURCE node's entire first-person conversation, so the
|
|
336
|
+
// identity re-assertion must ride the STRONGEST channel too, not only the
|
|
337
|
+
// session-start bearings (a trailing custom_message the model weighs only by
|
|
338
|
+
// recency). Prepend it to the kickoff PROMPT — the message that actually
|
|
339
|
+
// triggers the fork's first turn, so the model acts on "you are node X, a
|
|
340
|
+
// FORK of <source>, NOT them" as live instruction. When the daemon births this
|
|
341
|
+
// node from a scheduled `spawn` wake, the <crtr-wake> provenance block leads
|
|
342
|
+
// (so "a timer birthed you" precedes the task) — mirroring the fork-identity
|
|
343
|
+
// prepend. Only the pi argv prompt is reframed; the persisted goal (writeGoal
|
|
344
|
+
// above) keeps the raw task.
|
|
345
|
+
const wakeBlock = opts.wakeOrigin !== undefined ? `${buildWakeBearings(opts.wakeOrigin)}\n\n` : '';
|
|
346
|
+
const idBlock = forkFrom !== undefined ? `${buildIdentityAssertion(meta.node_id)}\n\n` : '';
|
|
347
|
+
const kickoff = `${wakeBlock}${idBlock}${opts.prompt}`;
|
|
348
|
+
// A child created DIRECTLY as an orchestrator (mode='orchestrator') boots
|
|
349
|
+
// with the orchestrator persona but bypasses promote(), which is where a
|
|
350
|
+
// roadmap scaffold would normally be seeded. Lay one down here (goal
|
|
351
|
+
// pre-filled from the task) so the orchestrator has its memory artifact from
|
|
352
|
+
// birth, instead of waking memory-less. Guarded so it never clobbers.
|
|
353
|
+
if (mode === 'orchestrator' && !hasRoadmap(meta.node_id)) {
|
|
354
|
+
seedRoadmap(meta.node_id, { goal: opts.prompt.trim() });
|
|
355
|
+
}
|
|
356
|
+
// (The three scoped long-term memory stores are seeded for EVERY node at birth
|
|
357
|
+
// in spawnNode — no orchestrator-gated seeding needed here.)
|
|
358
|
+
// A --root spawned from inside tmux opens its one viewer in the CALLER'S
|
|
359
|
+
// CURRENT session (below), so it appears where the spawner is working; a
|
|
360
|
+
// managed child is a headless broker with NO viewer at all.
|
|
361
|
+
const here = root ? currentTmux() : null;
|
|
362
|
+
const inv = buildPiArgv(meta, { prompt: kickoff, forkFrom });
|
|
363
|
+
// CRTR_SUBTREE (the spine root) rides the detached broker's env so it can group
|
|
364
|
+
// the subtree; the host sets CRTR_FRONT_DOOR itself.
|
|
365
|
+
inv.env = { ...inv.env, CRTR_SUBTREE: rootOfSpine(meta.node_id) };
|
|
366
|
+
// Broker is the only host: launch the detached broker ENGINE. A managed child
|
|
367
|
+
// gets NO viewer — spawning it returns once the engine is launched and a human
|
|
368
|
+
// opens a viewer on demand via `crtr node focus` / surface attach. A --root gets
|
|
369
|
+
// one foreground viewer below.
|
|
370
|
+
const placed = launchBrokerOrCrash(meta.node_id, inv, {
|
|
371
|
+
cwd: meta.cwd,
|
|
372
|
+
name: fullName(meta),
|
|
373
|
+
resuming: false,
|
|
374
|
+
});
|
|
375
|
+
// Definitive failure: no broker pid ⇒ the node has no engine. Crash it (so the
|
|
376
|
+
// daemon doesn't watch a zombie 'active') and throw so `crtr node new` exits
|
|
377
|
+
// non-zero. transition('crash') is a legal from-LIVE move (still 'active' from
|
|
378
|
+
// spawnNode). Mirrors the old window===null crash.
|
|
379
|
+
if (placed.pid === null) {
|
|
380
|
+
transition(meta.node_id, 'crash');
|
|
381
|
+
throw new Error(`failed to launch the broker engine for ${meta.node_id} (${meta.name}) — the node was not started.`);
|
|
382
|
+
}
|
|
383
|
+
recordPid(meta.node_id, placed.pid);
|
|
384
|
+
// A --root is spawned to be DRIVEN directly: when the caller is inside tmux,
|
|
385
|
+
// open ONE foreground viewer in the caller's CURRENT session and bring it
|
|
386
|
+
// forefront so whoever asked for it picks up the conversation. Outside tmux a
|
|
387
|
+
// --root opens no viewer (an agent handing a root off) — never a throw. A
|
|
388
|
+
// managed child opens nothing. Non-fatal if tmux refuses (openViewerWindow
|
|
389
|
+
// returns null) or the socket never binds — `focus` opens a viewer later.
|
|
390
|
+
let window = null;
|
|
391
|
+
let session = null;
|
|
392
|
+
if (root && here !== null && await waitForBrokerViewSocket(meta.node_id, placed.exited)) {
|
|
393
|
+
const viewer = openViewerWindow(meta.node_id, here.session, { name: fullName(meta), cwd: opts.cwd });
|
|
394
|
+
if (viewer !== null && viewer.pane !== null && viewer.pane !== '') {
|
|
395
|
+
session = here.session;
|
|
396
|
+
window = windowOfPane(viewer.pane);
|
|
397
|
+
if (window !== null) {
|
|
398
|
+
try {
|
|
399
|
+
focusWindow(here.session, window);
|
|
400
|
+
}
|
|
401
|
+
catch { /* best-effort */ }
|
|
373
402
|
}
|
|
374
|
-
catch { /* best-effort */ }
|
|
375
403
|
}
|
|
376
404
|
}
|
|
405
|
+
const saved = getNode(meta.node_id);
|
|
406
|
+
return { node: saved, window, session };
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
// Cleanup scope: a managed worktree is only removed on failure while NO
|
|
410
|
+
// persisted node identity exists yet. spawnNode() persists the node row
|
|
411
|
+
// (and its meta files) BEFORE the later fallible steps in this try block
|
|
412
|
+
// (writeGoal, roadmap seeding, launch, viewer placement) — so a flag set
|
|
413
|
+
// after spawnNode() merely returns would still read false if one of THOSE
|
|
414
|
+
// later steps threw, and this catch would then delete the worktree out
|
|
415
|
+
// from under an already-persisted node whose cwd points at it. Check the
|
|
416
|
+
// canvas directly instead: once any node row exists for this id, keep the
|
|
417
|
+
// worktree — the node stays alive (crashed, if the launch itself failed)
|
|
418
|
+
// with its worktree intact rather than pinned to a deleted path.
|
|
419
|
+
if (managedWorktree !== undefined && nodeId !== undefined && getNode(nodeId) === null) {
|
|
420
|
+
const repoRoot = managedWorktree.repo_root;
|
|
421
|
+
try {
|
|
422
|
+
gitSync(['worktree', 'remove', managedWorktree.path], repoRoot);
|
|
423
|
+
}
|
|
424
|
+
catch { /* best-effort */ }
|
|
425
|
+
try {
|
|
426
|
+
gitSync(['branch', '-d', managedWorktree.branch], repoRoot);
|
|
427
|
+
}
|
|
428
|
+
catch { /* best-effort */ }
|
|
429
|
+
}
|
|
430
|
+
throw err;
|
|
377
431
|
}
|
|
378
|
-
const saved = getNode(meta.node_id);
|
|
379
|
-
return { node: saved, window, session };
|
|
380
432
|
}
|
|
@@ -164,16 +164,13 @@ export declare function respawnPaneSync(opts: RespawnPaneOpts): boolean;
|
|
|
164
164
|
/** Turn a pi argv array into a single shell command string.
|
|
165
165
|
*
|
|
166
166
|
* The binary defaults to `CRTR_PI_BINARY` when that env var is set, else the
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* one today). The value may be a multi-word launcher (e.g. `node --import
|
|
175
|
-
* tsx/esm host.ts`); only the argv entries are shell-quoted, so a multi-word
|
|
176
|
-
* binary is spliced verbatim ahead of them. */
|
|
167
|
+
* bundled pi CLI launched with the current Node binary. CRTR_PI_BINARY is a
|
|
168
|
+
* TEST-ONLY substitution seam: the integration-test harness points it at a
|
|
169
|
+
* deterministic fake-pi vehicle so a real `crtr node new` reaches the fake
|
|
170
|
+
* instead of the LLM pi, without depending on tmux/shell PATH inheritance. An
|
|
171
|
+
* explicit `binary` arg still overrides the env. The value may be a multi-word
|
|
172
|
+
* launcher (e.g. `node --import tsx/esm host.ts`); only argv entries are
|
|
173
|
+
* shell-quoted, so a multi-word binary is spliced verbatim ahead of them. */
|
|
177
174
|
export declare function piCommand(argv: string[], binary?: string): string;
|
|
178
175
|
/** List all window ids present in `session`. Returns [] if the session does
|
|
179
176
|
* not exist or tmux fails for any reason. Each entry is the raw window id
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { spawnSync } from 'node:child_process';
|
|
11
11
|
import { readConfig } from '../config.js';
|
|
12
12
|
import { defaultCanvasNavConfig } from '../../types.js';
|
|
13
|
+
import { bundledPiCommandForShell } from './pi-cli.js';
|
|
13
14
|
import { surfaceTmuxStyleArgs } from './surface-bg.js';
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Shell quoting + tmux invocation
|
|
@@ -327,17 +328,14 @@ export function respawnPaneSync(opts) {
|
|
|
327
328
|
/** Turn a pi argv array into a single shell command string.
|
|
328
329
|
*
|
|
329
330
|
* The binary defaults to `CRTR_PI_BINARY` when that env var is set, else the
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
|
|
338
|
-
* tsx/esm host.ts`); only the argv entries are shell-quoted, so a multi-word
|
|
339
|
-
* binary is spliced verbatim ahead of them. */
|
|
340
|
-
export function piCommand(argv, binary = process.env['CRTR_PI_BINARY'] ?? 'pi') {
|
|
331
|
+
* bundled pi CLI launched with the current Node binary. CRTR_PI_BINARY is a
|
|
332
|
+
* TEST-ONLY substitution seam: the integration-test harness points it at a
|
|
333
|
+
* deterministic fake-pi vehicle so a real `crtr node new` reaches the fake
|
|
334
|
+
* instead of the LLM pi, without depending on tmux/shell PATH inheritance. An
|
|
335
|
+
* explicit `binary` arg still overrides the env. The value may be a multi-word
|
|
336
|
+
* launcher (e.g. `node --import tsx/esm host.ts`); only argv entries are
|
|
337
|
+
* shell-quoted, so a multi-word binary is spliced verbatim ahead of them. */
|
|
338
|
+
export function piCommand(argv, binary = process.env['CRTR_PI_BINARY'] ?? bundledPiCommandForShell(shellQuote)) {
|
|
341
339
|
return [binary, ...argv.map(shellQuote)].join(' ');
|
|
342
340
|
}
|
|
343
341
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type NodeMeta, type ManagedWorktree } from './canvas/index.js';
|
|
2
|
+
export interface WorktreeCommandResult {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class WorktreeError extends Error {
|
|
8
|
+
code: string;
|
|
9
|
+
next: string;
|
|
10
|
+
detail?: string;
|
|
11
|
+
constructor(code: string, message: string, next: string, detail?: string);
|
|
12
|
+
}
|
|
13
|
+
export declare function managedWorktreesRoot(): string;
|
|
14
|
+
export declare function managedWorktreePath(nodeId: string): string;
|
|
15
|
+
export declare function hasOpenManagedWorktree(node: NodeMeta | null | undefined): boolean;
|
|
16
|
+
export declare function openManagedWorktreeForNode(nodeId: string): ManagedWorktree | null;
|
|
17
|
+
export declare function createManagedWorktree(cwd: string, nodeId: string): ManagedWorktree;
|
|
18
|
+
export declare function isRebaseInProgress(cwd: string): boolean;
|
|
19
|
+
export interface CloseManagedWorktreeResult {
|
|
20
|
+
node_id: string;
|
|
21
|
+
branch: string;
|
|
22
|
+
worktree_path: string;
|
|
23
|
+
pushed_sha: string;
|
|
24
|
+
/** False when the push landed but the worktree checkout could not be removed
|
|
25
|
+
* automatically afterward — the close still counts as done (state is
|
|
26
|
+
* closed, push-final is unblocked); `worktree_remove_error` carries the
|
|
27
|
+
* manual cleanup note. */
|
|
28
|
+
worktree_removed: boolean;
|
|
29
|
+
worktree_remove_error?: string;
|
|
30
|
+
/** False when the push landed but the local branch could not be deleted
|
|
31
|
+
* afterward — same non-fatal treatment as worktree_removed. */
|
|
32
|
+
branch_deleted: boolean;
|
|
33
|
+
branch_delete_error?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function closeManagedWorktree(nodeId: string): CloseManagedWorktreeResult;
|