@north-light/crouter 0.3.197 → 0.3.199
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/api/client.d.ts +19 -19
- package/dist/api/client.js +29 -37
- package/dist/api/dto/broker-ops.d.ts +3 -1
- package/dist/api/dto/broker.d.ts +13 -2
- package/dist/api/dto/broker.js +4 -4
- package/dist/api/dto/human.d.ts +6 -5
- package/dist/api/dto/inbox.d.ts +126 -33
- package/dist/api/dto/nodes.d.ts +8 -2
- package/dist/api/dto/profiles.d.ts +23 -0
- package/dist/api/dto/profiles.js +2 -2
- package/dist/api/dto/worktree.d.ts +3 -5
- package/dist/api/routes.d.ts +3 -5
- package/dist/api/routes.js +4 -6
- package/dist/builtin-memory/00-runtime-base.md +8 -8
- package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
- package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
- package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
- package/dist/builtin-memory/insights/capture.md +2 -2
- package/dist/builtin-memory/insights/init.md +3 -3
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/memory-loading.md +1 -1
- package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -3
- package/dist/builtin-memory/internal/plugins.md +59 -2
- package/dist/builtin-memory/internal/storage-tiers.md +3 -1
- package/dist/builtin-memory/spec/roadmap.md +2 -2
- package/dist/builtin-memory/testing.md +1 -1
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
- package/dist/clients/attach/chrome/bash-jobs.js +3 -3
- package/dist/clients/attach/render/chat-view.d.ts +29 -9
- package/dist/clients/attach/render/chat-view.js +64 -35
- package/dist/clients/attach/render/markdown-source.js +0 -6
- package/dist/clients/attach/render/page-block.d.ts +34 -0
- package/dist/clients/attach/render/page-block.js +226 -0
- package/dist/clients/attach/render/tool-calls.js +16 -2
- package/dist/clients/attach/session/whip.d.ts +2 -0
- package/dist/clients/attach/session/whip.js +2 -0
- package/dist/clients/attach/viewer.js +1419 -982
- package/dist/clients/conversation/projection.js +18 -3
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -14
- package/dist/clients/inbox/controller.js +10 -16
- package/dist/clients/inbox/page-adapter.d.ts +3 -4
- package/dist/clients/inbox/page-adapter.js +3 -4
- package/dist/clients/inbox/resolve.d.ts +1 -1
- package/dist/clients/inbox/resolve.js +3 -3
- package/dist/clients/inbox/review/document-surface.js +3 -1
- package/dist/clients/inbox/review/state.d.ts +2 -1
- package/dist/clients/inbox/review/state.js +5 -16
- package/dist/clients/inbox/tui/input.js +37 -19
- package/dist/clients/inbox/tui/panel.js +5 -31
- package/dist/clients/inbox/tui/render.d.ts +11 -0
- package/dist/clients/inbox/tui/render.js +101 -36
- package/dist/clients/inbox/tui/slots.d.ts +4 -4
- package/dist/clients/inbox/tui/slots.js +19 -44
- package/dist/clients/inbox/tui/types.d.ts +6 -10
- package/dist/clients/inbox/tui.js +2 -2
- package/dist/commands/__tests__/human.test.js +118 -24
- package/dist/commands/__tests__/node-message.test.js +14 -0
- package/dist/commands/attention.js +4 -4
- package/dist/commands/canvas-browse.js +1 -1
- package/dist/commands/canvas-snapshot.js +1 -1
- package/dist/commands/canvas.js +1 -1
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/human/{doc.d.ts → components.d.ts} +1 -1
- package/dist/commands/human/components.js +77 -0
- package/dist/commands/human/feedback.d.ts +2 -0
- package/dist/commands/human/feedback.js +108 -0
- package/dist/commands/human/prompts.d.ts +17 -8
- package/dist/commands/human/prompts.js +217 -226
- package/dist/commands/human/queue.js +57 -88
- package/dist/commands/human/shared.d.ts +4 -15
- package/dist/commands/human/shared.js +19 -61
- package/dist/commands/human.js +32 -19
- package/dist/commands/memory/origin.js +2 -2
- package/dist/commands/node/bash.js +4 -4
- package/dist/commands/node/inspect.js +3 -3
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node/message.js +2 -2
- package/dist/commands/node-worktree.js +8 -8
- package/dist/commands/pkg/browse.js +1 -1
- package/dist/commands/pkg/plugin-manage.js +129 -10
- package/dist/commands/profile/delete.js +41 -15
- package/dist/commands/profile.js +1 -1
- package/dist/commands/push.js +1 -1
- package/dist/commands/surface/node/focus.js +29 -14
- package/dist/commands/surface-edit.js +3 -3
- package/dist/commands/surface-inbox.d.ts +2 -0
- package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
- package/dist/commands/surface.js +2 -1
- package/dist/commands/sys/doctor.js +60 -3
- package/dist/commands/sys/feedback.js +79 -25
- package/dist/commands/sys/settings.js +1 -1
- package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
- package/dist/core/__tests__/daemon-boot.test.js +7 -4
- package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
- package/dist/core/__tests__/extension-abort.test.js +5 -2
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
- package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
- package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
- package/dist/core/__tests__/human-deliver.test.js +41 -45
- package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
- package/dist/core/__tests__/plugin-page-components.test.js +157 -0
- package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
- package/dist/core/__tests__/seam/dormancy-release.test.js +103 -4
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -16
- package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
- package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
- package/dist/core/__tests__/serial/worktree.test.js +24 -198
- package/dist/core/__tests__/session-cycles.test.js +22 -0
- package/dist/core/__tests__/stop-guard.test.js +4 -4
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
- package/dist/core/bash-jobs.d.ts +6 -0
- package/dist/core/bash-jobs.js +10 -0
- package/dist/core/canvas/__tests__/attention.test.js +8 -5
- package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
- package/dist/core/canvas/attention.js +8 -6
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +21 -3
- package/dist/core/canvas/browse/model.d.ts +8 -2
- package/dist/core/canvas/browse/model.js +11 -4
- package/dist/core/canvas/browse/render.d.ts +4 -0
- package/dist/core/canvas/browse/render.js +5 -1
- package/dist/core/canvas/canvas.d.ts +33 -0
- package/dist/core/canvas/canvas.js +92 -0
- package/dist/core/canvas/crons.d.ts +9 -0
- package/dist/core/canvas/crons.js +51 -0
- package/dist/core/canvas/node-order.d.ts +22 -1
- package/dist/core/canvas/node-order.js +26 -1
- package/dist/core/canvas/render-source.d.ts +5 -0
- package/dist/core/canvas/render-source.js +1 -0
- package/dist/core/canvas/types.d.ts +9 -3
- package/dist/core/command-plugins/bundle.d.ts +6 -1
- package/dist/core/command-plugins/bundle.js +26 -13
- package/dist/core/command.js +8 -12
- package/dist/core/config.d.ts +47 -1
- package/dist/core/config.js +138 -4
- package/dist/core/help.d.ts +3 -0
- package/dist/core/help.js +1 -1
- package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-catalog.test.js +20 -0
- package/dist/core/human/__tests__/page-tickets.test.js +68 -34
- package/dist/core/human/__tests__/page.test.js +60 -119
- package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -16
- package/dist/core/human/answer-text.d.ts +3 -0
- package/dist/core/human/answer-text.js +98 -0
- package/dist/core/human/answer.d.ts +53 -0
- package/dist/core/human/answer.js +119 -0
- package/dist/core/human/component-docs.d.ts +18 -0
- package/dist/core/human/component-docs.js +434 -0
- package/dist/core/human/convention.d.ts +17 -3
- package/dist/core/human/convention.js +36 -5
- package/dist/core/human/feedback-companion.d.ts +20 -0
- package/dist/core/human/feedback-companion.js +98 -0
- package/dist/core/human/feedback.d.ts +65 -0
- package/dist/core/human/feedback.js +116 -0
- package/dist/core/human/page-catalog.d.ts +56 -5
- package/dist/core/human/page-catalog.js +151 -22
- package/dist/core/human/page-errors.d.ts +4 -0
- package/dist/core/human/page-errors.js +7 -0
- package/dist/core/human/page-eval.d.ts +20 -0
- package/dist/core/human/page-eval.js +123 -0
- package/dist/core/human/page-schema.d.ts +48 -116
- package/dist/core/human/page-schema.js +100 -72
- package/dist/core/human/page-synth.d.ts +2 -4
- package/dist/core/human/page-synth.js +20 -7
- package/dist/core/human/page.d.ts +13 -21
- package/dist/core/human/page.js +147 -391
- package/dist/core/human/review-schema.d.ts +1 -0
- package/dist/core/human/review-schema.js +1 -1
- package/dist/core/human/root.d.ts +6 -2
- package/dist/core/human/root.js +12 -4
- package/dist/core/human/scan.d.ts +3 -3
- package/dist/core/human/scan.js +21 -21
- package/dist/core/human/summary.d.ts +2 -6
- package/dist/core/human/summary.js +3 -59
- package/dist/core/human/tickets.d.ts +22 -15
- package/dist/core/human/tickets.js +90 -28
- package/dist/core/human/types.d.ts +14 -2
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +1 -0
- package/dist/core/preview-registry.js +8 -10
- package/dist/core/profiles/default-binding.d.ts +4 -0
- package/dist/core/profiles/default-binding.js +24 -0
- package/dist/core/profiles/deletion-reservation.d.ts +8 -0
- package/dist/core/profiles/deletion-reservation.js +38 -0
- package/dist/core/profiles/manifest.d.ts +3 -0
- package/dist/core/profiles/manifest.js +12 -0
- package/dist/core/profiles/select.js +2 -2
- package/dist/core/review/realize.js +12 -4
- package/dist/core/review/store.d.ts +6 -0
- package/dist/core/review/store.js +31 -0
- package/dist/core/runtime/bearings.d.ts +0 -4
- package/dist/core/runtime/bearings.js +4 -62
- package/dist/core/runtime/bin-contributions.d.ts +44 -0
- package/dist/core/runtime/bin-contributions.js +186 -0
- package/dist/core/runtime/boot-root.js +6 -1
- package/dist/core/runtime/broker/client-registry.d.ts +4 -0
- package/dist/core/runtime/broker/client-registry.js +13 -1
- package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
- package/dist/core/runtime/broker/extension-abort.js +4 -2
- package/dist/core/runtime/broker/fault-retry.js +4 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
- package/dist/core/runtime/broker/frame-dispatch.js +5 -6
- package/dist/core/runtime/broker/read-ops.d.ts +1 -1
- package/dist/core/runtime/broker/read-ops.js +2 -2
- package/dist/core/runtime/broker/rebind.d.ts +1 -0
- package/dist/core/runtime/broker/rebind.js +6 -1
- package/dist/core/runtime/broker-extension-render.js +19 -0
- package/dist/core/runtime/broker-protocol.d.ts +14 -0
- package/dist/core/runtime/broker.d.ts +1 -1
- package/dist/core/runtime/broker.js +25 -12
- package/dist/core/runtime/busy.d.ts +4 -3
- package/dist/core/runtime/busy.js +4 -3
- package/dist/core/runtime/front-door-env.d.ts +5 -0
- package/dist/core/runtime/front-door-env.js +16 -0
- package/dist/core/runtime/front-door.d.ts +1 -5
- package/dist/core/runtime/front-door.js +2 -5
- package/dist/core/runtime/host.d.ts +1 -1
- package/dist/core/runtime/invocation.d.ts +6 -0
- package/dist/core/runtime/invocation.js +10 -0
- package/dist/core/runtime/kickoff.js +34 -0
- package/dist/core/runtime/launch.d.ts +2 -6
- package/dist/core/runtime/node-read.js +4 -1
- package/dist/core/runtime/nodes.js +2 -0
- package/dist/core/runtime/promote.d.ts +1 -0
- package/dist/core/runtime/promote.js +3 -1
- package/dist/core/runtime/session-cycles.d.ts +6 -0
- package/dist/core/runtime/session-cycles.js +15 -1
- package/dist/core/runtime/session-visibility.d.ts +17 -8
- package/dist/core/runtime/session-visibility.js +19 -10
- package/dist/core/runtime/spawn-env.d.ts +9 -1
- package/dist/core/runtime/spawn-env.js +62 -2
- package/dist/core/runtime/spawn.js +4 -4
- package/dist/core/runtime/stop-guard.d.ts +2 -2
- package/dist/core/runtime/stop-guard.js +2 -2
- package/dist/core/runtime/stop-signals.d.ts +1 -1
- package/dist/core/runtime/stop-signals.js +2 -2
- package/dist/core/runtime/tmux-bindings.js +1 -1
- package/dist/core/session-model/session-state.d.ts +4 -3
- package/dist/core/session-model/session-state.js +14 -1
- package/dist/core/user-settings.d.ts +36 -2
- package/dist/core/user-settings.js +56 -14
- package/dist/core/worktree.d.ts +15 -45
- package/dist/core/worktree.js +59 -261
- package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/broker-ops.js +9 -5
- package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
- package/dist/daemon/api/handlers/feedback-comments.js +207 -0
- package/dist/daemon/api/handlers/human.js +24 -20
- package/dist/daemon/api/handlers/inbox.d.ts +10 -0
- package/dist/daemon/api/handlers/inbox.js +48 -109
- package/dist/daemon/api/handlers/profiles.js +11 -29
- package/dist/daemon/api/map.d.ts +6 -1
- package/dist/daemon/api/map.js +23 -0
- package/dist/daemon/api/server.js +3 -1
- package/dist/daemon/companion-retire.d.ts +2 -0
- package/dist/daemon/companion-retire.js +33 -0
- package/dist/daemon/cron-run.js +29 -17
- package/dist/daemon/crtrd.js +1 -1
- package/dist/daemon/fleet.d.ts +22 -10
- package/dist/daemon/fleet.js +43 -24
- package/dist/daemon/human/finish.d.ts +12 -6
- package/dist/daemon/human/finish.js +134 -54
- package/dist/daemon/human/sweep.js +9 -3
- package/dist/daemon/profile-delete.d.ts +7 -0
- package/dist/daemon/profile-delete.js +306 -0
- package/dist/daemon/reconcilers/broker-supervision.js +14 -9
- package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
- package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
- package/dist/daemon/reconcilers/live-obligation.js +30 -2
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
- package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
- package/dist/daemon/review/deliver.js +3 -3
- package/dist/daemon/review/finish.d.ts +0 -2
- package/dist/daemon/review/finish.js +1 -29
- package/dist/daemon/review/sweep.js +12 -1
- package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
- package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
- package/dist/pi-extensions/canvas-bash-valve.js +31 -13
- package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
- package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
- package/dist/pi-extensions/canvas-review-boundary.js +18 -9
- package/dist/pi-extensions/canvas-stophook.js +4 -4
- package/dist/shared/generated-context.js +2 -2
- package/dist/types.d.ts +61 -5
- package/dist/types.js +8 -2
- package/package.json +4 -4
- package/runtime.lock.json +5 -33
- package/scripts/install-runtime.mjs +19 -24
- package/dist/builtin-memory/init.md +0 -38
- package/dist/builtin-memory/plan.md +0 -19
- package/dist/builtin-memory/spec.md +0 -19
- package/dist/clients/attach/render/html-markdown.d.ts +0 -13
- package/dist/clients/attach/render/html-markdown.js +0 -206
- package/dist/commands/human/doc.js +0 -144
- package/dist/commands/human/inbox.d.ts +0 -2
- package/dist/core/human/__tests__/page-render.test.js +0 -49
- package/dist/core/human/markdown-html.d.ts +0 -8
- package/dist/core/human/markdown-html.js +0 -581
- package/dist/core/human/page-render.d.ts +0 -3
- package/dist/core/human/page-render.js +0 -131
- package/dist/pages/bundle.css +0 -1
- package/dist/pages/bundle.js +0 -1347
- package/dist/pages/comments.d.ts +0 -43
- package/dist/pages/comments.js +0 -125
- package/dist/pages/elements/cards.d.ts +0 -17
- package/dist/pages/elements/cards.js +0 -802
- package/dist/pages/elements/chart.js +0 -648
- package/dist/pages/elements/options.d.ts +0 -21
- package/dist/pages/elements/options.js +0 -629
- package/dist/pages/elements/pages.d.ts +0 -24
- package/dist/pages/elements/pages.js +0 -355
- package/dist/pages/elements/slot.d.ts +0 -21
- package/dist/pages/elements/slot.js +0 -105
- package/dist/pages/elements/table.d.ts +0 -19
- package/dist/pages/elements/table.js +0 -935
- package/dist/pages/elements/text.d.ts +0 -25
- package/dist/pages/elements/text.js +0 -968
- package/dist/pages/entry.d.ts +0 -8
- package/dist/pages/entry.js +0 -8
- package/dist/pages/host.d.ts +0 -158
- package/dist/pages/host.js +0 -182
- package/dist/pages/register.d.ts +0 -2
- package/dist/pages/register.js +0 -3
- package/dist/pages/slot-config.d.ts +0 -36
- package/dist/pages/slot-config.js +0 -50
- package/dist/pages/types.d.ts +0 -98
- package/dist/pages/types.js +0 -23
- /package/dist/{core/human/__tests__/page-render.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
- /package/dist/{pages/elements/chart.d.ts → core/__tests__/plugin-page-components.test.d.ts} +0 -0
|
@@ -4,19 +4,26 @@
|
|
|
4
4
|
// it is exhaustively unit-testable. The app layer (app.ts) wires the canvas data
|
|
5
5
|
// access (dashboardRowsAll / listNodes / subscriptionsOf) into buildTree, then
|
|
6
6
|
// drives flatten() on each keystroke.
|
|
7
|
-
import { statusRank } from '../node-order.js';
|
|
7
|
+
import { isFinishedWork, isLiveStatus, statusRank } from '../node-order.js';
|
|
8
8
|
export const TABS = ['All', 'Live', 'Dormant', 'Attention', 'Pinned'];
|
|
9
9
|
export const SORTS = ['tree', 'attention', 'relevance', 'recency'];
|
|
10
10
|
/** Does a node belong to this tab's slice?
|
|
11
11
|
* All — every node.
|
|
12
|
-
* Live — active | idle
|
|
13
|
-
*
|
|
12
|
+
* Live — what you can pick up right now: active | idle, PLUS a resident
|
|
13
|
+
* agent that stopped without finishing (its broker exited, but
|
|
14
|
+
* Enter resumes the conversation in place — see `isFinishedWork`).
|
|
15
|
+
* Finished work never appears here.
|
|
16
|
+
* Dormant — done | dead | canceled: literally "no engine running". A stopped
|
|
17
|
+
* resident agent is deliberately on BOTH tabs — it is resumable
|
|
18
|
+
* (Live) and it is not running (Dormant); both are true and each
|
|
19
|
+
* tab is the one a different search starts from.
|
|
14
20
|
* Attention — needs a human: has pending asks OR is parked on a fault.
|
|
15
21
|
* Pinned — user-starred (a local, view-only preference; `isPinned`). */
|
|
16
22
|
export function tabPredicate(tab, row, isPinned = false) {
|
|
17
23
|
switch (tab) {
|
|
18
24
|
case 'All': return true;
|
|
19
|
-
case 'Live': return row.status
|
|
25
|
+
case 'Live': return isLiveStatus(row.status)
|
|
26
|
+
|| !isFinishedWork({ lifecycle: row.lifecycle, finalized: row.finalized === true });
|
|
20
27
|
case 'Dormant': return row.status === 'done' || row.status === 'dead' || row.status === 'canceled';
|
|
21
28
|
case 'Attention': return row.asks > 0 || row.hanging != null;
|
|
22
29
|
case 'Pinned': return isPinned;
|
|
@@ -43,6 +43,10 @@ export interface RenderState {
|
|
|
43
43
|
* a descendant) is actively streaming. Holds the node id being confirmed; the
|
|
44
44
|
* footer becomes the y/n prompt. null/undefined = no pending confirm. */
|
|
45
45
|
pendingClose?: string | null;
|
|
46
|
+
/** One-shot footer confirmation for a keystroke whose whole effect is off-screen
|
|
47
|
+
* (`y` yank). Replaces the nav hints for exactly one frame; the app clears it on
|
|
48
|
+
* the next key. null/undefined = show the ordinary footer. */
|
|
49
|
+
flash?: string | null;
|
|
46
50
|
/** Set when the source is a remote canvas: mutation/focus keys (`x`/`K`/Enter)
|
|
47
51
|
* are disabled, so the footer drops their hints instead of advertising a
|
|
48
52
|
* dead action. undefined/false = local, byte-identical footer. */
|
|
@@ -428,7 +428,8 @@ const HELP_ROWS = [
|
|
|
428
428
|
['Pin', 'f', 'star / unstar — floats to top, Pinned tab'],
|
|
429
429
|
['Open', '⏎ o', 'resume the node (closes navigator)'],
|
|
430
430
|
['', 'O', 'resume, keep browsing (open in background)'],
|
|
431
|
-
['Act', '
|
|
431
|
+
['Act', 'y', 'copy the node id to the clipboard'],
|
|
432
|
+
['', 'm', 'message the node (no resume)'],
|
|
432
433
|
['', 'R', 'rename the node'],
|
|
433
434
|
['', 'x', 'close-out node + subtree'],
|
|
434
435
|
['', 'K', 'revive now (a hanging node)'],
|
|
@@ -533,6 +534,9 @@ export function renderFrame(state, size, caps = { color: false, color256: false
|
|
|
533
534
|
const warn = `⚠ ${who} is ⟳ — close it (and its subtree) anyway? y / n`;
|
|
534
535
|
lines.push(caps.color ? `${BOLD}${ESC}${FG_BRIGHT_YELLOW}m${clip(warn, width)}${RESET}` : `${REVERSE}${clip(warn, width)}${RESET}`);
|
|
535
536
|
}
|
|
537
|
+
else if (state.flash !== null && state.flash !== undefined && state.flash !== '') {
|
|
538
|
+
lines.push(caps.color ? `${BOLD}${clip(state.flash, width)}${RESET}` : `${REVERSE}${clip(state.flash, width)}${RESET}`);
|
|
539
|
+
}
|
|
536
540
|
else {
|
|
537
541
|
// When the cursor row has an active fault, lead the footer with the on-demand kick
|
|
538
542
|
// hint (K shells `node lifecycle revive <id> --now` — immediate revive instead of
|
|
@@ -90,6 +90,10 @@ export declare function clearPid(nodeId: string): void;
|
|
|
90
90
|
export declare function migrateLegacyPidIdentities(captureIdentity?: (pid: number) => string | undefined): {
|
|
91
91
|
migrated: string[];
|
|
92
92
|
};
|
|
93
|
+
/** Every hydrated node whose durable profile identity equals `profileId`.
|
|
94
|
+
* Unlike `listNodes`, this includes unclaimed warm-pool spares: profile
|
|
95
|
+
* deletion owns every matching row, whether or not it is user-visible. */
|
|
96
|
+
export declare function listNodesByProfile(profileId: string): NodeMeta[];
|
|
93
97
|
/** All rows, optionally filtered by status. Unclaimed warm spares are excluded. */
|
|
94
98
|
export declare function listNodes(filter?: {
|
|
95
99
|
status?: NodeStatus | NodeStatus[];
|
|
@@ -206,6 +210,11 @@ export declare function view(root: string): string[];
|
|
|
206
210
|
* that's still live (active|idle) — i.e. something that can actually wake it?
|
|
207
211
|
* If so, stopping is a legitimate await; if not, it must finish or escalate. */
|
|
208
212
|
export declare function hasActiveLiveSubscription(nodeId: string): boolean;
|
|
213
|
+
/** Whether a LIVE node still subscribes to this one. The mirror of
|
|
214
|
+
* `hasActiveLiveSubscription`: that asks what can still wake this node, this
|
|
215
|
+
* asks what this node still owes — a subscriber that is itself active or idle
|
|
216
|
+
* is waiting on a report this node has not delivered. */
|
|
217
|
+
export declare function hasLiveSubscriber(nodeId: string): boolean;
|
|
209
218
|
/** Whether a node explicitly awaits a controller that can still deliver. A
|
|
210
219
|
* parent relationship never implies this wait; the node must declare it. */
|
|
211
220
|
export declare function hasLiveMessageWait(nodeId: string): boolean;
|
|
@@ -301,6 +310,9 @@ export interface DeleteNodeResult {
|
|
|
301
310
|
* state are removed in one canvas write boundary. Best-effort dir removal
|
|
302
311
|
* follows the commit. */
|
|
303
312
|
export declare function deleteNode(nodeId: string): DeleteNodeResult;
|
|
313
|
+
/** Hard-delete a validated set of nodes in one row transaction, then remove
|
|
314
|
+
* their directories. The caller must tear down engines and viewers first. */
|
|
315
|
+
export declare function deleteNodes(nodeIds: readonly string[]): string[];
|
|
304
316
|
/** Retention sweep: remove TERMINAL nodes (status dead | done | canceled) whose
|
|
305
317
|
* `created` is older than `ttlDays`, bounding the otherwise-unbounded growth of
|
|
306
318
|
* node rows + dirs. Open managed-worktree nodes are retained so their checkout
|
|
@@ -324,6 +336,27 @@ export declare function pruneNodes(opts: {
|
|
|
324
336
|
dryRun?: boolean;
|
|
325
337
|
includeStale?: boolean;
|
|
326
338
|
}): PruneResult;
|
|
339
|
+
/** Drop rows whose on-disk `nodes/<id>/` dir is gone — a node that no longer
|
|
340
|
+
* exists in the only place it is durably stored.
|
|
341
|
+
*
|
|
342
|
+
* `createNode` scaffolds the dir and writes meta.json BEFORE seeding the row, and
|
|
343
|
+
* both delete paths ({@link deleteNode}, `deletePrunedNodes`) commit the row
|
|
344
|
+
* removal BEFORE removing the dir, so a row with no dir is never a transient of
|
|
345
|
+
* either — it is a node whose durable state was lost. Nothing about it is
|
|
346
|
+
* recoverable: `getNode` reads meta.json and returns null, so every surface that
|
|
347
|
+
* resolves a node (revive, focus, close, rename, message) rejects it as unknown
|
|
348
|
+
* while the roster keeps listing it. The row IS the residue, so deleting it is
|
|
349
|
+
* the whole repair; there is no dir left to remove.
|
|
350
|
+
*
|
|
351
|
+
* Because `active`/`idle` ghosts are the ones that jam surfaces, status is not a
|
|
352
|
+
* filter here — count-based pruning protects those rows precisely so daemon
|
|
353
|
+
* crash-recovery can resume them, and that reason does not survive the dir being
|
|
354
|
+
* gone.
|
|
355
|
+
*
|
|
356
|
+
* Refuses to act on "can't tell": an unreadable nodes root, or a readable one
|
|
357
|
+
* holding no node dirs at all while rows exist, reads as a detached or wiped
|
|
358
|
+
* store rather than per-node loss, and reaps nothing. Returns the reaped ids. */
|
|
359
|
+
export declare function reapGhostRows(): string[];
|
|
327
360
|
/** Count-based retention sweep: retain the `maxNodes` MOST-RECENTLY-ACTIVE
|
|
328
361
|
* nodes plus every protected node, so the real retained floor is
|
|
329
362
|
* `max(maxNodes, protectedCount)`. Bounds the otherwise-unbounded growth the
|
|
@@ -336,6 +336,21 @@ export function migrateLegacyPidIdentities(captureIdentity = (pid) => capturePid
|
|
|
336
336
|
* yet — surfacing it would put a phantom empty conversation in the user's
|
|
337
337
|
* sidebar. Claiming deletes the marker, which is what makes the node visible. */
|
|
338
338
|
const NOT_A_SPARE = 'node_id NOT IN (SELECT node_id FROM warm_pool)';
|
|
339
|
+
/** Every hydrated node whose durable profile identity equals `profileId`.
|
|
340
|
+
* Unlike `listNodes`, this includes unclaimed warm-pool spares: profile
|
|
341
|
+
* deletion owns every matching row, whether or not it is user-visible. */
|
|
342
|
+
export function listNodesByProfile(profileId) {
|
|
343
|
+
const ids = openDb()
|
|
344
|
+
.prepare('SELECT node_id FROM nodes WHERE profile_id = ? ORDER BY created')
|
|
345
|
+
.all(profileId);
|
|
346
|
+
return ids.map(({ node_id }) => {
|
|
347
|
+
const meta = getNode(node_id);
|
|
348
|
+
if (meta === null) {
|
|
349
|
+
throw new Error(`profile deletion cannot hydrate indexed node ${node_id}`);
|
|
350
|
+
}
|
|
351
|
+
return meta;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
339
354
|
/** All rows, optionally filtered by status. Unclaimed warm spares are excluded. */
|
|
340
355
|
export function listNodes(filter) {
|
|
341
356
|
const db = openDb();
|
|
@@ -591,6 +606,18 @@ export function hasActiveLiveSubscription(nodeId) {
|
|
|
591
606
|
.get(nodeId);
|
|
592
607
|
return r !== undefined;
|
|
593
608
|
}
|
|
609
|
+
/** Whether a LIVE node still subscribes to this one. The mirror of
|
|
610
|
+
* `hasActiveLiveSubscription`: that asks what can still wake this node, this
|
|
611
|
+
* asks what this node still owes — a subscriber that is itself active or idle
|
|
612
|
+
* is waiting on a report this node has not delivered. */
|
|
613
|
+
export function hasLiveSubscriber(nodeId) {
|
|
614
|
+
const r = openDb()
|
|
615
|
+
.prepare(`SELECT 1 FROM edges e JOIN nodes n ON n.node_id = e.from_id
|
|
616
|
+
WHERE e.type = 'subscribes_to' AND e.to_id = ? AND e.active = 1
|
|
617
|
+
AND n.status IN ('active', 'idle') LIMIT 1`)
|
|
618
|
+
.get(nodeId);
|
|
619
|
+
return r !== undefined;
|
|
620
|
+
}
|
|
594
621
|
/** Whether a node explicitly awaits a controller that can still deliver. A
|
|
595
622
|
* parent relationship never implies this wait; the node must declare it. */
|
|
596
623
|
export function hasLiveMessageWait(nodeId) {
|
|
@@ -732,6 +759,28 @@ export function deleteNode(nodeId) {
|
|
|
732
759
|
rmSync(nodeDir(nodeId), { recursive: true, force: true });
|
|
733
760
|
return result;
|
|
734
761
|
}
|
|
762
|
+
/** Hard-delete a validated set of nodes in one row transaction, then remove
|
|
763
|
+
* their directories. The caller must tear down engines and viewers first. */
|
|
764
|
+
export function deleteNodes(nodeIds) {
|
|
765
|
+
const ids = [...new Set(nodeIds)];
|
|
766
|
+
if (ids.length === 0)
|
|
767
|
+
return [];
|
|
768
|
+
const blocked = ids.filter(hasOpenManagedWorktree);
|
|
769
|
+
if (blocked.length > 0) {
|
|
770
|
+
throw new Error(`refusing to delete nodes with open managed worktrees: ${blocked.join(', ')}`);
|
|
771
|
+
}
|
|
772
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
773
|
+
const deleted = withCanvasWrite((db) => {
|
|
774
|
+
const existing = db
|
|
775
|
+
.prepare(`SELECT node_id FROM nodes WHERE node_id IN (${placeholders}) ORDER BY created`)
|
|
776
|
+
.all(...ids).map(({ node_id }) => node_id);
|
|
777
|
+
db.prepare(`DELETE FROM nodes WHERE node_id IN (${placeholders})`).run(...ids);
|
|
778
|
+
return existing;
|
|
779
|
+
});
|
|
780
|
+
for (const id of deleted)
|
|
781
|
+
rmSync(nodeDir(id), { recursive: true, force: true });
|
|
782
|
+
return deleted;
|
|
783
|
+
}
|
|
735
784
|
/** Retention sweep: remove TERMINAL nodes (status dead | done | canceled) whose
|
|
736
785
|
* `created` is older than `ttlDays`, bounding the otherwise-unbounded growth of
|
|
737
786
|
* node rows + dirs. Open managed-worktree nodes are retained so their checkout
|
|
@@ -789,6 +838,49 @@ export function pruneNodes(opts) {
|
|
|
789
838
|
deletePrunedNodes(candidates);
|
|
790
839
|
return { pruned: candidates, dryRun };
|
|
791
840
|
}
|
|
841
|
+
/** Drop rows whose on-disk `nodes/<id>/` dir is gone — a node that no longer
|
|
842
|
+
* exists in the only place it is durably stored.
|
|
843
|
+
*
|
|
844
|
+
* `createNode` scaffolds the dir and writes meta.json BEFORE seeding the row, and
|
|
845
|
+
* both delete paths ({@link deleteNode}, `deletePrunedNodes`) commit the row
|
|
846
|
+
* removal BEFORE removing the dir, so a row with no dir is never a transient of
|
|
847
|
+
* either — it is a node whose durable state was lost. Nothing about it is
|
|
848
|
+
* recoverable: `getNode` reads meta.json and returns null, so every surface that
|
|
849
|
+
* resolves a node (revive, focus, close, rename, message) rejects it as unknown
|
|
850
|
+
* while the roster keeps listing it. The row IS the residue, so deleting it is
|
|
851
|
+
* the whole repair; there is no dir left to remove.
|
|
852
|
+
*
|
|
853
|
+
* Because `active`/`idle` ghosts are the ones that jam surfaces, status is not a
|
|
854
|
+
* filter here — count-based pruning protects those rows precisely so daemon
|
|
855
|
+
* crash-recovery can resume them, and that reason does not survive the dir being
|
|
856
|
+
* gone.
|
|
857
|
+
*
|
|
858
|
+
* Refuses to act on "can't tell": an unreadable nodes root, or a readable one
|
|
859
|
+
* holding no node dirs at all while rows exist, reads as a detached or wiped
|
|
860
|
+
* store rather than per-node loss, and reaps nothing. Returns the reaped ids. */
|
|
861
|
+
export function reapGhostRows() {
|
|
862
|
+
const rows = listNodes();
|
|
863
|
+
if (rows.length === 0)
|
|
864
|
+
return [];
|
|
865
|
+
let onDisk;
|
|
866
|
+
try {
|
|
867
|
+
onDisk = new Set(readdirSync(nodesRoot()));
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
return []; // nodes root absent or unreadable — can't tell, so touch nothing
|
|
871
|
+
}
|
|
872
|
+
if (onDisk.size === 0)
|
|
873
|
+
return [];
|
|
874
|
+
const ghosts = rows.map((r) => r.node_id).filter((id) => !onDisk.has(id));
|
|
875
|
+
if (ghosts.length === 0)
|
|
876
|
+
return [];
|
|
877
|
+
withCanvasWrite((db) => {
|
|
878
|
+
const del = db.prepare('DELETE FROM nodes WHERE node_id = ?');
|
|
879
|
+
for (const id of ghosts)
|
|
880
|
+
del.run(id);
|
|
881
|
+
});
|
|
882
|
+
return ghosts;
|
|
883
|
+
}
|
|
792
884
|
/** Hard-delete a set of pruned nodes in one transaction (the edges→nodes and
|
|
793
885
|
* anchored-crons FKs cascade their coupled state), followed by best-effort
|
|
794
886
|
* on-disk `nodes/<id>/` removal. Shared by TTL and count pruning so their
|
|
@@ -159,6 +159,15 @@ export declare function listCrons(scope: {
|
|
|
159
159
|
export declare function cancelCron(cron_id: string, options?: {
|
|
160
160
|
preserveRunningProcess?: boolean;
|
|
161
161
|
}): boolean;
|
|
162
|
+
/** Cron ids a hard profile deletion will remove: matching execution profile,
|
|
163
|
+
* plus every row anchored to a node being deleted. */
|
|
164
|
+
export declare function cronIdsForProfileDeletion(profileId: string, anchorNodeIds: readonly string[]): string[];
|
|
165
|
+
/** Kill any leased matching/anchored runs, then delete their rows together. */
|
|
166
|
+
export declare function deleteCronsForProfile(profileId: string, anchorNodeIds: readonly string[]): string[];
|
|
167
|
+
/** Cron ids whose snapshotted profile will be detached. */
|
|
168
|
+
export declare function cronIdsForProfileDetach(profileId: string): string[];
|
|
169
|
+
/** Preserve matching crons while removing their deleted profile dependency. */
|
|
170
|
+
export declare function globalizeCronsForProfile(profileId: string): string[];
|
|
162
171
|
/** DELETE one cron by id — one-shot consumption after its run settles.
|
|
163
172
|
* Returns whether a live row was removed, so a consumer racing an explicit
|
|
164
173
|
* cancel can no-op instead of double-settling. */
|
|
@@ -191,6 +191,57 @@ export function cancelCron(cron_id, options) {
|
|
|
191
191
|
return true;
|
|
192
192
|
});
|
|
193
193
|
}
|
|
194
|
+
function profileCronWhere(profileId, anchorNodeIds) {
|
|
195
|
+
const anchors = [...new Set(anchorNodeIds)];
|
|
196
|
+
if (anchors.length === 0)
|
|
197
|
+
return { sql: 'profile = ?', params: [profileId] };
|
|
198
|
+
return {
|
|
199
|
+
sql: `profile = ? OR anchor_node IN (${anchors.map(() => '?').join(',')})`,
|
|
200
|
+
params: [profileId, ...anchors],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** Cron ids a hard profile deletion will remove: matching execution profile,
|
|
204
|
+
* plus every row anchored to a node being deleted. */
|
|
205
|
+
export function cronIdsForProfileDeletion(profileId, anchorNodeIds) {
|
|
206
|
+
const where = profileCronWhere(profileId, anchorNodeIds);
|
|
207
|
+
return openDb()
|
|
208
|
+
.prepare(`SELECT cron_id FROM crons WHERE ${where.sql} ORDER BY created, cron_id`)
|
|
209
|
+
.all(...where.params).map(({ cron_id }) => cron_id);
|
|
210
|
+
}
|
|
211
|
+
/** Kill any leased matching/anchored runs, then delete their rows together. */
|
|
212
|
+
export function deleteCronsForProfile(profileId, anchorNodeIds) {
|
|
213
|
+
return withCanvasWrite((db) => {
|
|
214
|
+
const where = profileCronWhere(profileId, anchorNodeIds);
|
|
215
|
+
const rows = db
|
|
216
|
+
.prepare(`SELECT * FROM crons WHERE ${where.sql} ORDER BY created, cron_id`)
|
|
217
|
+
.all(...where.params).map(cronFrom);
|
|
218
|
+
for (const cron of rows) {
|
|
219
|
+
if (cron.run_state === 'running')
|
|
220
|
+
killLeasedCronRun(cron);
|
|
221
|
+
}
|
|
222
|
+
db.prepare(`DELETE FROM crons WHERE ${where.sql}`).run(...where.params);
|
|
223
|
+
return rows.map(({ cron_id }) => cron_id);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/** Cron ids whose snapshotted profile will be detached. */
|
|
227
|
+
export function cronIdsForProfileDetach(profileId) {
|
|
228
|
+
return openDb()
|
|
229
|
+
.prepare('SELECT cron_id FROM crons WHERE profile = ? ORDER BY created, cron_id')
|
|
230
|
+
.all(profileId).map(({ cron_id }) => cron_id);
|
|
231
|
+
}
|
|
232
|
+
/** Preserve matching crons while removing their deleted profile dependency. */
|
|
233
|
+
export function globalizeCronsForProfile(profileId) {
|
|
234
|
+
return withCanvasWrite((db) => {
|
|
235
|
+
const ids = db
|
|
236
|
+
.prepare('SELECT cron_id FROM crons WHERE profile = ? ORDER BY created, cron_id')
|
|
237
|
+
.all(profileId).map(({ cron_id }) => cron_id);
|
|
238
|
+
if (ids.length > 0) {
|
|
239
|
+
db.prepare("UPDATE crons SET profile = NULL, scope = 'global', updated = ? WHERE profile = ?")
|
|
240
|
+
.run(new Date().toISOString(), profileId);
|
|
241
|
+
}
|
|
242
|
+
return ids;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
194
245
|
/** DELETE one cron by id — one-shot consumption after its run settles.
|
|
195
246
|
* Returns whether a live row was removed, so a consumer racing an explicit
|
|
196
247
|
* cancel can no-op instead of double-settling. */
|
|
@@ -1,3 +1,24 @@
|
|
|
1
|
-
import type { NodeStatus } from './types.js';
|
|
1
|
+
import type { NodeStatus, Lifecycle } from './types.js';
|
|
2
2
|
/** Sort live work before terminal work; unknown statuses sort last. */
|
|
3
3
|
export declare function statusRank(status: NodeStatus | undefined): number;
|
|
4
|
+
/** True when the node has a broker the daemon supervises — the statuses that
|
|
5
|
+
* mean "a live engine is (or should be) running right now". */
|
|
6
|
+
export declare function isLiveStatus(status: NodeStatus | undefined): boolean;
|
|
7
|
+
/** True when a node's work is FINISHED — a terminal one-shot worker, or any node
|
|
8
|
+
* that pushed a final report.
|
|
9
|
+
*
|
|
10
|
+
* This is deliberately NOT "is it running": a non-live status alone conflates two
|
|
11
|
+
* unrelated situations. Finished work is archive — opening it is inspection, and
|
|
12
|
+
* relaunching it puts a completed worker back to work (the stop-guard reprompts a
|
|
13
|
+
* terminal node that never pushed a final). A RESIDENT node with no final report
|
|
14
|
+
* is the opposite: its `done` is only `markCleanExitDone` recording a clean broker
|
|
15
|
+
* exit, which the stop-guard itself rules a legitimate stop (`reason: 'dormant'`),
|
|
16
|
+
* so it never finished anything and resuming it is simply reopening a conversation.
|
|
17
|
+
*
|
|
18
|
+
* `lifecycle` is optional because DashboardRow only carries it on the full browser
|
|
19
|
+
* snapshot; absent, this answers `true` (treat as finished), keeping the caller on
|
|
20
|
+
* the conservative inspect-don't-revive path. */
|
|
21
|
+
export declare function isFinishedWork(node: {
|
|
22
|
+
lifecycle?: Lifecycle;
|
|
23
|
+
finalized: boolean;
|
|
24
|
+
}): boolean;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
// node-order.ts — canonical
|
|
1
|
+
// node-order.ts — canonical ordering and status/lifecycle classification for
|
|
2
|
+
// canvas nodes. Pure (type-only imports) so every surface — the browser model,
|
|
3
|
+
// command leaves, renderers — can share one answer to "is this node live?" and
|
|
4
|
+
// "did this node finish?" without reaching the db.
|
|
2
5
|
/** Sort live work before terminal work; unknown statuses sort last. */
|
|
3
6
|
export function statusRank(status) {
|
|
4
7
|
switch (status) {
|
|
@@ -10,3 +13,25 @@ export function statusRank(status) {
|
|
|
10
13
|
default: return 5;
|
|
11
14
|
}
|
|
12
15
|
}
|
|
16
|
+
/** True when the node has a broker the daemon supervises — the statuses that
|
|
17
|
+
* mean "a live engine is (or should be) running right now". */
|
|
18
|
+
export function isLiveStatus(status) {
|
|
19
|
+
return status === 'active' || status === 'idle';
|
|
20
|
+
}
|
|
21
|
+
/** True when a node's work is FINISHED — a terminal one-shot worker, or any node
|
|
22
|
+
* that pushed a final report.
|
|
23
|
+
*
|
|
24
|
+
* This is deliberately NOT "is it running": a non-live status alone conflates two
|
|
25
|
+
* unrelated situations. Finished work is archive — opening it is inspection, and
|
|
26
|
+
* relaunching it puts a completed worker back to work (the stop-guard reprompts a
|
|
27
|
+
* terminal node that never pushed a final). A RESIDENT node with no final report
|
|
28
|
+
* is the opposite: its `done` is only `markCleanExitDone` recording a clean broker
|
|
29
|
+
* exit, which the stop-guard itself rules a legitimate stop (`reason: 'dormant'`),
|
|
30
|
+
* so it never finished anything and resuming it is simply reopening a conversation.
|
|
31
|
+
*
|
|
32
|
+
* `lifecycle` is optional because DashboardRow only carries it on the full browser
|
|
33
|
+
* snapshot; absent, this answers `true` (treat as finished), keeping the caller on
|
|
34
|
+
* the conservative inspect-don't-revive path. */
|
|
35
|
+
export function isFinishedWork(node) {
|
|
36
|
+
return node.lifecycle !== 'resident' || node.finalized;
|
|
37
|
+
}
|
|
@@ -34,6 +34,11 @@ export interface DashboardRow {
|
|
|
34
34
|
* agent you come back to. Drives the browser's resident-only lifecycle filter.
|
|
35
35
|
* Only populated by dashboardRowsAll (the browser snapshot). */
|
|
36
36
|
lifecycle?: Lifecycle;
|
|
37
|
+
/** True when the node pushed a final report. With `lifecycle` this separates
|
|
38
|
+
* work that FINISHED from a resident agent that merely stopped, which the
|
|
39
|
+
* status alone cannot (see `isFinishedWork` in node-order.ts). Only populated
|
|
40
|
+
* by dashboardRowsAll (the browser snapshot). */
|
|
41
|
+
finalized?: boolean;
|
|
37
42
|
/** "Most recent activity" sort key — epoch ms. The cheap boot stats the node's
|
|
38
43
|
* job/telemetry.json (rewritten on every turn_end at a DETERMINISTIC path, so
|
|
39
44
|
* no meta read), falling back to `created`'s epoch when a node never ran a turn.
|
|
@@ -372,6 +372,7 @@ export function dashboardRowFromNodeRow(row, focusedNodeIds, remote) {
|
|
|
372
372
|
profile_id: row.profile_id,
|
|
373
373
|
created: row.created,
|
|
374
374
|
lifecycle: row.lifecycle,
|
|
375
|
+
finalized: row.final_report != null,
|
|
375
376
|
mtimeMs: sessionMtime(row.node_id, row.created, remote),
|
|
376
377
|
streaming: !remote && hanging === null && live ? isStreaming(row.node_id, row.pi_pid) : false,
|
|
377
378
|
hanging,
|
|
@@ -49,6 +49,9 @@ export interface LaunchSpec {
|
|
|
49
49
|
}
|
|
50
50
|
export interface ManagedWorktree {
|
|
51
51
|
state: 'open' | 'closed';
|
|
52
|
+
/** A closed worktree remains registered until its owner has exited, so Git
|
|
53
|
+
* never deletes the caller's live checkout beneath it. */
|
|
54
|
+
cleanup?: 'pending' | 'complete';
|
|
52
55
|
path: string;
|
|
53
56
|
branch: string;
|
|
54
57
|
repo_root: string;
|
|
@@ -57,10 +60,13 @@ export interface ManagedWorktree {
|
|
|
57
60
|
created: string;
|
|
58
61
|
closed?: string;
|
|
59
62
|
}
|
|
60
|
-
/** Immutable
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
+
/** Immutable companion provenance. The origin session/leaf/model coordinates
|
|
64
|
+
* remain canonical in the review row rather than being duplicated into node
|
|
65
|
+
* metadata. `kind` names the surface the companion serves: absent or `review`
|
|
66
|
+
* is a human document review; `page_feedback` binds a page ticket's feedback
|
|
67
|
+
* conversation, with review_id = ticket id and target_file = the page source. */
|
|
63
68
|
export interface ReviewBinding {
|
|
69
|
+
kind?: 'review' | 'page_feedback';
|
|
64
70
|
review_id: string;
|
|
65
71
|
origin_node_id: string;
|
|
66
72
|
branch_file: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ValidatedCommandManifest } from '../command-manifests/manifest.js';
|
|
2
|
-
import type { KindConfig } from '../../types.js';
|
|
2
|
+
import type { KindConfig, PageComponentRegistration } from '../../types.js';
|
|
3
3
|
export interface BundleIssue {
|
|
4
4
|
code: string;
|
|
5
5
|
path?: string;
|
|
@@ -15,6 +15,11 @@ export interface ValidatedBundle {
|
|
|
15
15
|
* manifest, where `readMergedLaunchConfig` layers it — see
|
|
16
16
|
* `PluginManifest.kinds`. */
|
|
17
17
|
kinds?: Record<string, Partial<KindConfig>>;
|
|
18
|
+
/** Page-component contributions declared in `bundle.json` (optional). The
|
|
19
|
+
* installer copies this validated block into the synthesized plugin
|
|
20
|
+
* manifest, where `resolvePageComponents` reads it — see
|
|
21
|
+
* `PluginManifest.page_components`. */
|
|
22
|
+
page_components?: PageComponentRegistration[];
|
|
18
23
|
commands: ValidatedCommandManifest;
|
|
19
24
|
commandsBytes: Uint8Array;
|
|
20
25
|
directories: ReadonlyArray<{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Parser } from 'tar';
|
|
2
2
|
import { validateCommandManifest } from '../command-manifests/manifest.js';
|
|
3
3
|
import { invalidPluginKindsReasons } from '../config.js';
|
|
4
|
+
import { invalidPageComponentsReasons, normalizePageComponents } from '../human/page-catalog.js';
|
|
4
5
|
function issue(code, message, received, expected, next, path) {
|
|
5
6
|
return { code, message, received, expected, next, ...(path !== undefined ? { path } : {}) };
|
|
6
7
|
}
|
|
@@ -143,24 +144,35 @@ function parseBundleMetadata(bytes) {
|
|
|
143
144
|
};
|
|
144
145
|
}
|
|
145
146
|
const record = raw;
|
|
146
|
-
const unknownKeys = Object.keys(record).filter((key) => key !== 'bundleVersion' && key !== 'kinds');
|
|
147
|
+
const unknownKeys = Object.keys(record).filter((key) => key !== 'bundleVersion' && key !== 'kinds' && key !== 'page_components');
|
|
147
148
|
if (record['bundleVersion'] !== 1 || unknownKeys.length > 0) {
|
|
148
149
|
return {
|
|
149
|
-
issue: bundleInvalid('bundle.json must contain bundleVersion 1 and at most
|
|
150
|
+
issue: bundleInvalid('bundle.json must contain bundleVersion 1 and at most kinds and page_components blocks', JSON.stringify(raw), 'an object with bundleVersion set to 1 and optionally kinds and page_components', 'Set bundle.json to {"bundleVersion":1} plus an optional kinds object and page_components array.', 'bundle.json'),
|
|
150
151
|
};
|
|
151
152
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
153
|
+
// Install-time strictness: a bad block fails the install loudly here, because
|
|
154
|
+
// the read side (`mergeKinds`) drops invalid kind entries silently and would
|
|
155
|
+
// otherwise ship a kind that never registers.
|
|
156
|
+
if (record['kinds'] !== undefined) {
|
|
157
|
+
const reasons = invalidPluginKindsReasons(record['kinds']);
|
|
158
|
+
if (reasons.length > 0) {
|
|
159
|
+
return {
|
|
160
|
+
issue: bundleInvalid('bundle.json kinds block is invalid', reasons.join('; '), 'each entry a full KindConfig (whenToUse required) or a sparse patch of launch fields', 'Fix the kinds entries in bundle.json.', 'bundle.json'),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
162
163
|
}
|
|
163
|
-
|
|
164
|
+
if (record['page_components'] !== undefined) {
|
|
165
|
+
const reasons = invalidPageComponentsReasons(record['page_components']);
|
|
166
|
+
if (reasons.length > 0) {
|
|
167
|
+
return {
|
|
168
|
+
issue: bundleInvalid('bundle.json page_components block is invalid', reasons.join('; '), 'an array of page component registrations, each a kind string or {kind, description?, useWhen?, doc?, display?}', 'Fix the page_components entries in bundle.json.', 'bundle.json'),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
...(record['kinds'] !== undefined ? { kinds: record['kinds'] } : {}),
|
|
174
|
+
...(record['page_components'] !== undefined ? { page_components: normalizePageComponents(record['page_components']) } : {}),
|
|
175
|
+
};
|
|
164
176
|
}
|
|
165
177
|
function parseCommands(bytes, reservedCoreNames, coreCommandPaths) {
|
|
166
178
|
let raw;
|
|
@@ -206,6 +218,7 @@ export async function validatePluginBundle(archive, options) {
|
|
|
206
218
|
bundle: {
|
|
207
219
|
bundleVersion: 1,
|
|
208
220
|
...(metadata.kinds !== undefined ? { kinds: metadata.kinds } : {}),
|
|
221
|
+
...(metadata.page_components !== undefined ? { page_components: metadata.page_components } : {}),
|
|
209
222
|
commands: commandValidation.commands,
|
|
210
223
|
commandsBytes: commands.bytes,
|
|
211
224
|
directories: parsed.members
|
package/dist/core/command.js
CHANGED
|
@@ -439,9 +439,8 @@ export async function parseArgv(params, tokens, options) {
|
|
|
439
439
|
throw parseArgvError('bad_invocation', `unexpected extra positional argument: ${token}`, tokens.join(' '), undefined, 'Use --flag for parameters; only one positional allowed.');
|
|
440
440
|
}
|
|
441
441
|
// A bare positional is accepted when the leaf declares a positional param,
|
|
442
|
-
//
|
|
443
|
-
|
|
444
|
-
if (positionalParam === undefined && stdinParam === undefined) {
|
|
442
|
+
// or when its stdin parameter explicitly permits a positional body.
|
|
443
|
+
if (positionalParam === undefined && (stdinParam === undefined || stdinParam.allowPositional === false)) {
|
|
445
444
|
throw parseArgvError('bad_invocation', `this leaf takes no positional arguments: ${token}`, token, undefined, 'Use --flag for parameters. Run -h for the schema.');
|
|
446
445
|
}
|
|
447
446
|
positionalValues.push(token);
|
|
@@ -454,16 +453,12 @@ export async function parseArgv(params, tokens, options) {
|
|
|
454
453
|
? positionalValues
|
|
455
454
|
: positionalValues[0];
|
|
456
455
|
}
|
|
457
|
-
// Resolve stdin if declared.
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
// standard Unix convention for "read stdin instead" — it always reads,
|
|
461
|
-
// never becomes the literal value. And a positional alongside genuinely
|
|
462
|
-
// piped, non-empty stdin is ambiguous: never silently pick one, error loud
|
|
463
|
-
// and name both sources.
|
|
456
|
+
// Resolve stdin if declared. Some leaves permit one positional token to
|
|
457
|
+
// supply the stdin body; `-` then means read stdin. A positional alongside
|
|
458
|
+
// genuinely piped, non-empty stdin is ambiguous and errors loudly.
|
|
464
459
|
let stdinWasSupplied = false;
|
|
465
460
|
if (stdinParam !== undefined) {
|
|
466
|
-
if (positionalValues.length > 0 && positionalParam === undefined) {
|
|
461
|
+
if (positionalValues.length > 0 && positionalParam === undefined && stdinParam.allowPositional !== false) {
|
|
467
462
|
const positionalValue = positionalValues[0];
|
|
468
463
|
if (positionalValue === '-') {
|
|
469
464
|
const raw = await readStdinRaw();
|
|
@@ -547,7 +542,8 @@ export async function parseArgv(params, tokens, options) {
|
|
|
547
542
|
const missing = [];
|
|
548
543
|
for (const p of params) {
|
|
549
544
|
const key = flagNameToKey(p.name);
|
|
550
|
-
|
|
545
|
+
const missingRequiredBool = p.kind === 'flag' && p.type === 'bool' && !provided.has(key);
|
|
546
|
+
if (p.required && (missingRequiredBool || result[key] === undefined || result[key] === null)) {
|
|
551
547
|
const display = p.kind === 'positional'
|
|
552
548
|
? `positional ${p.name.toUpperCase()}`
|
|
553
549
|
: p.kind === 'flag'
|
package/dist/core/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Scope, ScopeConfig, ScopeState, KindConfig, ModelLaddersConfig, ModelProvider, ModelStrength, ModelRouteConfig, ModelRoutingConfig } from '../types.js';
|
|
1
|
+
import type { PageComponentRegistration, Scope, ScopeConfig, ScopeState, KindConfig, ModelLaddersConfig, ModelProvider, ModelStrength, ModelRouteConfig, ModelRoutingConfig } from '../types.js';
|
|
2
2
|
import { type BindingId } from './keybindings/catalog.js';
|
|
3
3
|
export declare function configPath(scope: Scope): string | null;
|
|
4
4
|
export declare function statePath(scope: Scope): string | null;
|
|
@@ -26,6 +26,52 @@ export declare function ensureScopeInitialized(scope: Scope, root: string): void
|
|
|
26
26
|
/** Normalize the user-owned list of attach actions allowed to bridge through an
|
|
27
27
|
* occupied tmux root binding. Unknown and non-attach ids have no effect. */
|
|
28
28
|
export declare function normalizeTmuxPassthrough(raw: unknown): BindingId[];
|
|
29
|
+
/** One malformed `bin` declaration. The read path retains these as doctor
|
|
30
|
+
* findings rather than letting malformed config break a node launch. */
|
|
31
|
+
export interface BinDeclarationIssue {
|
|
32
|
+
name: string;
|
|
33
|
+
message: string;
|
|
34
|
+
}
|
|
35
|
+
/** Validate raw `bin` declaration shape without touching the filesystem.
|
|
36
|
+
* Install uses these findings as a hard gate for plugin manifests; scope
|
|
37
|
+
* resolution reports them through `crtr sys doctor` and continues. */
|
|
38
|
+
export declare function binDeclarationIssues(raw: unknown): BinDeclarationIssue[];
|
|
39
|
+
/** Normalize one raw `bin` block (a scope `config.json`'s or a plugin
|
|
40
|
+
* manifest's) to name → root-relative path. Invalid entries never become
|
|
41
|
+
* commands; `binDeclarationIssues` keeps them visible to Doctor. */
|
|
42
|
+
export declare function normalizeBin(raw: unknown): Record<string, string>;
|
|
43
|
+
/** STRICT install-time validation for a plugin-declared `bin` block. Target
|
|
44
|
+
* filesystem state is checked later by Doctor because it can change after
|
|
45
|
+
* install. */
|
|
46
|
+
export declare function invalidPluginBinReasons(raw: unknown): string[];
|
|
47
|
+
/** Normalize a plugin manifest's advisory executable requirements. Invalid
|
|
48
|
+
* declarations stay out of the read path; source installs reject them through
|
|
49
|
+
* `invalidPluginRequiresReasons`. */
|
|
50
|
+
export declare function normalizeRequires(raw: unknown): Record<string, string>;
|
|
51
|
+
/** STRICT install-time validation for a plugin's advisory `requires` block.
|
|
52
|
+
* A missing executable is advisory; only malformed declarations fail install. */
|
|
53
|
+
export declare function invalidPluginRequiresReasons(raw: unknown): string[];
|
|
54
|
+
/** The ACTIVE page-component catalog every authoring, validation, daemon,
|
|
55
|
+
* inbox, and TUI consumer resolves: the user scope's own `page_components`
|
|
56
|
+
* entries plus the contributions of every ENABLED plugin installed under that
|
|
57
|
+
* same root, plugin name-sorted for determinism (directory listing order is
|
|
58
|
+
* filesystem-dependent). Installing, updating, or removing a plugin therefore
|
|
59
|
+
* adds, changes, or retires its components with no config edit.
|
|
60
|
+
*
|
|
61
|
+
* User scope only, deliberately: this catalog is read from the daemon, the
|
|
62
|
+
* CLI, and the inbox TUI, whose ambient cwds differ, and a page slot whose
|
|
63
|
+
* validity depended on which directory a process happened to start in would
|
|
64
|
+
* validate in one surface and fail in another. `page_components` has always
|
|
65
|
+
* been a user-scope block (`readConfig('user')`); its plugin channel keeps
|
|
66
|
+
* that boundary.
|
|
67
|
+
*
|
|
68
|
+
* Throws on a duplicate kind or derived JSX tag across sources, and on a
|
|
69
|
+
* plugin whose block is malformed — matching the existing catalog validation
|
|
70
|
+
* style, where a bad `page_components` block already fails config read rather
|
|
71
|
+
* than silently dropping a component the product expects to be authorable.
|
|
72
|
+
* Install-time validation (`invalidPageComponentsReasons`) is what keeps a
|
|
73
|
+
* malformed block from ever reaching this path. */
|
|
74
|
+
export declare function resolvePageComponents(): PageComponentRegistration[];
|
|
29
75
|
/** STRICT install-time validation for a plugin-declared `kinds` block —
|
|
30
76
|
* the loud counterpart to `mergeKinds`'s silent read-time dropping. Read
|
|
31
77
|
* paths must never throw on bad config, but an INSTALL delivering a bad
|