@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
|
@@ -14,7 +14,70 @@ export interface AskUserQuestionResult {
|
|
|
14
14
|
mode: "input" | "select" | "custom" | "cancelled"
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
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
|
|
37
|
-
const
|
|
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 ===
|
|
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 {
|
|
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.
|
|
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
|
-
"@
|
|
38
|
+
"@earendil-works/pi-coding-agent": ">=0.74.0",
|
|
39
39
|
"typebox": ">=1.0.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@
|
|
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
|
|
package/skills/03-work/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: 03-work
|
|
3
|
-
description: "Execute plan units with
|
|
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**
|
|
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
|
-
}
|