@north-light/crouter 0.3.234 → 0.3.235

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.
Files changed (58) hide show
  1. package/dist/api/dto/nodes.d.ts +11 -0
  2. package/dist/clients/attach/viewer.js +200 -200
  3. package/dist/commands/attention.js +1 -1
  4. package/dist/commands/cron.js +5 -5
  5. package/dist/commands/human.js +1 -1
  6. package/dist/commands/memory/__tests__/memory-list-and-lint-ignore.test.d.ts +1 -0
  7. package/dist/commands/memory/__tests__/memory-list-and-lint-ignore.test.js +96 -0
  8. package/dist/commands/memory/delete.js +1 -1
  9. package/dist/commands/memory/edit.js +6 -3
  10. package/dist/commands/memory/list.d.ts +4 -0
  11. package/dist/commands/memory/list.js +20 -14
  12. package/dist/commands/memory/move.js +1 -1
  13. package/dist/commands/memory/shared.d.ts +1 -0
  14. package/dist/commands/memory/shared.js +13 -1
  15. package/dist/commands/memory/write.js +4 -1
  16. package/dist/commands/node/inspect.js +11 -4
  17. package/dist/commands/node/lifecycle.js +1 -0
  18. package/dist/commands/node/message.js +8 -4
  19. package/dist/commands/node/subscription.js +5 -1
  20. package/dist/commands/node/wait.js +5 -1
  21. package/dist/commands/pkg/plugin-manage.js +1 -1
  22. package/dist/commands/pkg.js +1 -0
  23. package/dist/commands/profile/rename.js +1 -1
  24. package/dist/commands/sys/config.js +1 -0
  25. package/dist/commands/sys.js +1 -0
  26. package/dist/core/__tests__/cli-help-contract.test.d.ts +1 -0
  27. package/dist/core/__tests__/cli-help-contract.test.js +150 -0
  28. package/dist/core/__tests__/integration/command-plugins.test.js +21 -0
  29. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +11 -6
  30. package/dist/core/canvas/canvas.js +11 -1
  31. package/dist/core/canvas/meta-file.js +18 -27
  32. package/dist/core/canvas/remote-canvas-source.d.ts +8 -0
  33. package/dist/core/canvas/remote-canvas-source.js +9 -1
  34. package/dist/core/command-manifests/registry.d.ts +2 -0
  35. package/dist/core/command-plugins/compose.js +4 -2
  36. package/dist/core/command-plugins/discovery.d.ts +1 -0
  37. package/dist/core/command-plugins/discovery.js +1 -1
  38. package/dist/core/command.js +2 -2
  39. package/dist/core/fault-classifier.js +8 -0
  40. package/dist/core/pagination.d.ts +2 -0
  41. package/dist/core/pagination.js +2 -1
  42. package/dist/core/runtime/broker/fault-retry.js +6 -0
  43. package/dist/core/runtime/broker/rebind.js +50 -0
  44. package/dist/core/runtime/launch.d.ts +2 -2
  45. package/dist/core/runtime/launch.js +17 -4
  46. package/dist/core/runtime/stop-guard.js +28 -16
  47. package/dist/core/substrate/frontmatter-validation.d.ts +1 -0
  48. package/dist/core/substrate/frontmatter-validation.js +1 -1
  49. package/dist/daemon/api/__tests__/error-seam.test.js +27 -0
  50. package/dist/daemon/api/handlers/messages.js +12 -12
  51. package/dist/daemon/api/handlers/nodes.js +13 -1
  52. package/dist/daemon/api/handlers/worktree.js +6 -22
  53. package/dist/daemon/api/map.d.ts +4 -2
  54. package/dist/daemon/api/map.js +22 -2
  55. package/dist/daemon/reconcilers/broker-supervision.js +1 -1
  56. package/dist/pi-extensions/canvas-bash-valve.js +4 -3
  57. package/package.json +1 -1
  58. package/runtime.lock.json +2 -2
@@ -209,7 +209,7 @@ export const attentionBranch = defineBranch({
209
209
  help: {
210
210
  name: 'canvas attention',
211
211
  summary: 'aggregate pending human tickets across the canvas',
212
- model: '`count` returns a single integer (stdout.count is machine-parseable); `list` returns itemised asking-node entries. Scope with --node (one node) or --view (sub-DAG) — default is canvas-wide. A ticket is any humanloop interaction awaiting the user: an ask, a review, or a notification.',
212
+ model: '`count` returns a single integer (stdout.count is machine-parseable) and accepts --node or --view; `list` returns itemised asking-node entries and accepts --view only. Both default to canvas-wide. A ticket is any humanloop interaction awaiting the user: an ask, a review, or a notification.',
213
213
  },
214
214
  children: [attentionCount, attentionList, attentionMap],
215
215
  });
@@ -19,7 +19,7 @@ import { resolve as resolvePath } from 'node:path';
19
19
  import { defineBranch, defineLeaf } from '../core/command.js';
20
20
  import { InputError, readStdinRaw } from '../core/io.js';
21
21
  import { parseWhen, parseCadence, cadenceDisplay } from '../core/wake.js';
22
- import { resolveProfileOperand } from '../core/profiles/manifest.js';
22
+ import { loadProfileManifest, resolveProfileOperand } from '../core/profiles/manifest.js';
23
23
  import { profilesStateBlock } from '../core/profiles/state-block.js';
24
24
  import { cliClient, rethrowAsCliError } from './api-client.js';
25
25
  /** Canonicalize a profile operand (id, manifest name, or nearest match) to its profile id so
@@ -69,7 +69,7 @@ function parseRunTimeout(raw) {
69
69
  * caller sees the whole canvas home. */
70
70
  function scopeQuery() {
71
71
  const p = envProfileId();
72
- return p !== undefined && p.trim() !== '' ? { profile: canonicalProfileId(p) } : undefined;
72
+ return p !== undefined && p.trim() !== '' ? { profile: p } : undefined;
73
73
  }
74
74
  /** Resolve the chosen-else-inherited cwd, and refuse one that is not a
75
75
  * directory NOW — the row is a snapshot, so a bad cwd would fail at every
@@ -235,7 +235,7 @@ const addLeaf = defineLeaf({
235
235
  const profile = profileFlag !== undefined
236
236
  ? canonicalProfileId(profileFlag)
237
237
  : inherited !== undefined && inherited.trim() !== ''
238
- ? canonicalProfileId(inherited)
238
+ ? loadProfileManifest(inherited).profileId
239
239
  : undefined;
240
240
  const scopeFlag = input['scope'];
241
241
  const scope = scopeFlag ?? (profile !== undefined ? 'profile' : 'global');
@@ -286,7 +286,7 @@ const listLeaf = defineLeaf({
286
286
  { name: 'crons', type: 'object[]', required: true, constraint: 'One row per cron: cron_id, name, fire_at (next fire, UTC), recur (cadence display), state (active|paused), held (true while parked by an exit-75 gate — fires on daemon poke, else at fire_at), on_output, sink, expires_at, last_run (the most recent settled run: finished, exit_code, delivered — null if it never ran).' },
287
287
  ],
288
288
  outputKind: 'object',
289
- effects: [],
289
+ effects: ['None. Read-only.'],
290
290
  },
291
291
  run: async () => {
292
292
  const crons = await cliClient().listCrons(scopeQuery()).catch(rethrowAsCliError);
@@ -349,7 +349,7 @@ const showLeaf = defineLeaf({
349
349
  { name: 'runs', type: 'object[]', required: true, constraint: 'The bounded run log, most recent first: started, exit_code (-1 = timeout kill), duration_ms, stdout/stderr heads, delivered (what the sink did, or why it did not).' },
350
350
  ],
351
351
  outputKind: 'object',
352
- effects: [],
352
+ effects: ['None. Read-only.'],
353
353
  },
354
354
  run: async (input) => {
355
355
  const id = requireCronId(input);
@@ -41,7 +41,7 @@ export function registerHuman() {
41
41
  pageHelp.brief,
42
42
  pageHelp.authoring,
43
43
  pageHelp.componentSelection.join('\n'),
44
- 'Props, response contracts, and the full component catalog: `crtr human components`.',
44
+ 'Props, response contracts, and the full component catalog: `crtr human components`; select with `crtr human components list`, then read one with `crtr human components show <component>`.',
45
45
  ].join('\n\n'),
46
46
  },
47
47
  children: [
@@ -0,0 +1,96 @@
1
+ import { test, after } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { parseFrontmatterGeneric } from '../../../core/frontmatter.js';
7
+ import { resetScopeCache } from '../../../core/scope.js';
8
+ import { CrtrError } from '../../../core/errors.js';
9
+ import { editLeaf } from '../edit.js';
10
+ import { listLeaf } from '../list.js';
11
+ import { writeLeaf } from '../write.js';
12
+ const previousHome = process.env['HOME'];
13
+ const previousCrtrHome = process.env['CRTR_HOME'];
14
+ const home = mkdtempSync(join(tmpdir(), 'crtr-memory-list-'));
15
+ const canvasHome = mkdtempSync(join(tmpdir(), 'crtr-memory-list-canvas-'));
16
+ mkdirSync(join(home, '.crouter', 'memory'), { recursive: true });
17
+ process.env['HOME'] = home;
18
+ process.env['CRTR_HOME'] = canvasHome;
19
+ function writeMemory(name, frontmatter = 'kind: knowledge') {
20
+ writeFileSync(join(home, '.crouter', 'memory', name), `---\n${frontmatter}\n---\n\nBODY\n`, 'utf8');
21
+ }
22
+ after(() => {
23
+ resetScopeCache();
24
+ if (previousHome === undefined)
25
+ delete process.env['HOME'];
26
+ else
27
+ process.env['HOME'] = previousHome;
28
+ if (previousCrtrHome === undefined)
29
+ delete process.env['CRTR_HOME'];
30
+ else
31
+ process.env['CRTR_HOME'] = previousCrtrHome;
32
+ rmSync(home, { recursive: true, force: true });
33
+ rmSync(canvasHome, { recursive: true, force: true });
34
+ });
35
+ test('memory list resumes locale-aware pages across case boundaries', async () => {
36
+ writeMemory('upper.md', 'kind: knowledge\nname: A');
37
+ writeMemory('lower.md', 'kind: knowledge\nname: a');
38
+ writeMemory('INDEXED.md');
39
+ const first = (await listLeaf.run({ scope: 'user', limit: 2, cursor: undefined, kind: undefined, dir: undefined, paths: false }));
40
+ assert.deepEqual(first.items.map((item) => item.name), ['a', 'A']);
41
+ assert.ok(first.next_cursor);
42
+ const second = (await listLeaf.run({ scope: 'user', limit: 1, cursor: first.next_cursor, kind: undefined, dir: undefined, paths: false }));
43
+ assert.deepEqual(second.items.map((item) => item.name), ['INDEXED']);
44
+ const edge = (await listLeaf.run({ scope: 'user', limit: 3, cursor: undefined, kind: undefined, dir: undefined, paths: false }));
45
+ assert.deepEqual(edge.items.map((item) => item.name), ['a', 'A', 'INDEXED']);
46
+ assert.equal(edge.next_cursor, null);
47
+ });
48
+ test('memory write and edit set, validate, and unset lint-ignore', async () => {
49
+ const scalar = (await writeLeaf.run({
50
+ name: 'lint-scalar',
51
+ scope: 'user',
52
+ kind: 'knowledge',
53
+ whenAndWhyToRead: 'When lint length is intentional, this knowledge should be read because the document remains available.',
54
+ lintIgnore: 'length',
55
+ body: 'SCALAR',
56
+ }));
57
+ assert.equal(parseFrontmatterGeneric(readFileSync(scalar.path, 'utf8')).data?.['lint-ignore'], 'length');
58
+ const list = (await writeLeaf.run({
59
+ name: 'lint-list',
60
+ scope: 'user',
61
+ kind: 'knowledge',
62
+ whenAndWhyToRead: 'When lint breadth is intentional, this knowledge should be read because the document remains available.',
63
+ lintIgnore: ['length', 'broad-memory-read'],
64
+ body: 'LIST',
65
+ }));
66
+ assert.deepEqual(parseFrontmatterGeneric(readFileSync(list.path, 'utf8')).data?.['lint-ignore'], ['length', 'broad-memory-read']);
67
+ await editLeaf.run({
68
+ name: 'lint-scalar',
69
+ scope: 'user',
70
+ rationale: 'set the second suppressible rule',
71
+ lintIgnore: ['length', 'broad-memory-read'],
72
+ });
73
+ await editLeaf.run({
74
+ name: 'lint-scalar',
75
+ scope: 'user',
76
+ rationale: 'remove lint suppression',
77
+ unset: ['lint-ignore'],
78
+ });
79
+ assert.equal(parseFrontmatterGeneric(readFileSync(scalar.path, 'utf8')).data?.['lint-ignore'], undefined);
80
+ let error;
81
+ try {
82
+ await writeLeaf.run({
83
+ name: 'lint-invalid',
84
+ scope: 'user',
85
+ kind: 'knowledge',
86
+ whenAndWhyToRead: 'When invalid lint metadata is supplied, this knowledge should be read because it must be rejected.',
87
+ lintIgnore: ['length', 'not-a-rule'],
88
+ body: 'INVALID',
89
+ });
90
+ }
91
+ catch (caught) {
92
+ error = caught;
93
+ }
94
+ assert.ok(error instanceof CrtrError);
95
+ assert.equal(error.code, 'usage');
96
+ });
@@ -33,7 +33,7 @@ export const deleteLeaf = defineLeaf({
33
33
  summary: 'resolve an exact canonical name and remove that document from its store',
34
34
  params: [
35
35
  { kind: 'positional', name: 'name', required: true, constraint: 'Full canonical name of the document — a project document is `<namespace>/<local name>`. Resolution is exact, as `read` resolves it: a file path, a bare leaf, a `<scope>/<name>` spelling, and a trailing `/INDEX` all fail. A canonical directory’s own document is deleted at the directory name; its members are untouched and stay reachable. If two physical files in the selected store own the name, delete selects the first path in stable lexical order and reports it. Builtin and installed-plugin documents are read-only and refuse to delete, as does the repository-root document, which declares the immutable namespace for every document in the repository; a nested store’s root guide is ordinary and may be deleted.' },
36
- selectorParam('scope-read', { choices: [...MEMORY_WRITE_SCOPES] }, 'builtin is not a choice — builtin documents ship with the package and cannot be deleted.'),
36
+ selectorParam('scope-read', { choices: [...MEMORY_WRITE_SCOPES] }, 'Package-shipped documents cannot be deleted through this writable-scope selector.'),
37
37
  selectorParam('dir', {}, 'Deletes from that one store, including a non-winning duplicate no target view resolves to.'),
38
38
  ],
39
39
  output: [
@@ -4,11 +4,11 @@ import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
4
4
  import { readText, writeText } from '../../core/fs-utils.js';
5
5
  import { loadMemoryStoreView, resolveMemoryCandidates, } from '../../core/memory-resolver.js';
6
6
  import { appendHistoryRecord, buildHistoryRecord, historyLogPathFor, readHistoryRecords, } from '../../core/memory/history.js';
7
- import { DOC_RATIONALE_CONSTRAINT, GUIDE_CANONICAL_NAMES, GUIDE_DOC_LINKS, GUIDE_PREDICATE_VOCABULARY, GUIDE_ROUTING_LINE, GUIDE_SURFACES, coerceGate, coerceSurface, memoryExtensionCatalogForDoc, memoryExtensionFieldCatalogHelp, overlayParam, parseRequestedExtensionChanges, applyExtensionChanges, literalBodySegment, serializeMemoryDocLiteral, selectorParam, requireLocalNameInStore, requireUnambiguousDoc, resolveReadSelector, } from './shared.js';
7
+ import { DOC_RATIONALE_CONSTRAINT, GUIDE_CANONICAL_NAMES, GUIDE_DOC_LINKS, GUIDE_PREDICATE_VOCABULARY, GUIDE_ROUTING_LINE, GUIDE_SURFACES, coerceGate, coerceLintIgnore, coerceSurface, memoryExtensionCatalogForDoc, memoryExtensionFieldCatalogHelp, overlayParam, parseRequestedExtensionChanges, applyExtensionChanges, literalBodySegment, serializeMemoryDocLiteral, selectorParam, requireLocalNameInStore, requireUnambiguousDoc, resolveReadSelector, } from './shared.js';
8
8
  /** Frontmatter fields an overlay flag cannot express removing. `kind` and the
9
9
  * routing line are the document's contract (a doc without them is malformed);
10
10
  * `origin` and `last-updated` are runtime provenance. */
11
- const CLEARABLE_FIELDS = ['short-form', 'gate', 'surfaces', 'unlisted', 'slash', 'rationale'];
11
+ const CLEARABLE_FIELDS = ['short-form', 'gate', 'surfaces', 'unlisted', 'slash', 'rationale', 'lint-ignore'];
12
12
  export const editLeaf = defineLeaf({
13
13
  name: 'edit',
14
14
  description: 'revise an existing memory document, recording why',
@@ -19,7 +19,7 @@ export const editLeaf = defineLeaf({
19
19
  guide: 'The body you pipe is saved byte for byte. Nothing between stdin and disk parses, normalizes, summarizes, or rewrites it \u2014 the frontmatter fence is prepended and the text is otherwise untouched. Relaying a person\u2019s own words is therefore safe: pass them through unchanged and mark them with --verbatim.\n\n' +
20
20
  'Two different rationales exist and they never share a flag. --rationale is why THIS REVISION is happening; it is required, it lands in the revision history, and it never touches the document text. --doc-rationale replaces the document\u2019s standing `rationale` frontmatter field \u2014 the observed gap the doc exists to close.\n\n' +
21
21
  'Omitting stdin leaves the body exactly as it stands, which is how a frontmatter-only revision is made. Piping a body REPLACES the whole body; there is no partial or append mode. Emptying a document is not an edit \u2014 that intent is `crtr memory delete`.\n\n' +
22
- 'Omitted frontmatter flags preserve their fields. --unset is the only way to REMOVE one, and it is limited to fields a doc can legitimately live without (short-form, gate, surfaces, unlisted, slash, rationale). --surface replaces the whole surfaces list rather than adding to it.\n\n' +
22
+ 'Omitted frontmatter flags preserve their fields. --unset is the only way to REMOVE one, and it is limited to fields a doc can legitimately live without (short-form, gate, surfaces, unlisted, slash, rationale, lint-ignore). --surface replaces the whole surfaces list rather than adding to it.\n\n' +
23
23
  'An edit that changes nothing fails. A rationale attached to zero change is noise in the trail the history exists to build \u2014 if the doc already says what you meant, there is nothing to record.\n\n' +
24
24
  '`last-updated` is stamped on every successful edit; `origin` is preserved, so it keeps pointing at the conversation that created the doc rather than the last one to touch it.\n\n' +
25
25
  GUIDE_ROUTING_LINE + '\n\n' +
@@ -38,6 +38,7 @@ export const editLeaf = defineLeaf({
38
38
  overlayParam('unlisted', {}, 'Presence SETS the field; remove it with `--unset unlisted`.'),
39
39
  overlayParam('surface'),
40
40
  overlayParam('gate'),
41
+ overlayParam('lint-ignore'),
41
42
  overlayParam('slash', {}, 'Presence SETS the field; remove it with `--unset slash`.'),
42
43
  { kind: 'flag', name: 'doc-rationale', type: 'string', required: false, constraint: DOC_RATIONALE_CONSTRAINT },
43
44
  { kind: 'flag', name: 'extension', type: 'string', required: false, repeatable: true, constraint: 'Set one declared plugin field as `extensions.<plugin>.<field>=VALUE`. The path is required in full; booleans accept only true or false, numbers require finite numeric syntax, and strings/enums preserve the literal text after the first =. Every requested field validates before the document is revised.' },
@@ -105,6 +106,7 @@ export const editLeaf = defineLeaf({
105
106
  if (doc.scope === 'builtin') {
106
107
  throw usage(`${doc.name} is a builtin document shipped with the package (read-only) \u2014 it cannot be edited. Override it with a same-named doc at a writable scope instead (\`crtr memory write ${doc.name} ...\`).`, { memory: doc.name, scope: 'builtin' });
107
108
  }
109
+ const lintIgnore = coerceLintIgnore(input['lintIgnore']);
108
110
  const extensionChanges = parseRequestedExtensionChanges(input['extension'], input['unsetExtension'], memoryExtensionCatalogForDoc(doc));
109
111
  const before = readText(doc.path);
110
112
  const parsed = parseFrontmatterGeneric(before);
@@ -146,6 +148,7 @@ export const editLeaf = defineLeaf({
146
148
  }
147
149
  if (input['gate'] !== undefined)
148
150
  frontmatter['gate'] = coerceGate(input['gate']);
151
+ setIf('lint-ignore', lintIgnore);
149
152
  if (input['slash'] === true)
150
153
  frontmatter['slash'] = true;
151
154
  setIf('rationale', input['docRationale']);
@@ -20,4 +20,8 @@ export interface ListedMemoryDoc {
20
20
  * point of naming a store is to see what it physically holds, including a pair
21
21
  * that shares one identity. */
22
22
  export declare function listMemoryDocs(kindFilter: string | undefined, docs: readonly MemoryDoc[], dedup: boolean): ListedMemoryDoc[];
23
+ /** The stable key used by both inventory ordering and cursor resumption. */
24
+ export declare function memoryListKey(doc: Pick<ListedMemoryDoc, 'scope' | 'kind' | 'name' | 'physicalRelativePath'>): string;
25
+ /** Locale-aware total order for memory inventory keys. */
26
+ export declare function compareMemoryListKeys(left: string, right: string): number;
23
27
  export declare const listLeaf: import("../../core/command.js").LeafDef;
@@ -46,20 +46,26 @@ export function listMemoryDocs(kindFilter, docs, dedup) {
46
46
  candidates: byName.get(doc.name) ?? [doc],
47
47
  });
48
48
  }
49
- // Physical path is the final tiebreaker so two rows of one identity keep a
50
- // stable order — the cursor key ends on the same component.
51
- items.sort((a, b) => {
52
- const sr = scopeRank(a.scope) - scopeRank(b.scope);
53
- if (sr !== 0)
54
- return sr;
55
- const kc = a.kind.localeCompare(b.kind);
56
- if (kc !== 0)
57
- return kc;
58
- const nc = a.name.localeCompare(b.name);
59
- return nc !== 0 ? nc : a.physicalRelativePath.localeCompare(b.physicalRelativePath);
60
- });
49
+ items.sort((a, b) => compareMemoryListKeys(memoryListKey(a), memoryListKey(b)));
61
50
  return items;
62
51
  }
52
+ /** The stable key used by both inventory ordering and cursor resumption. */
53
+ export function memoryListKey(doc) {
54
+ return `${String(scopeRank(doc.scope) + 10).padStart(2, '0')}:${doc.kind}:${doc.name}:${doc.physicalRelativePath}`;
55
+ }
56
+ /** Locale-aware total order for memory inventory keys. */
57
+ export function compareMemoryListKeys(left, right) {
58
+ const [leftScope, leftKind, leftName, ...leftPath] = left.split(':');
59
+ const [rightScope, rightKind, rightName, ...rightPath] = right.split(':');
60
+ const scope = Number(leftScope) - Number(rightScope);
61
+ if (scope !== 0)
62
+ return scope;
63
+ const kind = leftKind.localeCompare(rightKind);
64
+ if (kind !== 0)
65
+ return kind;
66
+ const name = leftName.localeCompare(rightName);
67
+ return name !== 0 ? name : leftPath.join(':').localeCompare(rightPath.join(':'));
68
+ }
63
69
  export const listLeaf = defineLeaf({
64
70
  name: 'list',
65
71
  description: 'inventory of stored memory documents',
@@ -95,8 +101,8 @@ export const listLeaf = defineLeaf({
95
101
  const items = selector.store !== null
96
102
  ? listMemoryDocs(kindFilter, loadMemoryStoreView(requireMountedStore(selector.store), false).docs, false)
97
103
  : listMemoryDocs(kindFilter, listAllMemoryDocs(selector.scope, false, true), true);
98
- const keyOf = (d) => `${String(scopeRank(d.scope) + 10).padStart(2, '0')}:${d.kind}:${d.name}:${d.physicalRelativePath}`;
99
- const result = paginate(items, { limit, cursor }, { defaultLimit: 50, maxLimit: 200, keyOf, total: 'count' });
104
+ const keyOf = memoryListKey;
105
+ const result = paginate(items, { limit, cursor }, { defaultLimit: 50, maxLimit: 200, keyOf, compareKeys: compareMemoryListKeys, total: 'count' });
100
106
  const output = {
101
107
  items: result.items.map((d) => ({
102
108
  name: d.name,
@@ -53,7 +53,7 @@ export const moveLeaf = defineLeaf({
53
53
  params: [
54
54
  { kind: 'positional', name: 'name', required: true, constraint: 'Full canonical name of the document to move — a project document is `<namespace>/<local name>`. Resolution is exact, as `read` resolves it: a file path, a bare leaf, a `<scope>/<name>` spelling, and a trailing `/INDEX` all fail. If two physical files in the selected store own the name, move selects the first path in stable lexical order and recovers the collision. Builtin and installed-plugin documents are read-only and refuse to move, as does the repository-root document, which declares the immutable namespace for every document in the repository; a nested store’s root guide is ordinary and may move.' },
55
55
  { kind: 'flag', name: 'to', type: 'string', required: true, constraint: 'The new full canonical name, in the SAME store — the repository namespace and store path included, since a move never crosses stores. `/`-joined segments of [A-Za-z0-9_-], no trailing `INDEX` (a directory’s document answers at the directory name). A nested store’s effective prefix is a legal destination for its optional root guide. Rejected when a document in that store already answers to it, or when either `<local name>.md` or `<local name>/INDEX.md` already exists there.' },
56
- selectorParam('scope-read', { choices: [...MEMORY_WRITE_SCOPES] }, 'builtin is not a choice — builtin documents ship with the package and cannot move.'),
56
+ selectorParam('scope-read', { choices: [...MEMORY_WRITE_SCOPES] }, 'Package-shipped documents cannot move through this writable-scope selector.'),
57
57
  selectorParam('dir', {}, 'Source, destination, and inbound-link rewriting all stay inside that one store, so moving a non-winning duplicate leaves every other store untouched — and rewrites no link at all, because the name still resolves to the candidate that outranks it here.'),
58
58
  ],
59
59
  output: [
@@ -185,6 +185,7 @@ export declare const SELECTOR_PARAMS: Record<string, FlagParam>;
185
185
  /** One selector flag, with leaf-specific prose appended to the shared
186
186
  * constraint and any leaf-specific schema override applied. */
187
187
  export declare function selectorParam(name: string, overrides?: Partial<FlagParam>, extraConstraint?: string): FlagParam;
188
+ export declare function coerceLintIgnore(raw: unknown): string | string[] | undefined;
188
189
  export declare const FRONTMATTER_OVERLAY_PARAMS: Record<string, FlagParam>;
189
190
  /** One overlay flag, with leaf-specific prose appended to the shared
190
191
  * constraint and any leaf-specific schema override applied. */
@@ -18,6 +18,7 @@ import { NEUTRAL_PROJECT_MEMORY, scopeMemoryDir, projectScopeRoot, ensureProject
18
18
  import { loadProfileManifest, resolveProfileOperand, profileMemoryDir } from '../../core/profiles/manifest.js';
19
19
  import { memoryDir as nodeMemoryDir } from '../../core/runtime/memory.js';
20
20
  import { SURFACE_EVENTS, SURFACE_RUNGS } from '../../core/substrate/schema.js';
21
+ import { SUPPRESSIBLE_RULES } from '../../core/substrate/frontmatter-validation.js';
21
22
  import { exposureTarget, loadContextExposureState, registerExposure, saveContextExposureState, } from '../../core/substrate/injected-store.js';
22
23
  // The two memory kinds — knowledge (consult: procedural playbooks + factual
23
24
  // references merged) vs preference (behave: standing directives). Used as the
@@ -463,6 +464,7 @@ const FRONTMATTER_ORDER = [
463
464
  'gate',
464
465
  'slash',
465
466
  'rationale',
467
+ 'lint-ignore',
466
468
  'last-updated',
467
469
  'origin',
468
470
  ];
@@ -734,7 +736,7 @@ export const SELECTOR_PARAMS = {
734
736
  type: 'enum',
735
737
  choices: [...MEMORY_WRITE_SCOPES],
736
738
  required: false,
737
- constraint: 'Store to write to (builtin documents ship with the package and installed-plugin documents are managed by `crtr pkg`, so neither is writable). Default: project when cwd sits in a project, else user. `project` resolves to the NEAREST ancestor `.crouter/` walking up from cwd — in a nested workspace that can be a parent’s store, not the dir you are standing in; pass --dir to pin the exact project. `profile` requires a selected profile (CRTR_PROFILE_ID) or an explicit --profile. `node` writes the this-node store (`nodes/<CRTR_NODE_ID>/context/memory/`), the nearest scope, seen only by this running node. Conflicts with --dir.',
739
+ constraint: 'Store to write to (package-shipped documents and installed-plugin documents are managed outside this command, so neither is writable). Default: project when cwd sits in a project, else user. `project` resolves to the NEAREST ancestor `.crouter/` walking up from cwd — in a nested workspace that can be a parent’s store, not the dir you are standing in; pass --dir to pin the exact project. `profile` requires a selected profile (CRTR_PROFILE_ID) or an explicit --profile. `node` writes the this-node store (`nodes/<CRTR_NODE_ID>/context/memory/`), the nearest scope, seen only by this running node. Conflicts with --dir.',
738
740
  },
739
741
  'dir': {
740
742
  kind: 'flag',
@@ -760,6 +762,15 @@ export function selectorParam(name, overrides = {}, extraConstraint) {
760
762
  const constraint = extraConstraint === undefined ? base.constraint : `${base.constraint} ${extraConstraint}`;
761
763
  return { ...base, ...overrides, constraint };
762
764
  }
765
+ export function coerceLintIgnore(raw) {
766
+ if (raw === undefined)
767
+ return undefined;
768
+ const rules = Array.isArray(raw) ? raw : [raw];
769
+ if (rules.length === 0 || rules.some((rule) => typeof rule !== 'string' || !SUPPRESSIBLE_RULES.includes(rule))) {
770
+ throw usage(`invalid lint-ignore: ${JSON.stringify(raw)} (suppressible rules: ${SUPPRESSIBLE_RULES.map((rule) => `\`${rule}\``).join(', ')})`);
771
+ }
772
+ return rules.length === 1 ? rules[0] : rules;
773
+ }
763
774
  export const FRONTMATTER_OVERLAY_PARAMS = {
764
775
  'kind': { kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: false, constraint: 'Document kind.' },
765
776
  'when-and-why-to-read': { kind: 'flag', name: 'when-and-why-to-read', type: 'string', required: false, constraint: 'ONE routing sentence: "When <circumstance>, this <kind> should be read because <broader downstream payoff>." WHY is the reader\u2019s payoff \u2014 the consequence they secure for their task by reading \u2014 NEVER the doc summary, its rule, or that rule reworded as an outcome (a benefit-shaped restatement still fails). Rendered verbatim as the preview.' },
@@ -767,6 +778,7 @@ export const FRONTMATTER_OVERLAY_PARAMS = {
767
778
  'unlisted': { kind: 'flag', name: 'unlisted', type: 'bool', required: false, default: false, constraint: 'Suppress this doc from directory listings. Suppression only — explicit reads, [[links]], and surfaces entries still work.' },
768
779
  'surface': { kind: 'flag', name: 'surface', type: 'string', required: false, repeatable: true, constraint: 'One routing entry per occurrence, as a YAML/JSON object `{on, at, match?, match-frontmatter?, gate?}`; the flag set replaces the document’s whole surfaces list. `on` is boot|workspace-open|read|memory-read|command|pre-command; `at` is name|preview|content. `match` holds the event’s globs — required on read/memory-read/command/pre-command (a read entry may carry `match-frontmatter`, a predicate over the read file’s own frontmatter, instead), meaningless on boot/workspace-open. Optional entry `gate` is a node-config predicate using the same vocabulary as the document gate; its event constraints and gate both match before it participates. A `./`-anchored glob is relative: for `read` to the store’s owning repo dir, for `memory-read` to this doc’s routing anchor — its own canonical name when the doc is its directory’s document (`<dir>/INDEX.md`), otherwise the canonical directory it sits in. Participating entries fold to their highest `at`; there is no cross-entry deny precedence.' },
769
780
  'gate': { kind: 'flag', name: 'gate', type: 'string', required: false, constraint: 'Frontmatter gate \u2014 YAML/JSON object predicate over node config using the same field/matcher vocabulary described in the guide.' },
781
+ 'lint-ignore': { kind: 'flag', name: 'lint-ignore', type: 'enum', choices: [...SUPPRESSIBLE_RULES], required: false, repeatable: true, constraint: 'Suppress one authoring lint rule for this document. Repeat for a list; valid rules: length, broad-memory-read.' },
770
782
  'slash': { kind: 'flag', name: 'slash', type: 'bool', required: false, default: false, constraint: 'Presence flags this doc invocable as a pi slash command (`/<name>`, `/` in a nested name rendered as `:`) \u2014 the doc body becomes the command\u2019s injected prompt. Default false: most docs are consulted, not invoked.' },
771
783
  };
772
784
  /** One overlay flag, with leaf-specific prose appended to the shared
@@ -11,7 +11,7 @@ import { parsedSubstrateSurfaces } from '../../core/substrate/frontmatter-valida
11
11
  import { ensureDir, realpathOrSelf, walkFiles, writeText } from '../../core/fs-utils.js';
12
12
  import { associateRepository } from '../../core/memory/repository-association.js';
13
13
  import { appendHistoryRecord, buildHistoryRecord, historyLogPathFor, } from '../../core/memory/history.js';
14
- import { DOC_RATIONALE_CONSTRAINT, GUIDE_CANONICAL_NAMES, GUIDE_DOC_LINKS, GUIDE_PREDICATE_VOCABULARY, GUIDE_ROUTING_LINE, GUIDE_SURFACES, planNewDocumentPlacement, resolveWriteSelector, requireMountedStore, buildOrigin, coerceGate, coerceSurface, memoryExtensionCatalogForDoc, memoryExtensionFieldCatalogHelp, overlayParam, requireCanonicalName, selectorParam, parseRequestedExtensionChanges, applyExtensionChanges, serializeMemoryDoc, } from './shared.js';
14
+ import { DOC_RATIONALE_CONSTRAINT, GUIDE_CANONICAL_NAMES, GUIDE_DOC_LINKS, GUIDE_PREDICATE_VOCABULARY, GUIDE_ROUTING_LINE, GUIDE_SURFACES, planNewDocumentPlacement, resolveWriteSelector, requireMountedStore, buildOrigin, coerceGate, coerceLintIgnore, coerceSurface, memoryExtensionCatalogForDoc, memoryExtensionFieldCatalogHelp, overlayParam, requireCanonicalName, selectorParam, parseRequestedExtensionChanges, applyExtensionChanges, serializeMemoryDoc, } from './shared.js';
15
15
  function emptyMemoryStore(memoryDir) {
16
16
  return walkFiles(memoryDir, (name) => name.endsWith('.md') && name !== 'SKILL.md', (name) => name === '.crouter').length === 0;
17
17
  }
@@ -60,6 +60,7 @@ export const writeLeaf = defineLeaf({
60
60
  overlayParam('unlisted'),
61
61
  overlayParam('surface'),
62
62
  overlayParam('gate'),
63
+ overlayParam('lint-ignore'),
63
64
  overlayParam('slash'),
64
65
  { kind: 'flag', name: 'rationale', type: 'string', required: false, constraint: DOC_RATIONALE_CONSTRAINT },
65
66
  { kind: 'flag', name: 'extension', type: 'string', required: false, repeatable: true, constraint: 'Set one declared plugin field as `extensions.<plugin>.<field>=VALUE`. The path is required in full; booleans accept only true or false, numbers require finite numeric syntax, and strings/enums preserve the literal text after the first =. Every requested field validates before the document is written.' },
@@ -117,6 +118,7 @@ export const writeLeaf = defineLeaf({
117
118
  canonicalName = placement.canonicalName;
118
119
  path = placement.path;
119
120
  }
121
+ const lintIgnore = coerceLintIgnore(input['lintIgnore']);
120
122
  const extensionChanges = parseRequestedExtensionChanges(input['extension'], undefined, memoryExtensionCatalogForDoc({ scope, path }));
121
123
  const frontmatter = {};
122
124
  frontmatter['kind'] = kind;
@@ -141,6 +143,7 @@ export const writeLeaf = defineLeaf({
141
143
  }
142
144
  if (input['gate'] !== undefined)
143
145
  frontmatter['gate'] = coerceGate(input['gate']);
146
+ setIf('lint-ignore', lintIgnore);
144
147
  if (input['slash'] === true)
145
148
  frontmatter['slash'] = true;
146
149
  setIf('rationale', input['rationale']);
@@ -82,10 +82,13 @@ const nodeList = defineLeaf({
82
82
  return false;
83
83
  return true;
84
84
  });
85
- const nodes = (await Promise.all(rows.map(async (row) => {
85
+ const nodes = [];
86
+ for (const row of rows) {
86
87
  const detail = await canvasSource.getNode(row.node_id).catch(rethrowAsCliError);
87
- return { ...row, name: fullName(detail ?? row) };
88
- }))).filter((row) => name === undefined || name === '' || fuzzyMatch(name, row.name));
88
+ const named = { ...row, name: fullName(detail ?? row) };
89
+ if (name === undefined || name === '' || fuzzyMatch(name, named.name))
90
+ nodes.push(named);
91
+ }
89
92
  return { nodes };
90
93
  },
91
94
  });
@@ -119,6 +122,7 @@ const nodeShow = defineLeaf({
119
122
  ],
120
123
  output: [
121
124
  { name: 'node', type: 'object', required: true, constraint: 'The full node meta (system prompt + every telemetry field). The render surfaces a glanceable slice; `--json` carries the complete dump.' },
125
+ { name: 'fault', type: 'object', required: true, constraint: 'The active runtime fault projection, or null: {link, kind, retry: {disposition}, since}.' },
122
126
  { name: 'reports', type: 'object[]', required: true, constraint: 'Who this node subscribes to (its reports/down), each enriched with {node_id, name, kind, status, active} — these are typically its spawned children.' },
123
127
  { name: 'managers', type: 'object[]', required: true, constraint: 'Who subscribes to this node (its managers/up), same enriched shape.' },
124
128
  { name: 'artifacts', type: 'object', required: true, constraint: 'Counts of what the node left on disk by corpus: {report, doc, roadmap}. Enumerate them with `node inspect artifacts <id>`.' },
@@ -186,7 +190,7 @@ const nodeShow = defineLeaf({
186
190
  if (reports.length > 0)
187
191
  hints.push(`Drill into a child/report: \`crtr node inspect show <node_id>\` (${reports.map((r) => r['node_id']).join(', ')}).`);
188
192
  hints.push(`Full machine dump (system prompt + all telemetry): \`crtr --json node inspect show ${id}\`.`);
189
- return { node, reports, managers, artifacts, pending_crons, paths, follow_up: hints.join('\n') };
193
+ return { node, fault: node.fault ?? null, reports, managers, artifacts, pending_crons, paths, follow_up: hints.join('\n') };
190
194
  },
191
195
  render: (r) => {
192
196
  const node = r['node'];
@@ -195,12 +199,14 @@ const nodeShow = defineLeaf({
195
199
  const isRemoteShow = remoteShowResults.has(r);
196
200
  const alive = isRemoteShow ? 'remote (liveness not checked locally)' : (isPidAlive(pid) ? 'alive' : 'dead');
197
201
  const intent = node['intent'] ? `/${String(node['intent'])}` : '';
202
+ const fault = r['fault'];
198
203
  const parts = [];
199
204
  // 1) Glanceable meta slice — the debug-critical fields, not the full dump.
200
205
  parts.push(`# ${fullName(node)} (${id})`);
201
206
  const meta = [
202
207
  `- kind/mode/lifecycle: ${node['kind']} / ${node['mode']} / ${node['lifecycle']}`,
203
208
  `- status: ${node['status']}${intent}`,
209
+ ...(fault === null || fault === undefined ? [] : [`- fault: ${String(fault['kind'])} / ${String(fault['retry']?.disposition)} since ${String(fault['since'])}`]),
204
210
  `- broker pid: ${pid ?? '(none)'} — ${alive}`,
205
211
  `- cwd: ${node['cwd']}`,
206
212
  `- host: ${node['host_kind']}` + (node['profile_id'] ? ` · profile ${node['profile_id']}` : ''),
@@ -291,6 +297,7 @@ export const nodeInspect = defineBranch({
291
297
  help: {
292
298
  name: 'node inspect',
293
299
  summary: 'read one node (or the roster) — topology, context dir, and artifacts',
300
+ model: 'Inspection is read-only and never revives a node. The roster is the selection surface; one-node metadata points to progressively heavier views: a parsed broker snapshot, rendered conversation, context browser, or artifact references.',
294
301
  },
295
302
  children: [nodeList, nodeShow, nodeSnapshotLeaf, nodeTranscriptLeaf, nodeContextLeaf, nodeArtifactsLeaf],
296
303
  });
@@ -355,6 +355,7 @@ export const nodeLifecycle = defineBranch({
355
355
  help: {
356
356
  name: 'node lifecycle',
357
357
  summary: 'end, transition, or revive a node — recycle the pane, close + cascade-cancel a subtree, demote to terminal in place, or revive a dormant node',
358
+ model: 'Lifecycle actions preserve or replace an existing node identity. Finishing is permanent, closing is revivable, demotion changes the completion obligation in place, and revival relaunches a dormant broker. Teardown cascades only through descendants exclusively managed by the affected subtree.',
358
359
  },
359
360
  children: [nodeRecycle, nodeClose, nodeDemote, nodeReviveLeaf],
360
361
  });
@@ -56,7 +56,7 @@ function messageRequestParams() {
56
56
  { kind: 'stdin', name: 'body', required: false, constraint: 'Visible message body. The schema alone is a complete request.' },
57
57
  { kind: 'flag', name: 'to', type: 'string', required: false, constraint: 'Target an existing node by id. Exactly one of --to or --self is required.' },
58
58
  { kind: 'flag', name: 'self', type: 'bool', required: false, constraint: 'Target the calling node. Exactly one of --to or --self is required.' },
59
- { kind: 'flag', name: 'tier', type: 'enum', choices: ['critical', 'urgent', 'normal'], required: false, default: 'normal', constraint: 'Delivery urgency. Deferred never wakes an idle node.' },
59
+ { kind: 'flag', name: 'tier', type: 'enum', choices: ['critical', 'urgent', 'normal'], required: false, default: 'normal', constraint: 'Delivery urgency. Every accepted tier may revive a dormant target, because the request must be answered; deferred is rejected for that reason.' },
60
60
  { kind: 'flag', name: 'output-schema', type: 'string', required: true, constraint: `JSON Schema the result must satisfy. ${OUTPUT_SCHEMA_TRANSPORT} The target answers through a constrained submit tool, then keeps working. ${OUTPUT_SCHEMA_CONSTRAINTS}` },
61
61
  { kind: 'flag', name: 'situational-context', type: 'string', required: false, constraint: 'Non-empty hidden ambient context delivered beside the message, or alone.' },
62
62
  { kind: 'flag', name: 'reopen', type: 'bool', required: false, hidden: true, constraint: 'Clear a finalized target latch before delivery or fresh revive.' },
@@ -105,7 +105,7 @@ async function runMessageEngine(input) {
105
105
  const nodeMessageSend = defineLeaf({
106
106
  name: 'send',
107
107
  description: 'deliver an inbox message to an existing node immediately',
108
- whenToUse: 'you need to steer or inform a node now without creating an ongoing subscription',
108
+ whenToUse: 'a node that already exists should have information or new direction now — steering a worker mid-task, handing over a finding it needs, correcting a wrong turn, or waking a dormant node with fresh work. The target reads it and decides what to do with it. Use `node message request` instead when you need an answer back in a shape you specify, and `node subscription add` when the target should keep receiving this node’s pushes rather than this one delivery',
109
109
  help: {
110
110
  name: 'node message send',
111
111
  summary: 'deliver an inbox message to an existing node immediately',
@@ -120,7 +120,7 @@ const nodeMessageSend = defineLeaf({
120
120
  const nodeMessageRequest = defineLeaf({
121
121
  name: 'request',
122
122
  description: 'deliver an immediate typed-output request to an existing node',
123
- whenToUse: 'the target must answer once through a submit tool constrained by a JSON Schema',
123
+ whenToUse: 'a node that already exists must hand back one answer in a shape you fix — a choice among options, a structured extraction, a verdict your own logic will branch on — or must be held to answering at all before it goes quiet. Use `node message send` instead when prose in the target’s own judgment is enough, and `node new --output-schema` when no node holds the context yet and the work needs a fresh agent',
124
124
  help: {
125
125
  name: 'node message request',
126
126
  summary: 'deliver an immediate typed-output request to an existing node',
@@ -139,6 +139,10 @@ export const nodeMessage = defineBranch({
139
139
  name: 'message',
140
140
  description: 'deliver immediate inbox work to an existing node',
141
141
  whenToUse: 'you need one immediate delivery: send ordinary information, or request one schema-constrained result',
142
- help: { name: 'node message', summary: 'deliver now; choose ordinary delivery or a typed-output obligation' },
142
+ help: {
143
+ name: 'node message',
144
+ summary: 'deliver one immediate inbox entry to an existing node',
145
+ model: 'Both children append one inbox entry to a node that already exists, targeted by id or --self, and may revive it if it is dormant. They differ in what the delivery obligates. An ordinary message obligates nothing: the target reads it and acts however it judges best. A request installs a one-off submit schema and holds the target awake and owing until it answers; the answer is validated against the schema, written to the target’s result file, and pushed as an update report to the TARGET’S subscribers, so subscribe to it first if the answer has to reach you. Neither child creates that standing relationship — a durable delivery edge is `node subscription`, and work that needs a fresh agent is `node new`.',
146
+ },
143
147
  children: [nodeMessageSend, nodeMessageRequest],
144
148
  });
@@ -69,6 +69,10 @@ export const nodeSubscription = defineBranch({
69
69
  name: 'subscription',
70
70
  description: 'manage push-delivery edges between nodes',
71
71
  whenToUse: 'you need an ongoing active or passive subscriber-to-publisher delivery edge that spawn did not create, or need to remove one',
72
- help: { name: 'node subscription', summary: 'add or remove subscriber-to-publisher push-delivery edges' },
72
+ help: {
73
+ name: 'node subscription',
74
+ summary: 'add or remove subscriber-to-publisher push-delivery edges',
75
+ model: 'Subscriptions point from a subscriber to a publisher and control future push delivery. Active edges wake on each push; passive edges accumulate pushes for the subscriber’s next turn. Removing either edge stops subsequent delivery without changing messages already received.',
76
+ },
73
77
  children: [nodeSubscribe, nodeUnsubscribe],
74
78
  });
@@ -154,6 +154,10 @@ export const nodeWaitBranch = defineBranch({
154
154
  name: 'wait',
155
155
  description: 'declare a durable controller or deadline wait',
156
156
  whenToUse: 'a terminal node must dorm for one named controller, or race its inbox against a self-scheduled deadline',
157
- help: { name: 'node wait', summary: 'declare durable wait state; choose the event that settles it' },
157
+ help: {
158
+ name: 'node wait',
159
+ summary: 'declare durable wait state; choose the event that settles it',
160
+ model: 'Both waits belong to the calling terminal node and settle on one future event. A controller wait records an existing node as the wake source; a deadline wait arms an urgent one-shot cron that races any earlier inbox delivery and is canceled when that delivery wins.',
161
+ },
158
162
  children: [nodeWait, nodeWaitDeadline],
159
163
  });
@@ -791,7 +791,7 @@ export const pluginRemove = defineLeaf({
791
791
  summary: 'remove a plugin and its directory from the given scope',
792
792
  params: [
793
793
  { kind: 'positional', name: 'name', type: 'string', required: true, constraint: 'Plugin name to remove.' },
794
- { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: searches all scopes.' },
794
+ { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: searches every scope.' },
795
795
  ],
796
796
  output: [
797
797
  { name: 'scopes', type: 'string[]', required: true, constraint: 'Scopes the plugin was removed from.' },
@@ -14,6 +14,7 @@ export function registerPkg() {
14
14
  help: {
15
15
  name: 'pkg',
16
16
  summary: 'manage plugins and marketplace sources',
17
+ model: 'Marketplaces are configured source catalogs; plugins are installed extensions resolved from those catalogs or direct HTTP endpoints. Browsing searches the configured catalogs without changing source or install state.',
17
18
  },
18
19
  children: [pkgBrowseLeaf, pluginBranch, marketBranch],
19
20
  });
@@ -3,7 +3,7 @@ import { resolveProfileOperand, renameProfile } from '../../core/profiles/manife
3
3
  export const renameLeaf = defineLeaf({
4
4
  name: 'rename',
5
5
  description: "change a profile's display name",
6
- whenToUse: "you want to change a profile's display name — the gateable `profile` subject value and the name shown in bearings/listings. The profile id (its directory) never changes.",
6
+ whenToUse: "you want the profile to appear under a new name in bearings, listings, and `profile` subject gates. The profile id (its directory) never changes.",
7
7
  help: {
8
8
  name: 'profile rename',
9
9
  summary: "change a profile's manifest `name` only — the profile_id is stable and never changes",
@@ -257,6 +257,7 @@ export const configBranch = defineBranch({
257
257
  help: {
258
258
  name: 'sys config',
259
259
  summary: 'read and write crtr configuration, including model ladders; edit humanActions maps directly in ~/.crouter/config.json or <repo>/.crouter/config.json',
260
+ model: 'Configuration resolves through ordered scopes. Reads can resolve the effective value, writes target one writable scope, and path inspection reveals the backing files without reading values.',
260
261
  },
261
262
  children: [configGet, configSet, configPath],
262
263
  });
@@ -24,6 +24,7 @@ export function registerSys() {
24
24
  help: {
25
25
  name: 'sys',
26
26
  summary: 'crtr system configuration, settings, diagnostics, and self-management',
27
+ model: 'Configuration and settings own durable behavior; diagnostics and support observe it. Setup, migration, sync, and update change installed or generated state, while daemon control owns the supervisor lifecycle.',
27
28
  },
28
29
  children: [sysSettingsLeaf, configBranch, sysSetupLeaf, sysDoctorLeaf, sysSyspromptLeaf, promptReviewLeaf, sysFeedbackLeaf, sysLogsLeaf, supportBranch, sysUpdateLeaf, sysVersionLeaf, sysMigrateLeaf, sysSyncBranch, daemonBranch],
29
30
  });
@@ -0,0 +1 @@
1
+ export {};