@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.
Files changed (57) hide show
  1. package/README.md +2 -2
  2. package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +1 -1
  3. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +3 -3
  4. package/dist/cli.js +16 -26
  5. package/dist/commands/__tests__/skill.test.js +24 -28
  6. package/dist/commands/agent.d.ts +6 -0
  7. package/dist/commands/agent.js +585 -0
  8. package/dist/commands/debug.d.ts +1 -1
  9. package/dist/commands/debug.js +20 -7
  10. package/dist/commands/human.js +51 -19
  11. package/dist/commands/job.d.ts +9 -0
  12. package/dist/commands/job.js +100 -385
  13. package/dist/commands/{flow.d.ts → mode.d.ts} +1 -1
  14. package/dist/commands/mode.js +231 -0
  15. package/dist/commands/pkg.js +5 -0
  16. package/dist/commands/plan.d.ts +1 -1
  17. package/dist/commands/plan.js +24 -11
  18. package/dist/commands/skill.js +130 -107
  19. package/dist/commands/spec.d.ts +1 -1
  20. package/dist/commands/spec.js +24 -11
  21. package/dist/commands/sys.js +5 -0
  22. package/dist/core/__tests__/job.test.js +38 -74
  23. package/dist/core/__tests__/jobs.test.d.ts +1 -0
  24. package/dist/core/__tests__/jobs.test.js +98 -0
  25. package/dist/core/__tests__/resolver.test.d.ts +1 -0
  26. package/dist/core/__tests__/resolver.test.js +181 -0
  27. package/dist/core/__tests__/spawn.test.d.ts +1 -0
  28. package/dist/core/__tests__/spawn.test.js +138 -0
  29. package/dist/core/__tests__/subagents.test.d.ts +1 -0
  30. package/dist/core/__tests__/subagents.test.js +75 -0
  31. package/dist/core/__tests__/unknown-path.test.d.ts +1 -0
  32. package/dist/core/__tests__/unknown-path.test.js +52 -0
  33. package/dist/core/bootstrap.d.ts +2 -0
  34. package/dist/core/bootstrap.js +66 -0
  35. package/dist/core/command.d.ts +58 -2
  36. package/dist/core/command.js +62 -14
  37. package/dist/core/config.js +20 -2
  38. package/dist/core/frontmatter.d.ts +10 -0
  39. package/dist/core/frontmatter.js +24 -9
  40. package/dist/core/help.d.ts +39 -8
  41. package/dist/core/help.js +64 -32
  42. package/dist/core/jobs.d.ts +33 -13
  43. package/dist/core/jobs.js +259 -47
  44. package/dist/core/resolver.d.ts +1 -2
  45. package/dist/core/resolver.js +111 -47
  46. package/dist/core/spawn.d.ts +150 -10
  47. package/dist/core/spawn.js +493 -41
  48. package/dist/core/subagents.d.ts +18 -0
  49. package/dist/core/subagents.js +163 -0
  50. package/dist/prompts/agent.d.ts +12 -3
  51. package/dist/prompts/agent.js +51 -18
  52. package/dist/prompts/debug.js +14 -7
  53. package/dist/prompts/skill.js +16 -16
  54. package/dist/types.d.ts +22 -1
  55. package/dist/types.js +5 -2
  56. package/package.json +2 -2
  57. package/dist/commands/flow.js +0 -24
@@ -1,8 +1,10 @@
1
1
  // `crtr skill` subtree handlers — P3 implementation.
2
- // Sub-branches: find {list, search, grep}, read {show, where}, author {guide, scaffold}, state {enable, disable}.
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}:${s.name}`
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 show <name>` for full description + body.*',
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}:${sk.plugin}/${sk.name}`;
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 show <name>` for the full SKILL.md body. Run `crtr skill find list -h` for filters and verbosity.',
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 show <name>` for the full SKILL.md body. Run `crtr skill find search -h` for filters.',
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 sub-branch
327
+ // read leaf
326
328
  // ---------------------------------------------------------------------------
327
- const readShow = defineLeaf({
328
- name: 'show',
329
+ const readLeaf = defineLeaf({
330
+ name: 'read',
329
331
  help: {
330
- name: 'skill read show',
331
- summary: 'print SKILL.md body for a named skill',
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>:<name>, <scope>:<name>, <scope>:<plugin>/<name>.' },
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: true, constraint: 'SKILL.md body (with or without frontmatter per the --frontmatter flag).' },
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 rawContent = readText(skillObj.path);
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>:<skill> form.' },
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 { plugin: pluginName, name: skillName } = parseSkillQualifier(qualifier);
485
- if (!skillName) {
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 `_:` sentinel
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 show.' },
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 show.' },
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
- // Loaded-skills catalog (dynamicState for `skill -h`)
627
- // ---------------------------------------------------------------------------
628
- // A skill is a forest root within its source (scope+plugin) when no other
629
- // skill in that same source is its ancestor (`name` prefix + '/'). Nested
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}${s.plugin}`;
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 byPrefix = new Map();
651
- for (const group of bySource.values()) {
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
- for (const s of group) {
654
- const isChild = names.some((n) => n !== s.name && s.name.startsWith(n + '/'));
655
- if (isChild)
656
- continue;
657
- const prefix = s.plugin === SCOPE_SKILL_PLUGIN ? `${s.scope}:` : `${s.plugin}/`;
658
- let set = byPrefix.get(prefix);
659
- if (!set) {
660
- set = new Set();
661
- byPrefix.set(prefix, set);
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
- set.add(s.name);
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` when you have a name and need the SKILL.md content or its on-disk location. `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.',
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: 'read skill content or resolve location', useWhen: 'loading a skill to act on it' },
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, readBranch, authorBranch, stateBranch],
716
+ children: [findBranch, readLeaf, authorBranch, stateBranch],
694
717
  });
695
718
  }
@@ -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 flow spec new`:\n\n echo '<spec markdown>' | crtr flow 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 job start 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.";
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;
@@ -1,4 +1,4 @@
1
- // `crtr flow spec` subtree — spec new / show / list handlers.
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 flow spec new\`:
84
+ Run \`crtr mode spec new\`:
85
85
 
86
- echo '<spec markdown>' | crtr flow spec new <kebab-case-name>
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 job start reviewer\`.
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 job start).',
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 job start planner --artifact-path ${path} (returns {job_id}), then crtr job read result <job_id> --wait.`;
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 job start reviewer\`): crtr human review --file ${path} for anchored comments, then gate with crtr human approve --title "Approve this spec?".`;
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
  }
@@ -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',