@north-light/crouter 0.3.198 → 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 +18 -18
- 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 +81 -34
- 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 +6 -6
- 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 +2 -2
- 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/clients/attach/chrome/bash-jobs.js +3 -3
- package/dist/clients/attach/render/chat-view.d.ts +26 -1
- package/dist/clients/attach/render/chat-view.js +61 -11
- 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/session/whip.d.ts +2 -0
- package/dist/clients/attach/session/whip.js +2 -0
- package/dist/clients/attach/viewer.js +1419 -993
- 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 -21
- package/dist/clients/inbox/controller.js +10 -16
- package/dist/clients/inbox/page-adapter.d.ts +1 -3
- 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 +71 -27
- package/dist/clients/inbox/tui/slots.d.ts +3 -1
- package/dist/clients/inbox/tui/slots.js +16 -12
- package/dist/clients/inbox/tui/types.d.ts +2 -7
- package/dist/clients/inbox/tui.js +2 -2
- package/dist/commands/__tests__/human.test.js +71 -114
- package/dist/commands/__tests__/node-message.test.js +14 -0
- package/dist/commands/attention.js +4 -4
- 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/{html.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 +13 -5
- package/dist/commands/human/prompts.js +160 -97
- package/dist/commands/human/queue.js +57 -88
- package/dist/commands/human/shared.d.ts +2 -12
- package/dist/commands/human/shared.js +17 -11
- package/dist/commands/human.js +29 -21
- 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__/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 +31 -26
- 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/dormancy-release.test.js +40 -0
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -6
- 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/model.d.ts +8 -2
- package/dist/core/canvas/browse/model.js +11 -4
- package/dist/core/canvas/canvas.d.ts +7 -0
- package/dist/core/canvas/canvas.js +37 -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 +2 -1
- package/dist/core/config.d.ts +47 -1
- package/dist/core/config.js +137 -4
- package/dist/core/help.js +1 -1
- package/dist/core/human/__tests__/page-catalog.test.js +4 -0
- package/dist/core/human/__tests__/page-tickets.test.js +65 -52
- package/dist/core/human/__tests__/page.test.js +56 -98
- package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -6
- package/dist/core/human/answer-text.js +3 -3
- package/dist/core/human/answer.d.ts +6 -4
- package/dist/core/human/answer.js +4 -10
- package/dist/core/human/component-docs.d.ts +11 -7
- package/dist/core/human/component-docs.js +361 -88
- 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 +40 -5
- package/dist/core/human/page-catalog.js +102 -30
- 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 +23 -191
- package/dist/core/human/page-schema.js +60 -89
- package/dist/core/human/page-synth.d.ts +1 -1
- package/dist/core/human/page-synth.js +15 -18
- package/dist/core/human/page.d.ts +12 -27
- package/dist/core/human/page.js +142 -474
- 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 -4
- package/dist/core/human/scan.js +16 -15
- package/dist/core/human/summary.d.ts +1 -2
- package/dist/core/human/summary.js +2 -2
- package/dist/core/human/tickets.d.ts +20 -14
- package/dist/core/human/tickets.js +91 -31
- package/dist/core/human/types.d.ts +10 -2
- 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 +3 -3
- 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/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 +17 -6
- 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/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 +21 -2
- package/dist/core/user-settings.js +32 -11
- 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 +39 -115
- 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 +14 -16
- package/dist/daemon/human/finish.d.ts +12 -7
- package/dist/daemon/human/finish.js +101 -38
- package/dist/daemon/human/sweep.js +5 -1
- package/dist/daemon/profile-delete.d.ts +7 -0
- package/dist/daemon/profile-delete.js +306 -0
- package/dist/daemon/reconcilers/broker-supervision.js +3 -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/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.js +19 -5
- 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 +44 -2
- package/dist/types.js +7 -2
- package/package.json +4 -4
- package/runtime.lock.json +5 -33
- 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/commands/human/doc.d.ts +0 -2
- package/dist/commands/human/doc.js +0 -91
- package/dist/commands/human/html.js +0 -81
- package/dist/commands/human/inbox.d.ts +0 -2
- package/dist/core/human/__tests__/html-markdown.test.js +0 -52
- package/dist/core/human/__tests__/page-render.test.js +0 -66
- package/dist/core/human/html-markdown.d.ts +0 -3
- package/dist/core/human/html-markdown.js +0 -288
- package/dist/core/human/page-render.d.ts +0 -4
- package/dist/core/human/page-render.js +0 -105
- package/dist/pages/bundle.css +0 -1
- package/dist/pages/bundle.js +0 -1778
- package/dist/pages/comments.d.ts +0 -72
- package/dist/pages/comments.js +0 -176
- package/dist/pages/controls.d.ts +0 -98
- package/dist/pages/controls.js +0 -261
- package/dist/pages/elements/cards.d.ts +0 -28
- package/dist/pages/elements/cards.js +0 -915
- package/dist/pages/elements/chart.d.ts +0 -23
- package/dist/pages/elements/chart.js +0 -664
- package/dist/pages/elements/options.d.ts +0 -30
- package/dist/pages/elements/options.js +0 -742
- package/dist/pages/elements/pages.d.ts +0 -30
- package/dist/pages/elements/pages.js +0 -377
- package/dist/pages/elements/slot.d.ts +0 -26
- package/dist/pages/elements/slot.js +0 -112
- package/dist/pages/elements/table.d.ts +0 -36
- package/dist/pages/elements/table.js +0 -1043
- package/dist/pages/elements/text.d.ts +0 -31
- package/dist/pages/elements/text.js +0 -1175
- package/dist/pages/entry.d.ts +0 -8
- package/dist/pages/entry.js +0 -8
- package/dist/pages/host.d.ts +0 -134
- package/dist/pages/host.js +0 -165
- package/dist/pages/readonly.d.ts +0 -24
- package/dist/pages/readonly.js +0 -31
- package/dist/pages/register.d.ts +0 -2
- package/dist/pages/register.js +0 -3
- package/dist/pages/responses.d.ts +0 -37
- package/dist/pages/responses.js +0 -56
- 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__/html-markdown.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
- /package/dist/core/{human/__tests__/page-render.test.d.ts → __tests__/plugin-page-components.test.d.ts} +0 -0
|
@@ -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 });
|
|
@@ -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
|
});
|
|
@@ -8,12 +8,13 @@ import { tmpdir } from 'node:os';
|
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { createApiServer } from '../../daemon/api/server.js';
|
|
10
10
|
import { cancelHumanTicket, deliverTerminalResult, resolvePageTicket } from '../../daemon/human/finish.js';
|
|
11
|
-
import {
|
|
11
|
+
import { humanSend } from '../../commands/human/prompts.js';
|
|
12
12
|
import { getNode, listNodes, closeDb } from '../canvas/index.js';
|
|
13
13
|
import { readInboxSince } from '../feed/inbox.js';
|
|
14
14
|
import { resolveRef } from '../canvas/history.js';
|
|
15
15
|
import { apiSocketPath } from '../canvas/paths.js';
|
|
16
16
|
import { spawnNode } from '../runtime/nodes.js';
|
|
17
|
+
import { bindTicketReplyRoute } from '../human/convention.js';
|
|
17
18
|
import { ticketDir } from '../human/root.js';
|
|
18
19
|
import { readTicketResult, submitPage } from '../human/tickets.js';
|
|
19
20
|
let home;
|
|
@@ -39,16 +40,17 @@ async function withMutedStdout(fn) {
|
|
|
39
40
|
}
|
|
40
41
|
function bridgeUnder() {
|
|
41
42
|
const parent = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'asker' }).node_id;
|
|
42
|
-
const bridge = spawnNode({ kind: 'human', parent, cwd: home, name: 'human-
|
|
43
|
+
const bridge = spawnNode({ kind: 'human', parent, cwd: home, name: 'human-page', lifecycle: 'terminal' }).node_id;
|
|
43
44
|
return { parent, bridge };
|
|
44
45
|
}
|
|
45
46
|
function submitBridgePage(bridge) {
|
|
46
47
|
const dir = ticketDir(bridge);
|
|
47
48
|
const sourceDir = mkdtempSync(join(tmpdir(), 'crtr-deliver-page-'));
|
|
48
|
-
const sourceFile = join(sourceDir, 'page.
|
|
49
|
-
writeFileSync(sourceFile, '
|
|
49
|
+
const sourceFile = join(sourceDir, 'page.tsx');
|
|
50
|
+
writeFileSync(sourceFile, 'export default () => (\n <Page title="Ship it?" subtitle="Decide whether to ship now so the release can proceed.">\n <UserText id="question" initialText="" />\n </Page>\n);\n');
|
|
50
51
|
try {
|
|
51
|
-
|
|
52
|
+
bindTicketReplyRoute(dir, bridge);
|
|
53
|
+
submitPage({ dir, sourceFile, delivery: { placement: 'inline', inbox: true, reply: true } });
|
|
52
54
|
}
|
|
53
55
|
finally {
|
|
54
56
|
rmSync(sourceDir, { recursive: true, force: true });
|
|
@@ -87,40 +89,41 @@ after(() => {
|
|
|
87
89
|
delete process.env['CRTR_NODE_ID'];
|
|
88
90
|
});
|
|
89
91
|
describe('enqueue is producer-only', () => {
|
|
90
|
-
test('
|
|
92
|
+
test('send preserves title and subtitle from the authored page', async () => {
|
|
91
93
|
const parent = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'asker' }).node_id;
|
|
92
94
|
process.env['CRTR_NODE_ID'] = parent;
|
|
93
|
-
const page = join(home, 'release-decision.
|
|
94
|
-
writeFileSync(page, '
|
|
95
|
-
await assert.rejects(() =>
|
|
96
|
-
await assert.rejects(() =>
|
|
97
|
-
const result = (await
|
|
98
|
-
assert.equal(typeof result['
|
|
95
|
+
const page = join(home, 'release-decision.tsx');
|
|
96
|
+
writeFileSync(page, 'export default () => (\n <Page title="Release decision" subtitle="Decide whether to release now so the launch can proceed.">\n <p>This first line stays page content.</p>\n <UserText id="question" initialText="" />\n </Page>\n);\n');
|
|
97
|
+
await assert.rejects(() => humanSend.run({}), /a page source is required/);
|
|
98
|
+
await assert.rejects(() => humanSend.run({ page, document: 'export default () => <Page title="Dup" />;' }), /received both --page and a piped page document/);
|
|
99
|
+
const result = (await humanSend.run({ page, document: '' }));
|
|
100
|
+
assert.equal(typeof result['page_id'], 'string');
|
|
101
|
+
assert.equal(result['reply'], true);
|
|
99
102
|
assert.equal(typeof result['dir'], 'string');
|
|
100
103
|
for (const forbidden of ['pane_id', 'render_pane_id', 'pid', 'worker_pid'])
|
|
101
|
-
assert.ok(!(forbidden in result), `
|
|
104
|
+
assert.ok(!(forbidden in result), `send result must not expose ${forbidden}`);
|
|
102
105
|
const manifest = JSON.parse(readFileSync(join(result['dir'], 'page.json'), 'utf8'));
|
|
103
106
|
assert.equal(manifest.title, 'Release decision');
|
|
104
107
|
assert.equal(manifest.subtitle, 'Decide whether to release now so the launch can proceed.');
|
|
105
|
-
const document = readFileSync(join(result['dir'], 'page.
|
|
108
|
+
const document = readFileSync(join(result['dir'], 'page.tsx'), 'utf8');
|
|
106
109
|
assert.ok(document.includes('This first line stays page content.'));
|
|
107
110
|
assert.equal(manifest.slots.filter((slot) => slot.kind === 'text').length, 1);
|
|
108
111
|
});
|
|
109
|
-
test('
|
|
112
|
+
test('send materializes a piped page before validation and publication', async () => {
|
|
110
113
|
const parent = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'asker' }).node_id;
|
|
111
114
|
process.env['CRTR_NODE_ID'] = parent;
|
|
112
|
-
const document = '
|
|
113
|
-
const result = (await
|
|
115
|
+
const document = 'export default () => (\n <Page title="Piped question" subtitle="Answer the question to unblock this task.">\n <UserText id="question" initialText="" />\n </Page>\n);\n';
|
|
116
|
+
const result = (await humanSend.run({ document }));
|
|
114
117
|
const manifest = JSON.parse(readFileSync(join(result['dir'], 'page.json'), 'utf8'));
|
|
115
118
|
assert.equal(manifest.title, 'Piped question');
|
|
116
|
-
assert.equal(readFileSync(join(result['dir'], 'page.
|
|
119
|
+
assert.equal(readFileSync(join(result['dir'], 'page.tsx'), 'utf8'), document);
|
|
117
120
|
});
|
|
118
121
|
});
|
|
119
122
|
describe('terminal ticket delivery', () => {
|
|
120
123
|
test('resolvePageTicket pushFinals the readable answer once and wakes the subscribed parent', async () => {
|
|
121
124
|
const { parent, bridge } = bridgeUnder();
|
|
122
125
|
const dir = submitBridgePage(bridge);
|
|
123
|
-
const result = await resolvePageTicket(bridge, { question: { text: 'yes, ship', edited: true
|
|
126
|
+
const result = await resolvePageTicket(bridge, { question: { text: 'yes, ship', edited: true } });
|
|
124
127
|
assert.equal(result.kind, 'page');
|
|
125
128
|
closeDb();
|
|
126
129
|
assert.equal(getNode(bridge)?.status, 'done');
|
|
@@ -133,7 +136,7 @@ describe('terminal ticket delivery', () => {
|
|
|
133
136
|
test('a replay against a terminal bridge is a no-op', async () => {
|
|
134
137
|
const { parent, bridge } = bridgeUnder();
|
|
135
138
|
submitBridgePage(bridge);
|
|
136
|
-
await resolvePageTicket(bridge, { question: { text: 'yes', edited: true
|
|
139
|
+
await resolvePageTicket(bridge, { question: { text: 'yes', edited: true } });
|
|
137
140
|
assert.equal(await deliverTerminalResult(bridge), true);
|
|
138
141
|
closeDb();
|
|
139
142
|
const finals = readInboxSince(parent).filter((entry) => entry.from === bridge && entry.kind === 'final');
|
|
@@ -141,7 +144,7 @@ describe('terminal ticket delivery', () => {
|
|
|
141
144
|
});
|
|
142
145
|
test('cancelHumanTicket finishes the bridge and notifies non-actor subscribers', async () => {
|
|
143
146
|
const actor = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'actor' }).node_id;
|
|
144
|
-
const bridge = spawnNode({ kind: 'human', parent: actor, cwd: home, name: 'human-
|
|
147
|
+
const bridge = spawnNode({ kind: 'human', parent: actor, cwd: home, name: 'human-page', lifecycle: 'terminal' }).node_id;
|
|
145
148
|
const other = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'other' }).node_id;
|
|
146
149
|
const { subscribe } = await import('../canvas/canvas.js');
|
|
147
150
|
subscribe(other, bridge, true);
|
|
@@ -156,12 +159,14 @@ describe('terminal ticket delivery', () => {
|
|
|
156
159
|
assert.match(notes[0].label, /canceled/);
|
|
157
160
|
});
|
|
158
161
|
});
|
|
159
|
-
test('
|
|
160
|
-
const page = join(home, 'build-done.
|
|
161
|
-
writeFileSync(page, '
|
|
162
|
+
test('a display-only page publishes passively and creates no canvas node', async () => {
|
|
163
|
+
const page = join(home, 'build-done.tsx');
|
|
164
|
+
writeFileSync(page, 'export default () => (\n <Page title="Build done" subtitle="The build passed, so the change is ready to review.">\n <p>Build output is ready.</p>\n </Page>\n);\n');
|
|
162
165
|
const before = listNodes().length;
|
|
163
|
-
const result = (await
|
|
164
|
-
assert.equal(result['
|
|
166
|
+
const result = (await humanSend.run({ page }));
|
|
167
|
+
assert.equal(result['reply'], false);
|
|
168
|
+
assert.equal(result['inbox'], false);
|
|
169
|
+
assert.equal(typeof result['page_id'], 'string');
|
|
165
170
|
assert.equal(typeof result['dir'], 'string');
|
|
166
171
|
assert.ok(existsSync(join(result['dir'], 'page.json')));
|
|
167
172
|
assert.ok(readTicketResult(result['dir']) === null);
|