@crouton-kit/crouter 0.3.21 → 0.3.23

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 (111) hide show
  1. package/dist/commands/canvas-history/read.d.ts +1 -0
  2. package/dist/commands/canvas-history/read.js +61 -0
  3. package/dist/commands/canvas-history/search.d.ts +1 -0
  4. package/dist/commands/canvas-history/search.js +270 -0
  5. package/dist/commands/canvas-history/show.d.ts +1 -0
  6. package/dist/commands/canvas-history/show.js +79 -0
  7. package/dist/commands/canvas-history.d.ts +2 -0
  8. package/dist/commands/canvas-history.js +33 -0
  9. package/dist/commands/canvas.js +3 -2
  10. package/dist/commands/memory/find.js +7 -40
  11. package/dist/commands/memory/lint.js +19 -34
  12. package/dist/commands/memory/list.js +8 -18
  13. package/dist/commands/memory/read.js +4 -37
  14. package/dist/commands/pkg/bridge.d.ts +1 -0
  15. package/dist/commands/pkg/bridge.js +137 -0
  16. package/dist/commands/pkg/plugin-inspect.js +7 -6
  17. package/dist/commands/pkg.js +2 -1
  18. package/dist/commands/skill/author.js +35 -44
  19. package/dist/commands/skill/shared.d.ts +1 -5
  20. package/dist/commands/skill/shared.js +9 -63
  21. package/dist/commands/skill.js +5 -6
  22. package/dist/commands/sys/doctor.js +6 -144
  23. package/dist/core/__tests__/broker-dormant-wake.test.js +71 -34
  24. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +21 -5
  25. package/dist/core/__tests__/canvas-inbox-watcher.test.js +21 -5
  26. package/dist/core/__tests__/child-death-wake.test.js +133 -185
  27. package/dist/core/__tests__/daemon-liveness.test.js +28 -258
  28. package/dist/core/__tests__/flagship-lifecycle.test.js +156 -135
  29. package/dist/core/__tests__/frame-decoder-perf.test.js +51 -25
  30. package/dist/core/__tests__/{broker-attach-limits.test.js → full/broker-attach-limits.test.js} +62 -35
  31. package/dist/core/__tests__/{broker-attach-stream.test.js → full/broker-attach-stream.test.js} +54 -31
  32. package/dist/core/__tests__/full/broker-crash-teardown.test.js +143 -0
  33. package/dist/core/__tests__/full/broker-dialogs.test.js +153 -0
  34. package/dist/core/__tests__/full/broker-lifecycle.test.js +116 -0
  35. package/dist/core/__tests__/{cascade-close.test.js → full/cascade-close.test.js} +1 -1
  36. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +292 -0
  37. package/dist/core/__tests__/{dead-pane-regression.test.js → full/dead-pane-regression.test.js} +2 -2
  38. package/dist/core/__tests__/{detach-focus.test.js → full/detach-focus.test.js} +5 -5
  39. package/dist/core/__tests__/{human-new-window-regression.test.js → full/human-new-window-regression.test.js} +2 -2
  40. package/dist/core/__tests__/{placement-focus.test.js → full/placement-focus.test.js} +5 -5
  41. package/dist/core/__tests__/{placement-reconcile.test.js → full/placement-reconcile.test.js} +3 -3
  42. package/dist/core/__tests__/{placement-revive.test.js → full/placement-revive.test.js} +5 -5
  43. package/dist/core/__tests__/{placement-teardown.test.js → full/placement-teardown.test.js} +4 -4
  44. package/dist/core/__tests__/{review-render-pane-regression.test.js → full/review-render-pane-regression.test.js} +2 -2
  45. package/dist/core/__tests__/full/spike-harness.test.js +117 -0
  46. package/dist/core/__tests__/grace-clock.test.js +72 -75
  47. package/dist/core/__tests__/helpers/harness.d.ts +35 -1
  48. package/dist/core/__tests__/helpers/harness.js +70 -12
  49. package/dist/core/__tests__/live-mutation-verbs.test.js +99 -104
  50. package/dist/core/__tests__/live-mutation.test.js +111 -134
  51. package/dist/core/__tests__/revive.test.js +96 -86
  52. package/dist/core/__tests__/subscription-delivery.test.js +116 -138
  53. package/dist/core/__tests__/wake-origin.test.js +54 -50
  54. package/dist/core/bridge-map.d.ts +19 -0
  55. package/dist/core/bridge-map.js +73 -0
  56. package/dist/core/canvas/history.d.ts +91 -0
  57. package/dist/core/canvas/history.js +389 -0
  58. package/dist/core/canvas/index.d.ts +1 -0
  59. package/dist/core/canvas/index.js +1 -0
  60. package/dist/core/config.js +3 -22
  61. package/dist/core/memory-resolver.d.ts +9 -2
  62. package/dist/core/memory-resolver.js +47 -7
  63. package/dist/core/resolver.d.ts +1 -30
  64. package/dist/core/resolver.js +6 -546
  65. package/dist/core/runtime/persona.js +5 -7
  66. package/dist/core/scope.d.ts +7 -5
  67. package/dist/core/scope.js +16 -17
  68. package/dist/core/substrate/ceiling.d.ts +28 -0
  69. package/dist/core/substrate/ceiling.js +87 -0
  70. package/dist/core/substrate/index.d.ts +2 -0
  71. package/dist/core/substrate/index.js +1 -0
  72. package/dist/core/substrate/on-read.js +18 -2
  73. package/dist/core/substrate/render.d.ts +2 -3
  74. package/dist/core/substrate/render.js +63 -51
  75. package/dist/pi-extensions/canvas-inbox-watcher.js +19 -2
  76. package/dist/types.d.ts +3 -18
  77. package/dist/types.js +0 -8
  78. package/package.json +4 -3
  79. package/dist/builtin-skills/.crouter-plugin/plugin.json +0 -5
  80. package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +0 -157
  81. package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +0 -106
  82. package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +0 -49
  83. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +0 -49
  84. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +0 -156
  85. package/dist/builtin-skills/skills/design/SKILL.md +0 -51
  86. package/dist/builtin-skills/skills/development/SKILL.md +0 -109
  87. package/dist/builtin-skills/skills/planning/SKILL.md +0 -59
  88. package/dist/builtin-skills/skills/spec/SKILL.md +0 -83
  89. package/dist/commands/skill/state.d.ts +0 -3
  90. package/dist/commands/skill/state.js +0 -71
  91. package/dist/core/__tests__/broker-crash-teardown.test.js +0 -116
  92. package/dist/core/__tests__/broker-dialogs.test.js +0 -126
  93. package/dist/core/__tests__/broker-lifecycle.test.js +0 -87
  94. package/dist/core/__tests__/resolver.test.js +0 -181
  95. package/dist/core/__tests__/spike-harness.test.js +0 -242
  96. /package/dist/core/__tests__/{broker-attach-limits.test.d.ts → full/broker-attach-limits.test.d.ts} +0 -0
  97. /package/dist/core/__tests__/{broker-attach-stream.test.d.ts → full/broker-attach-stream.test.d.ts} +0 -0
  98. /package/dist/core/__tests__/{broker-crash-teardown.test.d.ts → full/broker-crash-teardown.test.d.ts} +0 -0
  99. /package/dist/core/__tests__/{broker-dialogs.test.d.ts → full/broker-dialogs.test.d.ts} +0 -0
  100. /package/dist/core/__tests__/{broker-lifecycle.test.d.ts → full/broker-lifecycle.test.d.ts} +0 -0
  101. /package/dist/core/__tests__/{cascade-close.test.d.ts → full/cascade-close.test.d.ts} +0 -0
  102. /package/dist/core/__tests__/{dead-pane-regression.test.d.ts → full/daemon-liveness-pane.full.test.d.ts} +0 -0
  103. /package/dist/core/__tests__/{detach-focus.test.d.ts → full/dead-pane-regression.test.d.ts} +0 -0
  104. /package/dist/core/__tests__/{human-new-window-regression.test.d.ts → full/detach-focus.test.d.ts} +0 -0
  105. /package/dist/core/__tests__/{placement-focus.test.d.ts → full/human-new-window-regression.test.d.ts} +0 -0
  106. /package/dist/core/__tests__/{placement-reconcile.test.d.ts → full/placement-focus.test.d.ts} +0 -0
  107. /package/dist/core/__tests__/{placement-revive.test.d.ts → full/placement-reconcile.test.d.ts} +0 -0
  108. /package/dist/core/__tests__/{placement-teardown.test.d.ts → full/placement-revive.test.d.ts} +0 -0
  109. /package/dist/core/__tests__/{resolver.test.d.ts → full/placement-teardown.test.d.ts} +0 -0
  110. /package/dist/core/__tests__/{review-render-pane-regression.test.d.ts → full/review-render-pane-regression.test.d.ts} +0 -0
  111. /package/dist/core/__tests__/{spike-harness.test.d.ts → full/spike-harness.test.d.ts} +0 -0
@@ -5,16 +5,9 @@ import { fileURLToPath } from 'node:url';
5
5
  import { CRTR_DIR_NAME } from '../types.js';
6
6
  import { usage } from './errors.js';
7
7
  let cachedProjectRoot;
8
- export function builtinSkillsRoot() {
9
- // Resolve relative to this file: src/core/scope.ts → src/builtin-skills/ OR dist/core/scope.js → dist/builtin-skills/
10
- const thisFile = fileURLToPath(import.meta.url);
11
- const coreDir = dirname(thisFile);
12
- const pkgDir = dirname(coreDir); // src/ or dist/
13
- return join(pkgDir, 'builtin-skills');
14
- }
15
8
  export function builtinViewsRoot() {
16
- // Resolved relative to this file exactly like builtinSkillsRoot — sibling of
17
- // builtin-skills: dist/core/scope.js → dist/builtin-views/ (src/ at dev time).
9
+ // Resolved relative to this file: dist/core/scope.js dist/builtin-views/
10
+ // (src/ at dev time).
18
11
  const thisFile = fileURLToPath(import.meta.url);
19
12
  const coreDir = dirname(thisFile);
20
13
  const pkgDir = dirname(coreDir); // src/ or dist/
@@ -22,11 +15,10 @@ export function builtinViewsRoot() {
22
15
  }
23
16
  export function builtinMemoryRoot() {
24
17
  // The substrate's shipped built-in documents live in their OWN package dir,
25
- // a sibling of builtin-skills — resolved relative to this file exactly like
26
- // builtinSkillsRoot/builtinViewsRoot: dist/core/scope.js → dist/builtin-memory/
18
+ // resolved relative to this file: dist/core/scope.js → dist/builtin-memory/
27
19
  // (src/ at dev time). This is a HARD special case (design verdict m2): builtin
28
- // memory is NOT scopeRoot('builtin')/memory, which would land under
29
- // builtin-skills/ builtin's scopeRoot is builtinSkillsRoot().
20
+ // memory is the only content the builtin scope carries the scope has no
21
+ // scopeRoot (scopeRoot('builtin') is null).
30
22
  const thisFile = fileURLToPath(import.meta.url);
31
23
  const coreDir = dirname(thisFile);
32
24
  const pkgDir = dirname(coreDir); // src/ or dist/
@@ -65,8 +57,10 @@ export function projectScopeRoot(startDir) {
65
57
  return findProjectScopeRoot(startDir);
66
58
  }
67
59
  export function scopeRoot(scope) {
60
+ // The builtin scope has no scope-root dir: its only content is memory
61
+ // (builtinMemoryRoot) and views (builtinViewsRoot), both hard special cases.
68
62
  if (scope === 'builtin')
69
- return builtinSkillsRoot();
63
+ return null;
70
64
  return scope === 'user' ? userScopeRoot() : projectScopeRoot();
71
65
  }
72
66
  export function requireScopeRoot(scope) {
@@ -98,15 +92,20 @@ export function scopeSkillsDir(scope) {
98
92
  return root ? join(root, 'skills') : null;
99
93
  }
100
94
  /** Where substrate memory documents live per scope. Builtin memory is the
101
- * special case — its own package dir (builtinMemoryRoot, a sibling of
102
- * builtin-skills), NOT scopeRoot('builtin')/memory — mirroring viewsDir's
103
- * builtin handling. User and project memory live at scopeRoot(scope)/memory. */
95
+ * special case — its own package dir (builtinMemoryRoot), since the builtin
96
+ * scope has no scopeRoot — mirroring viewsDir's builtin handling. User and
97
+ * project memory live at scopeRoot(scope)/memory. */
104
98
  export function scopeMemoryDir(scope) {
105
99
  if (scope === 'builtin')
106
100
  return builtinMemoryRoot();
107
101
  const root = scope === 'user' ? userScopeRoot() : projectScopeRoot();
108
102
  return root ? join(root, 'memory') : null;
109
103
  }
104
+ /** Where a plugin's substrate memory documents live: `<plugin.root>/memory`.
105
+ * The resolver mounts these under the virtual `<pluginName>/` namespace. */
106
+ export function pluginMemoryDir(plugin) {
107
+ return join(plugin.root, 'memory');
108
+ }
110
109
  /** Where view definition dirs live per scope. Builtin views sit directly under
111
110
  * builtinViewsRoot() (no `views/` segment — they ARE the builtin views dir),
112
111
  * matching the loader's `<root>/<name>/view.mjs` resolution. */
@@ -0,0 +1,28 @@
1
+ import { type Rung, type SubstrateDoc } from './schema.js';
2
+ /** The reserved basename (no extension) that marks a directory's index doc. */
3
+ export declare const INDEX_NAME = "INDEX";
4
+ /** Does this path-derived name denote a directory INDEX (`taste/INDEX`, or a
5
+ * top-level `INDEX`)? */
6
+ export declare function isIndexName(name: string): boolean;
7
+ /** The directory an INDEX doc represents: `taste/INDEX` → `taste`, `INDEX` → ''. */
8
+ export declare function indexDirOf(name: string): string;
9
+ /** The name a doc renders under in agent-facing surfaces: an INDEX doc renders
10
+ * under its directory name (the dir entry); every other doc keeps its name. */
11
+ export declare function displayName(name: string): string;
12
+ /** The two visibility surfaces an INDEX can cap. */
13
+ export type Surface = 'systemPromptVisibility' | 'fileReadVisibility';
14
+ /** Build the dir → governing-INDEX map. Precedence is first-occurrence-wins, so
15
+ * callers MUST pass docs precedence-ordered (project > user > builtin) — the
16
+ * highest-precedence INDEX governs a dir, mirroring the memory resolver. */
17
+ export declare function buildCeilingIndex(docs: SubstrateDoc[]): Map<string, SubstrateDoc>;
18
+ /** A doc's effective rung on `surface` after applying every ancestor INDEX
19
+ * ceiling: min(own rung, each ancestor INDEX's rung). A `none` ancestor INDEX
20
+ * hides the whole subtree (min with none = none). */
21
+ export declare function effectiveRung(doc: SubstrateDoc, ceil: Map<string, SubstrateDoc>, surface: Surface): Rung;
22
+ /** Apply ceilings to a doc list for a surface: each doc gets its effective rung
23
+ * on `surface` written back into that field, and INDEX docs are renamed to
24
+ * their dir entry. Returns NEW objects only where something changed (cheap,
25
+ * preserves identity for untouched docs). Does NOT drop `none`-rung docs — the
26
+ * caller filters per surface (boot drops them; node-local intentionally keeps
27
+ * them). */
28
+ export declare function applyCeilings(docs: SubstrateDoc[], surface: Surface): SubstrateDoc[];
@@ -0,0 +1,87 @@
1
+ // ceiling.ts — INDEX.md dir-level entries and the subtree rung ceiling.
2
+ //
3
+ // A directory under a memory store may carry an `INDEX.md` with the SAME
4
+ // substrate frontmatter as any doc. That file makes the directory a first-class
5
+ // substrate entry: it renders as ONE entry at dir level (headed by the dir
6
+ // name, not `…/INDEX`), and its rung is a CEILING for everything in its subtree.
7
+ //
8
+ // effective rung of a descendant = min(own rung, every ancestor INDEX's rung)
9
+ //
10
+ // So a dir's INDEX at `preview` lets its files surface at most at `preview`; an
11
+ // INDEX at `none` hides the whole subtree. This is the single, explicit
12
+ // mechanism for collapsing a directory in agent-facing surfaces (boot render +
13
+ // on-read) — there is no parallel auto-hiding heuristic. See design taste:
14
+ // `crtr memory read taste/document-substrate`.
15
+ import { rungRank } from './schema.js';
16
+ /** The reserved basename (no extension) that marks a directory's index doc. */
17
+ export const INDEX_NAME = 'INDEX';
18
+ /** Does this path-derived name denote a directory INDEX (`taste/INDEX`, or a
19
+ * top-level `INDEX`)? */
20
+ export function isIndexName(name) {
21
+ return name === INDEX_NAME || name.endsWith('/' + INDEX_NAME);
22
+ }
23
+ /** The directory an INDEX doc represents: `taste/INDEX` → `taste`, `INDEX` → ''. */
24
+ export function indexDirOf(name) {
25
+ return name === INDEX_NAME ? '' : name.slice(0, name.length - INDEX_NAME.length - 1);
26
+ }
27
+ /** The name a doc renders under in agent-facing surfaces: an INDEX doc renders
28
+ * under its directory name (the dir entry); every other doc keeps its name. */
29
+ export function displayName(name) {
30
+ return isIndexName(name) ? indexDirOf(name) : name;
31
+ }
32
+ /** Build the dir → governing-INDEX map. Precedence is first-occurrence-wins, so
33
+ * callers MUST pass docs precedence-ordered (project > user > builtin) — the
34
+ * highest-precedence INDEX governs a dir, mirroring the memory resolver. */
35
+ export function buildCeilingIndex(docs) {
36
+ const m = new Map();
37
+ for (const d of docs) {
38
+ if (!isIndexName(d.name))
39
+ continue;
40
+ const dir = indexDirOf(d.name);
41
+ if (!m.has(dir))
42
+ m.set(dir, d);
43
+ }
44
+ return m;
45
+ }
46
+ function minRung(a, b) {
47
+ return rungRank(a) <= rungRank(b) ? a : b;
48
+ }
49
+ /** Ancestor directory names of a doc, nearest-last: `a/b/c` → [`a/b`, `a`];
50
+ * `a` → []. An INDEX doc's OWN dir is included (a self-cap is a no-op), so a
51
+ * nested INDEX still inherits its parents' ceilings. */
52
+ function ancestorDirs(name) {
53
+ const parts = name.split('/');
54
+ parts.pop(); // drop the leaf
55
+ const out = [];
56
+ for (let i = parts.length; i > 0; i--)
57
+ out.push(parts.slice(0, i).join('/'));
58
+ return out;
59
+ }
60
+ /** A doc's effective rung on `surface` after applying every ancestor INDEX
61
+ * ceiling: min(own rung, each ancestor INDEX's rung). A `none` ancestor INDEX
62
+ * hides the whole subtree (min with none = none). */
63
+ export function effectiveRung(doc, ceil, surface) {
64
+ let rung = doc[surface];
65
+ for (const dir of ancestorDirs(doc.name)) {
66
+ const idx = ceil.get(dir);
67
+ if (idx)
68
+ rung = minRung(rung, idx[surface]);
69
+ }
70
+ return rung;
71
+ }
72
+ /** Apply ceilings to a doc list for a surface: each doc gets its effective rung
73
+ * on `surface` written back into that field, and INDEX docs are renamed to
74
+ * their dir entry. Returns NEW objects only where something changed (cheap,
75
+ * preserves identity for untouched docs). Does NOT drop `none`-rung docs — the
76
+ * caller filters per surface (boot drops them; node-local intentionally keeps
77
+ * them). */
78
+ export function applyCeilings(docs, surface) {
79
+ const ceil = buildCeilingIndex(docs);
80
+ return docs.map((d) => {
81
+ const rung = effectiveRung(d, ceil, surface);
82
+ const name = displayName(d.name);
83
+ if (rung === d[surface] && name === d.name)
84
+ return d;
85
+ return { ...d, [surface]: rung, name };
86
+ });
87
+ }
@@ -3,5 +3,7 @@ export type { DocKind, Rung, GatePredicate, SubstrateSchema, SubstrateDoc } from
3
3
  export { scopeForCwd, spineDepth, assembleNodeSubject } from './subject.js';
4
4
  export type { NodeConfigSubject } from './subject.js';
5
5
  export { gatePasses } from './gate.js';
6
+ export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveRung, applyCeilings, } from './ceiling.js';
7
+ export type { Surface } from './ceiling.js';
6
8
  export { renderSkillsSection, renderPreferencesSection, renderReferencesBlock } from './render.js';
7
9
  export { renderOnReadDocs } from './on-read.js';
@@ -14,5 +14,6 @@ KIND_DEFAULT_RUNGS,
14
14
  parseSubstrateFrontmatter, parseSubstrateDoc, previewLine, } from './schema.js';
15
15
  export { scopeForCwd, spineDepth, assembleNodeSubject } from './subject.js';
16
16
  export { gatePasses } from './gate.js';
17
+ export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveRung, applyCeilings, } from './ceiling.js';
17
18
  export { renderSkillsSection, renderPreferencesSection, renderReferencesBlock } from './render.js';
18
19
  export { renderOnReadDocs } from './on-read.js';
@@ -36,7 +36,7 @@ import { CRTR_DIR_NAME } from '../../types.js';
36
36
  import { pathExists, readText, walkFiles } from '../fs-utils.js';
37
37
  import { parseFrontmatterGeneric } from '../frontmatter.js';
38
38
  import { listAllMemoryDocs } from '../memory-resolver.js';
39
- import { assembleNodeSubject, gatePasses, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
39
+ import { assembleNodeSubject, buildCeilingIndex, displayName, effectiveRung, gatePasses, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
40
40
  import { cachedSubstrateDocs } from './session-cache.js';
41
41
  // Ancestor dirs we never look inside for a `.crouter/memory/` store (the read
42
42
  // file may live under a build/dependency tree; `.crouter` is NOT junk here — it
@@ -264,6 +264,18 @@ export function renderOnReadDocs(nodeId, readFilePath, seen = new Set()) {
264
264
  // Outermost-first: the nearest/most-specific doc reads last — closest to the
265
265
  // file content that follows it (the applies-to set, order -1, trails).
266
266
  candidates.sort((a, b) => b.order - a.order);
267
+ // INDEX ceiling: build the dir → governing-INDEX map over the whole resolved
268
+ // corpus PLUS the candidate docs, so a dir's INDEX caps (or `none`-hides) its
269
+ // subtree on-read exactly as it does at boot. The candidate's own
270
+ // fileReadVisibility and name are overridden by the effective rung / dir entry.
271
+ let ceil;
272
+ try {
273
+ const resolved = cachedSubstrateDocs(listAllMemoryDocs, parseSubstrateDoc);
274
+ ceil = buildCeilingIndex([...resolved, ...candidates.map((c) => c.doc)]);
275
+ }
276
+ catch {
277
+ ceil = buildCeilingIndex(candidates.map((c) => c.doc));
278
+ }
267
279
  const rendered = [];
268
280
  for (const c of candidates) {
269
281
  // Never re-surface the doc the agent is literally reading.
@@ -276,7 +288,11 @@ export function renderOnReadDocs(nodeId, readFilePath, seen = new Set()) {
276
288
  try {
277
289
  if (!gatePasses(c.doc, subject))
278
290
  continue; // gated out for this node
279
- block = renderDocEnvelope(c.doc, absReadFile);
291
+ const rung = effectiveRung(c.doc, ceil, 'fileReadVisibility');
292
+ const doc = rung === c.doc.fileReadVisibility && displayName(c.doc.name) === c.doc.name
293
+ ? c.doc
294
+ : { ...c.doc, fileReadVisibility: rung, name: displayName(c.doc.name) };
295
+ block = renderDocEnvelope(doc, absReadFile);
280
296
  }
281
297
  catch {
282
298
  continue; // a single bad doc never breaks the read
@@ -1,8 +1,7 @@
1
1
  /** The `## Skills` system-prompt section: every eligible `kind: skill` doc,
2
2
  * rendered at its `system-prompt-visibility`. `name`-rung skills collapse into
3
- * one compact catalog (group-collapsed, unioned with plugin/marketplace
4
- * skills); `preview`/`content`-rung skills each get a `###` sub-section.
5
- * Returns '' when nothing is eligible. */
3
+ * one compact, plugin-grouped catalog; `preview`/`content`-rung skills each get
4
+ * a `###` sub-section. Returns '' when nothing is eligible. */
6
5
  export declare function renderSkillsSection(nodeId: string): string;
7
6
  /** The `## Preferences` system-prompt section: every eligible `kind: preference`
8
7
  * doc as its own `###` sub-section, at its `system-prompt-visibility` (the
@@ -18,14 +18,13 @@
18
18
  // null for a non-substrate doc and is `.filter()`ed out; per-file loads are
19
19
  // wrapped so one bad file is skipped, not fatal.
20
20
  import { relative, sep } from 'node:path';
21
- import { SCOPE_SKILL_PLUGIN } from '../../types.js';
22
- import { listAllPlugins, listAllSkills } from '../resolver.js';
21
+ import { listAllPlugins } from '../resolver.js';
23
22
  import { renderCatalogSection } from '../../commands/skill/shared.js';
24
23
  import { listAllMemoryDocs } from '../memory-resolver.js';
25
24
  import { parseFrontmatterGeneric } from '../frontmatter.js';
26
25
  import { pathExists, readText, walkFiles } from '../fs-utils.js';
27
26
  import { memoryDir } from '../runtime/memory.js';
28
- import { assembleNodeSubject, gatePasses, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
27
+ import { applyCeilings, assembleNodeSubject, gatePasses, isIndexName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
29
28
  import { cachedAllPlugins, cachedNodeSubject, cachedSubstrateDocs, } from './session-cache.js';
30
29
  // ---------------------------------------------------------------------------
31
30
  // The shared per-doc pipeline.
@@ -35,7 +34,12 @@ import { cachedAllPlugins, cachedNodeSubject, cachedSubstrateDocs, } from './ses
35
34
  * system-prompt rung above `none`. Resolver = user + project + builtin scopes
36
35
  * (precedence-ordered); node-local is loaded separately (see nodeLocalDocs).
37
36
  * Uses the per-session cache so the full corpus is scanned+parsed at most once
38
- * per session across the three boot-render calls. */
37
+ * per session across the three boot-render calls.
38
+ *
39
+ * Ceilings are applied over the WHOLE corpus (cross-kind) BEFORE the kind
40
+ * filter: an INDEX.md renders as its dir entry (`taste`, not `taste/INDEX`) at
41
+ * its own rung, and every descendant's rung is capped by its ancestor INDEX
42
+ * rungs — a `none` dir hides its whole subtree. */
39
43
  function resolverDocs(subject, kind) {
40
44
  let docs;
41
45
  try {
@@ -44,10 +48,17 @@ function resolverDocs(subject, kind) {
44
48
  catch {
45
49
  return [];
46
50
  }
47
- return docs
51
+ return applyCeilings(docs, 'systemPromptVisibility')
48
52
  .filter((d) => d.kind === kind)
49
53
  .filter((d) => gatePasses(d, subject))
50
- .filter((d) => d.systemPromptVisibility !== 'none');
54
+ .filter((d) => d.systemPromptVisibility !== 'none')
55
+ // Re-sort after the INDEX rename (taste/INDEX → taste) so a dir entry sorts
56
+ // immediately BEFORE its children, keeping scope precedence (project > user
57
+ // > builtin) intact.
58
+ .sort((a, b) => scopeRank(a.scope) - scopeRank(b.scope) || a.name.localeCompare(b.name));
59
+ }
60
+ function scopeRank(scope) {
61
+ return scope === 'project' ? 0 : scope === 'user' ? 1 : 2;
51
62
  }
52
63
  /** The node-local memory docs eligible at boot. Node-local memory lives in the
53
64
  * canvas home (`nodes/<id>/context/memory/`) and is OUTSIDE the scope resolver,
@@ -114,50 +125,49 @@ function renderSubSection(d) {
114
125
  // ---------------------------------------------------------------------------
115
126
  // 1. Skills section — `## Skills` (system prompt).
116
127
  // ---------------------------------------------------------------------------
117
- /** The compact, group-collapsed `name`-rung catalog: the UNION of name-rung
118
- * substrate `skill` docs (the migrated scope-local + builtin skills, rendered
119
- * as scope-local `_` entries) WITH plugin/marketplace skills (which stay
120
- * resolver-provided, NOT migrated named-plugin skills at user/project scope;
121
- * the SCOPE_SKILL_PLUGIN sentinel and builtin scope are EXCLUDED because they
122
- * become substrate docs). Reuses skill/shared.ts's renderCatalogSection
123
- * group-collapse for a compact, source-grouped catalog. Returns '' when nothing
124
- * is in the catalog. */
125
- function renderSkillCatalog(nameRungSkillDocs) {
126
- let pluginSkills;
127
- try {
128
- pluginSkills = listAllSkills().filter((s) => s.enabled && s.plugin !== SCOPE_SKILL_PLUGIN && s.scope !== 'builtin');
129
- }
130
- catch {
131
- pluginSkills = [];
132
- }
133
- const entries = [
134
- ...pluginSkills.map((s) => ({ scope: s.scope, plugin: s.plugin, name: s.name })),
135
- ...nameRungSkillDocs.map((d) => ({ scope: d.scope, plugin: SCOPE_SKILL_PLUGIN, name: d.name })),
136
- ];
137
- if (entries.length === 0)
138
- return '';
139
- // Group by scope+plugin → forest-root names (drop nested children) so each
140
- // source contributes only its top-level skills.
128
+ /** The compact, group-collapsed `name`-rung catalog of substrate `skill` docs.
129
+ * Every leaf is a migrated/generated substrate doc (native, builtin, or a
130
+ * plugin's `<pluginName>/` subtree) there is no second, resolver-provided
131
+ * skill corpus. Each doc self-groups by its top-dir segment: a name with a
132
+ * slash sources to its top segment (the plugin name); a bare name sources to
133
+ * '' (a scope-local native/builtin skill). A plugin whose INDEX is elevated to
134
+ * preview/content renders as its own `### <plugin>` subsection, so its catalog
135
+ * group is dropped here (via `elevatedSources`) to avoid a double render.
136
+ * Reuses skill/shared.ts's renderCatalogSection group-collapse. Returns ''
137
+ * when nothing is in the catalog. */
138
+ function renderSkillCatalog(nameRungSkillDocs, elevatedSources) {
141
139
  const bySource = new Map();
142
- for (const e of entries) {
143
- const key = `${e.scope}\t${e.plugin}`;
144
- const arr = bySource.get(key);
145
- if (arr)
146
- arr.push(e);
140
+ for (const d of nameRungSkillDocs) {
141
+ // INDEX docs are structural ceilings (already renamed to their dir entry by
142
+ // applyCeilings), never catalog leaves — drop defensively.
143
+ if (isIndexName(d.name))
144
+ continue;
145
+ const slash = d.name.indexOf('/');
146
+ const plugin = slash === -1 ? '' : d.name.slice(0, slash);
147
+ const leaf = slash === -1 ? d.name : d.name.slice(slash + 1);
148
+ // A plugin represented by its own elevated ### subsection is dropped from the
149
+ // catalog so it is not rendered twice.
150
+ if (plugin !== '' && elevatedSources.has(plugin))
151
+ continue;
152
+ const key = `${d.scope}\t${plugin}`;
153
+ const src = bySource.get(key);
154
+ if (src)
155
+ src.roots.push(leaf);
147
156
  else
148
- bySource.set(key, [e]);
157
+ bySource.set(key, { scope: d.scope, plugin, roots: [leaf] });
149
158
  }
159
+ if (bySource.size === 0)
160
+ return '';
150
161
  const projectSources = [];
151
162
  const userSources = [];
152
- for (const [key, group] of bySource) {
153
- const [scope, plugin] = key.split('\t');
154
- const names = group.map((g) => g.name);
155
- const roots = names
156
- .filter((n) => !names.some((m) => m !== n && n.startsWith(m + '/')))
163
+ for (const { scope, plugin, roots } of bySource.values()) {
164
+ // Drop nested children so each source contributes only its top-level skills.
165
+ const top = roots
166
+ .filter((n) => !roots.some((m) => m !== n && n.startsWith(m + '/')))
157
167
  .sort();
158
- if (roots.length === 0)
168
+ if (top.length === 0)
159
169
  continue;
160
- (scope === 'project' ? projectSources : userSources).push({ plugin, roots });
170
+ (scope === 'project' ? projectSources : userSources).push({ plugin, roots: top });
161
171
  }
162
172
  const descriptions = new Map();
163
173
  try {
@@ -180,19 +190,21 @@ function renderSkillCatalog(nameRungSkillDocs) {
180
190
  }
181
191
  /** The `## Skills` system-prompt section: every eligible `kind: skill` doc,
182
192
  * rendered at its `system-prompt-visibility`. `name`-rung skills collapse into
183
- * one compact catalog (group-collapsed, unioned with plugin/marketplace
184
- * skills); `preview`/`content`-rung skills each get a `###` sub-section.
185
- * Returns '' when nothing is eligible. */
193
+ * one compact, plugin-grouped catalog; `preview`/`content`-rung skills each get
194
+ * a `###` sub-section. Returns '' when nothing is eligible. */
186
195
  export function renderSkillsSection(nodeId) {
187
196
  const subject = cachedNodeSubject(nodeId, assembleNodeSubject);
188
197
  if (subject === null)
189
198
  return '';
190
199
  const skills = resolverDocs(subject, 'skill');
191
- const catalog = renderSkillCatalog(skills.filter((d) => d.systemPromptVisibility === 'name'));
192
- const elevated = skills
193
- .filter((d) => d.systemPromptVisibility !== 'name')
194
- .map(renderSubSection);
195
- const blocks = [catalog, ...elevated].filter((s) => s !== '');
200
+ const elevated = skills.filter((d) => d.systemPromptVisibility !== 'name');
201
+ // A plugin whose INDEX is elevated surfaces as its own `### <plugin>`
202
+ // subsection (its display name is the plugin/dir name after ceiling rename);
203
+ // the catalog drops that plugin's group so it is not rendered twice.
204
+ const elevatedSources = new Set(elevated.map((d) => d.name));
205
+ const catalog = renderSkillCatalog(skills.filter((d) => d.systemPromptVisibility === 'name'), elevatedSources);
206
+ const elevatedBlocks = elevated.map(renderSubSection);
207
+ const blocks = [catalog, ...elevatedBlocks].filter((s) => s !== '');
196
208
  if (blocks.length === 0)
197
209
  return '';
198
210
  return `## Skills\n\n${blocks.join('\n\n')}`;
@@ -43,8 +43,24 @@ import { getNode } from '../core/canvas/index.js';
43
43
  // one watcher is live. Pattern copied verbatim from agent-inbox-watcher.ts.
44
44
  // ---------------------------------------------------------------------------
45
45
  let liveTimer;
46
- const TICK_MS = 800; // polling cadence
47
- const DEBOUNCE_MS = 1000; // flush once the burst has been quiet for this long
46
+ const DEFAULT_TICK_MS = 800; // polling cadence
47
+ const DEFAULT_DEBOUNCE_MS = 1000; // flush once the burst has been quiet for this long
48
+ // Testability seam: a test can inject a much shorter cadence so it doesn't have
49
+ // to sleep against the real ~800ms/1000ms wall-clock timing. Read per-registration
50
+ // (not as an import-time module const) so each registerCanvasInboxWatcher() call
51
+ // picks up the env current at that moment. Unset → production defaults.
52
+ function resolveCadence() {
53
+ const parse = (raw, fallback) => {
54
+ if (raw === undefined)
55
+ return fallback;
56
+ const n = Number(raw);
57
+ return Number.isFinite(n) && n > 0 ? n : fallback;
58
+ };
59
+ return {
60
+ tickMs: parse(process.env['CRTR_WATCHER_TICK_MS'], DEFAULT_TICK_MS),
61
+ debounceMs: parse(process.env['CRTR_WATCHER_DEBOUNCE_MS'], DEFAULT_DEBOUNCE_MS),
62
+ };
63
+ }
48
64
  // ---------------------------------------------------------------------------
49
65
  // Extension
50
66
  // ---------------------------------------------------------------------------
@@ -56,6 +72,7 @@ const DEBOUNCE_MS = 1000; // flush once the burst has been quiet for this long
56
72
  * the module-level liveTimer guard is the actual stacking prevention.
57
73
  */
58
74
  export function registerCanvasInboxWatcher(pi) {
75
+ const { tickMs: TICK_MS, debounceMs: DEBOUNCE_MS } = resolveCadence();
59
76
  // Capture the latest event context so isIdle() is readable inside the timer
60
77
  // callback, which has no ctx of its own.
61
78
  let lastCtx;
package/dist/types.d.ts CHANGED
@@ -20,6 +20,7 @@ export interface PluginManifest {
20
20
  source?: string;
21
21
  owner?: OwnerRef;
22
22
  kinds?: string[];
23
+ generator?: 'claude-bridge';
23
24
  }
24
25
  export interface MarketplacePluginEntry {
25
26
  name: string;
@@ -44,9 +45,6 @@ export interface ConfigPluginEntry {
44
45
  source_marketplace?: string;
45
46
  version?: string;
46
47
  }
47
- export interface ConfigSkillEntry {
48
- enabled: boolean;
49
- }
50
48
  export type AutoUpdateMode = 'notify' | 'apply' | false;
51
49
  export interface AutoUpdateConfig {
52
50
  crtr: AutoUpdateMode;
@@ -79,7 +77,6 @@ export interface ScopeConfig {
79
77
  schema_version: number;
80
78
  marketplaces: Record<string, ConfigMarketplaceEntry>;
81
79
  plugins: Record<string, ConfigPluginEntry>;
82
- skills: Record<string, ConfigSkillEntry>;
83
80
  auto_update: AutoUpdateConfig;
84
81
  max_panes_per_window: number;
85
82
  canvasNav: CanvasNavConfig;
@@ -107,16 +104,6 @@ export interface SkillFrontmatter {
107
104
  keywords?: string[];
108
105
  type?: SkillType;
109
106
  }
110
- export interface Skill {
111
- name: string;
112
- plugin: string;
113
- scope: Scope;
114
- path: string;
115
- pluginRoot: string;
116
- frontmatter: SkillFrontmatter;
117
- enabled: boolean;
118
- disabledIn?: Scope;
119
- }
120
107
  export interface SubagentFrontmatter {
121
108
  name: string;
122
109
  description?: string;
@@ -127,8 +114,8 @@ export interface SubagentFrontmatter {
127
114
  }
128
115
  export interface Subagent {
129
116
  name: string;
130
- /** Plugin the subagent belongs to, or SCOPE_SKILL_PLUGIN ('_') for a
131
- * scope-root agent stored at `<scope-root>/agents/<name>.md`. */
117
+ /** Plugin the subagent belongs to, or the sentinel '_' for a scope-root
118
+ * agent stored at `<scope-root>/agents/<name>.md`. */
132
119
  plugin: string;
133
120
  scope: Scope;
134
121
  /** Absolute path to the agent's .md file. */
@@ -165,7 +152,6 @@ export declare const STATE_FILE = "state.json";
165
152
  export declare const SKILL_ENTRY_FILE = "SKILL.md";
166
153
  export declare const SKILLS_DIR = "skills";
167
154
  export declare const AGENTS_DIR = "agents";
168
- export declare const SCOPE_SKILL_PLUGIN = "_";
169
155
  export declare const DEFAULT_MAX_PANES_PER_WINDOW = 3;
170
156
  export declare function defaultScopeConfig(): ScopeConfig;
171
157
  /** Built-in canvas-nav binds so an absent config still gives the intended UX.
@@ -173,5 +159,4 @@ export declare function defaultScopeConfig(): ScopeConfig;
173
159
  * 1..9 (focus report N) are generated by the menu layer, not literal entries.
174
160
  * graphBinds: empty — built-in GRAPH keys are reserved, custom keys add on. */
175
161
  export declare function defaultCanvasNavConfig(): CanvasNavConfig;
176
- export declare function skillConfigKey(plugin: string, name: string): string;
177
162
  export declare function defaultScopeState(): ScopeState;
package/dist/types.js CHANGED
@@ -23,17 +23,12 @@ export const SKILLS_DIR = 'skills';
23
23
  // Subagent definitions live as flat `<name>.md` files under `<root>/agents/`,
24
24
  // for both scope roots and plugins. Mirrors SKILLS_DIR.
25
25
  export const AGENTS_DIR = 'agents';
26
- // Sentinel plugin name for skills that live at a scope root (no plugin wrapper).
27
- // Stored as `<scope-root>/skills/<name>/SKILL.md`. Shown in listings without the
28
- // `_/` prefix.
29
- export const SCOPE_SKILL_PLUGIN = '_';
30
26
  export const DEFAULT_MAX_PANES_PER_WINDOW = 3;
31
27
  export function defaultScopeConfig() {
32
28
  return {
33
29
  schema_version: SCHEMA_VERSION,
34
30
  marketplaces: {},
35
31
  plugins: {},
36
- skills: {},
37
32
  auto_update: { crtr: 'notify', content: 'notify', interval_hours: 24 },
38
33
  max_panes_per_window: DEFAULT_MAX_PANES_PER_WINDOW,
39
34
  canvasNav: defaultCanvasNavConfig(),
@@ -55,9 +50,6 @@ export function defaultCanvasNavConfig() {
55
50
  graphBinds: {},
56
51
  };
57
52
  }
58
- export function skillConfigKey(plugin, name) {
59
- return `${plugin}/${name}`;
60
- }
61
53
  export function defaultScopeState() {
62
54
  return { marketplaces: {}, plugins: {} };
63
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouton-kit/crouter",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
4
4
  "description": "crtr — fast access to skills, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,11 +24,12 @@
24
24
  "bin"
25
25
  ],
26
26
  "scripts": {
27
- "build": "rm -rf dist && tsc && cp -R src/builtin-skills dist/builtin-skills && cp -R src/builtin-personas dist/builtin-personas && cp -R src/builtin-views dist/builtin-views && cp -R src/builtin-memory dist/builtin-memory",
27
+ "build": "rm -rf dist && tsc && cp -R src/builtin-personas dist/builtin-personas && cp -R src/builtin-views dist/builtin-views && cp -R src/builtin-memory dist/builtin-memory",
28
28
  "dev": "tsx src/cli.ts",
29
29
  "link": "npm link",
30
30
  "prepublishOnly": "npm run build",
31
- "test": "node --import tsx/esm --test 'src/**/__tests__/**/*.test.ts'"
31
+ "test": "node --import tsx/esm --test 'src/**/__tests__/*.test.ts'",
32
+ "test:full": "node --import tsx/esm --test 'src/**/__tests__/**/*.test.ts'"
32
33
  },
33
34
  "repository": {
34
35
  "type": "git",
@@ -1,5 +0,0 @@
1
- {
2
- "name": "crtr",
3
- "version": "0.2.0",
4
- "description": "Builtin crouter skills — authoring guidance shipped with the binary"
5
- }