pi-subagents 0.67.0 → 0.69.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.
Files changed (122) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +1 -1
  3. package/docs/agents.md +41 -12
  4. package/docs/configuration.md +61 -19
  5. package/docs/extension-api.md +5 -1
  6. package/docs/missions.md +2 -2
  7. package/docs/models.md +11 -79
  8. package/docs/observability.md +18 -8
  9. package/docs/standalone-background.md +13 -3
  10. package/docs/tool-reference.md +38 -14
  11. package/docs/watchdog.md +10 -12
  12. package/docs/workflows.md +59 -1
  13. package/index.ts +5 -2
  14. package/package.json +4 -2
  15. package/runner-peer-loader.mjs +24 -0
  16. package/runner-peer-preload.mjs +25 -11
  17. package/skills/pi-subagents/SKILL.md +18 -21
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  19. package/skills/pi-subagents/references/execution-controls.md +6 -4
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  21. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  22. package/skills/pi-subagents/references/prompting-and-roles.md +16 -12
  23. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  24. package/src/agents/agent-management.ts +57 -58
  25. package/src/agents/agent-serializer.ts +4 -3
  26. package/src/agents/agents.ts +185 -72
  27. package/src/agents/chain-serializer.ts +5 -0
  28. package/src/agents/runtime-agent-registry.ts +7 -6
  29. package/src/agents/skills.ts +1 -1
  30. package/src/api/preflight.ts +20 -16
  31. package/src/api/required-child-extensions.ts +6 -0
  32. package/src/extension/config.ts +10 -37
  33. package/src/extension/fanout-child.ts +3 -0
  34. package/src/extension/herdr-pi-bridge.ts +160 -0
  35. package/src/extension/index.ts +42 -31
  36. package/src/extension/public-execution.ts +3 -3
  37. package/src/extension/schemas.ts +23 -6
  38. package/src/extension/tool-description.ts +8 -7
  39. package/src/inspectors/ghostty/plugin.ts +13 -1
  40. package/src/intercom/native-supervisor-channel.ts +22 -18
  41. package/src/policy/authority.ts +4 -0
  42. package/src/profiles/profiles.ts +12 -6
  43. package/src/runs/background/active-run-index.ts +17 -1
  44. package/src/runs/background/async-execution.ts +309 -126
  45. package/src/runs/background/async-job-tracker.ts +8 -6
  46. package/src/runs/background/async-resume.ts +13 -4
  47. package/src/runs/background/async-status.ts +15 -4
  48. package/src/runs/background/auto-drain.ts +20 -10
  49. package/src/runs/background/binary-bootstrap.ts +5 -0
  50. package/src/runs/background/chain-append.ts +1 -1
  51. package/src/runs/background/chain-root-attachment.ts +14 -33
  52. package/src/runs/background/notify.ts +74 -6
  53. package/src/runs/background/result-files.ts +8 -4
  54. package/src/runs/background/result-watcher.ts +19 -2
  55. package/src/runs/background/run-child-session.ts +20 -29
  56. package/src/runs/background/runner-aliases.ts +4 -33
  57. package/src/runs/background/runner-child-launch.ts +4 -1
  58. package/src/runs/background/runner-child-sessions.ts +2 -2
  59. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  60. package/src/runs/background/scheduled-runs.ts +11 -5
  61. package/src/runs/background/stale-run-reconciler.ts +35 -11
  62. package/src/runs/background/subagent-runner.ts +413 -276
  63. package/src/runs/background/subagent-wait.ts +128 -23
  64. package/src/runs/background/wait-completions.ts +75 -27
  65. package/src/runs/background/wait-subscriptions.ts +9 -3
  66. package/src/runs/background/wait-tool.ts +4 -2
  67. package/src/runs/foreground/async-stop-action.ts +93 -3
  68. package/src/runs/foreground/execution.ts +115 -219
  69. package/src/runs/foreground/foreground-history.ts +2 -1
  70. package/src/runs/foreground/subagent-executor.ts +281 -80
  71. package/src/runs/shared/acceptance.ts +194 -37
  72. package/src/runs/shared/async-status-projection.ts +123 -33
  73. package/src/runs/shared/child-launch-plan.ts +15 -3
  74. package/src/runs/shared/child-launch.ts +19 -6
  75. package/src/runs/shared/child-runtime-config.ts +5 -0
  76. package/src/runs/shared/child-session.ts +94 -50
  77. package/src/runs/shared/child-tool-plan.ts +28 -16
  78. package/src/runs/shared/dynamic-fanout.ts +2 -2
  79. package/src/runs/shared/external-cli-contract.ts +11 -1
  80. package/src/runs/shared/external-cli-preflight.ts +6 -2
  81. package/src/runs/shared/herdr-connection.ts +134 -0
  82. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  83. package/src/runs/shared/herdr-machine.ts +279 -0
  84. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  85. package/src/runs/shared/herdr-placed-run.ts +263 -0
  86. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  87. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
  88. package/src/runs/shared/model-scope.ts +1 -1
  89. package/src/runs/shared/nested-events.ts +11 -2
  90. package/src/runs/shared/parallel-utils.ts +7 -2
  91. package/src/runs/shared/pi-spawn.ts +1 -1
  92. package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
  93. package/src/runs/shared/worktree-setup-command.ts +27 -4
  94. package/src/runs/shared/worktree.ts +30 -8
  95. package/src/shared/child-cache-retention.ts +43 -0
  96. package/src/shared/launch-contract.ts +6 -9
  97. package/src/shared/pruned-fork.ts +1 -1
  98. package/src/shared/required-child-extensions.ts +81 -0
  99. package/src/shared/settings.ts +5 -2
  100. package/src/shared/shortcuts.ts +0 -4
  101. package/src/shared/types.ts +81 -29
  102. package/src/slash/slash-commands.ts +0 -6
  103. package/src/slash/subagents-admin.ts +13 -9
  104. package/src/tui/render.ts +20 -10
  105. package/src/watchdog/child-status.ts +28 -36
  106. package/src/watchdog/lsp-diagnostics.ts +1 -1
  107. package/src/watchdog/model-selection.ts +1 -1
  108. package/src/watchdog/register-child.ts +10 -3
  109. package/src/watchdog/register-main.ts +20 -20
  110. package/src/watchdog/render.ts +1 -1
  111. package/src/watchdog/review.ts +14 -30
  112. package/src/watchdog/rules.ts +1 -1
  113. package/src/watchdog/runtime.ts +23 -12
  114. package/src/watchdog/settings.ts +3 -6
  115. package/src/watchdog/types.ts +3 -5
  116. package/src/watchdog/warning-format.ts +1 -1
  117. package/src/workflows/scripted-workflow.ts +68 -7
  118. package/src/workflows/workflow-receipt.ts +21 -3
  119. package/src/workflows/workflow-resources.ts +13 -2
  120. package/src/runs/shared/model-exclusions.ts +0 -374
  121. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  122. package/src/runs/shared/readonly-session-evidence.ts +0 -307
@@ -71,7 +71,7 @@ import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/
71
71
  import { disposeChildSessions } from "../runs/shared/child-session.ts";
72
72
  import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
73
73
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
74
- import { applyModelExclusionsConfig, loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
74
+ import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
75
75
  import { buildSubagentToolDescription, buildSubagentToolPromptMetadata } from "./tool-description.ts";
76
76
  import { formatWorkflowPreflightSummary, normalizeWorkflowPreflight } from "../workflows/workflow-preflight.ts";
77
77
  import { finalizeToolResult } from "./tool-result.ts";
@@ -431,8 +431,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
431
431
  cleanupOldChainDirs();
432
432
 
433
433
  const config = loadConfig();
434
- // Apply the process-wide exclusion TTL before any child launch can record a model failure.
435
- applyModelExclusionsConfig(config);
436
434
  const waitToolConfig = resolveWaitToolConfig(config.waitTool);
437
435
  const asyncByDefault = resolveAsyncByDefault(config);
438
436
  const fleetViewEnabled = config.fleetView !== false;
@@ -442,14 +440,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
442
440
  const tempArtifactsDir = getArtifactsDir(null);
443
441
  const artifactCleanupDays = config.artifactConfig?.cleanupDays ?? DEFAULT_ARTIFACT_CONFIG.cleanupDays;
444
442
  cleanupAllArtifactDirs(artifactCleanupDays);
445
- const resultIndexCleanupTimer = setTimeout(() => {
446
- try {
447
- cleanupResultIndexes(DIRS.results);
448
- } catch (error) {
449
- console.error("Failed to clean stale subagent result indexes:", error);
450
- }
451
- }, 30_000);
452
- resultIndexCleanupTimer.unref?.();
443
+ let resultIndexCleanupTimer: ReturnType<typeof setTimeout> | undefined;
453
444
 
454
445
  const state: SubagentState = {
455
446
  baseCwd: "",
@@ -595,23 +586,39 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
595
586
  const { startResultWatcher, transitionResultDelivery, primeExistingResults, stopResultWatcher } = resultWatcher;
596
587
  refreshResultDelivery = resultWatcher.refreshResultDelivery;
597
588
  const asyncRetentionAbort = new AbortController();
598
- const asyncRetentionTimer = setTimeout(async () => {
599
- try {
600
- await cleanupAsyncRetention({
601
- asyncDirRoot: DIRS.async,
602
- resultsDir: DIRS.results,
603
- signal: asyncRetentionAbort.signal,
604
- protectedRunIds: new Set([
605
- ...state.asyncJobs.keys(),
606
- ...(state.workflowControllers?.keys() ?? []),
607
- ...scheduledRunManager.referencedAsyncRunIds(),
608
- ]),
609
- });
610
- } catch (error) {
611
- console.error("Failed to clean retained async subagent state:", error);
589
+ let asyncRetentionTimer: ReturnType<typeof setTimeout> | undefined;
590
+ const startSessionMaintenance = () => {
591
+ if (!resultIndexCleanupTimer) {
592
+ resultIndexCleanupTimer = setTimeout(() => {
593
+ try {
594
+ cleanupResultIndexes(DIRS.results);
595
+ } catch (error) {
596
+ console.error("Failed to clean stale subagent result indexes:", error);
597
+ }
598
+ }, 30_000);
599
+ resultIndexCleanupTimer.unref?.();
612
600
  }
613
- }, ASYNC_RETENTION_DELAY_MS);
614
- asyncRetentionTimer.unref?.();
601
+ waitSubscriptionManager.start();
602
+ if (!asyncRetentionTimer) {
603
+ asyncRetentionTimer = setTimeout(async () => {
604
+ try {
605
+ await cleanupAsyncRetention({
606
+ asyncDirRoot: DIRS.async,
607
+ resultsDir: DIRS.results,
608
+ signal: asyncRetentionAbort.signal,
609
+ protectedRunIds: new Set([
610
+ ...state.asyncJobs.keys(),
611
+ ...(state.workflowControllers?.keys() ?? []),
612
+ ...scheduledRunManager.referencedAsyncRunIds(),
613
+ ]),
614
+ });
615
+ } catch (error) {
616
+ console.error("Failed to clean retained async subagent state:", error);
617
+ }
618
+ }, ASYNC_RETENTION_DELAY_MS);
619
+ asyncRetentionTimer.unref?.();
620
+ }
621
+ };
615
622
 
616
623
  const executorDeps: Parameters<typeof createSubagentExecutor>[0] = {
617
624
  pi,
@@ -824,7 +831,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
824
831
  registerWaitTool(pi, state, waitToolConfig.enabled, waitSubscriptionManager, waitToolConfig.defaultTimeoutMs);
825
832
 
826
833
  pi.on("agent_end", async (_event, ctx) => {
827
- if (!ctx.hasUI) await drainOutstandingWork({ state, events: pi.events });
834
+ if (!ctx.hasUI) await drainOutstandingWork({ state, events: pi.events, hasPendingSupervisorRequest: supervisorChannel.hasPendingRequests });
828
835
  const ownerSessionId = state.currentSessionId;
829
836
  if (!ownerSessionId) return;
830
837
  goalTurnId += 1;
@@ -1041,8 +1048,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
1041
1048
  state.workflowControllers?.clear();
1042
1049
  state.workflowChildStops?.clear();
1043
1050
  clearRuntimeAgentsForPi(pi);
1044
- clearTimeout(resultIndexCleanupTimer);
1045
- clearTimeout(asyncRetentionTimer);
1051
+ if (resultIndexCleanupTimer) clearTimeout(resultIndexCleanupTimer);
1052
+ resultIndexCleanupTimer = undefined;
1053
+ if (asyncRetentionTimer) clearTimeout(asyncRetentionTimer);
1054
+ asyncRetentionTimer = undefined;
1046
1055
  asyncRetentionAbort.abort();
1047
1056
  stopResultWatcher();
1048
1057
  resultDeliveryOwnership.clear();
@@ -1136,7 +1145,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
1136
1145
  if (event.reason !== "manual") suspendWidgetsForCompaction();
1137
1146
  });
1138
1147
 
1139
- pi.on("session_compact", () => {
1148
+ pi.on("session_compact", (event) => {
1149
+ if (event.reason !== "manual") return;
1140
1150
  const hasActiveAsyncWork = [...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running");
1141
1151
  if (!hasActiveAsyncWork || !withLastUiContext(() => true)) return;
1142
1152
  pi.sendMessage(
@@ -1151,6 +1161,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
1151
1161
 
1152
1162
  pi.on("session_start", (event, ctx) => {
1153
1163
  installRuntime(ctx);
1164
+ startSessionMaintenance();
1154
1165
  const recovering = event.reason === "startup" || event.reason === "reload" || event.reason === "resume";
1155
1166
  resetSessionState(ctx, recovering, event.previousSessionFile);
1156
1167
  releaseHostSessionLiveness();
@@ -89,10 +89,10 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
89
89
  if (hasNamedWorkflow && (params.workflowScript !== undefined || params.workflowScriptPath !== undefined)) {
90
90
  return { ok: false, error: "workflow is mutually exclusive with workflowScript and workflowScriptPath.", mode: "workflow" };
91
91
  }
92
- if (!hasNamedWorkflow && params.args !== undefined) {
93
- return { ok: false, error: "args requires a named workflow resource.", mode: "workflow" };
94
- }
95
92
  const hasWorkflowInput = params.workflowScript !== undefined || params.workflowScriptPath !== undefined || hasNamedWorkflow;
93
+ if (!hasWorkflowInput && params.args !== undefined) {
94
+ return { ok: false, error: "args requires workflow, workflowScript, or workflowScriptPath.", mode: "workflow" };
95
+ }
96
96
  const hasCapacityOverride = params.globalConcurrencyLimit !== undefined || params.maxSubagentSpawnsPerRun !== undefined;
97
97
  if (hasCapacityOverride) {
98
98
  const capacityOverrideError = validateWorkflowCapacityOverrides(params);
@@ -65,6 +65,11 @@ const JsonSchemaObject = Type.Unsafe({
65
65
  description: "Strict structured output; object-root JSON Schema only.",
66
66
  });
67
67
 
68
+ const OutputSchemaOverride = Type.Unsafe({
69
+ anyOf: [JsonSchemaObject, { type: "boolean" }],
70
+ description: "Structured output schema override; false disables an agent default.",
71
+ });
72
+
68
73
  // Provider boolean branches intentionally overapproximate false-only runtime inputs.
69
74
  // Restricted function-declaration converters only support string enum members.
70
75
  const AcceptanceOverride = Type.Unsafe({
@@ -78,6 +83,7 @@ const AcceptanceOverride = Type.Unsafe({
78
83
  },
79
84
  {
80
85
  type: "string",
86
+ pattern: "^\\s*\\{",
81
87
  },
82
88
  { type: "boolean" },
83
89
  { type: "object", additionalProperties: true },
@@ -148,8 +154,9 @@ export const ParallelTaskSchema = Type.Object({
148
154
  phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
149
155
  label: Type.Optional(Type.String({ description: "Optional user-facing label for this parallel task." })),
150
156
  as: Type.Optional(Type.String({ description: "Optional safe identifier used as {outputs.name} in later chain steps." })),
151
- outputSchema: Type.Optional(JsonSchemaObject),
157
+ outputSchema: Type.Optional(OutputSchemaOverride),
152
158
  cwd: Type.Optional(Type.String()),
159
+ machine: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Herdr saved machine id or label." })),
153
160
  count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
154
161
  output: Type.Optional(OutputOverride),
155
162
  outputMode: Type.Optional(OutputModeOverride),
@@ -180,8 +187,9 @@ export const DynamicParallelTemplateSchema = Type.Object({
180
187
  task: Type.Optional(Type.String({ description: "Task template with {item}, {item.path}, {task}, {previous}, {chain_dir}, and {outputs.name} variables." })),
181
188
  phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
182
189
  label: Type.Optional(Type.String({ description: "Optional user-facing label; item templates are supported." })),
183
- outputSchema: Type.Optional(JsonSchemaObject),
190
+ outputSchema: Type.Optional(OutputSchemaOverride),
184
191
  cwd: Type.Optional(Type.String()),
192
+ machine: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Herdr saved machine id or label." })),
185
193
  output: Type.Optional(OutputOverride),
186
194
  outputMode: Type.Optional(OutputModeOverride),
187
195
  reads: Type.Optional(ReadsOverride),
@@ -209,8 +217,9 @@ export const ChainItem = Type.Object({
209
217
  phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
210
218
  label: Type.Optional(Type.String({ description: "Optional user-facing label for this chain step." })),
211
219
  as: Type.Optional(Type.String({ description: "Optional safe identifier used as {outputs.name} in later chain steps." })),
212
- outputSchema: Type.Optional(JsonSchemaObject),
220
+ outputSchema: Type.Optional(OutputSchemaOverride),
213
221
  cwd: Type.Optional(Type.String()),
222
+ machine: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Herdr saved machine id or label." })),
214
223
  output: Type.Optional(OutputOverride),
215
224
  outputMode: Type.Optional(OutputModeOverride),
216
225
  reads: Type.Optional(ReadsOverride),
@@ -333,7 +342,7 @@ const SubagentParamProperties = {
333
342
  description: "create/update agent config; object or JSON string."
334
343
  })),
335
344
  workflow: Type.Optional(Type.String({ minLength: 1, description: "Extension-owned workflow resource." })),
336
- args: Type.Optional(Type.Unsafe({ type: "object", maxProperties: 16, additionalProperties: true, description: "Bounded plain-JSON resource args." })),
345
+ args: Type.Optional(Type.Unsafe({ type: "object", maxProperties: 16, additionalProperties: true, description: "Bounded plain-JSON args for named, inline, or file-backed workflows; raw-script args are exposed deeply frozen and persisted, so do not include secrets." })),
337
346
  workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Inline JavaScript statement body; raw/unknown provenance, no runs.host. Use explicit return and top-level await; see tool guidance/guide workflows." })),
338
347
  workflowScriptPath: Type.Optional(Type.String({ minLength: 1, description: "Raw script file; host reads from request cwd before sandbox. Mutually exclusive with workflowScript and workflow." })),
339
348
  globalConcurrencyLimit: Type.Optional(Type.Integer({ minimum: 1 })),
@@ -351,11 +360,13 @@ const SubagentParamProperties = {
351
360
  async: Type.Optional(Type.Boolean({ description: "Background; default asyncByDefault. false only to block parent." })),
352
361
  timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Timeout. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline. Alias maxRuntimeMs." })),
353
362
  maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs (same defaults)." })),
363
+ checkpointBeforeDeadlineMs: Type.Optional(Type.Integer({ minimum: 1, maximum: 2_147_483_647, description: "Async single-agent runs only: the runner requests that the child checkpoint and stop this many ms before the run deadline (best-effort; the deadline kill still applies)." })),
354
364
  toolTimeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Per-tool deadline (ms); fast builtins default 5m." })),
355
365
  toolBudget: Type.Optional(ToolBudgetOverride),
356
366
  usageBudget: Type.Optional(UsageBudgetOverride),
357
367
  agentScope: Type.Optional(Type.String({ description: "user/project/both (default); project wins collisions." })),
358
368
  cwd: Type.Optional(Type.String({ description: "Execution/project-pane directory." })),
369
+ machine: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Herdr saved machine id or label; runs an external CLI agent there. cwd then means the directory on that machine." })),
359
370
  artifacts: Type.Optional(Type.Boolean({ description: "Debug artifacts; default true." })),
360
371
  includeProgress: Type.Optional(Type.Boolean({ description: "Full result progress; default false." })),
361
372
  share: Type.Optional(Type.Boolean({ description: "Upload session to GitHub Gist; default false." })),
@@ -375,10 +386,16 @@ const SubagentParamProperties = {
375
386
  skill: Type.Optional(SkillOverride),
376
387
  model: Type.Optional(Type.String({ description: "Child model provider/id; bare id only if unique. Suffix :off/minimal/low/medium/high/xhigh/max overrides agent thinking default." })),
377
388
  fast: Type.Optional(Type.Boolean({ description: "Native OpenAI-Codex priority tier; default false, may cost more/quota." })),
378
- outputSchema: Type.Optional(JsonSchemaObject),
389
+ outputSchema: Type.Optional(OutputSchemaOverride),
379
390
  agentContract: Type.Optional(AgentContractOverride),
380
391
  acceptance: Type.Optional(AcceptanceOverride),
381
- gate: Type.Optional(Type.String({ minLength: 1, description: "Host gate command. Cannot be combined with acceptance; an explicit acceptance of false is treated as omitted." })),
392
+ gate: Type.Optional(Type.Unsafe({
393
+ anyOf: [
394
+ { type: "string", minLength: 1 },
395
+ { type: "object", properties: { command: { type: "string", minLength: 1 }, output: { type: "string", enum: ["json"] }, schema: { type: "object" }, timeoutMs: { type: "integer", minimum: 1 } }, required: ["command"], additionalProperties: false },
396
+ ],
397
+ description: "Host gate command run after the child finishes: a string, or { command, output: \"json\", schema?, timeoutMs? } whose passing stdout becomes structuredOutput (not with outputSchema). Cannot be combined with acceptance; an explicit acceptance of false is treated as omitted.",
398
+ })),
382
399
  };
383
400
 
384
401
  const SubagentParamsSchema = Type.Object(SubagentParamProperties);
@@ -9,27 +9,28 @@ const AGENT_SELECTION_GUIDANCE = 'First call {action:"list",capabilities:true}:
9
9
  const SUBAGENT_FAILURE_RECOVERY_GUIDANCE = "Workflow, child launch, prompt runtime, extension load or child tooling failure is a lane infrastructure blocker. Stop; report exact failure, run/status and repo/cwd/worktree/branch/ref; verify clean worktree or capture partial diff before same-protocol retry or asking the owner. Never silently switch to interactive_shell, pi -ne, Codex/Claude/Cursor CLI or foreground/external mode: governed-workflow fallback requires explicit owner approval, not Pi core's generic pi -ne hint. Explicit foreground/CLI requests and work outside that protocol remain valid.";
10
10
 
11
11
  export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
12
+ • Direct parent execution is the default. Invoke subagents only when delegation is authorized by the operator's current request or applicable user/project instructions; task size, complexity, risk, tool-call count, or recipe fit do not independently authorize delegation.
12
13
  • ${AGENT_SELECTION_GUIDANCE}
13
14
  • ${SUBAGENT_FAILURE_RECOVERY_GUIDANCE}
14
- • Omit action for execution. Multi-step/parallel work: exactly one top-level subagent workflow call with async:true; children launch only inside it.
15
+ • Omit action for execution. For an authorized delegated multi-step/parallel workflow: exactly one top-level subagent workflow call with async:true; children launch only inside it.
15
16
  • Async follows asyncByDefault (normally true); async:false only to block the parent, not for final reviews/gates. Consume results at dependency barriers. Native async completion wakes this session: return control, no sleep/poll or bg_wait merely for a wake. bg_wait is for provider/detached work without native notification needing a same-turn result.
16
- • Ordinary child subagents are not orchestrators; only configured fanout within depth/session limits. One writer per cwd/worktree; isolate concurrent writers. Fresh-context read-only reviewers, then parent synthesis/fixes. Oracle/advisor unknowns use supervisor dialogue; one-shot only when requested.
17
+ • Ordinary child subagents are not orchestrators; only configured fanout within depth/session limits. For an authorized delegated workflow, keep one writer per cwd/worktree and isolate concurrent writers. Use fresh-context read-only reviewers when independent review was requested, then parent synthesis/fixes. Oracle/advisor unknowns use supervisor dialogue; one-shot only when requested.
17
18
  • Bind durable output on runs.run/runs.all, not task filename prose; return actual outputReference/outputPathMapping/artifactPaths, evidence and residual risks.
18
19
  • children.list: resume only resumable rows. {action:"resume",id,message} detaches a follow-up/challenge with stored agent/model/tool contract. If none is resumable, label a same-role fallback challenge. Scripts await runs.run(newKey,{resume:runId,task}); continue from latest returned runId. Each distinct resume pass needs a new stable key; same-key reuse requires identical launch parameters.
19
20
  • Named resources own authority; raw workflowScript/workflowScriptPath cannot use runs.host. Granted commands/relative outputs use workflow cwd, never per-step cwd.
20
21
  • Inspect asyncId/asyncDir (status.json, events.jsonl, logs) with status/debug.run; control with interrupt/stop/resume/steer. Read {action:"guide",topic:"tool-reference"} for controls/evidence gates.`;
21
22
 
22
- const EXECUTION_GUIDANCE = `Delegate one child with {agent,task?}; otherwise choose exactly one of workflowScript, workflowScriptPath or {workflow,args}. agent/task exclude workflow inputs; task excludes action. agent may target management actions. action is management/control; validate accepts either script without launching. workflowScriptPath loads from request cwd before sandbox execution.
23
+ const EXECUTION_GUIDANCE = `Delegate one child with {agent,task?}; otherwise choose exactly one of {workflowScript,args?}, {workflowScriptPath,args?} or {workflow,args}. agent/task exclude workflow inputs; task excludes action. agent may target management actions. action is management/control; validate accepts either script without launching. workflowScriptPath loads from request cwd before sandbox execution.
23
24
  Scripts: JavaScript statement bodies with explicit return, top-level await, plain helpers/Promise chains; nested async function/arrow/method helpers are rejected. Await runs.run('key',{agent,task}) before .output; await runs.all([{key,agent,task},...]) for an ordered array, not a key map. Observe every stored run promise with direct await, Promise.race or Promise.all. Await/return runs.steer(key,message,options?) for a prior key, never raw run ids; queued/delivered/missed/failed receipts are not compliance proof.
24
- Before advanced orchestration (runs.lanes, rolling fanout, mission state, handoffs), read {action:"guide",topic:"workflows"} or the pi-subagents skill. Sandbox: runs, emit, console, JavaScript and enabled mission state; no filesystem/shell/Pi tools/host globals. External CLI agents support native options only when their runner declares them; read guide tool-reference before passing model, structured output, acceptance/agentContract, tool budget, fast, fork context or skills/tools.
25
+ Before advanced orchestration (runs.lanes, rolling fanout, mission state, handoffs), read {action:"guide",topic:"workflows"} or the pi-subagents skill. Raw-script sandboxes add deeply frozen args; all sandboxes provide runs, emit, console, JavaScript and enabled mission state, with no filesystem/shell/Pi tools/host globals. External CLI agents support native options only when their runner declares them; read guide tool-reference before passing model, structured output, acceptance/agentContract, tool budget, fast, fork context or skills/tools.
25
26
  Model override: first call {action:"models"}; copy exact provider/id, not agent names. Thinking uses model suffix, not watchdog-only thinking.
26
- Named resources: {workflow:'review',args:{task:'...'}} or {workflow:'run-ci',args:{command:'npm test'}}; args are bounded plain data. worktree:true requires clean source; baseRef defaults to HEAD at allocation or a supported named ref, never full 40/64-character commit IDs or revision expressions.`;
27
+ Named resources: {workflow:'review',args:{task:'...'}} or {workflow:'run-ci',args:{command:'npm test'}}. Raw scripts also accept bounded plain-data args; raw-script args persist as evidence, so never include secrets. worktree:true requires clean source; baseRef defaults to HEAD at allocation or a supported named ref, never full 40/64-character commit IDs or revision expressions.`;
27
28
 
28
29
  export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `${EXECUTION_GUIDANCE}\n\n${SUBAGENT_SAFETY_GUIDANCE}`;
29
30
 
30
- export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflow call.";
31
+ export const SUBAGENT_TOOL_PROMPT_SNIPPET = "For operator-requested delegation, use subagents; compose multi-child work in one workflow call.";
31
32
  export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
32
- "Use subagent only when delegation is needed.",
33
+ "Do not invoke subagents unless the operator requested delegation directly or through applicable instructions.",
33
34
  ];
34
35
 
35
36
  export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = DEFAULT_SUBAGENT_TOOL_DESCRIPTION;
@@ -1,6 +1,9 @@
1
1
  import { openGhosttyInspector, type GhosttyRunner } from "./actions.ts";
2
2
  import type { InspectorPlugin } from "../types.ts";
3
3
 
4
+ /** 独立 Ghostty.app 的 bundle id (作者 Mitchell Hashimoto)。 */
5
+ const GHOSTTY_BUNDLE_ID = "com.mitchellh.ghostty";
6
+
4
7
  export interface GhosttyPluginDeps {
5
8
  platform?: NodeJS.Platform;
6
9
  runner?: GhosttyRunner;
@@ -10,7 +13,16 @@ export function createGhosttyInspectorPlugin(deps: GhosttyPluginDeps = {}): Insp
10
13
  const platform = deps.platform ?? process.platform;
11
14
  return {
12
15
  name: "ghostty",
13
- available: (context) => platform === "darwin" && context.env.TERM_PROGRAM?.toLowerCase() === "ghostty",
16
+ available: (context) => {
17
+ // cmux 内嵌 Ghostty 内核, 也会把 TERM_PROGRAM 设成 "ghostty"。仅凭环境变量会让 plugin
18
+ // 在 cmux 下误接管, 随后 osascript 连不上真正的 Ghostty 应用而抛 -1728/-2741。
19
+ if (platform !== "darwin") return false;
20
+ if (context.env.TERM_PROGRAM?.toLowerCase() !== "ghostty") return false;
21
+ // macOS GUI 应用启动子进程时注入 __CFBundleIdentifier, 标识当前终端宿主 app。
22
+ // cmux 的 bundle id 是 com.cmuxterm.app, 而非 Ghostty; 即便系统同时装了独立 Ghostty,
23
+ // 也能据此判定当前终端不是 Ghostty, 避免误连独立 Ghostty 的窗口。
24
+ return context.env.__CFBundleIdentifier?.trim() === GHOSTTY_BUNDLE_ID;
25
+ },
14
26
  owns: () => false,
15
27
  open: (context, launch, params) => openGhosttyInspector(context, launch, params, deps.runner),
16
28
  };
@@ -612,6 +612,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
612
612
  start: () => void;
613
613
  activateTransport: () => void;
614
614
  findPendingAsks: (target: { runId: string; agent: string; childIndex: number }) => string[];
615
+ hasPendingRequests: () => boolean;
615
616
  dispose: () => void;
616
617
  pending: Map<string, PendingSupervisorRequest>;
617
618
  getSupervisorRequestState: (event: ControlEvent) => SupervisorRequestState;
@@ -732,11 +733,15 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
732
733
  continue;
733
734
  }
734
735
  seenFiles.add(file);
735
- if (request.expectsReply) {
736
- rememberPendingRequest(request);
737
- pending.set(request.id, request);
738
- markForegroundSupervisorAttention(request, state);
736
+ if (!request.expectsReply) {
737
+ // Progress is already visible through child activity; do not inject a
738
+ // parent message or trigger a parent model turn.
739
+ removeRequestFile(request.requestFile);
740
+ continue;
739
741
  }
742
+ rememberPendingRequest(request);
743
+ pending.set(request.id, request);
744
+ markForegroundSupervisorAttention(request, state);
740
745
  // The ask is already queued above. A sendMessage failure (no UI, stale context) must not
741
746
  // lose it, and must not abort the loop before the remaining asks register.
742
747
  try {
@@ -755,25 +760,19 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
755
760
  ...(request.childTarget ? { childTarget: request.childTarget } : {}),
756
761
  ...(request.interview !== undefined ? { interview: request.interview } : {}),
757
762
  requestBody: request.message,
758
- ...(request.expectsReply ? { replyHint: supervisorReplyHint(request.id) } : {}),
763
+ replyHint: supervisorReplyHint(request.id),
759
764
  },
760
765
  }, { triggerTurn: true });
761
- // sendMessage accepts synchronously; one-way updates stay on disk until it returns.
762
- if (!request.expectsReply) removeRequestFile(request.requestFile);
763
766
  } catch (error) {
764
- // Allow an existing later scan to retry an unaccepted one-way update.
765
- if (!request.expectsReply) seenFiles.delete(file);
766
767
  console.error(`Failed to surface supervisor request ${request.id} as a user turn:`, error);
767
768
  }
768
- if (request.expectsReply) {
769
- (pi as { events?: IntercomEventBus }).events?.emit(INTERCOM_DETACH_REQUEST_EVENT, {
770
- requestId: request.id,
771
- runId: request.runId,
772
- agent: request.agent,
773
- childIndex: request.childIndex,
774
- });
775
- if (pending.has(request.id)) markForegroundSupervisorAttention(request, state);
776
- }
769
+ (pi as { events?: IntercomEventBus }).events?.emit(INTERCOM_DETACH_REQUEST_EVENT, {
770
+ requestId: request.id,
771
+ runId: request.runId,
772
+ agent: request.agent,
773
+ childIndex: request.childIndex,
774
+ });
775
+ if (pending.has(request.id)) markForegroundSupervisorAttention(request, state);
777
776
  }
778
777
  channels?.retire?.();
779
778
  };
@@ -860,6 +859,11 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
860
859
  && requestLifecycle(request, state, now, runState(request)) === "pending" ? [request.id] : [];
861
860
  }).sort();
862
861
  },
862
+ hasPendingRequests: () => {
863
+ if (!started) return false;
864
+ poll();
865
+ return pending.size > 0;
866
+ },
863
867
  start: () => {
864
868
  if (started) return;
865
869
  started = true;
@@ -5,6 +5,8 @@ export const AUTHORITY_ACTIONS = [
5
5
  "scheduleCreate",
6
6
  "stopRun",
7
7
  "steerRun",
8
+ "inspectorOpen",
9
+ "projectOpen",
8
10
  ] as const;
9
11
 
10
12
  export type AuthorityAction = typeof AUTHORITY_ACTIONS[number];
@@ -18,6 +20,8 @@ const DEFAULT_AUTHORITY_POLICY: Record<AuthorityAction, AuthorityDecision> = {
18
20
  scheduleCreate: "auto",
19
21
  stopRun: "auto",
20
22
  steerRun: "auto",
23
+ inspectorOpen: "auto",
24
+ projectOpen: "confirm",
21
25
  };
22
26
 
23
27
  export function resolveAuthorityDecision(input: {
@@ -20,7 +20,7 @@ export type RecommendedRoleTier = "cheap" | "medium" | "strong";
20
20
  interface ProfileAgentOverride {
21
21
  model?: string;
22
22
  thinking?: string | false;
23
- fallbackModels?: string[] | false;
23
+ machine?: string;
24
24
  }
25
25
 
26
26
  export interface SubagentProfileFile {
@@ -144,10 +144,7 @@ function validateSubagentProfile(filePath: string, parsed: Record<string, unknow
144
144
  if (thinking !== undefined && thinking !== false && typeof thinking !== "string") {
145
145
  throw new Error(`Profile '${filePath}' has invalid thinking for '${name}'; expected a string or false.`);
146
146
  }
147
- const fallbackModels = override.fallbackModels;
148
- if (fallbackModels !== undefined && fallbackModels !== false && (!Array.isArray(fallbackModels) || fallbackModels.some((item) => typeof item !== "string"))) {
149
- throw new Error(`Profile '${filePath}' has invalid fallbackModels for '${name}'; expected an array of strings or false.`);
150
- }
147
+ if ((override as Record<string, unknown>).fallbackModels !== undefined) throw new Error(`Profile '${filePath}' uses removed field fallbackModels for '${name}'; configure one model instead.`);
151
148
  }
152
149
  const disableBuiltins = (subagents as Record<string, unknown>).disableBuiltins;
153
150
  if (disableBuiltins !== undefined && typeof disableBuiltins !== "boolean") {
@@ -489,10 +486,19 @@ export function applySubagentProfile(name: string): { filePath: string; settings
489
486
  : {};
490
487
  // A profile owns the complete agent mapping, but unrelated subagent settings
491
488
  // (notably disableBuiltins, modelScope, watchdog, etc.) survive profile switches.
489
+ // Machine placement is not a model choice, so an existing pin survives a profile switch too.
490
+ const agentOverrides: Record<string, ProfileAgentOverride> = { ...profile.subagents.agentOverrides };
491
+ const existingOverrides = existing.agentOverrides && typeof existing.agentOverrides === "object" && !Array.isArray(existing.agentOverrides)
492
+ ? existing.agentOverrides as Record<string, unknown>
493
+ : {};
494
+ for (const [name, value] of Object.entries(existingOverrides)) {
495
+ const machine = value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>).machine : undefined;
496
+ if (typeof machine === "string" && agentOverrides[name]?.machine === undefined) agentOverrides[name] = { ...agentOverrides[name], machine };
497
+ }
492
498
  settings.subagents = {
493
499
  ...existing,
494
500
  ...profile.subagents,
495
- agentOverrides: profile.subagents.agentOverrides,
501
+ agentOverrides,
496
502
  };
497
503
  writeJsonFile(settingsPath, settings);
498
504
  return { filePath, settingsPath };
@@ -76,7 +76,7 @@ export function releaseActiveRunIndex(asyncDir: string): void {
76
76
  releaseToolCallAliases(asyncDir);
77
77
  }
78
78
 
79
- export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state"], toolCallId?: string, options: { retryCapacityErrors?: boolean } = {}): void {
79
+ export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state"], toolCallId?: string, options: { retryCapacityErrors?: boolean; terminalIndexBeforeRelease?: boolean } = {}): void {
80
80
  const marker = markerPath(asyncDir);
81
81
  if (isActiveAsyncState(state)) {
82
82
  fs.mkdirSync(path.dirname(marker), { recursive: true });
@@ -94,6 +94,22 @@ export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state
94
94
  }
95
95
  return;
96
96
  }
97
+ if (options.terminalIndexBeforeRelease) {
98
+ fs.mkdirSync(path.dirname(marker), { recursive: true });
99
+ fs.writeFileSync(marker, "", { flag: "a" });
100
+ const status = readStatus(asyncDir);
101
+ if (status?.state === state) {
102
+ try {
103
+ updateTerminalRunIndex(asyncDir, status);
104
+ } catch (error) {
105
+ if (options.retryCapacityErrors) throw error;
106
+ console.error(`Failed to write async terminal-run index for '${asyncDir}':`, error);
107
+ return;
108
+ }
109
+ }
110
+ releaseActiveRunIndex(asyncDir);
111
+ return;
112
+ }
97
113
  releaseActiveRunIndex(asyncDir);
98
114
  const status = readStatus(asyncDir);
99
115
  if (status && status.state === state) {