@crouton-kit/crouter 0.3.21 → 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.
- package/dist/commands/canvas-history/read.d.ts +1 -0
- package/dist/commands/canvas-history/read.js +61 -0
- package/dist/commands/canvas-history/search.d.ts +1 -0
- package/dist/commands/canvas-history/search.js +270 -0
- package/dist/commands/canvas-history/show.d.ts +1 -0
- package/dist/commands/canvas-history/show.js +79 -0
- package/dist/commands/canvas-history.d.ts +2 -0
- package/dist/commands/canvas-history.js +33 -0
- package/dist/commands/canvas.js +3 -2
- package/dist/commands/memory/find.js +7 -40
- package/dist/commands/memory/lint.js +19 -34
- package/dist/commands/memory/list.js +8 -18
- package/dist/commands/memory/read.js +4 -37
- package/dist/commands/pkg/bridge.d.ts +1 -0
- package/dist/commands/pkg/bridge.js +137 -0
- package/dist/commands/pkg/plugin-inspect.js +7 -6
- package/dist/commands/pkg.js +2 -1
- package/dist/commands/skill/author.js +35 -44
- package/dist/commands/skill/shared.d.ts +1 -5
- package/dist/commands/skill/shared.js +9 -63
- package/dist/commands/skill.js +5 -6
- package/dist/commands/sys/doctor.js +6 -144
- package/dist/core/__tests__/broker-dormant-wake.test.js +71 -34
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +21 -5
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +21 -5
- package/dist/core/__tests__/child-death-wake.test.js +133 -185
- package/dist/core/__tests__/daemon-liveness.test.js +28 -258
- package/dist/core/__tests__/flagship-lifecycle.test.js +156 -135
- package/dist/core/__tests__/frame-decoder-perf.test.js +51 -25
- package/dist/core/__tests__/{broker-attach-limits.test.js → full/broker-attach-limits.test.js} +62 -35
- package/dist/core/__tests__/{broker-attach-stream.test.js → full/broker-attach-stream.test.js} +54 -31
- package/dist/core/__tests__/full/broker-crash-teardown.test.js +143 -0
- package/dist/core/__tests__/full/broker-dialogs.test.js +153 -0
- package/dist/core/__tests__/full/broker-lifecycle.test.js +116 -0
- package/dist/core/__tests__/{cascade-close.test.js → full/cascade-close.test.js} +1 -1
- package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +292 -0
- package/dist/core/__tests__/{dead-pane-regression.test.js → full/dead-pane-regression.test.js} +2 -2
- package/dist/core/__tests__/{detach-focus.test.js → full/detach-focus.test.js} +5 -5
- package/dist/core/__tests__/{human-new-window-regression.test.js → full/human-new-window-regression.test.js} +2 -2
- package/dist/core/__tests__/{placement-focus.test.js → full/placement-focus.test.js} +5 -5
- package/dist/core/__tests__/{placement-reconcile.test.js → full/placement-reconcile.test.js} +3 -3
- package/dist/core/__tests__/{placement-revive.test.js → full/placement-revive.test.js} +5 -5
- package/dist/core/__tests__/{placement-teardown.test.js → full/placement-teardown.test.js} +4 -4
- package/dist/core/__tests__/{review-render-pane-regression.test.js → full/review-render-pane-regression.test.js} +2 -2
- package/dist/core/__tests__/full/spike-harness.test.js +117 -0
- package/dist/core/__tests__/grace-clock.test.js +72 -75
- package/dist/core/__tests__/helpers/harness.d.ts +35 -1
- package/dist/core/__tests__/helpers/harness.js +70 -12
- package/dist/core/__tests__/live-mutation-verbs.test.js +99 -104
- package/dist/core/__tests__/live-mutation.test.js +111 -134
- package/dist/core/__tests__/revive.test.js +96 -86
- package/dist/core/__tests__/subscription-delivery.test.js +116 -138
- package/dist/core/__tests__/wake-origin.test.js +54 -50
- package/dist/core/bridge-map.d.ts +19 -0
- package/dist/core/bridge-map.js +73 -0
- package/dist/core/canvas/history.d.ts +91 -0
- package/dist/core/canvas/history.js +389 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/config.js +3 -22
- package/dist/core/memory-resolver.d.ts +9 -2
- package/dist/core/memory-resolver.js +47 -7
- package/dist/core/resolver.d.ts +1 -30
- package/dist/core/resolver.js +6 -546
- package/dist/core/runtime/persona.js +5 -7
- package/dist/core/scope.d.ts +7 -5
- package/dist/core/scope.js +16 -17
- package/dist/core/substrate/ceiling.d.ts +28 -0
- package/dist/core/substrate/ceiling.js +87 -0
- package/dist/core/substrate/index.d.ts +2 -0
- package/dist/core/substrate/index.js +1 -0
- package/dist/core/substrate/on-read.js +18 -2
- package/dist/core/substrate/render.d.ts +2 -3
- package/dist/core/substrate/render.js +63 -51
- package/dist/pi-extensions/canvas-inbox-watcher.js +19 -2
- package/dist/types.d.ts +3 -18
- package/dist/types.js +0 -8
- package/package.json +4 -3
- package/dist/builtin-skills/.crouter-plugin/plugin.json +0 -5
- package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +0 -157
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +0 -106
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +0 -49
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +0 -49
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +0 -156
- package/dist/builtin-skills/skills/design/SKILL.md +0 -51
- package/dist/builtin-skills/skills/development/SKILL.md +0 -109
- package/dist/builtin-skills/skills/planning/SKILL.md +0 -59
- package/dist/builtin-skills/skills/spec/SKILL.md +0 -83
- package/dist/commands/skill/state.d.ts +0 -3
- package/dist/commands/skill/state.js +0 -71
- package/dist/core/__tests__/broker-crash-teardown.test.js +0 -116
- package/dist/core/__tests__/broker-dialogs.test.js +0 -126
- package/dist/core/__tests__/broker-lifecycle.test.js +0 -87
- package/dist/core/__tests__/resolver.test.js +0 -181
- package/dist/core/__tests__/spike-harness.test.js +0 -242
- /package/dist/core/__tests__/{broker-attach-limits.test.d.ts → full/broker-attach-limits.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-attach-stream.test.d.ts → full/broker-attach-stream.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-crash-teardown.test.d.ts → full/broker-crash-teardown.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-dialogs.test.d.ts → full/broker-dialogs.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-lifecycle.test.d.ts → full/broker-lifecycle.test.d.ts} +0 -0
- /package/dist/core/__tests__/{cascade-close.test.d.ts → full/cascade-close.test.d.ts} +0 -0
- /package/dist/core/__tests__/{dead-pane-regression.test.d.ts → full/daemon-liveness-pane.full.test.d.ts} +0 -0
- /package/dist/core/__tests__/{detach-focus.test.d.ts → full/dead-pane-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-new-window-regression.test.d.ts → full/detach-focus.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-focus.test.d.ts → full/human-new-window-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-reconcile.test.d.ts → full/placement-focus.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-revive.test.d.ts → full/placement-reconcile.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-teardown.test.d.ts → full/placement-revive.test.d.ts} +0 -0
- /package/dist/core/__tests__/{resolver.test.d.ts → full/placement-teardown.test.d.ts} +0 -0
- /package/dist/core/__tests__/{review-render-pane-regression.test.d.ts → full/review-render-pane-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{spike-harness.test.d.ts → full/spike-harness.test.d.ts} +0 -0
package/dist/core/resolver.js
CHANGED
|
@@ -1,35 +1,12 @@
|
|
|
1
|
-
import { join
|
|
2
|
-
import { SCOPE_SKILL_PLUGIN, SKILL_ENTRY_FILE, SKILLS_DIR, skillConfigKey, } from '../types.js';
|
|
1
|
+
import { join } from 'node:path';
|
|
3
2
|
import { readConfig } from './config.js';
|
|
4
|
-
import { listDirs, pathExists
|
|
3
|
+
import { listDirs, pathExists } from './fs-utils.js';
|
|
5
4
|
import { readMarketplaceManifest, readPluginManifest } from './manifest.js';
|
|
6
|
-
import { parseFrontmatter } from './frontmatter.js';
|
|
7
|
-
import { ambiguous, notFound, usage } from './errors.js';
|
|
8
|
-
import { warn } from './output.js';
|
|
9
5
|
import { InputError } from './io.js';
|
|
10
|
-
import {
|
|
11
|
-
function getBuiltinPlugin() {
|
|
12
|
-
const root = builtinSkillsRoot();
|
|
13
|
-
if (!pathExists(root))
|
|
14
|
-
return null;
|
|
15
|
-
const manifest = readPluginManifest(root);
|
|
16
|
-
if (!manifest)
|
|
17
|
-
return null;
|
|
18
|
-
return {
|
|
19
|
-
name: manifest.name,
|
|
20
|
-
scope: 'builtin',
|
|
21
|
-
root,
|
|
22
|
-
manifest,
|
|
23
|
-
enabled: true,
|
|
24
|
-
builtin: true,
|
|
25
|
-
version: manifest.version,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
6
|
+
import { marketplacesDir, pluginsDir, projectScopeRoot, userScopeRoot, } from './scope.js';
|
|
28
7
|
export function listInstalledPlugins(scope) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return builtin ? [builtin] : [];
|
|
32
|
-
}
|
|
8
|
+
// The builtin scope has no scopeRoot, so pluginsDir('builtin') is null and
|
|
9
|
+
// this returns [] — builtin content is the memory substrate, not plugins.
|
|
33
10
|
const dir = pluginsDir(scope);
|
|
34
11
|
if (!dir || !pathExists(dir))
|
|
35
12
|
return [];
|
|
@@ -77,406 +54,12 @@ export function findPluginByName(name, scope) {
|
|
|
77
54
|
}
|
|
78
55
|
return null;
|
|
79
56
|
}
|
|
80
|
-
function loadScopeConfigs() {
|
|
81
|
-
const user = readConfig('user');
|
|
82
|
-
if (projectScopeRoot())
|
|
83
|
-
return { project: readConfig('project'), user };
|
|
84
|
-
return { user };
|
|
85
|
-
}
|
|
86
|
-
export function effectiveSkillEnabled(pluginName, skillName, cfgs) {
|
|
87
|
-
const key = skillConfigKey(pluginName, skillName);
|
|
88
|
-
if (cfgs.project && cfgs.project.skills[key] !== undefined) {
|
|
89
|
-
const e = cfgs.project.skills[key].enabled;
|
|
90
|
-
return e ? { enabled: true } : { enabled: false, disabledIn: 'project' };
|
|
91
|
-
}
|
|
92
|
-
if (cfgs.user.skills[key] !== undefined) {
|
|
93
|
-
const e = cfgs.user.skills[key].enabled;
|
|
94
|
-
return e ? { enabled: true } : { enabled: false, disabledIn: 'user' };
|
|
95
|
-
}
|
|
96
|
-
return { enabled: true };
|
|
97
|
-
}
|
|
98
|
-
/** Parse one skill file's frontmatter at the COLLECTION layer: the strict
|
|
99
|
-
* parser THROWS on invalid YAML (the frontmatter contract is "valid YAML"),
|
|
100
|
-
* so a single malformed SKILL.md must not brick a whole `skill find`/catalog
|
|
101
|
-
* scan across the corpus. On a parse error, emit a clear scoped notice naming
|
|
102
|
-
* the file and return null so the iterator SKIPS it and continues. A doc with
|
|
103
|
-
* no frontmatter block parses fine (data === null) and is kept. */
|
|
104
|
-
function readSkillFrontmatterSafe(file) {
|
|
105
|
-
try {
|
|
106
|
-
return parseFrontmatter(readText(file));
|
|
107
|
-
}
|
|
108
|
-
catch (e) {
|
|
109
|
-
const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
|
|
110
|
-
warn(`invalid frontmatter in ${file}: ${msg}`);
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
export function listSkillsInPlugin(plugin, cfgs) {
|
|
115
|
-
const skillsRoot = join(plugin.root, SKILLS_DIR);
|
|
116
|
-
if (!pathExists(skillsRoot))
|
|
117
|
-
return [];
|
|
118
|
-
const configs = cfgs === undefined ? loadScopeConfigs() : cfgs;
|
|
119
|
-
const skills = [];
|
|
120
|
-
const skillFiles = walkFiles(skillsRoot, (n) => n === SKILL_ENTRY_FILE);
|
|
121
|
-
for (const file of skillFiles) {
|
|
122
|
-
const rel = relative(skillsRoot, dirname(file));
|
|
123
|
-
const name = rel.split(sep).join('/');
|
|
124
|
-
if (!name)
|
|
125
|
-
continue;
|
|
126
|
-
const parsed = readSkillFrontmatterSafe(file);
|
|
127
|
-
if (parsed === null)
|
|
128
|
-
continue;
|
|
129
|
-
const { data } = parsed;
|
|
130
|
-
const { enabled, disabledIn } = effectiveSkillEnabled(plugin.name, name, configs);
|
|
131
|
-
skills.push({
|
|
132
|
-
name,
|
|
133
|
-
plugin: plugin.name,
|
|
134
|
-
scope: plugin.scope,
|
|
135
|
-
path: file,
|
|
136
|
-
pluginRoot: plugin.root,
|
|
137
|
-
frontmatter: data === null ? { name } : data,
|
|
138
|
-
enabled,
|
|
139
|
-
disabledIn,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
143
|
-
}
|
|
144
|
-
export function listScopeRootSkills(scope, cfgs) {
|
|
145
|
-
if (scope === 'builtin')
|
|
146
|
-
return [];
|
|
147
|
-
const skillsRoot = scopeSkillsDir(scope);
|
|
148
|
-
if (!skillsRoot || !pathExists(skillsRoot))
|
|
149
|
-
return [];
|
|
150
|
-
const configs = cfgs === undefined ? loadScopeConfigs() : cfgs;
|
|
151
|
-
const skills = [];
|
|
152
|
-
const skillFiles = walkFiles(skillsRoot, (n) => n === SKILL_ENTRY_FILE);
|
|
153
|
-
for (const file of skillFiles) {
|
|
154
|
-
const rel = relative(skillsRoot, dirname(file));
|
|
155
|
-
const name = rel.split(sep).join('/');
|
|
156
|
-
if (!name)
|
|
157
|
-
continue;
|
|
158
|
-
const parsed = readSkillFrontmatterSafe(file);
|
|
159
|
-
if (parsed === null)
|
|
160
|
-
continue;
|
|
161
|
-
const { data } = parsed;
|
|
162
|
-
const { enabled, disabledIn } = effectiveSkillEnabled(SCOPE_SKILL_PLUGIN, name, configs);
|
|
163
|
-
skills.push({
|
|
164
|
-
name,
|
|
165
|
-
plugin: SCOPE_SKILL_PLUGIN,
|
|
166
|
-
scope,
|
|
167
|
-
path: file,
|
|
168
|
-
pluginRoot: skillsRoot,
|
|
169
|
-
frontmatter: data === null ? { name } : data,
|
|
170
|
-
enabled,
|
|
171
|
-
disabledIn,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
175
|
-
}
|
|
176
|
-
export function listAllSkills(scopeFilter) {
|
|
177
|
-
const plugins = scopeFilter ? listInstalledPlugins(scopeFilter) : listAllPlugins();
|
|
178
|
-
const cfgs = loadScopeConfigs();
|
|
179
|
-
const scopes = scopeFilter
|
|
180
|
-
? [scopeFilter]
|
|
181
|
-
: [projectScopeRoot() ? 'project' : null, 'user'].filter(Boolean);
|
|
182
|
-
return [
|
|
183
|
-
...scopes.flatMap((s) => listScopeRootSkills(s, cfgs)),
|
|
184
|
-
...plugins.filter((p) => p.enabled).flatMap((p) => listSkillsInPlugin(p, cfgs)),
|
|
185
|
-
];
|
|
186
|
-
}
|
|
187
|
-
function enumerateNeighborPool(skill) {
|
|
188
|
-
if (skill.plugin === SCOPE_SKILL_PLUGIN) {
|
|
189
|
-
return listScopeRootSkills(skill.scope);
|
|
190
|
-
}
|
|
191
|
-
const plugin = listInstalledPlugins(skill.scope).find((p) => p.name === skill.plugin);
|
|
192
|
-
if (!plugin)
|
|
193
|
-
return [];
|
|
194
|
-
return listSkillsInPlugin(plugin);
|
|
195
|
-
}
|
|
196
|
-
export function listSkillSiblings(skill) {
|
|
197
|
-
const pool = enumerateNeighborPool(skill);
|
|
198
|
-
const segs = skill.name.split('/');
|
|
199
|
-
const depth = segs.length;
|
|
200
|
-
const parentPrefix = segs.slice(0, -1).join('/');
|
|
201
|
-
return pool.filter((s) => {
|
|
202
|
-
if (s.name === skill.name)
|
|
203
|
-
return false;
|
|
204
|
-
const sSegs = s.name.split('/');
|
|
205
|
-
if (sSegs.length !== depth)
|
|
206
|
-
return false;
|
|
207
|
-
if (parentPrefix === '')
|
|
208
|
-
return !s.name.includes('/');
|
|
209
|
-
return s.name.startsWith(parentPrefix + '/');
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
export function listSkillChildren(skill) {
|
|
213
|
-
const pool = enumerateNeighborPool(skill);
|
|
214
|
-
const prefix = skill.name + '/';
|
|
215
|
-
return pool.filter((s) => {
|
|
216
|
-
if (!s.name.startsWith(prefix))
|
|
217
|
-
return false;
|
|
218
|
-
const rest = s.name.slice(prefix.length);
|
|
219
|
-
return rest.length > 0 && !rest.includes('/');
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
export function resolveSkill(rawName, opts = {}) {
|
|
223
|
-
const parsed = parseSkillQualifier(rawName);
|
|
224
|
-
if (parsed.scope && opts.scope && parsed.scope !== opts.scope) {
|
|
225
|
-
throw usage(`scope conflict: identifier "${rawName}" uses scope "${parsed.scope}" but --scope is "${opts.scope}"`);
|
|
226
|
-
}
|
|
227
|
-
const effectiveScope = opts.scope ?? parsed.scope;
|
|
228
|
-
// Lookup-based disambiguation: if segments[0] matches an installed plugin, treat it as plugin.
|
|
229
|
-
// Otherwise the entire segments array is the skill path under the scope-direct plugin.
|
|
230
|
-
let pluginQualifier;
|
|
231
|
-
let skillName;
|
|
232
|
-
if (parsed.segments.length === 0) {
|
|
233
|
-
throw usage(`skill name required`);
|
|
234
|
-
}
|
|
235
|
-
if (opts.pluginFilter !== undefined) {
|
|
236
|
-
// Explicit plugin filter overrides disambiguation.
|
|
237
|
-
pluginQualifier = opts.pluginFilter;
|
|
238
|
-
skillName = parsed.segments.join('/');
|
|
239
|
-
}
|
|
240
|
-
else if (parsed.segments.length > 1) {
|
|
241
|
-
const maybePlugin = parsed.segments[0];
|
|
242
|
-
const pluginMatch = findPluginByName(maybePlugin, effectiveScope) ??
|
|
243
|
-
(effectiveScope === undefined ? null : findPluginByName(maybePlugin));
|
|
244
|
-
if (pluginMatch !== null) {
|
|
245
|
-
pluginQualifier = maybePlugin;
|
|
246
|
-
skillName = parsed.segments.slice(1).join('/');
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
pluginQualifier = undefined;
|
|
250
|
-
skillName = parsed.segments.join('/');
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
pluginQualifier = undefined;
|
|
255
|
-
skillName = parsed.segments[0];
|
|
256
|
-
}
|
|
257
|
-
if (pluginQualifier && opts.pluginFilter && pluginQualifier !== opts.pluginFilter) {
|
|
258
|
-
throw usage(`plugin conflict: identifier "${rawName}" uses plugin "${pluginQualifier}" but --plugin is "${opts.pluginFilter}"`);
|
|
259
|
-
}
|
|
260
|
-
const effectivePluginFilter = opts.pluginFilter ?? pluginQualifier;
|
|
261
|
-
const direct = findSkillMatches(skillName, pluginQualifier, effectiveScope, effectivePluginFilter);
|
|
262
|
-
if (direct.length > 0)
|
|
263
|
-
return pickMatch(direct, skillName, pluginQualifier);
|
|
264
|
-
// Leaf-name fallback: the caller supplied only the final path segment
|
|
265
|
-
// (e.g. "cli-design" for "ai/interface/cli-design"). A direct path lookup
|
|
266
|
-
// missed because the skill lives under a nested path. Match by last segment.
|
|
267
|
-
const byLeaf = findSkillsByLeaf(skillName, pluginQualifier, effectiveScope, effectivePluginFilter);
|
|
268
|
-
if (byLeaf.length === 1)
|
|
269
|
-
return byLeaf[0];
|
|
270
|
-
if (byLeaf.length > 1) {
|
|
271
|
-
throw ambiguous(formatLeafAmbiguousMessage(skillName, byLeaf), {
|
|
272
|
-
skill: skillName,
|
|
273
|
-
candidates: byLeaf.map((m) => ({
|
|
274
|
-
id: formatSkillId(m),
|
|
275
|
-
plugin: m.plugin,
|
|
276
|
-
scope: m.scope,
|
|
277
|
-
path: m.path,
|
|
278
|
-
})),
|
|
279
|
-
next: 'Multiple skills share this leaf name. Re-run with one of the full paths in candidates.',
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
throw notFound(formatNotFoundMessage(rawName, skillName, pluginQualifier), {
|
|
283
|
-
skill: skillName,
|
|
284
|
-
plugin: pluginQualifier,
|
|
285
|
-
scope: parsed.scope,
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
/** Canonical, unambiguous identifier for a skill. Scope-root skills are
|
|
289
|
-
* qualified by scope; plugin skills by plugin name. */
|
|
290
|
-
function formatSkillId(s) {
|
|
291
|
-
return s.plugin === SCOPE_SKILL_PLUGIN ? `${s.scope}/${s.name}` : `${s.plugin}/${s.name}`;
|
|
292
|
-
}
|
|
293
|
-
/** Match skills whose final path segment equals `leaf`. Only meaningful when
|
|
294
|
-
* `leaf` is a bare segment (no slash) — a slashed query can never equal a
|
|
295
|
-
* single segment, so this returns empty and the caller falls through. */
|
|
296
|
-
function findSkillsByLeaf(leaf, pluginQualifier, scope, pluginFilter) {
|
|
297
|
-
if (leaf.includes('/'))
|
|
298
|
-
return [];
|
|
299
|
-
let all;
|
|
300
|
-
try {
|
|
301
|
-
all = scope ? listAllSkills(scope) : listAllSkills();
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
return [];
|
|
305
|
-
}
|
|
306
|
-
return all.filter((s) => {
|
|
307
|
-
if ((s.name.split('/').pop() ?? s.name) !== leaf)
|
|
308
|
-
return false;
|
|
309
|
-
if (pluginQualifier && s.plugin !== pluginQualifier)
|
|
310
|
-
return false;
|
|
311
|
-
if (pluginFilter && s.plugin !== pluginFilter)
|
|
312
|
-
return false;
|
|
313
|
-
return true;
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
function formatLeafAmbiguousMessage(leaf, matches) {
|
|
317
|
-
const ids = matches.map(formatSkillId).join(', ');
|
|
318
|
-
return `ambiguous skill: ${leaf} matches multiple skills: ${ids}`;
|
|
319
|
-
}
|
|
320
|
-
function findSkillMatches(name, pluginQualifier, scope, pluginFilter) {
|
|
321
|
-
const plugins = scope ? listInstalledPlugins(scope) : listAllPlugins();
|
|
322
|
-
const enabledPlugins = plugins.filter((p) => p.enabled);
|
|
323
|
-
const cfgs = loadScopeConfigs();
|
|
324
|
-
const matches = [];
|
|
325
|
-
// Scope-root skills first — they're the user's own captured knowledge.
|
|
326
|
-
if (!pluginFilter &&
|
|
327
|
-
(pluginQualifier === undefined || pluginQualifier === SCOPE_SKILL_PLUGIN)) {
|
|
328
|
-
const scopes = scope
|
|
329
|
-
? [scope]
|
|
330
|
-
: [projectScopeRoot() ? 'project' : null, 'user'].filter(Boolean);
|
|
331
|
-
for (const s of scopes) {
|
|
332
|
-
const skillsRoot = scopeSkillsDir(s);
|
|
333
|
-
if (!skillsRoot)
|
|
334
|
-
continue;
|
|
335
|
-
const skillPath = join(skillsRoot, ...name.split('/'), SKILL_ENTRY_FILE);
|
|
336
|
-
if (!pathExists(skillPath))
|
|
337
|
-
continue;
|
|
338
|
-
const source = readText(skillPath);
|
|
339
|
-
const { data } = parseFrontmatter(source);
|
|
340
|
-
const { enabled, disabledIn } = effectiveSkillEnabled(SCOPE_SKILL_PLUGIN, name, cfgs);
|
|
341
|
-
matches.push({
|
|
342
|
-
name,
|
|
343
|
-
plugin: SCOPE_SKILL_PLUGIN,
|
|
344
|
-
scope: s,
|
|
345
|
-
path: skillPath,
|
|
346
|
-
pluginRoot: skillsRoot,
|
|
347
|
-
frontmatter: data === null ? { name } : data,
|
|
348
|
-
enabled,
|
|
349
|
-
disabledIn,
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
const ordered = orderPluginsByResolution(enabledPlugins);
|
|
354
|
-
for (const plugin of ordered) {
|
|
355
|
-
if (pluginQualifier && plugin.name !== pluginQualifier)
|
|
356
|
-
continue;
|
|
357
|
-
if (pluginFilter && plugin.name !== pluginFilter)
|
|
358
|
-
continue;
|
|
359
|
-
const skillPath = join(plugin.root, SKILLS_DIR, ...name.split('/'), SKILL_ENTRY_FILE);
|
|
360
|
-
if (!pathExists(skillPath))
|
|
361
|
-
continue;
|
|
362
|
-
const source = readText(skillPath);
|
|
363
|
-
const { data } = parseFrontmatter(source);
|
|
364
|
-
const { enabled, disabledIn } = effectiveSkillEnabled(plugin.name, name, cfgs);
|
|
365
|
-
matches.push({
|
|
366
|
-
name,
|
|
367
|
-
plugin: plugin.name,
|
|
368
|
-
scope: plugin.scope,
|
|
369
|
-
path: skillPath,
|
|
370
|
-
pluginRoot: plugin.root,
|
|
371
|
-
frontmatter: data === null ? { name } : data,
|
|
372
|
-
enabled,
|
|
373
|
-
disabledIn,
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
return matches;
|
|
377
|
-
}
|
|
378
|
-
function pickMatch(matches, name, pluginQualifier) {
|
|
379
|
-
if (matches.length === 1)
|
|
380
|
-
return matches[0];
|
|
381
|
-
const sameScopeAndPlugin = matches.every((m) => m.plugin === matches[0].plugin && m.scope === matches[0].scope);
|
|
382
|
-
if (sameScopeAndPlugin)
|
|
383
|
-
return matches[0];
|
|
384
|
-
if (!pluginQualifier)
|
|
385
|
-
return matches[0];
|
|
386
|
-
throw ambiguous(`ambiguous skill: ${name}`, {
|
|
387
|
-
skill: name,
|
|
388
|
-
candidates: matches.map((m) => ({
|
|
389
|
-
plugin: m.plugin,
|
|
390
|
-
scope: m.scope,
|
|
391
|
-
path: m.path,
|
|
392
|
-
})),
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
function formatNotFoundMessage(rawName, skillName, pluginQualifier) {
|
|
396
|
-
const suggestions = suggestSkills(skillName, pluginQualifier);
|
|
397
|
-
const lines = [`skill not found: ${rawName}`];
|
|
398
|
-
lines.push(' expected forms: <name>, <plugin>/<name>, <scope>/<name>, <scope>/<plugin>/<name>');
|
|
399
|
-
if (suggestions.length > 0) {
|
|
400
|
-
const formatted = suggestions
|
|
401
|
-
.map((s) => s.plugin === SCOPE_SKILL_PLUGIN ? s.name : `${s.plugin}/${s.name}`)
|
|
402
|
-
.slice(0, 3);
|
|
403
|
-
lines.push(` did you mean: ${formatted.join(', ')}`);
|
|
404
|
-
}
|
|
405
|
-
else {
|
|
406
|
-
lines.push(' run `crtr memory list` or `crtr memory find "<query>"` to discover skills');
|
|
407
|
-
}
|
|
408
|
-
return lines.join('\n');
|
|
409
|
-
}
|
|
410
|
-
function suggestSkills(name, plugin) {
|
|
411
|
-
let all;
|
|
412
|
-
try {
|
|
413
|
-
all = listAllSkills();
|
|
414
|
-
}
|
|
415
|
-
catch {
|
|
416
|
-
return [];
|
|
417
|
-
}
|
|
418
|
-
const target = name.toLowerCase();
|
|
419
|
-
const targetBase = target.split('/').pop() ?? target;
|
|
420
|
-
const targetPluginGuess = target.includes('/') ? target.split('/')[0] : undefined;
|
|
421
|
-
const exactName = all.filter((s) => s.name.toLowerCase() === target);
|
|
422
|
-
if (exactName.length > 0)
|
|
423
|
-
return exactName;
|
|
424
|
-
const exactBase = all.filter((s) => {
|
|
425
|
-
const sBase = s.name.toLowerCase().split('/').pop() ?? s.name.toLowerCase();
|
|
426
|
-
return sBase === targetBase;
|
|
427
|
-
});
|
|
428
|
-
if (exactBase.length > 0)
|
|
429
|
-
return exactBase;
|
|
430
|
-
const scored = all
|
|
431
|
-
.map((s) => {
|
|
432
|
-
const sName = s.name.toLowerCase();
|
|
433
|
-
const sBase = sName.split('/').pop() ?? sName;
|
|
434
|
-
const sPlugin = s.plugin.toLowerCase();
|
|
435
|
-
let score = 0;
|
|
436
|
-
if (plugin !== undefined && sPlugin === plugin.toLowerCase())
|
|
437
|
-
score += 5;
|
|
438
|
-
if (targetPluginGuess !== undefined && sPlugin === targetPluginGuess)
|
|
439
|
-
score += 5;
|
|
440
|
-
if (sName.includes(target) || target.includes(sName))
|
|
441
|
-
score += 4;
|
|
442
|
-
if (sBase.includes(targetBase) || targetBase.includes(sBase))
|
|
443
|
-
score += 3;
|
|
444
|
-
if (editDistance(sBase, targetBase) <= 2)
|
|
445
|
-
score += 4;
|
|
446
|
-
return { skill: s, score };
|
|
447
|
-
})
|
|
448
|
-
.filter((x) => x.score > 0)
|
|
449
|
-
.sort((a, b) => b.score - a.score);
|
|
450
|
-
return scored.slice(0, 3).map((x) => x.skill);
|
|
451
|
-
}
|
|
452
|
-
function editDistance(a, b) {
|
|
453
|
-
if (a === b)
|
|
454
|
-
return 0;
|
|
455
|
-
if (a.length === 0)
|
|
456
|
-
return b.length;
|
|
457
|
-
if (b.length === 0)
|
|
458
|
-
return a.length;
|
|
459
|
-
const prev = new Array(b.length + 1);
|
|
460
|
-
const curr = new Array(b.length + 1);
|
|
461
|
-
for (let j = 0; j <= b.length; j++)
|
|
462
|
-
prev[j] = j;
|
|
463
|
-
for (let i = 1; i <= a.length; i++) {
|
|
464
|
-
curr[0] = i;
|
|
465
|
-
for (let j = 1; j <= b.length; j++) {
|
|
466
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
467
|
-
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
468
|
-
}
|
|
469
|
-
for (let j = 0; j <= b.length; j++)
|
|
470
|
-
prev[j] = curr[j];
|
|
471
|
-
}
|
|
472
|
-
return prev[b.length];
|
|
473
|
-
}
|
|
474
57
|
const SCOPE_QUALIFIERS = new Set(['user', 'project']);
|
|
475
58
|
// Accepted identifier forms:
|
|
476
59
|
// <name> — bare name; scope-root first, then plugins
|
|
477
60
|
// <plugin>/<name> — explicit plugin (plugin may contain slashes)
|
|
478
61
|
// <scope>/<name> — scope-root in a specific scope
|
|
479
|
-
// <scope>/<plugin>/<name> — fully qualified
|
|
62
|
+
// <scope>/<plugin>/<name> — fully qualified
|
|
480
63
|
export function parseSkillQualifier(raw) {
|
|
481
64
|
if (raw.includes(':')) {
|
|
482
65
|
const suggested = raw.replace(/:/g, '/');
|
|
@@ -495,21 +78,6 @@ export function parseSkillQualifier(raw) {
|
|
|
495
78
|
}
|
|
496
79
|
return { segments };
|
|
497
80
|
}
|
|
498
|
-
function orderPluginsByResolution(plugins) {
|
|
499
|
-
const score = (p) => {
|
|
500
|
-
if (p.scope === 'builtin')
|
|
501
|
-
return 4;
|
|
502
|
-
const fromMarketplace = Boolean(p.sourceMarketplace);
|
|
503
|
-
if (p.scope === 'project' && !fromMarketplace)
|
|
504
|
-
return 0;
|
|
505
|
-
if (p.scope === 'user' && !fromMarketplace)
|
|
506
|
-
return 1;
|
|
507
|
-
if (p.scope === 'project' && fromMarketplace)
|
|
508
|
-
return 2;
|
|
509
|
-
return 3;
|
|
510
|
-
};
|
|
511
|
-
return [...plugins].sort((a, b) => score(a) - score(b));
|
|
512
|
-
}
|
|
513
81
|
export function listInstalledMarketplaces(scope) {
|
|
514
82
|
const dir = marketplacesDir(scope);
|
|
515
83
|
if (!dir || !pathExists(dir))
|
|
@@ -555,114 +123,6 @@ export function findMarketplaceByName(name, scope) {
|
|
|
555
123
|
}
|
|
556
124
|
return null;
|
|
557
125
|
}
|
|
558
|
-
export function resolveCategory(name, opts = {}) {
|
|
559
|
-
const parsed = parseSkillQualifier(name);
|
|
560
|
-
if (parsed.segments.length === 0)
|
|
561
|
-
return null;
|
|
562
|
-
const effectiveScope = opts.scope ?? parsed.scope;
|
|
563
|
-
let pluginQualifier;
|
|
564
|
-
let subpath;
|
|
565
|
-
if (opts.pluginFilter !== undefined) {
|
|
566
|
-
pluginQualifier = opts.pluginFilter;
|
|
567
|
-
const sub = parsed.segments.join('/');
|
|
568
|
-
subpath = sub || undefined;
|
|
569
|
-
}
|
|
570
|
-
else if (parsed.segments.length > 1) {
|
|
571
|
-
const maybePlugin = parsed.segments[0];
|
|
572
|
-
const pluginMatch = findPluginByName(maybePlugin, effectiveScope) ??
|
|
573
|
-
(effectiveScope === undefined ? null : findPluginByName(maybePlugin));
|
|
574
|
-
if (pluginMatch !== null) {
|
|
575
|
-
pluginQualifier = maybePlugin;
|
|
576
|
-
subpath = parsed.segments.slice(1).join('/');
|
|
577
|
-
}
|
|
578
|
-
else {
|
|
579
|
-
pluginQualifier = undefined;
|
|
580
|
-
subpath = parsed.segments.join('/');
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
else {
|
|
584
|
-
const maybePlugin = parsed.segments[0];
|
|
585
|
-
const pluginMatch = findPluginByName(maybePlugin, effectiveScope) ??
|
|
586
|
-
(effectiveScope === undefined ? null : findPluginByName(maybePlugin));
|
|
587
|
-
if (pluginMatch !== null) {
|
|
588
|
-
pluginQualifier = maybePlugin;
|
|
589
|
-
subpath = undefined;
|
|
590
|
-
}
|
|
591
|
-
else {
|
|
592
|
-
pluginQualifier = undefined;
|
|
593
|
-
subpath = maybePlugin;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
let skills;
|
|
597
|
-
let dir;
|
|
598
|
-
let id;
|
|
599
|
-
let resolvedScope;
|
|
600
|
-
if (pluginQualifier !== undefined) {
|
|
601
|
-
const plugin = findPluginByName(pluginQualifier, effectiveScope) ??
|
|
602
|
-
(effectiveScope === undefined ? null : findPluginByName(pluginQualifier));
|
|
603
|
-
if (!plugin)
|
|
604
|
-
return null;
|
|
605
|
-
resolvedScope = plugin.scope;
|
|
606
|
-
const allPluginSkills = listSkillsInPlugin(plugin);
|
|
607
|
-
if (subpath === undefined) {
|
|
608
|
-
skills = allPluginSkills;
|
|
609
|
-
dir = join(plugin.root, SKILLS_DIR);
|
|
610
|
-
id = pluginQualifier;
|
|
611
|
-
}
|
|
612
|
-
else {
|
|
613
|
-
skills = allPluginSkills.filter((s) => s.name.startsWith(subpath + '/'));
|
|
614
|
-
dir = join(plugin.root, SKILLS_DIR, ...subpath.split('/'));
|
|
615
|
-
id = `${pluginQualifier}/${subpath}`;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
else if (subpath !== undefined) {
|
|
619
|
-
const scope = effectiveScope ?? 'user';
|
|
620
|
-
resolvedScope = scope;
|
|
621
|
-
const skillsRoot = scopeSkillsDir(scope);
|
|
622
|
-
if (!skillsRoot)
|
|
623
|
-
return null;
|
|
624
|
-
const allScopeSkills = listScopeRootSkills(scope);
|
|
625
|
-
skills = allScopeSkills.filter((s) => s.name.startsWith(subpath + '/'));
|
|
626
|
-
dir = join(skillsRoot, ...subpath.split('/'));
|
|
627
|
-
id = `${scope}/${subpath}`;
|
|
628
|
-
}
|
|
629
|
-
else {
|
|
630
|
-
return null;
|
|
631
|
-
}
|
|
632
|
-
if (skills.length === 0)
|
|
633
|
-
return null;
|
|
634
|
-
const indexMd = join(dir, 'index.md');
|
|
635
|
-
const indexPath = pathExists(indexMd) ? indexMd : undefined;
|
|
636
|
-
return { id, plugin: pluginQualifier, scope: resolvedScope, dir, indexPath, skills };
|
|
637
|
-
}
|
|
638
|
-
export function buildCategoryIndex(cat) {
|
|
639
|
-
const lines = [];
|
|
640
|
-
lines.push(`# ${cat.id} — ${cat.skills.length} skills`);
|
|
641
|
-
lines.push('');
|
|
642
|
-
if (cat.indexPath !== undefined) {
|
|
643
|
-
const authored = readTextIfExists(cat.indexPath);
|
|
644
|
-
if (authored !== null) {
|
|
645
|
-
const { body } = parseFrontmatter(authored);
|
|
646
|
-
const trimmed = body.trim();
|
|
647
|
-
if (trimmed.length > 0) {
|
|
648
|
-
lines.push(trimmed);
|
|
649
|
-
lines.push('');
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
lines.push('## Skills');
|
|
654
|
-
const sorted = [...cat.skills].sort((a, b) => a.name.localeCompare(b.name));
|
|
655
|
-
for (const skill of sorted) {
|
|
656
|
-
const fullId = skill.plugin === SCOPE_SKILL_PLUGIN
|
|
657
|
-
? `${skill.scope}/${skill.name}`
|
|
658
|
-
: `${skill.plugin}/${skill.name}`;
|
|
659
|
-
const desc = skill.frontmatter.description ?? '(no description)';
|
|
660
|
-
lines.push(`- \`${fullId}\` — ${desc}`);
|
|
661
|
-
}
|
|
662
|
-
lines.push('');
|
|
663
|
-
lines.push('Read one with `crtr memory read <full-id>`. Search with `crtr memory find "<topic>"`.');
|
|
664
|
-
return lines.join('\n');
|
|
665
|
-
}
|
|
666
126
|
export function scopeRootsLabel() {
|
|
667
127
|
const proj = projectScopeRoot();
|
|
668
128
|
return proj ? `project=${proj}, user=${userScopeRoot()}` : `user=${userScopeRoot()}`;
|
|
@@ -21,9 +21,7 @@
|
|
|
21
21
|
// the caller delivers the guidance, then commits the ack.
|
|
22
22
|
import { getNode, updateNode } from '../canvas/index.js';
|
|
23
23
|
import { loadKernel, loadPersona, loadLifecycleFragment } from '../personas/index.js';
|
|
24
|
-
import {
|
|
25
|
-
import { readText } from '../fs-utils.js';
|
|
26
|
-
import { parseFrontmatter } from '../frontmatter.js';
|
|
24
|
+
import { resolveMemoryDoc } from '../memory-resolver.js';
|
|
27
25
|
import { readRoadmap, roadmapPath } from './roadmap.js';
|
|
28
26
|
import { orchestratorContextNote } from './bearings.js';
|
|
29
27
|
// ---------------------------------------------------------------------------
|
|
@@ -42,12 +40,12 @@ function scalarToString(v) {
|
|
|
42
40
|
return String(v);
|
|
43
41
|
return null;
|
|
44
42
|
}
|
|
45
|
-
/** Load a
|
|
46
|
-
* inline a kind's roadmap-shaping skill into the orchestration
|
|
43
|
+
/** Load a substrate doc's body text by name, or null if it can't be resolved.
|
|
44
|
+
* Used to inline a kind's roadmap-shaping skill into the orchestration
|
|
45
|
+
* guidance — the skill now lives as a substrate memory doc. */
|
|
47
46
|
function loadSkillBody(name) {
|
|
48
47
|
try {
|
|
49
|
-
|
|
50
|
-
return parseFrontmatter(readText(skill.path)).body.trim();
|
|
48
|
+
return resolveMemoryDoc(name).body.trim();
|
|
51
49
|
}
|
|
52
50
|
catch {
|
|
53
51
|
return null;
|
package/dist/core/scope.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { Scope } from '../types.js';
|
|
2
|
-
export declare function builtinSkillsRoot(): string;
|
|
1
|
+
import type { InstalledPlugin, Scope } from '../types.js';
|
|
3
2
|
export declare function builtinViewsRoot(): string;
|
|
4
3
|
export declare function builtinMemoryRoot(): string;
|
|
5
4
|
export declare function userScopeRoot(): string;
|
|
@@ -12,10 +11,13 @@ export declare function pluginsDir(scope: Scope): string | null;
|
|
|
12
11
|
export declare function marketplacesDir(scope: Scope): string | null;
|
|
13
12
|
export declare function scopeSkillsDir(scope: Scope): string | null;
|
|
14
13
|
/** Where substrate memory documents live per scope. Builtin memory is the
|
|
15
|
-
* special case — its own package dir (builtinMemoryRoot,
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* special case — its own package dir (builtinMemoryRoot), since the builtin
|
|
15
|
+
* scope has no scopeRoot — mirroring viewsDir's builtin handling. User and
|
|
16
|
+
* project memory live at scopeRoot(scope)/memory. */
|
|
18
17
|
export declare function scopeMemoryDir(scope: Scope): string | null;
|
|
18
|
+
/** Where a plugin's substrate memory documents live: `<plugin.root>/memory`.
|
|
19
|
+
* The resolver mounts these under the virtual `<pluginName>/` namespace. */
|
|
20
|
+
export declare function pluginMemoryDir(plugin: InstalledPlugin): string;
|
|
19
21
|
/** Where view definition dirs live per scope. Builtin views sit directly under
|
|
20
22
|
* builtinViewsRoot() (no `views/` segment — they ARE the builtin views dir),
|
|
21
23
|
* matching the loader's `<root>/<name>/view.mjs` resolution. */
|