@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 +14 -22
- package/extensions/ce-core/index.ts +89 -203
- package/extensions/ce-core/tools/ask-user-question-ui.ts +162 -0
- package/extensions/ce-core/tools/ask-user-question.ts +78 -5
- package/package.json +3 -3
- package/skills/02-plan/references/ceo-review-mode.md +1 -1
- package/skills/03-work/SKILL.md +5 -7
- package/extensions/ce-core/tools/async-mutex.ts +0 -28
- package/extensions/ce-core/tools/parallel-subagent.ts +0 -196
- package/extensions/ce-core/tools/subagent-depth-guard.ts +0 -86
- package/extensions/ce-core/tools/subagent-events.ts +0 -287
- package/extensions/ce-core/tools/subagent-json-runner.ts +0 -175
- package/extensions/ce-core/tools/subagent-renderer.ts +0 -478
- package/extensions/ce-core/tools/subagent.ts +0 -209
- package/skills/08-help/SKILL.md +0 -46
- package/skills/08-help/references/workflow-sequence.md +0 -135
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.
|
|
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** — ~
|
|
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
|
|
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
|
|
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) |
|
|
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: **~
|
|
148
|
+
New conversation overhead: **~4,130 tokens** (2.1% of 200K context).
|
|
157
149
|
|
|
158
150
|
| Component | Tokens |
|
|
159
151
|
|-----------|--------|
|
|
160
|
-
|
|
|
161
|
-
|
|
|
162
|
-
| Skill inlining (per invocation) | ~
|
|
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 |
|
|
195
|
-
| Tools |
|
|
196
|
-
| Rules |
|
|
197
|
-
| TypeScript lines | ~4,
|
|
198
|
-
| Tests |
|
|
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 "@
|
|
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 {
|
|
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 (
|
|
379
|
+
if (shouldNotify) {
|
|
366
380
|
ctx.ui.notify(`Switched model for ${stageKey}: ${model.provider}/${model.id}`, "info")
|
|
367
381
|
}
|
|
368
382
|
} else {
|
|
369
|
-
if (
|
|
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 (
|
|
387
|
+
} else if (shouldNotify) {
|
|
374
388
|
ctx.ui.notify(`Model not found for ${stageKey}: ${targetModelRef}`, "warning")
|
|
375
389
|
}
|
|
376
390
|
}
|
|
377
|
-
} else if (
|
|
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 (
|
|
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
|
-
|
|
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
|
+
}
|