@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
|
@@ -5,12 +5,14 @@ import { defineLeaf } from '../../core/command.js';
|
|
|
5
5
|
import { notFound, usage, general, network } from '../../core/errors.js';
|
|
6
6
|
import { findMarketplaceByName, findPluginByName, listAllPlugins } from '../../core/resolver.js';
|
|
7
7
|
import { pluginsDir, ensureProjectScopeRoot, userScopeRoot, resolveScopeArg, projectScopeRoot, requireScopeRoot, } from '../../core/scope.js';
|
|
8
|
-
import { updateConfig, updateState, ensureScopeInitialized, invalidPluginKindsReasons } from '../../core/config.js';
|
|
8
|
+
import { updateConfig, updateState, ensureScopeInitialized, invalidPluginKindsReasons, invalidPluginBinReasons, invalidPluginRequiresReasons, normalizeRequires } from '../../core/config.js';
|
|
9
|
+
import { invalidPageComponentsReasons } from '../../core/human/page-catalog.js';
|
|
9
10
|
import { pathExists, ensureDir, removePath, nowIso, linkOrCopy, isSymlink, atomicWriteJson } from '../../core/fs-utils.js';
|
|
10
|
-
import { clone, pull, deriveNameFromUrl, currentSha, isGitRepo } from '../../core/git.js';
|
|
11
|
+
import { clone, pull, deriveNameFromUrl, currentSha, gitSync, isGitRepo } from '../../core/git.js';
|
|
11
12
|
import { readMarketplaceManifest, readPluginManifest } from '../../core/manifest.js';
|
|
12
13
|
import { validatePluginCommands, validateEffectiveCommandPlugins } from '../../core/command-plugins/discovery.js';
|
|
13
14
|
import { normalizeEndpoint, validateEndpoint, validateHttpPluginTransport } from '../../core/command-plugins/endpoint.js';
|
|
15
|
+
import { buildBrokerEnv, resolvePathExecutable } from '../../core/runtime/spawn-env.js';
|
|
14
16
|
import { validatePluginBundle } from '../../core/command-plugins/bundle.js';
|
|
15
17
|
import { fetchHttpPluginBundle } from '../../core/command-plugins/transport/http-fetch.js';
|
|
16
18
|
import { SUBTREE_NAMES, coreCommandPaths } from '../../build-root.js';
|
|
@@ -45,6 +47,15 @@ function validateHttpInstall(name, endpoint, authEnv) {
|
|
|
45
47
|
throw usage(`invalid HTTP plugin transport: ${transportValidation.errors.join('; ')}`);
|
|
46
48
|
return transportValidation.transport;
|
|
47
49
|
}
|
|
50
|
+
/** Fold structured validation issues into the thrown message so every surface
|
|
51
|
+
* (prose and --json) names the exact offender — the error payload renderer
|
|
52
|
+
* carries only message/received/next scalars, so an `issues` array left in
|
|
53
|
+
* details alone never reaches the caller. The first issue's own remedy becomes
|
|
54
|
+
* the `Next:` line. */
|
|
55
|
+
function invalidBundleError(summary, issues) {
|
|
56
|
+
const lines = issues.map((i) => `- ${i.code}${i.path !== undefined ? ` at ${i.path}` : ''}: ${i.message} (received ${JSON.stringify(i.received)}, expected ${i.expected})`);
|
|
57
|
+
return general(`${summary}:\n${lines.join('\n')}`, { issues, ...(issues.length > 0 ? { next: issues[0].next } : {}) });
|
|
58
|
+
}
|
|
48
59
|
/** Fail a SOURCE install (git/local/marketplace) loudly when the plugin's
|
|
49
60
|
* authored manifest declares an invalid `kinds` block — the same install-time
|
|
50
61
|
* strictness `validatePluginBundle` applies to an archive's bundle.json. The
|
|
@@ -55,7 +66,56 @@ function assertManifestKindsValid(manifest, where) {
|
|
|
55
66
|
return;
|
|
56
67
|
const reasons = invalidPluginKindsReasons(manifest.kinds);
|
|
57
68
|
if (reasons.length > 0) {
|
|
58
|
-
throw general(`plugin manifest at ${where} declares an invalid kinds block`, { issues: reasons });
|
|
69
|
+
throw general(`plugin manifest at ${where} declares an invalid kinds block:\n${reasons.map((r) => `- ${r}`).join('\n')}`, { issues: reasons });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Fail a SOURCE install (git/local/marketplace) loudly when the plugin's
|
|
73
|
+
* authored manifest declares an invalid `bin` block. The read side
|
|
74
|
+
* (`normalizeBin`) drops invalid entries silently, so without this the plugin
|
|
75
|
+
* would install cleanly and its bare command would simply never appear on any
|
|
76
|
+
* node's PATH. */
|
|
77
|
+
function assertManifestBinValid(manifest, where) {
|
|
78
|
+
if (manifest.bin === undefined)
|
|
79
|
+
return;
|
|
80
|
+
const reasons = invalidPluginBinReasons(manifest.bin);
|
|
81
|
+
if (reasons.length > 0) {
|
|
82
|
+
throw general(`plugin manifest at ${where} declares an invalid bin block:\n${reasons.map((r) => `- ${r}`).join('\n')}`, { issues: reasons });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Fail a source install or update only when `requires` itself is malformed.
|
|
86
|
+
* A well-formed requirement missing from PATH stays advisory and becomes an
|
|
87
|
+
* install warning after the plugin is registered. */
|
|
88
|
+
function assertManifestRequiresValid(manifest, where) {
|
|
89
|
+
if (manifest.requires === undefined)
|
|
90
|
+
return;
|
|
91
|
+
const reasons = invalidPluginRequiresReasons(manifest.requires);
|
|
92
|
+
if (reasons.length > 0) {
|
|
93
|
+
throw general(`plugin manifest at ${where} declares an invalid requires block:\n${reasons.map((r) => `- ${r}`).join('\n')}`, { issues: reasons });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Advisory diagnostics for the PATH an ordinary node launched here receives.
|
|
97
|
+
* `buildBrokerEnv` includes contributed bare binaries, so one plugin can
|
|
98
|
+
* satisfy another plugin's requirement without either being installed globally. */
|
|
99
|
+
function missingRequirementWarnings(manifest) {
|
|
100
|
+
if (manifest.requires === undefined)
|
|
101
|
+
return [];
|
|
102
|
+
const env = buildBrokerEnv({ argv: [], env: { CRTR_NODE_CWD: process.cwd(), CRTR_PROFILE_ID: process.env['CRTR_PROFILE_ID'] ?? '' } });
|
|
103
|
+
return Object.entries(normalizeRequires(manifest.requires))
|
|
104
|
+
.filter(([name]) => resolvePathExecutable(name, env, process.cwd()) === null)
|
|
105
|
+
.map(([name, hint]) => `Warning: plugin "${manifest.name}" requires "${name}" on PATH — ${hint}`);
|
|
106
|
+
}
|
|
107
|
+
/** Fail a SOURCE install (git/local/marketplace) loudly when the plugin's
|
|
108
|
+
* authored manifest declares an invalid `page_components` block — the same
|
|
109
|
+
* install-time strictness `validatePluginBundle` applies to an archive's
|
|
110
|
+
* bundle.json. `resolvePageComponents` throws on a malformed block, so an
|
|
111
|
+
* unchecked install would break every page-authoring surface at read time
|
|
112
|
+
* instead of at the install that introduced it. */
|
|
113
|
+
function assertManifestPageComponentsValid(manifest, where) {
|
|
114
|
+
if (manifest.page_components === undefined)
|
|
115
|
+
return;
|
|
116
|
+
const reasons = invalidPageComponentsReasons(manifest.page_components);
|
|
117
|
+
if (reasons.length > 0) {
|
|
118
|
+
throw general(`plugin manifest at ${where} declares an invalid page_components block:\n${reasons.map((r) => `- ${r}`).join('\n')}`, { issues: reasons });
|
|
59
119
|
}
|
|
60
120
|
}
|
|
61
121
|
function invocationTransport(bundle) {
|
|
@@ -83,7 +143,7 @@ async function validateStagedBundle(name, scope, root, manifest) {
|
|
|
83
143
|
const staged = { name, scope, root, manifest, enabled: true };
|
|
84
144
|
const validation = validatePluginCommands(staged, new Set(SUBTREE_NAMES), await coreCommandPaths());
|
|
85
145
|
if (validation.issues.length > 0) {
|
|
86
|
-
throw
|
|
146
|
+
throw invalidBundleError(`bundle staged package for "${name}" is invalid`, validation.issues);
|
|
87
147
|
}
|
|
88
148
|
}
|
|
89
149
|
function isSourceInstalled(plugin) {
|
|
@@ -120,7 +180,7 @@ async function replaceBundlePlugin(name, bundleSource, scope, options) {
|
|
|
120
180
|
}
|
|
121
181
|
const validated = await validatePluginBundle(fetched.raw, { reservedCoreNames: new Set(SUBTREE_NAMES), coreCommandPaths: await coreCommandPaths() });
|
|
122
182
|
if (validated.bundle === undefined)
|
|
123
|
-
throw
|
|
183
|
+
throw invalidBundleError(`HTTP plugin bundle for "${name}" is invalid`, validated.issues);
|
|
124
184
|
const version = createHash('sha256').update(fetched.raw).digest('hex').slice(0, 12);
|
|
125
185
|
const manifest = {
|
|
126
186
|
name,
|
|
@@ -133,6 +193,11 @@ async function replaceBundlePlugin(name, bundleSource, scope, options) {
|
|
|
133
193
|
// what registers them: readMergedLaunchConfig reads kinds from installed
|
|
134
194
|
// plugin MANIFESTS, never from bundle.json directly.
|
|
135
195
|
...(validated.bundle.kinds !== undefined ? { kinds: validated.bundle.kinds } : {}),
|
|
196
|
+
// Page-component contributions from the archive's bundle.json, likewise
|
|
197
|
+
// already validated. `resolvePageComponents` reads the MANIFEST, so this
|
|
198
|
+
// copy is what makes the components authorable — and dropping it on a
|
|
199
|
+
// later archive that no longer declares them is what retires them.
|
|
200
|
+
...(validated.bundle.page_components !== undefined ? { page_components: validated.bundle.page_components } : {}),
|
|
136
201
|
};
|
|
137
202
|
const tmpRoot = join(scopeRootPath, 'tmp');
|
|
138
203
|
const staging = join(tmpRoot, `${name}.${process.pid}`);
|
|
@@ -177,7 +242,7 @@ async function replaceBundlePlugin(name, bundleSource, scope, options) {
|
|
|
177
242
|
const commands = await commandReport(name, scope);
|
|
178
243
|
if (commands === undefined)
|
|
179
244
|
throw general(`bundle plugin "${name}" has no staged command report`);
|
|
180
|
-
return { name, scope, path: root, transport: 'http', version, docs: validated.bundle.memory.length, kinds: Object.keys(validated.bundle.kinds ?? {}).length, commands };
|
|
245
|
+
return { name, scope, path: root, transport: 'http', version, docs: validated.bundle.memory.length, kinds: Object.keys(validated.bundle.kinds ?? {}).length, pageComponents: (validated.bundle.page_components ?? []).length, commands };
|
|
181
246
|
}
|
|
182
247
|
async function installHttpPlugin(name, endpoint, authEnv, scope) {
|
|
183
248
|
const source = validateHttpInstall(name, endpoint, authEnv);
|
|
@@ -240,6 +305,15 @@ function refreshMarketplace(name, cache, opts) {
|
|
|
240
305
|
cache.set(name, refreshed);
|
|
241
306
|
return refreshed;
|
|
242
307
|
}
|
|
308
|
+
/** Restore a source checkout after an updated manifest fails its install-time
|
|
309
|
+
* gate. `--merge` returns it to the previous commit without discarding local
|
|
310
|
+
* changes unrelated to the pull. */
|
|
311
|
+
function restoreSourceCheckout(root, previousSha) {
|
|
312
|
+
const restored = gitSync(['reset', '--merge', previousSha], root);
|
|
313
|
+
if (restored.status !== 0) {
|
|
314
|
+
throw general(`updated plugin manifest was rejected, but crouter could not restore the previous checkout at ${root}: ${restored.stderr.trim()}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
243
317
|
function updatePluginVersion(scope, name, version) {
|
|
244
318
|
updateConfig(scope, (cfg) => {
|
|
245
319
|
const entry = cfg.plugins[name];
|
|
@@ -276,6 +350,9 @@ function installFromGit(source, ref, scope) {
|
|
|
276
350
|
}
|
|
277
351
|
try {
|
|
278
352
|
assertManifestKindsValid(manifest, `${source}/.crouter-plugin/plugin.json`);
|
|
353
|
+
assertManifestPageComponentsValid(manifest, `${source}/.crouter-plugin/plugin.json`);
|
|
354
|
+
assertManifestBinValid(manifest, `${source}/.crouter-plugin/plugin.json`);
|
|
355
|
+
assertManifestRequiresValid(manifest, `${source}/.crouter-plugin/plugin.json`);
|
|
279
356
|
}
|
|
280
357
|
catch (error) {
|
|
281
358
|
removePath(tempDir);
|
|
@@ -307,6 +384,9 @@ function installFromLocal(source, scope) {
|
|
|
307
384
|
throw notFound(`plugin manifest not found at ${sourcePath}/.crouter-plugin/plugin.json`);
|
|
308
385
|
}
|
|
309
386
|
assertManifestKindsValid(manifest, `${sourcePath}/.crouter-plugin/plugin.json`);
|
|
387
|
+
assertManifestPageComponentsValid(manifest, `${sourcePath}/.crouter-plugin/plugin.json`);
|
|
388
|
+
assertManifestBinValid(manifest, `${sourcePath}/.crouter-plugin/plugin.json`);
|
|
389
|
+
assertManifestRequiresValid(manifest, `${sourcePath}/.crouter-plugin/plugin.json`);
|
|
310
390
|
const scopeRootPath = scope === 'project' ? ensureProjectScopeRoot() : userScopeRoot();
|
|
311
391
|
ensureScopeInitialized(scope, scopeRootPath);
|
|
312
392
|
const destDir = join(scopeRootPath, 'plugins', manifest.name);
|
|
@@ -356,6 +436,9 @@ export function installFromMarketplace(ref, scope) {
|
|
|
356
436
|
}
|
|
357
437
|
try {
|
|
358
438
|
assertManifestKindsValid(pluginManifest, `${destPluginDir}/.crouter-plugin/plugin.json`);
|
|
439
|
+
assertManifestPageComponentsValid(pluginManifest, `${destPluginDir}/.crouter-plugin/plugin.json`);
|
|
440
|
+
assertManifestBinValid(pluginManifest, `${destPluginDir}/.crouter-plugin/plugin.json`);
|
|
441
|
+
assertManifestRequiresValid(pluginManifest, `${destPluginDir}/.crouter-plugin/plugin.json`);
|
|
359
442
|
}
|
|
360
443
|
catch (error) {
|
|
361
444
|
removePath(destPluginDir);
|
|
@@ -377,10 +460,11 @@ async function updateOnePlugin(plugin, marketplaceCache, opts) {
|
|
|
377
460
|
const sourceInstalled = isSourceInstalled(plugin);
|
|
378
461
|
if (!sourceInstalled && plugin.manifest.bundle !== undefined) {
|
|
379
462
|
const replaced = await replaceBundlePlugin(plugin.name, plugin.manifest.bundle, plugin.scope, { enable: plugin.enabled });
|
|
380
|
-
return { name: plugin.name, transport: 'http', updated: true, version: replaced.version, docs: replaced.docs, kinds: replaced.kinds, commands: replaced.commands };
|
|
463
|
+
return { name: plugin.name, transport: 'http', updated: true, version: replaced.version, docs: replaced.docs, kinds: replaced.kinds, pageComponents: replaced.pageComponents, commands: replaced.commands };
|
|
381
464
|
}
|
|
382
465
|
if (plugin.manifest.transport?.kind === 'http' && !sourceInstalled)
|
|
383
466
|
rejectLegacyHttpPlugin(plugin.name);
|
|
467
|
+
const previousSha = currentSha(plugin.root);
|
|
384
468
|
let contentUpdate = { updated: false, sha: currentSha(plugin.root) ?? '' };
|
|
385
469
|
let marketplaceVersion;
|
|
386
470
|
if (plugin.sourceMarketplace !== undefined) {
|
|
@@ -401,14 +485,44 @@ async function updateOnePlugin(plugin, marketplaceCache, opts) {
|
|
|
401
485
|
contentUpdate = pullGitCheckout(plugin.root, `plugin "${plugin.name}"`, opts);
|
|
402
486
|
}
|
|
403
487
|
const manifest = readPluginManifest(plugin.root);
|
|
404
|
-
|
|
488
|
+
try {
|
|
489
|
+
if (manifest === null) {
|
|
490
|
+
throw general(`plugin manifest not found after updating ${plugin.root}: ${plugin.root}/.crouter-plugin/plugin.json`);
|
|
491
|
+
}
|
|
492
|
+
assertManifestKindsValid(manifest, `${plugin.root}/.crouter-plugin/plugin.json`);
|
|
493
|
+
assertManifestPageComponentsValid(manifest, `${plugin.root}/.crouter-plugin/plugin.json`);
|
|
494
|
+
assertManifestBinValid(manifest, `${plugin.root}/.crouter-plugin/plugin.json`);
|
|
495
|
+
assertManifestRequiresValid(manifest, `${plugin.root}/.crouter-plugin/plugin.json`);
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
if (contentUpdate.updated && previousSha !== null && isGitRepo(plugin.root)) {
|
|
499
|
+
restoreSourceCheckout(plugin.root, previousSha);
|
|
500
|
+
// Marketplace plugins are symlinks into the shared marketplace checkout.
|
|
501
|
+
// Its cached manifest and pull state must move back with that checkout so
|
|
502
|
+
// later entries in this bulk update neither see the rejected revision nor
|
|
503
|
+
// record its versions as accepted.
|
|
504
|
+
if (plugin.sourceMarketplace !== undefined) {
|
|
505
|
+
const cached = marketplaceCache.get(plugin.sourceMarketplace);
|
|
506
|
+
if (cached !== undefined) {
|
|
507
|
+
marketplaceCache.set(plugin.sourceMarketplace, {
|
|
508
|
+
...cached,
|
|
509
|
+
pull: { updated: false, sha: currentSha(cached.marketplace.root) ?? '' },
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
throw error;
|
|
515
|
+
}
|
|
516
|
+
updatePluginVersion(plugin.scope, plugin.name, marketplaceVersion ?? manifest.version);
|
|
405
517
|
const commands = await commandReport(plugin.name, plugin.scope);
|
|
518
|
+
const warnings = missingRequirementWarnings(manifest);
|
|
406
519
|
return {
|
|
407
520
|
name: plugin.name,
|
|
408
521
|
...(plugin.manifest.transport !== undefined ? { transport: plugin.manifest.transport.kind } : {}),
|
|
409
522
|
updated: contentUpdate.updated,
|
|
410
523
|
sha: contentUpdate.sha,
|
|
411
524
|
...(commands !== undefined ? { commands } : {}),
|
|
525
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
412
526
|
};
|
|
413
527
|
}
|
|
414
528
|
export const pluginInstall = defineLeaf({
|
|
@@ -434,13 +548,16 @@ export const pluginInstall = defineLeaf({
|
|
|
434
548
|
{ name: 'version', type: 'string', required: false, constraint: 'First 12 hex characters of the fetched archive SHA-256 for an --endpoint install.' },
|
|
435
549
|
{ name: 'docs', type: 'integer', required: false, constraint: 'Memory document count written from an --endpoint archive.' },
|
|
436
550
|
{ name: 'kinds', type: 'integer', required: false, constraint: 'Kind-registry entry count declared by an --endpoint archive\u2019s bundle.json.' },
|
|
551
|
+
{ name: 'pageComponents', type: 'integer', required: false, constraint: 'Page-component registration count declared by an --endpoint archive\u2019s bundle.json.' },
|
|
437
552
|
{ name: 'commands', type: 'object', required: false, constraint: 'Present only when the plugin declares a command manifest. {mounts: string[] (accepted top-level command names now live for the next invocation), issues: object[] (typed validation issues that rejected a contribution — {code, path?, message, received, expected, next})}. Validated statically; commands are never executed.' },
|
|
553
|
+
{ name: 'warnings', type: 'string[]', required: false, constraint: 'Advisory missing PATH executable requirements declared by the installed plugin. The install still succeeds and leaves the plugin enabled.' },
|
|
438
554
|
],
|
|
439
555
|
outputKind: 'object',
|
|
440
556
|
effects: [
|
|
441
557
|
'A ref install clones, links, or copies the plugin into the scope plugins directory and registers it with enabled=true. Marketplace installs refresh the source marketplace before resolving the plugin entry.',
|
|
442
|
-
'An --endpoint install fetches and validates one authenticated uncompressed tar archive before writing. It replaces the complete plugin directory with synthesized provenance and invocation metadata, commands.json, memory docs,
|
|
558
|
+
'An --endpoint install fetches and validates one authenticated uncompressed tar archive before writing. It replaces the complete plugin directory with synthesized provenance and invocation metadata, commands.json, memory docs, any kind-registry entries the bundle.json declares (they join the launch registry below this scope\u2019s own config.json kinds), and any page components it declares (they join the active page-component catalog alongside the user config entries); a fetch or validation failure preserves the prior package.',
|
|
443
559
|
'If the plugin declares a command manifest, its commands go live on the next crtr invocation. Exec commands run trusted local code only when explicitly invoked; HTTP commands call their declared endpoint only when explicitly invoked.',
|
|
560
|
+
'A plugin manifest `requires` declaration is checked against the PATH a node bash receives after install. Missing executables emit a warning with the declared install hint but never fail the install, disable the plugin, or remove its bare-binary contributions.',
|
|
444
561
|
],
|
|
445
562
|
},
|
|
446
563
|
run: async (input) => {
|
|
@@ -471,10 +588,12 @@ export const pluginInstall = defineLeaf({
|
|
|
471
588
|
: installFromGit(classified.value, gitRef, scope);
|
|
472
589
|
const plugin = findPluginByName(installed.name, installed.scope);
|
|
473
590
|
const commands = await commandReport(installed.name, installed.scope);
|
|
591
|
+
const warnings = plugin === null ? [] : missingRequirementWarnings(plugin.manifest);
|
|
474
592
|
return {
|
|
475
593
|
...installed,
|
|
476
594
|
...(plugin?.manifest.transport !== undefined ? { transport: plugin.manifest.transport.kind } : {}),
|
|
477
595
|
...(commands !== undefined ? { commands } : {}),
|
|
596
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
478
597
|
};
|
|
479
598
|
},
|
|
480
599
|
});
|
|
@@ -586,7 +705,7 @@ export const pluginUpdate = defineLeaf({
|
|
|
586
705
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Narrows resolution.' },
|
|
587
706
|
],
|
|
588
707
|
output: [
|
|
589
|
-
{ name: 'updated', type: 'object[]', required: true, constraint: 'One entry per plugin processed: {name, transport?, updated, sha?, version?, docs?, commands?, error?}. Archive plugins report transport=http and replace their complete directory after an authenticated refetch; version is the archive content identifier, docs is the written memory-document count,
|
|
708
|
+
{ name: 'updated', type: 'object[]', required: true, constraint: 'One entry per plugin processed: {name, transport?, updated, sha?, version?, docs?, kinds?, pageComponents?, commands?, warnings?, error?}. Archive plugins report transport=http and replace their complete directory after an authenticated refetch; version is the archive content identifier, docs is the written memory-document count, kinds is the declared kind-registry entry count, and pageComponents is the declared page-component registration count. sha is present for git updates. A bulk update reports and skips a failed archive plugin. commands is present for validated command plugins: {mounts: string[], issues: object[]}. warnings lists advisory PATH requirements that remain absent after update.' },
|
|
590
709
|
],
|
|
591
710
|
outputKind: 'object',
|
|
592
711
|
effects: ['Archive plugins unconditionally re-fetch, validate, and replace their complete package; a named failure exits nonzero and preserves the prior package. Bulk update reports a failed archive plugin and continues. Source-installed plugins retain their source update, version, and last_updated behavior.'],
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { defineLeaf } from '../../core/command.js';
|
|
2
|
-
import {
|
|
2
|
+
import { cliClient, rethrowAsCliError } from '../api-client.js';
|
|
3
3
|
export const deleteLeaf = defineLeaf({
|
|
4
4
|
name: 'delete',
|
|
5
|
-
description: '
|
|
6
|
-
whenToUse: 'a profile is no longer needed
|
|
5
|
+
description: 'force-delete a profile and its owned runtime state',
|
|
6
|
+
whenToUse: 'a profile is no longer needed and you intend either to remove every node and schedule pinned to it, or to preserve that state while detaching the profile identity',
|
|
7
7
|
help: {
|
|
8
8
|
name: 'profile delete',
|
|
9
|
-
summary: '
|
|
9
|
+
summary: 'force-delete a profile and its owned state, or detach that state before removing the profile',
|
|
10
10
|
params: [
|
|
11
11
|
{
|
|
12
12
|
kind: 'positional',
|
|
@@ -14,26 +14,52 @@ export const deleteLeaf = defineLeaf({
|
|
|
14
14
|
required: true,
|
|
15
15
|
constraint: 'Exact profile id, or a unique manifest name.',
|
|
16
16
|
},
|
|
17
|
+
{
|
|
18
|
+
kind: 'flag',
|
|
19
|
+
name: 'force',
|
|
20
|
+
type: 'bool',
|
|
21
|
+
required: true,
|
|
22
|
+
constraint: 'Required confirmation for this irreversible operation.',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
kind: 'flag',
|
|
26
|
+
name: 'detach',
|
|
27
|
+
type: 'bool',
|
|
28
|
+
required: false,
|
|
29
|
+
constraint: 'Preserve nodes and inbox/review history; clear their profile identity and globalize matching crons before removing the profile.',
|
|
30
|
+
},
|
|
17
31
|
],
|
|
18
32
|
output: [
|
|
19
|
-
{ name: 'profile_id', type: 'string', required: true, constraint: 'The
|
|
20
|
-
{ name: 'name', type: 'string', required: true, constraint: 'The
|
|
21
|
-
{ name: '
|
|
22
|
-
{ name: '
|
|
33
|
+
{ name: 'profile_id', type: 'string', required: true, constraint: 'The removed profile’s stable id.' },
|
|
34
|
+
{ name: 'name', type: 'string', required: true, constraint: 'The removed profile’s manifest name.' },
|
|
35
|
+
{ name: 'mode', type: 'string', required: true, constraint: '`deleted` for the destructive cascade; `detached` when --detach preserved nodes and history.' },
|
|
36
|
+
{ name: 'nodes', type: 'number', required: true, constraint: 'Nodes hard-deleted or detached.' },
|
|
37
|
+
{ name: 'crons', type: 'number', required: true, constraint: 'Crons deleted or globalized.' },
|
|
38
|
+
{ name: 'tickets', type: 'number', required: true, constraint: 'Originating inbox ticket directories removed; zero in detach mode.' },
|
|
39
|
+
{ name: 'reviews', type: 'number', required: true, constraint: 'Originating daemon review records removed; zero in detach mode.' },
|
|
40
|
+
{ name: 'default_pins', type: 'number', required: true, constraint: 'Per-directory default profile pins cleared.' },
|
|
41
|
+
{ name: 'accepted', type: 'boolean', required: false, constraint: 'True when the invoking node belongs to the profile and teardown begins after this result settles.' },
|
|
42
|
+
{ name: 'grace_ms', type: 'number', required: false, constraint: 'Acknowledgement grace before an accepted self-deletion begins.' },
|
|
43
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next action.' },
|
|
23
44
|
],
|
|
24
45
|
outputKind: 'object',
|
|
25
46
|
effects: [
|
|
26
|
-
'
|
|
47
|
+
'Always clears every per-directory default pin for the profile and removes the profile directory last. Blocks before mutation while any matching node owns an open managed worktree.',
|
|
48
|
+
'Without --detach: tears down and hard-deletes every node whose profile_id matches, deletes matching or anchored crons and their in-flight runs, and removes originating inbox tickets plus review records. Cross-profile children are preserved.',
|
|
49
|
+
'With --detach: preserves nodes, conversations, topology, inbox tickets, and reviews; rewrites node profile_id to null, restarts active brokers without the profile, and rewrites matching crons to global scope.',
|
|
27
50
|
],
|
|
28
51
|
},
|
|
29
52
|
run: async (input) => {
|
|
30
|
-
const
|
|
31
|
-
|
|
53
|
+
const result = await cliClient().deleteProfile(input['profile'], {
|
|
54
|
+
force: input['force'],
|
|
55
|
+
detach: input['detach'],
|
|
56
|
+
caller_node_id: process.env['CRTR_NODE_ID'] ?? null,
|
|
57
|
+
}).catch(rethrowAsCliError);
|
|
32
58
|
return {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
...result,
|
|
60
|
+
follow_up: result.accepted === true
|
|
61
|
+
? 'The deletion was accepted; this node will stop after its result settles.'
|
|
62
|
+
: 'Run `crtr profile list` to see remaining profiles.',
|
|
37
63
|
};
|
|
38
64
|
},
|
|
39
65
|
});
|
package/dist/commands/profile.js
CHANGED
|
@@ -30,7 +30,7 @@ export function registerProfile() {
|
|
|
30
30
|
help: {
|
|
31
31
|
name: 'profile',
|
|
32
32
|
summary: 'create, inspect, and manage profiles \u2014 agent identities with their own project purview and memory store',
|
|
33
|
-
model: 'A profile owns a `home` — the first of its project dirs unless `project add --home` re-points it — and every node created under the profile is pinned there, so cwd selects the profile and the profile then supplies where its nodes run. Only `node new --cwd` (or inheriting a spawner already elsewhere, notably inside a managed worktree) overrides that; the root profile has no home, so nodes under it stay in the directory that created them. `new` creates one from a name (+ at most one initial `--project`, which becomes its home). `list` inventories every profile as JSONL. `show` prints one manifest in full. `rename` changes only the manifest name \u2014 the profile_id is stable across a rename. `delete` removes
|
|
33
|
+
model: 'A profile owns a `home` — the first of its project dirs unless `project add --home` re-points it — and every node created under the profile is pinned there, so cwd selects the profile and the profile then supplies where its nodes run. Only `node new --cwd` (or inheriting a spawner already elsewhere, notably inside a managed worktree) overrides that; the root profile has no home, so nodes under it stay in the directory that created them. `new` creates one from a name (+ at most one initial `--project`, which becomes its home). `list` inventories every profile as JSONL. `show` prints one manifest in full. `rename` changes only the manifest name \u2014 the profile_id is stable across a rename. `delete --force` removes the profile after cascading through its nodes, matching schedules, originating inbox/review state, and default pins; `--detach --force` instead preserves nodes and human history while clearing the profile identity and globalizing its schedules. Both modes block while a matching node owns an open managed worktree and never touch the project directories the profile pointed at. `project` widens/narrows a profile\u2019s project purview (the dirs it resolves memory + config from). `default` pins which profile a directory defaults to at startup so the chooser stops asking \u2014 directory-scoped, independent of purview. `env` manages a profile-scoped 0600 store of env vars injected directly into every broker launched under that profile \u2014 set on stdin, list names only (never values), or remove one; no `spawnEnv.allow` entry needed. Every manifest verb but `new`/`list` takes <profile> as an exact id or a unique manifest name; an ambiguous name fails listing every matching id.',
|
|
34
34
|
},
|
|
35
35
|
children: [newLeaf, listLeaf, showLeaf, renameLeaf, deleteLeaf, projectBranch, defaultBranch, envBranch],
|
|
36
36
|
});
|
package/dist/commands/push.js
CHANGED
|
@@ -52,7 +52,7 @@ function makeTierLeaf(tier) {
|
|
|
52
52
|
params: [
|
|
53
53
|
{ kind: 'stdin', name: 'body', required: true, constraint: "Report body (markdown). Pipe it from a single-quoted heredoc (`<<'EOF'`) to preserve literal bytes." },
|
|
54
54
|
...(tier === 'final'
|
|
55
|
-
? [{ kind: 'flag', name: 'force', type: 'bool', required: false, default: false, constraint: 'Override the guard that blocks `push final` on a
|
|
55
|
+
? [{ kind: 'flag', name: 'force', type: 'bool', required: false, default: false, constraint: 'Override the guard that blocks `push final` on a node the user is attending (a resident with no one to report up to). Only pass this after the user explicitly confirms they want this node finished.' }]
|
|
56
56
|
: []),
|
|
57
57
|
],
|
|
58
58
|
output: [
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// `crtr surface node focus` — bring one viewer forward.
|
|
2
2
|
import { defineLeaf } from '../../../core/command.js';
|
|
3
3
|
import { InputError } from '../../../core/io.js';
|
|
4
|
+
import { isFinishedWork, isLiveStatus } from '../../../core/canvas/node-order.js';
|
|
4
5
|
import { cliClient, rethrowAsCliError } from '../../api-client.js';
|
|
5
6
|
import { currentTmux, displayPopup, focusViewer, shellQuote } from './placement.js';
|
|
6
7
|
export const surfaceNodeFocusLeaf = defineLeaf({
|
|
7
8
|
name: 'focus',
|
|
8
9
|
description: 'bring a node into view without accidentally reviving finished work',
|
|
9
|
-
whenToUse: 'you want to bring a specific node into view — live
|
|
10
|
+
whenToUse: 'you want to bring a specific node into view — a live node, or a resident agent that merely stopped, opens (resuming its saved conversation in place) beside your current pane; FINISHED work — a terminal worker, or any node that pushed a final report — opens its saved transcript instead of being put back to work. Use `node lifecycle revive` to deliberately relaunch finished work anyway. Use `surface node cycle` to walk neighbors, and `node message send` to steer without leaving where you are',
|
|
10
11
|
help: {
|
|
11
12
|
name: 'surface node focus',
|
|
12
|
-
summary: 'bring a node into view — attach to a live broker, or open a saved transcript for
|
|
13
|
+
summary: 'bring a node into view — attach to a live broker, resume a resident agent that stopped, or open a saved transcript for finished work without reviving it',
|
|
13
14
|
params: [
|
|
14
15
|
{ kind: 'positional', name: 'node', required: true, constraint: 'Node id to focus.' },
|
|
15
16
|
{ kind: 'flag', name: 'new-pane', type: 'bool', required: false, constraint: 'Always open a NEW viewer SIDE-BY-SIDE with your current pane instead of moving/reusing the node\'s existing viewer. Two agents on screen at once (F4).' },
|
|
@@ -19,11 +20,11 @@ export const surfaceNodeFocusLeaf = defineLeaf({
|
|
|
19
20
|
output: [
|
|
20
21
|
{ name: 'focused', type: 'boolean', required: true, constraint: 'True when the node\'s viewer was brought into view.' },
|
|
21
22
|
{ name: 'session', type: 'string', required: false, constraint: 'The tmux session the viewer lives in.' },
|
|
22
|
-
{ name: 'revived', type: 'boolean', required: true, constraint: 'True when focusing a live node
|
|
23
|
+
{ name: 'revived', type: 'boolean', required: true, constraint: 'True when focusing had to relaunch the node\'s broker (a live node whose engine was gone, or a stopped resident agent resumed in place); false when its viewer was already attached, and false for a saved-transcript inspection.' },
|
|
23
24
|
{ name: 'in_place', type: 'boolean', required: true, constraint: 'True when an existing viewer was navigated to (no new pane); false when a fresh viewer pane was opened or moved beside you.' },
|
|
24
25
|
],
|
|
25
26
|
outputKind: 'object',
|
|
26
|
-
effects: ['For active/idle nodes: opens/moves the one surface-attach viewer beside the caller, or navigates to its existing viewer.', 'For
|
|
27
|
+
effects: ['For active/idle nodes: opens/moves the one surface-attach viewer beside the caller, or navigates to its existing viewer.', 'For a resident node that stopped without finalizing: resumes its saved conversation in place (status returns to active) and opens its viewer — the same path as a live node.', 'For finished work (a terminal node, or any node with a final report): opens a read-only saved transcript popup. It does not launch a broker or change node status.', 'With --new-pane/--in-place on a live node: controls whether the viewer splits beside or replaces the caller pane.'],
|
|
27
28
|
},
|
|
28
29
|
run: async (input) => {
|
|
29
30
|
const id = input['node'];
|
|
@@ -38,17 +39,31 @@ export const surfaceNodeFocusLeaf = defineLeaf({
|
|
|
38
39
|
throw new InputError({
|
|
39
40
|
error: 'not_focusable',
|
|
40
41
|
message: `node ${id} is a human-ask (kind:human), not a conversation — it has no pi session to focus.`,
|
|
41
|
-
next: `The pending question is queued for the
|
|
42
|
+
next: `The pending question is queued for the user; see it with \`crtr human list\`, or retract it with \`crtr human cancel ${id}\`.`,
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
:
|
|
45
|
+
// Every focus needs a pane to place the viewer or popup into. Resolve it ONCE
|
|
46
|
+
// here and fail loudly when there is none: both downstream paths would
|
|
47
|
+
// otherwise return `focused: false` with no explanation, which reads as the
|
|
48
|
+
// command silently doing nothing.
|
|
49
|
+
const pane = input['pane'] ?? process.env['TMUX_PANE'] ?? currentTmux()?.pane;
|
|
50
|
+
if (pane === undefined || pane === '') {
|
|
51
|
+
throw new InputError({
|
|
52
|
+
error: 'no_pane',
|
|
53
|
+
message: `no tmux pane to focus ${id} into — surfaces need a tmux server, and this call has neither --pane nor a caller pane.`,
|
|
54
|
+
next: `Run it from inside tmux, pass \`--pane <id>\`, or read the node without a surface: \`crtr node inspect transcript ${id}\`.`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// Opening history is not resuming work — but a non-live STATUS does not by
|
|
58
|
+
// itself mean the node finished anything. Finished work (a terminal worker, or
|
|
59
|
+
// any node that pushed a final report) is archive: show its persisted
|
|
60
|
+
// transcript, because reviving it would set completed work back in motion, and
|
|
61
|
+
// `node lifecycle revive` remains the explicit way to do that on purpose.
|
|
62
|
+
// A RESIDENT node with no final report never finished — its `done` is just a
|
|
63
|
+
// clean broker exit — so it falls through to the live path below, where
|
|
64
|
+
// ensureAttach's true resume replays the conversation exactly as it stood.
|
|
65
|
+
if (!isLiveStatus(node.status) && isFinishedWork({ lifecycle: node.lifecycle, finalized: node.final_report != null })) {
|
|
66
|
+
const focused = displayPopup(pane, node.cwd, `crtr node inspect transcript ${shellQuote(id)}`);
|
|
52
67
|
return { focused, session: null, revived: false, in_place: false };
|
|
53
68
|
}
|
|
54
69
|
// Local viewer tail: open/move the broker-backed viewer beside the caller.
|
|
@@ -57,7 +72,7 @@ export const surfaceNodeFocusLeaf = defineLeaf({
|
|
|
57
72
|
const res = await focusViewer(id, {
|
|
58
73
|
cwd: node.cwd,
|
|
59
74
|
name: node.name,
|
|
60
|
-
pane
|
|
75
|
+
pane,
|
|
61
76
|
newPane: input['newPane'] === true,
|
|
62
77
|
inPlace: input['inPlace'] === true,
|
|
63
78
|
});
|
|
@@ -69,10 +69,10 @@ const EDITOR_EXPR = '${EDITOR:-${VISUAL}}';
|
|
|
69
69
|
export const surfaceEditLeaf = defineLeaf({
|
|
70
70
|
name: 'edit',
|
|
71
71
|
description: "open a file in the user's $EDITOR in a new pane beside the pane that opened this command",
|
|
72
|
-
whenToUse: "you (an agent) want the
|
|
72
|
+
whenToUse: "you (an agent) want the user to SEE a file WHILE THEY'RE WATCHING YOU — open it in their $EDITOR in a new pane split beside this node's viewer pane, without them hunting for it. Works headless (the broker reaches the tmux server over its socket). Requires this node to be on screen (a viewer open) or a caller pane; a background node with no viewer gets a no_pane error — report the path instead and it opens beside you once watched",
|
|
73
73
|
help: {
|
|
74
74
|
name: 'surface edit',
|
|
75
|
-
summary: "Open <file> in a new pane running the user's $EDITOR (else $VISUAL), split beside THE PANE THAT OPENED THIS: an explicit --pane, the caller's own $TMUX_PANE, or this node's viewer pane (the pane where the
|
|
75
|
+
summary: "Open <file> in a new pane running the user's $EDITOR (else $VISUAL), split beside THE PANE THAT OPENED THIS: an explicit --pane, the caller's own $TMUX_PANE, or this node's viewer pane (the pane where the user is watching it). Works from a HEADLESS broker (the tmux server is reached over its socket, no $TMUX needed). A background node with no viewer on screen has no pane to sit beside -> no_pane error. The split is -d, so focus is unchanged",
|
|
76
76
|
params: [
|
|
77
77
|
{
|
|
78
78
|
kind: 'positional',
|
|
@@ -126,7 +126,7 @@ export const surfaceEditLeaf = defineLeaf({
|
|
|
126
126
|
throw new InputError({
|
|
127
127
|
error: 'no_pane',
|
|
128
128
|
message: 'no pane to open the editor beside — this node has no viewer on screen (it is running in the background).',
|
|
129
|
-
next: 'Open a viewer on this node (`crtr surface node focus <id>` / `crtr surface attach`) and retry, pass --pane <pane-id>, or run from inside a tmux pane. To just point the
|
|
129
|
+
next: 'Open a viewer on this node (`crtr surface node focus <id>` / `crtr surface attach`) and retry, pass --pane <pane-id>, or run from inside a tmux pane. To just point the user at a file, report its path instead.',
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
132
|
const command = `${EDITOR_EXPR} ${shellQuote(file)}`;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// `crtr surface inbox` — the tmux keybinding's target for the human inbox.
|
|
2
|
+
//
|
|
3
|
+
// A screen-facing viewer control, not a ticket verb: it delivers the private
|
|
4
|
+
// attach-control action to one live viewer and focuses it. The human reaches
|
|
5
|
+
// their own inbox through the inbox-toggle key (or `/inbox` in the viewer,
|
|
6
|
+
// which toggles in-process and never shells out here).
|
|
7
|
+
import { defineLeaf } from '../core/command.js';
|
|
8
|
+
import { currentTmux, displayClientMessage, liveViewerPanes, paneLocation, selectPane, selectWindow, sendAttachControl, switchClient, } from '../core/runtime/placement-tmux.js';
|
|
3
9
|
const NO_VIEWER_MESSAGE = 'No crouter viewer is running — attach to a node first (crtr surface attach to <node>).';
|
|
4
10
|
function tmuxSocketFromEnvironment() {
|
|
5
11
|
const socket = process.env['TMUX']?.split(',')[0]?.trim();
|
|
@@ -40,12 +46,13 @@ function focusViewer(viewer, targetPane, clientSession) {
|
|
|
40
46
|
selectWindow(location.session, location.window);
|
|
41
47
|
selectPane(viewer.pane);
|
|
42
48
|
}
|
|
43
|
-
const
|
|
44
|
-
name: '
|
|
45
|
-
description: '
|
|
46
|
-
whenToUse: '
|
|
49
|
+
export const surfaceInboxLeaf = defineLeaf({
|
|
50
|
+
name: 'inbox',
|
|
51
|
+
description: 'human inbox toggle dispatcher (bound to the inbox-toggle key; not run by hand)',
|
|
52
|
+
whenToUse: 'never directly — the inbox-toggle keybinding routes the user’s own request through it. An agent that wants a decision uses crtr human send; how the user reaches their inbox is theirs.',
|
|
53
|
+
tier: 'hidden',
|
|
47
54
|
help: {
|
|
48
|
-
name: '
|
|
55
|
+
name: 'surface inbox',
|
|
49
56
|
summary: 'deliver the human inbox toggle to a live viewer',
|
|
50
57
|
guide: 'Selection prefers the target pane, then the resolved client session by newest viewer stamp, then newest globally; ties use pane id. The viewer decides whether the delivered toggle opens or closes the surface.',
|
|
51
58
|
params: [
|
|
@@ -88,14 +95,3 @@ const humanInboxToggle = defineLeaf({
|
|
|
88
95
|
return quiet ? undefined : outcome;
|
|
89
96
|
},
|
|
90
97
|
});
|
|
91
|
-
export const humanInbox = defineBranch({
|
|
92
|
-
name: 'inbox',
|
|
93
|
-
description: 'deliver the human inbox toggle to a live viewer',
|
|
94
|
-
whenToUse: 'answering, reviewing, or managing pending human tickets from a running crouter viewer.',
|
|
95
|
-
help: {
|
|
96
|
-
name: 'human inbox',
|
|
97
|
-
summary: 'deliver the human inbox toggle to a live viewer',
|
|
98
|
-
model: 'The toggle leaf sends a private viewer control action; it never hosts the inbox itself.',
|
|
99
|
-
},
|
|
100
|
-
children: [humanInboxToggle],
|
|
101
|
-
});
|
package/dist/commands/surface.js
CHANGED
|
@@ -8,6 +8,7 @@ import { surfaceEditLeaf } from './surface-edit.js';
|
|
|
8
8
|
import { registerSurfaceNode } from './surface-node.js';
|
|
9
9
|
import { surfaceInspectLeaf } from './surface-inspect.js';
|
|
10
10
|
import { surfaceReloadLeaf } from './surface-reload.js';
|
|
11
|
+
import { surfaceInboxLeaf } from './surface-inbox.js';
|
|
11
12
|
export function registerSurface() {
|
|
12
13
|
const attach = registerAttach('surface', 'important');
|
|
13
14
|
return defineBranch({
|
|
@@ -22,6 +23,6 @@ export function registerSurface() {
|
|
|
22
23
|
summary: 'open and manage screen-facing surfaces',
|
|
23
24
|
model: 'Choose `node` for navigation among node viewers. The other commands open a broker attachment, node Inspector, subtree spread, reload, or adjacent editor pane.',
|
|
24
25
|
},
|
|
25
|
-
children: [registerSurfaceNode(), attach, surfaceInspectLeaf, surfaceReloadLeaf, tmuxSpreadLeaf, surfaceEditLeaf],
|
|
26
|
+
children: [registerSurfaceNode(), attach, surfaceInspectLeaf, surfaceReloadLeaf, tmuxSpreadLeaf, surfaceEditLeaf, surfaceInboxLeaf],
|
|
26
27
|
});
|
|
27
28
|
}
|