@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
@@ -240,12 +240,62 @@ export function resolveSkill(rawName, opts = {}) {
240
240
  const direct = findSkillMatches(skillName, pluginQualifier, effectiveScope, effectivePluginFilter);
241
241
  if (direct.length > 0)
242
242
  return pickMatch(direct, skillName, pluginQualifier);
243
+ // Leaf-name fallback: the caller supplied only the final path segment
244
+ // (e.g. "cli-design" for "ai/interface/cli-design"). A direct path lookup
245
+ // missed because the skill lives under a nested path. Match by last segment.
246
+ const byLeaf = findSkillsByLeaf(skillName, pluginQualifier, effectiveScope, effectivePluginFilter);
247
+ if (byLeaf.length === 1)
248
+ return byLeaf[0];
249
+ if (byLeaf.length > 1) {
250
+ throw ambiguous(formatLeafAmbiguousMessage(skillName, byLeaf), {
251
+ skill: skillName,
252
+ candidates: byLeaf.map((m) => ({
253
+ id: formatSkillId(m),
254
+ plugin: m.plugin,
255
+ scope: m.scope,
256
+ path: m.path,
257
+ })),
258
+ next: 'Multiple skills share this leaf name. Re-run with one of the full paths in candidates.',
259
+ });
260
+ }
243
261
  throw notFound(formatNotFoundMessage(rawName, skillName, pluginQualifier), {
244
262
  skill: skillName,
245
263
  plugin: pluginQualifier,
246
264
  scope: parsed.scope,
247
265
  });
248
266
  }
267
+ /** Canonical, unambiguous identifier for a skill. Scope-root skills are
268
+ * qualified by scope; plugin skills by plugin name. */
269
+ function formatSkillId(s) {
270
+ return s.plugin === SCOPE_SKILL_PLUGIN ? `${s.scope}/${s.name}` : `${s.plugin}/${s.name}`;
271
+ }
272
+ /** Match skills whose final path segment equals `leaf`. Only meaningful when
273
+ * `leaf` is a bare segment (no slash) — a slashed query can never equal a
274
+ * single segment, so this returns empty and the caller falls through. */
275
+ function findSkillsByLeaf(leaf, pluginQualifier, scope, pluginFilter) {
276
+ if (leaf.includes('/'))
277
+ return [];
278
+ let all;
279
+ try {
280
+ all = scope ? listAllSkills(scope) : listAllSkills();
281
+ }
282
+ catch {
283
+ return [];
284
+ }
285
+ return all.filter((s) => {
286
+ if ((s.name.split('/').pop() ?? s.name) !== leaf)
287
+ return false;
288
+ if (pluginQualifier && s.plugin !== pluginQualifier)
289
+ return false;
290
+ if (pluginFilter && s.plugin !== pluginFilter)
291
+ return false;
292
+ return true;
293
+ });
294
+ }
295
+ function formatLeafAmbiguousMessage(leaf, matches) {
296
+ const ids = matches.map(formatSkillId).join(', ');
297
+ return `ambiguous skill: ${leaf} matches multiple skills: ${ids}`;
298
+ }
249
299
  function findSkillMatches(name, pluginQualifier, scope, pluginFilter) {
250
300
  const plugins = scope ? listInstalledPlugins(scope) : listAllPlugins();
251
301
  const enabledPlugins = plugins.filter((p) => p.enabled);
@@ -332,7 +382,7 @@ function formatNotFoundMessage(rawName, skillName, pluginQualifier) {
332
382
  lines.push(` did you mean: ${formatted.join(', ')}`);
333
383
  }
334
384
  else {
335
- lines.push(' run `crtr skill list` or `crtr skill search <query>` to discover skills');
385
+ lines.push(' run `crtr skill find list` or `crtr skill find search <query>` to discover skills');
336
386
  }
337
387
  return lines.join('\n');
338
388
  }
@@ -1,5 +1,5 @@
1
1
  export interface SpawnAgentOptions {
2
- /** First user message for the new claude session. */
2
+ /** First user message for the new agent session. */
3
3
  prompt: string;
4
4
  cwd: string;
5
5
  /** crtr job_id injected as CRTR_JOB_ID env var in the pane. */
@@ -10,8 +10,14 @@ export interface SpawnAgentOptions {
10
10
  };
11
11
  /** Max panes per tmux window before overflowing to a new window. */
12
12
  maxPanesPerWindow: number;
13
- /** Display name passed to `claude -n`; surfaces in pane title and /resume picker. */
13
+ /** Display name passed to the agent's `-n` flag; surfaces in pane title and resume picker. */
14
14
  name?: string;
15
+ /** Persona appended via `--append-system-prompt` (subagent body). */
16
+ systemPrompt?: string;
17
+ /** Model pattern/id passed via `--model`. */
18
+ model?: string;
19
+ /** Tool allow-list passed to pi via `--tools`. */
20
+ tools?: string[];
15
21
  }
16
22
  export interface SpawnAgentResult {
17
23
  status: 'spawned' | 'spawn-failed' | 'not-in-tmux';
@@ -22,10 +28,11 @@ export interface SpawnAgentResult {
22
28
  message: string;
23
29
  }
24
30
  export interface DetachOptions {
25
- /** Inner command to run in the pane. If omitted, build `claude <prompt>`. */
31
+ /** Inner command to run in the pane. If omitted, build the detected agent's
32
+ * invocation around `<prompt>`. */
26
33
  command?: string;
27
- /** Full first user message for the new claude session (claude mode only;
28
- * ignored when `command` is set). No custom system prompt. */
34
+ /** Full first user message for the new agent session (ignored when `command`
35
+ * is set). No custom system prompt. */
29
36
  prompt?: string;
30
37
  cwd: string;
31
38
  /** crtr job_id injected as CRTR_JOB_ID env var in the pane and used by the
@@ -42,7 +49,7 @@ export interface DetachOptions {
42
49
  * uses the attached client's currently-focused pane — which drifts if the
43
50
  * user switches windows between kickoff and spawn. */
44
51
  targetPane?: string;
45
- /** Display name passed to `claude -n`; ignored when `command` is set
52
+ /** Display name passed to the agent's `-n` flag; ignored when `command` is set
46
53
  * (caller controls the full argv in that mode). */
47
54
  name?: string;
48
55
  }
@@ -53,18 +60,124 @@ export interface DetachResult {
53
60
  }
54
61
  export declare function isInTmux(): boolean;
55
62
  export declare function shellQuote(s: string): string;
63
+ /** Coding-agent CLIs crtr knows how to spawn as a sibling worker. */
64
+ export type AgentKind = 'claude' | 'pi';
65
+ /**
66
+ * Detect which coding-agent CLI is hosting the current crtr process so spawns
67
+ * launch a matching sibling. pi exports `PI_CODING_AGENT=true` into its tool
68
+ * subprocess environment; Claude Code exports `CLAUDECODE` /
69
+ * `CLAUDE_CODE_SESSION_ID`. Defaults to claude when no signal is present
70
+ * (preserves prior behavior).
71
+ */
72
+ export declare function detectAgentKind(): AgentKind;
73
+ /**
74
+ * Normalize a `--model` value for the target agent CLI.
75
+ *
76
+ * Subagent frontmatter uses Claude Code's bare aliases (`sonnet`, `opus`,
77
+ * `haiku`, optionally with a `:thinking` suffix). The `claude` CLI resolves
78
+ * those natively, but `pi` maps a bare alias to its default provider —
79
+ * `amazon-bedrock` — which most users have not authenticated, so the spawn
80
+ * dies with "No API key found for amazon-bedrock". These aliases name Anthropic
81
+ * models, so under pi we pin them to the `anthropic/` provider (preserving any
82
+ * `:thinking` suffix). Values that already carry a `provider/` prefix or are
83
+ * concrete model ids are passed through untouched.
84
+ */
85
+ export declare function normalizeModelForKind(model: string, kind: AgentKind): string;
86
+ export interface AgentCommandOptions {
87
+ /** First user message delivered to the new agent session. */
88
+ prompt: string;
89
+ /** Display name (`-n`); surfaces in the pane title and resume picker. */
90
+ name?: string;
91
+ /** Fork an existing session into a fresh one rather than starting clean. */
92
+ fork?: {
93
+ sessionId: string;
94
+ };
95
+ /** Persona/system prompt appended to the agent's default (`--append-system-prompt`).
96
+ * Used to apply a subagent definition's body. */
97
+ systemPrompt?: string;
98
+ /** Model pattern/id passed via `--model` (both claude and pi). */
99
+ model?: string;
100
+ /** Tool allow-list. Passed to pi via `--tools`; ignored for claude, whose
101
+ * tool names and gating flag differ. */
102
+ tools?: string[];
103
+ }
104
+ /**
105
+ * Build the agent-CLI invocation (no job wrapper) for the given kind.
106
+ *
107
+ * claude: `claude [-n <name>] [--resume <id> --fork-session] \
108
+ * --dangerously-skip-permissions <prompt>`
109
+ * pi: `pi [-n <name>] [--fork <id>] <prompt>`
110
+ *
111
+ * pi has no permission popups, so it needs no skip-permissions flag.
112
+ */
113
+ export declare function buildAgentCommand(opts: AgentCommandOptions, kind?: AgentKind): string;
114
+ export interface AgentPrintArgv {
115
+ cmd: string;
116
+ args: string[];
117
+ }
118
+ /**
119
+ * Argv for a non-interactive print-mode run.
120
+ *
121
+ * claude: `claude [-n <name>] [--resume <id> --fork-session] -p \
122
+ * --dangerously-skip-permissions <prompt>`
123
+ * pi: `pi [-n <name>] [--fork <id>] -p <prompt>`
124
+ *
125
+ * Returned as a cmd + args array so callers can spawn without a shell.
126
+ */
127
+ export declare function buildAgentPrintArgv(opts: AgentCommandOptions, kind?: AgentKind): AgentPrintArgv;
128
+ /** Same as buildAgentPrintArgv but rendered as a single shell-quoted string. */
129
+ export declare function buildAgentPrintCommand(opts: AgentCommandOptions, kind?: AgentKind): string;
130
+ export interface HeadlessRunResult {
131
+ status: 'done' | 'failed';
132
+ /** Captured stdout on success; stdout+stderr (or an error message) on failure. */
133
+ output: string;
134
+ exitCode: number | null;
135
+ }
136
+ /**
137
+ * Run the agent headlessly and resolve once it exits. A blocking caller awaits
138
+ * this. stdout is captured as the result; a non-zero exit yields status
139
+ * 'failed' with the combined output.
140
+ */
141
+ export declare function runAgentHeadless(opts: {
142
+ prompt: string;
143
+ name?: string;
144
+ cwd: string;
145
+ systemPrompt?: string;
146
+ model?: string;
147
+ tools?: string[];
148
+ }): Promise<HeadlessRunResult>;
149
+ export interface HeadlessDetachResult {
150
+ status: 'spawned' | 'spawn-failed';
151
+ pid?: number;
152
+ message: string;
153
+ }
154
+ /**
155
+ * Launch a headless agent detached (background). Its print output is captured
156
+ * and delivered to the job via `crtr job submit`; a non-zero exit marks the job
157
+ * failed. Returns immediately with the wrapper pid (recorded for crash
158
+ * detection). No tmux required.
159
+ */
160
+ export declare function spawnHeadlessDetached(opts: {
161
+ prompt: string;
162
+ name?: string;
163
+ cwd: string;
164
+ jobId: string;
165
+ systemPrompt?: string;
166
+ model?: string;
167
+ tools?: string[];
168
+ }): HeadlessDetachResult;
56
169
  export declare function countPanesInCurrentWindow(): number;
57
170
  /**
58
171
  * Find a window in the current tmux session with fewer than `maxPanesPerWindow`
59
- * panes AND where every existing pane has `claude` as a foreground process.
60
- * Prefers the active window so the spawned pane is visible to the user;
61
- * otherwise falls back to the first other eligible window. Returns the tmux
62
- * window id (e.g. `@5`) to pass via `-t`, or null if no window qualifies.
172
+ * panes AND where every existing pane hosts an agent (claude or pi) as its
173
+ * foreground process. Prefers the active window so the spawned pane is visible
174
+ * to the user; otherwise falls back to the first other eligible window. Returns
175
+ * the tmux window id (e.g. `@5`) to pass via `-t`, or null if no window qualifies.
63
176
  *
64
177
  * Windows holding non-agent panes (dashboards, log tails, idle shells, editors,
65
178
  * REPLs, etc.) are skipped so spawning never disrupts those workflows. A pane
66
- * qualifies as long as `claude` is among its foreground commands — co-resident
67
- * helpers like `caffeinate` don't disqualify it.
179
+ * qualifies as long as an agent comm is among its foreground commands —
180
+ * co-resident helpers like `caffeinate` don't disqualify it.
68
181
  */
69
182
  export declare function findWindowWithSpace(maxPanesPerWindow: number): string | null;
70
183
  /**
@@ -78,23 +191,27 @@ export declare function findWindowWithSpace(maxPanesPerWindow: number): string |
78
191
  */
79
192
  export declare function scheduleKillCurrentPane(delaySeconds: number): boolean;
80
193
  /**
81
- * Fire-and-forget: launch an interactive `claude` in a new pane (or window),
194
+ * Fire-and-forget: launch an interactive agent in a new pane (or window),
82
195
  * then schedule the originating pane to be killed after `killAfterSeconds`.
83
196
  *
84
197
  * No custom system prompt — the task is delivered as the first user message.
85
- * Returns as soon as the new pane is up; does NOT wait for claude to finish.
198
+ * Returns as soon as the new pane is up; does NOT wait for the agent to finish.
86
199
  */
87
200
  export declare function spawnAndDetach(opts: DetachOptions): DetachResult;
201
+ /** Originating pane id + session id of the host (pi/claude) crtr runs under. */
202
+ export declare function originContext(): {
203
+ pane: string;
204
+ sessionId: string;
205
+ } | null;
206
+ /** Deterministic subagent session name for an originating pane id (e.g. `%5`). */
207
+ export declare function subagentSessionName(pane: string): string;
88
208
  /**
89
- * Async sibling spawn. Launches a claude session in a tmux pane, progressively
90
- * filling existing windows up to `maxPanesPerWindow` before creating a new
91
- * window. Returns immediately with the pane id; the parent stays alive.
92
- *
93
- * Placement order:
94
- * 1. Current window, if it has space.
95
- * 2. Any other window in the session with space.
96
- * 3. New window (every existing window at capacity).
209
+ * Async sibling spawn. Launches an interactive agent (claude or pi, per
210
+ * detectAgentKind) in the dedicated subagent session, progressively filling
211
+ * windows up to `maxPanesPerWindow` before creating a new window. Returns
212
+ * immediately with the pane id; the parent stays alive. Focus is never
213
+ * switched — the user jumps to the subagent session with Alt-o.
97
214
  *
98
- * If `fork` is set, uses `claude --resume <id> --fork-session`.
215
+ * If `fork` is set, forks the host session into a fresh one.
99
216
  */
100
217
  export declare function spawnAgent(opts: SpawnAgentOptions): SpawnAgentResult;