@leing2021/super-pi 0.23.13 → 0.25.0

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 CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  **Turn your AI coding agent into a reliable engineer.**
10
10
 
11
- Super Pi is a Pi-native engineering workflow layer: it adds stage discipline, durable artifacts, TDD gates, checkpoints, review, and learning loops on top of your coding agent. It is not a general-purpose multi-agent executor; subagents are optional helpers for bounded leaf tasks.
11
+ Super Pi is a Pi-native engineering workflow layer: it adds stage discipline, durable artifacts, TDD gates, checkpoints, review, and learning loops on top of your coding agent.
12
12
 
13
13
  Install, describe what you want to build, then keep saying "continue." Super Pi drives the full loop:
14
14
 
@@ -25,10 +25,9 @@ pi install npm:@leing2021/super-pi
25
25
  - **Five-step loop** — brainstorm → plan → work → review → learn, with automatic skill routing
26
26
  - **Checkpoint resume** — interrupted? Resume from the exact unit you left off
27
27
  - **TDD enforcement** — every unit follows RED → GREEN → REFACTOR with hard gates
28
- - **Controlled subagents** — inline execution by default; subagents are limited to bounded, non-interactive leaf tasks
29
28
  - **Evidence-first review** — auto-assigned reviewers across five axes, autofix loop
30
29
  - **Knowledge compounding** — solved problems become searchable solution artifacts
31
- - **Token-efficient** — ~2,600 tokens new-conversation overhead; progressive loading
30
+ - **Token-efficient** — ~4,200 tokens new-conversation overhead; progressive loading
32
31
 
33
32
  ---
34
33
 
@@ -45,7 +44,7 @@ You: I want to build a CLI tool that helps indie devs find early users
45
44
 
46
45
  → 01-brainstorm: structured discovery → requirements artifact
47
46
  → 02-plan: TDD-gated implementation units → plan artifact
48
- → 03-work: inline-first execution, bounded subagents, checkpoint resume
47
+ → 03-work: inline execution, checkpoint resume
49
48
  → 04-review: five-axis findings, autofix loop
50
49
  → 05-learn: knowledge compounding
51
50
 
@@ -73,18 +72,11 @@ You: /skill:03-work docs/plans/plan.md
73
72
  |-------|-------------|-----------|
74
73
  | **01-brainstorm** | Structured multi-round discovery, domain vocabulary persistence | `brainstorm_dialog` |
75
74
  | **02-plan** | TDD-gated implementation units, optional CEO Review | `plan_diff` |
76
- | **03-work** | Inline-first execution, bounded subagents, checkpoint resume, strict TDD, stop-the-line | `ce_subagent`, `ce_parallel_subagent` |
75
+ | **03-work** | Inline execution, checkpoint resume, strict TDD, stop-the-line | `session_checkpoint`, `task_splitter` |
77
76
  | **04-review** | Auto-assigned reviewers, five-axis findings, autofix loop | `review_router` |
78
77
  | **05-learn** | Pattern extraction → searchable solution artifacts | `pattern_extractor` |
79
78
  | **06-next** | Next-step recommendation + workflow status | `workflow_state` |
80
79
  | **07-worktree** | Isolated git worktree development | `worktree_manager` |
81
- | **08-help** | Phase 1 skill explainer | — |
82
-
83
- ### Subagent tool namespace
84
-
85
- Super Pi ships CE-specific tools named `ce_subagent` and `ce_parallel_subagent`. They are intentionally namespaced so they can coexist with the compatible third-party `pi-subagents` extension without tool-name collisions.
86
-
87
- These tools are helpers, not the core workflow. Use inline execution by default. Use CE subagents only for bounded, non-interactive, easily verifiable leaf tasks. Do not use them to invoke pipeline-stage skills (`01-brainstorm` through `05-learn`); run those stages directly with `/skill:<stage>`.
88
80
 
89
81
  ### Model & Thinking Routing
90
82
 
@@ -117,7 +109,7 @@ Super Pi is not a fork or wrapper. It extracts useful methods from the projects
117
109
  | Project | What Super Pi adopted |
118
110
  |---------|------------------------|
119
111
  | [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) | "Use when" skill trigger conditions, source-driven verification, stop-the-line hard gate, anti-rationalization, and the five-axis review baseline. Adopted as embedded micro-patterns only — no new skills, tools, commands, or agents. |
120
- | [everything-claude-code](https://github.com/affaan-m/everything-claude-code) | Parallel subagent orchestration, checkpoint resume, continuous learning loops, and token-conscious agent workflow design. |
112
+ | [everything-claude-code](https://github.com/affaan-m/everything-claude-code) | Checkpoint resume, continuous learning loops, and token-conscious agent workflow design. |
121
113
  | [humanlayer/12-factor-agents](https://github.com/humanlayer/12-factor-agents) | Context window ownership, compacting resolved errors, retry caps, and pre-fetching obvious prerequisites. Adopted as lightweight context hygiene rules inside the existing Phase 1 pipeline. |
122
114
  | [superpowers](https://github.com/obra/superpowers) | Strict TDD gates, design checklists, review discipline, and the idea that agents need hard gates instead of gentle suggestions. |
123
115
  | [compound-engineering-plugin](https://github.com/EveryInc/compound-engineering-plugin) | The five-step think → plan → build → review → learn loop and the knowledge-compounding backbone. |
@@ -153,13 +145,13 @@ All reviewers evaluate changes across: **correctness, readability, architecture,
153
145
 
154
146
  ## Token Cost
155
147
 
156
- New conversation overhead: **~2,600 tokens** (1.3% of 200K context).
148
+ New conversation overhead: **~4,130 tokens** (2.1% of 200K context).
157
149
 
158
150
  | Component | Tokens |
159
151
  |-----------|--------|
160
- | 8 skill registrations | ~490 |
161
- | System prompt | ~1,400 |
162
- | Skill inlining (per invocation) | ~500-800 |
152
+ | 17 skill registrations | ~1,710 |
153
+ | 22 tool schemas | ~2,420 |
154
+ | Skill inlining (per invocation) | ~300–1,200 |
163
155
 
164
156
  Progressive loading: only needed skills loaded on-demand.
165
157
 
@@ -191,11 +183,11 @@ Commit everything to git — these files are the project's traceable memory.
191
183
 
192
184
  | Component | Count |
193
185
  |-----------|------:|
194
- | Skills | 8 |
195
- | Tools | 19 |
196
- | Rules | 79 |
197
- | TypeScript lines | ~4,400 |
198
- | Tests | 209 (786 assertions) |
186
+ | Skills | 7 |
187
+ | Tools | 12 CE + 10 Pi built-in |
188
+ | Rules | 78 |
189
+ | TypeScript lines | ~4,100 |
190
+ | Tests | 180 (727 assertions) |
199
191
 
200
192
  Rules in `rules/` cover 11 common topics + language-specific sets (TypeScript, Rust, Go, Python, Java, Kotlin, C++, C#, Dart, Swift, Perl, PHP). Project-level overrides take priority.
201
193
 
@@ -1,14 +1,13 @@
1
1
  import { readFile } from "node:fs/promises"
2
2
  import path from "node:path"
3
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
4
4
  import { Type } from "typebox"
5
5
  import { createArtifactHelperTool, type ArtifactType } from "./tools/artifact-helper"
6
- import { createAskUserQuestionTool } from "./tools/ask-user-question"
7
- import { createSubagentTool } from "./tools/subagent"
6
+ import { createAskUserQuestionTool, CUSTOM_SENTINEL } from "./tools/ask-user-question"
7
+ import { createAskUserQuestionCustomFactory } from "./tools/ask-user-question-ui"
8
8
  import { createWorkflowStateTool } from "./tools/workflow-state"
9
9
  import { createWorktreeManagerTool } from "./tools/worktree-manager"
10
10
  import { createReviewRouterTool } from "./tools/review-router"
11
- import { createParallelSubagentTool } from "./tools/parallel-subagent"
12
11
  import { createSessionCheckpointTool } from "./tools/session-checkpoint"
13
12
  import { createTaskSplitterTool } from "./tools/task-splitter"
14
13
  import { createBrainstormDialogTool } from "./tools/brainstorm-dialog"
@@ -19,11 +18,6 @@ import { createContextHandoffTool } from "./tools/context-handoff"
19
18
  import { filterBashOutput } from "./tools/bash-output-filter"
20
19
  import { filterReadOutput } from "./tools/read-output-filter"
21
20
  import { COMPACTION_FOCUS_INSTRUCTIONS } from "./tools/compaction-optimizer"
22
- import { createJsonRunner, type JsonRunConfig } from "./tools/subagent-json-runner"
23
- import { getFinalOutput, isFailedResult, type SingleResult } from "./tools/subagent-events"
24
- import { renderSubagentCall, renderSubagentResult } from "./tools/subagent-renderer"
25
- import type { SubagentLiveRunner, SubagentLiveExecOptions, SubagentLiveDetails } from "./tools/subagent"
26
- import type { ParallelSubagentLiveDetails } from "./tools/parallel-subagent"
27
21
 
28
22
  const PIPELINE_STAGE_KEYS = new Set([
29
23
  "01-brainstorm",
@@ -33,6 +27,70 @@ const PIPELINE_STAGE_KEYS = new Set([
33
27
  "05-learn",
34
28
  ])
35
29
 
30
+ /**
31
+ * Module-level promise chain that serializes interactive `ask_user_question`
32
+ * UI calls within a single Pi process.
33
+ *
34
+ * Why: `@earendil-works/pi-coding-agent` renders extension selectors via a
35
+ * singleton field (`extensionSelector` / `extensionInput` in
36
+ * `interactive-mode.js`). When two selector calls overlap, the second
37
+ * overwrites the first, and the first promise never resolves — surfacing to
38
+ * the agent as a silent `No result provided`. See
39
+ * `docs/bug/ask-user-question-parallel-call-silent-failure.md`.
40
+ *
41
+ * This guard ensures at most one selector/input is on screen at a time.
42
+ */
43
+ let askUserQuestionExclusiveChain: Promise<unknown> = Promise.resolve()
44
+
45
+ function runAskUserQuestionExclusive<T>(task: () => Promise<T>): Promise<T> {
46
+ const run = askUserQuestionExclusiveChain.then(task, task)
47
+ // Keep the chain robust: a rejected run must not poison subsequent calls.
48
+ askUserQuestionExclusiveChain = run.then(
49
+ () => undefined,
50
+ () => undefined,
51
+ )
52
+ return run
53
+ }
54
+
55
+ /**
56
+ * Build the UI adapter for `ask_user_question`.
57
+ *
58
+ * In interactive TUI mode with `ctx.ui.custom` available, route `select`
59
+ * through the scrollable custom dialog so long questions/options stay readable
60
+ * (see `docs/bug/ask-user-question-long-text-not-scrollable.md`). Otherwise
61
+ * fall back to the built-in `ctx.ui.select()`.
62
+ */
63
+ function buildAskUserQuestionUi(ctx: any): import("./tools/ask-user-question").AskUserQuestionUi {
64
+ const useCustom = ctx?.mode === "tui" && typeof ctx?.ui?.custom === "function"
65
+ const input = async (question: string) => (await ctx.ui.input(question)) ?? null
66
+ const selectFallback = async (question: string, options: string[]) =>
67
+ (await ctx.ui.select(question, options)) ?? null
68
+
69
+ if (!useCustom) {
70
+ return { input, select: selectFallback }
71
+ }
72
+
73
+ const select = async (question: string, options: string[]): Promise<string | null> => {
74
+ // The custom sentinel is appended last (if present) and matches
75
+ // `CUSTOM_SENTINEL` or a `CUSTOM_SENTINEL (#n)` disambiguation suffix.
76
+ const last = options.length > 0 ? options[options.length - 1] : null
77
+ const customLabel = last !== null
78
+ && (last === CUSTOM_SENTINEL || last.startsWith(`${CUSTOM_SENTINEL} (#`))
79
+ ? last
80
+ : null
81
+ const factory = createAskUserQuestionCustomFactory({
82
+ question,
83
+ displayOptions: options,
84
+ customLabel,
85
+ })
86
+ const selection = (await ctx.ui.custom(factory)) as
87
+ { selectedLabel: string | null } | null | undefined
88
+ return selection?.selectedLabel ?? null
89
+ }
90
+
91
+ return { input, select }
92
+ }
93
+
36
94
  interface StrategySettings {
37
95
  modelStrategy?: Record<string, string>
38
96
  thinkingStrategy?: Record<string, string>
@@ -116,34 +174,6 @@ function parseModelRef(
116
174
  }
117
175
  }
118
176
 
119
- /**
120
- * Create a spawn-based subagent runner that uses pi --mode json
121
- * with per-process env (no global process.env mutation).
122
- */
123
- function createSubagentRunner(
124
- pi: ExtensionAPI,
125
- signal?: AbortSignal,
126
- ): SubagentLiveRunner {
127
- const jsonRunner = createJsonRunner()
128
- return async (prompt: string, options?: SubagentLiveExecOptions) => {
129
- const config: JsonRunConfig = {
130
- prompt,
131
- agent: "",
132
- task: "",
133
- cwd: (pi as any).cwd ?? process.cwd(),
134
- extraFlags: options?.extraFlags,
135
- extraEnv: options?.extraEnv,
136
- signal,
137
- onUpdate: options?.onUpdate,
138
- }
139
- const result = await jsonRunner.run(config)
140
- if (isFailedResult(result) || result.exitCode !== 0) {
141
- throw new Error(result.errorMessage || result.stderr || `Subagent failed for prompt: ${prompt}`)
142
- }
143
- return result
144
- }
145
- }
146
-
147
177
  const artifactHelperParams = Type.Object({
148
178
  repoRoot: Type.String({ description: "Repository root where workflow artifacts should be created" }),
149
179
  artifactType: Type.Union([
@@ -166,18 +196,6 @@ const askUserQuestionParams = Type.Object({
166
196
  allowCustom: Type.Optional(Type.Boolean({ description: "Allow a custom answer when options are present" })),
167
197
  })
168
198
 
169
- const subagentTaskSchema = Type.Object({
170
- agent: Type.String({ description: "Skill name to invoke via /skill:<name>" }),
171
- task: Type.String({ description: "Task text passed to the subagent" }),
172
- })
173
-
174
- const subagentParams = Type.Object({
175
- agent: Type.Optional(Type.String({ description: "Single subagent skill name" })),
176
- task: Type.Optional(Type.String({ description: "Single subagent task" })),
177
- chain: Type.Optional(Type.Array(subagentTaskSchema, { description: "Serial subagent chain with optional {previous} placeholder" })),
178
- inheritSkills: Type.Optional(Type.Boolean({ description: "Whether the subagent should inherit skills. Default: true" })),
179
- })
180
-
181
199
  const workflowStateParams = Type.Object({
182
200
  repoRoot: Type.String({ description: "Repository root to scan for workflow artifacts" }),
183
201
  })
@@ -200,19 +218,6 @@ const reviewRouterParams = Type.Object({
200
218
  deletions: Type.Number({ description: "Number of lines removed" }),
201
219
  })
202
220
 
203
- const parallelSubagentTaskSchema = Type.Object({
204
- agent: Type.String({ description: "Skill name to invoke via /skill:<name>" }),
205
- task: Type.String({ description: "Task text passed to the subagent" }),
206
- })
207
-
208
- const parallelSubagentParams = Type.Object({
209
- tasks: Type.Union([
210
- Type.Array(parallelSubagentTaskSchema),
211
- Type.String({ description: "JSON stringified array of tasks" }),
212
- ], { description: "Array of independent tasks to run concurrently (can be a JSON string)" }),
213
- inheritSkills: Type.Optional(Type.Boolean({ description: "Whether subagents should inherit skills. Default: true" })),
214
- })
215
-
216
221
  const sessionCheckpointParams = Type.Object({
217
222
  operation: Type.Union([
218
223
  Type.Literal("save"),
@@ -346,9 +351,18 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
346
351
  return { action: "continue" as const }
347
352
  }
348
353
 
354
+ // Skip model/thinking switching during streaming steers — these are
355
+ // mid-stream interrupts, not new pipeline invocations.
356
+ if (event.streamingBehavior === "steer") {
357
+ return { action: "continue" as const }
358
+ }
359
+
349
360
  const settings = await readSettings(ctx.cwd)
350
361
  const modelStrategy = settings?.modelStrategy
351
362
  const thinkingStrategy = settings?.thinkingStrategy
363
+ // Notification guard: only notify in interactive (TUI) or RPC modes.
364
+ // For interactive input capability checks (askUserQuestion), use ctx.hasUI directly.
365
+ const shouldNotify = ctx.mode === "tui" || ctx.mode === "rpc"
352
366
 
353
367
  // Model switching
354
368
  if (modelStrategy) {
@@ -362,19 +376,19 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
362
376
  if (model) {
363
377
  const switched = await pi.setModel(model)
364
378
  if (switched) {
365
- if (ctx.hasUI) {
379
+ if (shouldNotify) {
366
380
  ctx.ui.notify(`Switched model for ${stageKey}: ${model.provider}/${model.id}`, "info")
367
381
  }
368
382
  } else {
369
- if (ctx.hasUI) {
383
+ if (shouldNotify) {
370
384
  ctx.ui.notify(`No API key for ${stageKey}: ${model.provider}/${model.id}`, "warning")
371
385
  }
372
386
  }
373
- } else if (ctx.hasUI) {
387
+ } else if (shouldNotify) {
374
388
  ctx.ui.notify(`Model not found for ${stageKey}: ${targetModelRef}`, "warning")
375
389
  }
376
390
  }
377
- } else if (ctx.hasUI) {
391
+ } else if (shouldNotify) {
378
392
  ctx.ui.notify(`Invalid modelStrategy for ${stageKey}: ${targetModelRef}`, "warning")
379
393
  }
380
394
  }
@@ -399,7 +413,7 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
399
413
  const currentLevel = pi.getThinkingLevel()
400
414
  if (currentLevel !== normalized) {
401
415
  pi.setThinkingLevel(normalized)
402
- if (ctx.hasUI) {
416
+ if (shouldNotify) {
403
417
  ctx.ui.notify(`Switched thinking level for ${stageKey}: ${normalized}`, "info")
404
418
  }
405
419
  }
@@ -411,11 +425,9 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
411
425
 
412
426
  const artifactHelper = createArtifactHelperTool()
413
427
  const askUserQuestion = createAskUserQuestionTool()
414
- const subagent = createSubagentTool()
415
428
  const workflowState = createWorkflowStateTool()
416
429
  const worktreeManager = createWorktreeManagerTool()
417
430
  const reviewRouter = createReviewRouterTool()
418
- const parallelSubagent = createParallelSubagentTool()
419
431
  const sessionCheckpoint = createSessionCheckpointTool()
420
432
  const taskSplitter = createTaskSplitterTool()
421
433
  const brainstormDialog = createBrainstormDialogTool()
@@ -452,6 +464,11 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
452
464
  name: askUserQuestion.name,
453
465
  label: "Ask User Question",
454
466
  description: "Ask the user a structured question with optional choices and custom answers.",
467
+ promptSnippet: "Ask the user a structured question with optional choices and custom answers",
468
+ promptGuidelines: [
469
+ "Call ask_user_question one at a time, never two or more in the same assistant message: Pi renders selectors on a shared singleton, so parallel ask_user_question calls silently fail with 'No result provided'.",
470
+ "For ask_user_question, keep the question concise and put long analysis in prior text or a file first; ask_user_question normalizes long options to short labels but the full answer is still returned to you.",
471
+ ],
455
472
  parameters: askUserQuestionParams,
456
473
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
457
474
  if (!ctx.hasUI) {
@@ -462,17 +479,14 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
462
479
  }
463
480
  }
464
481
 
465
- const result = await askUserQuestion.execute(
482
+ const result = await runAskUserQuestionExclusive(() => askUserQuestion.execute(
466
483
  {
467
484
  question: params.question,
468
485
  options: params.options,
469
486
  allowCustom: params.allowCustom,
470
487
  },
471
- {
472
- input: async (question) => (await ctx.ui.input(question)) ?? null,
473
- select: async (question, options) => (await ctx.ui.select(question, options)) ?? null,
474
- },
475
- )
488
+ buildAskUserQuestionUi(ctx),
489
+ ))
476
490
 
477
491
  const contentText = result.answer === null
478
492
  ? "User cancelled."
@@ -489,45 +503,6 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
489
503
  },
490
504
  })
491
505
 
492
- pi.registerTool({
493
- name: subagent.name,
494
- label: "CE Subagent",
495
- description: "Run a single CE skill-based subagent or a serial chain in an isolated Pi process.",
496
- parameters: subagentParams,
497
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
498
- const result = await subagent.execute(
499
- {
500
- agent: params.agent,
501
- task: params.task,
502
- chain: params.chain,
503
- inheritSkills: params.inheritSkills,
504
- },
505
- createSubagentRunner(pi, signal),
506
- { onUpdate: onUpdate ? (details) => onUpdate({ content: [], details }) : undefined },
507
- )
508
-
509
- // Build final content from results
510
- const lastResult = result.results[result.results.length - 1]
511
- const finalText = lastResult ? getFinalOutput(lastResult.messages) : ""
512
- const isError = lastResult ? isFailedResult(lastResult) : false
513
-
514
- return {
515
- content: [{ type: "text", text: finalText || "(no output)" }],
516
- details: result,
517
- isError: isError || undefined,
518
- }
519
- },
520
- renderCall(args, theme, _context) {
521
- return renderSubagentCall(args, theme)
522
- },
523
- renderResult(result, renderContext, theme, _context) {
524
- // Handle partial updates where data is at top level (from onUpdate)
525
- // rather than nested in .details (from final result return value)
526
- const data = (result.details || result) as SubagentLiveDetails
527
- return renderSubagentResult(data, renderContext, theme)
528
- },
529
- })
530
-
531
506
  pi.registerTool({
532
507
  name: workflowState.name,
533
508
  label: "Workflow State",
@@ -599,73 +574,6 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
599
574
  },
600
575
  })
601
576
 
602
- pi.registerTool({
603
- name: parallelSubagent.name,
604
- label: "CE Parallel Subagent",
605
- description: "Run multiple CE skill-based subagent tasks concurrently. IMPORTANT: Provide 'tasks' as a clean JSON array object. If the environment forces a string, provide a valid JSON array string.",
606
- parameters: parallelSubagentParams,
607
- async execute(_toolCallId, params, signal, onUpdate) {
608
- let tasks: any[]
609
- if (typeof params.tasks === "string") {
610
- try {
611
- const cleaned = params.tasks.replace(/^```json\s*|```$/g, "").trim()
612
- tasks = JSON.parse(cleaned)
613
- } catch (e) {
614
- throw new Error(`Failed to parse tasks string as JSON: ${e instanceof Error ? e.message : String(e)}`)
615
- }
616
- } else {
617
- tasks = params.tasks
618
- }
619
-
620
- const result = await parallelSubagent.execute(
621
- {
622
- tasks,
623
- inheritSkills: params.inheritSkills,
624
- },
625
- createSubagentRunner(pi, signal),
626
- { onUpdate: onUpdate ? (details) => onUpdate({ content: [], details }) : undefined },
627
- )
628
-
629
- // Build compact summary content for LLM consumption
630
- const successCount = result.results.filter(r => r.exitCode === 0).length
631
- const failCount = result.results.filter(r => isFailedResult(r)).length
632
- const summaries = result.results.map((r, i) => {
633
- const icon = isFailedResult(r) ? "✗" : "✓"
634
- const output = getFinalOutput(r.messages) || r.errorMessage || r.stderr || "(no output)"
635
- // Compact: first non-empty line, stripped of markdown formatting
636
- const summaryLine = output.split("\n").find((l: string) => l.trim().length > 0) || ""
637
- const cleaned = summaryLine.replace(/^#+\s*/, "").replace(/\*\*/g, "").trim()
638
- const oneLine = cleaned.length > 200 ? cleaned.slice(0, 199) + "…" : cleaned
639
- return `${i + 1}. ${icon} ${r.agent} — ${oneLine}`
640
- })
641
-
642
- // Full outputs available in details for expanded view
643
- const fullOutputs = result.results.map(r => {
644
- const output = getFinalOutput(r.messages) || ""
645
- return output
646
- })
647
-
648
- return {
649
- content: [{
650
- type: "text",
651
- text: `Parallel: ${successCount}/${result.results.length} succeeded${failCount > 0 ? `, ${failCount} failed` : ""}\n${summaries.join("\n")}`,
652
- }],
653
- details: result,
654
- }
655
- },
656
- renderCall(args, theme, _context) {
657
- return renderSubagentCall({
658
- tasks: typeof args.tasks === "string" ? [] : args.tasks,
659
- }, theme)
660
- },
661
- renderResult(result, renderContext, theme, _context) {
662
- // Handle partial updates where data is at top level (from onUpdate)
663
- // rather than nested in .details (from final result return value)
664
- const data = (result.details || result) as ParallelSubagentLiveDetails
665
- return renderSubagentResult(data, renderContext, theme)
666
- },
667
- })
668
-
669
577
  pi.registerTool({
670
578
  name: sessionCheckpoint.name,
671
579
  label: "Session Checkpoint",
@@ -909,31 +817,9 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
909
817
 
910
818
  export { createArtifactHelperTool } from "./tools/artifact-helper"
911
819
  export { createAskUserQuestionTool } from "./tools/ask-user-question"
912
- export { createSubagentTool } from "./tools/subagent"
913
- export { checkSubagentDepth, getChildDepthEnv, DEFAULT_MAX_SUBAGENT_DEPTH } from "./tools/subagent-depth-guard"
914
- export { AsyncMutex } from "./tools/async-mutex"
915
- export { createJsonRunner } from "./tools/subagent-json-runner"
916
- export {
917
- parseJsonEvent,
918
- applyEventToResult,
919
- type SingleResult as SingleResultType,
920
- type UsageStats,
921
- type ParsedEvent,
922
- isFailedResult as isFailedSingleResult,
923
- formatUsageStats,
924
- makeInitialResult,
925
- getFinalOutput as getFinalOutputFromMessages,
926
- getDisplayItems,
927
- } from "./tools/subagent-events"
928
- export {
929
- formatToolCall,
930
- renderSubagentCall,
931
- renderSubagentResult,
932
- } from "./tools/subagent-renderer"
933
820
  export { createWorkflowStateTool } from "./tools/workflow-state"
934
821
  export { createWorktreeManagerTool } from "./tools/worktree-manager"
935
822
  export { createReviewRouterTool } from "./tools/review-router"
936
- export { createParallelSubagentTool } from "./tools/parallel-subagent"
937
823
  export { createSessionCheckpointTool } from "./tools/session-checkpoint"
938
824
  export { createTaskSplitterTool } from "./tools/task-splitter"
939
825
  export { createBrainstormDialogTool } from "./tools/brainstorm-dialog"
@@ -0,0 +1,162 @@
1
+ import {
2
+ Container,
3
+ Key,
4
+ matchesKey,
5
+ truncateToWidth,
6
+ wrapTextWithAnsi,
7
+ } from "@earendil-works/pi-tui"
8
+
9
+ /**
10
+ * Result shape returned by the scrollable ask_user_question custom UI.
11
+ *
12
+ * - `selectedLabel`: the normalized display label the user picked (may be the
13
+ * custom sentinel), or `null` when cancelled.
14
+ * - The caller maps `selectedLabel` back to the original full option string.
15
+ */
16
+ export interface AskUserQuestionSelection {
17
+ selectedLabel: string | null
18
+ }
19
+
20
+ interface AskUserQuestionCustomOptions {
21
+ question: string
22
+ displayOptions: string[]
23
+ customLabel: string | null
24
+ }
25
+
26
+ /**
27
+ * Maximum number of option/question rows rendered before the viewport scrolls.
28
+ * Keeps the dialog inside typical terminal heights while still being readable.
29
+ */
30
+ const MAX_VISIBLE_ROWS = 10
31
+
32
+ /**
33
+ * Render the question body as wrapped lines so long questions stay readable
34
+ * inside the scrollable custom dialog.
35
+ */
36
+ function renderQuestionLines(question: string, width: number, fg: (color: string, text: string) => string): string[] {
37
+ const lines = wrapTextWithAnsi(question, width)
38
+ return lines.map((line) => truncateToWidth(fg("text", line), width))
39
+ }
40
+
41
+ /**
42
+ * Scrollable selector component for `ask_user_question`.
43
+ *
44
+ * Renders a wrapped (multi-line) question and a list of single-line options.
45
+ * Handles:
46
+ * - up/down navigation with scroll offset
47
+ * - Enter to confirm
48
+ * - Escape / cancel keybinding to cancel
49
+ * - optional "Other" custom entry label
50
+ *
51
+ * Built to work inside Pi's `ctx.ui.custom()` factory contract:
52
+ * `(tui, theme, keybindings, done) => Component`.
53
+ */
54
+ export class AskUserQuestionSelector extends Container {
55
+ private readonly options: string[]
56
+ private readonly question: string
57
+ private readonly customLabel: string | null
58
+ private readonly theme: { fg: (color: string, text: string) => string }
59
+ private readonly done: (result: AskUserQuestionSelection) => void
60
+ private selectedIndex = 0
61
+ private scrollOffset = 0
62
+ private resolved = false
63
+
64
+ constructor(
65
+ opts: AskUserQuestionCustomOptions,
66
+ theme: { fg: (color: string, text: string) => string },
67
+ done: (result: AskUserQuestionSelection) => void,
68
+ ) {
69
+ super()
70
+ this.options = opts.displayOptions
71
+ this.question = opts.question
72
+ this.customLabel = opts.customLabel
73
+ this.theme = theme
74
+ this.done = done
75
+ }
76
+
77
+ /** Resolve a selected display label into the result handed to `done()`. */
78
+ private commit(index: number): void {
79
+ if (this.resolved) return
80
+ this.resolved = true
81
+ const selected = this.options[index] ?? null
82
+ this.done({ selectedLabel: selected })
83
+ }
84
+
85
+ handleInput(data: string): void {
86
+ if (matchesKey(data, Key.up) || data === "k") {
87
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1)
88
+ this.scrollOffset = Math.min(this.scrollOffset, this.selectedIndex)
89
+ } else if (matchesKey(data, Key.down) || data === "j") {
90
+ this.selectedIndex = Math.min(this.options.length - 1, this.selectedIndex + 1)
91
+ const maxStart = Math.max(0, this.selectedIndex - MAX_VISIBLE_ROWS + 1)
92
+ this.scrollOffset = Math.max(this.scrollOffset, maxStart)
93
+ } else if (matchesKey(data, Key.enter) || data === "\n") {
94
+ this.commit(this.selectedIndex)
95
+ } else if (matchesKey(data, Key.escape)) {
96
+ if (this.resolved) return
97
+ this.resolved = true
98
+ this.done({ selectedLabel: null })
99
+ }
100
+ }
101
+
102
+ render(width: number): string[] {
103
+ const lines: string[] = []
104
+ const fg = this.theme.fg
105
+ const innerWidth = Math.max(20, width - 2)
106
+
107
+ lines.push(truncateToWidth(fg("accent", "─".repeat(width)), width))
108
+ for (const line of renderQuestionLines(this.question, innerWidth, fg)) {
109
+ lines.push(` ${line}`)
110
+ }
111
+ lines.push("")
112
+
113
+ const visible = this.options.slice(
114
+ this.scrollOffset,
115
+ this.scrollOffset + MAX_VISIBLE_ROWS,
116
+ )
117
+ for (let i = 0; i < visible.length; i++) {
118
+ const optionIndex = this.scrollOffset + i
119
+ const isSelected = optionIndex === this.selectedIndex
120
+ const isCustom = this.customLabel !== null && visible[i] === this.customLabel
121
+ const prefix = isSelected ? fg("accent", "→ ") : " "
122
+ const marker = isCustom ? fg("muted", " ✎") : ""
123
+ lines.push(truncateToWidth(`${prefix}${fg(isSelected ? "accent" : "text", visible[i])}${marker}`, width))
124
+ }
125
+
126
+ if (this.options.length > MAX_VISIBLE_ROWS) {
127
+ lines.push(
128
+ truncateToWidth(
129
+ fg("muted", ` (${this.selectedIndex + 1}/${this.options.length})`),
130
+ width,
131
+ ),
132
+ )
133
+ }
134
+
135
+ lines.push("")
136
+ lines.push(
137
+ truncateToWidth(
138
+ fg("dim", " ↑↓ navigate · Enter select · Esc cancel"),
139
+ width,
140
+ ),
141
+ )
142
+ lines.push(truncateToWidth(fg("accent", "─".repeat(width)), width))
143
+ return lines
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Build the factory passed to `ctx.ui.custom()`.
149
+ *
150
+ * Returns either a normalized display label (the user picked an option / custom
151
+ * sentinel) or `null` when the user cancelled.
152
+ */
153
+ export function createAskUserQuestionCustomFactory(opts: AskUserQuestionCustomOptions) {
154
+ return (
155
+ _tui: unknown,
156
+ theme: { fg: (color: string, text: string) => string },
157
+ _keybindings: unknown,
158
+ done: (result: AskUserQuestionSelection) => void,
159
+ ): AskUserQuestionSelector => {
160
+ return new AskUserQuestionSelector(opts, theme, done)
161
+ }
162
+ }