@crouton-kit/crouter 0.3.3 → 0.3.11
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/README.md +2 -2
- package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +3 -3
- package/dist/cli.js +16 -26
- package/dist/commands/__tests__/skill.test.js +24 -28
- package/dist/commands/agent.d.ts +6 -0
- package/dist/commands/agent.js +585 -0
- package/dist/commands/debug.d.ts +1 -1
- package/dist/commands/debug.js +20 -7
- package/dist/commands/human.js +51 -19
- package/dist/commands/job.d.ts +9 -0
- package/dist/commands/job.js +100 -385
- package/dist/commands/{flow.d.ts → mode.d.ts} +1 -1
- package/dist/commands/mode.js +231 -0
- package/dist/commands/pkg.js +5 -0
- package/dist/commands/plan.d.ts +1 -1
- package/dist/commands/plan.js +24 -11
- package/dist/commands/skill.js +130 -107
- package/dist/commands/spec.d.ts +1 -1
- package/dist/commands/spec.js +24 -11
- package/dist/commands/sys.js +5 -0
- package/dist/core/__tests__/job.test.js +38 -74
- package/dist/core/__tests__/jobs.test.d.ts +1 -0
- package/dist/core/__tests__/jobs.test.js +98 -0
- package/dist/core/__tests__/resolver.test.d.ts +1 -0
- package/dist/core/__tests__/resolver.test.js +181 -0
- package/dist/core/__tests__/spawn.test.d.ts +1 -0
- package/dist/core/__tests__/spawn.test.js +138 -0
- package/dist/core/__tests__/subagents.test.d.ts +1 -0
- package/dist/core/__tests__/subagents.test.js +75 -0
- package/dist/core/__tests__/unknown-path.test.d.ts +1 -0
- package/dist/core/__tests__/unknown-path.test.js +52 -0
- package/dist/core/bootstrap.d.ts +2 -0
- package/dist/core/bootstrap.js +66 -0
- package/dist/core/command.d.ts +58 -2
- package/dist/core/command.js +62 -14
- package/dist/core/config.js +20 -2
- package/dist/core/frontmatter.d.ts +10 -0
- package/dist/core/frontmatter.js +24 -9
- package/dist/core/help.d.ts +39 -8
- package/dist/core/help.js +64 -32
- package/dist/core/jobs.d.ts +33 -13
- package/dist/core/jobs.js +259 -47
- package/dist/core/resolver.d.ts +1 -2
- package/dist/core/resolver.js +111 -47
- package/dist/core/spawn.d.ts +150 -10
- package/dist/core/spawn.js +493 -41
- package/dist/core/subagents.d.ts +18 -0
- package/dist/core/subagents.js +163 -0
- package/dist/prompts/agent.d.ts +12 -3
- package/dist/prompts/agent.js +51 -18
- package/dist/prompts/debug.js +14 -7
- package/dist/prompts/skill.js +16 -16
- package/dist/types.d.ts +22 -1
- package/dist/types.js +5 -2
- package/package.json +2 -2
- package/dist/commands/flow.js +0 -24
package/dist/commands/skill.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// `crtr skill` subtree handlers — P3 implementation.
|
|
2
|
-
// Sub-branches: find {list, search, grep},
|
|
2
|
+
// Sub-branches: find {list, search, grep}, author {guide, scaffold}, state {enable, disable}.
|
|
3
|
+
// Leaf children of skill: read.
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
import { writeFileSync } from 'node:fs';
|
|
5
6
|
import { defineBranch, defineLeaf } from '../core/command.js';
|
|
7
|
+
import { stateBlock } from '../core/help.js';
|
|
6
8
|
import { usage, general, notFound } from '../core/errors.js';
|
|
7
9
|
import { SCOPE_SKILL_PLUGIN, SKILL_ENTRY_FILE, SKILLS_DIR, SKILL_TYPES, isSkillType, skillConfigKey, } from '../types.js';
|
|
8
10
|
import { resolveSkill, listAllSkills, listInstalledPlugins, findPluginByName, parseSkillQualifier, listSkillSiblings, listSkillChildren, } from '../core/resolver.js';
|
|
@@ -17,7 +19,7 @@ import { skillCreatePrompt, skillTemplatePrompt } from '../prompts/skill.js';
|
|
|
17
19
|
// ---------------------------------------------------------------------------
|
|
18
20
|
function formatNeighborQualifier(s) {
|
|
19
21
|
return s.plugin === SCOPE_SKILL_PLUGIN
|
|
20
|
-
? `${s.scope}
|
|
22
|
+
? `${s.scope}/${s.name}`
|
|
21
23
|
: `${s.plugin}/${s.name}`;
|
|
22
24
|
}
|
|
23
25
|
function formatNeighborKeywords(s) {
|
|
@@ -33,7 +35,7 @@ function buildNeighborsSection(skill) {
|
|
|
33
35
|
return null;
|
|
34
36
|
const lines = [
|
|
35
37
|
'## Neighbors',
|
|
36
|
-
'*Auto-discovered from filesystem. Run `crtr skill read
|
|
38
|
+
'*Auto-discovered from filesystem. Run `crtr skill read <name>` for full description + body.*',
|
|
37
39
|
'',
|
|
38
40
|
];
|
|
39
41
|
if (siblings.length > 0) {
|
|
@@ -127,7 +129,7 @@ const findList = defineLeaf({
|
|
|
127
129
|
return po;
|
|
128
130
|
return a.name.localeCompare(b.name);
|
|
129
131
|
});
|
|
130
|
-
const keyOf = (sk) => `${sk.scope}
|
|
132
|
+
const keyOf = (sk) => `${sk.scope}/${sk.plugin}/${sk.name}`;
|
|
131
133
|
const params = {};
|
|
132
134
|
if (limit !== undefined)
|
|
133
135
|
params.limit = limit;
|
|
@@ -155,7 +157,7 @@ const findList = defineLeaf({
|
|
|
155
157
|
}),
|
|
156
158
|
next_cursor: result.next_cursor,
|
|
157
159
|
total: result.total,
|
|
158
|
-
follow_up: 'Use `crtr skill read
|
|
160
|
+
follow_up: 'Use `crtr skill read <name>` for the full SKILL.md body. Run `crtr skill find list -h` for filters and verbosity.',
|
|
159
161
|
};
|
|
160
162
|
},
|
|
161
163
|
});
|
|
@@ -240,7 +242,7 @@ const findSearch = defineLeaf({
|
|
|
240
242
|
score: h.score,
|
|
241
243
|
description: h.skill.frontmatter.description !== undefined ? h.skill.frontmatter.description : null,
|
|
242
244
|
})),
|
|
243
|
-
follow_up: 'Use `crtr skill read
|
|
245
|
+
follow_up: 'Use `crtr skill read <name>` for the full SKILL.md body. Run `crtr skill find search -h` for filters.',
|
|
244
246
|
};
|
|
245
247
|
},
|
|
246
248
|
});
|
|
@@ -322,25 +324,27 @@ const findBranch = defineBranch({
|
|
|
322
324
|
children: [findList, findSearch, findGrep],
|
|
323
325
|
});
|
|
324
326
|
// ---------------------------------------------------------------------------
|
|
325
|
-
// read
|
|
327
|
+
// read leaf
|
|
326
328
|
// ---------------------------------------------------------------------------
|
|
327
|
-
const
|
|
328
|
-
name: '
|
|
329
|
+
const readLeaf = defineLeaf({
|
|
330
|
+
name: 'read',
|
|
329
331
|
help: {
|
|
330
|
-
name: 'skill read
|
|
331
|
-
summary: '
|
|
332
|
+
name: 'skill read',
|
|
333
|
+
summary: 'load SKILL.md body and resolution metadata for a named skill',
|
|
332
334
|
params: [
|
|
333
|
-
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Forms: <name>, <plugin
|
|
335
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Forms: <name>, <plugin>/<name>, <scope>/<name>, <scope>/<plugin>/<name>.' },
|
|
334
336
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Narrows resolution when name is ambiguous.' },
|
|
335
337
|
{ kind: 'flag', name: 'plugin', type: 'string', required: false, constraint: 'Narrows resolution to a specific plugin.' },
|
|
336
|
-
{ kind: 'flag', name: 'frontmatter', type: 'bool', required: false, constraint: 'When present, includes YAML frontmatter in the output.' },
|
|
338
|
+
{ kind: 'flag', name: 'frontmatter', type: 'bool', required: false, constraint: 'When present, includes YAML frontmatter in the output content.' },
|
|
339
|
+
{ kind: 'flag', name: 'no-body', type: 'bool', required: false, constraint: 'When present, omits the body — returns resolution metadata only. Use to confirm a skill exists or locate it without loading SKILL.md.' },
|
|
337
340
|
],
|
|
338
341
|
output: [
|
|
339
342
|
{ name: 'name', type: 'string', required: true, constraint: 'Resolved skill name.' },
|
|
340
343
|
{ name: 'plugin', type: 'string', required: true, constraint: 'Plugin the skill belongs to.' },
|
|
341
344
|
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the skill was resolved from.' },
|
|
342
345
|
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to SKILL.md.' },
|
|
343
|
-
{ name: 'content', type: 'string', required:
|
|
346
|
+
{ name: 'content', type: 'string', required: false, constraint: 'SKILL.md body (with or without frontmatter per --frontmatter). Omitted when --no-body is set.' },
|
|
347
|
+
{ name: 'follow_up', type: 'string', required: false, constraint: 'Hints at variant flags. Present on default reads; omitted when --no-body is set.' },
|
|
344
348
|
],
|
|
345
349
|
outputKind: 'object',
|
|
346
350
|
effects: ['None. Read-only.'],
|
|
@@ -350,6 +354,7 @@ const readShow = defineLeaf({
|
|
|
350
354
|
const scopeStr = input['scope'];
|
|
351
355
|
const pluginFilter = input['plugin'];
|
|
352
356
|
const includeFrontmatter = input['frontmatter'];
|
|
357
|
+
const noBody = input['noBody'];
|
|
353
358
|
const resolveOpts = {};
|
|
354
359
|
if (scopeStr !== undefined) {
|
|
355
360
|
const resolved = resolveScopeArg(scopeStr);
|
|
@@ -359,70 +364,21 @@ const readShow = defineLeaf({
|
|
|
359
364
|
if (pluginFilter !== undefined)
|
|
360
365
|
resolveOpts.pluginFilter = pluginFilter;
|
|
361
366
|
const skillObj = resolveSkill(nameRaw, resolveOpts);
|
|
362
|
-
const
|
|
363
|
-
const rawBody = includeFrontmatter ? rawContent : parseFrontmatter(rawContent).body;
|
|
364
|
-
const content = appendNeighbors(skillObj, rawBody);
|
|
365
|
-
return {
|
|
366
|
-
name: skillObj.name,
|
|
367
|
-
plugin: skillObj.plugin,
|
|
368
|
-
scope: skillObj.scope,
|
|
369
|
-
path: skillObj.path,
|
|
370
|
-
content,
|
|
371
|
-
};
|
|
372
|
-
},
|
|
373
|
-
});
|
|
374
|
-
const readWhere = defineLeaf({
|
|
375
|
-
name: 'where',
|
|
376
|
-
help: {
|
|
377
|
-
name: 'skill read where',
|
|
378
|
-
summary: 'show resolution metadata for a named skill without reading its body',
|
|
379
|
-
params: [
|
|
380
|
-
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Same forms as skill read show.' },
|
|
381
|
-
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Narrows resolution.' },
|
|
382
|
-
{ kind: 'flag', name: 'plugin', type: 'string', required: false, constraint: 'Narrows resolution to a specific plugin.' },
|
|
383
|
-
],
|
|
384
|
-
output: [
|
|
385
|
-
{ name: 'name', type: 'string', required: true, constraint: 'Resolved skill name.' },
|
|
386
|
-
{ name: 'plugin', type: 'string', required: true, constraint: 'Plugin the skill belongs to.' },
|
|
387
|
-
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the skill was resolved from.' },
|
|
388
|
-
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to SKILL.md.' },
|
|
389
|
-
],
|
|
390
|
-
outputKind: 'object',
|
|
391
|
-
effects: ['None. Read-only.'],
|
|
392
|
-
},
|
|
393
|
-
run: async (input) => {
|
|
394
|
-
const nameRaw = input['name'];
|
|
395
|
-
const scopeStr = input['scope'];
|
|
396
|
-
const pluginFilter = input['plugin'];
|
|
397
|
-
const resolveOpts = {};
|
|
398
|
-
if (scopeStr !== undefined) {
|
|
399
|
-
const resolved = resolveScopeArg(scopeStr);
|
|
400
|
-
if (resolved !== 'all')
|
|
401
|
-
resolveOpts.scope = resolved;
|
|
402
|
-
}
|
|
403
|
-
if (pluginFilter !== undefined)
|
|
404
|
-
resolveOpts.pluginFilter = pluginFilter;
|
|
405
|
-
const skillObj = resolveSkill(nameRaw, resolveOpts);
|
|
406
|
-
return {
|
|
367
|
+
const out = {
|
|
407
368
|
name: skillObj.name,
|
|
408
369
|
plugin: skillObj.plugin,
|
|
409
370
|
scope: skillObj.scope,
|
|
410
371
|
path: skillObj.path,
|
|
411
372
|
};
|
|
373
|
+
if (noBody)
|
|
374
|
+
return out;
|
|
375
|
+
const rawContent = readText(skillObj.path);
|
|
376
|
+
const rawBody = includeFrontmatter ? rawContent : parseFrontmatter(rawContent).body;
|
|
377
|
+
out['content'] = appendNeighbors(skillObj, rawBody);
|
|
378
|
+
out['follow_up'] = 'Add --no-body to skip the body and return path/scope/plugin only. Add --frontmatter to include YAML frontmatter in content.';
|
|
379
|
+
return out;
|
|
412
380
|
},
|
|
413
381
|
});
|
|
414
|
-
const readBranch = defineBranch({
|
|
415
|
-
name: 'read',
|
|
416
|
-
help: {
|
|
417
|
-
name: 'skill read',
|
|
418
|
-
summary: 'read skill content or resolve its location',
|
|
419
|
-
children: [
|
|
420
|
-
{ name: 'show', desc: 'print SKILL.md body', useWhen: 'loading skill content to act on it' },
|
|
421
|
-
{ name: 'where', desc: 'show resolution metadata only', useWhen: 'verifying which skill resolves and from where, without loading its body' },
|
|
422
|
-
],
|
|
423
|
-
},
|
|
424
|
-
children: [readShow, readWhere],
|
|
425
|
-
});
|
|
426
382
|
// ---------------------------------------------------------------------------
|
|
427
383
|
// author sub-branch
|
|
428
384
|
// ---------------------------------------------------------------------------
|
|
@@ -461,7 +417,7 @@ const authorScaffold = defineLeaf({
|
|
|
461
417
|
name: 'skill author scaffold',
|
|
462
418
|
summary: 'create an empty SKILL.md stub at the given qualifier',
|
|
463
419
|
params: [
|
|
464
|
-
{ kind: 'positional', name: 'qualifier', required: true, constraint: 'Skill identifier in <plugin
|
|
420
|
+
{ kind: 'positional', name: 'qualifier', required: true, constraint: 'Skill identifier in <plugin>/<skill> form.' },
|
|
465
421
|
{ kind: 'flag', name: 'type', type: 'enum', choices: [...VALID_TYPES], required: false, constraint: 'One of: playbook, primer, reference, runbook, freeform.' },
|
|
466
422
|
{ kind: 'flag', name: 'description', type: 'string', required: false, constraint: 'Short description written to frontmatter.' },
|
|
467
423
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
|
|
@@ -481,16 +437,22 @@ const authorScaffold = defineLeaf({
|
|
|
481
437
|
const typeStr = input['type'];
|
|
482
438
|
const description = input['description'];
|
|
483
439
|
const scopeStr = input['scope'];
|
|
484
|
-
const
|
|
485
|
-
if (
|
|
440
|
+
const parsed = parseSkillQualifier(qualifier);
|
|
441
|
+
if (parsed.segments.length === 0) {
|
|
486
442
|
throw usage('skill name required in qualifier');
|
|
487
443
|
}
|
|
444
|
+
// For scaffold, the qualifier is always <plugin>/<skill>. If it's a single segment,
|
|
445
|
+
// treat it as a scope-direct skill name. Otherwise first segment is the plugin.
|
|
446
|
+
const pluginName = parsed.segments.length > 1 ? parsed.segments[0] : undefined;
|
|
447
|
+
const skillName = parsed.segments.length > 1
|
|
448
|
+
? parsed.segments.slice(1).join('/')
|
|
449
|
+
: parsed.segments[0];
|
|
488
450
|
if (typeStr !== undefined && !isSkillType(typeStr)) {
|
|
489
451
|
throw usage(`unknown skill type: ${typeStr} / valid: ${SKILL_TYPES.join(' | ')}`);
|
|
490
452
|
}
|
|
491
453
|
const skillType = typeStr !== undefined && isSkillType(typeStr) ? typeStr : undefined;
|
|
492
454
|
let skillFile;
|
|
493
|
-
// Scope-direct: no plugin qualifier, or explicit `_
|
|
455
|
+
// Scope-direct: no plugin qualifier, or explicit `_/` sentinel (internal only)
|
|
494
456
|
if (pluginName === undefined || pluginName === SCOPE_SKILL_PLUGIN) {
|
|
495
457
|
const scope = resolveWriteScope(scopeStr);
|
|
496
458
|
const scopeRootPath = requireScopeRoot(scope);
|
|
@@ -578,7 +540,7 @@ const stateEnable = defineLeaf({
|
|
|
578
540
|
name: 'skill state enable',
|
|
579
541
|
summary: 'enable a skill in the given scope',
|
|
580
542
|
params: [
|
|
581
|
-
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Same forms as skill read
|
|
543
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Same forms as skill read.' },
|
|
582
544
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
|
|
583
545
|
],
|
|
584
546
|
output: [
|
|
@@ -597,7 +559,7 @@ const stateDisable = defineLeaf({
|
|
|
597
559
|
name: 'skill state disable',
|
|
598
560
|
summary: 'disable a skill in the given scope, hiding it from list and agent discovery',
|
|
599
561
|
params: [
|
|
600
|
-
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Same forms as skill read
|
|
562
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Same forms as skill read.' },
|
|
601
563
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
|
|
602
564
|
],
|
|
603
565
|
output: [
|
|
@@ -622,12 +584,11 @@ const stateBranch = defineBranch({
|
|
|
622
584
|
},
|
|
623
585
|
children: [stateEnable, stateDisable],
|
|
624
586
|
});
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
// children stay discoverable via `skill find list` and the Neighbors section.
|
|
587
|
+
/** The skill subtree's live state: the loaded-skills catalog as a self-named
|
|
588
|
+
* `<skills count="N">` element. The tag carries the label and the count is an
|
|
589
|
+
* attribute, so the body is the grouped tree alone — no "Loaded skills (N)"
|
|
590
|
+
* header to duplicate. Returns null (block omitted) when discovery fails or
|
|
591
|
+
* nothing is loaded. */
|
|
631
592
|
function buildSkillCatalog() {
|
|
632
593
|
let skills;
|
|
633
594
|
try {
|
|
@@ -640,37 +601,93 @@ function buildSkillCatalog() {
|
|
|
640
601
|
return null;
|
|
641
602
|
const bySource = new Map();
|
|
642
603
|
for (const s of skills) {
|
|
643
|
-
const key = `${s.scope}
|
|
604
|
+
const key = `${s.scope}\t${s.plugin}`;
|
|
644
605
|
const arr = bySource.get(key);
|
|
645
606
|
if (arr)
|
|
646
607
|
arr.push(s);
|
|
647
608
|
else
|
|
648
609
|
bySource.set(key, [s]);
|
|
649
610
|
}
|
|
650
|
-
const
|
|
651
|
-
|
|
611
|
+
const projectSources = [];
|
|
612
|
+
const userSources = [];
|
|
613
|
+
for (const [key, group] of bySource) {
|
|
614
|
+
const [scope, plugin] = key.split('\t');
|
|
652
615
|
const names = group.map((g) => g.name);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
616
|
+
const roots = names
|
|
617
|
+
.filter((n) => !names.some((m) => m !== n && n.startsWith(m + '/')))
|
|
618
|
+
.sort();
|
|
619
|
+
if (roots.length === 0)
|
|
620
|
+
continue;
|
|
621
|
+
(scope === 'project' ? projectSources : userSources).push({ plugin, roots });
|
|
622
|
+
}
|
|
623
|
+
const body = [];
|
|
624
|
+
renderCatalogSection('Project', projectSources, body);
|
|
625
|
+
renderCatalogSection('User', userSources, body);
|
|
626
|
+
// renderCatalogSection leads each section with a blank separator; drop the
|
|
627
|
+
// leading one so the element body starts on its first real line.
|
|
628
|
+
while (body.length > 0 && body[0] === '')
|
|
629
|
+
body.shift();
|
|
630
|
+
return stateBlock('skills', { count: skills.length }, body.join('\n'));
|
|
631
|
+
}
|
|
632
|
+
function renderCatalogSection(label, sources, out) {
|
|
633
|
+
if (sources.length === 0)
|
|
634
|
+
return;
|
|
635
|
+
const count = sources.reduce((n, s) => n + s.roots.length, 0);
|
|
636
|
+
out.push('');
|
|
637
|
+
out.push(`${label} (${count})`);
|
|
638
|
+
const sentinel = sources.filter((s) => s.plugin === SCOPE_SKILL_PLUGIN);
|
|
639
|
+
const named = sources
|
|
640
|
+
.filter((s) => s.plugin !== SCOPE_SKILL_PLUGIN)
|
|
641
|
+
.sort((a, b) => a.plugin.localeCompare(b.plugin));
|
|
642
|
+
for (const s of sentinel) {
|
|
643
|
+
for (const n of s.roots)
|
|
644
|
+
out.push(` ${n}`);
|
|
645
|
+
}
|
|
646
|
+
if (named.length === 0)
|
|
647
|
+
return;
|
|
648
|
+
const classified = named.map((s) => {
|
|
649
|
+
const subcats = new Map();
|
|
650
|
+
const bare = [];
|
|
651
|
+
for (const n of s.roots) {
|
|
652
|
+
const slash = n.indexOf('/');
|
|
653
|
+
if (slash === -1) {
|
|
654
|
+
bare.push(n);
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
const sub = n.slice(0, slash);
|
|
658
|
+
const rest = n.slice(slash + 1);
|
|
659
|
+
const arr = subcats.get(sub);
|
|
660
|
+
if (arr)
|
|
661
|
+
arr.push(rest);
|
|
662
|
+
else
|
|
663
|
+
subcats.set(sub, [rest]);
|
|
662
664
|
}
|
|
663
|
-
|
|
665
|
+
}
|
|
666
|
+
return { plugin: s.plugin, roots: s.roots, subcats, bare };
|
|
667
|
+
});
|
|
668
|
+
// Smart-nest: a plugin nests iff it has 2+ distinct subcategories.
|
|
669
|
+
const nests = (p) => p.subcats.size >= 2;
|
|
670
|
+
const inlineLabels = classified.filter((p) => !nests(p)).map((p) => `${p.plugin}/`);
|
|
671
|
+
const inlineW = inlineLabels.reduce((m, p) => (p.length > m ? p.length : m), 0);
|
|
672
|
+
for (const p of classified) {
|
|
673
|
+
if (nests(p)) {
|
|
674
|
+
out.push(` ${p.plugin}/`);
|
|
675
|
+
if (p.bare.length > 0) {
|
|
676
|
+
out.push(` ${[...p.bare].sort().join(', ')}`);
|
|
677
|
+
}
|
|
678
|
+
const subKeys = [...p.subcats.keys()].sort();
|
|
679
|
+
const subLabels = subKeys.map((k) => `${k}/`);
|
|
680
|
+
const subW = subLabels.reduce((m, k) => (k.length > m ? k.length : m), 0);
|
|
681
|
+
for (let i = 0; i < subKeys.length; i++) {
|
|
682
|
+
const children = p.subcats.get(subKeys[i]).sort();
|
|
683
|
+
out.push(` ${subLabels[i].padEnd(subW)} ${children.join(', ')}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
const pluginLabel = `${p.plugin}/`;
|
|
688
|
+
out.push(` ${pluginLabel.padEnd(inlineW)} ${[...p.roots].sort().join(', ')}`);
|
|
664
689
|
}
|
|
665
690
|
}
|
|
666
|
-
const prefixes = [...byPrefix.keys()].sort();
|
|
667
|
-
const prefixW = prefixes.reduce((m, p) => (p.length > m ? p.length : m), 0);
|
|
668
|
-
const lines = [`Loaded skills (${skills.length})`];
|
|
669
|
-
for (const prefix of prefixes) {
|
|
670
|
-
const names = [...byPrefix.get(prefix)].sort();
|
|
671
|
-
lines.push(` ${prefix.padEnd(prefixW)} ${names.join(', ')}`);
|
|
672
|
-
}
|
|
673
|
-
return lines.join('\n');
|
|
674
691
|
}
|
|
675
692
|
// ---------------------------------------------------------------------------
|
|
676
693
|
// Root export
|
|
@@ -678,18 +695,24 @@ function buildSkillCatalog() {
|
|
|
678
695
|
export function registerSkill() {
|
|
679
696
|
return defineBranch({
|
|
680
697
|
name: 'skill',
|
|
698
|
+
rootEntry: {
|
|
699
|
+
concept: 'a SKILL.md you read to adopt its workflow',
|
|
700
|
+
desc: 'find, read, author, and manage skills',
|
|
701
|
+
useWhen: 'a task matches a loaded skill — read it before improvising. `crtr skill read <name>` loads one by name from the catalog below; `crtr skill find` only when the name is not already listed. Names are crtr identifiers, not file paths — never cat or find SKILL.md off disk.',
|
|
702
|
+
dynamicState: buildSkillCatalog,
|
|
703
|
+
},
|
|
681
704
|
help: {
|
|
682
705
|
name: 'skill',
|
|
683
706
|
summary: 'discover, read, author, and manage skill state',
|
|
684
|
-
model: '`find` when you do not yet know which skill applies — it locates candidates by topic, keyword, or body text. `read`
|
|
707
|
+
model: '`find` when you do not yet know which skill applies — it locates candidates by topic, keyword, or body text. `read` (leaf) loads SKILL.md by name; takes the name as a positional, returns body + metadata, accepts --no-body to skip the body. `author` when you are writing a new skill — it carries the template workflow and the scaffolder. `state` when a skill should be hidden from discovery without being removed. Append `-h` at any branch or leaf for its full schema.',
|
|
685
708
|
dynamicState: buildSkillCatalog,
|
|
686
709
|
children: [
|
|
687
710
|
{ name: 'find', desc: 'list, search, or grep skills', useWhen: 'discovering what skills are available' },
|
|
688
|
-
{ name: 'read', desc: '
|
|
711
|
+
{ name: 'read', desc: 'load SKILL.md body + metadata for a named skill', useWhen: 'loading a skill to act on it' },
|
|
689
712
|
{ name: 'author', desc: 'create and scaffold skills', useWhen: 'writing a new skill' },
|
|
690
713
|
{ name: 'state', desc: 'enable or disable skills', useWhen: 'toggling skill visibility' },
|
|
691
714
|
],
|
|
692
715
|
},
|
|
693
|
-
children: [findBranch,
|
|
716
|
+
children: [findBranch, readLeaf, authorBranch, stateBranch],
|
|
694
717
|
});
|
|
695
718
|
}
|
package/dist/commands/spec.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare const SPEC_NEW_GUIDE = "## Spec workflow\n\nBuild and save a design + requirements spec: a document describing what to\nbuild, the shape of the solution, and the behaviors it must satisfy. A spec is\nupstream of a plan \u2014 it captures decisions, not implementation steps.\n\nAnti-pattern: do not fish for clarifications upfront. Draft a concrete spec\nfirst based on your investigation, then iterate. A specific draft the user can\nreact to converges faster than a list of questions in a vacuum.\n\n### Phase 1: Shape\n\nBuild a comprehensive picture of the problem and the relevant code. Surface\nexisting patterns, constraints, and prior decisions.\n\nLaunch up to 3 Explore subagents IN PARALLEL (single message, multiple tool\ncalls). Use 1 agent for narrow, well-scoped problems; use more when the spec\ntouches several subsystems or you need to compare existing implementations.\nQuality over quantity \u2014 3 agents maximum.\n\nAfter exploration, draft a high-level design: the shape of the solution, new or\nchanged pieces, the boundaries.\n\n### Phase 2: Requirements\n\nTranslate the shape into concrete behavioral requirements. Each requirement\nmust be:\n\n- Testable \u2014 has a clear pass/fail condition.\n- Behavior-focused \u2014 describes what the system does, not how.\n- Scoped \u2014 covers one observable behavior.\n\nGroup requirements by capability. Plain English is fine. For conditional or\nstateful behaviors, EARS templates sharpen phrasing:\n`When <trigger>, the system shall <behavior>` or\n`If <condition>, then the system shall <response>`.\n\nFor larger / multi-component designs, walk the design end-to-end: at each step\nfrom trigger to final state, verify preconditions, state, failure handling, and\nhandoffs between components are specified. Skip this for small self-contained\nspecs.\n\n### Phase 3: Deepen\n\nRead the critical files identified during Phase 1. Reconcile requirements\nagainst the shape \u2014 if a requirement reveals a gap in the design, refine the\ndesign before saving.\n\nUse AskUserQuestion ONLY to clarify requirements or choose between approaches.\nNever use it to ask \"is this spec okay?\" or \"should I save?\".\n\n### Phase 4: Compose the spec body\n\nRequired sections:\n\n # Spec: <one-line title>\n\n ## Context\n <the problem this spec addresses, what motivates it, and the intended outcome.\n Include relevant constraints \u2014 user goals, stakeholders, deadlines.>\n\n ## Design\n <the shape of the solution. Components, data flow, key decisions and why they\n were chosen. Reference existing code with `file_path:line_number`.>\n\n ## Requirements\n <grouped behavioral requirements. Each one testable. Plain English is fine.>\n\n ### <Capability A>\n - <one observable behavior>\n\n ### <Capability B>\n - ...\n\n ## Out of scope\n <things explicitly NOT covered, so the next reader knows where the edges are.>\n\n ## Open questions\n <anything you could not resolve. Empty if all decisions are pinned.>\n\n### Phase 5: Save\n\nRun `crtr
|
|
1
|
+
export declare const SPEC_NEW_GUIDE = "## Spec workflow\n\nBuild and save a design + requirements spec: a document describing what to\nbuild, the shape of the solution, and the behaviors it must satisfy. A spec is\nupstream of a plan \u2014 it captures decisions, not implementation steps.\n\nAnti-pattern: do not fish for clarifications upfront. Draft a concrete spec\nfirst based on your investigation, then iterate. A specific draft the user can\nreact to converges faster than a list of questions in a vacuum.\n\n### Phase 1: Shape\n\nBuild a comprehensive picture of the problem and the relevant code. Surface\nexisting patterns, constraints, and prior decisions.\n\nLaunch up to 3 Explore subagents IN PARALLEL (single message, multiple tool\ncalls). Use 1 agent for narrow, well-scoped problems; use more when the spec\ntouches several subsystems or you need to compare existing implementations.\nQuality over quantity \u2014 3 agents maximum.\n\nAfter exploration, draft a high-level design: the shape of the solution, new or\nchanged pieces, the boundaries.\n\n### Phase 2: Requirements\n\nTranslate the shape into concrete behavioral requirements. Each requirement\nmust be:\n\n- Testable \u2014 has a clear pass/fail condition.\n- Behavior-focused \u2014 describes what the system does, not how.\n- Scoped \u2014 covers one observable behavior.\n\nGroup requirements by capability. Plain English is fine. For conditional or\nstateful behaviors, EARS templates sharpen phrasing:\n`When <trigger>, the system shall <behavior>` or\n`If <condition>, then the system shall <response>`.\n\nFor larger / multi-component designs, walk the design end-to-end: at each step\nfrom trigger to final state, verify preconditions, state, failure handling, and\nhandoffs between components are specified. Skip this for small self-contained\nspecs.\n\n### Phase 3: Deepen\n\nRead the critical files identified during Phase 1. Reconcile requirements\nagainst the shape \u2014 if a requirement reveals a gap in the design, refine the\ndesign before saving.\n\nUse AskUserQuestion ONLY to clarify requirements or choose between approaches.\nNever use it to ask \"is this spec okay?\" or \"should I save?\".\n\n### Phase 4: Compose the spec body\n\nRequired sections:\n\n # Spec: <one-line title>\n\n ## Context\n <the problem this spec addresses, what motivates it, and the intended outcome.\n Include relevant constraints \u2014 user goals, stakeholders, deadlines.>\n\n ## Design\n <the shape of the solution. Components, data flow, key decisions and why they\n were chosen. Reference existing code with `file_path:line_number`.>\n\n ## Requirements\n <grouped behavioral requirements. Each one testable. Plain English is fine.>\n\n ### <Capability A>\n - <one observable behavior>\n\n ### <Capability B>\n - ...\n\n ## Out of scope\n <things explicitly NOT covered, so the next reader knows where the edges are.>\n\n ## Open questions\n <anything you could not resolve. Empty if all decisions are pinned.>\n\n### Phase 5: Save\n\nRun `crtr mode spec new`:\n\n echo '<spec markdown>' | crtr mode spec new <kebab-case-name>\n\n- NAME: short kebab-case slug. Nested names become subdirectories\n (e.g. `auth/refresh-tokens`).\n- Pipe the full spec markdown composed in Phase 4 on stdin.\n\nOutput: `{path, follow_up}`. The `follow_up` field names the exact next call\n\u2014 run it.\n\n### Phase 6: Done\n\nAfter the reviewer approves the spec, your turn ends. Do not summarize in chat.\nFor a human gate, optionally run `crtr human review` on the spec for anchored\ncomments and `crtr human approve` to gate the handoff \u2014 this complements, not\nreplaces, `crtr mode reviewer`.\nIf the user is ready to plan, ask once whether to hand off; if yes, follow the\n`follow_up` instructions from the save output.";
|
|
2
2
|
import type { BranchDef } from '../core/command.js';
|
|
3
3
|
export declare function registerSpec(): BranchDef;
|
package/dist/commands/spec.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `crtr
|
|
1
|
+
// `crtr mode spec` subtree — spec new / show / list handlers.
|
|
2
2
|
export const SPEC_NEW_GUIDE = `## Spec workflow
|
|
3
3
|
|
|
4
4
|
Build and save a design + requirements spec: a document describing what to
|
|
@@ -81,9 +81,9 @@ Required sections:
|
|
|
81
81
|
|
|
82
82
|
### Phase 5: Save
|
|
83
83
|
|
|
84
|
-
Run \`crtr
|
|
84
|
+
Run \`crtr mode spec new\`:
|
|
85
85
|
|
|
86
|
-
echo '<spec markdown>' | crtr
|
|
86
|
+
echo '<spec markdown>' | crtr mode spec new <kebab-case-name>
|
|
87
87
|
|
|
88
88
|
- NAME: short kebab-case slug. Nested names become subdirectories
|
|
89
89
|
(e.g. \`auth/refresh-tokens\`).
|
|
@@ -97,7 +97,7 @@ Output: \`{path, follow_up}\`. The \`follow_up\` field names the exact next call
|
|
|
97
97
|
After the reviewer approves the spec, your turn ends. Do not summarize in chat.
|
|
98
98
|
For a human gate, optionally run \`crtr human review\` on the spec for anchored
|
|
99
99
|
comments and \`crtr human approve\` to gate the handoff — this complements, not
|
|
100
|
-
replaces, \`crtr
|
|
100
|
+
replaces, \`crtr mode reviewer\`.
|
|
101
101
|
If the user is ready to plan, ask once whether to hand off; if yes, follow the
|
|
102
102
|
\`follow_up\` instructions from the save output.`;
|
|
103
103
|
import { defineBranch, defineLeaf } from '../core/command.js';
|
|
@@ -107,7 +107,7 @@ export function registerSpec() {
|
|
|
107
107
|
const specNew = defineLeaf({
|
|
108
108
|
name: 'new',
|
|
109
109
|
help: {
|
|
110
|
-
name: 'spec new',
|
|
110
|
+
name: 'mode spec new',
|
|
111
111
|
summary: 'draft a specification artifact from intent',
|
|
112
112
|
guide: SPEC_NEW_GUIDE,
|
|
113
113
|
params: [
|
|
@@ -136,7 +136,7 @@ export function registerSpec() {
|
|
|
136
136
|
name: 'follow_up',
|
|
137
137
|
type: 'string',
|
|
138
138
|
required: true,
|
|
139
|
-
constraint: 'Recommended next call (planner
|
|
139
|
+
constraint: 'Recommended next call (planner spawn via `crtr mode planner`).',
|
|
140
140
|
},
|
|
141
141
|
],
|
|
142
142
|
outputKind: 'object',
|
|
@@ -146,9 +146,9 @@ export function registerSpec() {
|
|
|
146
146
|
const name = input['name'];
|
|
147
147
|
const body = input['body'];
|
|
148
148
|
const { path, oversize, lineCount } = saveArtifact('specs', name, body);
|
|
149
|
-
let follow_up = `Plan it: crtr
|
|
149
|
+
let follow_up = `Plan it: crtr mode planner ${path} (returns {job_id}), then crtr job read result <job_id> --wait.`;
|
|
150
150
|
follow_up +=
|
|
151
|
-
` Optional human gate before planning (complements, does not replace \`crtr
|
|
151
|
+
` Optional human gate before planning (complements, does not replace \`crtr mode reviewer\`): crtr human review --file ${path} for anchored comments, then gate with crtr human approve --title "Approve this spec?".`;
|
|
152
152
|
if (oversize) {
|
|
153
153
|
follow_up +=
|
|
154
154
|
` OVERSIZE ADVISORY: this spec is ${lineCount} lines (> ${OVERSIZE_WARN_LINES}). Split into focused sub-specs before planning.`;
|
|
@@ -159,7 +159,7 @@ export function registerSpec() {
|
|
|
159
159
|
const specShow = defineLeaf({
|
|
160
160
|
name: 'show',
|
|
161
161
|
help: {
|
|
162
|
-
name: 'spec show',
|
|
162
|
+
name: 'mode spec show',
|
|
163
163
|
summary: 'read a spec artifact by name',
|
|
164
164
|
params: [
|
|
165
165
|
{
|
|
@@ -202,7 +202,7 @@ export function registerSpec() {
|
|
|
202
202
|
const specList = defineLeaf({
|
|
203
203
|
name: 'list',
|
|
204
204
|
help: {
|
|
205
|
-
name: 'spec list',
|
|
205
|
+
name: 'mode spec list',
|
|
206
206
|
summary: 'paginated list of spec artifacts, sorted ascending by name',
|
|
207
207
|
params: [
|
|
208
208
|
{
|
|
@@ -272,7 +272,7 @@ export function registerSpec() {
|
|
|
272
272
|
return defineBranch({
|
|
273
273
|
name: 'spec',
|
|
274
274
|
help: {
|
|
275
|
-
name: 'spec',
|
|
275
|
+
name: 'mode spec',
|
|
276
276
|
summary: 'create and read specification artifacts',
|
|
277
277
|
model: 'Lifecycle: draft -> approved. Approved specs drive plan creation.',
|
|
278
278
|
children: [
|
|
@@ -281,6 +281,19 @@ export function registerSpec() {
|
|
|
281
281
|
{ name: 'list', desc: 'enumerate specs', useWhen: 'discovering what specs exist' },
|
|
282
282
|
],
|
|
283
283
|
},
|
|
284
|
+
slash: {
|
|
285
|
+
name: 'spec',
|
|
286
|
+
description: 'Spec mode — turn intent into a specification artifact (spec-driven workflow).',
|
|
287
|
+
argumentHint: '[what to spec]',
|
|
288
|
+
body: `You are entering **spec mode**: turn intent into a written specification artifact.
|
|
289
|
+
|
|
290
|
+
1. Run \`crtr mode spec new -h\` to load the spec-authoring workflow and output schema.
|
|
291
|
+
2. Follow that workflow to draft the spec, then save it by piping the markdown to \`crtr mode spec new <name>\` on stdin.
|
|
292
|
+
|
|
293
|
+
The request: $ARGUMENTS
|
|
294
|
+
|
|
295
|
+
If no request was given, ask the user what to spec before starting.`,
|
|
296
|
+
},
|
|
284
297
|
children: [specNew, specShow, specList],
|
|
285
298
|
});
|
|
286
299
|
}
|
package/dist/commands/sys.js
CHANGED
|
@@ -697,6 +697,11 @@ const sysVersionLeaf = defineLeaf({
|
|
|
697
697
|
export function registerSys() {
|
|
698
698
|
return defineBranch({
|
|
699
699
|
name: 'sys',
|
|
700
|
+
rootEntry: {
|
|
701
|
+
concept: 'crtr configuration, diagnostics, and self-management',
|
|
702
|
+
desc: 'config, doctor, update, version',
|
|
703
|
+
useWhen: 'managing the crtr installation',
|
|
704
|
+
},
|
|
700
705
|
help: {
|
|
701
706
|
name: 'sys',
|
|
702
707
|
summary: 'crtr system configuration, diagnostics, and self-management',
|