pi-subagents 0.66.0 → 0.68.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 (172) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/reviewer.md +3 -2
  5. package/docs/agents.md +43 -15
  6. package/docs/configuration.md +67 -23
  7. package/docs/extension-api.md +38 -19
  8. package/docs/missions.md +10 -2
  9. package/docs/models.md +11 -79
  10. package/docs/observability.md +22 -12
  11. package/docs/standalone-background.md +59 -0
  12. package/docs/tool-reference.md +33 -20
  13. package/docs/watchdog.md +39 -10
  14. package/docs/workflows.md +37 -13
  15. package/index.ts +5 -2
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +4 -2
  18. package/prompts/parallel-review.md +1 -1
  19. package/runner-peer-loader.mjs +24 -0
  20. package/runner-peer-preload.mjs +32 -0
  21. package/skills/pi-subagents/SKILL.md +32 -21
  22. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  23. package/skills/pi-subagents/references/execution-controls.md +11 -9
  24. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  25. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  26. package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
  27. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  28. package/src/agents/advertised-agent-prompt.ts +34 -3
  29. package/src/agents/agent-management.ts +57 -58
  30. package/src/agents/agent-serializer.ts +4 -3
  31. package/src/agents/agents.ts +190 -71
  32. package/src/agents/builtin-names.ts +1 -0
  33. package/src/agents/chain-serializer.ts +5 -0
  34. package/src/agents/runtime-agent-registry.ts +7 -6
  35. package/src/api/delegation.ts +4 -0
  36. package/src/api/preflight.ts +94 -59
  37. package/src/api/required-child-extensions.ts +6 -0
  38. package/src/api/shared-types.ts +2 -0
  39. package/src/extension/config.ts +10 -37
  40. package/src/extension/fanout-child.ts +66 -4
  41. package/src/extension/herdr-pi-bridge.ts +160 -0
  42. package/src/extension/index.ts +62 -39
  43. package/src/extension/public-execution.ts +7 -5
  44. package/src/extension/rpc.ts +4 -0
  45. package/src/extension/schemas.ts +81 -81
  46. package/src/extension/tool-description.ts +30 -82
  47. package/src/inspectors/actions.ts +148 -0
  48. package/src/inspectors/ghostty/actions.ts +74 -0
  49. package/src/inspectors/ghostty/plugin.ts +17 -0
  50. package/src/inspectors/herdr/actions.ts +99 -179
  51. package/src/inspectors/herdr/plugin.ts +20 -0
  52. package/src/inspectors/herdr/project-panes.ts +1 -1
  53. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  54. package/src/inspectors/plugins.ts +8 -0
  55. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  56. package/src/inspectors/types.ts +51 -0
  57. package/src/intercom/intercom-bridge.ts +50 -8
  58. package/src/intercom/native-supervisor-channel.ts +44 -31
  59. package/src/policy/authority.ts +4 -0
  60. package/src/profiles/profiles.ts +12 -6
  61. package/src/runs/background/active-async-capacity.ts +4 -0
  62. package/src/runs/background/active-run-index.ts +17 -1
  63. package/src/runs/background/async-execution.ts +348 -176
  64. package/src/runs/background/async-job-tracker.ts +8 -6
  65. package/src/runs/background/async-resume.ts +17 -12
  66. package/src/runs/background/async-status.ts +15 -4
  67. package/src/runs/background/auto-drain.ts +23 -10
  68. package/src/runs/background/binary-bootstrap.ts +38 -0
  69. package/src/runs/background/chain-append.ts +1 -1
  70. package/src/runs/background/chain-root-attachment.ts +14 -33
  71. package/src/runs/background/fleet-view.ts +30 -2
  72. package/src/runs/background/notify.ts +105 -7
  73. package/src/runs/background/owned-process-tree.ts +29 -2
  74. package/src/runs/background/result-files.ts +8 -4
  75. package/src/runs/background/result-watcher.ts +19 -2
  76. package/src/runs/background/run-child-session.ts +81 -33
  77. package/src/runs/background/run-status.ts +3 -0
  78. package/src/runs/background/runner-aliases.ts +12 -33
  79. package/src/runs/background/runner-child-launch.ts +6 -1
  80. package/src/runs/background/runner-child-sessions.ts +5 -4
  81. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  82. package/src/runs/background/scheduled-runs.ts +51 -18
  83. package/src/runs/background/stale-run-reconciler.ts +35 -11
  84. package/src/runs/background/steering.ts +20 -2
  85. package/src/runs/background/subagent-runner.ts +441 -304
  86. package/src/runs/background/subagent-wait.ts +176 -28
  87. package/src/runs/background/wait-completions.ts +75 -27
  88. package/src/runs/background/wait-subscriptions.ts +9 -3
  89. package/src/runs/background/wait-tool.ts +5 -3
  90. package/src/runs/foreground/async-steering-action.ts +18 -7
  91. package/src/runs/foreground/async-stop-action.ts +93 -3
  92. package/src/runs/foreground/execution.ts +134 -248
  93. package/src/runs/foreground/foreground-history.ts +2 -1
  94. package/src/runs/foreground/prompt-audit.ts +3 -1
  95. package/src/runs/foreground/subagent-executor.ts +374 -178
  96. package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
  97. package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
  98. package/src/runs/shared/acceptance.ts +38 -11
  99. package/src/runs/shared/async-status-projection.ts +127 -33
  100. package/src/runs/shared/capability-ceiling.ts +2 -0
  101. package/src/runs/shared/child-hooks.ts +25 -10
  102. package/src/runs/shared/child-launch-plan.ts +15 -3
  103. package/src/runs/shared/child-launch.ts +28 -5
  104. package/src/runs/shared/child-lifecycle.ts +6 -3
  105. package/src/runs/shared/child-runtime-config.ts +8 -1
  106. package/src/runs/shared/child-session.ts +127 -52
  107. package/src/runs/shared/child-tool-plan.ts +142 -11
  108. package/src/runs/shared/completion-guard.ts +5 -3
  109. package/src/runs/shared/dynamic-fanout.ts +2 -2
  110. package/src/runs/shared/effective-system-prompt.ts +33 -0
  111. package/src/runs/shared/external-cli-contract.ts +11 -1
  112. package/src/runs/shared/external-cli-preflight.ts +6 -2
  113. package/src/runs/shared/external-cli-runner.ts +9 -7
  114. package/src/runs/shared/herdr-connection.ts +134 -0
  115. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  116. package/src/runs/shared/herdr-machine.ts +279 -0
  117. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  118. package/src/runs/shared/herdr-placed-run.ts +263 -0
  119. package/src/runs/shared/llm-intent-arbiter.ts +12 -3
  120. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  121. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
  122. package/src/runs/shared/model-scope.ts +1 -1
  123. package/src/runs/shared/nested-events.ts +11 -2
  124. package/src/runs/shared/orca-progress-tabs.ts +1 -1
  125. package/src/runs/shared/parallel-utils.ts +7 -2
  126. package/src/runs/shared/pi-spawn.ts +10 -0
  127. package/src/runs/shared/subagent-prompt-runtime.ts +12 -4
  128. package/src/runs/shared/task-intent.ts +46 -13
  129. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  130. package/src/runs/shared/worktree-setup-command.ts +27 -4
  131. package/src/runs/shared/worktree.ts +45 -15
  132. package/src/shared/child-cache-retention.ts +43 -0
  133. package/src/shared/fork-context.ts +15 -72
  134. package/src/shared/launch-contract.ts +68 -8
  135. package/src/shared/opencode-session-headers.ts +30 -0
  136. package/src/shared/pruned-fork.ts +1 -1
  137. package/src/shared/required-child-extensions.ts +81 -0
  138. package/src/shared/settings.ts +5 -2
  139. package/src/shared/shortcuts.ts +0 -4
  140. package/src/shared/types.ts +74 -30
  141. package/src/slash/delegation-adapters.ts +3 -1
  142. package/src/slash/delegation-request.ts +14 -0
  143. package/src/slash/slash-commands.ts +2 -7
  144. package/src/slash/subagents-admin.ts +24 -13
  145. package/src/tui/fleet-status.ts +164 -19
  146. package/src/tui/fleet.ts +16 -14
  147. package/src/tui/render.ts +168 -37
  148. package/src/watchdog/child-status.ts +28 -28
  149. package/src/watchdog/lsp-diagnostics.ts +1 -1
  150. package/src/watchdog/model-selection.ts +21 -1
  151. package/src/watchdog/permission-arbiter.ts +3 -1
  152. package/src/watchdog/register-child.ts +10 -2
  153. package/src/watchdog/register-main.ts +39 -35
  154. package/src/watchdog/render.ts +1 -1
  155. package/src/watchdog/review.ts +123 -74
  156. package/src/watchdog/rules.ts +1 -1
  157. package/src/watchdog/runtime.ts +100 -27
  158. package/src/watchdog/scope.ts +1 -1
  159. package/src/watchdog/settings.ts +3 -0
  160. package/src/watchdog/tool-actions.ts +13 -12
  161. package/src/watchdog/turn-delta.ts +23 -0
  162. package/src/watchdog/types.ts +5 -3
  163. package/src/watchdog/warning-format.ts +1 -1
  164. package/src/workflows/scripted-workflow.ts +279 -10
  165. package/src/workflows/workflow-checklist.ts +2 -2
  166. package/src/workflows/workflow-receipt.ts +21 -3
  167. package/src/workflows/workflow-resources.ts +13 -2
  168. package/runner-server-preload.mjs +0 -13
  169. package/src/runs/shared/model-exclusions.ts +0 -374
  170. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  171. package/src/runs/shared/readonly-session-evidence.ts +0 -307
  172. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -1,17 +1,11 @@
1
1
  import { splitKnownThinkingSuffix as splitThinkingSuffix, type ModelInfo as AvailableModelInfo } from "../../shared/model-info.ts";
2
- import type { Usage } from "../../shared/types.ts";
3
- import { filterFallbackCandidates, findModelExclusion, parseModelKey, recordModelFailure } from "./model-exclusions.ts";
4
2
  import { checkModelScope, type ModelScopeCheckRule, type ModelScopeViolation, type ModelSource } from "./model-scope.ts";
5
- import { redactSecretValues } from "./permissions.ts";
6
3
 
7
4
  export type { AvailableModelInfo };
8
5
 
9
- interface ModelAttemptSummary {
10
- model: string;
11
- success: boolean;
12
- exitCode?: number | null;
13
- error?: string;
14
- usage?: Usage;
6
+ export interface ModelSelectionEvidence {
7
+ model?: string;
8
+ requestedModel?: string;
15
9
  }
16
10
 
17
11
  export { splitThinkingSuffix };
@@ -290,57 +284,6 @@ function enforceModelScopes(
290
284
  for (const violation of violations) (onWarn ?? defaultScopeWarn)(violation);
291
285
  }
292
286
 
293
- const MODEL_EXCLUSION_DIAGNOSTIC_MAX_LENGTH = 240;
294
- const MODEL_EXCLUSION_DIAGNOSTIC_MAX_ENTRIES = 20;
295
-
296
- function sanitizeModelExclusionDiagnostic(value: string | undefined, fallback: string): string {
297
- const normalized = typeof value === "string"
298
- ? value.replace(/[\u0000-\u001f\u007f\u2028\u2029]+/g, " ").trim()
299
- : "";
300
- return redactSecretValues(normalized || fallback).slice(0, MODEL_EXCLUSION_DIAGNOSTIC_MAX_LENGTH);
301
- }
302
-
303
- function formatModelExclusionExpiry(expiresAt: number): string {
304
- if (!Number.isFinite(expiresAt)) return "unknown";
305
- const date = new Date(expiresAt);
306
- return Number.isNaN(date.getTime()) ? "unknown" : date.toISOString();
307
- }
308
-
309
- function formatExcludedCandidateEvidence(candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>): string {
310
- const { provider, modelId } = parseModelKey(candidate);
311
- const displayCandidate = sanitizeModelExclusionDiagnostic(candidate, "unknown");
312
- const displayModel = sanitizeModelExclusionDiagnostic(modelId, "unknown");
313
- const displayProvider = sanitizeModelExclusionDiagnostic(provider ?? exclusion.provider, "unspecified");
314
- const reason = sanitizeModelExclusionDiagnostic(exclusion.reason, "runtime-failure");
315
- return `${displayCandidate} — model: ${displayModel}; provider: ${displayProvider}; reason: ${reason}; expires: ${formatModelExclusionExpiry(exclusion.expiresAt)}`;
316
- }
317
-
318
- const MODEL_UNAVAILABLE_EXCLUSION_PATTERNS = [
319
- /model.*not found/i,
320
- /unknown model/i,
321
- /model.*unavailable/i,
322
- /model.*disabled/i,
323
- ];
324
-
325
- function isCurrentRegistryModel(candidate: string, availableModels: AvailableModelInfo[] | undefined): boolean {
326
- if (!availableModels || availableModels.length === 0) return false;
327
- const { baseModel } = splitThinkingSuffix(candidate);
328
- return availableModels.some((entry) => entry.fullId === baseModel);
329
- }
330
-
331
- function ignoreStaleModelUnavailableExclusion(candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>, availableModels: AvailableModelInfo[] | undefined): boolean {
332
- const reason = exclusion.reason ?? "";
333
- return MODEL_UNAVAILABLE_EXCLUSION_PATTERNS.some((pattern) => pattern.test(reason)) && isCurrentRegistryModel(candidate, availableModels);
334
- }
335
-
336
- function throwForExplicitModelExclusion(model: string): void {
337
- const exclusion = findModelExclusion(model);
338
- if (!exclusion) return;
339
- const reason = redactSecretValues((exclusion.reason ?? "runtime-failure").replace(/[\u0000-\u001f\u007f]+/g, " ")).slice(0, 240);
340
- const expiry = Number.isFinite(exclusion.expiresAt) ? `; expires: ${new Date(exclusion.expiresAt).toISOString()}` : "";
341
- throw new Error(`Requested subagent model '${model}' is excluded and cannot be replaced by a fallback (reason: ${reason}${expiry}).`);
342
- }
343
-
344
287
  /**
345
288
  * Resolve the `--model` override passed to a spawned subagent.
346
289
  *
@@ -377,7 +320,6 @@ export function resolveSubagentModelOverride(
377
320
  const candidate = resolveSubagentModelCandidate(explicit, availableModels, preferredProvider);
378
321
  if (options?.source === "explicit") {
379
322
  resolved = candidate ?? resolveRequiredSubagentModelCandidate(explicit, availableModels, preferredProvider);
380
- throwForExplicitModelExclusion(resolved);
381
323
  resolvedFromRegistry = true;
382
324
  } else if (candidate) {
383
325
  resolved = candidate;
@@ -421,19 +363,15 @@ export function resolveEffectiveSubagentModel(
421
363
 
422
364
  export type ModelOrigin = ModelSource | "configured";
423
365
 
424
- export interface BuildModelCandidatesOptions {
425
- /** Fallback models warn by default and throw when strict scope enforcement is enabled. */
366
+ export interface ResolveModelSelectionOptions {
426
367
  scope?: ModelScopeCheckRule | ModelScopeCheckRule[];
427
368
  onWarn?: (violation: ModelScopeViolation) => void;
428
369
  /** The primary model came from the running parent session, not configuration. */
429
370
  primaryModelFromParent?: boolean;
430
- /** How the primary model was selected. Explicit stays strict and does not rotate to fallbacks. */
371
+ /** How the model was selected. */
431
372
  origin?: ModelOrigin;
432
373
  }
433
374
 
434
- const ZERO_USABLE_MODEL_CANDIDATES_ERROR =
435
- "No usable subagent models remain after registry, scope, and cached-exclusion filtering.";
436
-
437
375
  export function resolveModelOrigin(input: {
438
376
  explicitModel?: string | boolean;
439
377
  agentModel?: string | boolean;
@@ -458,174 +396,30 @@ export function inheritsParentModel(
458
396
  return Boolean(parentModel && (!trimmed || trimmed === INHERIT_MODEL));
459
397
  }
460
398
 
461
- export function buildModelCandidates(
462
- primaryModel: string | undefined,
463
- fallbackModels: string[] | undefined,
399
+ export function resolveModelSelection(
400
+ model: string | undefined,
464
401
  availableModels: AvailableModelInfo[] | undefined,
465
402
  preferredProvider?: string,
466
- options?: BuildModelCandidatesOptions,
467
- ): string[] {
468
- if (!primaryModel) throwForUnresolvedEnforcedInheritScope(options?.scope, true);
403
+ options?: ResolveModelSelectionOptions,
404
+ ): ModelSelectionEvidence {
405
+ if (!model) throwForUnresolvedEnforcedInheritScope(options?.scope, true);
469
406
  const origin = options?.origin ?? (options?.primaryModelFromParent ? "inherited" : "configured");
407
+ const requestedModel = origin === "inherited" ? undefined : model;
470
408
  const scopes = configuredScopes(options?.scope);
471
- type ExcludedCandidate = { candidate: string; exclusion: NonNullable<ReturnType<typeof findModelExclusion>> };
472
- const excludedCandidates: ExcludedCandidate[] = [];
473
- let excludedCandidateCount = 0;
474
- const warnCachedExclusion = (candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>) => {
475
- excludedCandidateCount++;
476
- if (excludedCandidates.length < MODEL_EXCLUSION_DIAGNOSTIC_MAX_ENTRIES) excludedCandidates.push({ candidate, exclusion });
477
- const displayCandidate = sanitizeModelExclusionDiagnostic(candidate, "unknown");
478
- const reason = sanitizeModelExclusionDiagnostic(exclusion.reason, "runtime-failure");
479
- console.warn(`[pi-subagents] Skipping model '${displayCandidate}' due to a cached exclusion (reason: ${reason}; expires: ${formatModelExclusionExpiry(exclusion.expiresAt)}).`);
480
- };
481
- if (origin === "explicit" && primaryModel) {
482
- const normalized = resolveRequiredSubagentModelCandidate(primaryModel.trim(), availableModels, preferredProvider);
483
- throwForExplicitModelExclusion(normalized);
409
+ if (origin === "explicit" && model) {
410
+ const normalized = resolveRequiredSubagentModelCandidate(model.trim(), availableModels, preferredProvider);
484
411
  enforceModelScopes(normalized, scopes, "explicit", options?.onWarn);
485
- primaryModel = normalized;
486
- }
487
- const seen = new Set<string>();
488
- const candidates: string[] = [];
489
- const rawCandidates = [primaryModel, ...(fallbackModels ?? [])];
490
- let skippedPrimary: string | undefined;
491
- let skippedFallback: string | undefined;
492
- for (let index = 0; index < rawCandidates.length; index++) {
493
- const raw = rawCandidates[index];
494
- if (!raw) continue;
495
- const model = raw.trim();
496
- const normalized = index === 0 && (origin === "inherited" || origin === "explicit" || options?.primaryModelFromParent)
497
- ? model
498
- : resolveSubagentModelCandidate(model, availableModels, preferredProvider);
499
- if (!normalized) {
500
- if (index === 0) skippedPrimary = model;
501
- else {
502
- skippedFallback ??= model;
503
- console.warn(`[pi-subagents] Skipping fallback model '${model}' because it is unavailable in this environment.`);
504
- }
505
- continue;
506
- }
507
- if (seen.has(normalized)) continue;
508
- if (index > 0 || scopes.some((scope) => scope.enforce === true && scope.strict === true)) {
509
- enforceModelScopes(normalized, scopes, "inherited", options?.onWarn);
510
- }
511
- seen.add(normalized);
512
- candidates.push(normalized);
513
- }
514
- const resolved = filterFallbackCandidates(candidates, {
515
- onExcluded: warnCachedExclusion,
516
- ignoreExclusion: (candidate, exclusion) => ignoreStaleModelUnavailableExclusion(candidate, exclusion, availableModels),
517
- });
518
- if (resolved.length === 0) {
519
- if (skippedPrimary) resolveRequiredSubagentModelCandidate(skippedPrimary, availableModels, preferredProvider);
520
- if (candidates.length === 0 && skippedFallback) resolveRequiredSubagentModelCandidate(skippedFallback, availableModels, preferredProvider);
521
- if (candidates.length > 0) {
522
- const shownExclusions = excludedCandidates;
523
- const omittedExclusions = excludedCandidateCount - shownExclusions.length;
524
- const evidence = shownExclusions.length > 0
525
- ? ` (excluded: ${shownExclusions.map(({ candidate, exclusion }) => formatExcludedCandidateEvidence(candidate, exclusion)).join("; ")}${omittedExclusions > 0 ? `; ... and ${omittedExclusions} more` : ""})`
526
- : "";
527
- throw new Error(`${ZERO_USABLE_MODEL_CANDIDATES_ERROR}${evidence}`);
528
- }
529
- return resolved;
412
+ model = normalized;
530
413
  }
531
- if (skippedPrimary) {
532
- console.warn(`[pi-subagents] Skipping primary model '${skippedPrimary}' because it is unavailable in this environment.`);
414
+ const resolved = model && (origin === "inherited" || origin === "explicit" || options?.primaryModelFromParent)
415
+ ? model.trim()
416
+ : model ? resolveRequiredSubagentModelCandidate(model.trim(), availableModels, preferredProvider) : undefined;
417
+ if (resolved && scopes.some((scope) => scope.enforce === true && scope.strict === true)) {
418
+ enforceModelScopes(resolved, scopes, "inherited", options?.onWarn);
533
419
  }
534
- return resolved;
535
- }
536
-
537
- const RETRYABLE_MODEL_FAILURE_PATTERNS = [
538
- /^REQUEST_LIMIT_EXCEEDED$/,
539
- /rate\s*limit/i,
540
- /usage\s*limit/i,
541
- /too many requests/i,
542
- /\b429\b/,
543
- /quota/i,
544
- /billing/i,
545
- /credit/i,
546
- /auth(?:entication)?/i,
547
- /unauthori[sz]ed/i,
548
- /forbidden/i,
549
- /api key/i,
550
- /token expired/i,
551
- /invalid key/i,
552
- /provider.*unavailable/i,
553
- /model.*unavailable/i,
554
- /model.*disabled/i,
555
- /model.*not found/i,
556
- /unknown model/i,
557
- /overloaded/i,
558
- /service unavailable/i,
559
- /temporar(?:ily)? unavailable/i,
560
- /connection\s+(?:error|reset|closed|aborted)/i,
561
- /connection refused/i,
562
- /fetch failed/i,
563
- /network error/i,
564
- /socket hang up/i,
565
- /stream ended without finish_reason/i,
566
- /upstream/i,
567
- /timed? out/i,
568
- /timeout/i,
569
- /\b500\b/,
570
- /\b502\b/,
571
- /\b503\b/,
572
- /\b504\b/,
573
- /internal server error/i,
574
- /cold.?start/i,
575
- /empty response/i,
576
- /no output/i,
577
- /model.*(?:load|fail|error)/i,
578
- ];
579
-
580
- /**
581
- * Failures reported as `<tool> failed (exit N): ...` or `<tool> failed with
582
- * exit code N` come from a tool call inside the child's task, not from the
583
- * provider/model, however network-flavored their details read. Retrying a
584
- * different model cannot fix them and would rerun the whole task. Tool names
585
- * include namespaced forms like `mcp.server/write`.
586
- */
587
- const TOOL_FAILURE_PREFIX = /^[\w.:@/-]+ failed (?:(?:\(exit \d+\):)|(?:with exit code \d+))(?:\s|$)/i;
588
-
589
- export function isRetryableModelFailure(error: string | undefined): boolean {
590
- if (!error) return false;
591
- if (TOOL_FAILURE_PREFIX.test(error.trim())) return false;
592
- return RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(error));
593
- }
594
-
595
- function messageError(message: unknown): string | undefined {
596
- if (!message || typeof message !== "object") return undefined;
597
- const value = (message as { errorMessage?: unknown }).errorMessage;
598
- return typeof value === "string" ? value : undefined;
599
- }
600
-
601
- export function isRetryableModelFailureAttempt(input: { error: string | undefined; messages?: readonly unknown[]; toolCount?: number }): boolean {
602
- if (!isRetryableModelFailure(input.error)) return false;
603
- if ((input.toolCount ?? 0) > 0) return false;
604
- if (input.error === "Subagent produced no output (possible model cold-start or empty response)." || /^Subagent produced no output after terminal assistant stopReason "[^"]+"\.$/.test(input.error ?? "")) return true;
605
- if ((input.toolCount ?? 0) === 0 && (input.messages?.length ?? 0) === 0) return true;
606
- const error = input.error?.trim();
607
- return Boolean(error && input.messages?.some((message) => messageError(message)?.trim() === error));
608
- }
609
-
610
- // Request-shape failures can match broad fallback signals such as "upstream",
611
- // but do not establish that the model is unhealthy for subsequent requests.
612
- const REQUEST_SHAPE_FAILURE_PATTERN = /\b(?:bad[ _]request|invalid[ _]argument|invalid_request_error)\b/i;
613
-
614
- export function recordRetryableModelFailure(model: string | undefined, error: string | undefined): void {
615
- if (!model || !error || !isRetryableModelFailure(error) || isContextOverflow(error)) return;
616
- if (REQUEST_SHAPE_FAILURE_PATTERN.test(error)) return;
617
- const { provider, modelId } = parseModelKey(model);
618
- recordModelFailure({ modelId, reason: error, ...(provider ? { provider } : {}) });
420
+ return { ...(resolved ? { model: resolved } : {}), ...(requestedModel ? { requestedModel } : {}) };
619
421
  }
620
-
621
- /**
622
- * Context-overflow signals. These are deliberately NOT part of
623
- * {@link RETRYABLE_MODEL_FAILURE_PATTERNS}: an overflow means the input was too
624
- * large for the model's context window, so retrying the same input on another
625
- * model (or the same model again) cannot succeed. Callers should treat overflow
626
- * as a terminal, non-retryable failure and surface a clear "input too large"
627
- * error instead of burning fallback attempts on a guaranteed failure.
628
- */
422
+ /** Context-overflow signals used to surface a clear input-too-large error. */
629
423
  const CONTEXT_OVERFLOW_PATTERNS = [
630
424
  /context(?: length| window| limit)? (?:exceed|overflow|too long)/i,
631
425
  /maximum context length/i,
@@ -642,13 +436,6 @@ const CONTEXT_OVERFLOW_PATTERNS = [
642
436
 
643
437
  export function isContextOverflow(error: string | undefined): boolean {
644
438
  if (!error) return false;
645
- if (TOOL_FAILURE_PREFIX.test(error.trim())) return false;
439
+ if (/^[\w.:@/-]+ failed (?:(?:\(exit \d+\):)|(?:with exit code \d+))(?:\s|$)/i.test(error.trim())) return false;
646
440
  return CONTEXT_OVERFLOW_PATTERNS.some((pattern) => pattern.test(error));
647
441
  }
648
-
649
- export function formatModelAttemptNote(attempt: ModelAttemptSummary, nextModel?: string): string {
650
- const failure = attempt.error?.trim() || `exit ${attempt.exitCode ?? 1}`;
651
- return nextModel
652
- ? `[fallback] ${attempt.model} failed: ${failure}. Retrying with ${nextModel}.`
653
- : `[fallback] ${attempt.model} failed: ${failure}.`;
654
- }
@@ -17,7 +17,7 @@ import { splitKnownThinkingSuffix } from "../../shared/model-info.ts";
17
17
 
18
18
  export interface ModelScopeRule {
19
19
  enforce?: boolean;
20
- /** Reject inherited and fallback models outside the allowlist instead of warning. */
20
+ /** Reject inherited models outside the allowlist instead of warning. */
21
21
  strict?: boolean;
22
22
  /** Glob-style allow patterns (only `*` is special), matched against `provider/id`. */
23
23
  allow?: string[];
@@ -232,10 +232,12 @@ function sanitizeLaunchResolvedExtensions(value: unknown): LaunchResolvedChildEx
232
232
  disableAmbientExtensions: raw.disableAmbientExtensions,
233
233
  runtime: stringList(raw.runtime),
234
234
  configured: stringList(raw.configured),
235
+ required: Array.isArray(raw.required) ? raw.required.filter((item): item is string => typeof item === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(item)).slice(0, 32) : [],
235
236
  effective: stringList(raw.effective),
236
237
  omitted: {
237
238
  runtime: omittedCount("runtime"),
238
239
  configured: omittedCount("configured"),
240
+ required: omittedCount("required"),
239
241
  effective: omittedCount("effective"),
240
242
  },
241
243
  };
@@ -1063,8 +1065,15 @@ export function isTopLevelAsyncDir(asyncDir: string): boolean {
1063
1065
  return containedPath(DIRS.async, resolved) && !containedPath(path.join(TEMP_ROOT_DIR, "nested-subagent-runs"), resolved);
1064
1066
  }
1065
1067
 
1066
- export function nestedResultsPath(rootRunId: string, id: string): string {
1068
+ export function nestedRunScope(rootRunId: string) {
1067
1069
  assertSafeId("rootRunId", rootRunId);
1070
+ return {
1071
+ asyncDirRoot: path.join(TEMP_ROOT_DIR, "nested-subagent-runs", rootRunId),
1072
+ resultsDir: path.join(DIRS.results, "nested", rootRunId),
1073
+ };
1074
+ }
1075
+
1076
+ export function nestedResultsPath(rootRunId: string, id: string): string {
1068
1077
  assertSafeId("id", id);
1069
- return path.join(DIRS.results, "nested", rootRunId, `${id}.json`);
1078
+ return path.join(nestedRunScope(rootRunId).resultsDir, `${id}.json`);
1070
1079
  }
@@ -29,7 +29,7 @@ const ORCA_CREATE_WATCHDOG_SCRIPT = [
29
29
  "function exists(file){try{return fs.existsSync(file)}catch{return false}}",
30
30
  "function keepQueued(){try{const now=new Date();fs.utimesSync(done,now,now)}catch{}}",
31
31
  "function predecessorReady(){if(previous==='-')return true;if(exists(previous.replace(/\\.pending$/,'.ready')))return true;try{return Date.now()-fs.statSync(previous).mtimeMs>=waitTimeout}catch{return true}}",
32
- "function updateManifest(state,stdout=''){if(manifest==='-')return;try{const payload=JSON.parse(fs.readFileSync(manifest,'utf8'));payload.state=state;payload.updatedAt=new Date().toISOString();const raw=stdout.trim().split(/\\r?\\n/).filter(Boolean).at(-1);if(raw){try{payload.orca=JSON.parse(raw)}catch{payload.orcaRaw=raw.slice(0,4096)}}fs.writeFileSync(manifest,JSON.stringify(payload,null,2)+'\\n')}catch{}}",
32
+ "function updateManifest(state,stdout=''){if(manifest==='-')return;try{const payload=JSON.parse(fs.readFileSync(manifest,'utf8'));payload.state=state;payload.updatedAt=new Date().toISOString();const raw=stdout.trim();if(raw){try{payload.orca=JSON.parse(raw)}catch{payload.orcaRaw=raw.slice(0,4096)}}fs.writeFileSync(manifest,JSON.stringify(payload,null,2)+'\\n')}catch{}}",
33
33
  "function start(){",
34
34
  " try{",
35
35
  " const child=spawn(command,args,{stdio:['ignore','pipe','ignore'],windowsHide:true});",
@@ -10,6 +10,10 @@ export interface RunnerSubagentStep {
10
10
  sessionName?: string;
11
11
  task: string;
12
12
  runner?: ResolvedRunnerConfig;
13
+ /** Herdr saved machine this external-cli step runs on; `cwd` is then the directory on that machine. */
14
+ machine?: import("../../shared/types.ts").HerdrMachineReference;
15
+ remoteReads?: string[] | false;
16
+ machineEnv?: Record<string, string>;
13
17
  externalJobFollowUp?: {
14
18
  sourceRunId: string;
15
19
  sourceStepIndex: number;
@@ -37,7 +41,7 @@ export interface RunnerSubagentStep {
37
41
  fast?: boolean;
38
42
  thinking?: string;
39
43
  thinkingCeiling?: import("../../shared/model-info.ts").ThinkingLevel;
40
- modelCandidates?: string[];
44
+ requestedModel?: string;
41
45
  /** The primary model is inherited from the parent session and should not be verified against the child-reported active registry model. */
42
46
  skipPrimaryModelVerification?: boolean;
43
47
  modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string; contextWindow?: number }>;
@@ -47,6 +51,8 @@ export interface RunnerSubagentStep {
47
51
  allowNestedSubagents?: boolean;
48
52
  extensions?: string[];
49
53
  subagentOnlyExtensions?: string[];
54
+ /** Private immutable host policy snapshot serialized to the native runner. */
55
+ requiredExtensions?: import("../../shared/required-child-extensions.ts").RequiredChildExtensionSnapshot;
50
56
  mcpDirectTools?: string[];
51
57
  mutationTools?: string[];
52
58
  completionGuard?: boolean;
@@ -237,7 +243,6 @@ export interface ParallelTaskResult {
237
243
  error?: string;
238
244
  timedOut?: boolean;
239
245
  model?: string;
240
- attemptedModels?: string[];
241
246
  outputTargetPath?: string;
242
247
  outputTargetExists?: boolean;
243
248
  }
@@ -49,6 +49,7 @@ export interface PiSpawnDeps {
49
49
  platform?: NodeJS.Platform;
50
50
  execPath?: string;
51
51
  argv1?: string;
52
+ bunVersion?: string;
52
53
  existsSync?: (filePath: string) => boolean;
53
54
  realpathSync?: (filePath: string) => string;
54
55
  readFileSync?: (filePath: string, encoding: "utf-8") => string;
@@ -58,6 +59,15 @@ export interface PiSpawnDeps {
58
59
  env?: NodeJS.ProcessEnv;
59
60
  }
60
61
 
62
+ /** Compiled Pi's entrypoint is virtual; execPath is the real (possibly renamed) image. */
63
+ export function resolveBunPiExecutable(deps: PiSpawnDeps = {}): string | undefined {
64
+ const bunVersion = deps.bunVersion ?? process.versions.bun;
65
+ const entry = deps.argv1 ?? process.argv[1];
66
+ if (!bunVersion || !entry || !/^(?:\/\$bunfs\/|B:[\\/]~BUN[\\/])/.test(entry)) return undefined;
67
+ const env = deps.env ?? process.env;
68
+ return env[PI_SUBAGENT_PI_BINARY_ENV]?.trim() || (deps.execPath ?? process.execPath);
69
+ }
70
+
61
71
  interface PiSpawnCommand {
62
72
  command: string;
63
73
  args: string[];
@@ -17,6 +17,7 @@ import { registerChildWatchdog } from "../../watchdog/register-child.ts";
17
17
  import type { ChildWatchdogConfig } from "../../watchdog/child-status.ts";
18
18
  import { requestWatchdogPermission, type WatchdogPermissionRequest, type WatchdogPermissionResult } from "../../watchdog/permission-arbiter.ts";
19
19
  import { SUBAGENT_WATCHDOG_WARNING_TYPE } from "../../watchdog/types.ts";
20
+ import { inheritedNestedRouteOf } from "./nested-events.ts";
20
21
  import { registerWaitTool } from "../background/wait-tool.ts";
21
22
  import { drainOutstandingWork } from "../background/auto-drain.ts";
22
23
  import {
@@ -451,10 +452,11 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI, config?:
451
452
  registerPermissionGate(pi, config.permissions, config.childWatchdog);
452
453
  registerToolBudget(pi, config.toolBudget);
453
454
  registerChildWatchdog(pi, config.childWatchdog, config.watchdogStatus);
454
- const waitState = {
455
+ const waitState = config.runtimeState ?? {
455
456
  baseCwd: "",
456
457
  currentSessionId: null,
457
458
  asyncJobs: new Map(),
459
+ foregroundRuns: new Map(),
458
460
  foregroundControls: new Map(),
459
461
  lastForegroundControlId: null,
460
462
  cleanupTimers: new Map(),
@@ -465,7 +467,8 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI, config?:
465
467
  watcherRestartTimer: null,
466
468
  resultFileCoalescer: { schedule: () => false, clear: () => {} },
467
469
  } as unknown as SubagentState;
468
- if (typeof pi.registerTool === "function") registerWaitTool(pi, waitState, config.waitTool.enabled, undefined, config.waitTool.defaultTimeoutMs);
470
+ const nestedRootRunId = inheritedNestedRouteOf(config)?.rootRunId;
471
+ if (typeof pi.registerTool === "function") registerWaitTool(pi, waitState, config.waitTool.enabled, undefined, config.waitTool.defaultTimeoutMs, { nestedRootRunId });
469
472
  const supervisorMetadata = childSupervisorMetadata(config);
470
473
  let nativeSupervisorClientRegistered = false;
471
474
  const registerNativeSupervisorClientOnce = (): void => {
@@ -486,13 +489,18 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI, config?:
486
489
  if (diagnostic) throw new Error(formatChildToolDiagnostic(diagnostic));
487
490
  });
488
491
  onRuntimeEvent("agent_end", async (_event: unknown, ctx: unknown) => {
489
- if ((ctx as { hasUI?: boolean } | undefined)?.hasUI === true) { drainObservation?.deny(); return; }
492
+ if ((ctx as { hasUI?: boolean } | undefined)?.hasUI === true) drainObservation?.deny();
490
493
  if (drainObservation) {
491
494
  try {
492
495
  if ((ctx as ExtensionContext)?.sessionManager?.getSessionFile() !== waitState.currentSessionId) drainObservation.deny();
493
496
  } catch { drainObservation.deny(); }
494
497
  }
495
- await drainOutstandingWork({ state: waitState, events: pi.events }, drainObservation);
498
+ config.holdFinalDrain?.(true);
499
+ try {
500
+ await drainOutstandingWork({ state: waitState, events: pi.events, nestedRootRunId, hasPendingSupervisorRequest: config.hasPendingSupervisorRequest }, drainObservation);
501
+ } finally {
502
+ config.holdFinalDrain?.(false);
503
+ }
496
504
  });
497
505
  if (config.structuredOutput) registerStructuredOutputTool(pi, config.structuredOutput);
498
506
 
@@ -7,6 +7,7 @@
7
7
  * - `classifyTaskMutationIntent` / `expectsImplementationMutation`: does the
8
8
  * task REQUIRE file changes? Consumed by the completion mutation guard,
9
9
  * which blocks completion, so its vocabulary is deliberately narrow.
10
+ * Verbs inside filenames and path-like tokens do not count.
10
11
  * - `taskMayMutate`: COULD the task plausibly change files? Consumed by
11
12
  * acceptance level inference, which only raises evidence gates, so its
12
13
  * vocabulary is deliberately broad (any bare write verb).
@@ -18,16 +19,20 @@
18
19
  */
19
20
 
20
21
  const REVIEW_ONLY_PATTERNS = [
21
- /\breview only\b/i,
22
+ /\b(?:review|read)[- ]only\b/i,
23
+ /\bno\s+(?:source\s+)?edits?\b/i,
24
+ /\bwithout\s+edits?\b/i,
22
25
  /\bsuggest fixes only\b/i,
23
26
  /\bonly return findings\b/i,
24
27
  /\breturn findings only\b/i,
25
28
  ];
26
29
 
30
+ const NO_EDIT_BOUNDARY_ASSERTION_PATTERN = /\bno\s+(?:source\s+)?edits?\s*,\s*(?:commits?|pushes?|merges?|installs?|changes?|writes?)\b/i;
31
+
27
32
  const REVIEWER_REQUIRED_EDIT_PATTERNS = [
28
33
  /\bmust\s+(?:edit|modify|change|fix|patch|apply|implement)\b/i,
29
34
  /\brequired\s+to\s+(?:edit|modify|change|fix|patch|apply|implement)\b/i,
30
- /(?:^|[.!?\n]\s*)implement\s+(?:the\s+)?(?:approved|requested|specified|file|code|source|fix(?:es)?|changes?)\b/i,
35
+ /(?:^|[.!?:;,\n]\s*)implement\s+(?:the\s+)?(?:approved|requested|specified|file|code|source|fix(?:es)?|changes?)\b/i,
31
36
  /\bregardless\s+of\s+findings\b/i,
32
37
  /\balways\s+(?:edit|modify|change|fix|patch|apply|implement)\b/i,
33
38
  /\bapply\s+(?:the\s+)?fix(?:es)?\s+directly\b/i,
@@ -40,7 +45,7 @@ const REVIEWER_REQUIRED_EDIT_PATTERNS = [
40
45
  // being swallowed as the object.
41
46
  // Accept serialized line separators too: workflow prompts can carry literal
42
47
  // `\\n`/`\\r\\n` between clauses instead of decoded newlines.
43
- const NO_EDIT_PROHIBITION_PATTERN = /(?:\b|\\(?:r\\n|n))(?:do not|don't|must not)\s+(?:edit|modify|write(?:\s+to)?|touch|change)\b((?:(?!\b(?:but|and|then)\b|\\(?:r\\n|n))[^.;,:!?\n–—-])*)/gi;
48
+ const NO_EDIT_PROHIBITION_PATTERN = /(?:\b|\\(?:r\\n|n))(?:do not|don't|must not)\s+(?:edit|modify|write(?:\s+to)?|touch|change|implement)\b((?:(?!\b(?:but|and|then)\b|\\(?:r\\n|n))[^.;,:!?\n–—-])*)/gi;
44
49
  const COORDINATED_NO_EDIT_PROHIBITION_PATTERN = /(?:\b|\\(?:r\\n|n))(?:do not|don't|must not)\s+((?=(?:(?!\\(?:r\\n|n))[^.;:!?\n–—-])*\b(?:and|or)\s+(?:edit|modify|write(?:\s+to)?|touch|change)\b)(?:(?!\\(?:r\\n|n))[^.;:!?\n–—-])*?\b(?:and|or)\s+(?:edit|modify|write(?:\s+to)?|touch|change)\b(?:(?!\b(?:but|and|then)\b|\\(?:r\\n|n))[^.;,:!?\n–—-])*)/gi;
45
50
 
46
51
  /** Objects of a no-edit prohibition that mean "the codebase in general" rather than a named scope. */
@@ -85,13 +90,25 @@ const RESEARCH_AGENT_PATTERNS = [
85
90
  // CLI flags like "--fix" and genuine clause-level dashes like "branch—fix it"),
86
91
  // strip the known severity compounds (must|should|needs + dash + verb) from the
87
92
  // task text before matching. Dash coverage: ASCII hyphen + U+2010..U+2015.
88
- const SEVERITY_COMPOUND_PATTERN = /\b(?:must|should|needs)[\-\u2010-\u2015](?:fix|edit|update|add|remove|replace|create|apply|make|do|implement|modify|delete|patch)\b/gi;
93
+ const SEVERITY_COMPOUND_PATTERN = /\b(?:must|should|needs)[-\u2010-\u2015](?:fix|edit|update|add|remove|replace|create|apply|make|do|implement|modify|delete|patch)\b/gi;
89
94
 
90
95
  function stripSeverityCompounds(task: string): string {
91
96
  return task.replace(SEVERITY_COMPOUND_PATTERN, " ");
92
97
  }
93
98
  export { stripSeverityCompounds };
94
99
 
100
+ // Quoted categories in a finding-classification request describe fixes, not
101
+ // instructions to perform them. Strip only that request, leaving sibling
102
+ // imperatives (including another "must fix") subject to the normal guards.
103
+ const FINDING_CLASSIFICATION_PATTERN = /\b(?:classify|categorize|weigh)\s+(?:(?:the|remaining)\s+)*(?:findings?|items?|issues?)\s+as\s+["“]must\s+fix\s+before\s+[^"”\n]+["”]\s+(?:vs\.?|versus)\s+["“]must\s+fix\s+before\s+[^"”\n]+["”]/gi;
104
+
105
+ const PATH_LIKE_TOKEN_PATTERN = /[^\s]+[/\\][^\s]+|[^\s/\\]+\.[A-Za-z][A-Za-z0-9]{0,9}\b/g;
106
+ const PATH_INTERNAL_IMPLEMENTATION_VERB = /\b(?:implement|edit|modify|refactor|delete|update|add|remove|replace|create)\b/i;
107
+
108
+ function stripPathLikeTokens(task: string): string {
109
+ return task.replace(PATH_LIKE_TOKEN_PATTERN, (token) => (PATH_INTERNAL_IMPLEMENTATION_VERB.test(token) ? " " : token));
110
+ }
111
+
95
112
  const FIX_OR_PATCH_IMPLEMENTATION_PATTERN = /\b(?:fix|patch)\s+(?:(?:it|this|that|them|each|any|all|these|those)\b|(?:(?:a|an|the|any|all)\s+)?(?:(?:failing|failed|broken|flaky|red|cold|start|current|existing|reported|approved|known|regression|unit|integration|e2e|source|typescript|type-?script|ts|type-?check|compiler)\s+)*(?:bug|defect|issues?|problems?|failures?|regressions?|tests?|errors?|items?|typos?|code|source|implementation|component|function|module|class|method|logic|file|files|readme|docs?|changelog|package\.json|config|manifest|extension|prompt|command|lint(?:ing)?|build|ci|type-?check|type\s+checking)\b)/i;
96
113
 
97
114
  const WORKER_IMPLEMENTATION_PATTERNS = [
@@ -103,12 +120,18 @@ const WORKER_IMPLEMENTATION_PATTERNS = [
103
120
  /\bdo those fixes\b/i,
104
121
  ];
105
122
 
106
- const GENERAL_IMPLEMENTATION_PATTERNS = [
123
+ const FOLLOW_ON_IMPLEMENTATION_PATTERN = /(?:^|[.!?:;,\n])\s*(?:fix|patch|update|add|remove|replace|create|delete)\s+(?:(?:the|a|an|this|that|these|those|requested|specified|current|existing|approved|your|our)\s+)(?!(?:report|summary|findings?|analysis|recommendations?|answer|response|proposal|plan|issue|bug report)\b)[a-z][\w./-]*/i;
124
+ const ADVISORY_INFINITIVE_PATTERN = /\b(?:explain|recommend|describe)\s+how\s+to\s+(?:fix|patch|update|add|remove|replace|create|delete|implement|edit|modify|refactor)\b/i;
125
+ const EXPLICIT_IMPLEMENTATION_PATTERNS = [
107
126
  /\b(?:implement|edit|modify|refactor)\b/i,
108
- FIX_OR_PATCH_IMPLEMENTATION_PATTERN,
109
127
  /\bapply\s+(?:the\s+)?(?:(?:suggested|proposed|recommended)\s+)?(?:changes?|fix(?:es)?|patch)\b/i,
110
128
  /\bmake\s+(?:the\s+)?changes\b/i,
111
129
  /\bdo those fixes\b/i,
130
+ ];
131
+
132
+ const GENERAL_IMPLEMENTATION_PATTERNS = [
133
+ ...EXPLICIT_IMPLEMENTATION_PATTERNS,
134
+ FIX_OR_PATCH_IMPLEMENTATION_PATTERN,
112
135
  /\b(?:update|add|remove|replace|delete|create)\s+(?:the\s+)?(?:file|files|code|source|implementation|test|tests|component|function|module|class|method|logic|import|imports|readme|docs?|changelog|package\.json|config|manifest|extension|prompt|command)\b/i,
113
136
  ];
114
137
 
@@ -141,10 +164,13 @@ interface NoEditProhibitionAnalysis {
141
164
  }
142
165
 
143
166
  function analyzeNoEditProhibitions(taskText: string): NoEditProhibitionAnalysis {
167
+ const readOnlyBoundary = NO_EDIT_BOUNDARY_ASSERTION_PATTERN.test(taskText);
144
168
  let present = REVIEW_ONLY_PATTERNS.some((pattern) => pattern.test(taskText))
145
- || NO_TOOL_INTENT_PATTERNS.some((pattern) => pattern.test(taskText));
146
- let blanket = present;
147
- let strippedText = stripPatterns(taskText, [...REVIEW_ONLY_PATTERNS, ...NO_TOOL_INTENT_PATTERNS]);
169
+ || NO_TOOL_INTENT_PATTERNS.some((pattern) => pattern.test(taskText))
170
+ || readOnlyBoundary;
171
+ let blanket = false;
172
+ let strippedText = stripPatterns(taskText, [...REVIEW_ONLY_PATTERNS, ...NO_TOOL_INTENT_PATTERNS, ADVISORY_INFINITIVE_PATTERN]);
173
+ if (readOnlyBoundary) strippedText = stripPatterns(strippedText, [NO_EDIT_BOUNDARY_ASSERTION_PATTERN]);
148
174
  const stripNoEditProhibition = (match: string, object: string, offset: number, source: string): string => {
149
175
  present = true;
150
176
  if (GENERIC_PROHIBITION_OBJECT.test(object) && !hasScopedProhibitionContinuation(source.slice(offset + match.length))) blanket = true;
@@ -172,16 +198,23 @@ function hasImplementationIntent(agent: string, taskText: string): boolean {
172
198
  }
173
199
 
174
200
  export function classifyTaskMutationIntent(agent: string, task: string): TaskMutationIntent {
175
- const taskText = stripSeverityCompounds(stripFrameworkInstructions(task));
201
+ const taskText = stripPatterns(stripSeverityCompounds(stripFrameworkInstructions(task)), [FINDING_CLASSIFICATION_PATTERN]);
176
202
  const taskTextWithoutScopedConstraints = stripPatterns(taskText, SCOPED_NO_EDIT_CONSTRAINT_PATTERNS);
177
203
  const prohibitions = analyzeNoEditProhibitions(taskTextWithoutScopedConstraints);
178
204
  if (prohibitions.present) {
179
205
  if (prohibitions.blanket) return { kind: "read-only" };
180
- return hasImplementationIntent(agent, prohibitions.strippedText) ? { kind: "implementation" } : { kind: "read-only" };
206
+ const remaining = stripPathLikeTokens(prohibitions.strippedText);
207
+ if (isReviewerStyleAgent(agent)) {
208
+ return hasImplementationIntent(agent, remaining) ? { kind: "implementation" } : { kind: "read-only" };
209
+ }
210
+ return EXPLICIT_IMPLEMENTATION_PATTERNS.some((pattern) => pattern.test(remaining))
211
+ || FOLLOW_ON_IMPLEMENTATION_PATTERN.test(remaining)
212
+ ? { kind: "implementation" }
213
+ : { kind: "read-only" };
181
214
  }
182
215
 
183
216
  if (RESEARCH_AGENT_PATTERNS.some((pattern) => pattern.test(agent))) return { kind: "read-only" };
184
- if (hasImplementationIntent(agent, taskText)) return { kind: "implementation" };
217
+ if (hasImplementationIntent(agent, stripPathLikeTokens(taskText))) return { kind: "implementation" };
185
218
  if (isReviewerStyleAgent(agent)) return { kind: "read-only" };
186
219
  return taskHasReadOnlyDeliverable(taskTextWithoutScopedConstraints) ? { kind: "read-only" } : { kind: "unknown" };
187
220
  }
@@ -204,7 +237,7 @@ const MAY_MUTATE_VERB_PATTERN = /\b(?:fix|implement|update|write|edit|modify|mig
204
237
  * does.
205
238
  */
206
239
  export function taskMayMutate(task: string): boolean {
207
- const taskText = stripPatterns(stripSeverityCompounds(stripFrameworkInstructions(task)), SCOPED_NO_EDIT_CONSTRAINT_PATTERNS);
240
+ const taskText = stripPatterns(stripSeverityCompounds(stripFrameworkInstructions(task)), [FINDING_CLASSIFICATION_PATTERN, ...SCOPED_NO_EDIT_CONSTRAINT_PATTERNS]);
208
241
  const prohibitions = analyzeNoEditProhibitions(taskText);
209
242
  if (prohibitions.blanket) return false;
210
243
  return MAY_MUTATE_VERB_PATTERN.test(stripPatterns(prohibitions.strippedText, READ_ONLY_DELIVERABLE_PATTERNS));
@@ -0,0 +1,18 @@
1
+ import type { AsyncStatus, SubagentState } from "../../shared/types.ts";
2
+
3
+ /** Projection-only guidance, not a control route or proof of current child liveness. */
4
+ export function workflowAsyncChildSteeringGuidance(status: AsyncStatus, state?: SubagentState): string[] {
5
+ if (status.mode !== "workflow" || (status.state !== "running" && status.state !== "queued")
6
+ || !state?.currentSessionId || status.sessionId !== state.currentSessionId
7
+ || !state.workflowControllers?.has(status.runId)) return [];
8
+ const ids = new Set((status.steps ?? []).flatMap((step) =>
9
+ step.async === true && step.status === "running"
10
+ && step.runner?.type !== "external-cli" && step.runner?.type !== "external-job"
11
+ && typeof step.runId === "string" && /^[A-Za-z0-9_-]+$/.test(step.runId) && step.runId !== status.runId
12
+ ? [step.runId] : []));
13
+ if (ids.size === 0) return [];
14
+ return [
15
+ "Async children recorded as running; direct steering rechecks controls. Choose an exact child ID (queued does not mean consumed):",
16
+ ...[...ids].map((id) => ` subagent({ action: "steer", id: "${id}", mode: "follow_up", message: "..." })`),
17
+ ];
18
+ }