@crouton-kit/crouter 0.3.45 → 0.3.47
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 +0 -1
- package/dist/builtin-memory/00-runtime-base.md +3 -0
- package/dist/builtin-memory/internal/nodes-and-canvas.md +2 -2
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +2 -2
- package/dist/builtin-views/chat/web.jsx +1 -1
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +11 -9
- package/dist/clients/attach/attach-cmd.js +557 -546
- package/dist/clients/attach/chat-view.d.ts +4 -0
- package/dist/clients/attach/chat-view.js +13 -2
- package/dist/clients/attach/mermaid-render.d.ts +23 -0
- package/dist/clients/attach/mermaid-render.js +124 -0
- package/dist/clients/web/source-cache.js +1 -1
- package/dist/commands/__tests__/revive-now-gate.test.js +2 -2
- package/dist/commands/canvas-history/read.js +3 -3
- package/dist/commands/canvas-history/search.js +1 -1
- package/dist/commands/canvas-history.js +11 -12
- package/dist/commands/canvas-rebuild-index.js +2 -2
- package/dist/commands/canvas.js +4 -4
- package/dist/commands/node-inspect-artifacts.d.ts +1 -0
- package/dist/commands/{canvas-history/show.js → node-inspect-artifacts.js} +7 -7
- package/dist/commands/node-lifecycle-revive.d.ts +12 -0
- package/dist/commands/node-lifecycle-revive.js +166 -0
- package/dist/commands/node-worktree.d.ts +2 -0
- package/dist/commands/{worktree.js → node-worktree.js} +15 -16
- package/dist/commands/node.js +16 -13
- package/dist/commands/push.js +1 -1
- package/dist/commands/revive.d.ts +0 -10
- package/dist/commands/revive.js +31 -181
- package/dist/core/__tests__/kickoff.test.js +2 -2
- package/dist/core/canvas/boot.js +1 -1
- package/dist/core/canvas/browse/app.js +6 -6
- package/dist/core/canvas/browse/render.js +1 -1
- package/dist/core/canvas/status-glyph.d.ts +1 -1
- package/dist/core/canvas/status-glyph.js +1 -1
- package/dist/core/preview-registry.js +27 -26
- package/dist/core/profiles/select.d.ts +12 -4
- package/dist/core/profiles/select.js +182 -22
- package/dist/core/runtime/bearings.js +1 -1
- package/dist/core/runtime/close.js +1 -1
- package/dist/core/runtime/kickoff.js +3 -3
- package/dist/core/runtime/revive.js +2 -2
- package/dist/core/view/stream-local.js +1 -1
- package/dist/core/worktree.js +4 -4
- package/dist/daemon/crtrd.js +3 -3
- package/dist/web/transport-stream.js +1 -1
- package/dist/web-client/assets/index-dpd0Rzuw.js +80 -0
- package/dist/web-client/index.html +1 -1
- package/package.json +2 -2
- package/scripts/postinstall.mjs +73 -0
- package/dist/commands/canvas-history/show.d.ts +0 -1
- package/dist/commands/worktree.d.ts +0 -2
- package/dist/web-client/assets/index-B00YpRQ1.js +0 -80
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// `crtr node worktree` — the node-owned managed-worktree lifecycle. Birth is
|
|
2
|
+
// `node new --worktree` (a flag on spawn); this branch holds the single close
|
|
3
|
+
// verb, symmetric with birth, so a git worktree's whole lifecycle sits under
|
|
4
|
+
// `node`.
|
|
1
5
|
import { defineBranch, defineLeaf } from '../core/command.js';
|
|
2
6
|
import { InputError } from '../core/io.js';
|
|
3
7
|
import { closeManagedWorktree, WorktreeError } from '../core/worktree.js';
|
|
@@ -27,7 +31,7 @@ const worktreeClose = defineLeaf({
|
|
|
27
31
|
description: 'land and close the current node-managed worktree',
|
|
28
32
|
whenToUse: 'you are inside the node that owns an open managed worktree and are ready to rebase it onto origin/main, push it to main, and (best-effort) remove the worktree and delete the branch before finishing',
|
|
29
33
|
help: {
|
|
30
|
-
name: 'worktree close',
|
|
34
|
+
name: 'node worktree close',
|
|
31
35
|
summary: 'land and close the current node-managed worktree',
|
|
32
36
|
params: [],
|
|
33
37
|
output: [
|
|
@@ -77,18 +81,13 @@ const worktreeClose = defineLeaf({
|
|
|
77
81
|
return base;
|
|
78
82
|
},
|
|
79
83
|
});
|
|
80
|
-
export
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
summary: 'manage a node-owned git worktree',
|
|
91
|
-
},
|
|
92
|
-
children: [worktreeClose],
|
|
93
|
-
});
|
|
94
|
-
}
|
|
84
|
+
export const nodeWorktreeBranch = defineBranch({
|
|
85
|
+
name: 'worktree',
|
|
86
|
+
description: 'manage a node-owned git worktree — close the one born by `node new --worktree`',
|
|
87
|
+
whenToUse: 'you are inside a node that was spawned with `node new --worktree` and are ready to land it: rebase onto origin/main, push, and close the worktree. Symmetric with `node new --worktree`, which births it',
|
|
88
|
+
help: {
|
|
89
|
+
name: 'node worktree',
|
|
90
|
+
summary: 'manage a node-owned git worktree — birth is `node new --worktree`; this branch closes it',
|
|
91
|
+
},
|
|
92
|
+
children: [worktreeClose],
|
|
93
|
+
});
|
package/dist/commands/node.js
CHANGED
|
@@ -19,6 +19,9 @@ import { readRoadmap, hasRoadmap, seedRoadmap } from '../core/runtime/roadmap.js
|
|
|
19
19
|
import { parseWhen, parseCadence, cadenceDisplay } from '../core/wake.js';
|
|
20
20
|
import { nodeSnapshotLeaf } from './node-snapshot.js';
|
|
21
21
|
import { nodeContextLeaf } from './node-context.js';
|
|
22
|
+
import { nodeArtifactsLeaf } from './node-inspect-artifacts.js';
|
|
23
|
+
import { nodeWorktreeBranch } from './node-worktree.js';
|
|
24
|
+
import { nodeReviveLeaf } from './node-lifecycle-revive.js';
|
|
22
25
|
import { recycleNode } from '../core/runtime/recycle.js';
|
|
23
26
|
import { readFaultAsync } from '../core/runtime/fault.js';
|
|
24
27
|
import { activeFaultForDisplay } from '../core/canvas/render.js';
|
|
@@ -128,7 +131,7 @@ const nodeNew = defineLeaf({
|
|
|
128
131
|
{ kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Display name (tmux window + resume picker). Defaults to the kind.' },
|
|
129
132
|
{ kind: 'flag', name: 'parent', type: 'string', required: false, constraint: 'Parent node id. Defaults to the calling node (CRTR_NODE_ID).' },
|
|
130
133
|
{ kind: 'flag', name: 'root', type: 'bool', required: false, constraint: 'Spawn an INDEPENDENT root instead of a managed child: no parent (top-level on the canvas), NO subscription back to you (you are NOT woken by it), resident lifecycle. It records spawned_by=you for provenance and is brought forefront so it can be driven directly. Use for a node you hand off and do not manage (e.g. a sub-orchestrator a human will discuss with). Mutually exclusive with --worktree.' },
|
|
131
|
-
{ kind: 'flag', name: 'worktree', type: 'bool', required: false, constraint: 'Use when the child will COMMIT to the repo while you or other agents may also be editing it — the worktree isolates its work so nothing collides; skip it for read-only work or when the child is the sole writer. Creates a crouter-managed git worktree for the spawned child, keyed by the child node id, at `~/.crouter/canvas/worktrees/<node-id>/` on branch `crtr/<node-id>` based on `origin/main`; the child\'s cwd is pinned there, and it lands the work serially onto origin/main later with `crtr worktree close`. Mutually exclusive with --root.' },
|
|
134
|
+
{ kind: 'flag', name: 'worktree', type: 'bool', required: false, constraint: 'Use when the child will COMMIT to the repo while you or other agents may also be editing it — the worktree isolates its work so nothing collides; skip it for read-only work or when the child is the sole writer. Creates a crouter-managed git worktree for the spawned child, keyed by the child node id, at `~/.crouter/canvas/worktrees/<node-id>/` on branch `crtr/<node-id>` based on `origin/main`; the child\'s cwd is pinned there, and it lands the work serially onto origin/main later with `crtr node worktree close`. Mutually exclusive with --root.' },
|
|
132
135
|
{ kind: 'flag', name: 'fork-from', type: 'string', required: false, constraint: 'FORK the new node from an existing pi conversation instead of starting it fresh: pass a node id (forks from that node\'s session), an absolute session `.jsonl` path, or a partial pi session uuid. pi copies that whole history into a NEW session for the child (the source is untouched), then the prompt is delivered as the next message — i.e. the child wakes up as a continuation of that conversation. Use to branch exploratory work off a node that already built up the context you need, instead of re-deriving it. One-shot at birth: the fork resumes its own session thereafter.' },
|
|
133
136
|
{ kind: 'flag', name: 'model', type: 'string', required: false, constraint: 'Override the model the node runs on: exact `provider/id` (e.g. openai-codex/gpt-5.5), a `provider/tier` (anthropic|openai + ultra|strong|medium|light, e.g. openai/ultra — pin a specific model family, useful for spawning cross-family children), a bare capability TIER (ultra|strong|medium|light), or a family alias (opus|sonnet|haiku). Must resolve to a concrete model OFFLINE — unlike `node config --model`, no live broker exists yet at spawn to free-text substring-search a registry. Omit to use the kind\'s persona default (advisor=ultra, explore=light, most other builtins=strong). The override is durable — it survives revives and polymorphs (promote/demote).' },
|
|
134
137
|
{ kind: 'flag', name: 'profile', type: 'string', required: false, constraint: 'Select the profile this node runs under — an exact profile id or a unique manifest name (see the <profiles> list below, or `crtr profile list`). Omit to INHERIT the calling node\'s current profile (root has none, so an uninherited child has none either). --root does NOT reset this — it only means top-level, still inherits/uses the selected profile unless this overrides it.' },
|
|
@@ -157,7 +160,7 @@ const nodeNew = defineLeaf({
|
|
|
157
160
|
dynamicState: () => [kindsStateBlock(), profilesStateBlock()].join('\n'),
|
|
158
161
|
outputKind: 'object',
|
|
159
162
|
effects: [
|
|
160
|
-
'Spawning now: creates a node under ~/.crouter/canvas/nodes/<id>/ and indexes it in canvas.db. Default (managed child): parent auto-subscribes (active) and is woken on the child\'s pushes. With --root: no subscription — records a spawned_by edge for provenance only. With --worktree: also creates a managed git worktree at ~/.crouter/canvas/worktrees/<node-id>/ on branch crtr/<node-id> based on origin/main; the child closes it later with `crtr worktree close` before finishing. With --output-schema: writes nodes/<id>/output-schema.json in terminal mode; the node must call submit, which writes context/result.json and pushes final. Launches the node\'s engine as a detached broker (the only host); a managed child opens NO viewer, a --root opens one in your current tmux session.',
|
|
163
|
+
'Spawning now: creates a node under ~/.crouter/canvas/nodes/<id>/ and indexes it in canvas.db. Default (managed child): parent auto-subscribes (active) and is woken on the child\'s pushes. With --root: no subscription — records a spawned_by edge for provenance only. With --worktree: also creates a managed git worktree at ~/.crouter/canvas/worktrees/<node-id>/ on branch crtr/<node-id> based on origin/main; the child closes it later with `crtr node worktree close` before finishing. With --output-schema: writes nodes/<id>/output-schema.json in terminal mode; the node must call submit, which writes context/result.json and pushes final. Launches the node\'s engine as a detached broker (the only host); a managed child opens NO viewer, a --root opens one in your current tmux session.',
|
|
161
164
|
'Armed (--at/--every/--when): inserts one detached `new`/`node_birth` triggers row (node_id NULL, owner=you, parent/cwd resolved now); NO node and NO window exist until fire time. The daemon spawns from the stored recipe at fire time, re-deriving the launch spec live.',
|
|
162
165
|
],
|
|
163
166
|
},
|
|
@@ -470,13 +473,13 @@ const nodeShow = defineLeaf({
|
|
|
470
473
|
// ---------------------------------------------------------------------------
|
|
471
474
|
const nodeInspect = defineBranch({
|
|
472
475
|
name: 'inspect',
|
|
473
|
-
description: 'read
|
|
474
|
-
whenToUse: 'reading
|
|
476
|
+
description: 'read one node — its graph position, context, and artifacts',
|
|
477
|
+
whenToUse: 'reading everything about ONE node you can name: enumerate the roster (`list`), inspect a node and its spine neighbors (`show`), browse its on-disk context dir (`context`), or list its reports/docs as refs (`artifacts`). Use `canvas dashboard` instead to render the whole tree SHAPE, or `canvas attention` to find which nodes are blocked on a human',
|
|
475
478
|
help: {
|
|
476
479
|
name: 'node inspect',
|
|
477
|
-
summary: 'read
|
|
480
|
+
summary: 'read one node (or the roster) — topology, context dir, and artifacts',
|
|
478
481
|
},
|
|
479
|
-
children: [nodeList, nodeShow, nodeSnapshotLeaf, nodeContextLeaf],
|
|
482
|
+
children: [nodeList, nodeShow, nodeSnapshotLeaf, nodeContextLeaf, nodeArtifactsLeaf],
|
|
480
483
|
});
|
|
481
484
|
// ---------------------------------------------------------------------------
|
|
482
485
|
// node focus — bring a node's viewer forefront (across roots if needed)
|
|
@@ -635,10 +638,10 @@ const nodeRecycle = defineLeaf({
|
|
|
635
638
|
const nodeClose = defineLeaf({
|
|
636
639
|
name: 'close',
|
|
637
640
|
description: 'close a node + cascade-cancel its exclusive subtree (revivable)',
|
|
638
|
-
whenToUse: 'you want to tear a node down WITHOUT finishing it, cascade-cancelling every descendant it exclusively owns: abandoning a line of work, killing a stuck or wrong-turn subtree, clearing a branch you no longer need. Windows die but nothing is deleted — each closed node keeps its pi session and can be revived later (`
|
|
641
|
+
whenToUse: 'you want to tear a node down WITHOUT finishing it, cascade-cancelling every descendant it exclusively owns: abandoning a line of work, killing a stuck or wrong-turn subtree, clearing a branch you no longer need. Windows die but nothing is deleted — each closed node keeps its pi session and can be revived later (`node lifecycle revive`). Use `node lifecycle recycle` instead to FINISH the agent in your pane with a final report, and `push final` when a worker should end its own work normally (Alt+C → x)',
|
|
639
642
|
help: {
|
|
640
643
|
name: 'node lifecycle close',
|
|
641
|
-
summary: 'close a node and cascade-cancel its subtree — kill its tmux window plus those of every descendant it EXCLUSIVELY owns (down the subscribes_to spine), mark them all canceled, and leave each a notice it reads on resume. A descendant still subscribed to by a manager outside the subtree is left running. Nothing is deleted: every closed node keeps its pi session and can be revived later (`crtr
|
|
644
|
+
summary: 'close a node and cascade-cancel its subtree — kill its tmux window plus those of every descendant it EXCLUSIVELY owns (down the subscribes_to spine), mark them all canceled, and leave each a notice it reads on resume. A descendant still subscribed to by a manager outside the subtree is left running. Nothing is deleted: every closed node keeps its pi session and can be revived later (`crtr node lifecycle revive`)',
|
|
642
645
|
params: [
|
|
643
646
|
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node to close. Defaults to the node occupying --pane (or your current pane).' },
|
|
644
647
|
{ kind: 'flag', name: 'pane', type: 'string', required: false, constraint: 'tmux pane id whose node to close. Defaults to $TMUX_PANE / your current pane. The Alt+C menu passes this for you.' },
|
|
@@ -1539,13 +1542,13 @@ const nodeDemote = defineLeaf({
|
|
|
1539
1542
|
// ---------------------------------------------------------------------------
|
|
1540
1543
|
const nodeLifecycle = defineBranch({
|
|
1541
1544
|
name: 'lifecycle',
|
|
1542
|
-
description: 'lifecycle actions — recycle, close, demote a node',
|
|
1543
|
-
whenToUse: 'you are ending or
|
|
1545
|
+
description: 'lifecycle actions — recycle, close, demote, or revive a node',
|
|
1546
|
+
whenToUse: 'you are ending, transitioning, or bringing back one node you can name: finish the agent in your pane and reboot a fresh root (`recycle`), tear a node + its exclusive subtree down without finishing (`close`), flip a node onto a finishing track in place (`demote`), or bring a dormant node back (`revive`). To merely SET the terminal/resident attribute headlessly use `node config --lifecycle`; to FINISH from inside a node\'s own turn use `crtr push final`; to mass-reconnect EVERY disconnected node at once use `canvas revive --all`.',
|
|
1544
1547
|
help: {
|
|
1545
1548
|
name: 'node lifecycle',
|
|
1546
|
-
summary: 'end or
|
|
1549
|
+
summary: 'end, transition, or revive a node — recycle the pane, close + cascade-cancel a subtree, demote to terminal in place, or revive a dormant node',
|
|
1547
1550
|
},
|
|
1548
|
-
children: [nodeRecycle, nodeClose, nodeDemote],
|
|
1551
|
+
children: [nodeRecycle, nodeClose, nodeDemote, nodeReviveLeaf],
|
|
1549
1552
|
});
|
|
1550
1553
|
// ---------------------------------------------------------------------------
|
|
1551
1554
|
// node yield — refresh: discard context, revive fresh against the roadmap
|
|
@@ -1820,6 +1823,6 @@ export function registerNode() {
|
|
|
1820
1823
|
'HOW: the new leaf returns a node id immediately and runs the worker as a detached headless broker; no viewer opens unless a surface asks for one. Match the kind to the work and read the leaf schema before invoking. You are woken when a child finishes — the wake message ALREADY IS the coalesced digest, so dereference the report paths in that digest that matter instead of acting on a one-line label. Integrate, then either delegate the next units or finish.\n\n' +
|
|
1821
1824
|
'FINISH: a worker ends its own work with a final push, which writes the canonical result, marks done, and closes the window; stopping without it is not finishing. For a job too big for one context window, promote to an orchestrator; when context fills, yield to refresh against that roadmap.',
|
|
1822
1825
|
},
|
|
1823
|
-
children: [nodeNew, nodeMsg, nodeSubscribe, nodeUnsubscribe, nodePromote, nodeYield, nodeFocus, nodeCycle, nodeInspect, nodeLifecycle, nodeConfig, nodeTriggers],
|
|
1826
|
+
children: [nodeNew, nodeMsg, nodeSubscribe, nodeUnsubscribe, nodePromote, nodeYield, nodeFocus, nodeCycle, nodeInspect, nodeLifecycle, nodeConfig, nodeTriggers, nodeWorktreeBranch],
|
|
1824
1827
|
});
|
|
1825
1828
|
}
|
package/dist/commands/push.js
CHANGED
|
@@ -101,7 +101,7 @@ function makeTierLeaf(tier) {
|
|
|
101
101
|
throw new InputError({
|
|
102
102
|
error: 'open_managed_worktree',
|
|
103
103
|
message: 'This node still owns an open managed worktree — `push final` is blocked until it is closed.',
|
|
104
|
-
next: 'Commit the remaining work in the worktree, run `crtr worktree close`, then rerun `crtr push final`.',
|
|
104
|
+
next: 'Commit the remaining work in the worktree, run `crtr node worktree close`, then rerun `crtr push final`.',
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
// A RESIDENT node with no subscribers is human-driven and has no one to
|
|
@@ -1,12 +1,2 @@
|
|
|
1
1
|
import type { LeafDef } from '../core/command.js';
|
|
2
|
-
import type { Fault } from '../core/runtime/fault.js';
|
|
3
|
-
/** --now eligibility for a live, non-busy fault: a daemon-owned AUTO retry
|
|
4
|
-
* (kick it early instead of waiting out the schedule), or any fault the
|
|
5
|
-
* canvas dashboard flags "needs you" (`faultNeedsYou`, status-glyph.ts) — the
|
|
6
|
-
* disposition that otherwise has NO sanctioned recovery verb (issue #115).
|
|
7
|
-
* Both are scoped to `pi→provider`: refuses a client-owned auto retry
|
|
8
|
-
* (redialing) already in flight, and refuses any OTHER link — notably a
|
|
9
|
-
* `daemon→node` wedge fault, which --now can't clear (SIGTERM doesn't touch
|
|
10
|
-
* the runaway subprocess; see `wedged-child-on-runaway-bash`). */
|
|
11
|
-
export declare function isNowEligibleFault(fault: Fault | null): boolean;
|
|
12
2
|
export declare const reviveLeaf: LeafDef;
|
package/dist/commands/revive.js
CHANGED
|
@@ -1,90 +1,42 @@
|
|
|
1
|
-
// `crtr canvas revive` —
|
|
1
|
+
// `crtr canvas revive` — mass node recovery.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// `.jsonl` already exists it CYCLES in place instead (same file, a fresh
|
|
8
|
-
// `crtr-cycle` branch — `resumed:false`), and only starts a genuinely clean
|
|
9
|
-
// session when no `.jsonl` exists yet (also `resumed:false`).
|
|
3
|
+
// Sweeps EVERY disconnected node (engine not running, resumable session) in
|
|
4
|
+
// one shot — the recovery for a reboot / mass-crash / daemon-down-a-while
|
|
5
|
+
// event. It is a two-step gate: run it once to PREVIEW the candidates
|
|
6
|
+
// (nothing is launched), then confirm to actually revive them.
|
|
10
7
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// event. It is a two-step gate: run it once to PREVIEW the candidates (nothing is
|
|
14
|
-
// launched), then confirm to actually revive them.
|
|
8
|
+
// Reviving ONE named node is a per-node lifecycle action and lives instead at
|
|
9
|
+
// `crtr node lifecycle revive <id>` (src/commands/node-lifecycle-revive.ts).
|
|
15
10
|
import { defineLeaf } from '../core/command.js';
|
|
16
11
|
import { InputError } from '../core/io.js';
|
|
17
|
-
import { reviveNode } from '../core/runtime/revive.js';
|
|
18
12
|
import { listDisconnected, reviveAll } from '../core/runtime/revive-all.js';
|
|
19
|
-
import { waitForBrokerViewSocket } from '../core/runtime/placement.js';
|
|
20
13
|
import { getNode, fullName } from '../core/canvas/index.js';
|
|
21
|
-
import { isPidAlive } from '../core/canvas/pid.js';
|
|
22
|
-
import { readFault } from '../core/runtime/fault.js';
|
|
23
|
-
import { isBusy } from '../core/runtime/busy.js';
|
|
24
|
-
import { faultNeedsYou } from '../core/canvas/status-glyph.js';
|
|
25
|
-
/** --now eligibility for a live, non-busy fault: a daemon-owned AUTO retry
|
|
26
|
-
* (kick it early instead of waiting out the schedule), or any fault the
|
|
27
|
-
* canvas dashboard flags "needs you" (`faultNeedsYou`, status-glyph.ts) — the
|
|
28
|
-
* disposition that otherwise has NO sanctioned recovery verb (issue #115).
|
|
29
|
-
* Both are scoped to `pi→provider`: refuses a client-owned auto retry
|
|
30
|
-
* (redialing) already in flight, and refuses any OTHER link — notably a
|
|
31
|
-
* `daemon→node` wedge fault, which --now can't clear (SIGTERM doesn't touch
|
|
32
|
-
* the runaway subprocess; see `wedged-child-on-runaway-bash`). */
|
|
33
|
-
export function isNowEligibleFault(fault) {
|
|
34
|
-
if (fault === null || fault.link !== 'pi→provider')
|
|
35
|
-
return false;
|
|
36
|
-
if (faultNeedsYou(fault))
|
|
37
|
-
return true;
|
|
38
|
-
return fault.retry.by === 'daemon';
|
|
39
|
-
}
|
|
40
14
|
// ---------------------------------------------------------------------------
|
|
41
|
-
// revive
|
|
15
|
+
// revive --all
|
|
42
16
|
// ---------------------------------------------------------------------------
|
|
43
17
|
export const reviveLeaf = defineLeaf({
|
|
44
18
|
name: 'revive',
|
|
45
|
-
description: '
|
|
46
|
-
whenToUse: 'you
|
|
19
|
+
description: 'mass-reconnect every disconnected node',
|
|
20
|
+
whenToUse: 'you need to bring back EVERY disconnected node at once (engine not running but a saved session intact) — the recovery after a reboot, a killed login/tmux session, a mass crash, or the daemon being down a while. You rarely need this for an ordinary crash — the daemon auto-revives those. To bring back ONE named node (a follow-up on a finished worker, a canceled node, or an on-demand kick of a hanging one), use `node lifecycle revive <id>` instead',
|
|
47
21
|
help: {
|
|
48
22
|
name: 'canvas revive',
|
|
49
|
-
summary: '
|
|
23
|
+
summary: 'mass-reconnect every disconnected node (engine not running, resumable saved session)',
|
|
50
24
|
params: [
|
|
51
|
-
{
|
|
52
|
-
kind: 'positional',
|
|
53
|
-
name: 'node',
|
|
54
|
-
required: false,
|
|
55
|
-
constraint: 'Node id to revive. Omit when using --all.',
|
|
56
|
-
},
|
|
57
25
|
{
|
|
58
26
|
kind: 'flag',
|
|
59
27
|
name: 'all',
|
|
60
28
|
type: 'bool',
|
|
61
29
|
required: false,
|
|
62
30
|
default: false,
|
|
63
|
-
constraint: '
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
kind: 'flag',
|
|
67
|
-
name: 'fresh',
|
|
68
|
-
type: 'bool',
|
|
69
|
-
required: false,
|
|
70
|
-
default: false,
|
|
71
|
-
constraint: 'When set, do NOT do a true resume. If the node already has a session `.jsonl`, this instead CYCLES it in place — reuses that SAME file, resets the tree leaf, and roots a fresh `crtr-cycle` branch (a clean-context restart that still carries the file\'s history, output `resumed:false`). Only when no `.jsonl` exists yet does this start a genuinely brand-new session (also `resumed:false`). Default (no --fresh): a true resume — replays the existing `.jsonl` as-is (`resumed:true`). Ignored with --all (which always resumes).',
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
kind: 'flag',
|
|
75
|
-
name: 'now',
|
|
76
|
-
type: 'bool',
|
|
77
|
-
required: false,
|
|
78
|
-
default: false,
|
|
79
|
-
constraint: 'On-demand kick of an active-fault node (parked on a `pi→provider` fault, broker still alive). Its broker pid is alive, so an ordinary revive no-ops (double-launch guard) — --now SIGTERMs the live broker instead, so the daemon\'s crash→grace→resume path brings it back in ~20s. Valid for a live `pi→provider` fault that is either a daemon-owned auto retry (kicks it early), or ANY fault the canvas dashboard flags "needs you" (e.g. exhausted retries, an auth fault you just fixed) — that disposition otherwise has no recovery verb. Mutually exclusive with --all/--fresh.',
|
|
31
|
+
constraint: 'Required (rejected without it) — confirms this is the graph-wide sweep, not a typo for a single-node revive (that lives at `node lifecycle revive <id>`). Excludes done/canceled (terminal-by-choice). Run it once to PREVIEW the candidates; nothing is launched until you confirm.',
|
|
80
32
|
},
|
|
81
|
-
// --force is a DELIBERATELY UNDOCUMENTED confirmation gate
|
|
82
|
-
//
|
|
83
|
-
// can have large, unforeseen consequences, so the agent must NOT be able
|
|
84
|
-
// discover a one-shot bypass by reading the schema. The unforced --all
|
|
85
|
-
// PREVIEWS the candidates and instructs the agent to double-check
|
|
86
|
-
// USER before re-running; only then, having been told the flag,
|
|
87
|
-
// --force. Documenting it here would defeat that gate.
|
|
33
|
+
// --force is a DELIBERATELY UNDOCUMENTED confirmation gate (hidden from
|
|
34
|
+
// -h; see FlagParam.hidden). Silas's intent (2026-06-13): a mass revive
|
|
35
|
+
// can have large, unforeseen consequences, so the agent must NOT be able
|
|
36
|
+
// to discover a one-shot bypass by reading the schema. The unforced --all
|
|
37
|
+
// path PREVIEWS the candidates and instructs the agent to double-check
|
|
38
|
+
// with the USER before re-running; only then, having been told the flag,
|
|
39
|
+
// does it pass --force. Documenting it here would defeat that gate.
|
|
88
40
|
{
|
|
89
41
|
kind: 'flag',
|
|
90
42
|
name: 'force',
|
|
@@ -92,135 +44,33 @@ export const reviveLeaf = defineLeaf({
|
|
|
92
44
|
required: false,
|
|
93
45
|
default: false,
|
|
94
46
|
hidden: true,
|
|
95
|
-
constraint: 'Confirm
|
|
47
|
+
constraint: 'Confirm the sweep (internal gate; see code comment).',
|
|
96
48
|
},
|
|
97
49
|
],
|
|
98
50
|
output: [
|
|
99
|
-
{ name: '
|
|
100
|
-
{ name: '
|
|
101
|
-
{ name: '
|
|
102
|
-
{ name: '
|
|
103
|
-
{ name: 'mode', type: 'string', required: false, constraint: '"preview" (the --all candidate list, nothing launched), "revived" (the --all sweep ran), or absent for a single-node revive.' },
|
|
104
|
-
{ name: 'candidates', type: 'string', required: false, constraint: '--all preview: the node ids that WOULD be revived (newline-joined). Empty when none are disconnected.' },
|
|
105
|
-
{ name: 'revived', type: 'string', required: false, constraint: '--all sweep: the node ids whose broker engine was relaunched (newline-joined).' },
|
|
106
|
-
{ name: 'failed', type: 'string', required: false, constraint: '--all sweep: node ids whose relaunch threw, with the error (newline-joined). Absent when none failed.' },
|
|
107
|
-
{ name: 'kicked', type: 'boolean', required: false, constraint: '--now only: true when the hanging node\'s live broker was SIGTERM\'d (the daemon resumes it on the saved session within ~20s). Absent for an ordinary revive.' },
|
|
51
|
+
{ name: 'mode', type: 'string', required: true, constraint: '"preview" (the candidate list, nothing launched) or "revived" (the sweep ran).' },
|
|
52
|
+
{ name: 'candidates', type: 'string', required: false, constraint: 'Preview: the node ids that WOULD be revived (newline-joined). Empty when none are disconnected.' },
|
|
53
|
+
{ name: 'revived', type: 'string', required: false, constraint: 'Sweep: the node ids whose broker engine was relaunched (newline-joined).' },
|
|
54
|
+
{ name: 'failed', type: 'string', required: false, constraint: 'Sweep: node ids whose relaunch threw, with the error (newline-joined). Absent when none failed.' },
|
|
108
55
|
],
|
|
109
56
|
outputKind: 'object',
|
|
110
57
|
effects: [
|
|
111
|
-
'Launches the
|
|
112
|
-
'Updates
|
|
113
|
-
'
|
|
114
|
-
'--all without confirmation launches NOTHING — it only previews the disconnected candidates.',
|
|
58
|
+
'Launches the detached headless broker engine (no tmux window) for every disconnected node.',
|
|
59
|
+
'Updates each revived node\'s canvas record: status=active, intent=null.',
|
|
60
|
+
'Without --force: launches NOTHING — only previews the disconnected candidates.',
|
|
115
61
|
],
|
|
116
62
|
},
|
|
117
63
|
run: async (input) => {
|
|
118
|
-
|
|
119
|
-
const now = input['now'] ?? false;
|
|
120
|
-
const nodeId = input['node'];
|
|
121
|
-
if (all) {
|
|
122
|
-
if (now) {
|
|
123
|
-
throw new InputError({
|
|
124
|
-
error: 'conflicting_args',
|
|
125
|
-
message: '`--now` kicks ONE hanging node; it cannot combine with `--all`.',
|
|
126
|
-
next: 'Run `crtr canvas revive <node> --now` for the hanging node, or `crtr canvas revive --all` (no --now).',
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
if (nodeId !== undefined) {
|
|
130
|
-
throw new InputError({
|
|
131
|
-
error: 'conflicting_args',
|
|
132
|
-
message: '`--all` reconnects every disconnected node; do not also pass a node id.',
|
|
133
|
-
next: 'Run `crtr canvas revive --all` (no node id), or `crtr canvas revive <node>` for one.',
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
return runReviveAll(input);
|
|
137
|
-
}
|
|
138
|
-
if (nodeId === undefined) {
|
|
64
|
+
if (input['all'] !== true) {
|
|
139
65
|
throw new InputError({
|
|
140
66
|
error: 'missing_parameter',
|
|
141
|
-
message: '
|
|
142
|
-
next: 'Run `crtr canvas revive
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
const fresh = input['fresh'] ?? false;
|
|
146
|
-
if (now && fresh) {
|
|
147
|
-
throw new InputError({
|
|
148
|
-
error: 'conflicting_args',
|
|
149
|
-
message: '`--now` kicks a live hanging broker (SIGTERM → daemon resumes the SAVED session); `--fresh` is meaningless there.',
|
|
150
|
-
next: 'Run `crtr canvas revive <node> --now` (resumes), or `crtr canvas revive <node> --fresh` (no --now) to relaunch clean.',
|
|
67
|
+
message: '`canvas revive` is the mass-recovery sweep and requires --all.',
|
|
68
|
+
next: 'Run `crtr canvas revive --all` to reconnect every disconnected node, or `crtr node lifecycle revive <id>` to bring back one named node.',
|
|
151
69
|
});
|
|
152
70
|
}
|
|
153
|
-
|
|
154
|
-
const meta = getNode(nodeId);
|
|
155
|
-
if (meta === null) {
|
|
156
|
-
throw new InputError({
|
|
157
|
-
error: 'not_found',
|
|
158
|
-
message: `no node: ${nodeId}`,
|
|
159
|
-
next: 'List nodes with `crtr node inspect list`.',
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
// --now: the on-demand kick for an active fault. The broker is ALIVE (so
|
|
163
|
-
// reviveNode would no-op the double-launch guard) — SIGTERM it so the daemon's
|
|
164
|
-
// ordinary crash→grace→resume path recovers it on the saved session, the same
|
|
165
|
-
// thing the daemon does at the auto-recovery grace, just on demand. Gated on
|
|
166
|
-
// a live pid and an eligible pi→provider fault (`isNowEligibleFault` above) so
|
|
167
|
-
// it can't be used to nuke a healthy node.
|
|
168
|
-
if (now) {
|
|
169
|
-
const pid = meta.pi_pid;
|
|
170
|
-
const stall = readFault(nodeId);
|
|
171
|
-
if (pid == null || !isPidAlive(pid)) {
|
|
172
|
-
throw new InputError({
|
|
173
|
-
error: 'not_hanging',
|
|
174
|
-
message: `${nodeId} has no live broker — nothing to kick. --now is for a HANGING node (live broker parked on an active fault).`,
|
|
175
|
-
next: 'Revive a dormant node with `crtr canvas revive ' + nodeId + '` (no --now).',
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
if (!isNowEligibleFault(stall)) {
|
|
179
|
-
throw new InputError({
|
|
180
|
-
error: 'not_hanging',
|
|
181
|
-
message: `${nodeId} is live but has no pi→provider fault eligible for --now (either healthy, or a client-owned retry / non-provider fault already being handled elsewhere) — --now refuses to SIGTERM a node that isn't stuck on a kickable provider fault.`,
|
|
182
|
-
next: 'Use --now only on a node the canvas shows as hanging (⚠) with a `pi→provider` fault. For a routine relaunch, omit --now.',
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
// The daemon never kills a BUSY engine (its verdict paths all return 'leave'
|
|
186
|
-
// on a live busy marker): a node mid-turn is making progress, not wedged —
|
|
187
|
-
// refuse rather than SIGTERM it mid-flight.
|
|
188
|
-
if (isBusy(nodeId)) {
|
|
189
|
-
throw new InputError({
|
|
190
|
-
error: 'busy',
|
|
191
|
-
message: `${nodeId} is mid-turn right now (busy) — --now refuses to SIGTERM a working engine; it has recovered on its own.`,
|
|
192
|
-
next: 'Re-check the canvas — if it\'s no longer marked hanging (⚠), nothing to do.',
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
try {
|
|
196
|
-
process.kill(pid, 'SIGTERM');
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
/* best-effort: the broker may have just died; the daemon owns the resume */
|
|
200
|
-
}
|
|
201
|
-
return {
|
|
202
|
-
window: null,
|
|
203
|
-
session: meta.tmux_session ?? null,
|
|
204
|
-
kicked: true,
|
|
205
|
-
message: `Sent SIGTERM to ${fullName(meta)} (${nodeId}, pid ${pid}). The daemon will resume it on the saved session within ~20s (crash→grace→resume).`,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
const result = reviveNode(nodeId, { resume: !fresh });
|
|
209
|
-
// Revive returns once the broker process is launched, which can be seconds
|
|
210
|
-
// before its view.sock listens. Callers (attach, the web shell's Wake) dial
|
|
211
|
-
// immediately on return, so block here until the socket accepts.
|
|
212
|
-
const ready = await waitForBrokerViewSocket(nodeId, result.launch?.exited);
|
|
213
|
-
return {
|
|
214
|
-
window: result.window ?? undefined,
|
|
215
|
-
session: result.session,
|
|
216
|
-
resumed: result.resumed,
|
|
217
|
-
ready,
|
|
218
|
-
};
|
|
71
|
+
return runReviveAll(input);
|
|
219
72
|
},
|
|
220
73
|
});
|
|
221
|
-
// ---------------------------------------------------------------------------
|
|
222
|
-
// revive --all
|
|
223
|
-
// ---------------------------------------------------------------------------
|
|
224
74
|
/** One line per candidate: `<id> <full name> [<status>]`. */
|
|
225
75
|
function describe(ids) {
|
|
226
76
|
return ids
|
|
@@ -100,8 +100,8 @@ test('a fresh revive is pointed at its subscriptions\' on-disk report history (c
|
|
|
100
100
|
assert.ok(!msg.includes(older), 'absolute report paths not rendered');
|
|
101
101
|
assert.ok(!msg.includes(newer), 'absolute report paths not rendered');
|
|
102
102
|
assert.ok(/canvas history read <ref>/.test(msg), 'points at one-report expansion');
|
|
103
|
-
assert.ok(/
|
|
104
|
-
assert.ok(/
|
|
103
|
+
assert.ok(/node inspect artifacts <node-id> --type report/.test(msg), 'points at full per-node expansion');
|
|
104
|
+
assert.ok(/node inspect artifacts <node-id> --type inbox/.test(msg), 'points at the cursor-independent full-history replay');
|
|
105
105
|
});
|
|
106
106
|
test('buildReviveKickoff is pure — building twice eats nothing', () => {
|
|
107
107
|
const id = 'n2';
|
package/dist/core/canvas/boot.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// broker it once named is long gone. Left unchecked this fools BOTH the
|
|
11
11
|
// daemon's own liveness poll (crtrd.ts) and reviveNode's double-launch guard
|
|
12
12
|
// (revive.ts) into believing a dead broker is still running — the node then
|
|
13
|
-
// stays permanently unrevivable until a human runs `
|
|
13
|
+
// stays permanently unrevivable until a human runs `node lifecycle revive --fresh`.
|
|
14
14
|
//
|
|
15
15
|
// The signal (primary): the Linux kernel's per-boot identity at
|
|
16
16
|
// `/proc/sys/kernel/random/boot_id` — a fresh random UUID minted on every
|
|
@@ -80,12 +80,12 @@ export async function runBrowse(opts = {}) {
|
|
|
80
80
|
// Safety net: an uncaught throw in the (un-unit-tested) keystroke path must
|
|
81
81
|
// never strand the tty in raw + alt-screen + hidden-cursor.
|
|
82
82
|
process.once('exit', cleanup);
|
|
83
|
-
/** On-demand kick of a hanging node: shell `
|
|
84
|
-
* SIGTERMs the live broker — the daemon resumes it). Synchronous +
|
|
85
|
-
* (stdio ignored) so it never takes over the TUI; best-effort. */
|
|
83
|
+
/** On-demand kick of a hanging node: shell `node lifecycle revive <id> --now`
|
|
84
|
+
* (which SIGTERMs the live broker — the daemon resumes it). Synchronous +
|
|
85
|
+
* quiet (stdio ignored) so it never takes over the TUI; best-effort. */
|
|
86
86
|
const reviveNow = (id) => {
|
|
87
87
|
try {
|
|
88
|
-
execFileSync('crtr', ['
|
|
88
|
+
execFileSync('crtr', ['node', 'lifecycle', 'revive', id, '--now'], { stdio: 'ignore' });
|
|
89
89
|
}
|
|
90
90
|
catch {
|
|
91
91
|
/* best-effort — the SIGTERM is what matters; the daemon owns the resume */
|
|
@@ -471,8 +471,8 @@ export async function runBrowse(opts = {}) {
|
|
|
471
471
|
}
|
|
472
472
|
// Revive now (on-demand kick): for a HANGING node, SIGTERM the live broker so
|
|
473
473
|
// the daemon's crash→grace→resume path brings it back in ~20s instead of
|
|
474
|
-
// waiting out the 5-min auto-revive grace. Shells `
|
|
475
|
-
// (browse goes through commands, never touching the runtime directly). 'k' is
|
|
474
|
+
// waiting out the 5-min auto-revive grace. Shells `node lifecycle revive <id>
|
|
475
|
+
// --now` (browse goes through commands, never touching the runtime directly). 'k' is
|
|
476
476
|
// vim-up here, so the kick is on SHIFT-K.
|
|
477
477
|
if (input === 'K') {
|
|
478
478
|
if (row !== undefined) {
|
|
@@ -472,7 +472,7 @@ export function renderFrame(state, size, caps = { color: false, color256: false
|
|
|
472
472
|
}
|
|
473
473
|
else {
|
|
474
474
|
// When the cursor row has an active fault, lead the footer with the on-demand kick
|
|
475
|
-
// hint (K shells `
|
|
475
|
+
// hint (K shells `node lifecycle revive <id> --now` — immediate revive instead of
|
|
476
476
|
// waiting for the daemon's retry schedule).
|
|
477
477
|
const selRow = state.visible[state.cursor];
|
|
478
478
|
const cursorRow = selRow !== undefined ? state.tree.nodes.get(selRow.id)?.row : undefined;
|
|
@@ -26,7 +26,7 @@ export declare function resolveNodeVisual(status: NodeStatus, opts: {
|
|
|
26
26
|
/** True for exactly the fault dispositions `faultDispositionText` renders as
|
|
27
27
|
* "needs you" — nothing (sdk/daemon/client) is auto-driving recovery, so the
|
|
28
28
|
* dashboard flags the node for the human. Exported so a recovery verb (e.g.
|
|
29
|
-
* `
|
|
29
|
+
* `node lifecycle revive --now`, src/commands/node-lifecycle-revive.ts) can gate on the SAME
|
|
30
30
|
* predicate the dashboard advertises instead of drifting out of sync with it
|
|
31
31
|
* (issue #115 — `--now` used to reject exactly the nodes shown as "needs you"). */
|
|
32
32
|
export declare function faultNeedsYou(fault: Pick<Fault, 'retry'>): boolean;
|
|
@@ -73,7 +73,7 @@ function retryCountdown(iso, now) {
|
|
|
73
73
|
/** True for exactly the fault dispositions `faultDispositionText` renders as
|
|
74
74
|
* "needs you" — nothing (sdk/daemon/client) is auto-driving recovery, so the
|
|
75
75
|
* dashboard flags the node for the human. Exported so a recovery verb (e.g.
|
|
76
|
-
* `
|
|
76
|
+
* `node lifecycle revive --now`, src/commands/node-lifecycle-revive.ts) can gate on the SAME
|
|
77
77
|
* predicate the dashboard advertises instead of drifting out of sync with it
|
|
78
78
|
* (issue #115 — `--now` used to reject exactly the nodes shown as "needs you"). */
|
|
79
79
|
export function faultNeedsYou(fault) {
|