@monoes/monomindcli 1.6.0 → 1.6.3

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.
@@ -0,0 +1,340 @@
1
+ ---
2
+ name: monomind-createtask
3
+ description: "Monomind — Ingest a prompt, file, or folder, deeply understand it, generate a full implementation plan, and create self-contained tasks on monotask that coder agents can pick up"
4
+ ---
5
+
6
+ If `$ARGUMENTS` is empty, output this and STOP:
7
+
8
+ > **Usage:** `/monomind:createtask <prompt | path-to-file | path-to-folder>`
9
+ >
10
+ > Examples:
11
+ > - `/monomind:createtask Build a webhook delivery system with retries and dead-letter queue`
12
+ > - `/monomind:createtask docs/superpowers/specs/2026-04-27-swarm-tab-redesign-design.md`
13
+ > - `/monomind:createtask docs/superpowers/specs/`
14
+ >
15
+ > This command deeply analyzes your input, generates a full implementation plan, and creates self-contained tasks on monotask that simple coder agents can execute without additional context.
16
+
17
+ Do NOT proceed further if no arguments were provided.
18
+
19
+ ---
20
+
21
+ ## Step 0: Check monotask CLI
22
+
23
+ Run:
24
+ ```bash
25
+ command -v monotask
26
+ ```
27
+
28
+ If `monotask` is NOT found, attempt to install:
29
+ ```bash
30
+ command -v cargo && cargo install monotask
31
+ ```
32
+
33
+ If `cargo` is also missing, output this and STOP:
34
+ > monotask requires Rust. Install Rust first:
35
+ > ```bash
36
+ > curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
37
+ > source "$HOME/.cargo/env"
38
+ > cargo install monotask
39
+ > ```
40
+
41
+ ---
42
+
43
+ ## Step 1: Classify and Ingest Input
44
+
45
+ Parse `$ARGUMENTS` to determine input type:
46
+
47
+ ### 1a: Detect input type
48
+
49
+ - If `$ARGUMENTS` is an existing **file path** (check with `test -f`): `INPUT_TYPE=file`
50
+ - If `$ARGUMENTS` is an existing **directory path** (check with `test -d`): `INPUT_TYPE=folder`
51
+ - Otherwise: `INPUT_TYPE=prompt`
52
+
53
+ ### 1b: Collect raw content
54
+
55
+ **If `INPUT_TYPE=prompt`:**
56
+ - Store the text as `RAW_CONTENT`.
57
+
58
+ **If `INPUT_TYPE=file`:**
59
+ - Read the file using the Read tool.
60
+ - Store the full contents as `RAW_CONTENT`.
61
+ - Store filename as `INPUT_LABEL`.
62
+
63
+ **If `INPUT_TYPE=folder`:**
64
+ - List all files in the directory (non-recursive, skip hidden files):
65
+ ```bash
66
+ find "$ARGUMENTS" -maxdepth 2 -type f ! -name '.*' | head -30
67
+ ```
68
+ - Read each file using the Read tool (up to 30 files, skip binary files).
69
+ - Concatenate all contents with `--- FILE: <path> ---` separators as `RAW_CONTENT`.
70
+ - Store folder path as `INPUT_LABEL`.
71
+
72
+ ### 1c: Enrich with knowledge systems
73
+
74
+ Run ALL of the following in parallel (skip any that error):
75
+
76
+ 1. **Knowledge graph — suggest**: Call `mcp__monobrain__graphify_suggest` with the first 200 chars of `RAW_CONTENT`.
77
+ 2. **Knowledge graph — query**: If any specific module/component names appear in `RAW_CONTENT`, call `mcp__monobrain__graphify_query` for each (up to 5 queries).
78
+ 3. **Memory search**: Call `mcp__monobrain__memory_search` with a summary of the input. Use top 5 results.
79
+ 4. **README**: Read `README.md` (first 200 lines). Skip if missing.
80
+ 5. **Package manifest**: Read whichever exists first: `package.json`, `Cargo.toml`, `pyproject.toml`, `go.mod`.
81
+ 6. **Repo name**: Run `git remote get-url origin`, extract last path segment, strip `.git`. Fallback: `basename` of cwd. Store as `REPO_NAME`.
82
+
83
+ Bundle everything into `FULL_CONTEXT` = `RAW_CONTENT` + graph results + memory results + README + manifest.
84
+
85
+ ---
86
+
87
+ ## Step 2: Setup Monotask Space and Task Board
88
+
89
+ ### Space
90
+ - Run `monotask space list` and check if a space named `$REPO_NAME` exists.
91
+ - If not, create it: `monotask space create "$REPO_NAME"`.
92
+ - Store `SPACE_ID`.
93
+
94
+ ### Task Board
95
+ - List boards via `monotask board list --json`. For each board ID, run `monotask column list <BOARD_ID> --json` to find one whose columns contain `Todo` (the `monomind-task` board).
96
+ - If the `monomind-task` board does not exist:
97
+ 1. Create it: `monotask board create "monomind-task" --json` — store `TASK_BOARD_ID`.
98
+ 2. Add to space: `monotask space boards add $SPACE_ID $TASK_BOARD_ID`.
99
+ 3. Create columns in order:
100
+ - `Backlog`
101
+ - `Todo`
102
+ - `In Progress`
103
+ - `Review`
104
+ - `Human in Loop`
105
+ - `Done`
106
+ - Store all column IDs mapped by name.
107
+
108
+ ---
109
+
110
+ ## Step 3: Deep Analysis — Understand the Document
111
+
112
+ Spawn a `Software Architect` agent via the Agent tool. Provide it with:
113
+
114
+ - The complete `FULL_CONTEXT`
115
+ - The user's original `$ARGUMENTS`
116
+
117
+ The agent MUST produce a structured analysis:
118
+
119
+ ```json
120
+ {
121
+ "summary": "2-3 sentence overview of what this document/prompt is about",
122
+ "goals": ["list of high-level goals or features described"],
123
+ "components": [
124
+ {
125
+ "name": "component or module name",
126
+ "description": "what it does",
127
+ "dependencies": ["other components it depends on"],
128
+ "files_likely_affected": ["paths from graphify or educated guesses"]
129
+ }
130
+ ],
131
+ "technical_constraints": ["any constraints, tech stack requirements, or limitations mentioned"],
132
+ "acceptance_criteria": ["testable conditions for when this is done"],
133
+ "risks": ["potential pitfalls, ambiguities, or unknowns"]
134
+ }
135
+ ```
136
+
137
+ Store as `ANALYSIS`.
138
+
139
+ ---
140
+
141
+ ## Step 4: Generate Implementation Plan
142
+
143
+ Spawn a `planner` agent via the Agent tool. Provide it with:
144
+
145
+ - The `ANALYSIS` from Step 3
146
+ - The `FULL_CONTEXT`
147
+ - The `REPO_NAME` and project info
148
+
149
+ The agent MUST produce an ordered list of implementation tasks. Each task must be **completely self-contained** — a coder agent with NO prior context should be able to execute it by reading only the task card.
150
+
151
+ For each task, produce:
152
+
153
+ ```json
154
+ {
155
+ "title": "Short action-oriented title (e.g. 'Add webhook retry logic with exponential backoff')",
156
+ "description": "Detailed description: WHAT to build, WHY it's needed, WHERE it fits in the system",
157
+ "context": "All relevant context a coder needs: existing patterns to follow, related files, API shapes, data models, config values. Include specific file paths from graphify results when available.",
158
+ "acceptance_criteria": ["list of testable conditions that prove this task is done"],
159
+ "checklist": ["step-by-step implementation steps the coder should follow"],
160
+ "agent_type": "recommended agent type (e.g. coder, backend-dev, Frontend Developer, Security Engineer)",
161
+ "priority": "critical | high | medium | low",
162
+ "effort": 1-10,
163
+ "dependencies": ["titles of other tasks that must complete first, or empty array"]
164
+ }
165
+ ```
166
+
167
+ **Rules for task generation:**
168
+ - Tasks MUST be ordered so dependencies come first.
169
+ - Each task should take a single agent 5-30 minutes to complete.
170
+ - Tasks that are too large MUST be split.
171
+ - Every task MUST include enough context that the coder doesn't need to read the original document.
172
+ - Include a test-writing step in every task's checklist.
173
+ - Assign agent types from available agents (coder, backend-dev, Frontend Developer, Security Engineer, etc.) based on the task domain.
174
+
175
+ Store as `TASKS` array.
176
+
177
+ ---
178
+
179
+ ## Step 5: Create Tasks on Monotask Board
180
+
181
+ For each task in the `TASKS` array, in dependency order:
182
+
183
+ 1. **Create the card** in the `Todo` column (dependency-free tasks) or `Backlog` column (has unfinished dependencies):
184
+ ```bash
185
+ monotask card create $TASK_BOARD_ID $COL_TODO "<title>" --json
186
+ ```
187
+ Store the returned `CARD_ID`.
188
+
189
+ 2. **Set description** with the full context block:
190
+ ```bash
191
+ monotask card set-description $TASK_BOARD_ID $CARD_ID "<description>\n\n## Context\n<context>"
192
+ ```
193
+
194
+ 3. **Add agent assignment comment**:
195
+ ```bash
196
+ monotask card comment add $TASK_BOARD_ID $CARD_ID "Assigned agent: <agent_type>"
197
+ ```
198
+
199
+ 4. **Add acceptance criteria comment**:
200
+ ```bash
201
+ monotask card comment add $TASK_BOARD_ID $CARD_ID "Acceptance criteria:\n- <criterion 1>\n- <criterion 2>\n..."
202
+ ```
203
+
204
+ 5. **Add dependency comment** (if any):
205
+ ```bash
206
+ monotask card comment add $TASK_BOARD_ID $CARD_ID "Dependencies: <task title 1>, <task title 2>"
207
+ ```
208
+
209
+ 6. **Set priority**:
210
+ ```bash
211
+ monotask card set-priority $TASK_BOARD_ID $CARD_ID <1-4>
212
+ ```
213
+ Map: critical=1, high=2, medium=3, low=4.
214
+
215
+ 7. **Create checklist** with implementation steps:
216
+ ```bash
217
+ monotask checklist add $TASK_BOARD_ID $CARD_ID "Implementation Steps" --json
218
+ ```
219
+ Store `CHECKLIST_ID`, then for each step:
220
+ ```bash
221
+ monotask checklist item-add $TASK_BOARD_ID $CARD_ID $CHECKLIST_ID "<step>"
222
+ ```
223
+
224
+ Batch card creation commands where possible to reduce round-trips.
225
+
226
+ ---
227
+
228
+ ## Step 6: Exploration Session — Suggest Missing Pieces
229
+
230
+ After all tasks are created, spawn a **second** `Software Architect` agent (fresh context) via the Agent tool. Provide it with:
231
+
232
+ - The `ANALYSIS` from Step 3
233
+ - The complete list of `TASKS` already created (titles + descriptions)
234
+ - The `FULL_CONTEXT`
235
+
236
+ The agent must act as a **critical reviewer** and identify:
237
+
238
+ ```json
239
+ {
240
+ "missing_pieces": [
241
+ {
242
+ "title": "What's missing",
243
+ "description": "Why this matters and what should be done",
244
+ "category": "testing | documentation | error-handling | monitoring | security | performance | accessibility | deployment | migration"
245
+ }
246
+ ],
247
+ "upcoming_plans": [
248
+ {
249
+ "title": "Natural follow-up work",
250
+ "description": "What this would add and why it's worth considering",
251
+ "category": "enhancement | optimization | scale | integration"
252
+ }
253
+ ]
254
+ }
255
+ ```
256
+
257
+ **Areas to explore:**
258
+ - Missing test coverage (unit, integration, e2e)
259
+ - Error handling and edge cases not covered
260
+ - Documentation that should be created or updated
261
+ - Security considerations (input validation, auth, rate limiting)
262
+ - Performance implications (caching, indexing, pagination)
263
+ - Monitoring and observability (logging, metrics, alerts)
264
+ - Migration or backwards compatibility concerns
265
+ - Deployment steps or configuration changes needed
266
+ - Accessibility requirements (for UI tasks)
267
+
268
+ ### Present to User
269
+
270
+ Output the suggestions in a clear format:
271
+
272
+ ```
273
+ ## Missing Pieces
274
+
275
+ | # | Category | Title | Description |
276
+ |---|-------------|------------------------------------|----------------------------|
277
+ | 1 | testing | Add integration tests for webhook | Currently only unit tests |
278
+ | 2 | security | Rate-limit webhook endpoints | Prevent abuse |
279
+
280
+ ## Potential Follow-ups
281
+
282
+ | # | Category | Title | Description |
283
+ |---|-------------|------------------------------------|----------------------------|
284
+ | 1 | enhancement | Add webhook analytics dashboard | Track delivery rates |
285
+ ```
286
+
287
+ Then ask:
288
+
289
+ > **Found N missing pieces and M potential follow-ups.**
290
+ >
291
+ > Reply with the numbers you want to add as tasks (e.g., `1, 3, 5` or `all` or `none`).
292
+ > Missing pieces will be added to **Todo**. Follow-ups will be added to **Backlog**.
293
+
294
+ ### Process User Selection
295
+
296
+ If the user selects items:
297
+ 1. For each selected **missing piece**: Create a task card in `Todo` with full context (same Step 5 process). Add a comment: `"Source: exploration — missing piece"`.
298
+ 2. For each selected **follow-up**: Create a task card in `Backlog` with full context. Add a comment: `"Source: exploration — follow-up suggestion"`.
299
+
300
+ If the user says `none` or declines, skip.
301
+
302
+ ---
303
+
304
+ ## Step 7: Final Summary
305
+
306
+ Output:
307
+
308
+ ```
309
+ ## Task Creation Complete
310
+
311
+ **Source:** <prompt text | file path | folder path>
312
+ **Space:** $REPO_NAME (ID: $SPACE_ID)
313
+ **Board:** monomind-task (ID: $TASK_BOARD_ID)
314
+
315
+ ### Tasks Created
316
+
317
+ | # | Title | Agent | Priority | Column |
318
+ |---|----------------------------------------|-------------|----------|---------|
319
+ | 1 | <title> | backend-dev | high | Todo |
320
+ | 2 | <title> | coder | medium | Todo |
321
+ | 3 | <title> | Frontend Developer | medium | Backlog |
322
+
323
+ **Total:** N tasks in Todo, M tasks in Backlog
324
+ **Estimated effort:** X points
325
+ ```
326
+
327
+ ---
328
+
329
+ ## Step 8: Offer to Execute
330
+
331
+ If there are tasks in Todo, ask:
332
+
333
+ > **N tasks are ready for execution.** Want me to start `/monomind:do` to process them?
334
+ >
335
+ > It will pick up tasks one by one, execute them with the assigned agent, review for bugs, and loop until the queue is empty.
336
+
337
+ If the user agrees, invoke:
338
+ ```
339
+ Skill("monomind-do", "--space $SPACE_ID --board $TASK_BOARD_ID")
340
+ ```
@@ -55,6 +55,35 @@ If `cargo` is also missing, output this and STOP:
55
55
 
56
56
  ---
57
57
 
58
+ ## Step 1.5: Initialize Loop State
59
+
60
+ Generate a loop ID and write the initial state file so the dashboard can track this run:
61
+ ```bash
62
+ mkdir -p .monomind/loops
63
+ export DO_LOOP_ID="do-$(date +%s%3N)"
64
+ cat > ".monomind/loops/${DO_LOOP_ID}.json" << EOF
65
+ {
66
+ "id": "${DO_LOOP_ID}",
67
+ "type": "do",
68
+ "prompt": "/monomind:do $ARGUMENTS",
69
+ "currentTask": "discovering...",
70
+ "spaceId": "${SPACE_ID:-}",
71
+ "boardId": "${TASK_BOARD_ID:-}",
72
+ "filter": "${FILTER:-}",
73
+ "startedAt": $(date +%s%3N),
74
+ "lastRunAt": $(date +%s%3N),
75
+ "nextRunAt": 0,
76
+ "status": "running"
77
+ }
78
+ EOF
79
+ ```
80
+
81
+ Also check if a stop was requested from a previous cycle:
82
+ ```bash
83
+ [ -f ".monomind/loops/${DO_LOOP_ID}.stop" ] && echo "DO_STOP_REQUESTED=true"
84
+ ```
85
+ If `DO_STOP_REQUESTED=true`, output `[monomind:do] Stop requested via dashboard. Halting.`, remove state files, and STOP.
86
+
58
87
  ## Step 2: Find Next Task
59
88
 
60
89
  1. List cards in `Todo` first (prioritized), then `Backlog`:
@@ -69,6 +98,13 @@ If `cargo` is also missing, output this and STOP:
69
98
  ```
70
99
  [monomind:do] No tasks in Todo or Backlog. Checking again in 2 minutes...
71
100
  ```
101
+ Update loop state before scheduling:
102
+ ```bash
103
+ NEXT_AT=$(( $(date +%s%3N) + 120000 ))
104
+ cat > ".monomind/loops/${DO_LOOP_ID}.json" << EOF
105
+ {"id":"${DO_LOOP_ID}","type":"do","prompt":"/monomind:do $ARGUMENTS","currentTask":"queue empty — waiting","spaceId":"${SPACE_ID:-}","boardId":"${TASK_BOARD_ID:-}","filter":"${FILTER:-}","startedAt":$(cat .monomind/loops/${DO_LOOP_ID}.json 2>/dev/null | python3 -c "import sys,json;print(json.load(sys.stdin).get('startedAt',0))" 2>/dev/null || date +%s%3N),"lastRunAt":$(date +%s%3N),"nextRunAt":${NEXT_AT},"status":"waiting"}
106
+ EOF
107
+ ```
72
108
  Then use `ScheduleWakeup` with `delaySeconds: 120` and prompt `/monomind:do --space $SPACE_ID --board $TASK_BOARD_ID` (plus `--filter` if one was set) to check again. STOP this iteration.
73
109
 
74
110
  4. Store `CURRENT_CARD_ID` and `CURRENT_CARD_TITLE`.
@@ -273,4 +309,9 @@ If no tasks remain, output:
273
309
  [monomind:do] All tasks processed. Queue empty.
274
310
  ```
275
311
 
312
+ Remove the loop state file:
313
+ ```bash
314
+ rm -f ".monomind/loops/${DO_LOOP_ID}.json" ".monomind/loops/${DO_LOOP_ID}.stop"
315
+ ```
316
+
276
317
  Do NOT schedule another wake-up. STOP.
@@ -37,6 +37,27 @@ Extract:
37
37
  - `MAX_REPS` — from `--times` flag, default `10`
38
38
  - `PROMPT` — everything remaining after flags are removed
39
39
  - `CURRENT_REP` — starts at `1`
40
+ - `LOOP_ID` — generate as `repeat-<unix-timestamp-ms>` (use `date +%s000`)
41
+
42
+ Write the initial loop state file so the dashboard can track this run:
43
+ ```bash
44
+ mkdir -p .monomind/loops
45
+ LOOP_ID="repeat-$(date +%s%3N)"
46
+ cat > ".monomind/loops/${LOOP_ID}.json" << EOF
47
+ {
48
+ "id": "${LOOP_ID}",
49
+ "type": "repeat",
50
+ "prompt": "PROMPT",
51
+ "interval": INTERVAL,
52
+ "currentRep": 1,
53
+ "maxReps": MAX_REPS,
54
+ "startedAt": $(date +%s%3N),
55
+ "lastRunAt": $(date +%s%3N),
56
+ "nextRunAt": $(date +%s%3N),
57
+ "status": "running"
58
+ }
59
+ EOF
60
+ ```
40
61
 
41
62
  Output:
42
63
  ```
@@ -58,6 +79,16 @@ Run the `PROMPT` as if the user typed it directly. This means:
58
79
 
59
80
  ## Step 3: Report and Schedule Next
60
81
 
82
+ Before scheduling the next run, check if a stop was requested:
83
+ ```bash
84
+ [ -f ".monomind/loops/${LOOP_ID}.stop" ] && echo "STOP_REQUESTED=true"
85
+ ```
86
+ If `STOP_REQUESTED=true`, output `[monomind:repeat] Stop requested via dashboard. Halting.` and remove the state files:
87
+ ```bash
88
+ rm -f ".monomind/loops/${LOOP_ID}.json" ".monomind/loops/${LOOP_ID}.stop"
89
+ ```
90
+ Then STOP.
91
+
61
92
  After execution completes, output:
62
93
  ```
63
94
  [monomind:repeat] Run CURRENT_REP/MAX_REPS complete. Next in INTERVAL minutes...
@@ -69,9 +100,32 @@ If `CURRENT_REP > MAX_REPS`, output:
69
100
  ```
70
101
  [monomind:repeat] All MAX_REPS repetitions complete.
71
102
  ```
103
+ Remove the state file:
104
+ ```bash
105
+ rm -f ".monomind/loops/${LOOP_ID}.json"
106
+ ```
72
107
  STOP. Do NOT schedule another wake-up.
73
108
 
74
- Otherwise, use `ScheduleWakeup` with:
109
+ Otherwise, update the loop state before scheduling:
110
+ ```bash
111
+ NEXT_AT=$(( $(date +%s%3N) + INTERVAL * 60 * 1000 ))
112
+ cat > ".monomind/loops/${LOOP_ID}.json" << EOF
113
+ {
114
+ "id": "${LOOP_ID}",
115
+ "type": "repeat",
116
+ "prompt": "PROMPT",
117
+ "interval": INTERVAL,
118
+ "currentRep": CURRENT_REP,
119
+ "maxReps": MAX_REPS,
120
+ "startedAt": STARTED_AT,
121
+ "lastRunAt": $(date +%s%3N),
122
+ "nextRunAt": ${NEXT_AT},
123
+ "status": "running"
124
+ }
125
+ EOF
126
+ ```
127
+
128
+ Use `ScheduleWakeup` with:
75
129
  - `delaySeconds`: `INTERVAL * 60`
76
130
  - `prompt`: `/monomind:repeat --every INTERVAL --times MAX_REPS --rep CURRENT_REP PROMPT`
77
131
  - `reason`: `"repeat run CURRENT_REP/MAX_REPS of: PROMPT"`
@@ -433,6 +433,7 @@ const handlers = {
433
433
  confidence: result.confidence,
434
434
  reason: result.reason,
435
435
  semanticRouting: result.semanticRouting || false,
436
+ llmRouting: result.llmRouting || false,
436
437
  updatedAt: new Date().toISOString(),
437
438
  };
438
439
  if (result.extrasMatches && result.extrasMatches.length > 0) {
@@ -167,6 +167,127 @@ function isNonDevTask(taskLower) {
167
167
  return false;
168
168
  }
169
169
 
170
+ // ─── Two-Stage LLM Router (for non-dev and ambiguous tasks) ─────────────────
171
+ // Stage 1: LLM picks category from ~9 categories
172
+ // Stage 2: LLM picks specific agent from agents in that category
173
+ // Falls back to keyword scoring if API unavailable
174
+
175
+ function getAnthropicKey() {
176
+ return process.env.ANTHROPIC_API_KEY || '';
177
+ }
178
+
179
+ function buildCategoryList() {
180
+ const registry = loadExtrasRegistry();
181
+ const cats = {};
182
+ for (const e of registry.extras) {
183
+ if (!cats[e.category]) cats[e.category] = [];
184
+ cats[e.category].push(e.name);
185
+ }
186
+ return Object.entries(cats).map(([name, agents]) => ({
187
+ name,
188
+ count: agents.length,
189
+ examples: agents.slice(0, 4).join(', '),
190
+ }));
191
+ }
192
+
193
+ function getAgentsInCategory(category) {
194
+ const registry = loadExtrasRegistry();
195
+ return registry.extras
196
+ .filter(e => e.category === category)
197
+ .map(e => ({ slug: e.slug, name: e.name, description: (e.description || '').slice(0, 120) }));
198
+ }
199
+
200
+ async function llmPick(systemPrompt, userPrompt) {
201
+ const key = getAnthropicKey();
202
+ if (!key) return null;
203
+ try {
204
+ const ac = new AbortController();
205
+ const timer = setTimeout(() => ac.abort(), 2000);
206
+ const resp = await fetch('https://api.anthropic.com/v1/messages', {
207
+ method: 'POST',
208
+ headers: {
209
+ 'Content-Type': 'application/json',
210
+ 'x-api-key': key,
211
+ 'anthropic-version': '2023-06-01',
212
+ },
213
+ body: JSON.stringify({
214
+ model: 'claude-haiku-4-5-20251001',
215
+ max_tokens: 60,
216
+ system: systemPrompt,
217
+ messages: [{ role: 'user', content: userPrompt }],
218
+ }),
219
+ signal: ac.signal,
220
+ });
221
+ clearTimeout(timer);
222
+ if (!resp.ok) return null;
223
+ const data = await resp.json();
224
+ const text = (data.content && data.content[0] && data.content[0].text) || '';
225
+ return text.trim();
226
+ } catch { return null; }
227
+ }
228
+
229
+ async function routeTaskLLM(task) {
230
+ const categories = buildCategoryList();
231
+ if (!categories.length) return null;
232
+
233
+ // Stage 1: pick category
234
+ const catList = categories.map((c, i) => `${i + 1}. ${c.name} (${c.count} agents, e.g. ${c.examples})`).join('\n');
235
+ const stage1System = 'You route tasks to agent categories. Reply with ONLY the category name, nothing else.';
236
+ const stage1User = `Task: "${task}"\n\nCategories:\n${catList}\n\nWhich category best fits this task? Reply with the category name only.`;
237
+
238
+ const pickedCat = await llmPick(stage1System, stage1User);
239
+ if (!pickedCat) return null;
240
+
241
+ // Normalize — find closest category match
242
+ const catLower = pickedCat.toLowerCase().replace(/[^a-z-]/g, '');
243
+ const matchedCat = categories.find(c => c.name.toLowerCase().replace(/[^a-z-]/g, '') === catLower);
244
+ const categoryName = matchedCat ? matchedCat.name : categories.find(c => catLower.includes(c.name.toLowerCase().replace(/[^a-z-]/g, '')))?.name;
245
+ if (!categoryName) return null;
246
+
247
+ // Stage 2: pick agent within category
248
+ const agents = getAgentsInCategory(categoryName);
249
+ if (!agents.length) return null;
250
+
251
+ const agentList = agents.map((a, i) => `${i + 1}. ${a.name} — ${a.description}`).join('\n');
252
+ const stage2System = 'You pick the best agent for a task. Reply with ONLY the agent name exactly as listed, nothing else.';
253
+ const stage2User = `Task: "${task}"\n\nAgents in ${categoryName}:\n${agentList}\n\nWhich agent is the best fit? Reply with the exact agent name only.`;
254
+
255
+ const pickedAgent = await llmPick(stage2System, stage2User);
256
+ if (!pickedAgent) return null;
257
+
258
+ // Find the agent entry — exact match only, fall back to keyword routing
259
+ const agentLower = pickedAgent.toLowerCase().trim();
260
+ const matched = agents.find(a => a.name.toLowerCase() === agentLower);
261
+ if (!matched) return null;
262
+
263
+ return {
264
+ agent: matched.name,
265
+ agentSlug: matched.slug,
266
+ confidence: 0.9,
267
+ reason: `LLM 2-stage: ${categoryName} → ${matched.name}`,
268
+ category: categoryName,
269
+ allInCategory: agents.map(a => ({ slug: a.slug, label: a.name, note: categoryName })),
270
+ };
271
+ }
272
+
273
+ /**
274
+ * Route multiple subtasks for swarm agent selection.
275
+ * Each subtask description gets its own 2-stage LLM routing.
276
+ * Returns array of { subtask, agent, agentSlug, confidence, reason }.
277
+ */
278
+ async function routeSwarmAgents(subtasks) {
279
+ if (!Array.isArray(subtasks) || !subtasks.length) return [];
280
+ const results = await Promise.all(subtasks.map(async (sub) => {
281
+ const desc = typeof sub === 'string' ? sub : sub.description || sub.task || '';
282
+ if (!desc) return { subtask: desc, agent: 'coder', agentSlug: 'coder', confidence: 0.5, reason: 'empty subtask' };
283
+ const llm = await routeTaskLLM(desc);
284
+ if (llm) return { subtask: desc, agent: llm.agent, agentSlug: llm.agentSlug, confidence: llm.confidence, reason: llm.reason };
285
+ const kw = routeTask(desc);
286
+ return { subtask: desc, agent: kw.agent, agentSlug: kw.agentSlug, confidence: kw.confidence, reason: kw.reason };
287
+ }));
288
+ return results;
289
+ }
290
+
170
291
  // ─── RouteLayer bridge (GAP-002) ─────────────────────────────────────────────
171
292
  // Cache a Promise so concurrent callers all await the same load operation.
172
293
  var _routeLayerPromise = null;
@@ -187,11 +308,33 @@ async function tryLoadRouteLayer() {
187
308
  }
188
309
 
189
310
  /**
190
- * Async variant — tries RouteLayer semantic routing first, falls back to keywords.
191
- * hook-handler.cjs route handler should call this instead of routeTask().
311
+ * Async variant — tries LLM 2-stage routing for non-dev tasks,
312
+ * RouteLayer semantic routing for dev tasks, falls back to keywords.
192
313
  */
193
314
  async function routeTaskSemantic(task) {
194
315
  if (typeof task !== 'string' || !task) return routeTask(task);
316
+ const taskLower = task.toLowerCase();
317
+
318
+ // For non-dev tasks or ambiguous defaults, try LLM 2-stage routing first
319
+ if (isNonDevTask(taskLower)) {
320
+ const llmResult = await routeTaskLLM(task);
321
+ if (llmResult) {
322
+ const extrasMatches = matchExtras(task);
323
+ return {
324
+ agent: llmResult.agent,
325
+ agentSlug: llmResult.agentSlug,
326
+ confidence: llmResult.confidence,
327
+ reason: llmResult.reason,
328
+ skillMatches: [],
329
+ extrasMatches,
330
+ specificAgents: llmResult.allInCategory.slice(0, 5),
331
+ llmRouting: true,
332
+ };
333
+ }
334
+ // LLM failed — fall through to keyword-based extras matching
335
+ }
336
+
337
+ // Dev tasks: try RouteLayer semantic routing
195
338
  const rl = await tryLoadRouteLayer();
196
339
  if (rl && rl.route) {
197
340
  try {
@@ -216,7 +359,25 @@ async function routeTaskSemantic(task) {
216
359
  }
217
360
  } catch (e) { /* fall through to keyword */ }
218
361
  }
219
- return routeTask(task);
362
+
363
+ // Default keyword fallback — also try LLM if no dev pattern matched
364
+ const keywordResult = routeTask(task);
365
+ if (keywordResult.confidence <= 0.5) {
366
+ const llmResult = await routeTaskLLM(task);
367
+ if (llmResult) {
368
+ return {
369
+ agent: llmResult.agent,
370
+ agentSlug: llmResult.agentSlug,
371
+ confidence: llmResult.confidence,
372
+ reason: llmResult.reason,
373
+ skillMatches: keywordResult.skillMatches,
374
+ extrasMatches: matchExtras(task),
375
+ specificAgents: llmResult.allInCategory.slice(0, 5),
376
+ llmRouting: true,
377
+ };
378
+ }
379
+ }
380
+ return keywordResult;
220
381
  }
221
382
 
222
383
  // ─── Main routing ─────────────────────────────────────────────────────────────
@@ -288,7 +449,7 @@ function loadExtrasAgent(slug) {
288
449
  } catch (e) { return null; }
289
450
  }
290
451
 
291
- module.exports = { routeTask, routeTaskSemantic, matchSkills, matchExtras, loadExtrasAgent, loadExtrasRegistry, loadSkillRegistry, AGENT_CAPABILITIES, TASK_PATTERNS };
452
+ module.exports = { routeTask, routeTaskSemantic, routeTaskLLM, routeSwarmAgents, matchSkills, matchExtras, loadExtrasAgent, loadExtrasRegistry, loadSkillRegistry, buildCategoryList, getAgentsInCategory, AGENT_CAPABILITIES, TASK_PATTERNS };
292
453
 
293
454
  // CLI
294
455
  if (require.main === module) {