@north-light/crouter 0.3.216 → 0.3.218

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 (70) hide show
  1. package/dist/api/client.d.ts +5 -2
  2. package/dist/api/client.js +7 -1
  3. package/dist/api/dto/canvas.d.ts +9 -0
  4. package/dist/api/dto/nodes.d.ts +27 -1
  5. package/dist/builtin-memory/04-base-worker.md +7 -1
  6. package/dist/builtin-memory/internal/plugins.md +67 -0
  7. package/dist/builtin-memory/memory-read-orientation.md +13 -0
  8. package/dist/clients/attach/chrome/bash-jobs.js +1 -1
  9. package/dist/clients/attach/photon_rs_bg.wasm +0 -0
  10. package/dist/clients/attach/viewer.js +516 -516
  11. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +2 -2
  12. package/dist/clients/inbox/tui/input.js +12 -8
  13. package/dist/clients/inbox/tui/slots.d.ts +3 -1
  14. package/dist/clients/inbox/tui/slots.js +9 -3
  15. package/dist/commands/human/shared.js +1 -1
  16. package/dist/commands/memory/edit.js +6 -1
  17. package/dist/commands/memory/lint.d.ts +1 -6
  18. package/dist/commands/memory/lint.js +18 -126
  19. package/dist/commands/memory/list.d.ts +1 -0
  20. package/dist/commands/memory/list.js +17 -2
  21. package/dist/commands/memory/read.js +15 -6
  22. package/dist/commands/memory/shared.d.ts +33 -3
  23. package/dist/commands/memory/shared.js +180 -1
  24. package/dist/commands/memory/write.js +6 -1
  25. package/dist/commands/pkg/browse/catalog.js +2 -0
  26. package/dist/commands/pkg/browse/model.d.ts +4 -1
  27. package/dist/commands/pkg/plugin-manage.js +209 -148
  28. package/dist/core/__tests__/human-cancel-guard.test.js +30 -12
  29. package/dist/core/bash-jobs.d.ts +2 -5
  30. package/dist/core/bash-jobs.js +4 -8
  31. package/dist/core/command-plugins/bundle.d.ts +4 -1
  32. package/dist/core/command-plugins/bundle.js +16 -2
  33. package/dist/core/human/__tests__/page.test.js +12 -0
  34. package/dist/core/human/component-docs.js +6 -1
  35. package/dist/core/human/page-schema.d.ts +14 -0
  36. package/dist/core/human/page-schema.js +24 -7
  37. package/dist/core/human/page.d.ts +2 -0
  38. package/dist/core/human/page.js +17 -6
  39. package/dist/core/human/scan.d.ts +5 -4
  40. package/dist/core/human/scan.js +7 -4
  41. package/dist/core/human/tickets.js +9 -2
  42. package/dist/core/io.js +3 -2
  43. package/dist/core/manifest.d.ts +2 -0
  44. package/dist/core/manifest.js +5 -0
  45. package/dist/core/memory/extensions.d.ts +30 -0
  46. package/dist/core/memory/extensions.js +219 -0
  47. package/dist/core/preview-result-path.d.ts +4 -0
  48. package/dist/core/preview-result-path.js +25 -0
  49. package/dist/core/substrate/frontmatter-validation.d.ts +13 -0
  50. package/dist/core/substrate/frontmatter-validation.js +101 -0
  51. package/dist/core/substrate/index.d.ts +1 -0
  52. package/dist/core/substrate/index.js +1 -0
  53. package/dist/daemon/api/__tests__/nodes-activity-query.test.d.ts +1 -0
  54. package/dist/daemon/api/__tests__/nodes-activity-query.test.js +101 -0
  55. package/dist/daemon/api/handlers/canvas.js +3 -0
  56. package/dist/daemon/api/handlers/inbox.js +4 -3
  57. package/dist/daemon/api/handlers/nodes.d.ts +1 -0
  58. package/dist/daemon/api/handlers/nodes.js +101 -19
  59. package/dist/daemon/api/handlers/reports.d.ts +4 -0
  60. package/dist/daemon/api/handlers/reports.js +14 -8
  61. package/dist/daemon/crtrd.js +7 -5
  62. package/dist/daemon/manage.d.ts +3 -0
  63. package/dist/daemon/manage.js +14 -0
  64. package/dist/pi-extensions/canvas-bash-valve.d.ts +4 -3
  65. package/dist/pi-extensions/canvas-bash-valve.js +19 -6
  66. package/dist/pi-extensions/canvas-preview-result.d.ts +0 -8
  67. package/dist/pi-extensions/canvas-preview-result.js +9 -23
  68. package/dist/types.d.ts +31 -0
  69. package/package.json +1 -1
  70. package/runtime.lock.json +2 -2
@@ -6,7 +6,9 @@
6
6
  import { existsSync, readdirSync, realpathSync, rmdirSync, statSync } from 'node:fs';
7
7
  import { dirname, join, resolve as resolvePath } from 'node:path';
8
8
  import { stringify as yamlStringify, parse as yamlParse } from 'yaml';
9
+ import { stateBlock } from '../../core/help.js';
9
10
  import { usage } from '../../core/errors.js';
11
+ import { memoryExtensionValidationCatalog, } from '../../core/memory/extensions.js';
10
12
  import { CRTR_DIR_NAME } from '../../types.js';
11
13
  import { scopeMemoryDir, projectScopeRoot, ensureProjectScopeRoot, resetScopeCache, } from '../../core/scope.js';
12
14
  import { loadProfileManifest, profileMemoryDir } from '../../core/profiles/manifest.js';
@@ -309,6 +311,183 @@ export function serializeMemoryDocLiteral(frontmatter, bodySegment) {
309
311
  const yamlText = yamlStringify(ordered).replace(/\n+$/, '');
310
312
  return `---\n${yamlText}\n---\n${bodySegment}`;
311
313
  }
314
+ /** Resolve declarations at the same scope and path a document will use. The
315
+ * catalog intentionally includes disabled plugins: they still authorize typed
316
+ * edits to values that stay inert until their plugin is enabled again. */
317
+ export function memoryExtensionCatalogForDoc(doc) {
318
+ return memoryExtensionValidationCatalog(doc);
319
+ }
320
+ function declarationValueDescription(declaration) {
321
+ if (declaration.type !== 'enum')
322
+ return declaration.type;
323
+ return `enum (${declaration.values.join('|')})`;
324
+ }
325
+ function extensionHelpValue(value) {
326
+ return typeof value === 'string' ? JSON.stringify(value) : String(value);
327
+ }
328
+ /** A bounded declaration catalog for the two authoring leaves. The manifest is
329
+ * the source for both the type and operation-specific guidance. */
330
+ export function memoryExtensionFieldCatalogHelp(operation) {
331
+ const scope = projectScopeRoot() === null ? 'user' : 'project';
332
+ const memoryDir = scopeMemoryDir(scope);
333
+ if (memoryDir === null)
334
+ return null;
335
+ const catalog = memoryExtensionCatalogForDoc({ scope, path: join(memoryDir, '.extension-help.md') });
336
+ const fields = Object.entries(catalog)
337
+ .flatMap(([namespace, declarations]) => Object.entries(declarations).map(([field, declaration]) => ({ namespace, field, declaration })))
338
+ .sort((left, right) => `${left.namespace}.${left.field}`.localeCompare(`${right.namespace}.${right.field}`));
339
+ if (fields.length === 0)
340
+ return null;
341
+ const maxRows = 50;
342
+ const rows = fields.slice(0, maxRows).map(({ namespace, field, declaration }) => {
343
+ const defaultValue = declaration.default === undefined ? '' : `; default ${extensionHelpValue(declaration.default)}`;
344
+ const help = operation === 'write' ? declaration.write_help : declaration.edit_help;
345
+ return `- \`extensions.${namespace}.${field}\` — ${declarationValueDescription(declaration)}${defaultValue}. ${help}`;
346
+ });
347
+ if (fields.length > maxRows)
348
+ rows.push(`- ${fields.length - maxRows} additional declared fields omitted from help.`);
349
+ return stateBlock('memory-extension-fields', { count: fields.length }, rows.join('\n'));
350
+ }
351
+ function parseExtensionPath(raw, flag) {
352
+ const match = /^extensions\.([^.]+)\.([^.]+)$/.exec(raw);
353
+ if (match === null)
354
+ return `${flag} requires the full path extensions.<plugin>.<field>: ${raw}`;
355
+ return { path: raw, namespace: match[1], field: match[2] };
356
+ }
357
+ function parseExtensionValue(path, raw, declaration) {
358
+ if (declaration.type === 'boolean') {
359
+ if (raw === 'true')
360
+ return true;
361
+ if (raw === 'false')
362
+ return false;
363
+ return `${path} expects boolean true or false, received ${JSON.stringify(raw)}`;
364
+ }
365
+ if (declaration.type === 'number') {
366
+ if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(raw)) {
367
+ return `${path} expects finite numeric syntax, received ${JSON.stringify(raw)}`;
368
+ }
369
+ const value = Number(raw);
370
+ return Number.isFinite(value) ? value : `${path} expects a finite number, received ${JSON.stringify(raw)}`;
371
+ }
372
+ if (declaration.type === 'enum' && !declaration.values.includes(raw)) {
373
+ return `${path} expects one of ${declaration.values.join('|')}, received ${JSON.stringify(raw)}`;
374
+ }
375
+ return raw;
376
+ }
377
+ /** Parse every requested extension mutation before the caller constructs any
378
+ * frontmatter. Set values are deliberately declaration-typed rather than YAML. */
379
+ export function parseRequestedExtensionChanges(setRaw, unsetRaw, catalog) {
380
+ const errors = [];
381
+ const sets = [];
382
+ const unsets = [];
383
+ const setPaths = new Set();
384
+ const unsetPaths = new Set();
385
+ for (const raw of setRaw ?? []) {
386
+ const delimiter = raw.indexOf('=');
387
+ if (delimiter < 0) {
388
+ errors.push(`--extension requires extensions.<plugin>.<field>=VALUE: ${raw}`);
389
+ continue;
390
+ }
391
+ const parsedPath = parseExtensionPath(raw.slice(0, delimiter), '--extension');
392
+ if (typeof parsedPath === 'string') {
393
+ errors.push(parsedPath);
394
+ continue;
395
+ }
396
+ if (setPaths.has(parsedPath.path)) {
397
+ errors.push(`duplicate --extension path: ${parsedPath.path}`);
398
+ continue;
399
+ }
400
+ setPaths.add(parsedPath.path);
401
+ const declaration = catalog[parsedPath.namespace]?.[parsedPath.field];
402
+ if (declaration === undefined) {
403
+ errors.push(`${parsedPath.path} is not declared by an installed plugin`);
404
+ continue;
405
+ }
406
+ const value = parseExtensionValue(parsedPath.path, raw.slice(delimiter + 1), declaration);
407
+ if (typeof value === 'string' && declaration.type !== 'string' && declaration.type !== 'enum') {
408
+ errors.push(value);
409
+ continue;
410
+ }
411
+ if (typeof value === 'string' && declaration.type === 'enum' && !declaration.values.includes(value)) {
412
+ errors.push(`${parsedPath.path} expects one of ${declaration.values.join('|')}, received ${JSON.stringify(value)}`);
413
+ continue;
414
+ }
415
+ sets.push({ ...parsedPath, value });
416
+ }
417
+ for (const raw of unsetRaw ?? []) {
418
+ const parsedPath = parseExtensionPath(raw, '--unset-extension');
419
+ if (typeof parsedPath === 'string') {
420
+ errors.push(parsedPath);
421
+ continue;
422
+ }
423
+ if (unsetPaths.has(parsedPath.path)) {
424
+ errors.push(`duplicate --unset-extension path: ${parsedPath.path}`);
425
+ continue;
426
+ }
427
+ unsetPaths.add(parsedPath.path);
428
+ if (catalog[parsedPath.namespace]?.[parsedPath.field] === undefined) {
429
+ errors.push(`${parsedPath.path} is not declared by an installed plugin`);
430
+ continue;
431
+ }
432
+ unsets.push(parsedPath);
433
+ }
434
+ for (const path of setPaths) {
435
+ if (unsetPaths.has(path))
436
+ errors.push(`${path} was requested in both --extension and --unset-extension`);
437
+ }
438
+ if (errors.length > 0) {
439
+ throw usage(`invalid plugin memory extension request:\n- ${errors.join('\n- ')}`, {
440
+ received: [...(setRaw ?? []), ...(unsetRaw ?? [])].join(', '),
441
+ next: 'Run `crtr memory write -h` or `crtr memory edit -h` for the installed field catalog and value syntax.',
442
+ });
443
+ }
444
+ return { sets, unsets };
445
+ }
446
+ /** Apply only requested extension paths. Existing unknown namespaces remain
447
+ * untouched; an unset prunes the empty namespace and empty root mapping. */
448
+ export function applyExtensionChanges(frontmatter, sets, unsets) {
449
+ if (sets.length === 0 && unsets.length === 0)
450
+ return;
451
+ const current = frontmatter['extensions'];
452
+ if (current !== undefined && (current === null || typeof current !== 'object' || Array.isArray(current))) {
453
+ throw usage('existing extensions frontmatter must be a mapping before extension fields can be changed', {
454
+ received: JSON.stringify(current),
455
+ next: 'Repair the extensions mapping, or make a core-field-only edit that preserves it unchanged.',
456
+ });
457
+ }
458
+ const extensions = { ...(current ?? {}) };
459
+ for (const set of sets) {
460
+ const existing = extensions[set.namespace];
461
+ if (existing !== undefined && (existing === null || typeof existing !== 'object' || Array.isArray(existing))) {
462
+ throw usage(`existing ${`extensions.${set.namespace}`} frontmatter must be a mapping before ${set.path} can change`, {
463
+ received: JSON.stringify(existing),
464
+ next: 'Repair the namespace mapping, or make a core-field-only edit that preserves it unchanged.',
465
+ });
466
+ }
467
+ extensions[set.namespace] = { ...(existing ?? {}), [set.field]: set.value };
468
+ }
469
+ for (const unset of unsets) {
470
+ const existing = extensions[unset.namespace];
471
+ if (existing === undefined)
472
+ continue;
473
+ if (existing === null || typeof existing !== 'object' || Array.isArray(existing)) {
474
+ throw usage(`existing ${`extensions.${unset.namespace}`} frontmatter must be a mapping before ${unset.path} can change`, {
475
+ received: JSON.stringify(existing),
476
+ next: 'Repair the namespace mapping, or make a core-field-only edit that preserves it unchanged.',
477
+ });
478
+ }
479
+ const fields = { ...existing };
480
+ delete fields[unset.field];
481
+ if (Object.keys(fields).length === 0)
482
+ delete extensions[unset.namespace];
483
+ else
484
+ extensions[unset.namespace] = fields;
485
+ }
486
+ if (Object.keys(extensions).length === 0)
487
+ delete frontmatter['extensions'];
488
+ else
489
+ frontmatter['extensions'] = extensions;
490
+ }
312
491
  // ---------------------------------------------------------------------------
313
492
  // Help material shared by `write` and `edit`
314
493
  //
@@ -343,6 +522,6 @@ export function overlayParam(name, overrides = {}, extraConstraint) {
343
522
  * happening. Same field, same prose, two flag names that cannot be confused. */
344
523
  export const DOC_RATIONALE_CONSTRAINT = 'Frontmatter rationale \u2014 the observed agent failure that made this doc necessary. Maintainer-facing only: never ships in any delivered surface (boot render, on-read injection, `memory read` content), visible only via `memory read --frontmatter`. Omitting the flag preserves an existing rationale unchanged.';
345
524
  export const GUIDE_SURFACES = 'Every doc appears in its directory’s listing by default (suppress with --unlisted); everything beyond the listing is explicit `surfaces` routing — entries declaring WHEN the doc delivers and how much. Events: `boot` delivers in the boot catalog every agent sees; `workspace-open` delivers in first-message context when cwd/profile mounts the doc’s project store (project stores only); `read` fires when the agent reads a matching file (globs vs the file’s absolute path and basename, `./`-anchored globs vs its path relative to the store’s owning repo dir, `match-frontmatter` predicates over the read file’s own YAML frontmatter); `memory-read` fires when another memory doc is read (globs vs that doc’s canonical name, `./` anchored to this doc’s own name directory); `command` fires when a matching shell command runs (globs vs the whole command string, `*` crossing `/`). `at` sets how much delivers: `name` the bare tag, `preview` the routing line, `content` the whole body. Each entry is paid by every future agent it fires on, so default down: no surfaces at all is correct for reference depth reached through listings and [[links]], and when a doc fits in a single sentence, deliver `content` directly — a `preview` routing line would run longer than the doc itself. Sentence-length `content` docs are correct; never pad a memory to be more verbose than the rule or fact it carries.';
346
- export const GUIDE_ROUTING_LINE = 'The routing line (--when-and-why-to-read) is the only text an agent reads before deciding to load the doc. The test for its because-clause: if it can be derived by paraphrasing the doc\u2019s advice, it is a restatement, not a payoff \u2014 a real payoff names a consequence in the reader\u2019s world that the document itself never asserts. Bad: "because only genuine first principles belong in taste memory." Bad: "because keeping the test loop fast and free of speculative tests protects the development pace" \u2014 the doc\u2019s rule as an outcome, derivable straight from its advice. Good: "because identifying throughlines in user taste lets future decisions be made faster and with less re-litigation." Someone mid-task who has not read the doc must be able to decide from this line alone whether the read is worth it. If you cannot name the concrete situation that triggers it, you do not yet understand the memory \u2014 ask the user one sharp question instead of improvising.';
525
+ export const GUIDE_ROUTING_LINE = 'The routing line (--when-and-why-to-read) is the only text an agent reads before deciding to load the doc. The test for its when-clause: it names an observable circumstance in the reader\u2019s current work, not the doc\u2019s topic reworded as an activity. If the WHEN can be inferred from the title alone, it is not a real trigger. Bad on a todo list: "When planning or prioritizing work across this profile." Good: "When the user mentions something from their todos, or asks what is still outstanding across this profile." The test for its because-clause: if it can be derived by paraphrasing the doc\u2019s advice, it is a restatement, not a payoff \u2014 a real payoff names a consequence in the reader\u2019s world that the document itself never asserts. Bad: "because only genuine first principles belong in taste memory." Bad: "because keeping the test loop fast and free of speculative tests protects the development pace" \u2014 the doc\u2019s rule as an outcome, derivable straight from its advice. Good: "because identifying throughlines in user taste lets future decisions be made faster and with less re-litigation." Someone mid-task who has not read the doc must be able to decide from this line alone whether the read is worth it. If you cannot name the concrete situation that triggers it, you do not yet understand the memory \u2014 ask the user one sharp question instead of improvising.';
347
526
  export const GUIDE_PREDICATE_VOCABULARY = 'Gate and match-frontmatter share the same predicate language: a field map is AND-ed across fields; dotted fields resolve nested values; field matchers may be scalar, array, or object. Scalar matchers do exact equality, with arrays matching any element. Array matchers do membership or intersection. Object matchers accept `eq`, `ne`, `in`, `nin`, `exists`, `contains`, `containsAll`, `containsAny`, `matches`, `imatches`, `gt`, `gte`, `lt`, and `lte`. Combinators are `all`, `any`, and `not`; sibling field matchers next to them are AND-ed in. An empty condition is inert. An unknown op never matches.';
348
527
  export const GUIDE_DOC_LINKS = 'Link related docs with `[[canonical/name]]`. A doc link in any body is a first-class cross-reference to another memory document by its exact canonical name \u2014 the same identifier `crtr memory read` takes. A bare directory name is a valid link too: following it returns that directory\u2019s listing, a browse entrance rather than a doc. Links are pointers, never transclusion: the linked body is loaded only when a reader follows it, so a link costs nothing until needed. `crtr memory lint` fails on a link that resolves to no document or directory, and `crtr memory read` lists a doc\u2019s resolvable links alongside its body. There is no alias or label form.';
@@ -2,7 +2,7 @@ import { defineLeaf } from '../../core/command.js';
2
2
  import { usage } from '../../core/errors.js';
3
3
  import { writeText, pathExists } from '../../core/fs-utils.js';
4
4
  import { appendHistoryRecord, buildHistoryRecord, historyLogPathFor, } from '../../core/memory/history.js';
5
- import { DOC_RATIONALE_CONSTRAINT, GUIDE_DOC_LINKS, GUIDE_PREDICATE_VOCABULARY, GUIDE_ROUTING_LINE, GUIDE_SURFACES, MEMORY_SCOPES, resolveWriteTarget, memoryFilePath, buildOrigin, coerceGate, coerceSurface, overlayParam, serializeMemoryDoc, } from './shared.js';
5
+ import { DOC_RATIONALE_CONSTRAINT, GUIDE_DOC_LINKS, GUIDE_PREDICATE_VOCABULARY, GUIDE_ROUTING_LINE, GUIDE_SURFACES, MEMORY_SCOPES, resolveWriteTarget, memoryFilePath, buildOrigin, coerceGate, coerceSurface, memoryExtensionCatalogForDoc, memoryExtensionFieldCatalogHelp, overlayParam, parseRequestedExtensionChanges, applyExtensionChanges, serializeMemoryDoc, } from './shared.js';
6
6
  // A topic's memory set stays usable only when authors treat cohesion as a creation
7
7
  // gate; otherwise small corrections accumulate as overlapping leaves whose routing
8
8
  // lines cannot tell a future reader which one owns the topic.
@@ -38,6 +38,7 @@ export const writeLeaf = defineLeaf({
38
38
  overlayParam('gate'),
39
39
  overlayParam('slash'),
40
40
  { kind: 'flag', name: 'rationale', type: 'string', required: false, constraint: DOC_RATIONALE_CONSTRAINT },
41
+ { 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.' },
41
42
  { kind: 'flag', name: 'scope', type: 'enum', choices: [...MEMORY_SCOPES], required: false, constraint: 'Target scope. Default: project when inside 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 directory. `profile` requires a selected profile (CRTR_PROFILE_ID) or an explicit --profile. `node` writes to the this-node store (`nodes/<CRTR_NODE_ID>/context/memory/`) — the nearest scope, seen only by this running node, requires a node context.' },
42
43
  { kind: 'flag', name: 'dir', type: 'string', required: false, constraint: 'Exact project directory to write under — targets `<dir>/.crouter/memory/` regardless of cwd or ancestor stores, scaffolding `.crouter/` there if absent. THE way to place a doc in a specific project’s store (e.g. another project in your profile’s purview) without cd’ing there, and the only way to target a dir shadowed by an ancestor store. Implies --scope project; rejects --scope user/profile.' },
43
44
  { kind: 'flag', name: 'profile', type: 'string', required: false, constraint: 'Profile id or name to write under, for --scope profile. Default: the process CRTR_PROFILE_ID (the node\u2019s selected profile). Resolved through the same profile lookup as `crtr profile show`. Rejected (usage error) unless --scope profile is also selected \u2014 never silently ignored for another scope.' },
@@ -54,6 +55,7 @@ export const writeLeaf = defineLeaf({
54
55
  { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands — read it back, revise it, or list the inventory.' },
55
56
  ],
56
57
  outputKind: 'object',
58
+ dynamicState: () => memoryExtensionFieldCatalogHelp('write'),
57
59
  effects: [
58
60
  'Creates memory/<name>.md at the resolved scope with the given frontmatter fields + stdin body, and opens memory/.history/<name>.jsonl with a `create` record.',
59
61
  ],
@@ -67,6 +69,7 @@ export const writeLeaf = defineLeaf({
67
69
  const dirArg = input['dir'];
68
70
  const { scope, memoryDir } = resolveWriteTarget(scopeArg, profileArg, dirArg);
69
71
  const path = memoryFilePath(memoryDir, name);
72
+ const extensionChanges = parseRequestedExtensionChanges(input['extension'], undefined, memoryExtensionCatalogForDoc({ scope, path }));
70
73
  // Create-only. Revision goes through `edit`, which requires a rationale and
71
74
  // records the change — a write that still overwrote would be a standing
72
75
  // bypass of the audit trail. The error is the migration path.
@@ -97,6 +100,7 @@ export const writeLeaf = defineLeaf({
97
100
  if (input['slash'] === true)
98
101
  frontmatter['slash'] = true;
99
102
  setIf('rationale', input['rationale']);
103
+ applyExtensionChanges(frontmatter, extensionChanges.sets, extensionChanges.unsets);
100
104
  // One field consumers read for recency, live from the first byte.
101
105
  frontmatter['last-updated'] = origin['created'];
102
106
  // A workspace-open entry fires only when cwd/profile mounts the doc's
@@ -124,6 +128,7 @@ export const writeLeaf = defineLeaf({
124
128
  ...(input['gate'] !== undefined ? ['gate'] : []),
125
129
  ...(input['slash'] === true ? ['slash'] : []),
126
130
  ...(input['rationale'] !== undefined ? ['rationale'] : []),
131
+ ...(extensionChanges.sets.length > 0 ? ['extensions'] : []),
127
132
  ];
128
133
  const selectedFrontmatter = selectedKeys.map((key) => ({ key, value: frontmatter[key] }));
129
134
  return {
@@ -8,6 +8,7 @@ import { qualifiedTopLevelCommandName } from "../../../core/command-manifests/re
8
8
  import { listDirs, pathExists, walkFiles } from "../../../core/fs-utils.js";
9
9
  import { currentSha, gitSync } from "../../../core/git.js";
10
10
  import { listPluginMemoryDocs } from "../../../core/memory-resolver.js";
11
+ import { memoryExtensionEffectiveCatalog, projectEffectiveMemoryExtensions } from "../../../core/memory/extensions.js";
11
12
  import { readMarketplaceManifest, readPluginManifest } from "../../../core/manifest.js";
12
13
  import { readConfig } from "../../../core/config.js";
13
14
  import { parseSubstrateDoc } from "../../../core/substrate/schema.js";
@@ -365,6 +366,7 @@ function docDetails(plugin) {
365
366
  slash: parsed.slash,
366
367
  ...(parsed.gate !== undefined ? { gate: parsed.gate } : {}),
367
368
  ...(parsed.rationale !== undefined ? { rationale: parsed.rationale } : {}),
369
+ extensions: projectEffectiveMemoryExtensions(doc.frontmatter?.["extensions"], memoryExtensionEffectiveCatalog(doc)),
368
370
  body: parsed.body,
369
371
  }];
370
372
  });
@@ -1,6 +1,6 @@
1
1
  import type { Field, InputParam } from "../../../core/help.js";
2
2
  import type { SurfaceEntry } from "../../../core/substrate/schema.js";
3
- import type { OwnerRef, Scope } from "../../../types.js";
3
+ import type { MemoryExtensionScalar, OwnerRef, Scope } from "../../../types.js";
4
4
  /** The scopes that can hold a marketplace, and that a plugin can be installed
5
5
  * into. Narrower than `Scope`: `builtin` names plugins that ship with crtr, and
6
6
  * `listAllMarketplaces()` never yields it, so a builtin marketplace or a
@@ -100,6 +100,9 @@ export interface DocDetail {
100
100
  /** Frontmatter `rationale`: the observed failure the doc exists to close.
101
101
  * Maintainer-facing — never delivered to a reading agent. */
102
102
  rationale?: string;
103
+ /** Effective valid plugin-owned metadata. Enabled declaration defaults appear
104
+ * here without materializing in the memory document. */
105
+ extensions: Record<string, Record<string, MemoryExtensionScalar>>;
103
106
  /** Markdown body, frontmatter stripped. */
104
107
  body: string;
105
108
  }