@crouton-kit/crouter 0.3.3 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +3 -3
- package/dist/cli.js +16 -26
- package/dist/commands/__tests__/skill.test.js +24 -28
- package/dist/commands/agent.d.ts +6 -0
- package/dist/commands/agent.js +585 -0
- package/dist/commands/debug.d.ts +1 -1
- package/dist/commands/debug.js +20 -7
- package/dist/commands/human.js +51 -19
- package/dist/commands/job.d.ts +9 -0
- package/dist/commands/job.js +100 -385
- package/dist/commands/{flow.d.ts → mode.d.ts} +1 -1
- package/dist/commands/mode.js +231 -0
- package/dist/commands/pkg.js +5 -0
- package/dist/commands/plan.d.ts +1 -1
- package/dist/commands/plan.js +24 -11
- package/dist/commands/skill.js +130 -107
- package/dist/commands/spec.d.ts +1 -1
- package/dist/commands/spec.js +24 -11
- package/dist/commands/sys.js +5 -0
- package/dist/core/__tests__/job.test.js +38 -74
- package/dist/core/__tests__/jobs.test.d.ts +1 -0
- package/dist/core/__tests__/jobs.test.js +98 -0
- package/dist/core/__tests__/resolver.test.d.ts +1 -0
- package/dist/core/__tests__/resolver.test.js +181 -0
- package/dist/core/__tests__/spawn.test.d.ts +1 -0
- package/dist/core/__tests__/spawn.test.js +138 -0
- package/dist/core/__tests__/subagents.test.d.ts +1 -0
- package/dist/core/__tests__/subagents.test.js +75 -0
- package/dist/core/__tests__/unknown-path.test.d.ts +1 -0
- package/dist/core/__tests__/unknown-path.test.js +52 -0
- package/dist/core/bootstrap.d.ts +2 -0
- package/dist/core/bootstrap.js +66 -0
- package/dist/core/command.d.ts +58 -2
- package/dist/core/command.js +62 -14
- package/dist/core/config.js +20 -2
- package/dist/core/frontmatter.d.ts +10 -0
- package/dist/core/frontmatter.js +24 -9
- package/dist/core/help.d.ts +39 -8
- package/dist/core/help.js +64 -32
- package/dist/core/jobs.d.ts +33 -13
- package/dist/core/jobs.js +259 -47
- package/dist/core/resolver.d.ts +1 -2
- package/dist/core/resolver.js +111 -47
- package/dist/core/spawn.d.ts +150 -10
- package/dist/core/spawn.js +493 -41
- package/dist/core/subagents.d.ts +18 -0
- package/dist/core/subagents.js +163 -0
- package/dist/prompts/agent.d.ts +12 -3
- package/dist/prompts/agent.js +51 -18
- package/dist/prompts/debug.js +14 -7
- package/dist/prompts/skill.js +16 -16
- package/dist/types.d.ts +22 -1
- package/dist/types.js +5 -2
- package/package.json +2 -2
- package/dist/commands/flow.js +0 -24
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
// `crtr agent` umbrella — spawn primitives.
|
|
2
|
+
//
|
|
3
|
+
// `agent new` is the single spawn command (general-purpose by default,
|
|
4
|
+
// `--agent <id>` overlays a defined subagent). `agent fork` carries the
|
|
5
|
+
// current session context into a sibling pane. Spawning creates a job record;
|
|
6
|
+
// monitoring lives at `crtr job`.
|
|
7
|
+
//
|
|
8
|
+
// Spec/plan/debug workflows live under `crtr mode` (src/commands/mode.ts).
|
|
9
|
+
//
|
|
10
|
+
// Terminal-write contract for spawned workers:
|
|
11
|
+
// A worker MAY call `crtr job submit` to deliver a result, but is not
|
|
12
|
+
// required to. A job reaches a terminal state by any of three signals:
|
|
13
|
+
// 1. `crtr job submit` writes result.md (done|failed).
|
|
14
|
+
// 2. The wrapper shell's `crtr job _fail` runs when claude exits normally
|
|
15
|
+
// without a submit (result.md absent → failed).
|
|
16
|
+
// 3. The hosting tmux pane is closed/killed — case 2 never runs (SIGHUP),
|
|
17
|
+
// so the jobs layer reaps the job when its recorded pane disappears.
|
|
18
|
+
// Spawns record their pane id so (3) and `crtr job cancel` can act on it.
|
|
19
|
+
import { defineBranch, defineLeaf } from '../core/command.js';
|
|
20
|
+
import { InputError } from '../core/io.js';
|
|
21
|
+
import { writeFileSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { stateBlock } from '../core/help.js';
|
|
24
|
+
import { usage, general } from '../core/errors.js';
|
|
25
|
+
import { listSubagents, resolveSubagent, subagentId, scopeAgentsDir, } from '../core/subagents.js';
|
|
26
|
+
import { resolveScopeArg, requireScopeRoot, projectScopeRoot } from '../core/scope.js';
|
|
27
|
+
import { ensureScopeInitialized } from '../core/config.js';
|
|
28
|
+
import { ensureDir, pathExists } from '../core/fs-utils.js';
|
|
29
|
+
import { createJob, appendEvent, recordJobPane, recordJobPid, writeMarkdownResult, readResult as readJobResult, } from '../core/jobs.js';
|
|
30
|
+
import { spawnAgent, isInTmux, detectAgentKind, runAgentHeadless, spawnHeadlessDetached, } from '../core/spawn.js';
|
|
31
|
+
import { agentNewPrompt } from '../prompts/agent.js';
|
|
32
|
+
import { readConfig } from '../core/config.js';
|
|
33
|
+
export const DEFAULT_KILL_SECS = 2;
|
|
34
|
+
const WAIT_BUDGET_MS = 10 * 60 * 1000;
|
|
35
|
+
// The built-in, persona-less agent type. `--agent general` (or omitting --agent)
|
|
36
|
+
// spawns the general-purpose worker; any other id overlays a defined subagent.
|
|
37
|
+
const GENERAL_AGENT = 'general';
|
|
38
|
+
// A spawn leaf returns a job handle, not a result. This follow_up is the
|
|
39
|
+
// ORCHESTRATOR's own next call: the agent that spawned the worker collects the
|
|
40
|
+
// result itself and reports the findings to the user. The observed failure mode
|
|
41
|
+
// is relaying these commands to the human ("run this to see the result") or
|
|
42
|
+
// inventing a batch-await that doesn't exist — the phrasing forecloses both.
|
|
43
|
+
export function followUpResult(jobId) {
|
|
44
|
+
return `You spawned this worker — collecting its result is your job, not the user's. When you're ready for it, run \`crtr job read result ${jobId} --wait\` yourself (blocks up to 10 min), then report the worker's findings. Spawned several? Call it once per job_id — there is no batch await. Never print these commands for the user to run.`;
|
|
45
|
+
}
|
|
46
|
+
export function resolveMaxPanes() {
|
|
47
|
+
const cfg = readConfig('user');
|
|
48
|
+
return cfg.max_panes_per_window;
|
|
49
|
+
}
|
|
50
|
+
export function assertTmux() {
|
|
51
|
+
if (!isInTmux()) {
|
|
52
|
+
throw new InputError({
|
|
53
|
+
error: 'not_in_tmux',
|
|
54
|
+
message: 'crtr agent new requires tmux (TMUX env var not set).',
|
|
55
|
+
next: 'Run inside a tmux session.',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Subagent catalog (dynamicState for `agent -h` and `agent subagent -h`)
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Preamble for the subagent catalog. Scoped to ONE job: how to invoke a worker
|
|
63
|
+
// and how to pick which agent runs it. The reach-for-it reflex and the
|
|
64
|
+
// when-to-delegate scenarios live on the root `useWhen` (which renders right
|
|
65
|
+
// above this on the root tool guide); the collection mechanics live on the
|
|
66
|
+
// spawn leaf's `follow_up`. Keeping each fact in one place avoids restating the
|
|
67
|
+
// same delegation pitch three times in the always-loaded root. Travels with the
|
|
68
|
+
// catalog to every surface it renders on (root, `agent -h`, `agent subagent -h`).
|
|
69
|
+
const SUBAGENT_USAGE = 'One spawn command — `agent new` — runs any of these. `--agent <id>` overlays a defined persona (its system prompt, model, and tools); omit it (or pass `--agent general`) for the general-purpose worker. Same command either way. Pick the most specific agent that fits the task, else general. A defined agent earns its keep when a task recurs with a stable shape (a scout, a reviewer); for a fast throwaway pass, general on a cheap model is usually enough.';
|
|
70
|
+
function descLine(a) {
|
|
71
|
+
const meta = [];
|
|
72
|
+
if (a.frontmatter.model !== undefined && a.frontmatter.model !== '')
|
|
73
|
+
meta.push(`model: ${a.frontmatter.model}`);
|
|
74
|
+
if (a.frontmatter.tools !== undefined && a.frontmatter.tools.length > 0)
|
|
75
|
+
meta.push(`tools: ${a.frontmatter.tools.join(',')}`);
|
|
76
|
+
const annot = meta.length > 0 ? ` (${meta.join('; ')})` : '';
|
|
77
|
+
const desc = (a.frontmatter.description !== undefined ? a.frontmatter.description : '').replace(/\s+/g, ' ').trim();
|
|
78
|
+
return `- ${subagentId(a)}${annot}: ${desc}`;
|
|
79
|
+
}
|
|
80
|
+
/** The defined-subagents catalog as a self-named `<subagents count="N">`
|
|
81
|
+
* element: a usage preamble plus one full-description line per agent (the
|
|
82
|
+
* discriminator for picking which to delegate to). Project agents list first.
|
|
83
|
+
* Soft-fails to null when discovery throws or nothing is defined, so the block
|
|
84
|
+
* is simply omitted on a cold path. */
|
|
85
|
+
function buildSubagentCatalog() {
|
|
86
|
+
let agents;
|
|
87
|
+
try {
|
|
88
|
+
agents = listSubagents();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
agents = [];
|
|
92
|
+
}
|
|
93
|
+
// Project agents (repo-specific) before user/builtin, then alphabetical.
|
|
94
|
+
const ordered = [...agents].sort((a, b) => {
|
|
95
|
+
const sa = a.scope === 'project' ? 0 : 1;
|
|
96
|
+
const sb = b.scope === 'project' ? 0 : 1;
|
|
97
|
+
return sa !== sb ? sa - sb : subagentId(a).localeCompare(subagentId(b));
|
|
98
|
+
});
|
|
99
|
+
const lines = [
|
|
100
|
+
SUBAGENT_USAGE,
|
|
101
|
+
'',
|
|
102
|
+
'Agents (select with `agent new --agent <id>`):',
|
|
103
|
+
`- ${GENERAL_AGENT} (default): general-purpose agent for any self-contained task — research, code search, multi-step work. Used when --agent is omitted or \`--agent ${GENERAL_AGENT}\`.`,
|
|
104
|
+
];
|
|
105
|
+
for (const a of ordered)
|
|
106
|
+
lines.push(descLine(a));
|
|
107
|
+
// count includes the built-in general agent alongside the defined ones.
|
|
108
|
+
return stateBlock('subagents', { count: agents.length + 1 }, lines.join('\n'));
|
|
109
|
+
}
|
|
110
|
+
// Decision guidance surfaced on `agent new -h`. Answers "when do I reach
|
|
111
|
+
// for this, how often, and which mode?" — the questions the flag constraints
|
|
112
|
+
// alone don't.
|
|
113
|
+
const PROMPT_GUIDE = `## How to invoke
|
|
114
|
+
|
|
115
|
+
The task goes on **stdin**, not as an argument; the agent id and flags are argv.
|
|
116
|
+
Three shapes cover almost everything:
|
|
117
|
+
|
|
118
|
+
# Blocking (default): waits and returns the result inline, like a function call
|
|
119
|
+
echo "find where request auth is handled" | crtr agent new
|
|
120
|
+
|
|
121
|
+
# With a persona: --agent overlays a defined subagent (see \`crtr agent subagent list\`)
|
|
122
|
+
echo "map the daemon lifecycle" | crtr agent new --agent explore
|
|
123
|
+
|
|
124
|
+
# Background fan-out: each spawn returns a job_id immediately; collect when ready
|
|
125
|
+
echo "task A" | crtr agent new --background # -> { "job_id": "..." }
|
|
126
|
+
echo "task B" | crtr agent new --background
|
|
127
|
+
crtr job read result <job_id> --wait # blocks until that worker finishes
|
|
128
|
+
|
|
129
|
+
Heredocs work for multi-line prompts: \`crtr agent new <<'EOF' ... EOF\`.
|
|
130
|
+
|
|
131
|
+
## When to delegate
|
|
132
|
+
|
|
133
|
+
Reach for this constantly — it is the main way to get work done without
|
|
134
|
+
burning your own context. A spawned agent runs in a fresh context window and
|
|
135
|
+
hands back only its conclusion, so your own thread stays lean. Delegate any
|
|
136
|
+
self-contained task you can describe in a prompt: exploring a codebase,
|
|
137
|
+
researching how something works, implementing a change, writing tests,
|
|
138
|
+
refactoring, reproducing a bug, drafting docs. When in doubt, delegate rather
|
|
139
|
+
than do it inline — especially for anything multi-step, file-heavy, or
|
|
140
|
+
parallelizable. Keep doing trivial, single-shot lookups yourself.
|
|
141
|
+
|
|
142
|
+
Two habits worth building. **Scout before you build:** before you start
|
|
143
|
+
working in unfamiliar code, spawn a quick recon agent to map it first — a fast,
|
|
144
|
+
cheap model (e.g. haiku) is plenty for "where does X live / how does Y work,"
|
|
145
|
+
and it keeps the exploration out of your own context. **Fan out independents:**
|
|
146
|
+
when a job splits into tasks that don't depend on each other, launch one
|
|
147
|
+
--background worker per task and let them run concurrently rather than doing
|
|
148
|
+
them in series.
|
|
149
|
+
|
|
150
|
+
This is the single spawn command: it runs the general-purpose agent by default,
|
|
151
|
+
and \`--agent <id>\` swaps in a defined persona instead (see below). Same path
|
|
152
|
+
either way — there is no separate command for custom agents.
|
|
153
|
+
|
|
154
|
+
One worker per independent task. For a big job, fan out several small,
|
|
155
|
+
well-scoped workers instead of one mega-prompt — they run concurrently and each
|
|
156
|
+
stays focused.
|
|
157
|
+
|
|
158
|
+
## Which mode
|
|
159
|
+
|
|
160
|
+
**Default (blocking)** — use for almost everything. It spawns the worker, waits,
|
|
161
|
+
and returns the result inline, like a function call. Pick this whenever you need
|
|
162
|
+
the answer before you can continue. Inside tmux the worker runs as an interactive
|
|
163
|
+
agent in a pane of the dedicated subagent session WITHOUT stealing your focus —
|
|
164
|
+
jump over to watch or steer it with Alt-o, or just wait for the inline result.
|
|
165
|
+
Outside tmux it runs as a print-mode child process. Either way the result comes
|
|
166
|
+
back inline.
|
|
167
|
+
|
|
168
|
+
**--background** — use when you do NOT need the result right now:
|
|
169
|
+
- Fan out: launch several independent workers, keep working, then collect
|
|
170
|
+
each at \`crtr job read result <id> --wait\` once you need them.
|
|
171
|
+
- Fire-and-continue: kick off a long task and proceed with other work.
|
|
172
|
+
You own the collection — a backgrounded worker is yours to retrieve and report,
|
|
173
|
+
not the user's.
|
|
174
|
+
|
|
175
|
+
**--headed** — legacy/no-op. Workers are always headed (interactive in a pane)
|
|
176
|
+
inside tmux now; this flag is kept so older invocations don't break but changes
|
|
177
|
+
nothing. To watch or steer a worker, spawn normally and press Alt-o.
|
|
178
|
+
|
|
179
|
+
## Choosing the agent
|
|
180
|
+
|
|
181
|
+
**--agent <id>** selects which agent runs the task. Omit it (or pass
|
|
182
|
+
\`--agent general\`) for the general-purpose agent. Pass a defined id to overlay
|
|
183
|
+
that subagent: a reusable persona defined in markdown with frontmatter (see
|
|
184
|
+
\`crtr agent subagent\`), whose body becomes the worker's appended system prompt
|
|
185
|
+
and whose declared model / (pi) tools are applied for the run. Everything else
|
|
186
|
+
(modes, output) is identical. Reach for a defined agent when a recurring task
|
|
187
|
+
has a stable persona (a scout, a reviewer); use general for one-off delegation.`;
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// agent new — the single spawn command. General-purpose by default;
|
|
190
|
+
// `--agent <id>` overlays a defined subagent persona.
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
const newPrompt = defineLeaf({
|
|
193
|
+
name: 'new',
|
|
194
|
+
help: {
|
|
195
|
+
name: 'agent new',
|
|
196
|
+
summary: 'spawn a worker (matches the host CLI: claude or pi) — the general-purpose agent by default, or a defined subagent via --agent. Blocking by default, returning the result inline; inside tmux the worker runs in an unfocused pane you can watch with Alt-o',
|
|
197
|
+
guide: PROMPT_GUIDE,
|
|
198
|
+
params: [
|
|
199
|
+
{ kind: 'stdin', name: 'prompt', required: true, constraint: 'Task/prompt sent to the spawned agent as the first user message.' },
|
|
200
|
+
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Working directory for the spawned agent. Defaults to process.cwd().' },
|
|
201
|
+
{ kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Display name passed to the agent CLI (`-n`); surfaces in pane title and resume picker. Defaults to the --agent id (or "general").' },
|
|
202
|
+
{ kind: 'flag', name: 'agent', type: 'string', required: false, default: 'general', constraint: 'Which agent runs the task. Omit or "general" for the general-purpose agent; any other id (<name> or <plugin>/<name>) overlays that defined subagent\'s persona/model/tools. List with `crtr agent subagent list`.' },
|
|
203
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'With --agent, narrows resolution when the subagent name is ambiguous across scopes.' },
|
|
204
|
+
{ kind: 'flag', name: 'model', type: 'string', required: false, constraint: 'Model pattern/id passed via `--model`. Overrides a subagent\'s declared model.' },
|
|
205
|
+
{ kind: 'flag', name: 'headed', type: 'bool', required: false, constraint: 'Legacy/no-op. Workers are always interactive panes in the dedicated subagent session (inside tmux); this flag is accepted for back-compat but changes nothing. Use Alt-o to watch/steer.' },
|
|
206
|
+
{ kind: 'flag', name: 'background', type: 'bool', required: false, constraint: 'Return a job handle immediately instead of blocking for the result. Collect later with `crtr job read result`.' },
|
|
207
|
+
],
|
|
208
|
+
output: [
|
|
209
|
+
{ name: 'job_id', type: 'string', required: true, constraint: 'Use with `crtr job read status|logs|result` and `crtr job cancel`.' },
|
|
210
|
+
{ name: 'agent', type: 'string', required: true, constraint: 'The agent that ran the task: "general" or a resolved subagent id.' },
|
|
211
|
+
{ name: 'status', type: 'string', required: false, constraint: 'Blocking runs only: done | failed | closed | timeout.' },
|
|
212
|
+
{ name: 'result_md', type: 'string', required: false, constraint: 'Blocking runs: the agent\'s output (a submitted markdown result in tmux, or print-mode stdout outside tmux).' },
|
|
213
|
+
{ name: 'result', type: 'object', required: false, constraint: 'Blocking runs: a structured result, when one was submitted programmatically.' },
|
|
214
|
+
{ name: 'reason', type: 'string', required: false, constraint: 'Blocking runs: explanation when status is failed or closed.' },
|
|
215
|
+
{ name: 'follow_up', type: 'string', required: false, constraint: 'Background runs only: your own next call — run it and report the worker\'s result; do not relay it to the user.' },
|
|
216
|
+
],
|
|
217
|
+
outputKind: 'object',
|
|
218
|
+
effects: [
|
|
219
|
+
'Default (blocking): inside tmux, runs an interactive agent in a pane of the dedicated subagent session (no focus change) that delivers its result via `crtr job submit`; outside tmux, runs a print-mode child process whose stdout is the result. Set CRTR_SUBAGENT_TMUX=off to force the child-process path. Either way the result is returned inline.',
|
|
220
|
+
'All spawns land in a per-host-session tmux session (crtr-agents-<pane>); Alt-o toggles between it and the originating pane.',
|
|
221
|
+
'--headed is legacy/no-op (workers are always headed inside tmux).',
|
|
222
|
+
'--background: returns a job handle immediately; the worker runs async and its result is collected via `crtr job`.',
|
|
223
|
+
'Always creates a job entry at $XDG_STATE_HOME/crtr/jobs/<job_id>/ and records the result there.',
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
run: async (input) => {
|
|
227
|
+
const prompt = input['prompt'];
|
|
228
|
+
const cwd = typeof input['cwd'] === 'string' ? input['cwd'] : process.cwd();
|
|
229
|
+
const background = input['background'] === true;
|
|
230
|
+
// Optional subagent overlay: --agent loads a persona (markdown + frontmatter)
|
|
231
|
+
// that supplies the appended system prompt, model, and (pi) tools. Without
|
|
232
|
+
// it, this is a plain general-purpose spawn.
|
|
233
|
+
const agentArg = typeof input['agent'] === 'string' && input['agent'] !== '' ? input['agent'] : undefined;
|
|
234
|
+
const scopeStr = input['scope'];
|
|
235
|
+
// `general` (or no --agent) is the built-in general-purpose worker: no
|
|
236
|
+
// persona overlay. Any other id resolves a defined subagent.
|
|
237
|
+
let sub;
|
|
238
|
+
if (agentArg !== undefined && agentArg !== GENERAL_AGENT) {
|
|
239
|
+
const resolveOpts = {};
|
|
240
|
+
if (scopeStr !== undefined) {
|
|
241
|
+
const resolved = resolveScopeArg(scopeStr);
|
|
242
|
+
if (resolved !== 'all')
|
|
243
|
+
resolveOpts.scope = resolved;
|
|
244
|
+
}
|
|
245
|
+
sub = resolveSubagent(agentArg, resolveOpts);
|
|
246
|
+
}
|
|
247
|
+
const agentId = sub !== undefined ? subagentId(sub) : GENERAL_AGENT;
|
|
248
|
+
const nameArg = typeof input['name'] === 'string' && input['name'] !== '' ? input['name'] : undefined;
|
|
249
|
+
const name = nameArg ?? agentId;
|
|
250
|
+
const systemPrompt = sub?.systemPrompt;
|
|
251
|
+
const tools = sub?.frontmatter.tools;
|
|
252
|
+
const model = (typeof input['model'] === 'string' && input['model'] !== '' ? input['model'] : undefined)
|
|
253
|
+
?? sub?.frontmatter.model;
|
|
254
|
+
const subOut = { agent: agentId };
|
|
255
|
+
const { jobId } = createJob(sub !== undefined ? 'subagent' : 'general', { cwd });
|
|
256
|
+
// Inside tmux (unless opted out with CRTR_SUBAGENT_TMUX=off), EVERY worker
|
|
257
|
+
// runs as an interactive agent in a pane of the dedicated subagent session.
|
|
258
|
+
// We never steal focus — `--headed` is retained only for back-compat and no
|
|
259
|
+
// longer changes behavior; jump to the session yourself with Alt-o. The pane
|
|
260
|
+
// agent delivers its result via `crtr job submit` (the instruction is
|
|
261
|
+
// appended to the prompt). Outside tmux there is no pane to host an
|
|
262
|
+
// interactive agent, so we fall back to a print-mode child process whose
|
|
263
|
+
// stdout is the result.
|
|
264
|
+
const useTmux = isInTmux() && process.env.CRTR_SUBAGENT_TMUX !== 'off';
|
|
265
|
+
if (useTmux) {
|
|
266
|
+
const result = spawnAgent({
|
|
267
|
+
prompt: agentNewPrompt(prompt, jobId),
|
|
268
|
+
cwd,
|
|
269
|
+
jobId,
|
|
270
|
+
maxPanesPerWindow: resolveMaxPanes(),
|
|
271
|
+
name,
|
|
272
|
+
systemPrompt,
|
|
273
|
+
model,
|
|
274
|
+
tools,
|
|
275
|
+
});
|
|
276
|
+
if (result.status === 'spawned') {
|
|
277
|
+
if (result.paneId !== undefined)
|
|
278
|
+
recordJobPane(jobId, result.paneId);
|
|
279
|
+
const paneLabel = result.paneId !== undefined ? result.paneId : 'unknown';
|
|
280
|
+
appendEvent(jobId, { level: 'info', event: 'worker_started', message: `pane ${paneLabel} spawned (unfocused)` });
|
|
281
|
+
if (background) {
|
|
282
|
+
return { job_id: jobId, ...subOut, follow_up: followUpResult(jobId) };
|
|
283
|
+
}
|
|
284
|
+
const r = await readJobResult(jobId, { waitMs: WAIT_BUDGET_MS });
|
|
285
|
+
const out = { job_id: jobId, ...subOut, status: r.status };
|
|
286
|
+
if (r.result_md !== undefined)
|
|
287
|
+
out['result_md'] = r.result_md;
|
|
288
|
+
if (r.result !== undefined)
|
|
289
|
+
out['result'] = r.result;
|
|
290
|
+
if (r.reason !== undefined)
|
|
291
|
+
out['reason'] = r.reason;
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
// tmux placement failed → fall through to the child-process path below.
|
|
295
|
+
appendEvent(jobId, { level: 'info', event: 'tmux_spawn_failed', message: `${result.message}; falling back to a print-mode child process` });
|
|
296
|
+
}
|
|
297
|
+
// ---- No tmux: print-mode child process (stdout is the result) ----
|
|
298
|
+
if (background) {
|
|
299
|
+
const res = spawnHeadlessDetached({ prompt, name, cwd, jobId, systemPrompt, model, tools });
|
|
300
|
+
if (res.status === 'spawn-failed') {
|
|
301
|
+
throw new InputError({ error: 'spawn_failed', message: res.message, next: 'Check the agent CLI is installed and on PATH.' });
|
|
302
|
+
}
|
|
303
|
+
if (res.pid !== undefined)
|
|
304
|
+
recordJobPid(jobId, res.pid);
|
|
305
|
+
appendEvent(jobId, { level: 'info', event: 'worker_started', message: res.message });
|
|
306
|
+
return { job_id: jobId, ...subOut, follow_up: followUpResult(jobId) };
|
|
307
|
+
}
|
|
308
|
+
appendEvent(jobId, { level: 'info', event: 'worker_started', message: 'agent started (blocking, no tmux)' });
|
|
309
|
+
const r = await runAgentHeadless({ prompt, name, cwd, systemPrompt, model, tools });
|
|
310
|
+
if (r.status === 'done') {
|
|
311
|
+
writeMarkdownResult(jobId, r.output, 'done');
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
writeMarkdownResult(jobId, r.output, 'failed', `agent exited with code ${r.exitCode ?? 'null'}`);
|
|
315
|
+
}
|
|
316
|
+
appendEvent(jobId, {
|
|
317
|
+
level: r.status === 'done' ? 'info' : 'error',
|
|
318
|
+
event: 'worker_finished',
|
|
319
|
+
message: `agent ${r.status}`,
|
|
320
|
+
});
|
|
321
|
+
return { job_id: jobId, ...subOut, status: r.status, result_md: r.output };
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// agent fork
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
const newFork = defineLeaf({
|
|
328
|
+
name: 'fork',
|
|
329
|
+
help: {
|
|
330
|
+
name: 'agent fork',
|
|
331
|
+
summary: 'fork the current agent session into a sibling pane; returns a job handle immediately',
|
|
332
|
+
params: [
|
|
333
|
+
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Working directory. Defaults to process.cwd().' },
|
|
334
|
+
{ kind: 'flag', name: 'name', type: 'string', required: true, constraint: 'Display name passed to the agent CLI (`-n`); surfaces in pane title and resume picker.' },
|
|
335
|
+
],
|
|
336
|
+
output: [
|
|
337
|
+
{ name: 'job_id', type: 'string', required: true, constraint: 'Use with `crtr job read *` and `crtr job cancel`.' },
|
|
338
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Your own next call — run it and report the worker\'s result; do not relay it to the user.' },
|
|
339
|
+
],
|
|
340
|
+
outputKind: 'object',
|
|
341
|
+
effects: [
|
|
342
|
+
'Claude Code only: requires $CLAUDE_CODE_SESSION_ID. pi does not expose its session id to subprocesses, so fork is unavailable under pi — use `agent new` instead.',
|
|
343
|
+
'Spawns a forked agent session in a sibling tmux pane.',
|
|
344
|
+
'Creates a job entry and result sidecar as with `agent new`.',
|
|
345
|
+
],
|
|
346
|
+
},
|
|
347
|
+
run: async (input) => {
|
|
348
|
+
assertTmux();
|
|
349
|
+
const agentKind = detectAgentKind();
|
|
350
|
+
if (agentKind === 'pi') {
|
|
351
|
+
throw new InputError({
|
|
352
|
+
error: 'fork_unsupported',
|
|
353
|
+
message: 'crtr agent fork is not supported under pi: pi does not expose the active session id to subprocesses.',
|
|
354
|
+
next: 'Use `crtr agent new` to spawn a fresh pi agent instead.',
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
const parentSessionId = process.env['CLAUDE_CODE_SESSION_ID'];
|
|
358
|
+
if (parentSessionId === undefined || parentSessionId === '') {
|
|
359
|
+
throw new InputError({
|
|
360
|
+
error: 'missing_session_id',
|
|
361
|
+
message: 'crtr agent fork requires $CLAUDE_CODE_SESSION_ID — must run inside Claude Code.',
|
|
362
|
+
next: 'Run this command from within a Claude Code session.',
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
const cwd = typeof input['cwd'] === 'string' ? input['cwd'] : process.cwd();
|
|
366
|
+
const name = input['name'];
|
|
367
|
+
const { jobId } = createJob('fork', { cwd });
|
|
368
|
+
const result = spawnAgent({
|
|
369
|
+
prompt: `Fork of session ${parentSessionId}`,
|
|
370
|
+
cwd,
|
|
371
|
+
jobId,
|
|
372
|
+
fork: { sessionId: parentSessionId },
|
|
373
|
+
maxPanesPerWindow: resolveMaxPanes(),
|
|
374
|
+
name,
|
|
375
|
+
});
|
|
376
|
+
if (result.status === 'not-in-tmux') {
|
|
377
|
+
throw new InputError({ error: 'not_in_tmux', message: result.message, next: 'Run inside a tmux session.' });
|
|
378
|
+
}
|
|
379
|
+
if (result.status === 'spawn-failed') {
|
|
380
|
+
throw new InputError({ error: 'spawn_failed', message: result.message, next: 'Check tmux is running and try again.' });
|
|
381
|
+
}
|
|
382
|
+
if (result.paneId !== undefined)
|
|
383
|
+
recordJobPane(jobId, result.paneId);
|
|
384
|
+
const forkPaneLabel = result.paneId !== undefined ? result.paneId : 'unknown';
|
|
385
|
+
appendEvent(jobId, { level: 'info', event: 'worker_started', message: `forked pane ${forkPaneLabel} spawned` });
|
|
386
|
+
return { job_id: jobId, follow_up: followUpResult(jobId) };
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// agent subagent (management branch: list / read / scaffold)
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
const subagentList = defineLeaf({
|
|
393
|
+
name: 'list',
|
|
394
|
+
help: {
|
|
395
|
+
name: 'agent subagent list',
|
|
396
|
+
summary: 'list defined subagents (markdown + frontmatter) discoverable from scope roots and plugins',
|
|
397
|
+
params: [
|
|
398
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project', 'all'], required: false, constraint: 'Default: all.' },
|
|
399
|
+
{ kind: 'flag', name: 'full', type: 'bool', required: false, constraint: 'When present, includes each subagent\'s model and tools.' },
|
|
400
|
+
],
|
|
401
|
+
output: [
|
|
402
|
+
{ name: 'items', type: 'object[]', required: true, constraint: 'Each: {id, name, plugin, scope, description}. With --full also {model, tools}. Sorted by name.' },
|
|
403
|
+
{ name: 'total', type: 'integer', required: true, constraint: 'Number of subagents returned.' },
|
|
404
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Next commands for reading or spawning a subagent.' },
|
|
405
|
+
],
|
|
406
|
+
outputKind: 'object',
|
|
407
|
+
effects: ['None. Read-only.'],
|
|
408
|
+
},
|
|
409
|
+
run: async (input) => {
|
|
410
|
+
const scopeStr = input['scope'];
|
|
411
|
+
const full = input['full'] === true;
|
|
412
|
+
let scopeFilter;
|
|
413
|
+
if (scopeStr !== undefined) {
|
|
414
|
+
const resolved = resolveScopeArg(scopeStr);
|
|
415
|
+
if (resolved !== 'all')
|
|
416
|
+
scopeFilter = resolved;
|
|
417
|
+
}
|
|
418
|
+
const agents = listSubagents(scopeFilter);
|
|
419
|
+
return {
|
|
420
|
+
items: agents.map((a) => {
|
|
421
|
+
const base = {
|
|
422
|
+
id: subagentId(a),
|
|
423
|
+
name: a.name,
|
|
424
|
+
plugin: a.plugin,
|
|
425
|
+
scope: a.scope,
|
|
426
|
+
description: a.frontmatter.description !== undefined ? a.frontmatter.description : null,
|
|
427
|
+
};
|
|
428
|
+
if (full) {
|
|
429
|
+
base['model'] = a.frontmatter.model !== undefined ? a.frontmatter.model : null;
|
|
430
|
+
base['tools'] = a.frontmatter.tools !== undefined ? a.frontmatter.tools : null;
|
|
431
|
+
}
|
|
432
|
+
return base;
|
|
433
|
+
}),
|
|
434
|
+
total: agents.length,
|
|
435
|
+
follow_up: 'Read one with `crtr agent subagent read <name>`; delegate with `crtr agent new --agent <name>`.',
|
|
436
|
+
};
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
const subagentRead = defineLeaf({
|
|
440
|
+
name: 'read',
|
|
441
|
+
help: {
|
|
442
|
+
name: 'agent subagent read',
|
|
443
|
+
summary: 'load a subagent\'s system prompt (markdown body) and metadata',
|
|
444
|
+
params: [
|
|
445
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Subagent identifier: <name> or <plugin>/<name>.' },
|
|
446
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Narrows resolution when the name is ambiguous.' },
|
|
447
|
+
{ kind: 'flag', name: 'no-body', type: 'bool', required: false, constraint: 'When present, omits the system prompt body — returns metadata only.' },
|
|
448
|
+
],
|
|
449
|
+
output: [
|
|
450
|
+
{ name: 'id', type: 'string', required: true, constraint: 'Resolved subagent id.' },
|
|
451
|
+
{ name: 'name', type: 'string', required: true, constraint: 'Subagent name.' },
|
|
452
|
+
{ name: 'plugin', type: 'string', required: true, constraint: 'Plugin the subagent belongs to, or _ for a scope-root agent.' },
|
|
453
|
+
{ name: 'scope', type: 'string', required: true, constraint: 'Scope it resolved from.' },
|
|
454
|
+
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the .md file.' },
|
|
455
|
+
{ name: 'description', type: 'string', required: true, constraint: 'Frontmatter description.' },
|
|
456
|
+
{ name: 'model', type: 'string | null', required: true, constraint: 'Declared model, or null.' },
|
|
457
|
+
{ name: 'tools', type: 'string[] | null', required: true, constraint: 'Declared tool allow-list, or null.' },
|
|
458
|
+
{ name: 'system_prompt', type: 'string', required: false, constraint: 'Markdown body applied as the appended system prompt. Omitted with --no-body.' },
|
|
459
|
+
],
|
|
460
|
+
outputKind: 'object',
|
|
461
|
+
effects: ['None. Read-only.'],
|
|
462
|
+
},
|
|
463
|
+
run: async (input) => {
|
|
464
|
+
const nameRaw = input['name'];
|
|
465
|
+
const scopeStr = input['scope'];
|
|
466
|
+
const noBody = input['noBody'] === true;
|
|
467
|
+
const resolveOpts = {};
|
|
468
|
+
if (scopeStr !== undefined) {
|
|
469
|
+
const resolved = resolveScopeArg(scopeStr);
|
|
470
|
+
if (resolved !== 'all')
|
|
471
|
+
resolveOpts.scope = resolved;
|
|
472
|
+
}
|
|
473
|
+
const sub = resolveSubagent(nameRaw, resolveOpts);
|
|
474
|
+
const out = {
|
|
475
|
+
id: subagentId(sub),
|
|
476
|
+
name: sub.name,
|
|
477
|
+
plugin: sub.plugin,
|
|
478
|
+
scope: sub.scope,
|
|
479
|
+
path: sub.path,
|
|
480
|
+
description: sub.frontmatter.description !== undefined ? sub.frontmatter.description : '',
|
|
481
|
+
model: sub.frontmatter.model !== undefined ? sub.frontmatter.model : null,
|
|
482
|
+
tools: sub.frontmatter.tools !== undefined ? sub.frontmatter.tools : null,
|
|
483
|
+
};
|
|
484
|
+
if (!noBody)
|
|
485
|
+
out['system_prompt'] = sub.systemPrompt;
|
|
486
|
+
return out;
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
const SUBAGENT_STUB = (name, description) => `---\nname: ${name}\ndescription: ${description}\n# model: claude-sonnet-4-5 # optional: model pattern/id passed via --model\n# tools: read, grep, find, ls, bash # optional (pi): tool allow-list passed via --tools\n---\n\nYou are ${name}. Describe the persona, responsibilities, and output format here.\nThis markdown body is applied as the spawned agent's appended system prompt.\n`;
|
|
490
|
+
const subagentScaffold = defineLeaf({
|
|
491
|
+
name: 'scaffold',
|
|
492
|
+
help: {
|
|
493
|
+
name: 'agent subagent scaffold',
|
|
494
|
+
summary: 'create a subagent definition stub (markdown + frontmatter) under <scope>/agents',
|
|
495
|
+
params: [
|
|
496
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Subagent name; also the filename stem (<name>.md).' },
|
|
497
|
+
{ kind: 'flag', name: 'description', type: 'string', required: false, constraint: 'Short description written to frontmatter. Required for the subagent to appear in listings.' },
|
|
498
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
|
|
499
|
+
],
|
|
500
|
+
output: [
|
|
501
|
+
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the scaffolded .md file.' },
|
|
502
|
+
{ name: 'id', type: 'string', required: true, constraint: 'Resolved subagent id.' },
|
|
503
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Next step to edit and use the subagent.' },
|
|
504
|
+
],
|
|
505
|
+
outputKind: 'object',
|
|
506
|
+
effects: [
|
|
507
|
+
'Creates `<scope-root>/agents/<name>.md` with a frontmatter + body stub.',
|
|
508
|
+
'Fails if the file already exists.',
|
|
509
|
+
],
|
|
510
|
+
},
|
|
511
|
+
run: async (input) => {
|
|
512
|
+
const name = input['name'].trim();
|
|
513
|
+
if (name === '' || name.includes('/')) {
|
|
514
|
+
throw usage('subagent name must be a non-empty single segment (no slashes)');
|
|
515
|
+
}
|
|
516
|
+
const description = typeof input['description'] === 'string' ? input['description'] : '';
|
|
517
|
+
const scopeStr = input['scope'];
|
|
518
|
+
let scope;
|
|
519
|
+
if (scopeStr !== undefined) {
|
|
520
|
+
const resolved = resolveScopeArg(scopeStr);
|
|
521
|
+
if (resolved === 'all')
|
|
522
|
+
throw usage('scope must be user or project, not all');
|
|
523
|
+
scope = resolved;
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
scope = projectScopeRoot() !== null ? 'project' : 'user';
|
|
527
|
+
}
|
|
528
|
+
const scopeRootPath = requireScopeRoot(scope);
|
|
529
|
+
ensureScopeInitialized(scope, scopeRootPath);
|
|
530
|
+
const dir = scopeAgentsDir(scope);
|
|
531
|
+
if (dir === null)
|
|
532
|
+
throw general(`no agents dir for scope ${scope}`);
|
|
533
|
+
const filePath = join(dir, `${name}.md`);
|
|
534
|
+
if (pathExists(filePath))
|
|
535
|
+
throw general(`subagent already exists: ${filePath}`);
|
|
536
|
+
ensureDir(dir);
|
|
537
|
+
writeFileSync(filePath, SUBAGENT_STUB(name, description), 'utf8');
|
|
538
|
+
return {
|
|
539
|
+
path: filePath,
|
|
540
|
+
id: name,
|
|
541
|
+
follow_up: `Edit ${filePath}, then delegate with \`crtr agent new --agent ${name}\`.`,
|
|
542
|
+
};
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
const subagentBranch = defineBranch({
|
|
546
|
+
name: 'subagent',
|
|
547
|
+
help: {
|
|
548
|
+
name: 'agent subagent',
|
|
549
|
+
summary: 'define and inspect reusable subagent personas (markdown + frontmatter)',
|
|
550
|
+
model: 'A subagent is a markdown file with YAML frontmatter (name, description, optional model/tools) whose body becomes a spawned worker\'s appended system prompt — the same model as the pi subagent extension, surfaced through crtr. Files live under `<scope-root>/agents/*.md` (and plugins\' `agents/`). `list` enumerates them, `read` loads one\'s body + metadata, `scaffold` creates a stub. Spawn one with `crtr agent new --agent <name>`.',
|
|
551
|
+
dynamicState: buildSubagentCatalog,
|
|
552
|
+
children: [
|
|
553
|
+
{ name: 'list', desc: 'list defined subagents', useWhen: 'discovering which personas are available' },
|
|
554
|
+
{ name: 'read', desc: 'load a subagent\'s system prompt + metadata', useWhen: 'inspecting a persona before using or editing it' },
|
|
555
|
+
{ name: 'scaffold', desc: 'create a subagent stub under <scope>/agents', useWhen: 'defining a new subagent' },
|
|
556
|
+
],
|
|
557
|
+
},
|
|
558
|
+
children: [subagentList, subagentRead, subagentScaffold],
|
|
559
|
+
});
|
|
560
|
+
// ---------------------------------------------------------------------------
|
|
561
|
+
// agent (root umbrella)
|
|
562
|
+
// ---------------------------------------------------------------------------
|
|
563
|
+
export function registerAgent() {
|
|
564
|
+
return defineBranch({
|
|
565
|
+
name: 'agent',
|
|
566
|
+
rootEntry: {
|
|
567
|
+
concept: 'workers you spawn to offload work to a fresh context. `agent new` runs an agent on any task and hands back just the conclusion; results collected by job handle',
|
|
568
|
+
desc: 'spawn agent workers and manage subagent personas',
|
|
569
|
+
useWhen: 'almost any self-contained task — reach for `agent new` OFTEN, and earlier than feels necessary, to keep work off your own context. Two habits pay off most: (1) before working in unfamiliar code, send a quick recon agent to map it first — a fast, cheap model (e.g. haiku) handles "where does X live / how does Y work" fine and keeps the digging out of your context; (2) when subtasks are independent, fan them out as parallel workers instead of doing them in series. Each worker runs in a fresh context and hands back only its conclusion. Blocking by default (returns inline like a function call); inside tmux the worker runs in an unfocused pane of the dedicated subagent session — press Alt-o to watch or steer it. --background fans out without waiting. Select a persona with `--agent <id>` (see `agent subagent`). Only trivial one-shot lookups are worth doing inline.',
|
|
570
|
+
dynamicState: buildSubagentCatalog,
|
|
571
|
+
},
|
|
572
|
+
help: {
|
|
573
|
+
name: 'agent',
|
|
574
|
+
summary: 'spawn agent workers and manage subagent personas',
|
|
575
|
+
model: '`agent new` spawns a worker (general-purpose by default, or a defined persona via `--agent <id>`); `agent fork` carries the current session context to a new pane; `agent subagent` manages persona definitions. Spawned workers register as jobs — monitor and collect at `crtr job`. Spec, plan, and debug workflows live under `crtr mode`.',
|
|
576
|
+
dynamicState: buildSubagentCatalog,
|
|
577
|
+
children: [
|
|
578
|
+
{ name: 'new', desc: 'spawn a worker — general-purpose by default, or a defined subagent via --agent', useWhen: 'delegating any self-contained task (the main spawn command)' },
|
|
579
|
+
{ name: 'fork', desc: 'fork current session into a sibling pane', useWhen: 'branching the current session\'s context into a new agent' },
|
|
580
|
+
{ name: 'subagent', desc: 'define and inspect reusable subagent personas', useWhen: 'managing markdown subagent definitions or seeing what personas exist' },
|
|
581
|
+
],
|
|
582
|
+
},
|
|
583
|
+
children: [newPrompt, newFork, subagentBranch],
|
|
584
|
+
});
|
|
585
|
+
}
|
package/dist/commands/debug.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare const FLOW_DEBUG_GUIDE = "## Debug workflow \u2014 reproduce first\n\nAudience: the agent that ran `crtr
|
|
1
|
+
export declare const FLOW_DEBUG_GUIDE = "## Debug workflow \u2014 reproduce first\n\nAudience: the agent that ran `crtr mode debug`. A reproduction agent is\nalready spawned in a sibling pane. It writes ONE failing integration test and\nnever fixes anything. You do everything after: gate on the repro, root-cause,\nfix, verify against that same test.\n\n### Phase 0: Await the repro agent\n\nRun `crtr job read result <job_id> --wait` (10-min budget).\nOn status:\"timeout\": re-issue the wait, or run `crtr job read logs <job_id> --follow`\nuntil the job is terminal.\n\n### Phase 1: Gate on reproduction\n\n`reproduces:true`: read `test_path`, run `test_command` YOURSELF, confirm\nit fails for the stated reason. Do not trust the agent's claim \u2014 if it passes\nor fails differently, treat repro as NOT achieved. This test is the regression\ngate; it stays in the suite after the fix.\n`status:\"failed\"` / `reproduces:false` / your run disproves it: no repro\nharness. Continue, but record \"no reproduction \u2014 fix unverified; do not claim\nverified-fixed.\"\n\n### Phase 2: Reconnaissance\n\nRead the key files yourself \u2014 entry point, failure point, the data flow\nbetween. `git log` / `git blame` near the failure: recent changes are\nhigh-signal.\n\n### Phase 3: Assess difficulty, scale investigators\n\nSimple \u2192 solo (Explore subagents for tracing if the area is large).\nMedium \u2192 2\u20133 parallel `devcore:senior-advisor`: data-flow tracer, assumption\nauditor, change investigator.\nHard (intermittent, races, \"been stuck\", many modules) \u2192 3\u20135 parallel:\nend-to-end tracer, assumption breaker, git archaeologist, boundary inspector.\nGive investigators file paths, observed behavior, and concrete tasks \u2014 never\nyour hypotheses. Challenge theories against each other; the one that survives\ndisconfirmation wins.\n\n### Phase 4: Fix\n\nMinimal root-cause fix. No scope expansion, no drive-by refactor.\n\n### Phase 5: Verify\n\nRe-run `test_command`: it MUST now pass. Run the broader suite for\nregressions. If there was no repro test, state the fix is unverified by\nreproduction and recommend explicit manual verification.\n\n### Phase 6: Report\n\nRoot cause (exact line + why), evidence, the now-passing repro test path,\nconfidence (High/Medium/Low; if not High, name what is uncertain).\n\n### Constraints\n\nThe repro test is the regression guard \u2014 it stays; a fix-agent must never\nweaken it. Investigators run in forked contexts; they return summaries, not\nraw output. No code changes during Phases 2\u20133 except the repro test.";
|
|
2
2
|
import type { LeafDef } from '../core/command.js';
|
|
3
3
|
export declare function registerDebug(): LeafDef;
|