pi-code 0.8.0 → 1.0.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.
@@ -26,8 +26,9 @@ import { type Static, Type } from 'typebox'
26
26
  import { capForContext } from '../internal/output-guard.js'
27
27
  import { isProjectApproved, isProjectApprovedSilently } from '../internal/project-approval.js'
28
28
  import { SUBAGENT_CHANNEL } from '../internal/subagent-events.js'
29
- import { type AgentConfig, type AgentScope, discoverAgents } from './agents.js'
30
- import { activeBackgroundRuns, backgroundStatusText, cancelBackgroundRun, MAX_BACKGROUND_RUNS, startBackgroundRun } from './background.js'
29
+ import { skillDirs } from '../skills.js'
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
32
 
32
33
  const MAX_PARALLEL_TASKS = 8
33
34
  const MAX_CONCURRENCY = 4
@@ -259,6 +260,10 @@ interface RunAgentOptions {
259
260
  onUpdate?: OnUpdateCallback
260
261
  makeDetails: (results: SingleResult[]) => SubagentDetails
261
262
  onPhase?: SubagentPhaseSink
263
+ /** Skill directories to preload from, resolved where project trust is known. */
264
+ skillRoots?: string[]
265
+ /** Models this user can actually run, for resolving a tier alias. */
266
+ availableModels?: ReadonlyArray<{ id: string }>
262
267
  }
263
268
 
264
269
  /** Publishes a child run's start/stop for the hooks extension's SubagentStart/Stop. */
@@ -294,7 +299,7 @@ async function runSingleAgentInner(options: RunAgentOptions): Promise<SingleResu
294
299
  }
295
300
  }
296
301
 
297
- const args = agentInvocationArgs(agent)
302
+ const args = agentInvocationArgs(agent, resolveModelAlias(agent.modelAlias, options.availableModels ?? []))
298
303
 
299
304
  let tmpPromptDir: string | null = null
300
305
  let tmpPromptPath: string | null = null
@@ -321,8 +326,9 @@ async function runSingleAgentInner(options: RunAgentOptions): Promise<SingleResu
321
326
  }
322
327
 
323
328
  try {
324
- if (agent.systemPrompt.trim()) {
325
- const tmp = await writePromptToTempFile(agent.name, agent.systemPrompt)
329
+ const promptWithSkills = withPreloadedSkills(agent.systemPrompt, agent.skills, options.skillRoots ?? [])
330
+ if (promptWithSkills.trim()) {
331
+ const tmp = await writePromptToTempFile(agent.name, promptWithSkills)
326
332
  tmpPromptDir = tmp.dir
327
333
  tmpPromptPath = tmp.filePath
328
334
  args.push('--append-system-prompt', tmpPromptPath)
@@ -461,6 +467,7 @@ const SubagentParams = Type.Object({
461
467
  background: Type.Optional(Type.Boolean({ description: 'Run the single-mode task in the background: returns a run id immediately and a notification arrives when it completes.' })),
462
468
  status: Type.Optional(Type.Boolean({ description: 'Set true (alone, no other params) to list background runs instead of running anything.' })),
463
469
  cancel: Type.Optional(Type.String({ description: 'Background run id to cancel (from the id returned when it started, or from status).' })),
470
+ resume: Type.Optional(Type.String({ description: 'Finished background run id to continue with a follow-up task; the child keeps everything it already saw. Pass task with it.' })),
464
471
  })
465
472
 
466
473
  /**
@@ -490,6 +497,21 @@ type SubagentParamsStatic = Static<typeof SubagentParams>
490
497
  type ChainStepParam = Static<typeof ChainItem>
491
498
  type TaskItemParam = Static<typeof TaskItem>
492
499
 
500
+ /** 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 {
502
+ const output = capForContext(run.output ?? '') || '(no output)'
503
+ return `Background subagent run ${run.id} (${run.agent}) ${run.state} after ${run.turns} turns.\n\n${output}`
504
+ }
505
+
506
+ /** 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 {
508
+ if (!task) return 'Pass task with resume: the follow-up needs an instruction.'
509
+ const outcome = resumeBackgroundRun(id, task, onComplete)
510
+ if (outcome === 'resumed') return `Resumed background run ${id} with the follow-up task; a notification will arrive on completion.`
511
+ if (outcome === 'still-running') return `Background run ${id} is still running; wait for it or cancel it first.`
512
+ return `Unknown background run: ${id}.\n\n${backgroundStatusText()}`
513
+ }
514
+
493
515
  /** What to tell the model about a cancel request. */
494
516
  export function cancelResultText(id: string): string {
495
517
  const outcome = cancelBackgroundRun(id)
@@ -506,6 +528,8 @@ interface ModeContext {
506
528
  onUpdate: OnUpdateCallback | undefined
507
529
  makeDetails: MakeDetails
508
530
  onPhase?: SubagentPhaseSink
531
+ skillRoots: string[]
532
+ availableModels: ReadonlyArray<{ id: string }>
509
533
  }
510
534
 
511
535
  async function checkProjectAgentGate(params: SubagentParamsStatic, agents: AgentConfig[], ctx: ExtensionContext, projectAgentsDir: string | null, gateMode: SubagentMode, makeDetails: MakeDetails): Promise<ToolResult | null> {
@@ -539,11 +563,14 @@ async function checkProjectAgentGate(params: SubagentParamsStatic, agents: Agent
539
563
  }
540
564
 
541
565
  /** CLI args shared by foreground and background children, from the agent's config. */
542
- function agentInvocationArgs(agent: AgentConfig): string[] {
566
+ function agentInvocationArgs(agent: AgentConfig, aliasModel?: string): string[] {
543
567
  const args: string[] = ['--mode', 'json', '-p', '--no-session']
544
- // pi reads a thinking level from the model pattern's :suffix, so Claude's effort
545
- // has a seam only when the agent pins a concrete model.
546
- if (agent.model) args.push('--model', agent.effort ? `${agent.model}:${agent.effort}` : agent.model)
568
+ // A concrete model wins; otherwise a Claude tier alias resolved against the models
569
+ // this user can actually run. pi reads a thinking level from the model pattern's
570
+ // :suffix when a model is pinned, and from --thinking otherwise.
571
+ const model = agent.model ?? aliasModel
572
+ if (model) args.push('--model', agent.effort ? `${model}:${agent.effort}` : model)
573
+ else if (agent.effort) args.push('--thinking', agent.effort)
547
574
  if (agent.tools && agent.tools.length > 0) args.push('--tools', agent.tools.join(','))
548
575
  if (agent.disallowedTools && agent.disallowedTools.length > 0) args.push('--exclude-tools', agent.disallowedTools.join(','))
549
576
  return args
@@ -570,7 +597,7 @@ function removeTmpPrompt(tmpPrompt: { dir: string; filePath: string } | undefine
570
597
  }
571
598
  }
572
599
 
573
- async function runBackgroundMode(params: SubagentParamsStatic, agents: AgentConfig[], defaultCwd: string, pi: ExtensionAPI, makeDetails: MakeDetails): Promise<ToolResult> {
600
+ async function runBackgroundMode(params: SubagentParamsStatic, agents: AgentConfig[], defaultCwd: string, pi: ExtensionAPI, makeDetails: MakeDetails, skillRoots: string[], availableModels: ReadonlyArray<{ id: string }>): Promise<ToolResult> {
574
601
  const task = params.task
575
602
  const agentName = params.agent
576
603
  if (!task || !agentName) {
@@ -590,10 +617,11 @@ async function runBackgroundMode(params: SubagentParamsStatic, agents: AgentConf
590
617
  if (activeBackgroundRuns() >= MAX_BACKGROUND_RUNS) {
591
618
  return backgroundCapResult(makeDetails)
592
619
  }
593
- const args = agentInvocationArgs(agent)
620
+ const args = agentInvocationArgs(agent, resolveModelAlias(agent.modelAlias, availableModels))
594
621
  let tmpPrompt: { dir: string; filePath: string } | undefined
595
- if (agent.systemPrompt.trim()) {
596
- tmpPrompt = await writePromptToTempFile(agent.name, agent.systemPrompt)
622
+ const promptWithSkills = withPreloadedSkills(agent.systemPrompt, agent.skills, skillRoots)
623
+ if (promptWithSkills.trim()) {
624
+ tmpPrompt = await writePromptToTempFile(agent.name, promptWithSkills)
597
625
  args.push('--append-system-prompt', tmpPrompt.filePath)
598
626
  }
599
627
  args.push(`Task: ${task}`)
@@ -601,15 +629,7 @@ async function runBackgroundMode(params: SubagentParamsStatic, agents: AgentConf
601
629
  const id = startBackgroundRun(agent.name, task, { command: invocation.command, args: invocation.args, cwd: params.cwd ?? defaultCwd }, (run) => {
602
630
  removeTmpPrompt(tmpPrompt)
603
631
  pi.events.emit(SUBAGENT_CHANNEL, { phase: 'stop', agentType: run.agent, agentId: run.id })
604
- const output = capForContext(run.output ?? '') || '(no output)'
605
- pi.sendMessage(
606
- {
607
- customType: 'subagent-background',
608
- content: `Background subagent run ${run.id} (${run.agent}) ${run.state} after ${run.turns} turns.\n\n${output}`,
609
- display: true,
610
- },
611
- { triggerTurn: true },
612
- )
632
+ pi.sendMessage({ customType: 'subagent-background', content: backgroundCompletionText(run), display: true }, { triggerTurn: true })
613
633
  })
614
634
  if (id === null) {
615
635
  // Lost the cap race to a parallel batch: the atomic check inside startBackgroundRun refused.
@@ -659,6 +679,8 @@ async function runChainMode(chain: ChainStepParam[], mode: ModeContext): Promise
659
679
  onUpdate: chainUpdate,
660
680
  makeDetails: makeDetails('chain'),
661
681
  onPhase: mode.onPhase,
682
+ skillRoots: mode.skillRoots,
683
+ availableModels: mode.availableModels,
662
684
  })
663
685
  results.push(result)
664
686
 
@@ -774,6 +796,8 @@ async function runSingleMode(agentName: string, task: string, cwd: string | unde
774
796
  onUpdate,
775
797
  makeDetails: makeDetails('single'),
776
798
  onPhase: mode.onPhase,
799
+ skillRoots: mode.skillRoots,
800
+ availableModels: mode.availableModels,
777
801
  })
778
802
  const isError = result.exitCode !== 0 || result.stopReason === 'error' || result.stopReason === 'aborted'
779
803
  if (isError) {
@@ -1077,6 +1101,10 @@ function renderParallelResult(results: SingleResult[], expanded: boolean, theme:
1077
1101
  }
1078
1102
 
1079
1103
  export default function subagentExtension(pi: ExtensionAPI) {
1104
+ const notifyBackgroundCompletion = (run: { id: string; agent: string; state: string; turns: number; output?: string }): void => {
1105
+ pi.sendMessage({ customType: 'subagent-background', content: backgroundCompletionText(run), display: true }, { triggerTurn: true })
1106
+ }
1107
+
1080
1108
  // Claude surfaces each agent's description so the model can pick one autonomously.
1081
1109
  // Rebuilt per turn (agents are rediscovered per invocation too); project agents are
1082
1110
  // included only when the project is already approved, read without prompting, since
@@ -1129,6 +1157,10 @@ export default function subagentExtension(pi: ExtensionAPI) {
1129
1157
  results,
1130
1158
  })
1131
1159
 
1160
+ if (params.resume) {
1161
+ return { content: [{ type: 'text', text: resumeResultText(params.resume, params.task, notifyBackgroundCompletion) }], details: makeDetails('single')([]) }
1162
+ }
1163
+
1132
1164
  if (params.cancel) {
1133
1165
  return { content: [{ type: 'text', text: cancelResultText(params.cancel) }], details: makeDetails('single')([]) }
1134
1166
  }
@@ -1157,9 +1189,16 @@ export default function subagentExtension(pi: ExtensionAPI) {
1157
1189
  const gateResult = await checkProjectAgentGate(params, agents, ctx, discovery.projectAgentsDir, gateMode, makeDetails)
1158
1190
  if (gateResult) return gateResult
1159
1191
 
1160
- if (params.background) return runBackgroundMode(params, agents, ctx.cwd, pi, makeDetails)
1192
+ // Project skills only preload once the project is approved, matching the
1193
+ // gate the skills extension applies to discovery itself.
1194
+ const skillRoots = skillDirs(ctx.cwd, os.homedir(), isProjectApprovedSilently(ctx))
1195
+ // Tier aliases resolve against what this user is authenticated for; an
1196
+ // unavailable tier still falls back to the session model.
1197
+ const availableModels = ctx.modelRegistry?.getAvailable?.() ?? []
1198
+
1199
+ if (params.background) return runBackgroundMode(params, agents, ctx.cwd, pi, makeDetails, skillRoots, availableModels)
1161
1200
 
1162
- const mode: ModeContext = { agents, defaultCwd: ctx.cwd, signal, onUpdate, makeDetails, onPhase: (phase, agentType, agentId) => pi.events.emit(SUBAGENT_CHANNEL, { phase, agentType, agentId }) }
1201
+ const mode: ModeContext = { agents, defaultCwd: ctx.cwd, signal, onUpdate, makeDetails, skillRoots, availableModels, onPhase: (phase, agentType, agentId) => pi.events.emit(SUBAGENT_CHANNEL, { phase, agentType, agentId }) }
1163
1202
 
1164
1203
  if (params.chain?.length) return runChainMode(params.chain, mode)
1165
1204
  if (params.tasks?.length) return runParallelMode(params.tasks, mode)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-code",
3
- "version": "0.8.0",
3
+ "version": "1.0.0",
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",