@crouton-kit/crouter 0.3.3 → 0.3.8
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 +6 -6
- package/dist/commands/__tests__/skill.test.js +24 -28
- package/dist/commands/{flow.d.ts → agent.d.ts} +1 -1
- package/dist/commands/agent.js +384 -0
- package/dist/commands/debug.d.ts +1 -1
- package/dist/commands/debug.js +7 -7
- package/dist/commands/job.js +54 -379
- package/dist/commands/plan.d.ts +1 -1
- package/dist/commands/plan.js +11 -11
- package/dist/commands/skill.js +114 -107
- package/dist/commands/spec.d.ts +1 -1
- package/dist/commands/spec.js +11 -11
- 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 +66 -0
- package/dist/core/__tests__/resolver.test.d.ts +1 -0
- package/dist/core/__tests__/resolver.test.js +113 -0
- package/dist/core/config.js +20 -2
- package/dist/core/jobs.d.ts +26 -12
- package/dist/core/jobs.js +151 -42
- package/dist/core/resolver.d.ts +1 -2
- package/dist/core/resolver.js +60 -46
- package/dist/core/spawn.d.ts +26 -3
- package/dist/core/spawn.js +144 -11
- package/dist/prompts/agent.d.ts +3 -3
- package/dist/prompts/agent.js +20 -18
- package/dist/prompts/debug.js +14 -7
- package/dist/prompts/skill.js +16 -16
- package/dist/types.d.ts +1 -1
- package/dist/types.js +2 -2
- package/package.json +2 -2
- package/dist/commands/flow.js +0 -24
package/dist/commands/plan.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `crtr
|
|
1
|
+
// `crtr agent plan` subtree — plan new / show / list handlers.
|
|
2
2
|
export const PLAN_NEW_GUIDE = `## Planning workflow
|
|
3
3
|
|
|
4
4
|
Build and save an implementation plan: a map another agent can execute without
|
|
@@ -74,9 +74,9 @@ parallelism to unlock.
|
|
|
74
74
|
|
|
75
75
|
### Phase 5: Save
|
|
76
76
|
|
|
77
|
-
Run \`crtr
|
|
77
|
+
Run \`crtr agent plan new\`:
|
|
78
78
|
|
|
79
|
-
echo '<plan markdown>' | crtr
|
|
79
|
+
echo '<plan markdown>' | crtr agent plan new <kebab-case-name> [--spec <spec-name>]
|
|
80
80
|
|
|
81
81
|
- NAME: short kebab-case slug. Nested names become subdirectories
|
|
82
82
|
(e.g. \`auth/jwt-refresh\`).
|
|
@@ -99,9 +99,9 @@ are under-decomposed.
|
|
|
99
99
|
After the reviewer approves the plan, your turn ends. Do not summarize in chat.
|
|
100
100
|
For a human gate, optionally put the plan in front of a person with \`crtr
|
|
101
101
|
human review\` (anchored comments) and gate the handoff with \`crtr human
|
|
102
|
-
approve\`. This complements — it does not replace — \`crtr
|
|
102
|
+
approve\`. This complements — it does not replace — \`crtr agent new reviewer\`.
|
|
103
103
|
If the user is ready to build, ask once whether to hand off; if yes, run:
|
|
104
|
-
\`crtr
|
|
104
|
+
\`crtr agent new implementer\` with the plan path.`;
|
|
105
105
|
export const PLAN_SHOW_GUIDE = '';
|
|
106
106
|
import { defineBranch, defineLeaf } from '../core/command.js';
|
|
107
107
|
import { saveArtifact, readArtifact, listArtifacts, OVERSIZE_WARN_LINES } from '../core/artifact.js';
|
|
@@ -110,7 +110,7 @@ export function registerPlan() {
|
|
|
110
110
|
const planNew = defineLeaf({
|
|
111
111
|
name: 'new',
|
|
112
112
|
help: {
|
|
113
|
-
name: 'plan new',
|
|
113
|
+
name: 'agent plan new',
|
|
114
114
|
summary: 'draft a plan from intent and optional spec alignment',
|
|
115
115
|
guide: PLAN_NEW_GUIDE,
|
|
116
116
|
params: [
|
|
@@ -146,7 +146,7 @@ export function registerPlan() {
|
|
|
146
146
|
name: 'follow_up',
|
|
147
147
|
type: 'string',
|
|
148
148
|
required: true,
|
|
149
|
-
constraint: 'Recommended next call (reviewer
|
|
149
|
+
constraint: 'Recommended next call (reviewer spawn via `crtr agent new reviewer`).',
|
|
150
150
|
},
|
|
151
151
|
],
|
|
152
152
|
outputKind: 'object',
|
|
@@ -163,7 +163,7 @@ export function registerPlan() {
|
|
|
163
163
|
if (spec !== undefined)
|
|
164
164
|
meta['spec'] = spec;
|
|
165
165
|
const { path, oversize, lineCount } = saveArtifact('plans', name, body, meta);
|
|
166
|
-
let follow_up = `Review it: crtr
|
|
166
|
+
let follow_up = `Review it: crtr agent new reviewer ${path} --kind plan (returns {job_id}), then crtr job read result <job_id> --wait.`;
|
|
167
167
|
follow_up +=
|
|
168
168
|
` Optional human gate (complements, does not replace the agent reviewer): crtr human review --file ${path} for anchored comments, then gate handoff with crtr human approve --title "Approve this plan?".`;
|
|
169
169
|
if (oversize) {
|
|
@@ -176,7 +176,7 @@ export function registerPlan() {
|
|
|
176
176
|
const planShow = defineLeaf({
|
|
177
177
|
name: 'show',
|
|
178
178
|
help: {
|
|
179
|
-
name: 'plan show',
|
|
179
|
+
name: 'agent plan show',
|
|
180
180
|
summary: 'read a plan artifact by name',
|
|
181
181
|
params: [
|
|
182
182
|
{
|
|
@@ -225,7 +225,7 @@ export function registerPlan() {
|
|
|
225
225
|
const planList = defineLeaf({
|
|
226
226
|
name: 'list',
|
|
227
227
|
help: {
|
|
228
|
-
name: 'plan list',
|
|
228
|
+
name: 'agent plan list',
|
|
229
229
|
summary: 'paginated list of plan artifacts, sorted ascending by name',
|
|
230
230
|
params: [
|
|
231
231
|
{
|
|
@@ -295,7 +295,7 @@ export function registerPlan() {
|
|
|
295
295
|
return defineBranch({
|
|
296
296
|
name: 'plan',
|
|
297
297
|
help: {
|
|
298
|
-
name: 'plan',
|
|
298
|
+
name: 'agent plan',
|
|
299
299
|
summary: 'create and read plan artifacts',
|
|
300
300
|
model: 'Lifecycle: draft -> active -> handed-off.',
|
|
301
301
|
children: [
|
package/dist/commands/skill.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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';
|
|
@@ -17,7 +18,7 @@ import { skillCreatePrompt, skillTemplatePrompt } from '../prompts/skill.js';
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
function formatNeighborQualifier(s) {
|
|
19
20
|
return s.plugin === SCOPE_SKILL_PLUGIN
|
|
20
|
-
? `${s.scope}
|
|
21
|
+
? `${s.scope}/${s.name}`
|
|
21
22
|
: `${s.plugin}/${s.name}`;
|
|
22
23
|
}
|
|
23
24
|
function formatNeighborKeywords(s) {
|
|
@@ -33,7 +34,7 @@ function buildNeighborsSection(skill) {
|
|
|
33
34
|
return null;
|
|
34
35
|
const lines = [
|
|
35
36
|
'## Neighbors',
|
|
36
|
-
'*Auto-discovered from filesystem. Run `crtr skill read
|
|
37
|
+
'*Auto-discovered from filesystem. Run `crtr skill read <name>` for full description + body.*',
|
|
37
38
|
'',
|
|
38
39
|
];
|
|
39
40
|
if (siblings.length > 0) {
|
|
@@ -127,7 +128,7 @@ const findList = defineLeaf({
|
|
|
127
128
|
return po;
|
|
128
129
|
return a.name.localeCompare(b.name);
|
|
129
130
|
});
|
|
130
|
-
const keyOf = (sk) => `${sk.scope}
|
|
131
|
+
const keyOf = (sk) => `${sk.scope}/${sk.plugin}/${sk.name}`;
|
|
131
132
|
const params = {};
|
|
132
133
|
if (limit !== undefined)
|
|
133
134
|
params.limit = limit;
|
|
@@ -155,7 +156,7 @@ const findList = defineLeaf({
|
|
|
155
156
|
}),
|
|
156
157
|
next_cursor: result.next_cursor,
|
|
157
158
|
total: result.total,
|
|
158
|
-
follow_up: 'Use `crtr skill read
|
|
159
|
+
follow_up: 'Use `crtr skill read <name>` for the full SKILL.md body. Run `crtr skill find list -h` for filters and verbosity.',
|
|
159
160
|
};
|
|
160
161
|
},
|
|
161
162
|
});
|
|
@@ -240,7 +241,7 @@ const findSearch = defineLeaf({
|
|
|
240
241
|
score: h.score,
|
|
241
242
|
description: h.skill.frontmatter.description !== undefined ? h.skill.frontmatter.description : null,
|
|
242
243
|
})),
|
|
243
|
-
follow_up: 'Use `crtr skill read
|
|
244
|
+
follow_up: 'Use `crtr skill read <name>` for the full SKILL.md body. Run `crtr skill find search -h` for filters.',
|
|
244
245
|
};
|
|
245
246
|
},
|
|
246
247
|
});
|
|
@@ -322,25 +323,27 @@ const findBranch = defineBranch({
|
|
|
322
323
|
children: [findList, findSearch, findGrep],
|
|
323
324
|
});
|
|
324
325
|
// ---------------------------------------------------------------------------
|
|
325
|
-
// read
|
|
326
|
+
// read leaf
|
|
326
327
|
// ---------------------------------------------------------------------------
|
|
327
|
-
const
|
|
328
|
-
name: '
|
|
328
|
+
const readLeaf = defineLeaf({
|
|
329
|
+
name: 'read',
|
|
329
330
|
help: {
|
|
330
|
-
name: 'skill read
|
|
331
|
-
summary: '
|
|
331
|
+
name: 'skill read',
|
|
332
|
+
summary: 'load SKILL.md body and resolution metadata for a named skill',
|
|
332
333
|
params: [
|
|
333
|
-
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Forms: <name>, <plugin
|
|
334
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Forms: <name>, <plugin>/<name>, <scope>/<name>, <scope>/<plugin>/<name>.' },
|
|
334
335
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Narrows resolution when name is ambiguous.' },
|
|
335
336
|
{ 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.' },
|
|
337
|
+
{ kind: 'flag', name: 'frontmatter', type: 'bool', required: false, constraint: 'When present, includes YAML frontmatter in the output content.' },
|
|
338
|
+
{ 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
339
|
],
|
|
338
340
|
output: [
|
|
339
341
|
{ name: 'name', type: 'string', required: true, constraint: 'Resolved skill name.' },
|
|
340
342
|
{ name: 'plugin', type: 'string', required: true, constraint: 'Plugin the skill belongs to.' },
|
|
341
343
|
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the skill was resolved from.' },
|
|
342
344
|
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to SKILL.md.' },
|
|
343
|
-
{ name: 'content', type: 'string', required:
|
|
345
|
+
{ name: 'content', type: 'string', required: false, constraint: 'SKILL.md body (with or without frontmatter per --frontmatter). Omitted when --no-body is set.' },
|
|
346
|
+
{ name: 'follow_up', type: 'string', required: false, constraint: 'Hints at variant flags. Present on default reads; omitted when --no-body is set.' },
|
|
344
347
|
],
|
|
345
348
|
outputKind: 'object',
|
|
346
349
|
effects: ['None. Read-only.'],
|
|
@@ -350,6 +353,7 @@ const readShow = defineLeaf({
|
|
|
350
353
|
const scopeStr = input['scope'];
|
|
351
354
|
const pluginFilter = input['plugin'];
|
|
352
355
|
const includeFrontmatter = input['frontmatter'];
|
|
356
|
+
const noBody = input['noBody'];
|
|
353
357
|
const resolveOpts = {};
|
|
354
358
|
if (scopeStr !== undefined) {
|
|
355
359
|
const resolved = resolveScopeArg(scopeStr);
|
|
@@ -359,70 +363,21 @@ const readShow = defineLeaf({
|
|
|
359
363
|
if (pluginFilter !== undefined)
|
|
360
364
|
resolveOpts.pluginFilter = pluginFilter;
|
|
361
365
|
const skillObj = resolveSkill(nameRaw, resolveOpts);
|
|
362
|
-
const
|
|
363
|
-
const rawBody = includeFrontmatter ? rawContent : parseFrontmatter(rawContent).body;
|
|
364
|
-
const content = appendNeighbors(skillObj, rawBody);
|
|
365
|
-
return {
|
|
366
|
+
const out = {
|
|
366
367
|
name: skillObj.name,
|
|
367
368
|
plugin: skillObj.plugin,
|
|
368
369
|
scope: skillObj.scope,
|
|
369
370
|
path: skillObj.path,
|
|
370
|
-
content,
|
|
371
371
|
};
|
|
372
|
+
if (noBody)
|
|
373
|
+
return out;
|
|
374
|
+
const rawContent = readText(skillObj.path);
|
|
375
|
+
const rawBody = includeFrontmatter ? rawContent : parseFrontmatter(rawContent).body;
|
|
376
|
+
out['content'] = appendNeighbors(skillObj, rawBody);
|
|
377
|
+
out['follow_up'] = 'Add --no-body to skip the body and return path/scope/plugin only. Add --frontmatter to include YAML frontmatter in content.';
|
|
378
|
+
return out;
|
|
372
379
|
},
|
|
373
380
|
});
|
|
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 {
|
|
407
|
-
name: skillObj.name,
|
|
408
|
-
plugin: skillObj.plugin,
|
|
409
|
-
scope: skillObj.scope,
|
|
410
|
-
path: skillObj.path,
|
|
411
|
-
};
|
|
412
|
-
},
|
|
413
|
-
});
|
|
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
381
|
// ---------------------------------------------------------------------------
|
|
427
382
|
// author sub-branch
|
|
428
383
|
// ---------------------------------------------------------------------------
|
|
@@ -461,7 +416,7 @@ const authorScaffold = defineLeaf({
|
|
|
461
416
|
name: 'skill author scaffold',
|
|
462
417
|
summary: 'create an empty SKILL.md stub at the given qualifier',
|
|
463
418
|
params: [
|
|
464
|
-
{ kind: 'positional', name: 'qualifier', required: true, constraint: 'Skill identifier in <plugin
|
|
419
|
+
{ kind: 'positional', name: 'qualifier', required: true, constraint: 'Skill identifier in <plugin>/<skill> form.' },
|
|
465
420
|
{ kind: 'flag', name: 'type', type: 'enum', choices: [...VALID_TYPES], required: false, constraint: 'One of: playbook, primer, reference, runbook, freeform.' },
|
|
466
421
|
{ kind: 'flag', name: 'description', type: 'string', required: false, constraint: 'Short description written to frontmatter.' },
|
|
467
422
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
|
|
@@ -481,16 +436,22 @@ const authorScaffold = defineLeaf({
|
|
|
481
436
|
const typeStr = input['type'];
|
|
482
437
|
const description = input['description'];
|
|
483
438
|
const scopeStr = input['scope'];
|
|
484
|
-
const
|
|
485
|
-
if (
|
|
439
|
+
const parsed = parseSkillQualifier(qualifier);
|
|
440
|
+
if (parsed.segments.length === 0) {
|
|
486
441
|
throw usage('skill name required in qualifier');
|
|
487
442
|
}
|
|
443
|
+
// For scaffold, the qualifier is always <plugin>/<skill>. If it's a single segment,
|
|
444
|
+
// treat it as a scope-direct skill name. Otherwise first segment is the plugin.
|
|
445
|
+
const pluginName = parsed.segments.length > 1 ? parsed.segments[0] : undefined;
|
|
446
|
+
const skillName = parsed.segments.length > 1
|
|
447
|
+
? parsed.segments.slice(1).join('/')
|
|
448
|
+
: parsed.segments[0];
|
|
488
449
|
if (typeStr !== undefined && !isSkillType(typeStr)) {
|
|
489
450
|
throw usage(`unknown skill type: ${typeStr} / valid: ${SKILL_TYPES.join(' | ')}`);
|
|
490
451
|
}
|
|
491
452
|
const skillType = typeStr !== undefined && isSkillType(typeStr) ? typeStr : undefined;
|
|
492
453
|
let skillFile;
|
|
493
|
-
// Scope-direct: no plugin qualifier, or explicit `_
|
|
454
|
+
// Scope-direct: no plugin qualifier, or explicit `_/` sentinel (internal only)
|
|
494
455
|
if (pluginName === undefined || pluginName === SCOPE_SKILL_PLUGIN) {
|
|
495
456
|
const scope = resolveWriteScope(scopeStr);
|
|
496
457
|
const scopeRootPath = requireScopeRoot(scope);
|
|
@@ -578,7 +539,7 @@ const stateEnable = defineLeaf({
|
|
|
578
539
|
name: 'skill state enable',
|
|
579
540
|
summary: 'enable a skill in the given scope',
|
|
580
541
|
params: [
|
|
581
|
-
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Same forms as skill read
|
|
542
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Same forms as skill read.' },
|
|
582
543
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
|
|
583
544
|
],
|
|
584
545
|
output: [
|
|
@@ -597,7 +558,7 @@ const stateDisable = defineLeaf({
|
|
|
597
558
|
name: 'skill state disable',
|
|
598
559
|
summary: 'disable a skill in the given scope, hiding it from list and agent discovery',
|
|
599
560
|
params: [
|
|
600
|
-
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Same forms as skill read
|
|
561
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Same forms as skill read.' },
|
|
601
562
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
|
|
602
563
|
],
|
|
603
564
|
output: [
|
|
@@ -622,12 +583,6 @@ const stateBranch = defineBranch({
|
|
|
622
583
|
},
|
|
623
584
|
children: [stateEnable, stateDisable],
|
|
624
585
|
});
|
|
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.
|
|
631
586
|
function buildSkillCatalog() {
|
|
632
587
|
let skills;
|
|
633
588
|
try {
|
|
@@ -640,38 +595,90 @@ function buildSkillCatalog() {
|
|
|
640
595
|
return null;
|
|
641
596
|
const bySource = new Map();
|
|
642
597
|
for (const s of skills) {
|
|
643
|
-
const key = `${s.scope}
|
|
598
|
+
const key = `${s.scope}\t${s.plugin}`;
|
|
644
599
|
const arr = bySource.get(key);
|
|
645
600
|
if (arr)
|
|
646
601
|
arr.push(s);
|
|
647
602
|
else
|
|
648
603
|
bySource.set(key, [s]);
|
|
649
604
|
}
|
|
650
|
-
const
|
|
651
|
-
|
|
605
|
+
const projectSources = [];
|
|
606
|
+
const userSources = [];
|
|
607
|
+
for (const [key, group] of bySource) {
|
|
608
|
+
const [scope, plugin] = key.split('\t');
|
|
652
609
|
const names = group.map((g) => g.name);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
if (!set) {
|
|
660
|
-
set = new Set();
|
|
661
|
-
byPrefix.set(prefix, set);
|
|
662
|
-
}
|
|
663
|
-
set.add(s.name);
|
|
664
|
-
}
|
|
610
|
+
const roots = names
|
|
611
|
+
.filter((n) => !names.some((m) => m !== n && n.startsWith(m + '/')))
|
|
612
|
+
.sort();
|
|
613
|
+
if (roots.length === 0)
|
|
614
|
+
continue;
|
|
615
|
+
(scope === 'project' ? projectSources : userSources).push({ plugin, roots });
|
|
665
616
|
}
|
|
666
|
-
const prefixes = [...byPrefix.keys()].sort();
|
|
667
|
-
const prefixW = prefixes.reduce((m, p) => (p.length > m ? p.length : m), 0);
|
|
668
617
|
const lines = [`Loaded skills (${skills.length})`];
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
lines.push(` ${prefix.padEnd(prefixW)} ${names.join(', ')}`);
|
|
672
|
-
}
|
|
618
|
+
renderCatalogSection('Project', projectSources, lines);
|
|
619
|
+
renderCatalogSection('User', userSources, lines);
|
|
673
620
|
return lines.join('\n');
|
|
674
621
|
}
|
|
622
|
+
function renderCatalogSection(label, sources, out) {
|
|
623
|
+
if (sources.length === 0)
|
|
624
|
+
return;
|
|
625
|
+
const count = sources.reduce((n, s) => n + s.roots.length, 0);
|
|
626
|
+
out.push('');
|
|
627
|
+
out.push(`${label} (${count})`);
|
|
628
|
+
const sentinel = sources.filter((s) => s.plugin === SCOPE_SKILL_PLUGIN);
|
|
629
|
+
const named = sources
|
|
630
|
+
.filter((s) => s.plugin !== SCOPE_SKILL_PLUGIN)
|
|
631
|
+
.sort((a, b) => a.plugin.localeCompare(b.plugin));
|
|
632
|
+
for (const s of sentinel) {
|
|
633
|
+
for (const n of s.roots)
|
|
634
|
+
out.push(` ${n}`);
|
|
635
|
+
}
|
|
636
|
+
if (named.length === 0)
|
|
637
|
+
return;
|
|
638
|
+
const classified = named.map((s) => {
|
|
639
|
+
const subcats = new Map();
|
|
640
|
+
const bare = [];
|
|
641
|
+
for (const n of s.roots) {
|
|
642
|
+
const slash = n.indexOf('/');
|
|
643
|
+
if (slash === -1) {
|
|
644
|
+
bare.push(n);
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
const sub = n.slice(0, slash);
|
|
648
|
+
const rest = n.slice(slash + 1);
|
|
649
|
+
const arr = subcats.get(sub);
|
|
650
|
+
if (arr)
|
|
651
|
+
arr.push(rest);
|
|
652
|
+
else
|
|
653
|
+
subcats.set(sub, [rest]);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return { plugin: s.plugin, roots: s.roots, subcats, bare };
|
|
657
|
+
});
|
|
658
|
+
// Smart-nest: a plugin nests iff it has 2+ distinct subcategories.
|
|
659
|
+
const nests = (p) => p.subcats.size >= 2;
|
|
660
|
+
const inlineLabels = classified.filter((p) => !nests(p)).map((p) => `${p.plugin}/`);
|
|
661
|
+
const inlineW = inlineLabels.reduce((m, p) => (p.length > m ? p.length : m), 0);
|
|
662
|
+
for (const p of classified) {
|
|
663
|
+
if (nests(p)) {
|
|
664
|
+
out.push(` ${p.plugin}/`);
|
|
665
|
+
if (p.bare.length > 0) {
|
|
666
|
+
out.push(` ${[...p.bare].sort().join(', ')}`);
|
|
667
|
+
}
|
|
668
|
+
const subKeys = [...p.subcats.keys()].sort();
|
|
669
|
+
const subLabels = subKeys.map((k) => `${k}/`);
|
|
670
|
+
const subW = subLabels.reduce((m, k) => (k.length > m ? k.length : m), 0);
|
|
671
|
+
for (let i = 0; i < subKeys.length; i++) {
|
|
672
|
+
const children = p.subcats.get(subKeys[i]).sort();
|
|
673
|
+
out.push(` ${subLabels[i].padEnd(subW)} ${children.join(', ')}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
const pluginLabel = `${p.plugin}/`;
|
|
678
|
+
out.push(` ${pluginLabel.padEnd(inlineW)} ${[...p.roots].sort().join(', ')}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
675
682
|
// ---------------------------------------------------------------------------
|
|
676
683
|
// Root export
|
|
677
684
|
// ---------------------------------------------------------------------------
|
|
@@ -681,15 +688,15 @@ export function registerSkill() {
|
|
|
681
688
|
help: {
|
|
682
689
|
name: 'skill',
|
|
683
690
|
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`
|
|
691
|
+
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
692
|
dynamicState: buildSkillCatalog,
|
|
686
693
|
children: [
|
|
687
694
|
{ name: 'find', desc: 'list, search, or grep skills', useWhen: 'discovering what skills are available' },
|
|
688
|
-
{ name: 'read', desc: '
|
|
695
|
+
{ name: 'read', desc: 'load SKILL.md body + metadata for a named skill', useWhen: 'loading a skill to act on it' },
|
|
689
696
|
{ name: 'author', desc: 'create and scaffold skills', useWhen: 'writing a new skill' },
|
|
690
697
|
{ name: 'state', desc: 'enable or disable skills', useWhen: 'toggling skill visibility' },
|
|
691
698
|
],
|
|
692
699
|
},
|
|
693
|
-
children: [findBranch,
|
|
700
|
+
children: [findBranch, readLeaf, authorBranch, stateBranch],
|
|
694
701
|
});
|
|
695
702
|
}
|
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 agent spec new`:\n\n echo '<spec markdown>' | crtr agent 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 agent new 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 agent 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 agent spec new\`:
|
|
85
85
|
|
|
86
|
-
echo '<spec markdown>' | crtr
|
|
86
|
+
echo '<spec markdown>' | crtr agent 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 agent new 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: 'agent 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 agent new 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 agent new 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 agent new 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: 'agent 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: 'agent 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: 'agent spec',
|
|
276
276
|
summary: 'create and read specification artifacts',
|
|
277
277
|
model: 'Lifecycle: draft -> approved. Approved specs drive plan creation.',
|
|
278
278
|
children: [
|