@crouton-kit/crouter 0.3.8 → 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 (46) hide show
  1. package/dist/cli.js +14 -24
  2. package/dist/commands/agent.d.ts +4 -0
  3. package/dist/commands/agent.js +444 -243
  4. package/dist/commands/debug.d.ts +1 -1
  5. package/dist/commands/debug.js +20 -7
  6. package/dist/commands/human.js +51 -19
  7. package/dist/commands/job.d.ts +9 -0
  8. package/dist/commands/job.js +50 -10
  9. package/dist/commands/mode.d.ts +2 -0
  10. package/dist/commands/mode.js +231 -0
  11. package/dist/commands/pkg.js +5 -0
  12. package/dist/commands/plan.d.ts +1 -1
  13. package/dist/commands/plan.js +24 -11
  14. package/dist/commands/skill.js +20 -4
  15. package/dist/commands/spec.d.ts +1 -1
  16. package/dist/commands/spec.js +24 -11
  17. package/dist/commands/sys.js +5 -0
  18. package/dist/core/__tests__/job.test.js +11 -11
  19. package/dist/core/__tests__/jobs.test.js +33 -1
  20. package/dist/core/__tests__/resolver.test.js +69 -1
  21. package/dist/core/__tests__/spawn.test.d.ts +1 -0
  22. package/dist/core/__tests__/spawn.test.js +138 -0
  23. package/dist/core/__tests__/subagents.test.d.ts +1 -0
  24. package/dist/core/__tests__/subagents.test.js +75 -0
  25. package/dist/core/__tests__/unknown-path.test.d.ts +1 -0
  26. package/dist/core/__tests__/unknown-path.test.js +52 -0
  27. package/dist/core/bootstrap.d.ts +2 -0
  28. package/dist/core/bootstrap.js +66 -0
  29. package/dist/core/command.d.ts +58 -2
  30. package/dist/core/command.js +62 -14
  31. package/dist/core/frontmatter.d.ts +10 -0
  32. package/dist/core/frontmatter.js +24 -9
  33. package/dist/core/help.d.ts +39 -8
  34. package/dist/core/help.js +64 -32
  35. package/dist/core/jobs.d.ts +8 -2
  36. package/dist/core/jobs.js +109 -6
  37. package/dist/core/resolver.js +51 -1
  38. package/dist/core/spawn.d.ts +140 -23
  39. package/dist/core/spawn.js +392 -73
  40. package/dist/core/subagents.d.ts +18 -0
  41. package/dist/core/subagents.js +163 -0
  42. package/dist/prompts/agent.d.ts +10 -1
  43. package/dist/prompts/agent.js +34 -3
  44. package/dist/types.d.ts +21 -0
  45. package/dist/types.js +3 -0
  46. package/package.json +2 -2
@@ -0,0 +1,163 @@
1
+ // Subagent discovery + resolution.
2
+ //
3
+ // Subagents are markdown files with YAML frontmatter, modeled on the pi
4
+ // subagent extension but surfaced through the crtr `agent` CLI. Each file
5
+ // declares a name/description (+ optional tools/model) in frontmatter; its
6
+ // markdown body becomes the spawned agent's appended system prompt.
7
+ //
8
+ // Layout mirrors skills, one level shallower (flat files, not nested dirs):
9
+ // <scope-root>/agents/<name>.md — scope-root agents (user/project)
10
+ // <plugin-root>/agents/<name>.md — plugin-provided agents
11
+ //
12
+ // Resolution precedence matches skills: project before user before builtin;
13
+ // scope-root agents before plugin agents within a scope.
14
+ import { join, basename } from 'node:path';
15
+ import { readdirSync } from 'node:fs';
16
+ import { AGENTS_DIR, SCOPE_SKILL_PLUGIN } from '../types.js';
17
+ import { listAllPlugins, listInstalledPlugins } from './resolver.js';
18
+ import { parseFrontmatterGeneric } from './frontmatter.js';
19
+ import { pathExists, readText } from './fs-utils.js';
20
+ import { projectScopeRoot, scopeRoot } from './scope.js';
21
+ import { ambiguous, notFound } from './errors.js';
22
+ /** `<scope-root>/agents` for a given scope, or null when the scope has no root. */
23
+ export function scopeAgentsDir(scope) {
24
+ const root = scopeRoot(scope);
25
+ return root ? join(root, AGENTS_DIR) : null;
26
+ }
27
+ function coerceTools(value) {
28
+ if (Array.isArray(value)) {
29
+ const arr = value.map((v) => String(v).trim()).filter(Boolean);
30
+ return arr.length > 0 ? arr : undefined;
31
+ }
32
+ if (typeof value === 'string') {
33
+ const arr = value
34
+ .split(',')
35
+ .map((t) => t.trim())
36
+ .filter(Boolean);
37
+ return arr.length > 0 ? arr : undefined;
38
+ }
39
+ return undefined;
40
+ }
41
+ function parseSubagentFile(filePath, scope, plugin) {
42
+ let source;
43
+ try {
44
+ source = readText(filePath);
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ const { data, body } = parseFrontmatterGeneric(source);
50
+ if (data === null)
51
+ return null;
52
+ // Name defaults to the filename stem when frontmatter omits it. A description
53
+ // is required for the agent to be useful in listings; skip files without one.
54
+ const fileStem = basename(filePath).replace(/\.md$/i, '');
55
+ const name = typeof data.name === 'string' && data.name.trim() !== ''
56
+ ? data.name.trim()
57
+ : fileStem;
58
+ if (typeof data.description !== 'string' || data.description.trim() === '') {
59
+ return null;
60
+ }
61
+ const fm = {
62
+ name,
63
+ description: data.description.trim(),
64
+ tools: coerceTools(data.tools),
65
+ model: typeof data.model === 'string' && data.model.trim() !== '' ? data.model.trim() : undefined,
66
+ };
67
+ return {
68
+ name,
69
+ plugin,
70
+ scope,
71
+ path: filePath,
72
+ frontmatter: fm,
73
+ systemPrompt: body,
74
+ };
75
+ }
76
+ function listAgentsInDir(dir, scope, plugin) {
77
+ if (!pathExists(dir))
78
+ return [];
79
+ let entries;
80
+ try {
81
+ entries = readdirSync(dir, { withFileTypes: true });
82
+ }
83
+ catch {
84
+ return [];
85
+ }
86
+ const out = [];
87
+ for (const e of entries) {
88
+ if (!e.name.toLowerCase().endsWith('.md'))
89
+ continue;
90
+ if (!e.isFile() && !e.isSymbolicLink())
91
+ continue;
92
+ const parsed = parseSubagentFile(join(dir, e.name), scope, plugin);
93
+ if (parsed !== null)
94
+ out.push(parsed);
95
+ }
96
+ return out;
97
+ }
98
+ /** Scope-root agents under `<scope-root>/agents/*.md`. */
99
+ export function listScopeRootSubagents(scope) {
100
+ if (scope === 'builtin')
101
+ return [];
102
+ const dir = scopeAgentsDir(scope);
103
+ if (!dir)
104
+ return [];
105
+ return listAgentsInDir(dir, scope, SCOPE_SKILL_PLUGIN);
106
+ }
107
+ /** All subagents: scope-root agents (project, user) plus enabled plugins. */
108
+ export function listSubagents(scopeFilter) {
109
+ const scopes = scopeFilter
110
+ ? [scopeFilter]
111
+ : [projectScopeRoot() ? 'project' : null, 'user'].filter(Boolean);
112
+ const fromScopeRoots = scopes.flatMap((s) => listScopeRootSubagents(s));
113
+ const plugins = scopeFilter ? listInstalledPlugins(scopeFilter) : listAllPlugins();
114
+ const fromPlugins = plugins
115
+ .filter((p) => p.enabled)
116
+ .flatMap((p) => listAgentsInDir(join(p.root, AGENTS_DIR), p.scope, p.name));
117
+ return [...fromScopeRoots, ...fromPlugins].sort((a, b) => a.name.localeCompare(b.name));
118
+ }
119
+ /** Canonical, unambiguous identifier: `<plugin>/<name>`, or bare `<name>` for
120
+ * scope-root agents. */
121
+ export function subagentId(a) {
122
+ return a.plugin === SCOPE_SKILL_PLUGIN ? a.name : `${a.plugin}/${a.name}`;
123
+ }
124
+ /** Resolve a subagent by name. Accepts a bare `<name>` or a `<plugin>/<name>`
125
+ * qualifier. Project precedes user precedes builtin; scope-root precedes
126
+ * plugin. Throws notFound / ambiguous as the skill resolver does. */
127
+ export function resolveSubagent(rawName, opts = {}) {
128
+ const slash = rawName.indexOf('/');
129
+ const pluginQualifier = opts.plugin ?? (slash !== -1 ? rawName.slice(0, slash) : undefined);
130
+ const name = slash !== -1 && opts.plugin === undefined ? rawName.slice(slash + 1) : rawName;
131
+ const all = listSubagents(opts.scope);
132
+ let matches = all.filter((a) => a.name === name);
133
+ if (pluginQualifier !== undefined) {
134
+ matches = matches.filter((a) => a.plugin === pluginQualifier ||
135
+ (pluginQualifier === SCOPE_SKILL_PLUGIN && a.plugin === SCOPE_SKILL_PLUGIN));
136
+ }
137
+ if (matches.length === 1)
138
+ return matches[0];
139
+ if (matches.length === 0) {
140
+ const known = all.map(subagentId).slice(0, 8).join(', ');
141
+ throw notFound(`subagent not found: ${rawName}`, {
142
+ subagent: name,
143
+ plugin: pluginQualifier,
144
+ next: known !== ''
145
+ ? `Known subagents: ${known}. Run \`crtr agent subagent list\` for the full set.`
146
+ : 'No subagents defined. Run `crtr agent subagent author -h` to scaffold one.',
147
+ });
148
+ }
149
+ // Multiple matches: prefer the highest-precedence scope/source deterministically.
150
+ const score = (a) => {
151
+ const scopeScore = a.scope === 'project' ? 0 : a.scope === 'user' ? 1 : 2;
152
+ const sourceScore = a.plugin === SCOPE_SKILL_PLUGIN ? 0 : 1;
153
+ return scopeScore * 2 + sourceScore;
154
+ };
155
+ const sorted = [...matches].sort((a, b) => score(a) - score(b));
156
+ if (score(sorted[0]) !== score(sorted[1]))
157
+ return sorted[0];
158
+ throw ambiguous(`ambiguous subagent: ${rawName}`, {
159
+ subagent: name,
160
+ candidates: matches.map((m) => ({ id: subagentId(m), scope: m.scope, path: m.path })),
161
+ next: 'Qualify with `<plugin>/<name>` or pass --scope to disambiguate.',
162
+ });
163
+ }
@@ -2,7 +2,7 @@
2
2
  * First user message for a spec → plan handoff.
3
3
  *
4
4
  * Thin prompt: the worker discovers the full planning workflow by running
5
- * `crtr agent plan new -h`, then saves the plan via `crtr agent plan new`. This
5
+ * `crtr mode plan new -h`, then saves the plan via `crtr mode plan new`. This
6
6
  * avoids embedding the planPrompt() blob here and keeps the prompt in sync
7
7
  * with the live CLI without any coupling.
8
8
  */
@@ -16,3 +16,12 @@ export declare function implementHandoffPrompt(planPath: string, jobId: string):
16
16
  * The reviewer submits via `crtr job submit` rather than `crtr agent submit`.
17
17
  */
18
18
  export declare function reviewerHandoffPrompt(artifactPath: string, artifactKind: 'plan' | 'spec', specPath: string | null, jobId: string): string;
19
+ /**
20
+ * First user message for a general `agent new` worker.
21
+ *
22
+ * The worker runs as an interactive agent in a tmux pane (not print mode), so
23
+ * its stdout is NOT captured as the result — it must deliver its answer via
24
+ * `crtr job submit`, exactly like the mode-handoff workers. The original task
25
+ * is sent verbatim; the submit contract is appended after a separator.
26
+ */
27
+ export declare function agentNewPrompt(task: string, jobId: string): string;
@@ -3,7 +3,7 @@ import { planReviewPrompt, specReviewPrompt } from './review.js';
3
3
  * First user message for a spec → plan handoff.
4
4
  *
5
5
  * Thin prompt: the worker discovers the full planning workflow by running
6
- * `crtr agent plan new -h`, then saves the plan via `crtr agent plan new`. This
6
+ * `crtr mode plan new -h`, then saves the plan via `crtr mode plan new`. This
7
7
  * avoids embedding the planPrompt() blob here and keeps the prompt in sync
8
8
  * with the live CLI without any coupling.
9
9
  */
@@ -12,9 +12,9 @@ export function planHandoffPrompt(specPath, jobId) {
12
12
 
13
13
  **Spec:** ${specPath}
14
14
 
15
- 1. Run \`crtr agent plan new -h\` to load the planning workflow and output schema.
15
+ 1. Run \`crtr mode plan new -h\` to load the planning workflow and output schema.
16
16
  2. Read the spec end-to-end.
17
- 3. Follow the workflow from step 1 and save the plan by passing the plan markdown to \`crtr agent plan new\` on stdin.
17
+ 3. Follow the workflow from step 1 and save the plan by passing the plan markdown to \`crtr mode plan new\` on stdin.
18
18
  4. When done, submit a short markdown report on stdin:
19
19
 
20
20
  \`\`\`bash
@@ -151,3 +151,34 @@ export function reviewerHandoffPrompt(artifactPath, artifactKind, specPath, jobI
151
151
 
152
152
  After calling \`crtr job submit\`, your turn ends and the pane closes itself. Do NOT chat or summarize after submission.`;
153
153
  }
154
+ /**
155
+ * First user message for a general `agent new` worker.
156
+ *
157
+ * The worker runs as an interactive agent in a tmux pane (not print mode), so
158
+ * its stdout is NOT captured as the result — it must deliver its answer via
159
+ * `crtr job submit`, exactly like the mode-handoff workers. The original task
160
+ * is sent verbatim; the submit contract is appended after a separator.
161
+ */
162
+ export function agentNewPrompt(task, jobId) {
163
+ return `${task}
164
+
165
+ ---
166
+
167
+ When you have finished the task above, deliver your final answer by piping your
168
+ full markdown result to \`crtr job submit\` — this is how the result is returned
169
+ to whoever spawned you:
170
+
171
+ \`\`\`bash
172
+ crtr job submit ${jobId} <<'MD'
173
+ <your complete answer / findings>
174
+ MD
175
+ \`\`\`
176
+
177
+ If you cannot complete the task, submit a failure with a reason instead:
178
+
179
+ \`\`\`bash
180
+ crtr job submit ${jobId} --status failed --reason "<why>"
181
+ \`\`\`
182
+
183
+ Do your work first; submit exactly once when done. After submitting, your turn ends.`;
184
+ }
package/dist/types.d.ts CHANGED
@@ -90,6 +90,26 @@ export interface Skill {
90
90
  enabled: boolean;
91
91
  disabledIn?: Scope;
92
92
  }
93
+ export interface SubagentFrontmatter {
94
+ name: string;
95
+ description?: string;
96
+ /** Tool allow-list (pi tool names). Passed through to pi via `--tools`. */
97
+ tools?: string[];
98
+ /** Model pattern/id passed to the agent CLI via `--model`. */
99
+ model?: string;
100
+ }
101
+ export interface Subagent {
102
+ name: string;
103
+ /** Plugin the subagent belongs to, or SCOPE_SKILL_PLUGIN ('_') for a
104
+ * scope-root agent stored at `<scope-root>/agents/<name>.md`. */
105
+ plugin: string;
106
+ scope: Scope;
107
+ /** Absolute path to the agent's .md file. */
108
+ path: string;
109
+ frontmatter: SubagentFrontmatter;
110
+ /** Markdown body — used as the spawned agent's appended system prompt. */
111
+ systemPrompt: string;
112
+ }
93
113
  export interface InstalledPlugin {
94
114
  name: string;
95
115
  scope: Scope;
@@ -117,6 +137,7 @@ export declare const CONFIG_FILE = "config.json";
117
137
  export declare const STATE_FILE = "state.json";
118
138
  export declare const SKILL_ENTRY_FILE = "SKILL.md";
119
139
  export declare const SKILLS_DIR = "skills";
140
+ export declare const AGENTS_DIR = "agents";
120
141
  export declare const SCOPE_SKILL_PLUGIN = "_";
121
142
  export declare const DEFAULT_MAX_PANES_PER_WINDOW = 3;
122
143
  export declare function defaultScopeConfig(): ScopeConfig;
package/dist/types.js CHANGED
@@ -20,6 +20,9 @@ export const CONFIG_FILE = 'config.json';
20
20
  export const STATE_FILE = 'state.json';
21
21
  export const SKILL_ENTRY_FILE = 'SKILL.md';
22
22
  export const SKILLS_DIR = 'skills';
23
+ // Subagent definitions live as flat `<name>.md` files under `<root>/agents/`,
24
+ // for both scope roots and plugins. Mirrors SKILLS_DIR.
25
+ export const AGENTS_DIR = 'agents';
23
26
  // Sentinel plugin name for skills that live at a scope root (no plugin wrapper).
24
27
  // Stored as `<scope-root>/skills/<name>/SKILL.md`. Shown in listings without the
25
28
  // `_/` prefix.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouton-kit/crouter",
3
- "version": "0.3.8",
3
+ "version": "0.3.11",
4
4
  "description": "crtr — fast access to skills, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "license": "MIT",
37
37
  "dependencies": {
38
- "@crouton-kit/humanloop": "^0.3.11",
38
+ "@crouton-kit/humanloop": "^0.3.12",
39
39
  "commander": "^13.0.0"
40
40
  },
41
41
  "devDependencies": {