@crouton-kit/crouter 0.3.20 → 0.3.22

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 (144) hide show
  1. package/dist/builtin-memory/crouter-development/marketplaces.md +2 -4
  2. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +3 -5
  3. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +3 -6
  4. package/dist/builtin-memory/crouter-development/personas.md +2 -6
  5. package/dist/builtin-memory/crouter-development/plugins.md +2 -4
  6. package/dist/builtin-memory/design.md +2 -4
  7. package/dist/builtin-memory/development.md +3 -3
  8. package/dist/builtin-memory/planning.md +3 -4
  9. package/dist/builtin-memory/spec.md +3 -10
  10. package/dist/builtin-personas/orchestration-kernel.md +1 -1
  11. package/dist/commands/canvas-history/read.d.ts +1 -0
  12. package/dist/commands/canvas-history/read.js +61 -0
  13. package/dist/commands/canvas-history/search.d.ts +1 -0
  14. package/dist/commands/canvas-history/search.js +270 -0
  15. package/dist/commands/canvas-history/show.d.ts +1 -0
  16. package/dist/commands/canvas-history/show.js +79 -0
  17. package/dist/commands/canvas-history.d.ts +2 -0
  18. package/dist/commands/canvas-history.js +33 -0
  19. package/dist/commands/canvas.js +3 -2
  20. package/dist/commands/human/queue.js +2 -2
  21. package/dist/commands/memory/__tests__/lint-schema.test.js +29 -0
  22. package/dist/commands/memory/find.js +14 -52
  23. package/dist/commands/memory/lint.d.ts +7 -0
  24. package/dist/commands/memory/lint.js +29 -35
  25. package/dist/commands/memory/list.js +8 -18
  26. package/dist/commands/memory/read.js +4 -37
  27. package/dist/commands/memory/shared.js +1 -2
  28. package/dist/commands/memory/write.js +6 -9
  29. package/dist/commands/node.js +47 -25
  30. package/dist/commands/pkg/bridge.d.ts +1 -0
  31. package/dist/commands/pkg/bridge.js +137 -0
  32. package/dist/commands/pkg/plugin-inspect.js +7 -6
  33. package/dist/commands/pkg.js +2 -1
  34. package/dist/commands/push.js +10 -15
  35. package/dist/commands/skill/author.js +35 -44
  36. package/dist/commands/skill/shared.d.ts +1 -5
  37. package/dist/commands/skill/shared.js +9 -63
  38. package/dist/commands/skill.js +5 -6
  39. package/dist/commands/sys/doctor.js +6 -144
  40. package/dist/commands/tmux-spread.js +16 -1
  41. package/dist/commands/view-cycle.js +9 -5
  42. package/dist/commands/view-run.js +3 -2
  43. package/dist/core/__tests__/broker-dormant-wake.test.js +71 -34
  44. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +21 -5
  45. package/dist/core/__tests__/canvas-inbox-watcher.test.js +21 -5
  46. package/dist/core/__tests__/child-death-wake.test.js +133 -185
  47. package/dist/core/__tests__/context-intro.test.js +23 -27
  48. package/dist/core/__tests__/daemon-liveness.test.js +28 -258
  49. package/dist/core/__tests__/flagship-lifecycle.test.js +156 -135
  50. package/dist/core/__tests__/frame-decoder-perf.test.js +51 -25
  51. package/dist/core/__tests__/{broker-attach-limits.test.js → full/broker-attach-limits.test.js} +62 -35
  52. package/dist/core/__tests__/{broker-attach-stream.test.js → full/broker-attach-stream.test.js} +54 -31
  53. package/dist/core/__tests__/full/broker-crash-teardown.test.js +143 -0
  54. package/dist/core/__tests__/full/broker-dialogs.test.js +153 -0
  55. package/dist/core/__tests__/full/broker-lifecycle.test.js +116 -0
  56. package/dist/core/__tests__/{cascade-close.test.js → full/cascade-close.test.js} +4 -3
  57. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +292 -0
  58. package/dist/core/__tests__/{dead-pane-regression.test.js → full/dead-pane-regression.test.js} +2 -2
  59. package/dist/core/__tests__/{detach-focus.test.js → full/detach-focus.test.js} +7 -7
  60. package/dist/core/__tests__/{human-new-window-regression.test.js → full/human-new-window-regression.test.js} +2 -2
  61. package/dist/core/__tests__/{placement-focus.test.js → full/placement-focus.test.js} +5 -5
  62. package/dist/core/__tests__/{placement-reconcile.test.js → full/placement-reconcile.test.js} +3 -3
  63. package/dist/core/__tests__/{placement-revive.test.js → full/placement-revive.test.js} +5 -5
  64. package/dist/core/__tests__/{placement-teardown.test.js → full/placement-teardown.test.js} +4 -4
  65. package/dist/core/__tests__/{review-render-pane-regression.test.js → full/review-render-pane-regression.test.js} +2 -2
  66. package/dist/core/__tests__/full/spike-harness.test.d.ts +1 -0
  67. package/dist/core/__tests__/full/spike-harness.test.js +117 -0
  68. package/dist/core/__tests__/grace-clock.test.js +72 -75
  69. package/dist/core/__tests__/helpers/harness.d.ts +35 -1
  70. package/dist/core/__tests__/helpers/harness.js +70 -12
  71. package/dist/core/__tests__/live-mutation-verbs.test.js +100 -105
  72. package/dist/core/__tests__/live-mutation.test.js +111 -134
  73. package/dist/core/__tests__/memory.test.js +4 -4
  74. package/dist/core/__tests__/revive.test.js +96 -86
  75. package/dist/core/__tests__/subscription-delivery.test.js +116 -138
  76. package/dist/core/__tests__/wake-origin.test.js +54 -50
  77. package/dist/core/bootstrap.js +18 -14
  78. package/dist/core/bridge-map.d.ts +19 -0
  79. package/dist/core/bridge-map.js +73 -0
  80. package/dist/core/canvas/history.d.ts +91 -0
  81. package/dist/core/canvas/history.js +389 -0
  82. package/dist/core/canvas/index.d.ts +1 -0
  83. package/dist/core/canvas/index.js +1 -0
  84. package/dist/core/config.js +3 -22
  85. package/dist/core/help.d.ts +2 -2
  86. package/dist/core/help.js +1 -1
  87. package/dist/core/memory-resolver.d.ts +9 -2
  88. package/dist/core/memory-resolver.js +47 -7
  89. package/dist/core/render.d.ts +4 -3
  90. package/dist/core/render.js +38 -41
  91. package/dist/core/resolver.d.ts +1 -30
  92. package/dist/core/resolver.js +6 -546
  93. package/dist/core/runtime/bearings.d.ts +2 -2
  94. package/dist/core/runtime/bearings.js +2 -2
  95. package/dist/core/runtime/persona.js +5 -7
  96. package/dist/core/scope.d.ts +7 -5
  97. package/dist/core/scope.js +16 -17
  98. package/dist/core/substrate/ceiling.d.ts +28 -0
  99. package/dist/core/substrate/ceiling.js +87 -0
  100. package/dist/core/substrate/index.d.ts +2 -0
  101. package/dist/core/substrate/index.js +1 -0
  102. package/dist/core/substrate/on-read.js +18 -2
  103. package/dist/core/substrate/render.d.ts +3 -5
  104. package/dist/core/substrate/render.js +68 -59
  105. package/dist/core/substrate/schema.d.ts +18 -13
  106. package/dist/core/substrate/schema.js +12 -11
  107. package/dist/pi-extensions/canvas-context-intro.js +5 -3
  108. package/dist/pi-extensions/canvas-inbox-watcher.js +19 -2
  109. package/dist/types.d.ts +3 -18
  110. package/dist/types.js +0 -8
  111. package/package.json +4 -3
  112. package/dist/builtin-skills/.crouter-plugin/plugin.json +0 -5
  113. package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +0 -157
  114. package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +0 -106
  115. package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +0 -49
  116. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +0 -49
  117. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +0 -156
  118. package/dist/builtin-skills/skills/design/SKILL.md +0 -51
  119. package/dist/builtin-skills/skills/development/SKILL.md +0 -109
  120. package/dist/builtin-skills/skills/planning/SKILL.md +0 -59
  121. package/dist/builtin-skills/skills/spec/SKILL.md +0 -83
  122. package/dist/commands/skill/state.d.ts +0 -3
  123. package/dist/commands/skill/state.js +0 -71
  124. package/dist/core/__tests__/broker-crash-teardown.test.js +0 -116
  125. package/dist/core/__tests__/broker-dialogs.test.js +0 -126
  126. package/dist/core/__tests__/broker-lifecycle.test.js +0 -87
  127. package/dist/core/__tests__/resolver.test.js +0 -181
  128. package/dist/core/__tests__/spike-harness.test.js +0 -242
  129. /package/dist/{core/__tests__/broker-attach-limits.test.d.ts → commands/memory/__tests__/lint-schema.test.d.ts} +0 -0
  130. /package/dist/core/__tests__/{broker-attach-stream.test.d.ts → full/broker-attach-limits.test.d.ts} +0 -0
  131. /package/dist/core/__tests__/{broker-crash-teardown.test.d.ts → full/broker-attach-stream.test.d.ts} +0 -0
  132. /package/dist/core/__tests__/{broker-dialogs.test.d.ts → full/broker-crash-teardown.test.d.ts} +0 -0
  133. /package/dist/core/__tests__/{broker-lifecycle.test.d.ts → full/broker-dialogs.test.d.ts} +0 -0
  134. /package/dist/core/__tests__/{cascade-close.test.d.ts → full/broker-lifecycle.test.d.ts} +0 -0
  135. /package/dist/core/__tests__/{dead-pane-regression.test.d.ts → full/cascade-close.test.d.ts} +0 -0
  136. /package/dist/core/__tests__/{detach-focus.test.d.ts → full/daemon-liveness-pane.full.test.d.ts} +0 -0
  137. /package/dist/core/__tests__/{human-new-window-regression.test.d.ts → full/dead-pane-regression.test.d.ts} +0 -0
  138. /package/dist/core/__tests__/{placement-focus.test.d.ts → full/detach-focus.test.d.ts} +0 -0
  139. /package/dist/core/__tests__/{placement-reconcile.test.d.ts → full/human-new-window-regression.test.d.ts} +0 -0
  140. /package/dist/core/__tests__/{placement-revive.test.d.ts → full/placement-focus.test.d.ts} +0 -0
  141. /package/dist/core/__tests__/{placement-teardown.test.d.ts → full/placement-reconcile.test.d.ts} +0 -0
  142. /package/dist/core/__tests__/{resolver.test.d.ts → full/placement-revive.test.d.ts} +0 -0
  143. /package/dist/core/__tests__/{review-render-pane-regression.test.d.ts → full/placement-teardown.test.d.ts} +0 -0
  144. /package/dist/core/__tests__/{spike-harness.test.d.ts → full/review-render-pane-regression.test.d.ts} +0 -0
@@ -177,8 +177,8 @@ export const humanCancel = defineLeaf({
177
177
  return { canceled: true, job_id: jobId };
178
178
  },
179
179
  render: (r) => r['canceled'] === true
180
- ? `<canceled job_id="${r['job_id']}"/>`
181
- : `<cancel-noop job_id="${r['job_id']}">${r['reason'] ?? 'nothing to cancel'}</cancel-noop>`,
180
+ ? `Canceled human interaction ${r['job_id']} — its TUI pane is closed and subscribers were notified no answer is coming.`
181
+ : `Nothing to cancel for ${r['job_id']} — ${r['reason'] ?? 'nothing to cancel'}.`,
182
182
  });
183
183
  // ---------------------------------------------------------------------------
184
184
  // _run (hidden worker; not listed in branch help)
@@ -0,0 +1,29 @@
1
+ // Bug-regression: guards the hard cut that merged the substrate's `when`/`why`
2
+ // frontmatter pair into the single `when-and-why-to-read` read-routing field.
3
+ // The CTO ruling (taste/why-field-means-why-to-read) requires the migration be
4
+ // a hard cut: an old-shape doc must FAIL at `crtr memory lint`, never be read at
5
+ // runtime. `lintSubstrateSchema` is that enforcement seam — these lock it.
6
+ //
7
+ // Run: node --import tsx/esm --test src/commands/memory/__tests__/lint-schema.test.ts
8
+ import { test } from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import { lintSubstrateSchema } from '../lint.js';
11
+ const ROUTING = 'When you are X, this reference should be read because Y';
12
+ test('lint rejects a doc still carrying the retired `when` key', () => {
13
+ const err = lintSubstrateSchema({ kind: 'reference', when: 'when X' });
14
+ assert.ok(err !== null, 'old-shape `when` must produce a finding');
15
+ assert.match(err, /when-and-why-to-read/, 'the message points at the new field');
16
+ });
17
+ test('lint rejects a doc still carrying the retired `why` key', () => {
18
+ const err = lintSubstrateSchema({ kind: 'reference', why: 'because Y' });
19
+ assert.ok(err !== null, 'old-shape `why` must produce a finding');
20
+ assert.match(err, /when-and-why-to-read/, 'the message points at the new field');
21
+ });
22
+ test('lint rejects a substrate doc missing the merged routing field', () => {
23
+ const err = lintSubstrateSchema({ kind: 'reference' });
24
+ assert.ok(err !== null, 'a substrate doc must carry when-and-why-to-read');
25
+ assert.match(err, /when-and-why-to-read/);
26
+ });
27
+ test('lint accepts the merged new-shape frontmatter', () => {
28
+ assert.equal(lintSubstrateSchema({ kind: 'reference', 'when-and-why-to-read': ROUTING }), null);
29
+ });
@@ -2,9 +2,6 @@ import { defineLeaf } from '../../core/command.js';
2
2
  import { usage } from '../../core/errors.js';
3
3
  import { listAllMemoryDocs } from '../../core/memory-resolver.js';
4
4
  import { parseSubstrateDoc } from '../../core/substrate/index.js';
5
- import { listAllSkills } from '../../core/resolver.js';
6
- import { parseFrontmatter } from '../../core/frontmatter.js';
7
- import { readText } from '../../core/fs-utils.js';
8
5
  import { MEMORY_KINDS } from './shared.js';
9
6
  function substrateUnit(d) {
10
7
  return {
@@ -12,42 +9,19 @@ function substrateUnit(d) {
12
9
  kind: d.kind,
13
10
  scope: d.scope,
14
11
  path: d.path,
15
- when: d.when,
16
- why: d.why,
12
+ routing: d.whenAndWhyToRead,
17
13
  shortForm: d.shortForm,
18
14
  loadBody: () => d.body,
19
15
  };
20
16
  }
21
- function skillUnit(s) {
22
- const desc = s.frontmatter.description ?? '';
23
- const keywords = s.frontmatter.keywords ? s.frontmatter.keywords.join(' ') : '';
24
- return {
25
- name: s.name,
26
- kind: 'skill',
27
- scope: s.scope,
28
- path: s.path,
29
- // A skill's description plays the when+why role; keywords ride along so the
30
- // ranker weighs them too. short_form surfaces the description as the hook.
31
- when: [desc, keywords].filter((x) => x).join(' '),
32
- why: '',
33
- shortForm: desc,
34
- loadBody: () => parseFrontmatter(readText(s.path)).body,
35
- };
36
- }
37
- /** The unioned candidate set: every substrate memory document + every skill,
38
- * optionally narrowed to one kind. find searches EVERYTHING — it never applies
39
- * gate or visibility-rung filtering (design §11#3). The skill half is wrapped
40
- * defensively: the skill corpus is parsed by the resolver, which currently
41
- * hard-throws on at least one shipped skill's frontmatter (a pre-existing
42
- * regression in the yaml-parser swap, owned by the frontmatter/skill track). A
43
- * broken skill corpus must degrade the union to substrate-docs-only, never
44
- * crash `memory find`.
17
+ /** The candidate set: every substrate memory document (native + plugin, supplied
18
+ * by listAllMemoryDocs), optionally narrowed to one kind. find searches
19
+ * EVERYTHING it never applies gate or visibility-rung filtering (design
20
+ * §11#3).
45
21
  *
46
- * Dedup by (scope, name): the substrate and skill-plugin corpora overlap
47
- * (builtin-memory/ contains migrated copies of builtin-skills/; a skill name
48
- * can also appear in multiple installed plugins at the same scope). The FIRST
49
- * unit encountered for a given identity wins — substrate docs before skill-plugin
50
- * docs, which mirrors the memory resolver's own precedence. */
22
+ * Dedup by (scope, name): native and plugin docs of the same scope can collide
23
+ * on identity. The FIRST unit encountered for a given identity wins — native
24
+ * before plugin, which mirrors the memory resolver's own precedence. */
51
25
  function candidates(kindFilter) {
52
26
  const seen = new Set(); // "scope/name" → first wins
53
27
  const add = (u) => {
@@ -66,26 +40,17 @@ function candidates(kindFilter) {
66
40
  continue;
67
41
  add(substrateUnit(sub));
68
42
  }
69
- if (kindFilter === undefined || kindFilter === 'skill') {
70
- try {
71
- for (const skill of listAllSkills())
72
- add(skillUnit(skill));
73
- }
74
- catch {
75
- /* skill corpus unavailable — search substrate documents alone */
76
- }
77
- }
78
43
  return units;
79
44
  }
80
45
  export const findLeaf = defineLeaf({
81
46
  name: 'find',
82
47
  description: 'relevance search across memory documents',
83
- whenToUse: 'you do not yet know which document applies and need to discover what is stored — ranks documents by relevance, weighted over name, when, why, and short-form (add --body to also weigh body text). Searches the full scope set regardless of any visibility gate. Use --grep instead when you need an exact regex or literal-string match across document bodies rather than a ranked topic match.',
48
+ whenToUse: 'you do not yet know which document applies and need to discover what is stored — ranks documents by relevance, weighted over name, the read-routing line, and short-form (add --body to also weigh body text). Searches the full scope set regardless of any visibility gate. Use --grep instead when you need an exact regex or literal-string match across document bodies rather than a ranked topic match.',
84
49
  help: {
85
50
  name: 'memory find',
86
- summary: 'relevance search across memory documents, weighted over name/when/why/short-form (and body with --body)',
51
+ summary: 'relevance search across memory documents, weighted over name/routing-line/short-form (and body with --body)',
87
52
  params: [
88
- { kind: 'positional', name: 'query', required: true, constraint: 'With ranked search (default): whitespace-separated terms, matched case-insensitively and weighted over name, when, why, and short-form (plus body with --body); documents matching more/stronger fields rank higher. With --grep: an ECMAScript regex applied to each document body line.' },
53
+ { kind: 'positional', name: 'query', required: true, constraint: 'With ranked search (default): whitespace-separated terms, matched case-insensitively and weighted over name, the read-routing line, and short-form (plus body with --body); documents matching more/stronger fields rank higher. With --grep: an ECMAScript regex applied to each document body line.' },
89
54
  { kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: false, constraint: 'Filter to a single kind. Default: all kinds.' },
90
55
  { kind: 'flag', name: 'grep', type: 'bool', required: false, constraint: 'Treat the query as an ECMAScript regex and match it against document bodies, instead of weighted relevance ranking. Mutually exclusive with --body.' },
91
56
  { kind: 'flag', name: 'body', type: 'bool', required: false, constraint: 'Also weigh document body text in the relevance ranking (in addition to name/when/why/short-form). Ignored under --grep, which always scans bodies.' },
@@ -134,7 +99,7 @@ export const findLeaf = defineLeaf({
134
99
  follow_up: 'Read a document in full with `crtr memory read <name>`. Drop --grep for a ranked topic search.',
135
100
  };
136
101
  }
137
- // --- ranked mode: weighted relevance over name/when/why/short-form(+body) ---
102
+ // --- ranked mode: weighted relevance over name/routing-line/short-form(+body) ---
138
103
  const terms = query
139
104
  .toLowerCase()
140
105
  .split(/\s+/)
@@ -144,18 +109,15 @@ export const findLeaf = defineLeaf({
144
109
  const hits = [];
145
110
  for (const u of units) {
146
111
  const nameLc = u.name.toLowerCase();
147
- const whenLc = u.when.toLowerCase();
148
- const whyLc = u.why.toLowerCase();
112
+ const routingLc = u.routing.toLowerCase();
149
113
  const shortLc = u.shortForm.toLowerCase();
150
114
  const bodyLc = weighBody ? u.loadBody().toLowerCase() : null;
151
115
  let score = 0;
152
116
  for (const term of terms) {
153
117
  if (nameLc.includes(term))
154
118
  score += 10;
155
- if (whenLc.includes(term))
119
+ if (routingLc.includes(term))
156
120
  score += 5;
157
- if (whyLc.includes(term))
158
- score += 4;
159
121
  if (shortLc.includes(term))
160
122
  score += 3;
161
123
  if (bodyLc !== null && bodyLc.includes(term))
@@ -1 +1,8 @@
1
+ /** Schema checks for a doc living in a substrate memory dir: a memory store
2
+ * holds ONLY substrate docs, so a missing/invalid `kind` is an authoring
3
+ * error here (elsewhere it just means "not a substrate doc"). Rung and gate
4
+ * values are checked RAW — the runtime parser silently falls back to kind
5
+ * defaults / inert gates, which is exactly the silent tolerance this lint
6
+ * exists to catch at authoring time. */
7
+ export declare function lintSubstrateSchema(fm: Record<string, unknown> | null): string | null;
1
8
  export declare const lintLeaf: import("../../core/command.js").LeafDef;
@@ -1,20 +1,17 @@
1
1
  // `crtr memory lint` — the permanent valid-YAML gate over the bounded corpus
2
2
  // (the CTO green-checkpoint: zero frontmatter parse errors at authoring time,
3
3
  // so an invalid doc fails HERE instead of being silently isolated at runtime).
4
- // Bounded corpus = the substrate memory dirs (project/user/builtin) +
5
- // every installed plugin's skills tree + ~/.claude/skills. Never a
4
+ // Bounded corpus = the substrate memory dirs (project/user/builtin). Never a
6
5
  // filesystem-wide scan.
7
- import { homedir } from 'node:os';
8
- import { basename, join, relative, sep } from 'node:path';
6
+ import { basename, relative, sep } from 'node:path';
9
7
  import { defineLeaf } from '../../core/command.js';
10
8
  import { general } from '../../core/errors.js';
11
9
  import { warn } from '../../core/output.js';
12
10
  import { pathExists, readText, walkFiles } from '../../core/fs-utils.js';
13
11
  import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
14
- import { listAllPlugins } from '../../core/resolver.js';
15
- import { scopeMemoryDir } from '../../core/scope.js';
12
+ import { listInstalledPlugins } from '../../core/resolver.js';
13
+ import { pluginMemoryDir, scopeMemoryDir } from '../../core/scope.js';
16
14
  import { isDocKind, RUNGS } from '../../core/substrate/index.js';
17
- import { SKILL_ENTRY_FILE, SKILLS_DIR } from '../../types.js';
18
15
  /** The parser normalizes the `always` alias to `content`, so it lints valid. */
19
16
  const VALID_RUNGS = [...RUNGS, 'always'];
20
17
  const RUNG_FIELDS = ['system-prompt-visibility', 'file-read-visibility'];
@@ -24,12 +21,21 @@ const RUNG_FIELDS = ['system-prompt-visibility', 'file-read-visibility'];
24
21
  * values are checked RAW — the runtime parser silently falls back to kind
25
22
  * defaults / inert gates, which is exactly the silent tolerance this lint
26
23
  * exists to catch at authoring time. */
27
- function lintSubstrateSchema(fm) {
24
+ export function lintSubstrateSchema(fm) {
28
25
  if (fm === null)
29
26
  return 'missing frontmatter: a memory store doc requires `kind: skill|reference|preference`';
30
27
  if (!isDocKind(fm.kind)) {
31
28
  return `invalid kind: ${JSON.stringify(fm.kind)} (expected skill|reference|preference)`;
32
29
  }
30
+ // The retired `when`/`why` pair was merged into one read-routing field. The
31
+ // hard cut is enforced HERE: an old-shape doc must fail, never be silently
32
+ // read at runtime.
33
+ if ('when' in fm || 'why' in fm) {
34
+ return 'retired `when`/`why` keys: merge them into one `when-and-why-to-read` line — "When <circumstance>, this <kind> should be read <because <payoff>>."';
35
+ }
36
+ if (typeof fm['when-and-why-to-read'] !== 'string' || fm['when-and-why-to-read'].trim() === '') {
37
+ return 'missing `when-and-why-to-read`: one read-routing line — "When <circumstance>, this <kind> should be read <because <payoff>>."';
38
+ }
33
39
  for (const field of RUNG_FIELDS) {
34
40
  const v = fm[field];
35
41
  if (v !== undefined && (typeof v !== 'string' || !VALID_RUNGS.includes(v))) {
@@ -69,14 +75,14 @@ function lintFile(file, substrateStore, findings) {
69
75
  export const lintLeaf = defineLeaf({
70
76
  name: 'lint',
71
77
  description: 'validate frontmatter across the whole bounded document corpus',
72
- whenToUse: 'you authored or migrated documents and want the authoring-time gate: strict-parse every doc in the bounded corpus (memory stores, plugin skills, ~/.claude/skills) and fail loudly on any invalid YAML or substrate schema violation. Run it before shipping doc changes; CI-friendly (non-zero exit on any finding).',
78
+ whenToUse: 'you authored or migrated documents and want the authoring-time gate: strict-parse every doc in the bounded corpus (the substrate memory stores) and fail loudly on any invalid YAML or substrate schema violation. Run it before shipping doc changes; CI-friendly (non-zero exit on any finding).',
73
79
  help: {
74
80
  name: 'memory lint',
75
81
  summary: 'strict-parse frontmatter across the bounded corpus; non-zero exit on any finding',
76
82
  params: [],
77
83
  output: [
78
84
  { name: 'checked', type: 'number', required: true, constraint: 'Files linted across all corpora.' },
79
- { name: 'corpora', type: 'object', required: true, constraint: 'Per-corpus file counts: {memory_stores, plugin_skills, claude_skills}.' },
85
+ { name: 'corpora', type: 'object', required: true, constraint: 'Per-corpus file counts: {memory_stores}.' },
80
86
  { name: 'findings', type: 'object[]', required: true, constraint: 'One row per failure: {path, error}. Empty when green.' },
81
87
  ],
82
88
  outputKind: 'object',
@@ -85,40 +91,28 @@ export const lintLeaf = defineLeaf({
85
91
  run: async () => {
86
92
  const findings = [];
87
93
  let memoryCount = 0;
88
- let pluginCount = 0;
89
- let claudeCount = 0;
90
- // 1. Substrate memory stores (project/user/builtin), schema-aware.
94
+ // Substrate memory stores (project/user/builtin), schema-aware. A scope's
95
+ // corpus is its NATIVE memory dir plus each enabled plugin's memory dir —
96
+ // plugin docs are substrate docs and lint through the same schema gate.
91
97
  // MEMORY.md index files are not substrate docs — YAML-parse only.
92
- for (const scope of ['project', 'user', 'builtin']) {
93
- const dir = scopeMemoryDir(scope);
98
+ const lintDir = (dir) => {
94
99
  if (!dir || !pathExists(dir))
95
- continue;
100
+ return;
96
101
  for (const file of walkFiles(dir, (n) => n.endsWith('.md'))) {
97
102
  if (!relative(dir, file).split(sep).join('/'))
98
103
  continue;
99
104
  memoryCount += 1;
100
105
  lintFile(file, basename(file) !== 'MEMORY.md', findings);
101
106
  }
102
- }
103
- // 2. Every installed plugin's skills tree (builtin + plugin + marketplace).
104
- for (const plugin of listAllPlugins()) {
105
- const skillsRoot = join(plugin.root, SKILLS_DIR);
106
- if (!pathExists(skillsRoot))
107
- continue;
108
- for (const file of walkFiles(skillsRoot, (n) => n === SKILL_ENTRY_FILE)) {
109
- pluginCount += 1;
110
- lintFile(file, false, findings);
111
- }
112
- }
113
- // 3. ~/.claude/skills — read by the runtime, so it lints too.
114
- const claudeSkills = join(homedir(), '.claude', SKILLS_DIR);
115
- if (pathExists(claudeSkills)) {
116
- for (const file of walkFiles(claudeSkills, (n) => n === SKILL_ENTRY_FILE)) {
117
- claudeCount += 1;
118
- lintFile(file, false, findings);
107
+ };
108
+ for (const scope of ['project', 'user', 'builtin']) {
109
+ lintDir(scopeMemoryDir(scope));
110
+ for (const plugin of listInstalledPlugins(scope)) {
111
+ if (plugin.enabled)
112
+ lintDir(pluginMemoryDir(plugin));
119
113
  }
120
114
  }
121
- const checked = memoryCount + pluginCount + claudeCount;
115
+ const checked = memoryCount;
122
116
  if (findings.length > 0) {
123
117
  // Human/agent path renders only the message — surface every offender as
124
118
  // a scoped stderr notice (the --json path carries them in details too).
@@ -132,7 +126,7 @@ export const lintLeaf = defineLeaf({
132
126
  }
133
127
  return {
134
128
  checked,
135
- corpora: { memory_stores: memoryCount, plugin_skills: pluginCount, claude_skills: claudeCount },
129
+ corpora: { memory_stores: memoryCount },
136
130
  findings: [],
137
131
  follow_up: 'Corpus green — zero invalid frontmatter docs.',
138
132
  };
@@ -1,7 +1,6 @@
1
1
  import { defineLeaf } from '../../core/command.js';
2
2
  import { listAllMemoryDocs } from '../../core/memory-resolver.js';
3
- import { parseSubstrateDoc } from '../../core/substrate/index.js';
4
- import { listAllSkills } from '../../core/resolver.js';
3
+ import { parseSubstrateDoc, isIndexName, indexDirOf } from '../../core/substrate/index.js';
5
4
  import { MEMORY_KINDS, MEMORY_SCOPES, scopeRank } from './shared.js';
6
5
  export const listLeaf = defineLeaf({
7
6
  name: 'list',
@@ -15,7 +14,7 @@ export const listLeaf = defineLeaf({
15
14
  { kind: 'flag', name: 'scope', type: 'enum', choices: [...MEMORY_SCOPES], required: false, constraint: 'Filter to a single scope. Default: all resolved scopes.' },
16
15
  ],
17
16
  output: [
18
- { name: 'items', type: 'object[]', required: true, constraint: 'One row per document. Each: {name, title, short_form, kind, scope}. short_form is the abbreviated hook — shown here and nowhere else. Sorted by scope then kind then name ascending.' },
17
+ { name: 'items', type: 'object[]', required: true, constraint: 'One row per document. Each: {name, title, short_form, kind, scope, is_dir}. A directory INDEX.md surfaces as a dir entry (is_dir true, name = the directory path with a trailing slash); is_dir is false for an ordinary doc. short_form is the abbreviated hook — shown here and nowhere else. Sorted by scope then kind then name ascending.' },
19
18
  { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands — read a document in full or narrow the inventory.' },
20
19
  ],
21
20
  outputKind: 'object',
@@ -40,21 +39,11 @@ export const listLeaf = defineLeaf({
40
39
  continue;
41
40
  if (kindFilter !== undefined && sub.kind !== kindFilter)
42
41
  continue;
43
- addItem({ name: sub.name, kind: sub.kind, scope: sub.scope, shortForm: sub.shortForm });
44
- }
45
- // Skill-plugin corpus (scope-root skills + plugin/marketplace skills).
46
- // Only include when the kindFilter is absent or explicitly 'skill'.
47
- if (kindFilter === undefined || kindFilter === 'skill') {
48
- try {
49
- for (const skill of listAllSkills(scopeFilter)) {
50
- const raw = skill.frontmatter.description;
51
- const desc = typeof raw === 'string' ? raw : '';
52
- addItem({ name: skill.name, kind: 'skill', scope: skill.scope, shortForm: desc });
53
- }
54
- }
55
- catch {
56
- /* skill corpus unavailable — list substrate documents alone */
57
- }
42
+ // A directory INDEX surfaces as a dir entry: its dir path with a trailing
43
+ // slash, flagged is_dir, so the inventory distinguishes it from a doc.
44
+ const isDir = isIndexName(sub.name);
45
+ const name = isDir ? indexDirOf(sub.name) + '/' : sub.name;
46
+ addItem({ name, kind: sub.kind, scope: sub.scope, shortForm: sub.shortForm, isDir });
58
47
  }
59
48
  items.sort((a, b) => {
60
49
  const sr = scopeRank(a.scope) - scopeRank(b.scope);
@@ -72,6 +61,7 @@ export const listLeaf = defineLeaf({
72
61
  short_form: d.shortForm,
73
62
  kind: d.kind,
74
63
  scope: d.scope,
64
+ is_dir: d.isDir,
75
65
  })),
76
66
  follow_up: 'Read one in full with `crtr memory read <name>`. Narrow with --kind / --scope, or search a topic with `crtr memory find <query>`.',
77
67
  };
@@ -2,8 +2,6 @@ import { defineLeaf } from '../../core/command.js';
2
2
  import { CrtrError, notFound } from '../../core/errors.js';
3
3
  import { resolveMemoryDoc } from '../../core/memory-resolver.js';
4
4
  import { parseSubstrateDoc } from '../../core/substrate/index.js';
5
- import { resolveSkill } from '../../core/resolver.js';
6
- import { parseFrontmatter } from '../../core/frontmatter.js';
7
5
  import { readText } from '../../core/fs-utils.js';
8
6
  import { MEMORY_KINDS } from './shared.js';
9
7
  export const readLeaf = defineLeaf({
@@ -33,9 +31,9 @@ export const readLeaf = defineLeaf({
33
31
  const nameRaw = input['name'];
34
32
  const kindFilter = input['kind'];
35
33
  const includeFrontmatter = input['frontmatter'];
36
- // 1) Primary: resolve a substrate/memory document (memory/ dirs across
37
- // project>user>builtin). A not_found here is not fatal it falls through
38
- // to the skill resolver below (the D4 union).
34
+ // Resolve a substrate/memory document across scopes (project>user>builtin,
35
+ // with leaf-name fallback). The substrate corpus now includes plugin docs
36
+ // (mounted under <pluginName>/), so this resolves every name.
39
37
  let doc;
40
38
  try {
41
39
  doc = resolveMemoryDoc(nameRaw);
@@ -51,8 +49,7 @@ export const readLeaf = defineLeaf({
51
49
  : typeof doc.frontmatter?.['kind'] === 'string'
52
50
  ? doc.frontmatter['kind']
53
51
  : 'reference';
54
- // --kind asserts the resolved kind; a mismatch falls through to skills
55
- // (which only satisfy --kind=skill).
52
+ // --kind asserts the resolved kind; a mismatch falls through to not-found.
56
53
  if (kindFilter === undefined || kind === kindFilter) {
57
54
  const content = includeFrontmatter ? readText(doc.path) : doc.body;
58
55
  return {
@@ -65,36 +62,6 @@ export const readLeaf = defineLeaf({
65
62
  };
66
63
  }
67
64
  }
68
- // 2) D4 union fallthrough: the name is not a memory document (or its kind
69
- // did not match --kind) — resolve it as a plugin/marketplace/scope skill
70
- // so skills are readable through `crtr memory read` too. Skills are kind
71
- // "skill", so honor --kind by skipping this path for other kinds.
72
- if (kindFilter === undefined || kindFilter === 'skill') {
73
- let skill;
74
- try {
75
- skill = resolveSkill(nameRaw);
76
- }
77
- catch (e) {
78
- // not_found → fall through to the memory-not-found below. A genuine
79
- // CrtrError (e.g. ambiguous) propagates. Any other throw means the skill
80
- // corpus is currently unparseable (the yaml-parser regression owned by
81
- // the frontmatter/skill track) — degrade to not-found rather than 500.
82
- if (e instanceof CrtrError && e.code !== 'not_found')
83
- throw e;
84
- }
85
- if (skill !== undefined) {
86
- const raw = readText(skill.path);
87
- const content = includeFrontmatter ? raw : parseFrontmatter(raw).body;
88
- return {
89
- name: skill.name,
90
- kind: 'skill',
91
- scope: skill.scope,
92
- path: skill.path,
93
- content,
94
- follow_up: 'Add --frontmatter to include the YAML header. Browse the inventory with `crtr memory list`.',
95
- };
96
- }
97
- }
98
65
  throw notFound(`memory document not found: ${nameRaw}`, {
99
66
  memory: nameRaw,
100
67
  next: 'Run `crtr memory find <query>` to discover documents, or `crtr memory list` to browse the inventory.',
@@ -91,8 +91,7 @@ export function coerceAppliesTo(raw) {
91
91
  // first in this order; any preserved-on-update extras append after.
92
92
  const FRONTMATTER_ORDER = [
93
93
  'kind',
94
- 'when',
95
- 'why',
94
+ 'when-and-why-to-read',
96
95
  'short-form',
97
96
  'system-prompt-visibility',
98
97
  'file-read-visibility',
@@ -13,8 +13,7 @@ export const writeLeaf = defineLeaf({
13
13
  params: [
14
14
  { kind: 'positional', name: 'name', required: true, constraint: 'Path-derived identity (e.g. `topic` or `area/topic`) → memory/<name>.md at the resolved scope. Updated in place if it already exists, otherwise created.' },
15
15
  { kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: true, constraint: 'Document kind.' },
16
- { kind: 'flag', name: 'when', type: 'string', required: false, constraint: 'Frontmatter whenText a short string describing when this document should be read.' },
17
- { kind: 'flag', name: 'why', type: 'string', required: false, constraint: 'Frontmatter whyText — a short string describing why this document matters.' },
16
+ { kind: 'flag', name: 'when-and-why-to-read', type: 'string', required: false, constraint: 'The read-routing line, authored as ONE sentence: "When <circumstance>, this <kind> should be read <because <payoff>>." It states WHEN to read this doc and WHY the read is worth it — read-routing, NEVER a justification of why the content should be obeyed. Rendered verbatim as the preview. Required when creating.' },
18
17
  { kind: 'flag', name: 'short-form', type: 'string', required: false, constraint: 'Frontmatter short-form — a very abbreviated version of the content, the hook shown in `crtr memory list`.' },
19
18
  { kind: 'flag', name: 'system-prompt-visibility', type: 'enum', choices: [...VISIBILITY_RUNGS], required: false, constraint: 'Rung controlling how much of this document auto-loads into the system prompt / CLI help.' },
20
19
  { kind: 'flag', name: 'file-read-visibility', type: 'enum', choices: [...VISIBILITY_RUNGS], required: false, constraint: 'Rung controlling how much of this document surfaces when it is read off disk.' },
@@ -44,11 +43,10 @@ export const writeLeaf = defineLeaf({
44
43
  const { scope, memoryDir } = resolveWriteTarget(scopeArg);
45
44
  const path = memoryFilePath(memoryDir, name);
46
45
  const created = !pathExists(path);
47
- // CREATE requires the two prose fields that compose the generated preview
48
- // routing line without them every preview renders the degenerate
49
- // '", read this <kind>. ."'. UPDATE inherits them from the existing doc.
50
- if (created && (input['when'] === undefined || input['why'] === undefined)) {
51
- throw usage(`creating ${name} requires --when and --why (they compose the preview routing line "{when}, read this ${kind}. {why}.")`);
46
+ // CREATE requires the read-routing line that becomes the preview — without
47
+ // it every preview renders empty. UPDATE inherits it from the existing doc.
48
+ if (created && input['whenAndWhyToRead'] === undefined) {
49
+ throw usage(`creating ${name} requires --when-and-why-to-read: one read-routing sentence "When <circumstance>, this ${kind} should be read <because <payoff>>." (rendered verbatim as the preview).`);
52
50
  }
53
51
  // In-place update: start from the existing frontmatter (preserving fields
54
52
  // not passed this time), then overlay the provided ones. Create: start clean.
@@ -62,8 +60,7 @@ export const writeLeaf = defineLeaf({
62
60
  if (value !== undefined)
63
61
  frontmatter[key] = value;
64
62
  };
65
- setIf('when', input['when']);
66
- setIf('why', input['why']);
63
+ setIf('when-and-why-to-read', input['whenAndWhyToRead']);
67
64
  setIf('short-form', input['shortForm']);
68
65
  setIf('system-prompt-visibility', input['systemPromptVisibility']);
69
66
  setIf('file-read-visibility', input['fileReadVisibility']);