@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.
@@ -14,7 +14,70 @@ export interface AskUserQuestionResult {
14
14
  mode: "input" | "select" | "custom" | "cancelled"
15
15
  }
16
16
 
17
- const CUSTOM_OPTION = "Other"
17
+ /** Sentinel label used to offer a free-text custom answer. */
18
+ export const CUSTOM_SENTINEL = "Other"
19
+
20
+ /**
21
+ * Maximum display width for a normalized option label. Long labels overflow the
22
+ * selector row in the built-in `ctx.ui.select()` renderer (see
23
+ * `docs/bug/ask-user-question-long-options-truncated.md`), so labels are kept
24
+ * to a single line and truncated to this width.
25
+ */
26
+ export const MAX_OPTION_LABEL_WIDTH = 60
27
+
28
+ /**
29
+ * Build the single-line display label for one option.
30
+ *
31
+ * Rules:
32
+ * - Take only the first line (drop embedded `\n`).
33
+ * - Trim surrounding whitespace.
34
+ * - Truncate to {@link MAX_OPTION_LABEL_WIDTH} with an ellipsis when needed.
35
+ */
36
+ export function toOptionDisplayLabel(option: string): string {
37
+ const firstLine = option.split("\n", 1)[0] ?? ""
38
+ const trimmed = firstLine.trim()
39
+ if (trimmed.length <= MAX_OPTION_LABEL_WIDTH) {
40
+ return trimmed
41
+ }
42
+ return trimmed.slice(0, MAX_OPTION_LABEL_WIDTH - 1) + "…"
43
+ }
44
+
45
+ /**
46
+ * Build display labels for all options, disambiguating collisions with a
47
+ * numeric suffix so the selector never shows two identical rows.
48
+ *
49
+ * @returns A map from display label back to the original full option string.
50
+ * When collisions exist, labels become `<label> (#<n>)`.
51
+ */
52
+ export function normalizeQuestionOptions(options: string[]): Map<string, string> {
53
+ const labelToOriginal = new Map<string, string>()
54
+ const labelCounts = new Map<string, number>()
55
+
56
+ for (const original of options) {
57
+ const baseLabel = toOptionDisplayLabel(original)
58
+ const count = (labelCounts.get(baseLabel) ?? 0) + 1
59
+ labelCounts.set(baseLabel, count)
60
+
61
+ const label = count === 1 ? baseLabel : `${baseLabel} (#${count})`
62
+ // Guard against the extremely unlikely suffix collision by re-checking.
63
+ const finalLabel = labelToOriginal.has(label) ? `${label} (#${count})` : label
64
+ labelToOriginal.set(finalLabel, original)
65
+ }
66
+
67
+ return labelToOriginal
68
+ }
69
+
70
+ /** Choose a display label for the custom-answer sentinel that never collides. */
71
+ export function resolveCustomSentinelLabel(labelToOriginal: Map<string, string>): string {
72
+ if (!labelToOriginal.has(CUSTOM_SENTINEL)) {
73
+ return CUSTOM_SENTINEL
74
+ }
75
+ let index = 2
76
+ while (labelToOriginal.has(`${CUSTOM_SENTINEL} (#${index})`)) {
77
+ index += 1
78
+ }
79
+ return `${CUSTOM_SENTINEL} (#${index})`
80
+ }
18
81
 
19
82
  export function createAskUserQuestionTool() {
20
83
  return {
@@ -33,21 +96,31 @@ export function createAskUserQuestionTool() {
33
96
  }
34
97
 
35
98
  const allowCustom = input.allowCustom ?? true
36
- const selectOptions = allowCustom ? [...options, CUSTOM_OPTION] : options
37
- const selected = await ui.select(input.question, selectOptions)
99
+ const labelToOriginal = normalizeQuestionOptions(options)
100
+ const customLabel = allowCustom
101
+ ? resolveCustomSentinelLabel(labelToOriginal)
102
+ : null
103
+ const displayOptions = customLabel
104
+ ? [...labelToOriginal.keys(), customLabel]
105
+ : [...labelToOriginal.keys()]
106
+
107
+ const selected = await ui.select(input.question, displayOptions)
38
108
 
39
109
  if (selected === null) {
40
110
  return { answer: null, mode: "cancelled" }
41
111
  }
42
112
 
43
- if (allowCustom && selected === CUSTOM_OPTION) {
113
+ if (allowCustom && customLabel && selected === customLabel) {
44
114
  const customAnswer = await ui.input("Your answer")
45
115
  return customAnswer === null
46
116
  ? { answer: null, mode: "cancelled" }
47
117
  : { answer: customAnswer, mode: "custom" }
48
118
  }
49
119
 
50
- return { answer: selected, mode: "select" }
120
+ return {
121
+ answer: labelToOriginal.get(selected) ?? selected,
122
+ mode: "select",
123
+ }
51
124
  },
52
125
  }
53
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leing2021/super-pi",
3
- "version": "0.23.13",
3
+ "version": "0.25.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Pi-native Compound Engineering package for iterative development workflows",
@@ -35,11 +35,11 @@
35
35
  "test": "bun test"
36
36
  },
37
37
  "peerDependencies": {
38
- "@mariozechner/pi-coding-agent": ">=0.69.0",
38
+ "@earendil-works/pi-coding-agent": ">=0.74.0",
39
39
  "typebox": ">=1.0.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@mariozechner/pi-coding-agent": "0.69.0",
42
+ "@earendil-works/pi-coding-agent": "^0.76.0",
43
43
  "bun-types": "^1.3.12"
44
44
  },
45
45
  "pi": {
@@ -23,7 +23,7 @@ Re-examine the plan's assumptions:
23
23
  - What is the actual user/business outcome? Is the plan the most direct path?
24
24
  - What happens if we do nothing? Real pain or hypothetical?
25
25
 
26
- Present findings as premises the user must agree with. Use `ask_user_question` for each.
26
+ Present findings as premises the user must agree with. Use `ask_user_question` for each — **one at a time, sequentially** (never two `ask_user_question` calls in the same assistant message; Pi selectors are serialized and parallel calls silently fail).
27
27
 
28
28
  ### 2. Dream State Mapping
29
29
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: 03-work
3
- description: "Execute plan units with parallel subagents, TDD enforcement, and checkpoint resume. Use when a plan path is ready for implementation."
3
+ description: "Execute plan units with TDD enforcement and checkpoint resume. Use when a plan path is ready for implementation."
4
4
  ---
5
5
 
6
6
  # Work
@@ -19,16 +19,14 @@ See [shared pipeline instructions](../references/pipeline-config.md) for model r
19
19
  2. **Priority:** project-level `{repo-root}/rules/` overrides package defaults
20
20
  3. **Distinguish input:** plan path vs bare prompt
21
21
  4. Derive tasks from plan **implementation units**
22
- 5. **Execution mode:**
23
- - **Inline mode** is the default execution path
24
- - **`ce_parallel_subagent`** only for bounded, non-interactive, easily verifiable leaf tasks
25
- - **`ce_subagent`** only for rare dependent serial chains of leaf tasks
26
- - Never use either tool to invoke pipeline-stage skills (`01-brainstorm through 05-learn`). Run those stages directly with `/skill:<stage>`.
22
+ 5. **Execution mode:** **inline mode** — all plan units execute inline in the current session. No built-in subagent tools.
27
23
  6. Use **`session_checkpoint`** to track progress and enable resume
28
24
  7. Use **`task_splitter`** to analyze dependencies before execution
29
25
  8. If in **worktree** (via `07-worktree`), execute inside it
30
26
  9. End by recommending `04-review`
31
27
 
28
+ > **Advanced:** If you need external child agent delegation (background runs, parallel audits), install `pi-subagents` separately. Super Pi does not require it.
29
+
32
30
  ## Hard gates — TDD enforcement
33
31
 
34
32
  Every step follows **RED → GREEN → REFACTOR**:
@@ -76,7 +74,7 @@ If the same tool, command, or implementation unit fails 3 consecutive times, sto
76
74
  3. Read implementation units if plan path
77
75
  4. Load `session_checkpoint` to skip completed units
78
76
  5. Use `task_splitter` for dependency analysis
79
- 6. Execute: **inline mode** by default; use subagents only for bounded, non-interactive, easily verifiable leaf tasks
77
+ 6. Execute: **inline mode** all units run in the current session
80
78
  7. Follow TDD per unit: RED → minimal code → GREEN → refactor → unit-level **verification**
81
79
  8. **Source-driven gate:** Before implementing framework/library-specific code, verify the API or pattern against official documentation. Flag unverified patterns as UNVERIFIED in output.
82
80
  9. Record progress via `references/progress-update-format.md`
@@ -1,28 +0,0 @@
1
- /**
2
- * Async mutex for serializing critical sections that mutate shared state
3
- * (e.g. process.env) across concurrent async operations.
4
- */
5
- export class AsyncMutex {
6
- private queue: (() => void)[] = []
7
- private locked = false
8
-
9
- async acquire(): Promise<() => void> {
10
- return new Promise<() => void>((resolve) => {
11
- const tryAcquire = () => {
12
- if (!this.locked) {
13
- this.locked = true
14
- resolve(() => this.release())
15
- } else {
16
- this.queue.push(tryAcquire)
17
- }
18
- }
19
- tryAcquire()
20
- })
21
- }
22
-
23
- private release(): void {
24
- this.locked = false
25
- const next = this.queue.shift()
26
- if (next) next()
27
- }
28
- }
@@ -1,196 +0,0 @@
1
- /**
2
- * CE parallel subagent tool — runs multiple CE skill-based subagent tasks concurrently.
3
- *
4
- * Features:
5
- * - Recursion depth guard (PI_SUBAGENT_DEPTH / PI_SUBAGENT_MAX_DEPTH)
6
- * - Optional context slimming via --no-skills flag
7
- * - Concurrency limit via mapWithConcurrencyLimit (from pi official example)
8
- * - Live TUI updates with running placeholders and per-task status
9
- * - Promise.allSettled semantics: one failure does NOT cancel others
10
- */
11
-
12
- import {
13
- checkSubagentDepth,
14
- getChildDepthEnv,
15
- } from "./subagent-depth-guard"
16
- import {
17
- type SingleResult,
18
- makeInitialResult,
19
- isFailedResult,
20
- getFinalOutput,
21
- makeFailedResult,
22
- invokeRunner,
23
- type AnyRunner,
24
- } from "./subagent-events"
25
- import {
26
- assertNonPipelineStageSkill,
27
- type SubagentLiveRunner,
28
- type SubagentLiveExecOptions,
29
- type SubagentRunner,
30
- } from "./subagent"
31
-
32
- // ---------------------------------------------------------------------------
33
- // Constants (from pi official example)
34
- // ---------------------------------------------------------------------------
35
-
36
- const MAX_PARALLEL_TASKS = 8
37
- const MAX_CONCURRENCY = 4
38
-
39
- // ---------------------------------------------------------------------------
40
- // mapWithConcurrencyLimit (directly from pi official subagent example)
41
- // ---------------------------------------------------------------------------
42
-
43
- async function mapWithConcurrencyLimit<TIn, TOut>(
44
- items: TIn[],
45
- concurrency: number,
46
- fn: (item: TIn, index: number) => Promise<TOut>,
47
- ): Promise<TOut[]> {
48
- if (items.length === 0) return []
49
- const limit = Math.max(1, Math.min(concurrency, items.length))
50
- const results: TOut[] = new Array(items.length)
51
- let nextIndex = 0
52
- const workers = new Array(limit).fill(null).map(async () => {
53
- while (true) {
54
- const current = nextIndex++
55
- if (current >= items.length) return
56
- results[current] = await fn(items[current], current)
57
- }
58
- })
59
- await Promise.all(workers)
60
- return results
61
- }
62
-
63
- // ---------------------------------------------------------------------------
64
- // Public types
65
- // ---------------------------------------------------------------------------
66
-
67
- export interface ParallelSubagentTask {
68
- agent: string
69
- task: string
70
- }
71
-
72
- export interface ParallelSubagentInput {
73
- tasks: ParallelSubagentTask[]
74
- /** Whether subagents should inherit skills. Default: true */
75
- inheritSkills?: boolean
76
- }
77
-
78
- export interface ParallelSubagentLiveDetails {
79
- mode: "parallel"
80
- results: SingleResult[]
81
- }
82
-
83
- // ---------------------------------------------------------------------------
84
- // Tool factory
85
- // ---------------------------------------------------------------------------
86
-
87
- export function createParallelSubagentTool() {
88
- return {
89
- name: "ce_parallel_subagent" as const,
90
-
91
- async execute(
92
- input: ParallelSubagentInput,
93
- runner: SubagentLiveRunner | SubagentRunner,
94
- toolCtx?: { onUpdate?: (details: ParallelSubagentLiveDetails) => void },
95
- ): Promise<ParallelSubagentLiveDetails> {
96
- // Recursion depth guard
97
- const depthCheck = checkSubagentDepth()
98
- if (!depthCheck.allowed) {
99
- throw new Error(depthCheck.reason!)
100
- }
101
-
102
- if (!input.tasks || input.tasks.length === 0) {
103
- throw new Error("ce_parallel_subagent requires at least one task")
104
- }
105
-
106
- if (input.tasks.length > MAX_PARALLEL_TASKS) {
107
- throw new Error(
108
- `Too many parallel tasks (${input.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
109
- )
110
- }
111
-
112
- for (const task of input.tasks) {
113
- assertNonPipelineStageSkill(task.agent, "ce_parallel_subagent")
114
- }
115
-
116
- const execOptions = buildParallelExecOptions(input.inheritSkills)
117
-
118
- // Initialize all results as running placeholders
119
- const allResults: SingleResult[] = input.tasks.map(({ agent, task }) =>
120
- makeInitialResult(agent, task),
121
- )
122
-
123
- // Emit initial state (all running)
124
- emitParallelUpdate(allResults, toolCtx)
125
-
126
- // Run tasks with concurrency limit
127
- await mapWithConcurrencyLimit(input.tasks, MAX_CONCURRENCY, async (t, index) => {
128
- const liveOptions: SubagentLiveExecOptions = {
129
- ...execOptions,
130
- onUpdate: (partial: SingleResult) => {
131
- allResults[index] = partial
132
- emitParallelUpdate(allResults, toolCtx)
133
- },
134
- }
135
-
136
- try {
137
- const result = await invokeRunner(runner, `/skill:${t.agent} ${t.task}`, liveOptions)
138
- allResults[index] = result
139
- } catch (e) {
140
- // Preserve allSettled semantics: one failure does not cancel others
141
- allResults[index] = makeFailedResult(t.agent, t.task, e instanceof Error ? e.message : String(e))
142
- }
143
- emitParallelUpdate(allResults, toolCtx)
144
- return allResults[index]
145
- })
146
-
147
- return { mode: "parallel", results: allResults }
148
- },
149
- }
150
- }
151
-
152
- // ---------------------------------------------------------------------------
153
- // Helpers
154
- // ---------------------------------------------------------------------------
155
-
156
- function emitParallelUpdate(
157
- results: SingleResult[],
158
- toolCtx?: { onUpdate?: (details: ParallelSubagentLiveDetails) => void },
159
- ) {
160
- if (toolCtx?.onUpdate) {
161
- toolCtx.onUpdate({ mode: "parallel", results: [...results] })
162
- }
163
- }
164
-
165
- function buildParallelExecOptions(inheritSkills?: boolean): { extraFlags?: string[]; extraEnv: Record<string, string> } {
166
- const flags: string[] = []
167
- const env: Record<string, string> = {}
168
-
169
- if (inheritSkills === false) {
170
- flags.push("--no-skills")
171
- }
172
-
173
- Object.assign(env, getChildDepthEnv())
174
-
175
- return {
176
- extraFlags: flags.length > 0 ? flags : undefined,
177
- extraEnv: env,
178
- }
179
- }
180
-
181
- // ---------------------------------------------------------------------------
182
- // Legacy backward-compat types and exports
183
- // ---------------------------------------------------------------------------
184
-
185
- /** @deprecated Use ParallelSubagentLiveDetails instead */
186
- export interface ParallelResultItem {
187
- status: "fulfilled" | "rejected"
188
- value?: string
189
- reason?: string
190
- }
191
-
192
- /** @deprecated Use ParallelSubagentLiveDetails instead */
193
- export interface ParallelSubagentResult {
194
- mode: "parallel"
195
- outputs: ParallelResultItem[]
196
- }
@@ -1,86 +0,0 @@
1
- /**
2
- * Subagent recursion depth guard.
3
- *
4
- * Prevents uncontrolled recursive subagent spawning by tracking depth
5
- * via environment variables. Inspired by pi-subagents' depth control
6
- * but implemented as a minimal, standalone module.
7
- *
8
- * Environment variables:
9
- * PI_SUBAGENT_DEPTH — current nesting depth (default: 0)
10
- * PI_SUBAGENT_MAX_DEPTH — maximum allowed depth (default: 2)
11
- */
12
-
13
- /** Default maximum subagent nesting depth */
14
- export const DEFAULT_MAX_SUBAGENT_DEPTH = 2
15
-
16
- /**
17
- * Read the current subagent depth from environment.
18
- * Returns 0 if unset or invalid.
19
- */
20
- export function getCurrentDepth(): number {
21
- const raw = process.env.PI_SUBAGENT_DEPTH
22
- if (!raw) return 0
23
- const depth = parseInt(raw, 10)
24
- return Number.isNaN(depth) ? 0 : depth
25
- }
26
-
27
- /**
28
- * Read the maximum allowed depth from environment.
29
- * Falls back to DEFAULT_MAX_SUBAGENT_DEPTH if unset or invalid.
30
- */
31
- export function getMaxDepth(): number {
32
- const raw = process.env.PI_SUBAGENT_MAX_DEPTH
33
- if (!raw) return DEFAULT_MAX_SUBAGENT_DEPTH
34
- const max = parseInt(raw, 10)
35
- return Number.isNaN(max) || max < 0 ? DEFAULT_MAX_SUBAGENT_DEPTH : max
36
- }
37
-
38
- /**
39
- * Check whether subagent execution is allowed at the current depth.
40
- * Returns an object with:
41
- * allowed — whether execution can proceed
42
- * depth — the current depth
43
- * max — the maximum depth
44
- * reason — human-readable explanation if blocked
45
- */
46
- export function checkSubagentDepth(): {
47
- allowed: boolean
48
- depth: number
49
- max: number
50
- reason?: string
51
- } {
52
- const depth = getCurrentDepth()
53
- const max = getMaxDepth()
54
-
55
- if (depth >= max) {
56
- return {
57
- allowed: false,
58
- depth,
59
- max,
60
- reason: `Subagent recursion depth limit reached (depth=${depth}, max=${max}). ` +
61
- `Set PI_SUBAGENT_MAX_DEPTH to increase the limit.`,
62
- }
63
- }
64
-
65
- return { allowed: true, depth, max }
66
- }
67
-
68
- /**
69
- * Build environment variables to pass to a child subagent process.
70
- * Increments the depth counter so the child can guard its own spawns.
71
- *
72
- * @param overrides — optional overrides for max depth per-agent
73
- */
74
- export function getChildDepthEnv(overrides?: {
75
- maxDepth?: number
76
- }): Record<string, string> {
77
- const currentDepth = getCurrentDepth()
78
- const childDepth = currentDepth + 1
79
- let maxDepth = overrides?.maxDepth ?? getMaxDepth()
80
- if (maxDepth < 0) maxDepth = DEFAULT_MAX_SUBAGENT_DEPTH
81
-
82
- return {
83
- PI_SUBAGENT_DEPTH: String(childDepth),
84
- PI_SUBAGENT_MAX_DEPTH: String(maxDepth),
85
- }
86
- }