@north-light/crouter 0.3.263 → 0.3.270
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 +5 -1
- package/dist/api/client.js +6 -0
- package/dist/api/dto/broker.d.ts +11 -2
- package/dist/api/dto/broker.js +2 -2
- package/dist/api/dto/canvas.d.ts +53 -2
- package/dist/api/dto/nodes.d.ts +10 -2
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +50 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +46 -6
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +6 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +7 -2
- package/dist/clients/attach/session/whip-achievements.d.ts +4 -0
- package/dist/clients/attach/session/whip-achievements.js +145 -0
- package/dist/clients/attach/slash/dispatch.js +5 -3
- package/dist/clients/attach/viewer.js +607 -606
- package/dist/commands/canvas-history/grep.js +3 -22
- package/dist/commands/canvas-history/read.js +5 -3
- package/dist/commands/canvas-history/search.js +3 -22
- package/dist/commands/canvas-history/shared.d.ts +8 -0
- package/dist/commands/canvas-history/shared.js +64 -1
- package/dist/commands/canvas-history/stats.d.ts +1 -0
- package/dist/commands/canvas-history/stats.js +68 -0
- package/dist/commands/canvas-history.js +4 -3
- package/dist/commands/node/create.js +2 -2
- package/dist/commands/pkg/plugin-inspect.js +2 -2
- package/dist/commands/pkg/plugin-manage.js +3 -3
- package/dist/commands/sys/config.js +19 -2
- package/dist/commands/sys/doctor.js +4 -3
- package/dist/commands/sys/panels/models-panel.js +3 -1
- package/dist/core/__tests__/human-deliver.test.js +1 -1
- package/dist/core/__tests__/integration/lifecycle-hooks.test.d.ts +1 -0
- package/dist/core/__tests__/integration/lifecycle-hooks.test.js +182 -0
- package/dist/core/__tests__/integration/revive.test.js +19 -17
- package/dist/core/__tests__/model-pin-durability.test.js +25 -1
- package/dist/core/__tests__/push-final-guard.test.js +1 -1
- package/dist/core/__tests__/relaunch-root.test.js +17 -0
- package/dist/core/__tests__/revive-capacity.test.js +20 -20
- package/dist/core/__tests__/revive-parked-fresh.test.js +14 -14
- package/dist/core/__tests__/seam/broker-cap-freeze.test.js +2 -2
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +19 -4
- package/dist/core/__tests__/seam/dormancy-release.test.js +7 -7
- package/dist/core/__tests__/seam/held-deferred-human-prompt.test.js +1 -1
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +19 -2
- package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +2 -2
- package/dist/core/broker-client/__tests__/transport-relay.test.js +3 -1
- package/dist/core/broker-client/transport-relay.js +5 -1
- package/dist/core/canvas/__tests__/history-transcript.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/history-transcript.test.js +102 -0
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +13 -10
- package/dist/core/canvas/browse/model.js +16 -1
- package/dist/core/canvas/browse/render.d.ts +23 -3
- package/dist/core/canvas/browse/render.js +429 -64
- package/dist/core/canvas/canvas.js +12 -8
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/history.d.ts +36 -1
- package/dist/core/canvas/history.js +328 -7
- package/dist/core/canvas/labels.d.ts +2 -2
- package/dist/core/canvas/migrations.js +32 -0
- package/dist/core/canvas/node-recap.d.ts +23 -0
- package/dist/core/canvas/node-recap.js +67 -0
- package/dist/core/canvas/render-source.d.ts +33 -1
- package/dist/core/canvas/render-source.js +80 -22
- package/dist/core/canvas/types.d.ts +9 -0
- package/dist/core/command-hooks/discovery.d.ts +27 -1
- package/dist/core/command-hooks/discovery.js +52 -0
- package/dist/core/command-hooks/index.d.ts +4 -3
- package/dist/core/command-hooks/index.js +2 -1
- package/dist/core/command-hooks/lifecycle-catalog.d.ts +3 -0
- package/dist/core/command-hooks/lifecycle-catalog.js +4 -0
- package/dist/core/command-hooks/report.d.ts +11 -2
- package/dist/core/command-hooks/report.js +10 -1
- package/dist/core/command-hooks/schema.d.ts +16 -1
- package/dist/core/command-hooks/schema.js +128 -40
- package/dist/core/command-hooks/transport/exec-lifecycle.d.ts +12 -0
- package/dist/core/command-hooks/transport/exec-lifecycle.js +152 -0
- package/dist/core/config.js +13 -5
- package/dist/core/human/feedback-companion.js +1 -1
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +2 -1
- package/dist/core/model-routes.js +1 -1
- package/dist/core/review/realize.js +1 -1
- package/dist/core/runtime/broker/auth-reload.js +4 -1
- package/dist/core/runtime/broker/engine-drive.d.ts +14 -3
- package/dist/core/runtime/broker/engine-drive.js +29 -1
- package/dist/core/runtime/broker/fault-retry.js +7 -2
- package/dist/core/runtime/broker/frame-client.js +3 -0
- package/dist/core/runtime/broker/frame-dispatch.js +7 -0
- package/dist/core/runtime/broker/inbox.d.ts +12 -2
- package/dist/core/runtime/broker/inbox.js +68 -38
- package/dist/core/runtime/broker/rebind.js +7 -0
- package/dist/core/runtime/broker-protocol.d.ts +16 -1
- package/dist/core/runtime/broker.d.ts +1 -1
- package/dist/core/runtime/broker.js +27 -3
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/interactive-deliver.d.ts +8 -2
- package/dist/core/runtime/interactive-deliver.js +12 -3
- package/dist/core/runtime/launch-target.js +1 -1
- package/dist/core/runtime/launch.d.ts +12 -6
- package/dist/core/runtime/launch.js +19 -17
- package/dist/core/runtime/model-swap.d.ts +5 -4
- package/dist/core/runtime/model-swap.js +7 -5
- package/dist/core/runtime/node-read.d.ts +2 -0
- package/dist/core/runtime/node-read.js +18 -9
- package/dist/core/runtime/promote.js +2 -1
- package/dist/core/runtime/reset.js +4 -2
- package/dist/core/runtime/revive-all.d.ts +1 -1
- package/dist/core/runtime/revive-all.js +2 -2
- package/dist/core/runtime/revive.d.ts +2 -2
- package/dist/core/runtime/revive.js +71 -13
- package/dist/core/runtime/session-visibility.d.ts +10 -14
- package/dist/core/runtime/session-visibility.js +43 -30
- package/dist/core/runtime/spawn.d.ts +3 -3
- package/dist/core/runtime/spawn.js +20 -3
- package/dist/core/runtime/stamp/channel.d.ts +22 -0
- package/dist/core/runtime/stamp/channel.js +83 -0
- package/dist/core/runtime/stamp/protocol.d.ts +90 -0
- package/dist/core/runtime/stamp/protocol.js +113 -0
- package/dist/core/runtime/tmux-driver.d.ts +11 -1
- package/dist/core/runtime/tmux-driver.js +27 -11
- package/dist/core/runtime/turn-visibility.d.ts +10 -0
- package/dist/core/runtime/turn-visibility.js +31 -0
- package/dist/core/self-update.js +2 -5
- package/dist/core/tui/draw.d.ts +1 -0
- package/dist/core/tui/draw.js +5 -1
- package/dist/core/tui/markdown.d.ts +17 -0
- package/dist/core/tui/markdown.js +407 -0
- package/dist/core/user-settings.d.ts +4 -0
- package/dist/core/user-settings.js +1 -0
- package/dist/daemon/api/__tests__/node-model-validation.test.js +21 -0
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +2 -2
- package/dist/daemon/api/bridge.d.ts +3 -2
- package/dist/daemon/api/bridge.js +34 -9
- package/dist/daemon/api/handlers/attach.js +4 -4
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/canvas.js +62 -16
- package/dist/daemon/api/handlers/messages.js +14 -3
- package/dist/daemon/api/handlers/nodes.js +17 -7
- package/dist/daemon/api/map.js +2 -0
- package/dist/daemon/cron/sinks.js +3 -3
- package/dist/daemon/fleet.js +1 -1
- package/dist/daemon/messaging/node-message.js +16 -2
- package/dist/daemon/profile-delete.js +1 -1
- package/dist/daemon/reconcilers/broker-supervision.js +26 -9
- package/dist/daemon/reconcilers/live-obligation.d.ts +8 -1
- package/dist/daemon/reconcilers/live-obligation.js +17 -0
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +6 -6
- package/dist/hook-authoring.d.ts +31 -1
- package/dist/hook-authoring.js +42 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +27 -2
- package/dist/pi-extensions/canvas-recap.js +4 -0
- package/dist/pi-extensions/canvas-stamp.d.ts +14 -0
- package/dist/pi-extensions/canvas-stamp.js +77 -0
- package/dist/pi-extensions/canvas-stophook.js +3 -0
- package/dist/shared/__tests__/env-boundary.test.js +4 -2
- package/dist/shared/crtr-version.d.ts +2 -0
- package/dist/shared/crtr-version.js +18 -0
- package/dist/shared/env.d.ts +10 -0
- package/dist/shared/env.js +19 -4
- package/dist/shared/generated-context.d.ts +1 -2
- package/dist/shared/generated-context.js +3 -4
- package/dist/types.d.ts +10 -0
- package/dist/types.js +4 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { defineLeaf } from '../../core/command.js';
|
|
2
2
|
import { usage } from '../../core/errors.js';
|
|
3
3
|
import { cliClient, rethrowAsCliError } from '../api-client.js';
|
|
4
|
-
import { filterParams, parseFilters } from './shared.js';
|
|
4
|
+
import { applyFilters, filterParams, parseFilters } from './shared.js';
|
|
5
5
|
export const grepLeaf = defineLeaf({
|
|
6
6
|
name: 'grep',
|
|
7
7
|
description: 'exact regex line search across the cwd\'s node history',
|
|
8
8
|
whenToUse: 'you know the exact string or pattern you\'re looking for — an error message, a literal identifier, a code fragment — and want every matching body LINE across every node that ran in this cwd, not a relevance ranking. The pattern is required (an ECMAScript regex, case-insensitive). Narrow with --type/--kind/--status/--since, scope elsewhere with --cwd/--all-cwds/--under/--node. Want ranked/browse search instead? Use `canvas history search` — a distinct leaf, not a flag here. Use `canvas history read <ref>` to read a hit\'s full body, `node inspect artifacts <node-id>` to list one node\'s artifacts, and `node lifecycle revive <id>` to reopen a node you found.',
|
|
9
9
|
help: {
|
|
10
10
|
name: 'canvas history grep',
|
|
11
|
-
summary: 'required-pattern regex line search over the per-cwd episodic record (reports + context docs + meta)',
|
|
11
|
+
summary: 'required-pattern regex line search over the per-cwd episodic record (reports + context docs + meta; transcripts and inbox opt-in)',
|
|
12
12
|
params: [
|
|
13
13
|
{ kind: 'positional', name: 'pattern', required: true, constraint: 'An ECMAScript regex, matched case-insensitively against each body line. One hit per matching line.' },
|
|
14
14
|
...filterParams,
|
|
@@ -43,26 +43,7 @@ export const grepLeaf = defineLeaf({
|
|
|
43
43
|
// closures cannot cross the wire.
|
|
44
44
|
const f = parseFilters(input);
|
|
45
45
|
const q = { pattern, limit };
|
|
46
|
-
|
|
47
|
-
q.cwd = f.cwd;
|
|
48
|
-
if (f.allCwds)
|
|
49
|
-
q.all_cwds = true;
|
|
50
|
-
if (f.under !== undefined)
|
|
51
|
-
q.under = f.under;
|
|
52
|
-
if (f.nodes !== undefined)
|
|
53
|
-
q.nodes = f.nodes;
|
|
54
|
-
if (f.types !== undefined)
|
|
55
|
-
q.types = f.types;
|
|
56
|
-
if (f.reportKind !== undefined)
|
|
57
|
-
q.report_kind = f.reportKind;
|
|
58
|
-
if (f.kinds !== undefined)
|
|
59
|
-
q.kinds = f.kinds;
|
|
60
|
-
if (f.statuses !== undefined)
|
|
61
|
-
q.statuses = f.statuses;
|
|
62
|
-
if (f.sinceMs !== undefined)
|
|
63
|
-
q.since_ms = f.sinceMs;
|
|
64
|
-
if (f.untilMs !== undefined)
|
|
65
|
-
q.until_ms = f.untilMs;
|
|
46
|
+
applyFilters(q, f);
|
|
66
47
|
if (cursor !== undefined)
|
|
67
48
|
q.cursor = cursor;
|
|
68
49
|
try {
|
|
@@ -9,13 +9,14 @@ export const readLeaf = defineLeaf({
|
|
|
9
9
|
name: 'canvas history read',
|
|
10
10
|
summary: 'resolve a <node-id>:<relpath> ref to its full artifact body',
|
|
11
11
|
params: [
|
|
12
|
-
{ kind: 'positional', name: 'ref', required: true, constraint: 'The <node-id>:<relpath> handle from `canvas history search` (e.g. mq186ky0-c754531c:reports/20260607T075536-final.md, or <node-id>:meta for a node\'s identity).' },
|
|
12
|
+
{ kind: 'positional', name: 'ref', required: true, constraint: 'The <node-id>:<relpath> handle from `canvas history search` (e.g. mq186ky0-c754531c:reports/20260607T075536-final.md, or <node-id>:meta for a node\'s identity). <node-id>:session addresses a node\'s whole conversation; <node-id>:session/<index> one message in it.' },
|
|
13
13
|
{ kind: 'flag', name: 'frontmatter', type: 'bool', required: false, constraint: 'Include the artifact\'s YAML frontmatter. Stripped by default.' },
|
|
14
|
+
{ kind: 'flag', name: 'format', type: 'enum', choices: ['rendered', 'raw'], required: false, default: 'rendered', constraint: 'raw returns the verbatim bytes behind the ref — the session .jsonl for a bare session ref, the single persisted jsonl entry for one message, the unparsed file for a report or doc. Use it when the rendered view may be hiding what you are chasing.' },
|
|
14
15
|
],
|
|
15
16
|
output: [
|
|
16
17
|
{ name: 'ref', type: 'string', required: true, constraint: 'Echo of the resolved ref.' },
|
|
17
18
|
{ name: 'node', type: 'string', required: true, constraint: 'Node name + id.' },
|
|
18
|
-
{ name: 'source', type: 'string', required: true, constraint: 'report | doc | roadmap | meta (+ report-kind for reports).' },
|
|
19
|
+
{ name: 'source', type: 'string', required: true, constraint: 'report | doc | roadmap | meta | inbox | transcript (+ report-kind for reports).' },
|
|
19
20
|
{ name: 'ts', type: 'string', required: true, constraint: 'Artifact timestamp.' },
|
|
20
21
|
{ name: 'content', type: 'string', required: true, constraint: 'Full body. Frontmatter stripped unless --frontmatter.' },
|
|
21
22
|
],
|
|
@@ -25,6 +26,7 @@ export const readLeaf = defineLeaf({
|
|
|
25
26
|
run: async (input) => {
|
|
26
27
|
const ref = input['ref'].trim();
|
|
27
28
|
const includeFrontmatter = input['frontmatter'] === true;
|
|
29
|
+
const format = input['format'] ?? 'rendered';
|
|
28
30
|
// Fast local shape check; ref resolution (+ its usage/not-found errors) runs
|
|
29
31
|
// server-side in crtrd (`GET /v1/canvas/history/read`).
|
|
30
32
|
if (!ref.includes(':')) {
|
|
@@ -34,7 +36,7 @@ export const readLeaf = defineLeaf({
|
|
|
34
36
|
});
|
|
35
37
|
}
|
|
36
38
|
try {
|
|
37
|
-
const res = await cliClient().historyRead({ ref, frontmatter: includeFrontmatter });
|
|
39
|
+
const res = await cliClient().historyRead({ ref, frontmatter: includeFrontmatter, format });
|
|
38
40
|
return { ref: res.ref, node: res.node, source: res.source, ts: res.ts, content: res.content };
|
|
39
41
|
}
|
|
40
42
|
catch (err) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineLeaf } from '../../core/command.js';
|
|
2
2
|
import { cliClient, rethrowAsCliError } from '../api-client.js';
|
|
3
|
-
import { filterParams, parseFilters } from './shared.js';
|
|
3
|
+
import { applyFilters, filterParams, parseFilters } from './shared.js';
|
|
4
4
|
const SORTS = ['relevance', 'recency', 'oldest'];
|
|
5
5
|
export const searchLeaf = defineLeaf({
|
|
6
6
|
name: 'search',
|
|
@@ -8,7 +8,7 @@ export const searchLeaf = defineLeaf({
|
|
|
8
8
|
whenToUse: 'you want to find past work by what it SAYS — a design, a final report, a roadmap, a finding — across every node that ran in this cwd, ranked by relevance. Omit the query to browse the record by recency ("what happened here lately"). Narrow with --type/--kind/--status/--since, scope elsewhere with --cwd/--all-cwds/--under/--node. Need an exact regex over bodies instead of ranking? Use `canvas history grep` — a distinct leaf, not a flag here. Use `canvas history read <ref>` to read a hit in full, `node inspect artifacts <node-id>` to list one node\'s artifacts, and `node lifecycle revive <id>` to reopen a node you found.',
|
|
9
9
|
help: {
|
|
10
10
|
name: 'canvas history search',
|
|
11
|
-
summary: 'ranked/filtered/sorted content search over the per-cwd episodic record (reports + context docs + meta)',
|
|
11
|
+
summary: 'ranked/filtered/sorted content search over the per-cwd episodic record (reports + context docs + meta; transcripts and inbox opt-in)',
|
|
12
12
|
params: [
|
|
13
13
|
{ kind: 'positional', name: 'query', required: false, constraint: 'Whitespace-separated terms, matched case-insensitively and weighted across node name/description, report titles, and doc headings (and full bodies with --body); artifacts matching more/stronger fields rank higher. Omit to browse by recency (no relevance ranking; --sort still applies).' },
|
|
14
14
|
...filterParams,
|
|
@@ -45,26 +45,7 @@ export const searchLeaf = defineLeaf({
|
|
|
45
45
|
const q = { snippet_lines: snippetLines, limit, sort: sortMode };
|
|
46
46
|
if (query !== '')
|
|
47
47
|
q.query = query;
|
|
48
|
-
|
|
49
|
-
q.cwd = f.cwd;
|
|
50
|
-
if (f.allCwds)
|
|
51
|
-
q.all_cwds = true;
|
|
52
|
-
if (f.under !== undefined)
|
|
53
|
-
q.under = f.under;
|
|
54
|
-
if (f.nodes !== undefined)
|
|
55
|
-
q.nodes = f.nodes;
|
|
56
|
-
if (f.types !== undefined)
|
|
57
|
-
q.types = f.types;
|
|
58
|
-
if (f.reportKind !== undefined)
|
|
59
|
-
q.report_kind = f.reportKind;
|
|
60
|
-
if (f.kinds !== undefined)
|
|
61
|
-
q.kinds = f.kinds;
|
|
62
|
-
if (f.statuses !== undefined)
|
|
63
|
-
q.statuses = f.statuses;
|
|
64
|
-
if (f.sinceMs !== undefined)
|
|
65
|
-
q.since_ms = f.sinceMs;
|
|
66
|
-
if (f.untilMs !== undefined)
|
|
67
|
-
q.until_ms = f.untilMs;
|
|
48
|
+
applyFilters(q, f);
|
|
68
49
|
if (weighBody)
|
|
69
50
|
q.weigh_body = true;
|
|
70
51
|
if (full)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { InputParam } from '../../core/help.js';
|
|
2
2
|
export declare const TYPES: string[];
|
|
3
|
+
export declare const ORIGINS: string[];
|
|
4
|
+
export declare const ROLES: string[];
|
|
3
5
|
export declare const NODE_KINDS: string[];
|
|
4
6
|
export declare const STATUSES: string[];
|
|
5
7
|
/** ISO 8601 (`2026-06-01`) \u2192 epoch ms (absolute); relative (`7d`, `2w`, `1mo`)
|
|
@@ -21,7 +23,13 @@ export interface ParsedFilters {
|
|
|
21
23
|
statuses: string[] | undefined;
|
|
22
24
|
sinceMs: number | undefined;
|
|
23
25
|
untilMs: number | undefined;
|
|
26
|
+
origins: string[] | undefined;
|
|
27
|
+
roles: string[] | undefined;
|
|
28
|
+
from: string | undefined;
|
|
24
29
|
}
|
|
25
30
|
/** Parse + validate the shared scope/filter flags from a leaf's raw input.
|
|
26
31
|
* Pure and client-side; the corpus scan itself runs server-side in crtrd. */
|
|
27
32
|
export declare function parseFilters(input: Record<string, unknown>): ParsedFilters;
|
|
33
|
+
/** Copy every parsed filter onto a wire query. search, grep, and stats share
|
|
34
|
+
* the whole filter vocabulary, so the mapping lives once. */
|
|
35
|
+
export declare function applyFilters(q: Record<string, unknown>, f: ParsedFilters): void;
|
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
// seam. This is internal reuse only: neither leaf's public flag set or output
|
|
6
6
|
// shape is shared, and nothing here is re-exported as a command alias.
|
|
7
7
|
import { usage } from '../../core/errors.js';
|
|
8
|
-
export const TYPES = ['report', 'doc', 'roadmap', 'meta', 'inbox'];
|
|
8
|
+
export const TYPES = ['report', 'doc', 'roadmap', 'meta', 'inbox', 'transcript'];
|
|
9
|
+
export const ORIGINS = [
|
|
10
|
+
'human', 'inbox', 'wake', 'revive', 'restart-continuation',
|
|
11
|
+
'stop-guard', 'park', 'recovery', 'context-nudge', 'kickoff', 'unknown',
|
|
12
|
+
];
|
|
13
|
+
export const ROLES = ['user', 'assistant', 'toolResult'];
|
|
9
14
|
export const NODE_KINDS = ['advisor', 'developer', 'spec', 'design', 'review', 'explore', 'plan', 'general'];
|
|
10
15
|
export const STATUSES = ['active', 'idle', 'done', 'dead', 'canceled'];
|
|
11
16
|
const REL_UNITS = {
|
|
@@ -47,6 +52,9 @@ export const filterParams = [
|
|
|
47
52
|
{ kind: 'flag', name: 'status', type: 'string', required: false, constraint: `Node lifecycle status; comma-separated for several. One of: ${STATUSES.join(', ')} (e.g. done = completed work only).` },
|
|
48
53
|
{ kind: 'flag', name: 'since', type: 'string', required: false, constraint: 'Lower bound on artifact timestamp. ISO 8601 (2026-06-01) or relative (7d, 2w, 1mo).' },
|
|
49
54
|
{ kind: 'flag', name: 'until', type: 'string', required: false, constraint: 'Upper bound on artifact timestamp. ISO 8601 or relative.' },
|
|
55
|
+
{ kind: 'flag', name: 'origin', type: 'string', required: false, constraint: `What drove a message to arrive; comma-separated for several. One of: ${ORIGINS.join(', ')}. Applies to transcript + inbox artifacts, which are the only ones that arrive — passing it therefore excludes every generated turn on its own. \`unknown\` is a message carrying no stamp, and is its own bucket rather than a fallback.` },
|
|
56
|
+
{ kind: 'flag', name: 'role', type: 'string', required: false, constraint: `Transcript message role; comma-separated for several. One of: ${ROLES.join(', ')}.` },
|
|
57
|
+
{ kind: 'flag', name: 'from', type: 'string', required: false, constraint: 'Case-insensitive substring against sender attribution — the node that sent a delivery. Applies to transcript + inbox artifacts.' },
|
|
50
58
|
];
|
|
51
59
|
/** Parse + validate the shared scope/filter flags from a leaf's raw input.
|
|
52
60
|
* Pure and client-side; the corpus scan itself runs server-side in crtrd. */
|
|
@@ -61,6 +69,28 @@ export function parseFilters(input) {
|
|
|
61
69
|
if (bad !== undefined)
|
|
62
70
|
throw usage(`--type must be one of: ${TYPES.join(', ')}; received: ${bad}`);
|
|
63
71
|
}
|
|
72
|
+
const origins = splitCsv(input['origin']);
|
|
73
|
+
if (origins !== undefined) {
|
|
74
|
+
const bad = origins.find((o) => !ORIGINS.includes(o));
|
|
75
|
+
if (bad !== undefined)
|
|
76
|
+
throw usage(`--origin must be one of: ${ORIGINS.join(', ')}; received: ${bad}`);
|
|
77
|
+
}
|
|
78
|
+
const roles = splitCsv(input['role']);
|
|
79
|
+
if (roles !== undefined) {
|
|
80
|
+
const bad = roles.find((r) => !ROLES.includes(r));
|
|
81
|
+
if (bad !== undefined)
|
|
82
|
+
throw usage(`--role must be one of: ${ROLES.join(', ')}; received: ${bad}`);
|
|
83
|
+
}
|
|
84
|
+
// Session files are large and there is no persistent index, so the whole
|
|
85
|
+
// conversation corpus is reachable only through a narrowed scan. The guard
|
|
86
|
+
// lives in the failure rather than in a permanently visible constraint:
|
|
87
|
+
// agents who never search transcripts never pay to understand it.
|
|
88
|
+
if (types !== undefined && types.includes('transcript')) {
|
|
89
|
+
const narrowed = input['node'] !== undefined || input['under'] !== undefined || input['since'] !== undefined;
|
|
90
|
+
if (!narrowed) {
|
|
91
|
+
throw usage('--type transcript needs a narrowing flag: pass --node, --under, or --since.', { next: 'Transcripts are the full conversation corpus — scanning every node in a cwd reads every session file. Narrow first.' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
64
94
|
return {
|
|
65
95
|
cwd,
|
|
66
96
|
allCwds,
|
|
@@ -72,5 +102,38 @@ export function parseFilters(input) {
|
|
|
72
102
|
statuses: splitCsv(input['status']),
|
|
73
103
|
sinceMs: input['since'] !== undefined ? parseWhen('since', input['since']) : undefined,
|
|
74
104
|
untilMs: input['until'] !== undefined ? parseWhen('until', input['until']) : undefined,
|
|
105
|
+
origins,
|
|
106
|
+
roles,
|
|
107
|
+
from: input['from'],
|
|
75
108
|
};
|
|
76
109
|
}
|
|
110
|
+
/** Copy every parsed filter onto a wire query. search, grep, and stats share
|
|
111
|
+
* the whole filter vocabulary, so the mapping lives once. */
|
|
112
|
+
export function applyFilters(q, f) {
|
|
113
|
+
if (f.cwd !== undefined)
|
|
114
|
+
q['cwd'] = f.cwd;
|
|
115
|
+
if (f.allCwds)
|
|
116
|
+
q['all_cwds'] = true;
|
|
117
|
+
if (f.under !== undefined)
|
|
118
|
+
q['under'] = f.under;
|
|
119
|
+
if (f.nodes !== undefined)
|
|
120
|
+
q['nodes'] = f.nodes;
|
|
121
|
+
if (f.types !== undefined)
|
|
122
|
+
q['types'] = f.types;
|
|
123
|
+
if (f.reportKind !== undefined)
|
|
124
|
+
q['report_kind'] = f.reportKind;
|
|
125
|
+
if (f.kinds !== undefined)
|
|
126
|
+
q['kinds'] = f.kinds;
|
|
127
|
+
if (f.statuses !== undefined)
|
|
128
|
+
q['statuses'] = f.statuses;
|
|
129
|
+
if (f.sinceMs !== undefined)
|
|
130
|
+
q['since_ms'] = f.sinceMs;
|
|
131
|
+
if (f.untilMs !== undefined)
|
|
132
|
+
q['until_ms'] = f.untilMs;
|
|
133
|
+
if (f.origins !== undefined)
|
|
134
|
+
q['origins'] = f.origins;
|
|
135
|
+
if (f.roles !== undefined)
|
|
136
|
+
q['roles'] = f.roles;
|
|
137
|
+
if (f.from !== undefined)
|
|
138
|
+
q['from'] = f.from;
|
|
139
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const statsLeaf: import("../../core/command.js").LeafDef;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// `canvas history stats` — the grouped-count projection over the same corpus
|
|
2
|
+
// search and grep scan. A distinct leaf rather than a flag on search because
|
|
3
|
+
// the output shape differs: counts by one dimension, not a hit list. Grouping
|
|
4
|
+
// by origin is what turns the per-message stamps into a census — "what actually
|
|
5
|
+
// drove work here" — without a second command family.
|
|
6
|
+
import { defineLeaf } from '../../core/command.js';
|
|
7
|
+
import { usage } from '../../core/errors.js';
|
|
8
|
+
import { cliClient, rethrowAsCliError } from '../api-client.js';
|
|
9
|
+
import { applyFilters, filterParams, parseFilters } from './shared.js';
|
|
10
|
+
const GROUP_KEYS = ['origin', 'node', 'kind', 'day', 'type', 'role'];
|
|
11
|
+
export const statsLeaf = defineLeaf({
|
|
12
|
+
name: 'stats',
|
|
13
|
+
description: 'grouped counts over the cwd\'s node history — the projection view',
|
|
14
|
+
whenToUse: 'you want the SHAPE of the record rather than individual hits — how much of it came from where, who produced it, or when it happened. Group by origin to census what drove agents to run (cron wakes, inbox deliveries, recovery re-drives, people), by node/kind to see who produced the work, by day for volume over time, by type for corpus composition. Takes every scope and filter `canvas history search` takes, so any query you can search you can also count. Want the matching artifacts themselves? Use `canvas history search` or `canvas history grep`.',
|
|
15
|
+
help: {
|
|
16
|
+
name: 'canvas history stats',
|
|
17
|
+
summary: 'counts by one dimension over the per-cwd episodic record — origin census, per-node volume, activity by day',
|
|
18
|
+
params: [
|
|
19
|
+
{ kind: 'flag', name: 'group-by', type: 'enum', choices: GROUP_KEYS, required: false, default: 'type', constraint: 'The dimension to count by. origin = what drove an inbound message (transcript + inbox only); role = transcript message role; node / kind = who produced it; day = artifact date; type = corpus composition.' },
|
|
20
|
+
...filterParams,
|
|
21
|
+
],
|
|
22
|
+
output: [
|
|
23
|
+
{ name: 'group_by', type: 'string', required: true, constraint: 'The dimension counted.' },
|
|
24
|
+
{ name: 'groups', type: 'object[]', required: true, constraint: '{key, count}, descending by count then key.' },
|
|
25
|
+
{ name: 'total', type: 'int', required: true, constraint: 'Artifacts that carried the grouped dimension. Lower than `scanned` when grouping by one not every artifact has (origin, role).' },
|
|
26
|
+
{ name: 'scanned', type: 'int', required: true, constraint: 'Artifacts matched by the filters before grouping.' },
|
|
27
|
+
],
|
|
28
|
+
outputKind: 'object',
|
|
29
|
+
effects: ['None. Read-only.'],
|
|
30
|
+
},
|
|
31
|
+
run: async (input) => {
|
|
32
|
+
const by = input['groupBy'] ?? 'type';
|
|
33
|
+
if (!GROUP_KEYS.includes(by)) {
|
|
34
|
+
throw usage(`--group-by must be one of: ${GROUP_KEYS.join(', ')}; received: ${by}`);
|
|
35
|
+
}
|
|
36
|
+
// Filter validation stays client-side (pure); the corpus scan + grouping
|
|
37
|
+
// run server-side in crtrd, where canvas.db and the session files live.
|
|
38
|
+
const f = parseFilters(input);
|
|
39
|
+
const q = { group_by: by };
|
|
40
|
+
applyFilters(q, f);
|
|
41
|
+
try {
|
|
42
|
+
const res = await cliClient().historyStats(q);
|
|
43
|
+
return { group_by: res.group_by, groups: res.groups, total: res.total, scanned: res.scanned };
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
rethrowAsCliError(err);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
render: (r) => {
|
|
50
|
+
const groups = r['groups'];
|
|
51
|
+
const by = String(r['group_by']);
|
|
52
|
+
const total = r['total'];
|
|
53
|
+
const scanned = r['scanned'];
|
|
54
|
+
if (groups.length === 0) {
|
|
55
|
+
return `0 artifacts carried \`${by}\` (${scanned} scanned).`;
|
|
56
|
+
}
|
|
57
|
+
const width = Math.max(...groups.map((g) => g.key.length));
|
|
58
|
+
const max = groups[0].count;
|
|
59
|
+
const rows = groups
|
|
60
|
+
.map((g) => {
|
|
61
|
+
const bar = '█'.repeat(Math.max(1, Math.round((g.count / max) * 24)));
|
|
62
|
+
return ` ${g.key.padEnd(width)} ${String(g.count).padStart(6)} ${bar}`;
|
|
63
|
+
})
|
|
64
|
+
.join('\n');
|
|
65
|
+
const tail = total === scanned ? `${total} artifacts` : `${total} of ${scanned} scanned carried \`${by}\``;
|
|
66
|
+
return `by ${by}:\n\n${rows}\n\n- ${tail}`;
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -10,6 +10,7 @@ import { defineBranch } from '../core/command.js';
|
|
|
10
10
|
import { searchLeaf } from './canvas-history/search.js';
|
|
11
11
|
import { grepLeaf } from './canvas-history/grep.js';
|
|
12
12
|
import { readLeaf } from './canvas-history/read.js';
|
|
13
|
+
import { statsLeaf } from './canvas-history/stats.js';
|
|
13
14
|
/** A tiny per-cwd `<corpus>` marker for branch -h — just enough context to
|
|
14
15
|
* anchor the help text without scanning nodes or files. Uses only the injected
|
|
15
16
|
* node cwd env (or the process cwd outside a node), so help stays cheap. */
|
|
@@ -19,13 +20,13 @@ function corpusBlock() {
|
|
|
19
20
|
}
|
|
20
21
|
export const historyBranch = defineBranch({
|
|
21
22
|
name: 'history',
|
|
22
|
-
description: 'search and recall the cwd-wide content corpus — every node\'s reports
|
|
23
|
+
description: 'search and recall the cwd-wide content corpus — every node\'s reports, context docs, and conversations',
|
|
23
24
|
whenToUse: 'you want to find or re-read what was DONE in a cwd — a past design, a final report, a roadmap, a finding — across every node that ran there, WITHOUT already knowing which node. Use it when picking up prior work ("that caching work from last week"), recovering an artifact, or surveying a project\'s history. Already have the node id and just want everything IT left behind? Use `node inspect artifacts <id>` instead — this branch is the cwd-wide search corpus, not a per-node listing. Distinct from `crtr memory` (curated semantic knowledge, not episodes) and from `canvas dashboard`/`node inspect` (graph topology, not content).',
|
|
24
25
|
help: {
|
|
25
26
|
name: 'canvas history',
|
|
26
27
|
summary: 'search and recall the cwd-wide content corpus — every node\'s reports and context docs in this cwd',
|
|
27
|
-
model: 'The accumulated record of every node that ran in a cwd and the artifacts it left — its reports (final/update outcome summaries)
|
|
28
|
+
model: 'The accumulated record of every node that ran in a cwd and the artifacts it left — its reports (final/update outcome summaries), context docs (specs, designs, roadmaps, findings), and the conversations themselves. `search` finds by content (ranked, filtered, sorted; omit the query to browse by recency); `grep` finds by an exact ECMAScript regex over body lines (pattern required, one hit per matching line — use this over `search` when you know the literal string/pattern, not just the topic); `read` pulls one hit\'s full body by its <node-id>:<relpath> ref, `--format raw` for the verbatim bytes; `stats` counts the same filtered corpus by one dimension instead of listing hits. Corpus is a DIMENSION here, not a location: `--type` selects which record you search, and every leaf takes the same scope and filters. This is the episodic record of what was DONE — distinct from `crtr memory`, which is curated semantic knowledge. Already have a node id and want everything IT left behind (not a cwd-wide search)? Use `node inspect artifacts <node-id>` instead. Two corpora are opt-in diagnostics rather than curated work: `--type inbox` is one node\'s full cursor-independent inbox.jsonl including un-clipped message bodies, and `--type transcript` is the conversation itself, message by message, each inbound turn carrying the origin that drove it (`--origin`, `--role`, `--from`, and `stats --group-by origin` for the census). Transcripts require a narrowing flag because there is no index. To reopen a node you found, use `node lifecycle revive`; for its place in the graph, `node inspect show`.',
|
|
28
29
|
dynamicState: corpusBlock,
|
|
29
30
|
},
|
|
30
|
-
children: [searchLeaf, grepLeaf, readLeaf],
|
|
31
|
+
children: [searchLeaf, grepLeaf, readLeaf, statsLeaf],
|
|
31
32
|
});
|
|
@@ -94,8 +94,8 @@ function nodeNewParams() {
|
|
|
94
94
|
whenToUse: 'Use only when the user, task, or governing agent guidance already requires a specific provider, model, capability tier, or family. Omit it for routine delegation: the selected persona kind and profile own normal model selection, and review personas may enforce a quality floor.',
|
|
95
95
|
value: `Accepted forms: ${MODEL_SPEC_FORMS}. Concrete provider/id requests must be registered. Portable provider/tier, bare-tier, and family-alias requests resolve through the configured model ladders.`,
|
|
96
96
|
effects: [
|
|
97
|
-
'The resolved override is stored on the node and in its launch recipe, so
|
|
98
|
-
'
|
|
97
|
+
'The resolved concrete override is stored on the node and in its launch recipe, so every launch and revive uses that exact provider/model.',
|
|
98
|
+
'Portable tiers and aliases resolve through the configured ladders; a persona quality floor may raise a weaker request before its resolved model is stored.',
|
|
99
99
|
],
|
|
100
100
|
},
|
|
101
101
|
},
|
|
@@ -107,10 +107,10 @@ export const pluginShow = defineLeaf({
|
|
|
107
107
|
{ name: 'manifest', type: 'object', required: true, constraint: 'Full plugin.json contents.' },
|
|
108
108
|
{ name: 'docs', type: 'object[]', required: true, constraint: 'Each: {name, path}. Memory docs provided by the plugin (its `<pluginName>/` memory subtree).' },
|
|
109
109
|
{ name: 'commands', type: 'object', required: false, constraint: 'Present only when the plugin declares a command manifest (manifest.commands). {manifestPath: string, mounts: string[] (accepted top-level command names), extensibleBranches: string[] (mount names accepting repository fragments), issues: object[] ({code, path?, message, received, expected, next})}. Validated statically; command execution never occurs.' },
|
|
110
|
-
{ name: 'hooks', type: 'object', required: false, constraint: 'Present for every hook declaration, including hook-only plugins. {manifestPath?, executablePath?, declarations: {target, phase, op, description, effects}[], issues: object[] ({code, path?, message, received, expected, next}), trust: string}. Static only: no hook executable runs.' },
|
|
110
|
+
{ name: 'hooks', type: 'object', required: false, constraint: 'Present for every hook declaration, including hook-only plugins. {manifestPath?, executablePath?, declarations: {target, phase, op, description, effects}[], lifecycle?: {event, phase, op, description, effects}[], issues: object[] ({code, path?, message, received, expected, next}), trust: string}. Static only: no hook executable runs.' },
|
|
111
111
|
],
|
|
112
112
|
outputKind: 'object',
|
|
113
|
-
effects: ['None. Read-only. Command and hook manifests are validated statically; plugin executables are never executed.'],
|
|
113
|
+
effects: ['None. Read-only. Command and lifecycle-hook manifests are validated statically; plugin executables are never executed.'],
|
|
114
114
|
},
|
|
115
115
|
run: async (input) => {
|
|
116
116
|
const name = input['name'];
|
|
@@ -729,7 +729,7 @@ export const pluginInstall = defineLeaf({
|
|
|
729
729
|
{ name: 'kinds', type: 'integer', required: false, constraint: 'Kind-registry entry count declared by an --endpoint archive\u2019s bundle.json.' },
|
|
730
730
|
{ name: 'pageComponents', type: 'integer', required: false, constraint: 'Page-component registration count declared by an --endpoint archive\u2019s bundle.json.' },
|
|
731
731
|
{ 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.' },
|
|
732
|
-
{ name: 'hooks', type: 'object', required: false, constraint: 'Present only when the source plugin declares hooks. {manifestPath?, executablePath?, declarations: {target, phase, op, description, effects}[], issues: object[], trust: string}.
|
|
732
|
+
{ name: 'hooks', type: 'object', required: false, constraint: 'Present only when the source plugin declares hooks. {manifestPath?, executablePath?, declarations: {target, phase, op, description, effects}[], lifecycle?: {event, phase, op, description, effects}[], issues: object[], trust: string}. Command targets, lifecycle events, and replacement collisions are validated statically; hook executables are never run.' },
|
|
733
733
|
{ 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.' },
|
|
734
734
|
{ name: 'memory', type: 'object', required: false, constraint: 'Present only when the plugin ships a memory store. {docs: integer (documents checked), migrated: string[] (store-relative documents rewritten to reach exact canonical identity), linkedSource?: string (the caller-owned directory converged in place, present only when the install keeps that directory as a live link)}.' },
|
|
735
735
|
],
|
|
@@ -738,7 +738,7 @@ export const pluginInstall = defineLeaf({
|
|
|
738
738
|
'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.',
|
|
739
739
|
'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.',
|
|
740
740
|
'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.',
|
|
741
|
-
'Source plugin hooks go live on the next crtr invocation. They run implicitly with caller authority, may receive normalized inputs and after results, may block or replace declared targets, and may transmit received data. Hook executables are never run during install or update validation.',
|
|
741
|
+
'Source plugin command hooks go live on the next crtr invocation. They run implicitly with caller authority, may receive normalized inputs and after results, may block or replace declared targets, and may transmit received data. Lifecycle hooks run unprompted at every declared event before node launch and may transmit node identity and resolved paths. Hook executables are never run during install or update validation.',
|
|
742
742
|
'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.',
|
|
743
743
|
'Before activation the candidate\u2019s memory corpus is migrated to exact canonical identity and refused if it cannot mount. A staged candidate is migrated in staging and rolls back with the package; a local path or marketplace-relative source stays a live link, so its own directory is rewritten in place, keeps that rewrite whether or not the install completes, and is named as `memory.linkedSource`. A refused corpus writes nothing and installs nothing.',
|
|
744
744
|
],
|
|
@@ -904,7 +904,7 @@ export const pluginUpdate = defineLeaf({
|
|
|
904
904
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Narrows resolution.' },
|
|
905
905
|
],
|
|
906
906
|
output: [
|
|
907
|
-
{ name: 'updated', type: 'object[]', required: true, constraint: 'One entry per plugin processed: {name, transport?, updated, sha?, version?, docs?, kinds?, pageComponents?, commands?, hooks?, memory?, warnings?, error?}. memory is present for a plugin shipping a memory store and reports the same corpus migration install performs: {docs, migrated, linkedSource?}. 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[]}. hooks is present for source hook plugins: {manifestPath?, executablePath?, declarations: {target, phase, op, description, effects}[], issues: object[], trust: string}; it is static and never executes hooks. warnings lists advisory PATH requirements that remain absent after update.' }
|
|
907
|
+
{ name: 'updated', type: 'object[]', required: true, constraint: 'One entry per plugin processed: {name, transport?, updated, sha?, version?, docs?, kinds?, pageComponents?, commands?, hooks?, memory?, warnings?, error?}. memory is present for a plugin shipping a memory store and reports the same corpus migration install performs: {docs, migrated, linkedSource?}. 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[]}. hooks is present for source hook plugins: {manifestPath?, executablePath?, declarations: {target, phase, op, description, effects}[], lifecycle?: {event, phase, op, description, effects}[], issues: object[], trust: string}; it is static and never executes hooks. warnings lists advisory PATH requirements that remain absent after update.' }
|
|
908
908
|
],
|
|
909
909
|
outputKind: 'object',
|
|
910
910
|
effects: [
|
|
@@ -39,6 +39,11 @@ function assertUserScopeBrokerThresholds(key, scope) {
|
|
|
39
39
|
throw usage('brokerThresholds are available only in user scope');
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
function assertUserScopeLifecycle(key, scope) {
|
|
43
|
+
if (key.split('.', 1)[0] === 'lifecycle' && scope !== 'user') {
|
|
44
|
+
throw usage('lifecycle settings are available only in user scope');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
42
47
|
function assertWorkingGerundsScope(key, scope) {
|
|
43
48
|
if (key === 'working_gerunds' && scope !== 'user') {
|
|
44
49
|
throw usage('working_gerunds is available only in user scope');
|
|
@@ -132,6 +137,16 @@ function setNestedValue(cfg, key, rawValue, value) {
|
|
|
132
137
|
cfg.brokerThresholds[parts[1]] = threshold;
|
|
133
138
|
return;
|
|
134
139
|
}
|
|
140
|
+
if (topKey === 'lifecycle') {
|
|
141
|
+
if (parts.length !== 2 || parts[1] !== 'unattendedParkMs') {
|
|
142
|
+
throw usage(`lifecycle key must be unattendedParkMs`);
|
|
143
|
+
}
|
|
144
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 1) {
|
|
145
|
+
throw usage(`lifecycle.unattendedParkMs must be an integer >= 1`);
|
|
146
|
+
}
|
|
147
|
+
cfg.lifecycle.unattendedParkMs = Math.floor(value);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
135
150
|
throw usage(`unsupported key path for set: ${key}`);
|
|
136
151
|
}
|
|
137
152
|
// Leaf definitions
|
|
@@ -143,7 +158,7 @@ const configGet = defineLeaf({
|
|
|
143
158
|
name: 'sys config get',
|
|
144
159
|
summary: 'read a config value by dotted key',
|
|
145
160
|
params: [
|
|
146
|
-
{ kind: 'positional', name: 'key', type: 'string', required: true, constraint: `Dotted key path. Top-level keys: ${CONFIG_TOP_LEVEL_HELP}. brokerThresholds
|
|
161
|
+
{ kind: 'positional', name: 'key', type: 'string', required: true, constraint: `Dotted key path. Top-level keys: ${CONFIG_TOP_LEVEL_HELP}. brokerThresholds and lifecycle are user-only; keybindings is readable only as the whole user-scope object and editable only through sys settings.` },
|
|
147
162
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Scope to read from. Default: user.' },
|
|
148
163
|
],
|
|
149
164
|
output: [
|
|
@@ -158,6 +173,7 @@ const configGet = defineLeaf({
|
|
|
158
173
|
const key = input['key'];
|
|
159
174
|
const scope = resolveScope(input['scope']);
|
|
160
175
|
assertUserScopeBrokerThresholds(key, scope);
|
|
176
|
+
assertUserScopeLifecycle(key, scope);
|
|
161
177
|
if (key.startsWith('keybindings.') || (key === 'keybindings' && scope !== 'user')) {
|
|
162
178
|
throw notFound('keybindings are available only as the whole user-scope object');
|
|
163
179
|
}
|
|
@@ -177,7 +193,7 @@ const configSet = defineLeaf({
|
|
|
177
193
|
name: 'sys config set',
|
|
178
194
|
summary: 'write a config value by dotted key',
|
|
179
195
|
params: [
|
|
180
|
-
{ kind: 'positional', name: 'key', type: 'string', required: true, constraint: `Dotted key path. User-scope scalar settings: ${SCALAR_SETTING_HELP}. Also supported: working_gerunds and whip_messages (non-empty JSON string arrays), brokerThresholds.warning and brokerThresholds.automaticReviveCap (user scope; integer >= 1), modelLadders.defaultProvider, and modelLadders.<anthropic|openai>.<ultra|strong|medium|light>. Keybindings are edited only through sys settings. humanActions is an object map edited directly in ~/.crouter/config.json or <repo>/.crouter/config.json.` },
|
|
196
|
+
{ kind: 'positional', name: 'key', type: 'string', required: true, constraint: `Dotted key path. User-scope scalar settings: ${SCALAR_SETTING_HELP}. Also supported: working_gerunds and whip_messages (non-empty JSON string arrays), brokerThresholds.warning and brokerThresholds.automaticReviveCap (user scope; integer >= 1), lifecycle.unattendedParkMs (user scope; integer ms >= 1), modelLadders.defaultProvider, and modelLadders.<anthropic|openai>.<ultra|strong|medium|light>. Keybindings are edited only through sys settings. humanActions is an object map edited directly in ~/.crouter/config.json or <repo>/.crouter/config.json.` },
|
|
181
197
|
{ kind: 'flag', name: 'value', type: 'string', required: true, constraint: 'value VALUE — string, required. working_gerunds and whip_messages accept JSON arrays; other values are stored as-is if quoted and coerced to number or boolean when unambiguous.' },
|
|
182
198
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Scope to write to. Default: user. Project scope is supported only for modelLadders.' },
|
|
183
199
|
],
|
|
@@ -193,6 +209,7 @@ const configSet = defineLeaf({
|
|
|
193
209
|
const rawValue = input['value'];
|
|
194
210
|
const scope = resolveScope(input['scope']);
|
|
195
211
|
assertUserScopeBrokerThresholds(key, scope);
|
|
212
|
+
assertUserScopeLifecycle(key, scope);
|
|
196
213
|
assertWorkingGerundsScope(key, scope);
|
|
197
214
|
assertWhipMessagesScope(key, scope);
|
|
198
215
|
assertUserSettingScope(key, scope);
|
|
@@ -178,8 +178,9 @@ async function runHookPluginChecks(scopes) {
|
|
|
178
178
|
continue;
|
|
179
179
|
const issues = registry.issues.filter((issue) => issue.plugin === plugin.name && issue.scope === plugin.scope);
|
|
180
180
|
if (issues.length === 0) {
|
|
181
|
-
const
|
|
182
|
-
|
|
181
|
+
const commandDeclarations = validation.hooks.map((hook) => `${hook.target} [${hook.phase} ${hook.op}]`);
|
|
182
|
+
const lifecycleDeclarations = (validation.manifest?.schemaVersion === 2 ? validation.manifest.lifecycle : []).map((hook) => `${hook.event} [${hook.phase} ${hook.op}; effects: ${hook.effects.join(', ')}]`);
|
|
183
|
+
results.push(pass(plugin.scope, `plugin:${plugin.name}:hooks`, `effective hook declaration(s): ${[...commandDeclarations, ...lifecycleDeclarations].join('; ')}`));
|
|
183
184
|
continue;
|
|
184
185
|
}
|
|
185
186
|
issues.forEach((issue, index) => {
|
|
@@ -578,7 +579,7 @@ export const sysDoctorLeaf = defineLeaf({
|
|
|
578
579
|
{ kind: 'flag', name: 'remote', type: 'bool', required: false, constraint: 'Check git remotes with ls-remote (slow — makes network calls).' },
|
|
579
580
|
],
|
|
580
581
|
output: [
|
|
581
|
-
{ 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. humanActions:<name>@<config-origin> checks validate each user/project scope completion-action declaration: action-name and argv shape, cwd directory, argv[0] regular-file existence, and execute permission; they never execute an action. A malformed effective declaration fails; a malformed shadowed declaration is a pass-level warning that names the winning and shadowed origins. 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 checks appear as plugin:<name>:commands and plugin:<name>:hooks; both are validated statically and never execute plugin code. Hook failures identify manifest/path/target/collision problems and carry disable/update/remove/show remediation; healthy hooks list effective target, phase, and op. 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.' },
|
|
582
|
+
{ 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. humanActions:<name>@<config-origin> checks validate each user/project scope completion-action declaration: action-name and argv shape, cwd directory, argv[0] regular-file existence, and execute permission; they never execute an action. A malformed effective declaration fails; a malformed shadowed declaration is a pass-level warning that names the winning and shadowed origins. 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 checks appear as plugin:<name>:commands and plugin:<name>:hooks; both are validated statically and never execute plugin code. Hook failures identify manifest/path/target/event/collision problems and carry disable/update/remove/show remediation; healthy command hooks list effective target, phase, and op, while lifecycle hooks list event, phase, op, and effects. 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.' },
|
|
582
583
|
{ name: 'ok', type: 'boolean', required: true, constraint: 'True when no unresolved fail checks remain.' },
|
|
583
584
|
],
|
|
584
585
|
outputKind: 'object',
|
|
@@ -961,7 +961,9 @@ export class ModelsPanel {
|
|
|
961
961
|
const contentBudget = Math.max(1, height);
|
|
962
962
|
const frame = (lines) => renderRows(lines, width);
|
|
963
963
|
if (this.mode === "dimension-picker")
|
|
964
|
-
|
|
964
|
+
// Provider ids run to dozens of options, so the picker windows on its
|
|
965
|
+
// selection like the list does — a tail cut would scroll the cursor off.
|
|
966
|
+
return frame(this.fitBody(this.renderChoicePicker(), MODELS_CHROME_LINES + this.choiceIndex, contentBudget));
|
|
965
967
|
if (this.mode === "model-picker")
|
|
966
968
|
return frame(fitTail(this.modelSelector?.render(width) ?? [], contentBudget));
|
|
967
969
|
if (this.mode === "thinking-picker")
|
|
@@ -192,7 +192,7 @@ describe('terminal ticket delivery', () => {
|
|
|
192
192
|
await deliverTerminalResult(bridge);
|
|
193
193
|
assert.equal(getNode(companion)?.final_report, finalReport, 'terminal replay preserves the first canonical final');
|
|
194
194
|
assert.equal(getNode(companion)?.finalized_at, finalizedAt, 'terminal replay preserves the first finalization time');
|
|
195
|
-
assert.
|
|
195
|
+
await assert.rejects(reviveNode(companion, { resume: true, capacity: 'freeze' }), /finalization latch/);
|
|
196
196
|
const { bridge: dismissedBridge } = bridgeUnder();
|
|
197
197
|
const dismissedDir = submitBridgePage(dismissedBridge);
|
|
198
198
|
const dismissedCompanion = spawnNode({ kind: 'general', parent: dismissedBridge, creator: dismissedBridge, cwd: home, name: 'dismissed feedback companion', lifecycle: 'resident' }).node_id;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|