pi-code 1.0.2 → 1.0.4
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 +1 -1
- package/extensions/commands.ts +10 -3
- package/extensions/context-imports.ts +5 -2
- package/extensions/git-checkpoint.ts +22 -1
- package/extensions/hooks.ts +76 -20
- package/extensions/internal/command-file.ts +102 -23
- package/extensions/internal/output-guard.ts +12 -1
- package/extensions/internal/project-approval.ts +18 -1
- package/extensions/mcp.ts +90 -19
- package/extensions/memory.ts +39 -7
- package/extensions/notify.ts +1 -1
- package/extensions/plan-mode/index.ts +78 -11
- package/extensions/status-line.ts +4 -1
- package/extensions/subagent/agents.ts +10 -21
- package/extensions/subagent/background.ts +106 -22
- package/extensions/subagent/index.ts +39 -28
- package/extensions/todo.ts +2 -2
- package/extensions/web.ts +7 -1
- package/package.json +1 -1
|
@@ -28,7 +28,7 @@ import { isProjectApproved, isProjectApprovedSilently } from '../internal/projec
|
|
|
28
28
|
import { SUBAGENT_CHANNEL } from '../internal/subagent-events.js'
|
|
29
29
|
import { skillDirs } from '../skills.js'
|
|
30
30
|
import { type AgentConfig, type AgentScope, discoverAgents, resolveModelAlias, withPreloadedSkills } from './agents.js'
|
|
31
|
-
import { activeBackgroundRuns, backgroundStatusText, cancelBackgroundRun, MAX_BACKGROUND_RUNS, resumeBackgroundRun, startBackgroundRun } from './background.js'
|
|
31
|
+
import { activeBackgroundRuns, backgroundRun, backgroundStatusText, cancelBackgroundRun, MAX_BACKGROUND_RUNS, resumeBackgroundRun, startBackgroundRun } from './background.js'
|
|
32
32
|
|
|
33
33
|
const MAX_PARALLEL_TASKS = 8
|
|
34
34
|
const MAX_CONCURRENCY = 4
|
|
@@ -343,6 +343,9 @@ async function runSingleAgentInner(options: RunAgentOptions): Promise<SingleResu
|
|
|
343
343
|
cwd: cwd ?? defaultCwd,
|
|
344
344
|
shell: false,
|
|
345
345
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
346
|
+
// Its own group, so an abort reaches grandchildren too: killing only the
|
|
347
|
+
// direct child orphans a build or dev server the agent started.
|
|
348
|
+
detached: true,
|
|
346
349
|
// The marker lets the child's subagent tool refuse to nest further.
|
|
347
350
|
env: { ...process.env, PI_CODE_SUBAGENT: '1' },
|
|
348
351
|
})
|
|
@@ -401,19 +404,24 @@ async function runSingleAgentInner(options: RunAgentOptions): Promise<SingleResu
|
|
|
401
404
|
resolve(1)
|
|
402
405
|
})
|
|
403
406
|
|
|
407
|
+
const killGroup = (sig: NodeJS.Signals): void => {
|
|
408
|
+
try {
|
|
409
|
+
process.kill(-proc.pid!, sig)
|
|
410
|
+
} catch {
|
|
411
|
+
try {
|
|
412
|
+
proc.kill(sig)
|
|
413
|
+
} catch {
|
|
414
|
+
/* already gone */
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
404
418
|
if (signal) {
|
|
405
419
|
onAbort = () => {
|
|
406
420
|
wasAborted = true
|
|
407
|
-
|
|
421
|
+
killGroup('SIGTERM')
|
|
408
422
|
// proc.killed only reports that the signal was sent, not that the child died. Escalate
|
|
409
423
|
// on a timer that the 'close' handler clears once the child has actually exited.
|
|
410
|
-
killTimer = setTimeout(() =>
|
|
411
|
-
try {
|
|
412
|
-
proc.kill('SIGKILL')
|
|
413
|
-
} catch {
|
|
414
|
-
/* already gone */
|
|
415
|
-
}
|
|
416
|
-
}, 5000)
|
|
424
|
+
killTimer = setTimeout(() => killGroup('SIGKILL'), 5000)
|
|
417
425
|
}
|
|
418
426
|
if (signal.aborted) onAbort()
|
|
419
427
|
else signal.addEventListener('abort', onAbort, { once: true })
|
|
@@ -498,17 +506,25 @@ type ChainStepParam = Static<typeof ChainItem>
|
|
|
498
506
|
type TaskItemParam = Static<typeof TaskItem>
|
|
499
507
|
|
|
500
508
|
/** The completion notice a background run sends when it finishes. */
|
|
501
|
-
export function backgroundCompletionText(run: { id: string; agent: string; state: string; turns: number; output?: string }): string {
|
|
509
|
+
export function backgroundCompletionText(run: { id: string; agent: string; state: string; turns: number; output?: string; stderr?: string }): string {
|
|
502
510
|
const output = capForContext(run.output ?? '') || '(no output)'
|
|
503
|
-
|
|
511
|
+
// A child that dies at boot writes its reason only to stderr; without this the
|
|
512
|
+
// notice reads "failed after 0 turns ... (no output)" with nothing to act on.
|
|
513
|
+
const diagnostics = run.state === 'failed' && run.stderr ? `\n\nstderr tail:\n${capForContext(run.stderr)}` : ''
|
|
514
|
+
return `Background subagent run ${run.id} (${run.agent}) ${run.state} after ${run.turns} turns.\n\n${output}${diagnostics}`
|
|
504
515
|
}
|
|
505
516
|
|
|
506
517
|
/** What to tell the model about a resume request. */
|
|
507
|
-
export function resumeResultText(id: string, task: string | undefined, onComplete: (run: { id: string; agent: string; state: string; turns: number; output?: string }) => void): string {
|
|
518
|
+
export function resumeResultText(id: string, task: string | undefined, onComplete: (run: { id: string; agent: string; state: string; turns: number; output?: string; stderr?: string }) => void, onResumed?: (run: { id: string; agent: string }) => void): string {
|
|
508
519
|
if (!task) return 'Pass task with resume: the follow-up needs an instruction.'
|
|
509
520
|
const outcome = resumeBackgroundRun(id, task, onComplete)
|
|
510
|
-
if (outcome === 'resumed')
|
|
521
|
+
if (outcome === 'resumed') {
|
|
522
|
+
const run = backgroundRun(id)
|
|
523
|
+
if (run) onResumed?.({ id: run.id, agent: run.agent })
|
|
524
|
+
return `Resumed background run ${id} with the follow-up task; a notification will arrive on completion.`
|
|
525
|
+
}
|
|
511
526
|
if (outcome === 'still-running') return `Background run ${id} is still running; wait for it or cancel it first.`
|
|
527
|
+
if (outcome === 'at-capacity') return `Background run cap reached (${MAX_BACKGROUND_RUNS} concurrent); wait for a run to finish before resuming ${id}.`
|
|
512
528
|
return `Unknown background run: ${id}.\n\n${backgroundStatusText()}`
|
|
513
529
|
}
|
|
514
530
|
|
|
@@ -628,15 +644,11 @@ async function runBackgroundMode(params: SubagentParamsStatic, agents: AgentConf
|
|
|
628
644
|
const invocation = getPiInvocation(args)
|
|
629
645
|
const id = startBackgroundRun(agent.name, task, { command: invocation.command, args: invocation.args, cwd: params.cwd ?? defaultCwd, promptBody: tmpPrompt ? promptWithSkills : undefined }, (run) => {
|
|
630
646
|
removeTmpPrompt(tmpPrompt)
|
|
647
|
+
// Both calls throw once the session that started the run is disposed; driveRun
|
|
648
|
+
// catches for the whole callback, so neither can escape into the child's close
|
|
649
|
+
// listener and become an uncaughtException.
|
|
631
650
|
pi.events.emit(SUBAGENT_CHANNEL, { phase: 'stop', agentType: run.agent, agentId: run.id })
|
|
632
|
-
|
|
633
|
-
// that session is disposed; an escaping error here would reach Node as an
|
|
634
|
-
// uncaughtException and take the process down with it.
|
|
635
|
-
try {
|
|
636
|
-
pi.sendMessage({ customType: 'subagent-background', content: backgroundCompletionText(run), display: true }, { triggerTurn: true })
|
|
637
|
-
} catch {
|
|
638
|
-
// the session that asked for this run is gone; nothing left to notify
|
|
639
|
-
}
|
|
651
|
+
pi.sendMessage({ customType: 'subagent-background', content: backgroundCompletionText(run), display: true }, { triggerTurn: true })
|
|
640
652
|
})
|
|
641
653
|
if (id === null) {
|
|
642
654
|
// Lost the cap race to a parallel batch: the atomic check inside startBackgroundRun refused.
|
|
@@ -1112,12 +1124,11 @@ function renderParallelResult(results: SingleResult[], expanded: boolean, theme:
|
|
|
1112
1124
|
}
|
|
1113
1125
|
|
|
1114
1126
|
export default function subagentExtension(pi: ExtensionAPI) {
|
|
1115
|
-
const notifyBackgroundCompletion = (run: { id: string; agent: string; state: string; turns: number; output?: string }): void => {
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1127
|
+
const notifyBackgroundCompletion = (run: { id: string; agent: string; state: string; turns: number; output?: string; stderr?: string }): void => {
|
|
1128
|
+
// Runs through driveRun's guard, same as the background-mode callback above.
|
|
1129
|
+
// The stop event fires here too, so SubagentStop hooks see resumed runs end.
|
|
1130
|
+
pi.events.emit(SUBAGENT_CHANNEL, { phase: 'stop', agentType: run.agent, agentId: run.id })
|
|
1131
|
+
pi.sendMessage({ customType: 'subagent-background', content: backgroundCompletionText(run), display: true }, { triggerTurn: true })
|
|
1121
1132
|
}
|
|
1122
1133
|
|
|
1123
1134
|
// Claude surfaces each agent's description so the model can pick one autonomously.
|
|
@@ -1173,7 +1184,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
1173
1184
|
})
|
|
1174
1185
|
|
|
1175
1186
|
if (params.resume) {
|
|
1176
|
-
return { content: [{ type: 'text', text: resumeResultText(params.resume, params.task, notifyBackgroundCompletion) }], details: makeDetails('single')([]) }
|
|
1187
|
+
return { content: [{ type: 'text', text: resumeResultText(params.resume, params.task, notifyBackgroundCompletion, (run) => pi.events.emit(SUBAGENT_CHANNEL, { phase: 'start', agentType: run.agent, agentId: run.id })) }], details: makeDetails('single')([]) }
|
|
1177
1188
|
}
|
|
1178
1189
|
|
|
1179
1190
|
if (params.cancel) {
|
package/extensions/todo.ts
CHANGED
|
@@ -337,8 +337,8 @@ export default function todoExtension(pi: ExtensionAPI) {
|
|
|
337
337
|
|
|
338
338
|
const details = msg.details as (Omit<TodoDetails, 'todos'> & { todos?: LegacyTodo[] }) | undefined
|
|
339
339
|
// pi persists a failed tool call as `details: {}`, which is truthy: a rejected
|
|
340
|
-
// or blocked
|
|
341
|
-
//
|
|
340
|
+
// or blocked call would otherwise throw here and break replay for the rest of
|
|
341
|
+
// the session, losing the list on every resume, fork and compaction.
|
|
342
342
|
if (Array.isArray(details?.todos)) {
|
|
343
343
|
replayTodos = details.todos.map(normalizeTodo)
|
|
344
344
|
replayNextId = details.nextId
|
package/extensions/web.ts
CHANGED
|
@@ -194,6 +194,9 @@ async function fetchText(rawUrl: string, transport = httpFetch): Promise<{ text:
|
|
|
194
194
|
userAgent: USER_AGENT,
|
|
195
195
|
})
|
|
196
196
|
if (response.status >= 300 && response.status < 400) {
|
|
197
|
+
// The hop's body is never read; without the cancel its socket stays held
|
|
198
|
+
// until the 20s abort timeout, once per hop.
|
|
199
|
+
void response.body?.cancel().catch(() => {})
|
|
197
200
|
const location = response.headers.get('location')
|
|
198
201
|
if (!location) throw new Error(`redirect without location from ${url.hostname}`)
|
|
199
202
|
url = new URL(location, url)
|
|
@@ -202,7 +205,10 @@ async function fetchText(rawUrl: string, transport = httpFetch): Promise<{ text:
|
|
|
202
205
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new Error(`unsupported redirect scheme ${url.protocol} from ${rawUrl}`)
|
|
203
206
|
continue
|
|
204
207
|
}
|
|
205
|
-
if (!response.ok)
|
|
208
|
+
if (!response.ok) {
|
|
209
|
+
void response.body?.cancel().catch(() => {})
|
|
210
|
+
throw new Error(`HTTP ${response.status} for ${url}`)
|
|
211
|
+
}
|
|
206
212
|
return { text: await readCapped(response), contentType: response.headers.get('content-type') ?? '' }
|
|
207
213
|
}
|
|
208
214
|
throw new Error(`too many redirects for ${rawUrl}`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Claude Code experience for the pi coding agent: reads your .claude config (rules, commands, skills, hooks, output styles, MCP servers, agents) and adds todo, checkpoints, memory, web, and subagents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|