@north-light/crouter 0.3.198 → 0.3.200
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__/helpers/harness.js +17 -55
- 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/broker-crash-teardown.test.js +4 -5
- 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 +6 -5
- 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()
|
|
@@ -28,7 +28,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
28
28
|
import { createRequire } from 'node:module';
|
|
29
29
|
import { createNode, getNode, recordPid, subscribersOf, subscriptionsOf, } from '../../canvas/canvas.js';
|
|
30
30
|
import { closeDb } from '../../canvas/db.js';
|
|
31
|
-
import {
|
|
31
|
+
import { spawnChild } from '../../runtime/spawn.js';
|
|
32
32
|
import { superviseTick } from '../../../daemon/crtrd.js';
|
|
33
33
|
import { DaemonFleet } from '../../../daemon/fleet.js';
|
|
34
34
|
import { apiSocketPath } from '../../canvas/paths.js';
|
|
@@ -150,11 +150,11 @@ export async function createHarness(opts = {}) {
|
|
|
150
150
|
const tmpHome = mkdtempSync(join(tmpdir(), 'crtr-harness-HOME-'));
|
|
151
151
|
const session = `${opts.sessionPrefix ?? 'crtr-harness'}-${process.pid}-${Date.now().toString(36)}`;
|
|
152
152
|
// The harness reads/writes the isolated canvas in-process. CRTR_PI_BINARY in
|
|
153
|
-
// OUR env makes in-process
|
|
153
|
+
// OUR env makes in-process launches (spawnChild and superviseTick → reviveNode)
|
|
154
154
|
// bake the fake-pi into the command string.
|
|
155
155
|
process.env['CRTR_HOME'] = home;
|
|
156
156
|
// HOME must be isolated in THIS process too, not just in the subprocess envs
|
|
157
|
-
// below: an in-process launch (spawnHeadlessChild's
|
|
157
|
+
// below: an in-process launch (spawnHeadlessChild's spawnChild, superviseTick
|
|
158
158
|
// pass-2 wakes) builds its broker env from process.env, so a real HOME here
|
|
159
159
|
// makes every broker read the developer's OWN ~/.pi/agent — auth.json and the
|
|
160
160
|
// managed subscription pools — instead of agentHome, and provider tests then
|
|
@@ -202,7 +202,7 @@ export async function createHarness(opts = {}) {
|
|
|
202
202
|
resetEventSourceForTests();
|
|
203
203
|
bindDaemonEventSource();
|
|
204
204
|
// The harness also stands in for the daemon's FLEET: every in-harness broker
|
|
205
|
-
// launch (reviveNode
|
|
205
|
+
// launch (spawnChild, reviveNode, superviseTick pass-2 wakes) registers its
|
|
206
206
|
// ChildProcess handle here, and real child exits enqueue exit-policy jobs on
|
|
207
207
|
// this serial work lane — the same single-lane discipline crtrd runs, but
|
|
208
208
|
// drainable to a deterministic quiescent point. Reset first for the same
|
|
@@ -367,17 +367,9 @@ export async function createHarness(opts = {}) {
|
|
|
367
367
|
}
|
|
368
368
|
function nodeDirs() {
|
|
369
369
|
try {
|
|
370
|
-
// Node ids
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
// opaque and its segment count has already changed once — mirror
|
|
374
|
-
// profiles/manifest.ts's ID_SHAPE rather than hard-coding a segment
|
|
375
|
-
// count). Scope the scan to "one or more lowercase-alnum segments ending
|
|
376
|
-
// in an 8-hex random suffix" so a stray non-node sibling under nodes/ is
|
|
377
|
-
// never miscounted as a new node dir — e.g. a `reports/` dir created for
|
|
378
|
-
// an empty node id (the rare CI flake on compiled-seam run 27459304455 that
|
|
379
|
-
// broke the spawn counters with `expected exactly 1 new node dir, got
|
|
380
|
-
// [<id>, reports]`), or literal `reports` (no hyphen, no hex suffix).
|
|
370
|
+
// Node ids end in an 8-hex random suffix and may have any number of
|
|
371
|
+
// lowercase-alphanumeric prefix segments. Only return those node dirs so
|
|
372
|
+
// fixture cleanup never treats a non-node sibling as a canvas row.
|
|
381
373
|
return readdirSync(join(home, 'nodes')).filter((d) => /^[a-z0-9]+(?:-[a-z0-9]+)*-[0-9a-f]{8}$/.test(d));
|
|
382
374
|
}
|
|
383
375
|
catch {
|
|
@@ -507,46 +499,16 @@ export async function createHarness(opts = {}) {
|
|
|
507
499
|
return id;
|
|
508
500
|
},
|
|
509
501
|
async spawnHeadlessChild(parentId, task, o = {}) {
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
args.push('--kind', o.kind);
|
|
521
|
-
if (o.mode)
|
|
522
|
-
args.push('--mode', o.mode);
|
|
523
|
-
if (o.model)
|
|
524
|
-
args.push('--model', o.model);
|
|
525
|
-
const res = cli(parentId, args);
|
|
526
|
-
if (res.code !== 0) {
|
|
527
|
-
throw new Error(`spawnHeadlessChild(${parentId}) failed (code ${res.code})\n--stdout--\n${res.stdout}\n--stderr--\n${res.stderr}`);
|
|
528
|
-
}
|
|
529
|
-
const added = nodeDirs().filter((d) => !before.has(d));
|
|
530
|
-
if (added.length !== 1) {
|
|
531
|
-
throw new Error(`spawnHeadlessChild: expected exactly 1 new node dir, got [${added.join(', ')}]`);
|
|
532
|
-
}
|
|
533
|
-
const childId = added[0];
|
|
534
|
-
closeDb();
|
|
535
|
-
const fixtureRow = getNode(childId);
|
|
536
|
-
const fixturePid = fixtureRow?.pi_pid ?? null;
|
|
537
|
-
const fixtureIdentity = fixtureRow?.pi_pid_identity ?? null;
|
|
538
|
-
const priorBoots = harness.bootCount(childId);
|
|
539
|
-
if (fixturePid !== null && fixtureIdentity !== null && recordedPidLiveness(fixturePid, fixtureIdentity) === 'alive') {
|
|
540
|
-
process.kill(fixturePid, 'SIGTERM');
|
|
541
|
-
await waitFor(() => recordedPidLiveness(fixturePid, fixtureIdentity) !== 'alive', {
|
|
542
|
-
timeoutMs: 10_000,
|
|
543
|
-
intervalMs: 25,
|
|
544
|
-
label: `API-fixture broker exit for ${childId}`,
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
reviveNode(childId, { resume: false });
|
|
548
|
-
await harness.awaitBoot(childId, { minCount: priorBoots + 1 });
|
|
549
|
-
return childId;
|
|
502
|
+
const result = await spawnChild({
|
|
503
|
+
kind: o.kind ?? 'general',
|
|
504
|
+
cwd: CROUTER,
|
|
505
|
+
prompt: task,
|
|
506
|
+
parent: parentId,
|
|
507
|
+
...(o.mode !== undefined ? { mode: o.mode } : {}),
|
|
508
|
+
...(o.model !== undefined ? { model: o.model } : {}),
|
|
509
|
+
});
|
|
510
|
+
await harness.awaitBoot(result.node.node_id);
|
|
511
|
+
return result.node.node_id;
|
|
550
512
|
},
|
|
551
513
|
cli,
|
|
552
514
|
async stop(nodeId, reason = 'stop', errorMessage) {
|
|
@@ -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
|
});
|