@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
@@ -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'];
@@ -78,14 +75,14 @@ function lintFile(file, substrateStore, findings) {
78
75
  export const lintLeaf = defineLeaf({
79
76
  name: 'lint',
80
77
  description: 'validate frontmatter across the whole bounded document corpus',
81
- 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).',
82
79
  help: {
83
80
  name: 'memory lint',
84
81
  summary: 'strict-parse frontmatter across the bounded corpus; non-zero exit on any finding',
85
82
  params: [],
86
83
  output: [
87
84
  { name: 'checked', type: 'number', required: true, constraint: 'Files linted across all corpora.' },
88
- { 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}.' },
89
86
  { name: 'findings', type: 'object[]', required: true, constraint: 'One row per failure: {path, error}. Empty when green.' },
90
87
  ],
91
88
  outputKind: 'object',
@@ -94,40 +91,28 @@ export const lintLeaf = defineLeaf({
94
91
  run: async () => {
95
92
  const findings = [];
96
93
  let memoryCount = 0;
97
- let pluginCount = 0;
98
- let claudeCount = 0;
99
- // 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.
100
97
  // MEMORY.md index files are not substrate docs — YAML-parse only.
101
- for (const scope of ['project', 'user', 'builtin']) {
102
- const dir = scopeMemoryDir(scope);
98
+ const lintDir = (dir) => {
103
99
  if (!dir || !pathExists(dir))
104
- continue;
100
+ return;
105
101
  for (const file of walkFiles(dir, (n) => n.endsWith('.md'))) {
106
102
  if (!relative(dir, file).split(sep).join('/'))
107
103
  continue;
108
104
  memoryCount += 1;
109
105
  lintFile(file, basename(file) !== 'MEMORY.md', findings);
110
106
  }
111
- }
112
- // 2. Every installed plugin's skills tree (builtin + plugin + marketplace).
113
- for (const plugin of listAllPlugins()) {
114
- const skillsRoot = join(plugin.root, SKILLS_DIR);
115
- if (!pathExists(skillsRoot))
116
- continue;
117
- for (const file of walkFiles(skillsRoot, (n) => n === SKILL_ENTRY_FILE)) {
118
- pluginCount += 1;
119
- lintFile(file, false, findings);
120
- }
121
- }
122
- // 3. ~/.claude/skills — read by the runtime, so it lints too.
123
- const claudeSkills = join(homedir(), '.claude', SKILLS_DIR);
124
- if (pathExists(claudeSkills)) {
125
- for (const file of walkFiles(claudeSkills, (n) => n === SKILL_ENTRY_FILE)) {
126
- claudeCount += 1;
127
- 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));
128
113
  }
129
114
  }
130
- const checked = memoryCount + pluginCount + claudeCount;
115
+ const checked = memoryCount;
131
116
  if (findings.length > 0) {
132
117
  // Human/agent path renders only the message — surface every offender as
133
118
  // a scoped stderr notice (the --json path carries them in details too).
@@ -141,7 +126,7 @@ export const lintLeaf = defineLeaf({
141
126
  }
142
127
  return {
143
128
  checked,
144
- corpora: { memory_stores: memoryCount, plugin_skills: pluginCount, claude_skills: claudeCount },
129
+ corpora: { memory_stores: memoryCount },
145
130
  findings: [],
146
131
  follow_up: 'Corpus green — zero invalid frontmatter docs.',
147
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.',
@@ -0,0 +1 @@
1
+ export declare const bridgeBranch: import("../../core/command.js").BranchDef;
@@ -0,0 +1,137 @@
1
+ // `crtr pkg bridge` — generate crouter substrate docs from a Claude skills root
2
+ // and file-level reconcile the bridge plugin (design §2). The mapping lives in
3
+ // core/bridge-map.ts (one testable source of truth); the extension shells to
4
+ // this command rather than duplicating the transform.
5
+ import { join, basename, dirname, isAbsolute } from 'node:path';
6
+ import { existsSync, readdirSync } from 'node:fs';
7
+ import { defineBranch, defineLeaf } from '../../core/command.js';
8
+ import { usage } from '../../core/errors.js';
9
+ import { userScopeRoot } from '../../core/scope.js';
10
+ import { ensureDir, writeJson, writeText, readText, removePath, walkFiles, } from '../../core/fs-utils.js';
11
+ import { parseFrontmatter, parseFrontmatterGeneric } from '../../core/frontmatter.js';
12
+ import { buildBridgeDoc } from '../../core/bridge-map.js';
13
+ import { PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE } from '../../types.js';
14
+ /** Enumerate `<source>/**\/SKILL.md`, stopping recursion at the first SKILL.md
15
+ * on any branch — a dir that holds SKILL.md is a skill; its subdirs are assets,
16
+ * not nested skills. */
17
+ function enumerateSkillFiles(root) {
18
+ const out = [];
19
+ if (!existsSync(root))
20
+ return out;
21
+ const stack = [root];
22
+ while (stack.length) {
23
+ const dir = stack.pop();
24
+ let entries;
25
+ try {
26
+ entries = readdirSync(dir, { withFileTypes: true });
27
+ }
28
+ catch {
29
+ continue;
30
+ }
31
+ const hasSkill = entries.some((e) => e.isFile() && e.name === 'SKILL.md');
32
+ if (hasSkill) {
33
+ out.push(join(dir, 'SKILL.md'));
34
+ continue; // stop descent — deeper dirs are assets
35
+ }
36
+ for (const e of entries)
37
+ if (e.isDirectory())
38
+ stack.push(join(dir, e.name));
39
+ }
40
+ return out;
41
+ }
42
+ const bridgeSync = defineLeaf({
43
+ name: 'sync',
44
+ description: 'generate substrate docs from a Claude skills root',
45
+ whenToUse: 'mirroring a Claude Code skills directory into a crtr bridge plugin: maps each SKILL.md to a substrate memory doc and file-level reconciles the plugin (regenerate, prune stale, preserve hand-authored leaves + INDEX)',
46
+ help: {
47
+ name: 'pkg bridge sync',
48
+ summary: 'mirror a Claude skills root into a crtr bridge plugin (idempotent reconcile)',
49
+ guide: 'Each <source>/skills/<skillname>/SKILL.md becomes ~/.crouter/plugins/<name>/memory/<skillname>.md, stamped generator:claude-bridge. Stamped leaves whose source skill is gone are pruned; hand-authored leaves and any INDEX.md are never touched.',
50
+ params: [
51
+ { kind: 'flag', name: 'source', type: 'path', required: true, constraint: 'Absolute path to a Claude skills root (a dir whose <skillname>/SKILL.md leaves are bridged; recursion stops at the first SKILL.md).' },
52
+ { kind: 'flag', name: 'name', type: 'string', required: true, constraint: 'Bridge plugin name. Dest plugin dir is ~/.crouter/plugins/<name>/.' },
53
+ { kind: 'flag', name: 'source-desc', type: 'string', required: false, constraint: 'Optional plugin description written into the generated plugin.json.' },
54
+ ],
55
+ output: [
56
+ { name: 'plugin', type: 'string', required: true, constraint: 'Bridge plugin name.' },
57
+ { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the bridge plugin directory.' },
58
+ { name: 'written', type: 'number', required: true, constraint: 'Count of substrate leaves generated from source SKILL.md files.' },
59
+ { name: 'pruned', type: 'number', required: true, constraint: 'Count of stale generator:claude-bridge leaves deleted (source skill gone).' },
60
+ { name: 'preserved', type: 'number', required: true, constraint: 'Count of untouched protected leaves (INDEX.md or unstamped hand-authored docs).' },
61
+ ],
62
+ outputKind: 'object',
63
+ effects: ['Writes ~/.crouter/plugins/<name>/.crouter-plugin/plugin.json and memory/*.md. Deletes stale generator:claude-bridge leaves. Never writes or deletes INDEX.md or unstamped docs.'],
64
+ },
65
+ run: async (input) => {
66
+ const source = input['source'];
67
+ const name = input['name'];
68
+ const sourceDesc = input['sourceDesc'];
69
+ if (!isAbsolute(source)) {
70
+ throw usage(`--source must be an absolute path: ${source}`);
71
+ }
72
+ if (!existsSync(source)) {
73
+ throw usage(`--source does not exist: ${source}`);
74
+ }
75
+ const destRoot = join(userScopeRoot(), 'plugins', name);
76
+ const memoryDir = join(destRoot, 'memory');
77
+ // 1. Ensure the bridge plugin manifest.
78
+ const manifestDir = join(destRoot, PLUGIN_MANIFEST_DIR);
79
+ ensureDir(manifestDir);
80
+ const manifest = {
81
+ name,
82
+ version: '0.0.0',
83
+ ...(sourceDesc !== undefined ? { description: sourceDesc } : {}),
84
+ source,
85
+ generator: 'claude-bridge',
86
+ };
87
+ writeJson(join(manifestDir, PLUGIN_MANIFEST_FILE), manifest);
88
+ // 2 + 3. Regenerate one stamped leaf per source skill.
89
+ ensureDir(memoryDir);
90
+ const writtenPaths = new Set();
91
+ for (const file of enumerateSkillFiles(source)) {
92
+ const skillName = basename(dirname(file));
93
+ const parsed = parseFrontmatter(readText(file));
94
+ const doc = buildBridgeDoc({
95
+ name: parsed.data?.name ?? skillName,
96
+ description: parsed.data?.description ?? '',
97
+ body: parsed.body,
98
+ });
99
+ const dest = join(memoryDir, `${skillName}.md`);
100
+ writeText(dest, doc);
101
+ writtenPaths.add(dest);
102
+ }
103
+ const written = writtenPaths.size;
104
+ // 4. Reconcile: prune stale stamped leaves; never touch INDEX.md or
105
+ // unstamped (hand-authored) leaves.
106
+ let pruned = 0;
107
+ let preserved = 0;
108
+ for (const f of walkFiles(memoryDir, (n) => n.endsWith('.md'))) {
109
+ if (writtenPaths.has(f))
110
+ continue; // just regenerated → counted in `written`
111
+ if (basename(f) === 'INDEX.md') {
112
+ preserved++;
113
+ continue;
114
+ }
115
+ const generator = parseFrontmatterGeneric(readText(f)).data?.['generator'];
116
+ if (generator === 'claude-bridge') {
117
+ removePath(f);
118
+ pruned++;
119
+ }
120
+ else {
121
+ preserved++;
122
+ }
123
+ }
124
+ return { plugin: name, path: destRoot, written, pruned, preserved };
125
+ },
126
+ });
127
+ export const bridgeBranch = defineBranch({
128
+ name: 'bridge',
129
+ description: 'mirror Claude skills into crtr bridge plugins',
130
+ whenToUse: 'generating a crtr bridge plugin from a Claude Code skills root — the substrate-doc mapping and file-level reconcile invoked by the pi bridge extension',
131
+ help: {
132
+ name: 'pkg bridge',
133
+ summary: 'mirror Claude skills roots into crtr bridge plugins',
134
+ model: 'A bridge plugin is a generator:claude-bridge plugin whose memory/ docs are generated from a Claude skills root. Hand-authored leaves and INDEX ceilings are preserved across syncs.',
135
+ },
136
+ children: [bridgeSync],
137
+ });
@@ -1,7 +1,8 @@
1
1
  import { defineBranch, defineLeaf } from '../../core/command.js';
2
2
  import { notFound } from '../../core/errors.js';
3
3
  import { paginate } from '../../core/pagination.js';
4
- import { listInstalledPlugins, findPluginByName, listSkillsInPlugin, } from '../../core/resolver.js';
4
+ import { listInstalledPlugins, findPluginByName, } from '../../core/resolver.js';
5
+ import { listAllMemoryDocs } from '../../core/memory-resolver.js';
5
6
  import { resolveScopeArg, projectScopeRoot } from '../../core/scope.js';
6
7
  // ---------------------------------------------------------------------------
7
8
  // plugin.inspect.list
@@ -78,8 +79,8 @@ const pluginList = defineLeaf({
78
79
  // ---------------------------------------------------------------------------
79
80
  const pluginShow = defineLeaf({
80
81
  name: 'show',
81
- description: 'read plugin manifest and skill inventory',
82
- whenToUse: 'reading one installed plugin in detail to decide before you enable, disable, or remove it — returns the full manifest, the skills it provides, its scope, and whether it is currently enabled. Use `pkg plugin inspect list` instead to enumerate plugins rather than drill into one',
82
+ description: 'read plugin manifest and substrate inventory',
83
+ whenToUse: 'reading one installed plugin in detail to decide before you enable, disable, or remove it — returns the full manifest, the substrate docs it provides (its `<pluginName>/` memory subtree), its scope, and whether it is currently enabled. Use `pkg plugin inspect list` instead to enumerate plugins rather than drill into one',
83
84
  help: {
84
85
  name: 'pkg plugin inspect show',
85
86
  summary: 'read plugin manifest and metadata by name',
@@ -93,7 +94,7 @@ const pluginShow = defineLeaf({
93
94
  { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the plugin directory.' },
94
95
  { name: 'enabled', type: 'boolean', required: true, constraint: 'Whether the plugin is active.' },
95
96
  { name: 'manifest', type: 'object', required: true, constraint: 'Full plugin.json contents.' },
96
- { name: 'skills', type: 'object[]', required: true, constraint: 'Each: {name, path, enabled}. Skills provided by the plugin.' },
97
+ { name: 'skills', type: 'object[]', required: true, constraint: 'Each: {name, path}. Substrate docs provided by the plugin (its `<pluginName>/` memory subtree).' },
97
98
  ],
98
99
  outputKind: 'object',
99
100
  effects: ['None. Read-only.'],
@@ -117,7 +118,8 @@ const pluginShow = defineLeaf({
117
118
  if (found === null) {
118
119
  throw notFound(`plugin not found: ${name}`);
119
120
  }
120
- const skills = listSkillsInPlugin(found);
121
+ const prefix = `${found.name}/`;
122
+ const skills = listAllMemoryDocs().filter((d) => d.name.startsWith(prefix));
121
123
  return {
122
124
  name: found.name,
123
125
  scope: found.scope,
@@ -127,7 +129,6 @@ const pluginShow = defineLeaf({
127
129
  skills: skills.map((s) => ({
128
130
  name: s.name,
129
131
  path: s.path,
130
- enabled: s.enabled,
131
132
  })),
132
133
  };
133
134
  },
@@ -4,6 +4,7 @@
4
4
  import { defineBranch } from '../core/command.js';
5
5
  import { pluginBranch } from './pkg/plugin.js';
6
6
  import { marketBranch } from './pkg/market.js';
7
+ import { bridgeBranch } from './pkg/bridge.js';
7
8
  export function registerPkg() {
8
9
  return defineBranch({
9
10
  name: 'pkg',
@@ -16,6 +17,6 @@ export function registerPkg() {
16
17
  name: 'pkg',
17
18
  summary: 'manage plugins and plugin marketplaces',
18
19
  },
19
- children: [pluginBranch, marketBranch],
20
+ children: [pluginBranch, marketBranch, bridgeBranch],
20
21
  });
21
22
  }
@@ -1,14 +1,14 @@
1
- import { join } from 'node:path';
1
+ import { dirname } from 'node:path';
2
2
  import { writeFileSync } from 'node:fs';
3
3
  import { defineLeaf, defineBranch } from '../../core/command.js';
4
4
  import { usage, general, notFound } from '../../core/errors.js';
5
- import { SCOPE_SKILL_PLUGIN, SKILL_ENTRY_FILE, SKILLS_DIR, SKILL_TYPES, isSkillType, } from '../../types.js';
5
+ import { SKILL_TYPES, isSkillType } from '../../types.js';
6
6
  import { parseSkillQualifier, findPluginByName, } from '../../core/resolver.js';
7
- import { resolveScopeArg, requireScopeRoot, scopeSkillsDir, } from '../../core/scope.js';
8
- import { serializeFrontmatter } from '../../core/frontmatter.js';
7
+ import { resolveScopeArg, requireScopeRoot, scopeMemoryDir, pluginMemoryDir, } from '../../core/scope.js';
9
8
  import { ensureScopeInitialized } from '../../core/config.js';
10
9
  import { ensureDir, pathExists } from '../../core/fs-utils.js';
11
10
  import { skillCreatePrompt, skillTemplatePrompt } from '../../prompts/skill.js';
11
+ import { memoryFilePath, serializeMemoryDoc } from '../memory/shared.js';
12
12
  import { VALID_TYPES, resolveWriteScope } from './shared.js';
13
13
  export const authorGuide = defineLeaf({
14
14
  name: 'guide',
@@ -42,25 +42,25 @@ export const authorGuide = defineLeaf({
42
42
  });
43
43
  export const authorScaffold = defineLeaf({
44
44
  name: 'scaffold',
45
- description: 'create an empty SKILL.md stub',
46
- whenToUse: 'creating the empty SKILL.md stub at a `<plugin>/<skill>` qualifier before you fill in content — it writes the frontmatter and file, then points you at the authoring guide.',
45
+ description: 'create an empty skill substrate doc stub',
46
+ whenToUse: 'creating the empty `kind: skill` substrate doc at a `<plugin>/<skill>` qualifier (or a bare `<skill>` name for a scope-owned doc) before you fill in content — it writes the frontmatter and file, then points you at the authoring guide.',
47
47
  help: {
48
48
  name: 'skill author scaffold',
49
- summary: 'create an empty SKILL.md stub at the given qualifier',
49
+ summary: 'create an empty kind:skill substrate doc stub at the given qualifier',
50
50
  params: [
51
- { kind: 'positional', name: 'qualifier', required: true, constraint: 'Skill identifier in <plugin>/<skill> form.' },
52
- { kind: 'flag', name: 'type', type: 'enum', choices: [...VALID_TYPES], required: false, constraint: 'One of: playbook, primer, reference, runbook, freeform.' },
53
- { kind: 'flag', name: 'description', type: 'string', required: false, constraint: 'Short description written to frontmatter.' },
51
+ { kind: 'positional', name: 'qualifier', required: true, constraint: 'Skill identifier: <plugin>/<skill> to scaffold into a plugin\'s memory/, or a bare <skill> for the scope-owned memory/.' },
52
+ { kind: 'flag', name: 'type', type: 'enum', choices: [...VALID_TYPES], required: false, constraint: 'One of: playbook, primer, reference, runbook, freeform. Drives the follow-up authoring guide only — not stored on the substrate doc.' },
53
+ { kind: 'flag', name: 'description', type: 'string', required: false, constraint: 'Read-routing line written to the doc\'s when-and-why-to-read frontmatter.' },
54
54
  { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
55
55
  ],
56
56
  output: [
57
- { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the scaffolded SKILL.md.' },
57
+ { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the scaffolded substrate doc.' },
58
58
  { name: 'follow_up', type: 'string', required: true, constraint: 'Recommended next call to load the authoring guide.' },
59
59
  ],
60
60
  outputKind: 'object',
61
61
  effects: [
62
- 'Creates the skill directory and SKILL.md stub at the resolved location.',
63
- 'Writes frontmatter with name, description (if provided), and type (if provided).',
62
+ 'Creates a `kind: skill` substrate doc stub under the resolved memory/ dir (scope-owned, or the named plugin\'s).',
63
+ 'Writes substrate frontmatter: kind:skill, when-and-why-to-read (from --description if given), system-prompt-visibility:name, file-read-visibility:none.',
64
64
  ],
65
65
  },
66
66
  run: async (input) => {
@@ -82,31 +82,20 @@ export const authorScaffold = defineLeaf({
82
82
  throw usage(`unknown skill type: ${typeStr} / valid: ${SKILL_TYPES.join(' | ')}`);
83
83
  }
84
84
  const skillType = typeStr !== undefined && isSkillType(typeStr) ? typeStr : undefined;
85
- let skillFile;
86
- // Scope-direct: no plugin qualifier, or explicit `_/` sentinel (internal only)
87
- if (pluginName === undefined || pluginName === SCOPE_SKILL_PLUGIN) {
85
+ // Resolve the memory/ dir to scaffold into: the named plugin's, or the
86
+ // scope-owned one. One substrate, one author path.
87
+ let memoryDir;
88
+ if (pluginName === undefined) {
88
89
  const scope = resolveWriteScope(scopeStr);
89
90
  const scopeRootPath = requireScopeRoot(scope);
90
91
  ensureScopeInitialized(scope, scopeRootPath);
91
- const skillsRoot = scopeSkillsDir(scope);
92
- if (!skillsRoot) {
93
- throw general(`no skills dir for scope ${scope}`);
92
+ const dir = scopeMemoryDir(scope);
93
+ if (!dir) {
94
+ throw general(`no memory dir for scope ${scope}`);
94
95
  }
95
- const skillDir = join(skillsRoot, ...skillName.split('/'));
96
- skillFile = join(skillDir, SKILL_ENTRY_FILE);
97
- if (pathExists(skillFile)) {
98
- throw general(`skill already exists: ${skillFile}`);
99
- }
100
- ensureDir(skillDir);
101
- const fm = serializeFrontmatter({
102
- name: skillName,
103
- description,
104
- type: skillType,
105
- });
106
- writeFileSync(skillFile, fm, 'utf8');
96
+ memoryDir = dir;
107
97
  }
108
98
  else {
109
- // Plugin-scoped scaffold
110
99
  const scopeForLookup = scopeStr !== undefined
111
100
  ? (() => {
112
101
  const r = resolveScopeArg(scopeStr);
@@ -119,19 +108,21 @@ export const authorScaffold = defineLeaf({
119
108
  if (!plugin) {
120
109
  throw notFound(`plugin not found: ${pluginName}`);
121
110
  }
122
- const skillDir = join(plugin.root, SKILLS_DIR, ...skillName.split('/'));
123
- skillFile = join(skillDir, SKILL_ENTRY_FILE);
124
- if (pathExists(skillFile)) {
125
- throw general(`skill already exists: ${skillFile}`);
126
- }
127
- ensureDir(skillDir);
128
- const fm = serializeFrontmatter({
129
- name: skillName,
130
- description,
131
- type: skillType,
132
- });
133
- writeFileSync(skillFile, fm, 'utf8');
111
+ memoryDir = pluginMemoryDir(plugin);
112
+ }
113
+ const skillFile = memoryFilePath(memoryDir, skillName);
114
+ if (pathExists(skillFile)) {
115
+ throw general(`skill already exists: ${skillFile}`);
134
116
  }
117
+ ensureDir(dirname(skillFile));
118
+ // Catalog-default substrate frontmatter (design §1.5): a name-rung,
119
+ // file-read-none skill doc. --description becomes the read-routing line.
120
+ const frontmatter = { kind: 'skill' };
121
+ if (description !== undefined)
122
+ frontmatter['when-and-why-to-read'] = description;
123
+ frontmatter['system-prompt-visibility'] = 'name';
124
+ frontmatter['file-read-visibility'] = 'none';
125
+ writeFileSync(skillFile, serializeMemoryDoc(frontmatter, ''), 'utf8');
135
126
  const typeHint = skillType !== undefined ? `--type ${skillType} ` : '';
136
127
  const follow_up = `crtr skill author guide ${typeHint}--topic "${skillName}"`;
137
128
  return { path: skillFile, follow_up };
@@ -1,8 +1,4 @@
1
- import type { Skill, Scope } from '../../types.js';
2
- export declare function formatNeighborQualifier(s: Skill): string;
3
- export declare function formatNeighborKeywords(s: Skill): string;
4
- export declare function buildNeighborsSection(skill: Skill): string | null;
5
- export declare function appendNeighbors(skill: Skill, body: string): string;
1
+ import type { Scope } from '../../types.js';
6
2
  export declare function resolveWriteScope(scopeStr: string | undefined): Scope;
7
3
  export declare const VALID_TYPES: readonly ["playbook", "primer", "reference", "runbook", "freeform"];
8
4
  export type CatalogSource = {