@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
package/dist/commands/surface.js
CHANGED
|
@@ -8,6 +8,7 @@ import { surfaceEditLeaf } from './surface-edit.js';
|
|
|
8
8
|
import { registerSurfaceNode } from './surface-node.js';
|
|
9
9
|
import { surfaceInspectLeaf } from './surface-inspect.js';
|
|
10
10
|
import { surfaceReloadLeaf } from './surface-reload.js';
|
|
11
|
+
import { surfaceInboxLeaf } from './surface-inbox.js';
|
|
11
12
|
export function registerSurface() {
|
|
12
13
|
const attach = registerAttach('surface', 'important');
|
|
13
14
|
return defineBranch({
|
|
@@ -22,6 +23,6 @@ export function registerSurface() {
|
|
|
22
23
|
summary: 'open and manage screen-facing surfaces',
|
|
23
24
|
model: 'Choose `node` for navigation among node viewers. The other commands open a broker attachment, node Inspector, subtree spread, reload, or adjacent editor pane.',
|
|
24
25
|
},
|
|
25
|
-
children: [registerSurfaceNode(), attach, surfaceInspectLeaf, surfaceReloadLeaf, tmuxSpreadLeaf, surfaceEditLeaf],
|
|
26
|
+
children: [registerSurfaceNode(), attach, surfaceInspectLeaf, surfaceReloadLeaf, tmuxSpreadLeaf, surfaceEditLeaf, surfaceInboxLeaf],
|
|
26
27
|
});
|
|
27
28
|
}
|
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { lstat, readFile, readdir, readlink } from 'node:fs/promises';
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { defineLeaf } from '../../core/command.js';
|
|
5
|
-
import { readConfig, updateConfig } from '../../core/config.js';
|
|
5
|
+
import { normalizeRequires, readConfig, updateConfig } from '../../core/config.js';
|
|
6
6
|
import { scopeRoot, listScopes, marketplacesDir, pluginsDir } from '../../core/scope.js';
|
|
7
7
|
import { readMarketplaceManifest, readPluginManifest } from '../../core/manifest.js';
|
|
8
8
|
import { pathExists, listDirs, removePath } from '../../core/fs-utils.js';
|
|
@@ -13,6 +13,9 @@ import { readFault } from '../../core/runtime/fault.js';
|
|
|
13
13
|
import { detectNerdFont, nerdFontInstallCommand, NERD_FONT_SELECT_HINT } from '../../core/runtime/nerd-font.js';
|
|
14
14
|
import { detectPackageManager } from './setup-core.js';
|
|
15
15
|
import { validateEffectiveCommandPlugins } from '../../core/command-plugins/discovery.js';
|
|
16
|
+
import { resolveBinContributions } from '../../core/runtime/bin-contributions.js';
|
|
17
|
+
import { buildBrokerEnv, resolvePathExecutable } from '../../core/runtime/spawn-env.js';
|
|
18
|
+
import { listAllPlugins } from '../../core/resolver.js';
|
|
16
19
|
import { SUBTREE_NAMES, coreCommandPaths } from '../../build-root.js';
|
|
17
20
|
import { processGeneration, processLocatedGeneration, runtimeManifest, selectedGeneration } from './shared.js';
|
|
18
21
|
function pass(scope, name, message) {
|
|
@@ -138,6 +141,58 @@ async function runCommandPluginChecks(scopes) {
|
|
|
138
141
|
}
|
|
139
142
|
return results;
|
|
140
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Report every bare-binary contribution (`bin` in a plugin manifest or a scope
|
|
146
|
+
* `config.json`) that failed to reach a node's PATH, plus the ones that did.
|
|
147
|
+
* Purely static — the declared executable is NEVER run, exactly as plugin
|
|
148
|
+
* command manifests are only ever validated. Every remediation is a
|
|
149
|
+
* human_action: fixing a contributor's declaration or its file is the owner's
|
|
150
|
+
* call, never doctor's. Only contributions from a scope in `scopes` are
|
|
151
|
+
* reported, so `--scope` narrows these checks too.
|
|
152
|
+
*/
|
|
153
|
+
function runBinContributionChecks(scopes) {
|
|
154
|
+
const inScope = new Set(scopes);
|
|
155
|
+
const { bins, issues } = resolveBinContributions(process.cwd(), process.env['CRTR_PROFILE_ID'] || null);
|
|
156
|
+
const results = [];
|
|
157
|
+
for (const issue of issues) {
|
|
158
|
+
if (!inScope.has(issue.scope))
|
|
159
|
+
continue;
|
|
160
|
+
results.push(failCheck(issue.scope, `bin:${issue.name}`, `${issue.origin}: ${issue.message}`, {
|
|
161
|
+
kind: 'human_action',
|
|
162
|
+
description: `Fix the \`bin.${issue.name}\` declaration in ${issue.origin} — point it at an executable regular file inside that contributor's own root, or remove it. A same-precedence collision is resolved by renaming one of the claimants.`,
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
for (const bin of bins.values()) {
|
|
166
|
+
if (!inScope.has(bin.scope))
|
|
167
|
+
continue;
|
|
168
|
+
results.push(pass(bin.scope, `bin:${bin.name}`, `${bin.origin} → ${bin.target}`));
|
|
169
|
+
}
|
|
170
|
+
return results;
|
|
171
|
+
}
|
|
172
|
+
/** Check each enabled plugin's advisory PATH requirements against the exact
|
|
173
|
+
* broker environment a node launched from this cwd/profile receives. */
|
|
174
|
+
function runRequiresChecks(scopes) {
|
|
175
|
+
const inScope = new Set(scopes);
|
|
176
|
+
const env = buildBrokerEnv({ argv: [], env: { CRTR_NODE_CWD: process.cwd(), CRTR_PROFILE_ID: process.env['CRTR_PROFILE_ID'] ?? '' } });
|
|
177
|
+
const results = [];
|
|
178
|
+
for (const plugin of listAllPlugins()) {
|
|
179
|
+
if (!inScope.has(plugin.scope) || !plugin.enabled || plugin.manifest.requires === undefined)
|
|
180
|
+
continue;
|
|
181
|
+
for (const [name, hint] of Object.entries(normalizeRequires(plugin.manifest.requires))) {
|
|
182
|
+
const target = resolvePathExecutable(name, env, process.cwd());
|
|
183
|
+
if (target !== null) {
|
|
184
|
+
results.push(pass(plugin.scope, `requires:${name}`, `plugin "${plugin.name}" → ${target}`));
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
results.push(failCheck(plugin.scope, `requires:${name}`, `plugin "${plugin.name}" requires executable "${name}" on PATH`, {
|
|
188
|
+
kind: 'human_action',
|
|
189
|
+
description: hint,
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return results;
|
|
195
|
+
}
|
|
141
196
|
async function inventory(root) {
|
|
142
197
|
const files = [];
|
|
143
198
|
async function walk(dir) {
|
|
@@ -422,12 +477,12 @@ export const sysDoctorLeaf = defineLeaf({
|
|
|
422
477
|
{ kind: 'flag', name: 'remote', type: 'bool', required: false, constraint: 'Check git remotes with ls-remote (slow — makes network calls).' },
|
|
423
478
|
],
|
|
424
479
|
output: [
|
|
425
|
-
{ name: 'checks', type: 'object[]', required: true, constraint: 'Each: {scope, name, status, message, fixed?, remediation?}. status: pass | fail. scope may be user | project | canvas. Runtime checks validate selected and process generation manifests, sealing/containment, and full SHA-256 inventories. terminal:nerd-font (user scope) reports whether a Nerd Font is installed for the viewer\'s icon glyphs; its remediation is always a human_action carrying the platform install command, never auto-fixed. Plugin manifest checks appear as plugin:<name>:commands (validated statically, never executed); their remediation is always a human_action (disable/update/remove the plugin). remediation (when present) is {kind, description, ...payload} where kind is remove_config_key | rm_path | human_action; fault human_action remediations include the canonical logs command. Sorted by scope then name.' },
|
|
480
|
+
{ name: 'checks', type: 'object[]', required: true, constraint: 'Each: {scope, name, status, message, fixed?, remediation?}. status: pass | fail. scope may be user | project | canvas. bin:<name> checks report bare-binary contributions (a `bin` block in a plugin manifest or a scope config.json): a pass names the contributor and the resolved target, a fail names an unsafe/reserved command name, malformed target declaration, missing/non-executable/escaping target, or same-precedence name collision that installed nothing; their remediation is always a human_action. requires:<name> checks report each enabled plugin manifest PATH requirement: a pass names the plugin and resolved executable path; a fail names the plugin and carries its declared install hint as remediation. Runtime checks validate selected and process generation manifests, sealing/containment, and full SHA-256 inventories. terminal:nerd-font (user scope) reports whether a Nerd Font is installed for the viewer\'s icon glyphs; its remediation is always a human_action carrying the platform install command, never auto-fixed. Plugin manifest checks appear as plugin:<name>:commands (validated statically, never executed); their remediation is always a human_action (disable/update/remove the plugin). remediation (when present) is {kind, description, ...payload} where kind is remove_config_key | rm_path | human_action; fault human_action remediations include the canonical logs command. Sorted by scope then name.' },
|
|
426
481
|
{ name: 'ok', type: 'boolean', required: true, constraint: 'True when no unresolved fail checks remain.' },
|
|
427
482
|
],
|
|
428
483
|
outputKind: 'object',
|
|
429
484
|
effects: [
|
|
430
|
-
'Read-only unless --fix is passed. Runtime verification reads and hashes every selected/process generation file; plugin command manifests
|
|
485
|
+
'Read-only unless --fix is passed. Runtime verification reads and hashes every selected/process generation file; plugin command manifests, bare-binary (`bin`) declarations, and advisory `requires` PATH checks never execute plugin or contributed binaries.',
|
|
431
486
|
'With --fix: applies each non-pass check\'s `remediation` — removes stale config entries and deletes dangling plugin/marketplace directories; canvas fault findings and plugin command findings stay read-only (--fix never chmods or rewrites third-party plugin content).',
|
|
432
487
|
'Each non-pass result carries a structured `remediation` describing the fix action (absolute paths, exact config keys, or a human_action with its command) so callers can apply it directly without --fix.',
|
|
433
488
|
],
|
|
@@ -445,6 +500,8 @@ export const sysDoctorLeaf = defineLeaf({
|
|
|
445
500
|
if (scopes.includes('user'))
|
|
446
501
|
allResults.push(runNerdFontCheck());
|
|
447
502
|
allResults.push(...await runCommandPluginChecks(scopes));
|
|
503
|
+
allResults.push(...runBinContributionChecks(scopes));
|
|
504
|
+
allResults.push(...runRequiresChecks(scopes));
|
|
448
505
|
allResults.push(...await runFaultChecks());
|
|
449
506
|
const processLocation = processGeneration();
|
|
450
507
|
allResults.push(...await generationChecks('selected', selectedGeneration(), processLocation === null));
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
// `crtr sys feedback` —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// `crtr sys feedback` — report a crtr harness bug when the harness itself
|
|
2
|
+
// misbehaves (a command errors unexpectedly, hangs, churns, or acts contrary to
|
|
3
|
+
// its documented contract). The agent supplies one prose description; everything
|
|
4
|
+
// else (version, node identity, recent activity) is auto-collected and templated
|
|
5
|
+
// into the report body.
|
|
6
|
+
//
|
|
7
|
+
// WHERE the report goes is a property of the deployment, not of the agent: this
|
|
8
|
+
// command owns the report — its contract, its context collection, its templating
|
|
9
|
+
// — while the deployment owns delivery. A host with `gh` and a person behind it
|
|
10
|
+
// opens the issue directly; a hosted runtime (a Northlight guest VM, say) has no
|
|
11
|
+
// GitHub credential and shouldn't be given one, so it sets
|
|
12
|
+
// CRTR_FEEDBACK_ENDPOINT and its backend forwards the report with an identity
|
|
13
|
+
// crtr never has to hold. Exactly one sink is chosen per deployment — this is a
|
|
14
|
+
// configured provider, not a fallback chain. Delivery failure on either path
|
|
15
|
+
// still writes a durable local file so the report is never lost.
|
|
7
16
|
import { spawnSync } from 'node:child_process';
|
|
8
17
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
9
18
|
import { join } from 'node:path';
|
|
@@ -19,6 +28,12 @@ import { readPackageVersion } from './shared.js';
|
|
|
19
28
|
/** Canonical repo for crtr harness feedback — feedback is always about the
|
|
20
29
|
* tool, regardless of which project the reporting agent is working in. */
|
|
21
30
|
const FEEDBACK_REPO = 'vallum-security/crouter';
|
|
31
|
+
/** Deployment-configured delivery. When CRTR_FEEDBACK_ENDPOINT names an HTTP
|
|
32
|
+
* endpoint, it is the sink and gh is not consulted; CRTR_FEEDBACK_TOKEN, when
|
|
33
|
+
* present, is sent as a bearer credential. */
|
|
34
|
+
const ENDPOINT_ENV = 'CRTR_FEEDBACK_ENDPOINT';
|
|
35
|
+
const TOKEN_ENV = 'CRTR_FEEDBACK_TOKEN';
|
|
36
|
+
const ENDPOINT_TIMEOUT_MS = 15_000;
|
|
22
37
|
async function collectContext() {
|
|
23
38
|
const env = process.env;
|
|
24
39
|
const nodeId = env['CRTR_NODE_ID'] ?? null;
|
|
@@ -106,7 +121,47 @@ function buildBody(message, ctx) {
|
|
|
106
121
|
}
|
|
107
122
|
return parts.join('\n') + '\n';
|
|
108
123
|
}
|
|
109
|
-
/**
|
|
124
|
+
/** POST the report to the deployment's configured endpoint. The endpoint may
|
|
125
|
+
* answer with a `url` naming where the report landed; anything else about the
|
|
126
|
+
* response body is the backend's business, not ours. */
|
|
127
|
+
async function deliverToEndpoint(endpoint, title, body, message, ctx) {
|
|
128
|
+
const token = process.env[TOKEN_ENV];
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetch(endpoint, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: {
|
|
133
|
+
'content-type': 'application/json',
|
|
134
|
+
...(token !== undefined && token !== '' ? { authorization: `Bearer ${token}` } : {}),
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify({ title, body, message, context: ctx }),
|
|
137
|
+
signal: AbortSignal.timeout(ENDPOINT_TIMEOUT_MS),
|
|
138
|
+
});
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
const detail = (await res.text().catch(() => '')).trim().slice(0, 400);
|
|
141
|
+
return { error: `${ENDPOINT_ENV} returned HTTP ${res.status}${detail === '' ? '' : `: ${detail}`}` };
|
|
142
|
+
}
|
|
143
|
+
const payload = (await res.json().catch(() => null));
|
|
144
|
+
const url = typeof payload?.url === 'string' ? payload.url : undefined;
|
|
145
|
+
return url === undefined ? {} : { url };
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
return { error: `${ENDPOINT_ENV} unreachable: ${err instanceof Error ? err.message : String(err)}` };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Open the GitHub issue directly with the host's own gh credential. */
|
|
152
|
+
function deliverToGitHub(title, body) {
|
|
153
|
+
const res = spawnSync('gh', ['issue', 'create', '--repo', FEEDBACK_REPO, '--title', title, '--body-file', '-'], { input: body, encoding: 'utf8' });
|
|
154
|
+
if (res.error === undefined && res.status === 0) {
|
|
155
|
+
const url = res.stdout.trim().split('\n').filter((l) => l.startsWith('http')).pop() ?? res.stdout.trim();
|
|
156
|
+
return { url };
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
error: res.error !== undefined
|
|
160
|
+
? `gh not available: ${res.error.message}`
|
|
161
|
+
: (res.stderr?.trim() || `gh exited ${res.status}`),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** Persist a report locally when delivery fails, so nothing is lost. Returns
|
|
110
165
|
* the absolute path written. */
|
|
111
166
|
function saveLocally(title, body) {
|
|
112
167
|
const dir = join(userScopeRoot(), 'feedback');
|
|
@@ -118,17 +173,17 @@ function saveLocally(title, body) {
|
|
|
118
173
|
}
|
|
119
174
|
export const sysFeedbackLeaf = defineLeaf({
|
|
120
175
|
name: 'feedback',
|
|
121
|
-
description: 'report a crtr harness bug
|
|
122
|
-
whenToUse: 'a crtr command itself misbehaves — it errors unexpectedly, hangs, churns, double-spawns, or acts contrary to its documented contract. NOT for your task work or decisions
|
|
176
|
+
description: 'report a crtr harness bug to whoever maintains this crtr install',
|
|
177
|
+
whenToUse: 'a crtr command itself misbehaves — it errors unexpectedly, hangs, churns, double-spawns, or acts contrary to its documented contract. NOT for your task work or decisions the user should make (that is `crtr human send`); this reports the TOOL, filing a report with the crtr maintainers with your node context auto-attached.',
|
|
123
178
|
help: {
|
|
124
179
|
name: 'sys feedback',
|
|
125
|
-
summary: 'file a
|
|
180
|
+
summary: 'file a report describing a crtr harness bug, with node context auto-attached',
|
|
126
181
|
params: [
|
|
127
182
|
{
|
|
128
183
|
kind: 'positional',
|
|
129
184
|
name: 'message',
|
|
130
185
|
required: true,
|
|
131
|
-
constraint: 'What broke, in your own words: the command you ran, what you expected, what happened instead. Be concrete — this is the only part
|
|
186
|
+
constraint: 'What broke, in your own words: the command you ran, what you expected, what happened instead. Be concrete — this is the only part you write; everything else is auto-collected.',
|
|
132
187
|
},
|
|
133
188
|
{
|
|
134
189
|
kind: 'flag',
|
|
@@ -140,14 +195,14 @@ export const sysFeedbackLeaf = defineLeaf({
|
|
|
140
195
|
],
|
|
141
196
|
output: [
|
|
142
197
|
{ name: 'status', type: 'string', required: true, constraint: 'filed | saved-locally.' },
|
|
143
|
-
{ name: 'url', type: 'string', required: false, constraint: '
|
|
144
|
-
{ name: 'path', type: 'string', required: false, constraint: 'Local report path when status is saved-locally
|
|
198
|
+
{ name: 'url', type: 'string', required: false, constraint: 'Where the report landed, when the sink names a location.' },
|
|
199
|
+
{ name: 'path', type: 'string', required: false, constraint: 'Local report path when status is saved-locally.' },
|
|
145
200
|
{ name: 'error', type: 'string', required: false, constraint: 'Why filing fell back to local save.' },
|
|
146
201
|
],
|
|
147
202
|
outputKind: 'object',
|
|
148
203
|
effects: [
|
|
149
|
-
`
|
|
150
|
-
'On
|
|
204
|
+
`Sends the report where this install delivers harness feedback: the ${ENDPOINT_ENV} endpoint when the deployment sets one, otherwise a public GitHub issue on ${FEEDBACK_REPO} via gh.`,
|
|
205
|
+
'On delivery failure, writes the report to ~/.crouter/feedback/ instead — never lost.',
|
|
151
206
|
],
|
|
152
207
|
},
|
|
153
208
|
run: async (input) => {
|
|
@@ -158,17 +213,16 @@ export const sysFeedbackLeaf = defineLeaf({
|
|
|
158
213
|
const ctx = await collectContext();
|
|
159
214
|
const title = deriveTitle(input['title'], message);
|
|
160
215
|
const body = buildBody(message, ctx);
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
216
|
+
const endpoint = process.env[ENDPOINT_ENV]?.trim();
|
|
217
|
+
const delivery = endpoint !== undefined && endpoint !== ''
|
|
218
|
+
? await deliverToEndpoint(endpoint, title, body, message, ctx)
|
|
219
|
+
: deliverToGitHub(title, body);
|
|
220
|
+
if (delivery.error === undefined) {
|
|
221
|
+
return delivery.url === undefined ? { status: 'filed' } : { status: 'filed', url: delivery.url };
|
|
165
222
|
}
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
const reason = res.error !== undefined
|
|
169
|
-
? `gh not available: ${res.error.message}`
|
|
170
|
-
: (res.stderr?.trim() || `gh exited ${res.status}`);
|
|
223
|
+
// The sink rejected or was unreachable — persist locally so the report
|
|
224
|
+
// survives, and tell the agent why.
|
|
171
225
|
const path = saveLocally(title, body);
|
|
172
|
-
return { status: 'saved-locally', path, error:
|
|
226
|
+
return { status: 'saved-locally', path, error: delivery.error };
|
|
173
227
|
},
|
|
174
228
|
});
|
|
@@ -9,7 +9,7 @@ import { BROKER_LIMIT_OVERRIDE_PRECEDENCE } from './setup.js';
|
|
|
9
9
|
export const sysSettingsLeaf = defineLeaf({
|
|
10
10
|
name: 'settings',
|
|
11
11
|
description: 'open the settings page — providers, models, keybindings, display, panes, broker limits, updates',
|
|
12
|
-
whenToUse: '
|
|
12
|
+
whenToUse: 'the user wants to change crouter configuration interactively. Agents should use sys config get/set, which is scriptable; sys setup installs a fresh machine.',
|
|
13
13
|
help: {
|
|
14
14
|
name: 'sys settings',
|
|
15
15
|
summary: 'open the interactive settings page for an already-installed crouter',
|
|
@@ -104,7 +104,7 @@ test('an on-change cron that cancels itself still delivers its final output', as
|
|
|
104
104
|
assert.equal(markSelfCanceledCronRun('self-cancel-delivery', readFileSync(runIdPath, 'utf8')), true);
|
|
105
105
|
assert.equal(cancelCron('self-cancel-delivery', { preserveRunningProcess: true }), true);
|
|
106
106
|
const record = await run;
|
|
107
|
-
assert.equal(record?.delivered, 'delivered: human
|
|
107
|
+
assert.equal(record?.delivered, 'delivered: human send --inbox');
|
|
108
108
|
});
|
|
109
109
|
test('cronLaunchesNode classifies by command verb and armed sink', () => {
|
|
110
110
|
const of = (command, sink = '{}') => cronLaunchesNode({ command, sink });
|
|
@@ -131,8 +131,11 @@ test('an unplaced active node is left alone before it boots', async () => {
|
|
|
131
131
|
// node with no fleet entry must NOT be spuriously revived by the tick. Pass-2
|
|
132
132
|
// is the ONLY wake path for a released node, and it fires solely on an unseen
|
|
133
133
|
// inbox entry — a null recorded pid plus a saved session must never read as a
|
|
134
|
-
// "stranded relaunch" (that inference branch is deleted).
|
|
135
|
-
|
|
134
|
+
// "stranded relaunch" (that inference branch is deleted). With nothing left to
|
|
135
|
+
// wake it and no live subscriber awaiting its report, the same pass settles it
|
|
136
|
+
// as a terminal node torn down before finishing rather than leaving a
|
|
137
|
+
// permanently protected idle row.
|
|
138
|
+
test('idle-release node with a boot-cleared (null) pid and no unseen inbox is settled, never revived', async () => {
|
|
136
139
|
createNode(node('D', {
|
|
137
140
|
status: 'idle',
|
|
138
141
|
intent: 'idle-release',
|
|
@@ -143,8 +146,8 @@ test('idle-release node with a boot-cleared (null) pid and no unseen inbox stays
|
|
|
143
146
|
await superviseTick(t0); // first observation
|
|
144
147
|
await superviseTick(t0 + 60_000); // well past the boot grace
|
|
145
148
|
const after = getNode('D');
|
|
146
|
-
assert.equal(after.status, '
|
|
147
|
-
assert.equal(after.intent,
|
|
149
|
+
assert.equal(after.status, 'canceled', 'a dormant terminal row nobody awaits becomes prunable canceled history');
|
|
150
|
+
assert.equal(after.intent, null, 'no revive fired — the row is settled, not relaunched');
|
|
148
151
|
assert.equal(after.pi_pid, null, 'no fresh pid — reviveNode was never invoked');
|
|
149
152
|
});
|
|
150
153
|
// Daemon start guard: spawnDaemon must not report success before the daemon is
|
|
@@ -4,11 +4,7 @@
|
|
|
4
4
|
// pure function mapping a known-dead node's durable row state to its recovery
|
|
5
5
|
// action, used at exactly two call sites — the exit-policy job and the startup
|
|
6
6
|
// recovery sweep — so exit recovery and startup recovery can never diverge.
|
|
7
|
-
// This file pins every table row directly
|
|
8
|
-
// five deleted liveness-inference branches used to carry (grace-clock,
|
|
9
|
-
// stranded-relaunch, daemon-liveness), plus the `retryResumeMode` policy pins
|
|
10
|
-
// ported verbatim from the deleted grace-clock suite (the function is pure and
|
|
11
|
-
// unchanged — only its caller moved into the table).
|
|
7
|
+
// This file pins every table row directly.
|
|
12
8
|
import { test } from 'node:test';
|
|
13
9
|
import assert from 'node:assert/strict';
|
|
14
10
|
import { classifyDeadNode, retryResumeMode } from '../../daemon/fleet.js';
|
|
@@ -24,7 +20,7 @@ function row(over = {}) {
|
|
|
24
20
|
};
|
|
25
21
|
}
|
|
26
22
|
// ===========================================================================
|
|
27
|
-
// The
|
|
23
|
+
// The eight table rows, in evaluation order (design flow B).
|
|
28
24
|
// ===========================================================================
|
|
29
25
|
test('row 1: a deleted row (orphan of a pruned/closed node) is forgotten', () => {
|
|
30
26
|
assert.equal(classifyDeadNode(null, false), 'forget');
|
|
@@ -44,42 +40,30 @@ test('row 4: intent=idle-release (chosen dormancy) takes no action — pass-2 in
|
|
|
44
40
|
assert.equal(classifyDeadNode(row({ intent: 'idle-release' }), false), 'dormant');
|
|
45
41
|
assert.equal(classifyDeadNode(row({ intent: 'idle-release', pi_session_id: 'sess' }), false), 'dormant', 'a saved session does not turn chosen dormancy into a crash');
|
|
46
42
|
});
|
|
47
|
-
test('row 5:
|
|
48
|
-
assert.equal(classifyDeadNode(row({ pi_session_id: 'sess' }), true, true), 'respawn-resume');
|
|
49
|
-
assert.equal(classifyDeadNode(row({ pi_session_id: 'sess', cycle_pending: true }), true, true), 'respawn-resume', 'the clean-abort certificate proves this interrupted turn is resumable, even if a prior cycle was pending');
|
|
50
|
-
assert.equal(classifyDeadNode(row(), true, true), 'respawn-cycle', 'a certificate without a saved session cannot turn a dirty busy death into a resume');
|
|
51
|
-
});
|
|
52
|
-
test('row 6: a set busy marker without clean-abort evidence is a dirty mid-turn crash — ALWAYS a recovery cycle', () => {
|
|
53
|
-
assert.equal(classifyDeadNode(row(), true), 'respawn-cycle');
|
|
54
|
-
assert.equal(classifyDeadNode(row({ pi_session_id: 'sess' }), true), 'respawn-cycle');
|
|
55
|
-
assert.equal(classifyDeadNode(row({ pi_session_id: 'sess', cycle_pending: true }), true), 'respawn-cycle');
|
|
56
|
-
});
|
|
57
|
-
test('row 7: a saved session with no crash markers resumes strictly; a pending cycle retries AS a cycle', () => {
|
|
43
|
+
test('row 5: every saved session resumes strictly unless its own pending cycle must be retried', () => {
|
|
58
44
|
assert.equal(classifyDeadNode(row({ pi_session_id: 'sess' }), false), 'respawn-resume');
|
|
59
|
-
assert.equal(classifyDeadNode(row({ pi_session_id: 'sess'
|
|
60
|
-
assert.equal(classifyDeadNode(row({ pi_session_id: 'sess', cycle_pending: false }),
|
|
45
|
+
assert.equal(classifyDeadNode(row({ pi_session_id: 'sess' }), true), 'respawn-resume', 'a dirty interrupted turn resumes in place; the respawn continuation re-drives it');
|
|
46
|
+
assert.equal(classifyDeadNode(row({ pi_session_id: 'sess', cycle_pending: false }), true), 'respawn-resume');
|
|
47
|
+
assert.equal(classifyDeadNode(row({ pi_session_id: 'sess', cycle_pending: true }), true), 'respawn-cycle', 'an interrupted unconfirmed cycling attempt still retries AS that cycle');
|
|
48
|
+
});
|
|
49
|
+
test('row 6: a sessionless node that died mid-turn relaunches fresh instead of becoming a boot failure', () => {
|
|
50
|
+
assert.equal(classifyDeadNode(row(), true), 'respawn-fresh');
|
|
61
51
|
});
|
|
62
|
-
test('row
|
|
52
|
+
test('row 7: an ordinary fork dead before binding its child session retries the immutable fork birth', () => {
|
|
63
53
|
assert.equal(classifyDeadNode(row({ fork_from: '/reviews/branch-point.jsonl' }), false), 'respawn-fresh');
|
|
64
54
|
});
|
|
65
|
-
test('row
|
|
55
|
+
test('row 8: sessionless with no fork claim — a fresh launch died before session_start — is a boot failure', () => {
|
|
66
56
|
assert.equal(classifyDeadNode(row(), false), 'boot-failure');
|
|
67
57
|
});
|
|
68
58
|
// ===========================================================================
|
|
69
|
-
// retryResumeMode (pure)
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// (cycle_pending), OR when a surviving busy marker proves the broker died
|
|
73
|
-
// mid-turn. Idle crashes resume STRICTLY (resume:true). See canvas.test.ts's
|
|
74
|
-
// updateNode/getNode round-trip for the companion guarantee that cycle_pending
|
|
75
|
-
// survives the persistence the policy job reads it back from.
|
|
59
|
+
// retryResumeMode (pure): only an unconfirmed cycling launch must cycle again.
|
|
60
|
+
// An interrupted ordinary turn resumes strictly; `#respawn` adds the runtime
|
|
61
|
+
// continuation that re-drives the stopped work in the same conversation.
|
|
76
62
|
// ===========================================================================
|
|
77
|
-
test('retryResumeMode:
|
|
78
|
-
assert.equal(retryResumeMode({ cycle_pending: true }
|
|
79
|
-
assert.equal(retryResumeMode({ cycle_pending: false }, true
|
|
80
|
-
assert.equal(retryResumeMode({ cycle_pending:
|
|
81
|
-
assert.equal(retryResumeMode({
|
|
82
|
-
assert.equal(retryResumeMode(
|
|
83
|
-
assert.equal(retryResumeMode({}, false), true, 'cycle_pending undefined idle crash resumes strictly');
|
|
84
|
-
assert.equal(retryResumeMode(null, false), true, 'no meta and no busy marker resumes strictly');
|
|
63
|
+
test('retryResumeMode: pending cycles recover as cycles; every confirmed saved session resumes strictly', () => {
|
|
64
|
+
assert.equal(retryResumeMode({ cycle_pending: true }), false, 'a pending cycling attempt retries AS a cycle');
|
|
65
|
+
assert.equal(retryResumeMode({ cycle_pending: false }), true, 'an ordinary saved session resumes strictly');
|
|
66
|
+
assert.equal(retryResumeMode({ cycle_pending: null }), true, 'cycle_pending=null resumes strictly');
|
|
67
|
+
assert.equal(retryResumeMode({}), true, 'cycle_pending undefined resumes strictly');
|
|
68
|
+
assert.equal(retryResumeMode(null), true, 'no pending cycle resumes strictly');
|
|
85
69
|
});
|
|
@@ -10,9 +10,12 @@ test('refresh extension abort clears Pi follow-ups before abort while an ordinar
|
|
|
10
10
|
abortSessionFromExtension(session);
|
|
11
11
|
assert.deepEqual(events, ['abort'], 'ordinary extension abort leaves queued messages in the current conversation');
|
|
12
12
|
events.length = 0;
|
|
13
|
-
|
|
13
|
+
let refreshAborts = 0;
|
|
14
|
+
runRefreshAbort(() => abortSessionFromExtension(session, () => { refreshAborts += 1; }));
|
|
14
15
|
assert.deepEqual(events, ['clear', 'abort'], 'refresh removes queued messages before stopping the discarded conversation');
|
|
16
|
+
assert.equal(refreshAborts, 1, 'refresh stages provenance with the host abort');
|
|
15
17
|
events.length = 0;
|
|
16
|
-
abortSessionFromExtension(session);
|
|
18
|
+
abortSessionFromExtension(session, () => { refreshAborts += 1; });
|
|
17
19
|
assert.deepEqual(events, ['abort'], 'the refresh scope does not leak into a later critical abort');
|
|
20
|
+
assert.equal(refreshAborts, 1, 'an ordinary extension abort does not stage refresh provenance');
|
|
18
21
|
});
|
|
@@ -188,8 +188,19 @@ declare class FakeSession {
|
|
|
188
188
|
private syncTreeSnapshot;
|
|
189
189
|
setSessionName(_name: string): void;
|
|
190
190
|
prompt(text: string, _options?: unknown): Promise<void>;
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
/** The unrun queues the welcome snapshot reports (`snapshot.queued`). Real
|
|
192
|
+
* pi holds a steer + followUp queue and drains it when the next turn runs;
|
|
193
|
+
* the fake records the same texts and clears them in emitTurn, so a client
|
|
194
|
+
* attaching mid-queue sees what a real broker would hand it. Both readers
|
|
195
|
+
* exist on the real AgentSession, so omitting them does not make the fake
|
|
196
|
+
* merely queueless — it makes buildSnapshot throw, and every attach test
|
|
197
|
+
* then times out waiting for a welcome the broker never built. */
|
|
198
|
+
private readonly steering;
|
|
199
|
+
private readonly followUps;
|
|
200
|
+
getSteeringMessages(): readonly string[];
|
|
201
|
+
getFollowUpMessages(): readonly string[];
|
|
202
|
+
steer(text: string): Promise<void>;
|
|
203
|
+
followUp(text: string): Promise<void>;
|
|
193
204
|
/** In-memory recording only — no proof file, matching the offline in-process
|
|
194
205
|
* contract broker-sdk-wiring.test.ts's REAL-SDK tests already use. Purely
|
|
195
206
|
* defensive: broker.ts's driveEngine now calls `session.sendCustomMessage`
|
|
@@ -224,7 +224,22 @@ class SessionTree {
|
|
|
224
224
|
buildSessionContext() {
|
|
225
225
|
return { messages: this.branchMessages(), thinkingLevel: 'off', model: null };
|
|
226
226
|
}
|
|
227
|
+
/** Every entry in the session file, active branch or abandoned tail alike —
|
|
228
|
+
* the real SessionManager.getEntries(). */
|
|
229
|
+
getEntries() {
|
|
230
|
+
return [...this.entries.values()];
|
|
231
|
+
}
|
|
232
|
+
/** The active branch root→leaf — the real SessionManager.getBranch(). The
|
|
233
|
+
* extensions that scan for their own marker read THIS, not the whole file:
|
|
234
|
+
* a new cycle must not see a previous branch's markers. */
|
|
235
|
+
getBranch() {
|
|
236
|
+
return this.branchIds().map((id) => this.entries.get(id)).filter((entry) => entry !== undefined);
|
|
237
|
+
}
|
|
227
238
|
branchMessages() {
|
|
239
|
+
return this.branchIds().map((id) => this.entries.get(id)?.message).filter((m) => m !== undefined);
|
|
240
|
+
}
|
|
241
|
+
/** The active branch's entry ids, root→leaf. */
|
|
242
|
+
branchIds() {
|
|
228
243
|
const ids = [];
|
|
229
244
|
let cur = this.leafId;
|
|
230
245
|
while (cur !== null) {
|
|
@@ -235,7 +250,7 @@ class SessionTree {
|
|
|
235
250
|
cur = entry.parentId;
|
|
236
251
|
}
|
|
237
252
|
ids.reverse();
|
|
238
|
-
return ids
|
|
253
|
+
return ids;
|
|
239
254
|
}
|
|
240
255
|
node(id) {
|
|
241
256
|
const entry = this.entries.get(id);
|
|
@@ -446,10 +461,27 @@ class FakeSession {
|
|
|
446
461
|
this.promptFailuresRemaining--;
|
|
447
462
|
return this.emitTurn(text, { fail, runHooks: true });
|
|
448
463
|
}
|
|
449
|
-
|
|
464
|
+
/** The unrun queues the welcome snapshot reports (`snapshot.queued`). Real
|
|
465
|
+
* pi holds a steer + followUp queue and drains it when the next turn runs;
|
|
466
|
+
* the fake records the same texts and clears them in emitTurn, so a client
|
|
467
|
+
* attaching mid-queue sees what a real broker would hand it. Both readers
|
|
468
|
+
* exist on the real AgentSession, so omitting them does not make the fake
|
|
469
|
+
* merely queueless — it makes buildSnapshot throw, and every attach test
|
|
470
|
+
* then times out waiting for a welcome the broker never built. */
|
|
471
|
+
steering = [];
|
|
472
|
+
followUps = [];
|
|
473
|
+
getSteeringMessages() {
|
|
474
|
+
return this.steering;
|
|
475
|
+
}
|
|
476
|
+
getFollowUpMessages() {
|
|
477
|
+
return this.followUps;
|
|
478
|
+
}
|
|
479
|
+
steer(text) {
|
|
480
|
+
this.steering.push(text);
|
|
450
481
|
return Promise.resolve();
|
|
451
482
|
}
|
|
452
|
-
followUp(
|
|
483
|
+
followUp(text) {
|
|
484
|
+
this.followUps.push(text);
|
|
453
485
|
return Promise.resolve();
|
|
454
486
|
}
|
|
455
487
|
/** In-memory recording only — no proof file, matching the offline in-process
|
|
@@ -560,6 +592,9 @@ class FakeSession {
|
|
|
560
592
|
// still climbs monotonically past the 32 MiB byte cap and is shed. Without the
|
|
561
593
|
// pace the burst outruns the reader's event loop and sheds the fast viewer too.
|
|
562
594
|
const yieldTick = () => pad > 0 ? new Promise((r) => setTimeout(r, 3)) : new Promise((r) => setImmediate(r));
|
|
595
|
+
// A turn consumes whatever was queued for it, as the real session does.
|
|
596
|
+
this.steering.length = 0;
|
|
597
|
+
this.followUps.length = 0;
|
|
563
598
|
this.streaming = true;
|
|
564
599
|
if (opts.runHooks === true) {
|
|
565
600
|
await this.fire('agent_start', {}, this.buildCtx());
|
|
@@ -789,6 +824,7 @@ class FakeSession {
|
|
|
789
824
|
CRTR_MODE: env['CRTR_MODE'] ?? null,
|
|
790
825
|
CRTR_LIFECYCLE: env['CRTR_LIFECYCLE'] ?? null,
|
|
791
826
|
CRTR_NODE_CWD: env['CRTR_NODE_CWD'] ?? null,
|
|
827
|
+
CRTR_PROFILE_ID: env['CRTR_PROFILE_ID'] ?? null,
|
|
792
828
|
CRTR_HOME: env['CRTR_HOME'] ?? null,
|
|
793
829
|
CRTR_PARENT_NODE_ID: env['CRTR_PARENT_NODE_ID'] ?? null,
|
|
794
830
|
CRTR_ROOT_SESSION: env['CRTR_ROOT_SESSION'] ?? null,
|
|
@@ -829,6 +865,14 @@ class FakeSession {
|
|
|
829
865
|
getSessionId: () => this.sm.getSessionId(),
|
|
830
866
|
getSessionFile: () => this.sm.getSessionFile(),
|
|
831
867
|
getLeafId: () => this.turnEndLeafId ?? this.tree.getLeafId(),
|
|
868
|
+
// The session-scanning extensions (context-intro, review-boundary,
|
|
869
|
+
// recap) read the branch at session_start to decide whether their own
|
|
870
|
+
// marker is already present. Both exist on the real SessionManager, so
|
|
871
|
+
// a fake missing them is not a degraded fixture but a broken one: the
|
|
872
|
+
// handler throws, session_start never completes, and every attach test
|
|
873
|
+
// times out waiting for a welcome that the broker never sends.
|
|
874
|
+
getBranch: () => this.tree.getBranch(),
|
|
875
|
+
getEntries: () => this.tree.getEntries(),
|
|
832
876
|
},
|
|
833
877
|
getContextUsage: () => ({ tokens: 1000 }),
|
|
834
878
|
// Wired to the broker's shutdownHandler: the stophook calls ctx.shutdown()
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
// Run with: node --import tsx/esm --test src/core/__tests__/human-cancel-guard.test.ts
|
|
2
2
|
//
|
|
3
|
-
// N2 — humanCancel on
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// N2 — humanCancel on a ticket the store has already settled must short-circuit to
|
|
4
|
+
// {canceled:false, reason:'already_resolved'} rather than throwing. A second cancel
|
|
5
|
+
// is the ordinary race (the human submits while the agent retracts), so the client
|
|
6
|
+
// guard in queue.ts turns the daemon's 409 into an idempotent no-op.
|
|
7
7
|
import { test, before, after, beforeEach, afterEach } from 'node:test';
|
|
8
8
|
import assert from 'node:assert/strict';
|
|
9
9
|
import { mkdtempSync, mkdirSync, rmSync, existsSync } from 'node:fs';
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
|
-
import { createNode
|
|
12
|
+
import { createNode } from '../canvas/canvas.js';
|
|
13
13
|
import { closeDb } from '../canvas/db.js';
|
|
14
|
+
import { nodeDir } from '../canvas/paths.js';
|
|
15
|
+
import { atomicWriteJson, bindTicketReplyRoute, pageManifestPath } from '../human/convention.js';
|
|
16
|
+
import { cancelTicket } from '../human/tickets.js';
|
|
14
17
|
import { humanCancel } from '../../commands/human/queue.js';
|
|
15
18
|
import { createApiServer } from '../../daemon/api/server.js';
|
|
16
19
|
import { apiSocketPath } from '../canvas/paths.js';
|
|
@@ -35,12 +38,29 @@ function node(id) {
|
|
|
35
38
|
name: id,
|
|
36
39
|
created: new Date().toISOString(),
|
|
37
40
|
cwd: '/tmp/work',
|
|
38
|
-
kind: '
|
|
41
|
+
kind: 'human',
|
|
39
42
|
mode: 'base',
|
|
40
43
|
lifecycle: 'terminal',
|
|
41
44
|
status: 'active',
|
|
42
45
|
};
|
|
43
46
|
}
|
|
47
|
+
/** Publish a reply-bearing page ticket in the bridge node's directory, exactly as
|
|
48
|
+
* `human send` does: reply route first, then the page manifest. */
|
|
49
|
+
function publishTicket(id) {
|
|
50
|
+
const dir = nodeDir(id);
|
|
51
|
+
mkdirSync(dir, { recursive: true });
|
|
52
|
+
bindTicketReplyRoute(dir, id);
|
|
53
|
+
atomicWriteJson(pageManifestPath(dir), {
|
|
54
|
+
schema: 'crtr.page/v2',
|
|
55
|
+
dialect: 'jsx',
|
|
56
|
+
title: id,
|
|
57
|
+
subtitle: 'Acknowledge this ticket so the request can proceed.',
|
|
58
|
+
delivery: { placement: 'inline', inbox: true, reply: true },
|
|
59
|
+
document: 'page.tsx',
|
|
60
|
+
steps: 1,
|
|
61
|
+
slots: [],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
44
64
|
before(() => {
|
|
45
65
|
home = mkdtempSync(join(tmpdir(), 'crtr-humancancel-'));
|
|
46
66
|
process.env['CRTR_HOME'] = home;
|
|
@@ -60,12 +80,13 @@ after(() => {
|
|
|
60
80
|
rmSync(home, { recursive: true, force: true });
|
|
61
81
|
delete process.env['CRTR_HOME'];
|
|
62
82
|
});
|
|
63
|
-
test('cancel on an already-canceled
|
|
83
|
+
test('cancel on an already-canceled ticket is a no-op, never throws on the finalize', async () => {
|
|
64
84
|
const id = 'canceledJob';
|
|
65
85
|
createNode(node(id));
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
publishTicket(id);
|
|
87
|
+
cancelTicket(nodeDir(id), { actor: 'human' }); // settled by the first cancel
|
|
88
|
+
const res = (await humanCancel.run({ ticket_id: id }));
|
|
68
89
|
assert.equal(res['canceled'], false);
|
|
69
90
|
assert.equal(res['reason'], 'already_resolved');
|
|
70
|
-
assert.equal(res['
|
|
91
|
+
assert.equal(res['ticket_id'], id);
|
|
71
92
|
});
|