@leing2021/super-pi 0.23.8 → 0.23.10

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
@@ -118,6 +118,7 @@ Super Pi is not a fork or wrapper. It extracts useful methods from the projects
118
118
  |---------|------------------------|
119
119
  | [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
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. |
121
+ | [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. |
121
122
  | [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. |
122
123
  | [compound-engineering-plugin](https://github.com/EveryInc/compound-engineering-plugin) | The five-step think → plan → build → review → learn loop and the knowledge-compounding backbone. |
123
124
  | [gstack](https://github.com/garrytan/gstack) | YC-style forcing questions, CEO Review cognitive frameworks, browser QA patterns, failure maps, and evidence-first validation. |
@@ -19,10 +19,12 @@ import { createContextHandoffTool } from "./tools/context-handoff"
19
19
  import { filterBashOutput } from "./tools/bash-output-filter"
20
20
  import { filterReadOutput } from "./tools/read-output-filter"
21
21
  import { COMPACTION_FOCUS_INSTRUCTIONS } from "./tools/compaction-optimizer"
22
- import { AsyncMutex } from "./tools/async-mutex"
23
- import type { SubagentExecOptions, SubagentRunner } from "./tools/subagent"
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"
24
27
 
25
- const _subagentEnvMutex = new AsyncMutex()
26
28
  const PIPELINE_STAGE_KEYS = new Set([
27
29
  "01-brainstorm",
28
30
  "02-plan",
@@ -115,43 +117,30 @@ function parseModelRef(
115
117
  }
116
118
 
117
119
  /**
118
- * Create a subagent runner that handles env injection with mutex protection
119
- * for concurrency safety when multiple parallel subagents share the process.
120
+ * Create a spawn-based subagent runner that uses pi --mode json
121
+ * with per-process env (no global process.env mutation).
120
122
  */
121
123
  function createSubagentRunner(
122
124
  pi: ExtensionAPI,
123
125
  signal?: AbortSignal,
124
- ): SubagentRunner {
125
- return async (prompt: string, options?: SubagentExecOptions) => {
126
- const args = ["--no-session", ...(options?.extraFlags ?? []), "-p", prompt]
127
- const release = await _subagentEnvMutex.acquire()
128
- const savedEnv: Record<string, string | undefined> = {}
129
- const extraEnv = options?.extraEnv ?? {}
130
- for (const [key, value] of Object.entries(extraEnv)) {
131
- savedEnv[key] = process.env[key]
132
- process.env[key] = value
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.cwd ?? process.cwd(),
134
+ extraFlags: options?.extraFlags,
135
+ extraEnv: options?.extraEnv,
136
+ signal,
137
+ onUpdate: options?.onUpdate,
133
138
  }
134
- try {
135
- const execResult = await pi.exec("pi", args, {
136
- signal,
137
- timeout: 10 * 60 * 1000,
138
- })
139
-
140
- if (execResult.code !== 0) {
141
- throw new Error(execResult.stderr || execResult.stdout || `Subagent failed for prompt: ${prompt}`)
142
- }
143
-
144
- return (execResult.stdout || "").trim()
145
- } finally {
146
- for (const [key, oldValue] of Object.entries(savedEnv)) {
147
- if (oldValue === undefined) {
148
- delete process.env[key]
149
- } else {
150
- process.env[key] = oldValue
151
- }
152
- }
153
- release()
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}`)
154
142
  }
143
+ return result
155
144
  }
156
145
  }
157
146
 
@@ -505,7 +494,7 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
505
494
  label: "CE Subagent",
506
495
  description: "Run a single CE skill-based subagent or a serial chain in an isolated Pi process.",
507
496
  parameters: subagentParams,
508
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
497
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
509
498
  const result = await subagent.execute(
510
499
  {
511
500
  agent: params.agent,
@@ -514,13 +503,26 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
514
503
  inheritSkills: params.inheritSkills,
515
504
  },
516
505
  createSubagentRunner(pi, signal),
506
+ { onUpdate },
517
507
  )
518
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
+
519
514
  return {
520
- content: [{ type: "text", text: result.outputs[result.outputs.length - 1] ?? "" }],
515
+ content: [{ type: "text", text: finalText || "(no output)" }],
521
516
  details: result,
517
+ isError: isError || undefined,
522
518
  }
523
519
  },
520
+ renderCall(args, theme, _context) {
521
+ return renderSubagentCall(args, theme)
522
+ },
523
+ renderResult(result, renderContext, theme, _context) {
524
+ return renderSubagentResult(result.details as SubagentLiveDetails, renderContext, theme)
525
+ },
524
526
  })
525
527
 
526
528
  pi.registerTool({
@@ -599,7 +601,7 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
599
601
  label: "CE Parallel Subagent",
600
602
  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.",
601
603
  parameters: parallelSubagentParams,
602
- async execute(_toolCallId, params, signal) {
604
+ async execute(_toolCallId, params, signal, onUpdate) {
603
605
  let tasks: any[]
604
606
  if (typeof params.tasks === "string") {
605
607
  try {
@@ -618,13 +620,32 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
618
620
  inheritSkills: params.inheritSkills,
619
621
  },
620
622
  createSubagentRunner(pi, signal),
623
+ { onUpdate },
621
624
  )
622
625
 
626
+ // Build summary content
627
+ const successCount = result.results.filter(r => r.exitCode === 0).length
628
+ const failCount = result.results.filter(r => isFailedResult(r)).length
629
+ const summaries = result.results.map(r => {
630
+ const output = getFinalOutput(r.messages) || r.errorMessage || r.stderr || "(no output)"
631
+ const status = isFailedResult(r) ? "failed" : "completed"
632
+ return `### [${r.agent}] ${status}\n\n${output}`
633
+ })
634
+
623
635
  return {
624
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
636
+ content: [{
637
+ type: "text",
638
+ text: `Parallel: ${successCount}/${result.results.length} succeeded${failCount > 0 ? `, ${failCount} failed` : ""}\n\n${summaries.join("\n\n---\n\n")}`,
639
+ }],
625
640
  details: result,
626
641
  }
627
642
  },
643
+ renderCall(args, theme, _context) {
644
+ return renderSubagentCall(args, theme)
645
+ },
646
+ renderResult(result, renderContext, theme, _context) {
647
+ return renderSubagentResult(result.details as SubagentLiveDetails, renderContext, theme)
648
+ },
628
649
  })
629
650
 
630
651
  pi.registerTool({
@@ -873,6 +894,24 @@ export { createAskUserQuestionTool } from "./tools/ask-user-question"
873
894
  export { createSubagentTool } from "./tools/subagent"
874
895
  export { checkSubagentDepth, getChildDepthEnv, DEFAULT_MAX_SUBAGENT_DEPTH } from "./tools/subagent-depth-guard"
875
896
  export { AsyncMutex } from "./tools/async-mutex"
897
+ export { createJsonRunner } from "./tools/subagent-json-runner"
898
+ export {
899
+ parseJsonEvent,
900
+ applyEventToResult,
901
+ type SingleResult as SingleResultType,
902
+ type UsageStats,
903
+ type ParsedEvent,
904
+ isFailedResult as isFailedSingleResult,
905
+ formatUsageStats,
906
+ makeInitialResult,
907
+ getFinalOutput as getFinalOutputFromMessages,
908
+ getDisplayItems,
909
+ } from "./tools/subagent-events"
910
+ export {
911
+ formatToolCall,
912
+ renderSubagentCall,
913
+ renderSubagentResult,
914
+ } from "./tools/subagent-renderer"
876
915
  export { createWorkflowStateTool } from "./tools/workflow-state"
877
916
  export { createWorktreeManagerTool } from "./tools/worktree-manager"
878
917
  export { createReviewRouterTool } from "./tools/review-router"
@@ -4,13 +4,63 @@
4
4
  * Features:
5
5
  * - Recursion depth guard (PI_SUBAGENT_DEPTH / PI_SUBAGENT_MAX_DEPTH)
6
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
7
10
  */
8
11
 
9
12
  import {
10
13
  checkSubagentDepth,
11
14
  getChildDepthEnv,
12
15
  } from "./subagent-depth-guard"
13
- import { assertNonPipelineStageSkill, type SubagentExecOptions, type SubagentRunner } from "./subagent"
16
+ import {
17
+ type SingleResult,
18
+ makeInitialResult,
19
+ isFailedResult,
20
+ getFinalOutput,
21
+ makeFailedResult,
22
+ invokeRunner,
23
+ } from "./subagent-events"
24
+ import {
25
+ assertNonPipelineStageSkill,
26
+ type SubagentLiveRunner,
27
+ type SubagentLiveExecOptions,
28
+ } from "./subagent"
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Constants (from pi official example)
32
+ // ---------------------------------------------------------------------------
33
+
34
+ const MAX_PARALLEL_TASKS = 8
35
+ const MAX_CONCURRENCY = 4
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // mapWithConcurrencyLimit (directly from pi official subagent example)
39
+ // ---------------------------------------------------------------------------
40
+
41
+ async function mapWithConcurrencyLimit<TIn, TOut>(
42
+ items: TIn[],
43
+ concurrency: number,
44
+ fn: (item: TIn, index: number) => Promise<TOut>,
45
+ ): Promise<TOut[]> {
46
+ if (items.length === 0) return []
47
+ const limit = Math.max(1, Math.min(concurrency, items.length))
48
+ const results: TOut[] = new Array(items.length)
49
+ let nextIndex = 0
50
+ const workers = new Array(limit).fill(null).map(async () => {
51
+ while (true) {
52
+ const current = nextIndex++
53
+ if (current >= items.length) return
54
+ results[current] = await fn(items[current], current)
55
+ }
56
+ })
57
+ await Promise.all(workers)
58
+ return results
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Public types
63
+ // ---------------------------------------------------------------------------
14
64
 
15
65
  export interface ParallelSubagentTask {
16
66
  agent: string
@@ -23,25 +73,24 @@ export interface ParallelSubagentInput {
23
73
  inheritSkills?: boolean
24
74
  }
25
75
 
26
- export interface ParallelResultItem {
27
- status: "fulfilled" | "rejected"
28
- value?: string
29
- reason?: string
30
- }
31
-
32
- export interface ParallelSubagentResult {
76
+ export interface ParallelSubagentLiveDetails {
33
77
  mode: "parallel"
34
- outputs: ParallelResultItem[]
78
+ results: SingleResult[]
35
79
  }
36
80
 
37
- function buildPrompt(agent: string, task: string): string {
38
- return `/skill:${agent} ${task}`
39
- }
81
+ // ---------------------------------------------------------------------------
82
+ // Tool factory
83
+ // ---------------------------------------------------------------------------
40
84
 
41
85
  export function createParallelSubagentTool() {
42
86
  return {
43
- name: "ce_parallel_subagent",
44
- async execute(input: ParallelSubagentInput, runner: SubagentRunner): Promise<ParallelSubagentResult> {
87
+ name: "ce_parallel_subagent" as const,
88
+
89
+ async execute(
90
+ input: ParallelSubagentInput,
91
+ runner: SubagentLiveRunner,
92
+ toolCtx?: { onUpdate?: (details: ParallelSubagentLiveDetails) => void },
93
+ ): Promise<ParallelSubagentLiveDetails> {
45
94
  // Recursion depth guard
46
95
  const depthCheck = checkSubagentDepth()
47
96
  if (!depthCheck.allowed) {
@@ -52,49 +101,73 @@ export function createParallelSubagentTool() {
52
101
  throw new Error("ce_parallel_subagent requires at least one task")
53
102
  }
54
103
 
104
+ if (input.tasks.length > MAX_PARALLEL_TASKS) {
105
+ throw new Error(
106
+ `Too many parallel tasks (${input.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
107
+ )
108
+ }
109
+
55
110
  for (const task of input.tasks) {
56
111
  assertNonPipelineStageSkill(task.agent, "ce_parallel_subagent")
57
112
  }
58
113
 
59
- // Default: parallel workers get a slim context (no inherited skills)
60
- const inheritSkills = input.inheritSkills ?? false
61
- const execOptions = buildParallelExecOptions(inheritSkills)
114
+ const execOptions = buildParallelExecOptions(input.inheritSkills)
62
115
 
63
- const promises = input.tasks.map(({ agent, task }) =>
64
- runner(buildPrompt(agent, task), execOptions),
116
+ // Initialize all results as running placeholders
117
+ const allResults: SingleResult[] = input.tasks.map(({ agent, task }) =>
118
+ makeInitialResult(agent, task),
65
119
  )
66
120
 
67
- const settled = await Promise.allSettled(promises)
121
+ // Emit initial state (all running)
122
+ emitParallelUpdate(allResults, toolCtx)
123
+
124
+ // Run tasks with concurrency limit
125
+ await mapWithConcurrencyLimit(input.tasks, MAX_CONCURRENCY, async (t, index) => {
126
+ const liveOptions: SubagentLiveExecOptions = {
127
+ ...execOptions,
128
+ onUpdate: (partial: SingleResult) => {
129
+ allResults[index] = partial
130
+ emitParallelUpdate(allResults, toolCtx)
131
+ },
132
+ }
68
133
 
69
- const outputs: ParallelResultItem[] = settled.map((result) => {
70
- if (result.status === "fulfilled") {
71
- return { status: "fulfilled", value: result.value }
134
+ try {
135
+ const result = await invokeRunner(runner, `/skill:${t.agent} ${t.task}`, liveOptions)
136
+ allResults[index] = result
137
+ } catch (e) {
138
+ // Preserve allSettled semantics: one failure does not cancel others
139
+ allResults[index] = makeFailedResult(t.agent, t.task, e instanceof Error ? e.message : String(e))
72
140
  }
73
- return { status: "rejected", reason: result.reason?.message || String(result.reason) }
141
+ emitParallelUpdate(allResults, toolCtx)
142
+ return allResults[index]
74
143
  })
75
144
 
76
- return {
77
- mode: "parallel",
78
- outputs,
79
- }
145
+ return { mode: "parallel", results: allResults }
80
146
  },
81
147
  }
82
148
  }
83
149
 
84
- /**
85
- * Build exec options for parallel subagent workers.
86
- * Defaults to slim context (no inherited skills).
87
- */
88
- function buildParallelExecOptions(inheritSkills: boolean): SubagentExecOptions {
150
+ // ---------------------------------------------------------------------------
151
+ // Helpers
152
+ // ---------------------------------------------------------------------------
153
+
154
+ function emitParallelUpdate(
155
+ results: SingleResult[],
156
+ toolCtx?: { onUpdate?: (details: ParallelSubagentLiveDetails) => void },
157
+ ) {
158
+ if (toolCtx?.onUpdate) {
159
+ toolCtx.onUpdate({ mode: "parallel", results: [...results] })
160
+ }
161
+ }
162
+
163
+ function buildParallelExecOptions(inheritSkills?: boolean): { extraFlags?: string[]; extraEnv: Record<string, string> } {
89
164
  const flags: string[] = []
90
165
  const env: Record<string, string> = {}
91
166
 
92
- // Context slimming
93
- if (!inheritSkills) {
167
+ if (inheritSkills !== true) {
94
168
  flags.push("--no-skills")
95
169
  }
96
170
 
97
- // Recursion depth
98
171
  Object.assign(env, getChildDepthEnv())
99
172
 
100
173
  return {
@@ -102,3 +175,20 @@ function buildParallelExecOptions(inheritSkills: boolean): SubagentExecOptions {
102
175
  extraEnv: env,
103
176
  }
104
177
  }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Legacy backward-compat types and exports
181
+ // ---------------------------------------------------------------------------
182
+
183
+ /** @deprecated Use ParallelSubagentLiveDetails instead */
184
+ export interface ParallelResultItem {
185
+ status: "fulfilled" | "rejected"
186
+ value?: string
187
+ reason?: string
188
+ }
189
+
190
+ /** @deprecated Use ParallelSubagentLiveDetails instead */
191
+ export interface ParallelSubagentResult {
192
+ mode: "parallel"
193
+ outputs: ParallelResultItem[]
194
+ }