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.
- package/CHANGELOG.md +138 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +43 -15
- package/docs/configuration.md +67 -23
- package/docs/extension-api.md +38 -19
- package/docs/missions.md +10 -2
- package/docs/models.md +11 -79
- package/docs/observability.md +22 -12
- package/docs/standalone-background.md +59 -0
- package/docs/tool-reference.md +33 -20
- package/docs/watchdog.md +39 -10
- package/docs/workflows.md +37 -13
- package/index.ts +5 -2
- package/inspector-runner.mjs +2 -2
- package/package.json +4 -2
- package/prompts/parallel-review.md +1 -1
- package/runner-peer-loader.mjs +24 -0
- package/runner-peer-preload.mjs +32 -0
- package/skills/pi-subagents/SKILL.md +32 -21
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +11 -9
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
- package/skills/pi-subagents/references/review-and-validation.md +3 -3
- package/src/agents/advertised-agent-prompt.ts +34 -3
- package/src/agents/agent-management.ts +57 -58
- package/src/agents/agent-serializer.ts +4 -3
- package/src/agents/agents.ts +190 -71
- package/src/agents/builtin-names.ts +1 -0
- package/src/agents/chain-serializer.ts +5 -0
- package/src/agents/runtime-agent-registry.ts +7 -6
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +94 -59
- package/src/api/required-child-extensions.ts +6 -0
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +10 -37
- package/src/extension/fanout-child.ts +66 -4
- package/src/extension/herdr-pi-bridge.ts +160 -0
- package/src/extension/index.ts +62 -39
- package/src/extension/public-execution.ts +7 -5
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +81 -81
- package/src/extension/tool-description.ts +30 -82
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +44 -31
- package/src/policy/authority.ts +4 -0
- package/src/profiles/profiles.ts +12 -6
- package/src/runs/background/active-async-capacity.ts +4 -0
- package/src/runs/background/active-run-index.ts +17 -1
- package/src/runs/background/async-execution.ts +348 -176
- package/src/runs/background/async-job-tracker.ts +8 -6
- package/src/runs/background/async-resume.ts +17 -12
- package/src/runs/background/async-status.ts +15 -4
- package/src/runs/background/auto-drain.ts +23 -10
- package/src/runs/background/binary-bootstrap.ts +38 -0
- package/src/runs/background/chain-append.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +14 -33
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +105 -7
- package/src/runs/background/owned-process-tree.ts +29 -2
- package/src/runs/background/result-files.ts +8 -4
- package/src/runs/background/result-watcher.ts +19 -2
- package/src/runs/background/run-child-session.ts +81 -33
- package/src/runs/background/run-status.ts +3 -0
- package/src/runs/background/runner-aliases.ts +12 -33
- package/src/runs/background/runner-child-launch.ts +6 -1
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/runner-http-dispatcher.ts +119 -0
- package/src/runs/background/scheduled-runs.ts +51 -18
- package/src/runs/background/stale-run-reconciler.ts +35 -11
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +441 -304
- package/src/runs/background/subagent-wait.ts +176 -28
- package/src/runs/background/wait-completions.ts +75 -27
- package/src/runs/background/wait-subscriptions.ts +9 -3
- package/src/runs/background/wait-tool.ts +5 -3
- package/src/runs/foreground/async-steering-action.ts +18 -7
- package/src/runs/foreground/async-stop-action.ts +93 -3
- package/src/runs/foreground/execution.ts +134 -248
- package/src/runs/foreground/foreground-history.ts +2 -1
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +374 -178
- package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
- package/src/runs/shared/acceptance.ts +38 -11
- package/src/runs/shared/async-status-projection.ts +127 -33
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +25 -10
- package/src/runs/shared/child-launch-plan.ts +15 -3
- package/src/runs/shared/child-launch.ts +28 -5
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +8 -1
- package/src/runs/shared/child-session.ts +127 -52
- package/src/runs/shared/child-tool-plan.ts +142 -11
- package/src/runs/shared/completion-guard.ts +5 -3
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-contract.ts +11 -1
- package/src/runs/shared/external-cli-preflight.ts +6 -2
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/herdr-connection.ts +134 -0
- package/src/runs/shared/herdr-external-adapters.ts +169 -0
- package/src/runs/shared/herdr-machine.ts +279 -0
- package/src/runs/shared/herdr-pi-protocol.ts +59 -0
- package/src/runs/shared/herdr-placed-run.ts +263 -0
- package/src/runs/shared/llm-intent-arbiter.ts +12 -3
- package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
- package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
- package/src/runs/shared/model-scope.ts +1 -1
- package/src/runs/shared/nested-events.ts +11 -2
- package/src/runs/shared/orca-progress-tabs.ts +1 -1
- package/src/runs/shared/parallel-utils.ts +7 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +12 -4
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree-setup-command.ts +27 -4
- package/src/runs/shared/worktree.ts +45 -15
- package/src/shared/child-cache-retention.ts +43 -0
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +68 -8
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/pruned-fork.ts +1 -1
- package/src/shared/required-child-extensions.ts +81 -0
- package/src/shared/settings.ts +5 -2
- package/src/shared/shortcuts.ts +0 -4
- package/src/shared/types.ts +74 -30
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -7
- package/src/slash/subagents-admin.ts +24 -13
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +16 -14
- package/src/tui/render.ts +168 -37
- package/src/watchdog/child-status.ts +28 -28
- package/src/watchdog/lsp-diagnostics.ts +1 -1
- package/src/watchdog/model-selection.ts +21 -1
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +10 -2
- package/src/watchdog/register-main.ts +39 -35
- package/src/watchdog/render.ts +1 -1
- package/src/watchdog/review.ts +123 -74
- package/src/watchdog/rules.ts +1 -1
- package/src/watchdog/runtime.ts +100 -27
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +3 -0
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +5 -3
- package/src/watchdog/warning-format.ts +1 -1
- package/src/workflows/scripted-workflow.ts +279 -10
- package/src/workflows/workflow-checklist.ts +2 -2
- package/src/workflows/workflow-receipt.ts +21 -3
- package/src/workflows/workflow-resources.ts +13 -2
- package/runner-server-preload.mjs +0 -13
- package/src/runs/shared/model-exclusions.ts +0 -374
- package/src/runs/shared/readonly-model-continuation.ts +0 -69
- package/src/runs/shared/readonly-session-evidence.ts +0 -307
- /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
|
|
10
|
-
model
|
|
11
|
-
|
|
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
|
|
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
|
|
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
|
|
462
|
-
|
|
463
|
-
fallbackModels: string[] | undefined,
|
|
399
|
+
export function resolveModelSelection(
|
|
400
|
+
model: string | undefined,
|
|
464
401
|
availableModels: AvailableModelInfo[] | undefined,
|
|
465
402
|
preferredProvider?: string,
|
|
466
|
-
options?:
|
|
467
|
-
):
|
|
468
|
-
if (!
|
|
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
|
-
|
|
472
|
-
|
|
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
|
-
|
|
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
|
-
|
|
532
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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(
|
|
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()
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
/\
|
|
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
|
-
/(?:^|[
|
|
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)[
|
|
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
|
|
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
|
-
|
|
147
|
-
let
|
|
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
|
-
|
|
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
|
+
}
|