@crouton-kit/crouter 0.3.14 → 0.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +45 -67
- package/dist/commands/human.js +4 -14
- package/dist/commands/node.d.ts +11 -0
- package/dist/commands/node.js +221 -99
- 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 +129 -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 +196 -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 +266 -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 +178 -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 +334 -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 +105 -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 +205 -4
- package/dist/core/canvas/focuses.d.ts +22 -0
- package/dist/core/canvas/focuses.js +81 -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 +24 -12
- 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 +26 -12
- package/dist/core/runtime/launch.js +78 -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 +39 -1
- package/dist/core/runtime/nodes.js +69 -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 +299 -0
- package/dist/core/runtime/placement.js +688 -0
- 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-chrome.d.ts +1 -0
- package/dist/core/runtime/tmux-chrome.js +4 -0
- package/dist/core/runtime/tmux.d.ts +113 -20
- package/dist/core/runtime/tmux.js +221 -39
- 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 +594 -262
- package/dist/pi-extensions/canvas-resume.d.ts +22 -0
- package/dist/pi-extensions/canvas-resume.js +173 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
- package/dist/pi-extensions/canvas-stophook.js +340 -228
- package/dist/types.d.ts +28 -0
- package/dist/types.js +16 -0
- package/package.json +2 -2
- package/dist/core/runtime/presence.d.ts +0 -38
- package/dist/core/runtime/presence.js +0 -154
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// The canvas: one global graph of nodes + edges. Phase 0 of the pi-native
|
|
2
2
|
// agent runtime. Topology in sqlite (WAL), node flesh on disk.
|
|
3
3
|
export * from './types.js';
|
|
4
|
+
export * from './labels.js';
|
|
4
5
|
export * from './paths.js';
|
|
5
6
|
export * from './canvas.js';
|
|
7
|
+
export * from './focuses.js';
|
|
8
|
+
export * from './telemetry.js';
|
|
6
9
|
export { openDb, closeDb } from './db.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { NodeMeta } from './types.js';
|
|
2
|
+
/** The minimal shape `fullName` needs — satisfied by NodeMeta and by anything
|
|
3
|
+
* else carrying name + kind (description optional). */
|
|
4
|
+
export interface NamedNode {
|
|
5
|
+
name: string;
|
|
6
|
+
kind: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
/** A node's human label: its explicit handle plus the pi-generated description
|
|
10
|
+
* of its first task, e.g. `fix-auth refactor-login-flow`. The handle is
|
|
11
|
+
* dropped when it's just the kind default (non-informative, and the kind is
|
|
12
|
+
* surfaced elsewhere), so a default-named node reads as its description alone
|
|
13
|
+
* (`refactor-login-flow`). Falls back to the bare name when no description
|
|
14
|
+
* exists yet (a node not yet named, e.g. a bare root before its first
|
|
15
|
+
* message). Never empty. */
|
|
16
|
+
export declare function fullName(node: NamedNode): string;
|
|
17
|
+
/** The pi session display name — the editor label in the top-left. Format is
|
|
18
|
+
* `<kind> (<mode>) <fullName> <cycle>` where `<fullName>` is the node's handle
|
|
19
|
+
* plus the pi-generated description of its first task (see fullName) and
|
|
20
|
+
* `<cycle>` is the revive count (meta.cycles). So `developer (orchestrator)
|
|
21
|
+
* fix-auth refactor-auth-flow 2` reads as a developer orchestrator on its 2nd
|
|
22
|
+
* cycle working the auth refactor. The name segment is omitted while it
|
|
23
|
+
* collapses to the bare kind (a bare root before its first message is named).
|
|
24
|
+
* Recomputed from meta on every revive (and pushed live via pi.setSessionName
|
|
25
|
+
* when a bare root is named mid-session), so a base→orchestrator polymorph, a
|
|
26
|
+
* fresh cycle, or a first-message naming all update the label. */
|
|
27
|
+
export declare function editorLabel(meta: NodeMeta): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// labels.ts — human-facing node labels derived from a node's meta.
|
|
2
|
+
//
|
|
3
|
+
// One source of truth for "what do we call this node on screen". `fullName`
|
|
4
|
+
// combines the node's explicit handle (meta.name) with the pi-generated
|
|
5
|
+
// description of its first task (meta.description) — the same string the editor
|
|
6
|
+
// label, the canvas dashboard tree, the nav spine, and the tmux window tab all
|
|
7
|
+
// render, so a node reads identically everywhere it appears.
|
|
8
|
+
/** A node's human label: its explicit handle plus the pi-generated description
|
|
9
|
+
* of its first task, e.g. `fix-auth refactor-login-flow`. The handle is
|
|
10
|
+
* dropped when it's just the kind default (non-informative, and the kind is
|
|
11
|
+
* surfaced elsewhere), so a default-named node reads as its description alone
|
|
12
|
+
* (`refactor-login-flow`). Falls back to the bare name when no description
|
|
13
|
+
* exists yet (a node not yet named, e.g. a bare root before its first
|
|
14
|
+
* message). Never empty. */
|
|
15
|
+
export function fullName(node) {
|
|
16
|
+
const desc = (node.description ?? '').trim();
|
|
17
|
+
const handle = node.name && node.name !== node.kind ? node.name : '';
|
|
18
|
+
const combined = [handle, desc].filter((s) => s !== '').join(' ');
|
|
19
|
+
return combined !== '' ? combined : node.name;
|
|
20
|
+
}
|
|
21
|
+
/** The pi session display name — the editor label in the top-left. Format is
|
|
22
|
+
* `<kind> (<mode>) <fullName> <cycle>` where `<fullName>` is the node's handle
|
|
23
|
+
* plus the pi-generated description of its first task (see fullName) and
|
|
24
|
+
* `<cycle>` is the revive count (meta.cycles). So `developer (orchestrator)
|
|
25
|
+
* fix-auth refactor-auth-flow 2` reads as a developer orchestrator on its 2nd
|
|
26
|
+
* cycle working the auth refactor. The name segment is omitted while it
|
|
27
|
+
* collapses to the bare kind (a bare root before its first message is named).
|
|
28
|
+
* Recomputed from meta on every revive (and pushed live via pi.setSessionName
|
|
29
|
+
* when a bare root is named mid-session), so a base→orchestrator polymorph, a
|
|
30
|
+
* fresh cycle, or a first-message naming all update the label. */
|
|
31
|
+
export function editorLabel(meta) {
|
|
32
|
+
const base = `${meta.kind} (${meta.mode})`;
|
|
33
|
+
const full = fullName(meta);
|
|
34
|
+
const cycle = meta.cycles ?? 0;
|
|
35
|
+
return full !== '' && full !== meta.kind ? `${base} ${full} ${cycle}` : `${base} ${cycle}`;
|
|
36
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { existsSync, readFileSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { getNode, listNodes, subscriptionsOf, view } from './canvas.js';
|
|
18
|
+
import { fullName } from './labels.js';
|
|
18
19
|
import { jobDir } from './paths.js';
|
|
19
20
|
import { countAsks } from './attention.js';
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
@@ -25,6 +26,7 @@ const STATUS_GLYPH = {
|
|
|
25
26
|
idle: '○',
|
|
26
27
|
done: '✓',
|
|
27
28
|
dead: '✗',
|
|
29
|
+
canceled: '⊘',
|
|
28
30
|
};
|
|
29
31
|
function readNodeTelemetry(nodeId) {
|
|
30
32
|
const path = join(jobDir(nodeId), 'telemetry.json');
|
|
@@ -58,7 +60,7 @@ function nodeLine(nodeId, indent, connector) {
|
|
|
58
60
|
const ctx = fmtCtx(tel.tokens_in);
|
|
59
61
|
const asks = countAsks(nodeId);
|
|
60
62
|
const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
|
|
61
|
-
return `${indent}${connector}${glyph} ${node
|
|
63
|
+
return `${indent}${connector}${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}`;
|
|
62
64
|
}
|
|
63
65
|
/**
|
|
64
66
|
* Recursively walk the subscription sub-DAG rooted at `nodeId`, appending
|
|
@@ -110,7 +112,7 @@ export function renderTree(rootId) {
|
|
|
110
112
|
const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
|
|
111
113
|
const glyph = STATUS_GLYPH[node.status] ?? '?';
|
|
112
114
|
const out = [];
|
|
113
|
-
out.push(`${glyph} ${node
|
|
115
|
+
out.push(`${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}`);
|
|
114
116
|
// visited starts with root already rendered (walkTree doesn't re-emit root).
|
|
115
117
|
const visited = new Set([rootId]);
|
|
116
118
|
const children = subscriptionsOf(rootId);
|
|
@@ -138,14 +140,24 @@ export function renderForest() {
|
|
|
138
140
|
// a spawned_by edge + subscribe). Fall back to parent===null because querying
|
|
139
141
|
// the full edge table would require opening the db here.
|
|
140
142
|
//
|
|
141
|
-
// Fine to use parent===null: roots are created by
|
|
143
|
+
// Fine to use parent===null: roots are created by bare `crtr` / `node new --root`
|
|
142
144
|
// without a parent; non-roots always have a parent.
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
145
|
+
//
|
|
146
|
+
// Filter to LIVE roots: each `/new` parks a `done` root (option C relaunch)
|
|
147
|
+
// with parent===null, so an unfiltered forest would render every parked root
|
|
148
|
+
// as a sibling tree and clutter the dashboard. Showing only active|idle roots
|
|
149
|
+
// drops parked (`done`) roots and, as a bonus, stray `dead`/`canceled` roots.
|
|
150
|
+
// Parked roots stay reachable by id (inspect / revive / focus).
|
|
151
|
+
const roots = all.filter((n) => n.parent === null && (n.status === 'active' || n.status === 'idle'));
|
|
152
|
+
// No LIVE roots: render an empty/placeholder forest rather than resurrecting
|
|
153
|
+
// parked (`done`) / dead / canceled roots. The live-only filter is the intent;
|
|
154
|
+
// falling back to all-status roots would re-clutter the dashboard with the very
|
|
155
|
+
// parked trees the filter drops (e.g. a sole root `/quit`'d with no `/new`).
|
|
156
|
+
// Parked roots stay reachable by id (inspect / revive / focus).
|
|
157
|
+
if (roots.length === 0)
|
|
158
|
+
return '(no live roots)';
|
|
147
159
|
const parts = [];
|
|
148
|
-
for (const r of
|
|
160
|
+
for (const r of roots) {
|
|
149
161
|
parts.push(renderTree(r.node_id));
|
|
150
162
|
}
|
|
151
163
|
return parts.join('\n\n');
|
|
@@ -160,7 +172,7 @@ export function dashboardRows(rootId) {
|
|
|
160
172
|
const tel = readNodeTelemetry(id);
|
|
161
173
|
return [{
|
|
162
174
|
node_id: id,
|
|
163
|
-
name: node
|
|
175
|
+
name: fullName(node),
|
|
164
176
|
status: node.status,
|
|
165
177
|
kind: node.kind,
|
|
166
178
|
mode: node.mode,
|
|
@@ -173,9 +185,12 @@ export function dashboardRows(rootId) {
|
|
|
173
185
|
export function dashboardRowsAll() {
|
|
174
186
|
return listNodes().flatMap((row) => {
|
|
175
187
|
const tel = readNodeTelemetry(row.node_id);
|
|
188
|
+
// listNodes() returns the db projection (no description); read the meta to
|
|
189
|
+
// get the full label. Falls back to the row name if the meta is gone.
|
|
190
|
+
const meta = getNode(row.node_id);
|
|
176
191
|
return [{
|
|
177
192
|
node_id: row.node_id,
|
|
178
|
-
name: row.name,
|
|
193
|
+
name: meta !== null ? fullName(meta) : row.name,
|
|
179
194
|
status: row.status,
|
|
180
195
|
kind: row.kind,
|
|
181
196
|
mode: row.mode,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface Telemetry {
|
|
2
|
+
tokens_in?: number;
|
|
3
|
+
tokens_out?: number;
|
|
4
|
+
/** Live context-window size from the last turn_end (pi's getContextUsage). */
|
|
5
|
+
context_tokens?: number;
|
|
6
|
+
model?: string;
|
|
7
|
+
updated_at?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Read a node's telemetry record. Missing/corrupt → {}. Never throws. */
|
|
10
|
+
export declare function readTelemetry(nodeId: string): Telemetry;
|
|
11
|
+
/** The node's current context-window size in tokens, or null when unknown.
|
|
12
|
+
* Prefers the live `context_tokens` gauge; falls back to cumulative
|
|
13
|
+
* `tokens_in` (the dashboard's coarse proxy) only when no live gauge exists. */
|
|
14
|
+
export declare function readContextTokens(nodeId: string): number | null;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// telemetry.ts — read a node's job/telemetry.json (written by canvas-stophook
|
|
2
|
+
// on every turn_end). Best-effort throughout: a missing or corrupt file yields
|
|
3
|
+
// an empty record, never a throw.
|
|
4
|
+
//
|
|
5
|
+
// tokens_in / tokens_out cumulative, non-cached throughput across the session.
|
|
6
|
+
// context_tokens the LIVE context-window gauge (pi's footer figure)
|
|
7
|
+
// captured on the last turn_end — the accurate measure
|
|
8
|
+
// of how full the node's window currently is. Absent
|
|
9
|
+
// until the first turn_end records a usable gauge.
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { jobDir } from './paths.js';
|
|
13
|
+
/** Read a node's telemetry record. Missing/corrupt → {}. Never throws. */
|
|
14
|
+
export function readTelemetry(nodeId) {
|
|
15
|
+
const path = join(jobDir(nodeId), 'telemetry.json');
|
|
16
|
+
if (!existsSync(path))
|
|
17
|
+
return {};
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** The node's current context-window size in tokens, or null when unknown.
|
|
26
|
+
* Prefers the live `context_tokens` gauge; falls back to cumulative
|
|
27
|
+
* `tokens_in` (the dashboard's coarse proxy) only when no live gauge exists. */
|
|
28
|
+
export function readContextTokens(nodeId) {
|
|
29
|
+
const tel = readTelemetry(nodeId);
|
|
30
|
+
if (typeof tel.context_tokens === 'number')
|
|
31
|
+
return tel.context_tokens;
|
|
32
|
+
if (typeof tel.tokens_in === 'number')
|
|
33
|
+
return tel.tokens_in;
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** What a node is doing right now. UI shows active+idle; `done` is hidden but
|
|
2
|
-
* revivable;
|
|
3
|
-
|
|
2
|
+
* revivable; `canceled` is a user-closed node (also hidden, also revivable —
|
|
3
|
+
* not a fault); only `dead` is a fault. */
|
|
4
|
+
export type NodeStatus = 'active' | 'idle' | 'done' | 'dead' | 'canceled';
|
|
4
5
|
/** Does stopping finalize the node? terminal = worker (finalizes on push --final);
|
|
5
6
|
* resident = manager/orchestrator (stays dormant, woken by inbox). */
|
|
6
7
|
export type Lifecycle = 'terminal' | 'resident';
|
|
@@ -25,11 +26,21 @@ export interface LaunchSpec {
|
|
|
25
26
|
/** Extra env injected into the pi process. */
|
|
26
27
|
env: Record<string, string>;
|
|
27
28
|
}
|
|
28
|
-
/** A node's
|
|
29
|
-
*
|
|
30
|
-
|
|
29
|
+
/** A node's DURABLE IDENTITY — the subset that `meta.json` persists on disk.
|
|
30
|
+
* Written rarely (birth, polymorph, session-id capture, naming); never touched
|
|
31
|
+
* by a status flip, an intent change, a focus swap, or a pid stamp. The db row
|
|
32
|
+
* indexes the queryable identity columns; `rebuildIndex()` re-derives them from
|
|
33
|
+
* here. (Live runtime state lives in NodeRuntime, authoritative in the row.) */
|
|
34
|
+
export interface NodeIdentity {
|
|
31
35
|
node_id: string;
|
|
32
36
|
name: string;
|
|
37
|
+
/** A 2-4 word kebab-case handle derived from the node's first prompt (named
|
|
38
|
+
* headlessly by pi; see runtime/naming.ts). Shown in the editor label. */
|
|
39
|
+
description?: string;
|
|
40
|
+
/** How many times this node has been (re)launched — born at 0, bumped on
|
|
41
|
+
* every revive. The trailing N in the editor label, so a refresh/crash cycle
|
|
42
|
+
* reads at a glance. */
|
|
43
|
+
cycles?: number;
|
|
33
44
|
created: string;
|
|
34
45
|
/** The dir this node is pinned to — its cwd (where pi runs, bash executes). */
|
|
35
46
|
cwd: string;
|
|
@@ -37,24 +48,93 @@ export interface NodeMeta {
|
|
|
37
48
|
kind: string;
|
|
38
49
|
mode: Mode;
|
|
39
50
|
lifecycle: Lifecycle;
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
/** The last persona state {mode,lifecycle} the node was GIVEN transition
|
|
52
|
+
* guidance for. Meta-only (not a db column). Born equal to the node's initial
|
|
53
|
+
* {mode,lifecycle} at spawn so a fresh node never gets spurious guidance. The
|
|
54
|
+
* persona injector (runtime/persona.ts) compares the live {mode,lifecycle}
|
|
55
|
+
* against this and, on drift, injects guidance for the new state then commits
|
|
56
|
+
* it here. */
|
|
57
|
+
persona_ack?: {
|
|
58
|
+
mode: Mode;
|
|
59
|
+
lifecycle: Lifecycle;
|
|
60
|
+
};
|
|
61
|
+
/** Spine parent — my manager (who subscribes to me); drives canvas nesting +
|
|
62
|
+
* orphaning. null for a root (top-level, no manager). */
|
|
42
63
|
parent?: string | null;
|
|
64
|
+
/** Provenance — who spawned me (the `spawned_by` edge). Decoupled from
|
|
65
|
+
* `parent` so an INDEPENDENT root (parent=null) still records its lineage.
|
|
66
|
+
* Audit only; null for a user-opened root. Defaults to `parent` for a child. */
|
|
67
|
+
spawned_by?: string | null;
|
|
43
68
|
/** New subscriptions this node opens default to passive when true. */
|
|
44
69
|
passive_default?: boolean;
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
70
|
+
/** REVIVE-HOME — the tmux session a node is (re)opened into when it must
|
|
71
|
+
* generate but is NOT focused (the durable revive target, distinct from the
|
|
72
|
+
* live LOCATION held by the runtime `tmux_session`). Set once at birth
|
|
73
|
+
* (managed child → the shared backstage `nodeSession()`; inline root → the
|
|
74
|
+
* adopted caller session; independent `--root` → the caller session), and
|
|
75
|
+
* rewritten only by demote-recycle. Durable identity (like `cwd`), never
|
|
76
|
+
* touched by a focus swap — this is what keeps a background revive off the
|
|
77
|
+
* user's session. Legacy metas omit it; readers default to
|
|
78
|
+
* `tmux_session ?? nodeSession()` (see `homeSessionOf`). */
|
|
79
|
+
home_session?: string;
|
|
80
|
+
/** The pi session id for `--session <id>` revival. */
|
|
48
81
|
pi_session_id?: string | null;
|
|
82
|
+
/** Absolute path to pi's session `.jsonl` file, captured at session_start via
|
|
83
|
+
* ctx.sessionManager.getSessionFile(). Preferred over pi_session_id when
|
|
84
|
+
* resuming: pi resolves a BARE `--session <id>` relative to the launch cwd
|
|
85
|
+
* first (and shows an interactive cross-project "Fork? [y/N]" prompt when the
|
|
86
|
+
* revive cwd differs from the session's creation cwd), whereas an absolute
|
|
87
|
+
* PATH is opened directly — immune to any cwd discrepancy. Null for older
|
|
88
|
+
* nodes booted before this field existed → revive falls back to the bare id. */
|
|
89
|
+
pi_session_file?: string | null;
|
|
49
90
|
/** Full pi launch recipe; rewritten on every polymorph. */
|
|
50
91
|
launch?: LaunchSpec;
|
|
92
|
+
}
|
|
93
|
+
/** A node's LIVE RUNTIME state — authoritative in the WAL'd `nodes` row, each
|
|
94
|
+
* field mutated by exactly one atomic single-statement `UPDATE` (setStatus /
|
|
95
|
+
* setIntent / setPresence / recordPid+clearPid). NOT persisted to meta.json and
|
|
96
|
+
* NOT re-derivable by `rebuildIndex()` — it describes live process/presence
|
|
97
|
+
* state that is meaningless after the event that would lose the db; the daemon
|
|
98
|
+
* reconciles it from tmux reality, not from a stale file. */
|
|
99
|
+
export interface NodeRuntime {
|
|
100
|
+
/** What the node is doing right now. */
|
|
101
|
+
status: NodeStatus;
|
|
102
|
+
/** Why the node last stopped (done | refresh | idle-release). Drives reap-vs-revive.
|
|
103
|
+
* Optional on the hydrated view (a fresh construction omits it); the row
|
|
104
|
+
* column is always present, defaulting null. */
|
|
105
|
+
intent?: ExitIntent;
|
|
106
|
+
/** OS pid of the live pi process, recorded on boot (stophook session_start).
|
|
107
|
+
* The daemon's authoritative liveness signal: an inline root runs pi as a
|
|
108
|
+
* child of a persistent login shell, so its tmux window outlives a dead pi —
|
|
109
|
+
* window-existence alone can't detect the death, but a dead pid can. Cleared
|
|
110
|
+
* to null by a window-backed relaunch (reviveNode) until the fresh pi
|
|
111
|
+
* re-records it; left intact by an in-place respawn (reviveInPlace) so a
|
|
112
|
+
* failed respawn surfaces as a dead pid. */
|
|
113
|
+
pi_pid?: number | null;
|
|
51
114
|
/** Presence: the tmux session (its root's home) and window this node renders
|
|
52
115
|
* in while active. Cleared when the node goes done/dead and its window closes.
|
|
53
|
-
*
|
|
116
|
+
* The row IS the presence registry (one atomic setPresence per move).
|
|
117
|
+
* v3: a DERIVED CACHE of `pane`'s current location — reconciled from the pane,
|
|
118
|
+
* never trusted when a user move could have desynced them. */
|
|
54
119
|
tmux_session?: string | null;
|
|
55
120
|
window?: string | null;
|
|
121
|
+
/** LOCATION's authoritative handle — the durable tmux `%pane_id` this node's
|
|
122
|
+
* pane is anchored on. tmux preserves it across `move-pane`/`join-pane`/
|
|
123
|
+
* `break-pane` and window renumbering, so `window`/`tmux_session` above are a
|
|
124
|
+
* cache reconciled from it and pane-existence is the liveness probe. A
|
|
125
|
+
* not-focused + not-generating node has `pane = null` (no pane). Authoritative
|
|
126
|
+
* in the row exactly like `window`/`tmux_session` — a RUNTIME field, NOT meta
|
|
127
|
+
* identity — written by the one atomic `setPresence` UPDATE. */
|
|
128
|
+
pane?: string | null;
|
|
56
129
|
}
|
|
57
|
-
/** The
|
|
130
|
+
/** The hydrated node view `getNode()` returns: durable identity (from meta.json)
|
|
131
|
+
* ∪ live runtime (from the row). Keeps the historical `NodeMeta` name and field
|
|
132
|
+
* set so every `meta.X` read across the codebase typechecks unchanged — but
|
|
133
|
+
* `writeMeta` serializes only the NodeIdentity subset and the runtime fields are
|
|
134
|
+
* hydrated from the authoritative row. */
|
|
135
|
+
export type NodeMeta = NodeIdentity & NodeRuntime;
|
|
136
|
+
/** The queryable projection of a node stored as a canvas.db row: the indexed
|
|
137
|
+
* identity columns PLUS the authoritative runtime columns. */
|
|
58
138
|
export interface NodeRow {
|
|
59
139
|
node_id: string;
|
|
60
140
|
name: string;
|
|
@@ -65,6 +145,13 @@ export interface NodeRow {
|
|
|
65
145
|
cwd: string;
|
|
66
146
|
parent: string | null;
|
|
67
147
|
created: string;
|
|
148
|
+
/** Authoritative runtime columns (see NodeRuntime). */
|
|
149
|
+
intent: ExitIntent;
|
|
150
|
+
pi_pid: number | null;
|
|
151
|
+
window: string | null;
|
|
152
|
+
tmux_session: string | null;
|
|
153
|
+
/** The durable LOCATION handle (the tmux `%pane_id`); see NodeRuntime.pane. */
|
|
154
|
+
pane: string | null;
|
|
68
155
|
}
|
|
69
156
|
/** An edge as stored. For `subscribes_to`, `from` is the subscriber and `to`
|
|
70
157
|
* is the publisher (A subscribes_to B ⇒ A receives B's output). For
|
|
@@ -78,6 +165,22 @@ export interface Edge {
|
|
|
78
165
|
active: boolean;
|
|
79
166
|
created: string;
|
|
80
167
|
}
|
|
168
|
+
/** A FOCUS row as stored in the `focuses` table (canvas.db, migration v6): one
|
|
169
|
+
* durable on-screen viewport the user looks at, bound to one node. Plural —
|
|
170
|
+
* many focuses live at once across windows and sessions (the plural
|
|
171
|
+
* generalization of the old single focus pointer). Anchored on the durable tmux
|
|
172
|
+
* `%pane_id`; `session` is a derived cache reconciled from the pane. `node_id`
|
|
173
|
+
* is UNIQUE — a node occupies at most one focus (Q5). */
|
|
174
|
+
export interface FocusRow {
|
|
175
|
+
/** Stable internal id for the viewport (the table's primary key). */
|
|
176
|
+
focus_id: string;
|
|
177
|
+
/** The durable `%pane_id` realizing the focus, or null before it is placed. */
|
|
178
|
+
pane: string | null;
|
|
179
|
+
/** Derived cache of the user session the pane lives in (reconciled from pane). */
|
|
180
|
+
session: string | null;
|
|
181
|
+
/** The node this focus shows. UNIQUE → a node occupies ≤1 focus. */
|
|
182
|
+
node_id: string;
|
|
183
|
+
}
|
|
81
184
|
/** A subscription as seen from one endpoint. */
|
|
82
185
|
export interface SubscriptionRef {
|
|
83
186
|
/** The node id at the other end of the edge. */
|
package/dist/core/command.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RootHelp, RootEntry, BranchHelp, LeafHelp, InputParam } from './help.js';
|
|
1
|
+
import type { RootHelp, RootEntry, BranchHelp, LeafHelp, InputParam, SubTier } from './help.js';
|
|
2
2
|
import { CrtrError } from './errors.js';
|
|
3
3
|
/** Opt-in flag that surfaces a node as an editor slash command (a pi prompt
|
|
4
4
|
* template / Claude Code command). When set, the bootstrap auto-writes a
|
|
@@ -19,6 +19,15 @@ export interface SlashSpec {
|
|
|
19
19
|
export interface LeafDef {
|
|
20
20
|
kind: 'leaf';
|
|
21
21
|
name: string;
|
|
22
|
+
/** Short description for this node's <subcommand> row in its parent's -h. */
|
|
23
|
+
description?: string;
|
|
24
|
+
/** Selection rubric for the parent's listing — plainly states when to reach
|
|
25
|
+
* for this command (expansive with examples for judgment-heavy ones, concise
|
|
26
|
+
* for single-purpose). Rendered verbatim, no prefix. */
|
|
27
|
+
whenToUse?: string;
|
|
28
|
+
/** Visibility tier in ancestor listings (see SubTier). Default 'normal';
|
|
29
|
+
* 'hidden' keeps an internal leaf out of every listing. */
|
|
30
|
+
tier?: SubTier;
|
|
22
31
|
help: LeafHelp;
|
|
23
32
|
/** Opt into editor slash-command exposure (see SlashSpec). */
|
|
24
33
|
slash?: SlashSpec;
|
|
@@ -31,6 +40,15 @@ export interface LeafDef {
|
|
|
31
40
|
export interface BranchDef {
|
|
32
41
|
kind: 'branch';
|
|
33
42
|
name: string;
|
|
43
|
+
/** Short description for this node's <subcommand> row in its parent's -h.
|
|
44
|
+
* Unused on a top-level subtree (its root representation is its rootEntry). */
|
|
45
|
+
description?: string;
|
|
46
|
+
/** Selection rubric for the parent's listing — plainly states when to reach
|
|
47
|
+
* for this command (expansive with examples for judgment-heavy ones, concise
|
|
48
|
+
* for single-purpose). Rendered verbatim, no prefix. */
|
|
49
|
+
whenToUse?: string;
|
|
50
|
+
/** Visibility tier in ancestor listings (see SubTier). Default 'normal'. */
|
|
51
|
+
tier?: SubTier;
|
|
34
52
|
help: BranchHelp;
|
|
35
53
|
/** How this subtree represents itself one level up. Present on top-level
|
|
36
54
|
* subtrees (assembled into root -h by defineRoot); omitted on nested
|
|
@@ -47,6 +65,9 @@ export interface RootDef {
|
|
|
47
65
|
}
|
|
48
66
|
export declare function defineLeaf(opts: {
|
|
49
67
|
name: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
whenToUse?: string;
|
|
70
|
+
tier?: SubTier;
|
|
50
71
|
help: LeafHelp;
|
|
51
72
|
slash?: SlashSpec;
|
|
52
73
|
run: (input: Record<string, unknown>) => Promise<Record<string, unknown> | void>;
|
|
@@ -54,6 +75,9 @@ export declare function defineLeaf(opts: {
|
|
|
54
75
|
}): LeafDef;
|
|
55
76
|
export declare function defineBranch(opts: {
|
|
56
77
|
name: string;
|
|
78
|
+
description?: string;
|
|
79
|
+
whenToUse?: string;
|
|
80
|
+
tier?: SubTier;
|
|
57
81
|
help: BranchHelp;
|
|
58
82
|
rootEntry?: RootEntry;
|
|
59
83
|
slash?: SlashSpec;
|
package/dist/core/command.js
CHANGED
|
@@ -17,6 +17,9 @@ export function defineLeaf(opts) {
|
|
|
17
17
|
return {
|
|
18
18
|
kind: 'leaf',
|
|
19
19
|
name: opts.name,
|
|
20
|
+
description: opts.description,
|
|
21
|
+
whenToUse: opts.whenToUse,
|
|
22
|
+
tier: opts.tier,
|
|
20
23
|
help: opts.help,
|
|
21
24
|
slash: opts.slash,
|
|
22
25
|
run: opts.run,
|
|
@@ -28,24 +31,29 @@ export function defineLeaf(opts) {
|
|
|
28
31
|
function visibleSubCount(def) {
|
|
29
32
|
if (def.kind !== 'branch')
|
|
30
33
|
return 0;
|
|
31
|
-
return def.help.
|
|
34
|
+
return (def.help.listing ?? []).filter((c) => c.tier !== 'hidden').length;
|
|
32
35
|
}
|
|
33
36
|
export function defineBranch(opts) {
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
// Assemble the parent-level listing straight from the child defs — each node
|
|
38
|
+
// owns its own description/whenToUse/tier, so the parent copies nothing
|
|
39
|
+
// (principle 16). Bottom-up construction guarantees a child branch's listing
|
|
40
|
+
// is already computed, so subCount is available here.
|
|
41
|
+
opts.help.listing = opts.children.map((c) => {
|
|
42
|
+
const subCount = visibleSubCount(c);
|
|
43
|
+
return {
|
|
44
|
+
name: c.name,
|
|
45
|
+
description: c.description ?? '',
|
|
46
|
+
whenToUse: c.whenToUse ?? '',
|
|
47
|
+
tier: c.tier ?? 'normal',
|
|
48
|
+
...(subCount > 0 ? { subCount } : {}),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
46
51
|
return {
|
|
47
52
|
kind: 'branch',
|
|
48
53
|
name: opts.name,
|
|
54
|
+
description: opts.description,
|
|
55
|
+
whenToUse: opts.whenToUse,
|
|
56
|
+
tier: opts.tier,
|
|
49
57
|
help: opts.help,
|
|
50
58
|
rootEntry: opts.rootEntry,
|
|
51
59
|
slash: opts.slash,
|
|
@@ -82,13 +90,13 @@ export function defineRoot(opts) {
|
|
|
82
90
|
.map((s) => {
|
|
83
91
|
// Promote this subtree's common/important children into root, and count
|
|
84
92
|
// how many other (non-hidden) direct subcommands stay behind `<name> -h`.
|
|
85
|
-
const visible = s.help.
|
|
93
|
+
const visible = (s.help.listing ?? []).filter((c) => c.tier !== 'hidden');
|
|
86
94
|
const promoted = visible
|
|
87
95
|
.filter((c) => c.tier === 'common' || c.tier === 'important')
|
|
88
96
|
.map((c) => ({
|
|
89
97
|
path: `${s.name} ${c.name}`,
|
|
90
98
|
// important carries its shortform desc; common shows the bare path.
|
|
91
|
-
desc: c.tier === 'important' ? c.
|
|
99
|
+
desc: c.tier === 'important' ? c.description : undefined,
|
|
92
100
|
}));
|
|
93
101
|
return {
|
|
94
102
|
name: s.name,
|
package/dist/core/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { CONFIG_FILE, STATE_FILE, SCHEMA_VERSION, defaultScopeConfig, defaultScopeState } from '../types.js';
|
|
2
|
+
import { CONFIG_FILE, STATE_FILE, SCHEMA_VERSION, defaultScopeConfig, defaultScopeState, defaultCanvasNavConfig } from '../types.js';
|
|
3
3
|
import { readJsonIfExists, writeJson, ensureDir } from './fs-utils.js';
|
|
4
4
|
import { scopeRoot, requireScopeRoot } from './scope.js';
|
|
5
5
|
import { diag } from './io.js';
|
|
@@ -74,6 +74,39 @@ export function ensureScopeInitialized(scope, root) {
|
|
|
74
74
|
writeJson(cfgPath, defaultScopeConfig());
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
+
/** Deep-merge one bind table over its built-in defaults: a user adding a single
|
|
78
|
+
* bind must not wipe the rest. Each user entry is validated (a `run` string is
|
|
79
|
+
* required) before it replaces/extends a key. */
|
|
80
|
+
function mergeBinds(base, over) {
|
|
81
|
+
const out = { ...base };
|
|
82
|
+
if (over !== null && typeof over === 'object') {
|
|
83
|
+
for (const [k, v] of Object.entries(over)) {
|
|
84
|
+
if (v !== null && typeof v === 'object' && typeof v.run === 'string') {
|
|
85
|
+
const b = v;
|
|
86
|
+
out[k] = {
|
|
87
|
+
run: b.run,
|
|
88
|
+
...(b.confirm === true ? { confirm: true } : {}),
|
|
89
|
+
...(typeof b.desc === 'string' ? { desc: b.desc } : {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/** Validate + deep-merge a user `canvasNav` block over the built-in defaults.
|
|
97
|
+
* An absent or malformed block falls back wholesale to defaults. */
|
|
98
|
+
function mergeCanvasNav(raw) {
|
|
99
|
+
const defaults = defaultCanvasNavConfig();
|
|
100
|
+
if (raw === null || typeof raw !== 'object')
|
|
101
|
+
return defaults;
|
|
102
|
+
const r = raw;
|
|
103
|
+
const prefixKey = typeof r.prefixKey === 'string' && r.prefixKey.trim() !== '' ? r.prefixKey : defaults.prefixKey;
|
|
104
|
+
return {
|
|
105
|
+
prefixKey,
|
|
106
|
+
prefixBinds: mergeBinds(defaults.prefixBinds, r.prefixBinds),
|
|
107
|
+
graphBinds: mergeBinds(defaults.graphBinds, r.graphBinds),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
77
110
|
function normalizeMode(value, fallback) {
|
|
78
111
|
if (value === true)
|
|
79
112
|
return 'notify';
|
|
@@ -103,7 +136,8 @@ function mergeConfig(partial) {
|
|
|
103
136
|
const max_panes_per_window = typeof rawMaxPanes === 'number' && Number.isFinite(rawMaxPanes) && rawMaxPanes >= 1
|
|
104
137
|
? Math.floor(rawMaxPanes)
|
|
105
138
|
: defaults.max_panes_per_window;
|
|
106
|
-
|
|
139
|
+
const canvasNav = mergeCanvasNav(partial.canvasNav);
|
|
140
|
+
return { schema_version, marketplaces, plugins, skills, auto_update, max_panes_per_window, canvasNav };
|
|
107
141
|
}
|
|
108
142
|
export function updateConfig(scope, mutate) {
|
|
109
143
|
const cfg = readConfig(scope);
|
package/dist/core/feed/feed.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// file-system friendliness and lexicographic sort alignment.
|
|
10
10
|
import { writeFileSync, renameSync, mkdirSync, existsSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
|
-
import { reportsDir, subscribersOf,
|
|
12
|
+
import { reportsDir, subscribersOf, } from '../canvas/index.js';
|
|
13
|
+
import { transition } from '../runtime/lifecycle.js';
|
|
13
14
|
import { appendInbox } from './inbox.js';
|
|
14
15
|
import { appendPassive } from './passive.js';
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
@@ -93,8 +94,7 @@ export async function push(nodeId, opts) {
|
|
|
93
94
|
}
|
|
94
95
|
// (c) Finalise node when kind === 'final'.
|
|
95
96
|
if (kind === 'final') {
|
|
96
|
-
|
|
97
|
-
updateNode(nodeId, { intent: 'done' });
|
|
97
|
+
transition(nodeId, 'finalize');
|
|
98
98
|
}
|
|
99
99
|
return { reportPath, deliveredTo };
|
|
100
100
|
}
|
|
@@ -41,7 +41,9 @@ export declare function writeCursor(nodeId: string, iso: string): void;
|
|
|
41
41
|
*
|
|
42
42
|
* Format (per sender group):
|
|
43
43
|
* From <sender> — <N> update(s):
|
|
44
|
-
* [<kind>] <label> (ref: <path>)
|
|
44
|
+
* [<kind>] <label> (ref: <path>) ← push: pointer, dereference the ref
|
|
45
|
+
* [<kind>] ← ref-less msg: full body inlined
|
|
46
|
+
* <body line>
|
|
45
47
|
* …
|
|
46
48
|
*
|
|
47
49
|
* A header line announces the total count and instructs the receiver to
|