pi-subagents 0.46.0 → 0.47.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 +23 -0
- package/docs/agents.md +1 -1
- package/docs/configuration.md +14 -6
- package/docs/extension-api.md +1 -1
- package/docs/missions.md +5 -3
- package/docs/models.md +3 -1
- package/docs/observability.md +3 -3
- package/docs/tool-reference.md +2 -2
- package/docs/watchdog.md +1 -1
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +4 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
- package/src/extension/config.ts +3 -0
- package/src/extension/fanout-child.ts +5 -4
- package/src/extension/index.ts +30 -3
- package/src/extension/rpc.ts +3 -6
- package/src/extension/schemas.ts +25 -5
- package/src/extension/tool-description.ts +26 -8
- package/src/inspectors/herdr/project-panes.ts +2 -1
- package/src/missions/store.ts +2 -1
- package/src/missions/workflow-state.ts +19 -13
- package/src/runs/background/async-execution.ts +10 -5
- package/src/runs/background/async-job-tracker.ts +15 -0
- package/src/runs/background/async-resume.ts +19 -3
- package/src/runs/background/async-status.ts +6 -1
- package/src/runs/background/control-channel.ts +36 -0
- package/src/runs/background/result-watcher.ts +16 -2
- package/src/runs/background/scheduled-runs.ts +2 -1
- package/src/runs/background/stale-run-reconciler.ts +2 -21
- package/src/runs/background/subagent-runner.ts +47 -6
- package/src/runs/foreground/async-steering-action.ts +1 -1
- package/src/runs/foreground/chain-execution.ts +3 -0
- package/src/runs/foreground/execution.ts +3 -0
- package/src/runs/foreground/subagent-executor.ts +95 -12
- package/src/runs/foreground/workflow-foreground-steering.ts +187 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +8 -4
- package/src/runs/shared/model-scope.ts +12 -2
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/worktree.ts +3 -2
- package/src/shared/artifacts.ts +14 -14
- package/src/shared/display-text.ts +100 -0
- package/src/shared/formatters.ts +4 -6
- package/src/shared/settings.ts +15 -2
- package/src/shared/types.ts +11 -1
- package/src/shared/utils.ts +43 -33
- package/src/slash/slash-commands.ts +3 -1
- package/src/tui/fleet-status.ts +14 -10
- package/src/tui/render.ts +30 -26
- package/src/watchdog/change-signature.ts +4 -3
|
@@ -4,7 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
5
5
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { resolveAgentName, type AgentConfig, type AgentScope } from "../../agents/agents.ts";
|
|
7
|
-
import { getArtifactsDir, getChainRunsDir, getProjectArtifactPackagingWarning } from "../../shared/artifacts.ts";
|
|
7
|
+
import { getArtifactsDir, getChainRunsDir, getProjectArtifactPackagingWarning, getProjectSubagentsDir } from "../../shared/artifacts.ts";
|
|
8
8
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
9
9
|
import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
|
|
10
10
|
import { resolveEffectiveThinking, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
isParallelStep,
|
|
40
40
|
isDynamicParallelStep,
|
|
41
41
|
resolveChainPath,
|
|
42
|
+
resolveExistingReadPaths,
|
|
42
43
|
resolveStepBehavior,
|
|
43
44
|
suppressProgressForReadOnlyTask,
|
|
44
45
|
taskDisallowsFileUpdates,
|
|
@@ -51,7 +52,7 @@ import {
|
|
|
51
52
|
type StepOverrides,
|
|
52
53
|
} from "../../shared/settings.ts";
|
|
53
54
|
import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
|
|
54
|
-
import { buildAsyncRunnerSteps, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable, workflowAwaitedAsyncResultPath } from "../background/async-execution.ts";
|
|
55
|
+
import { buildAsyncRunnerSteps, DEFAULT_ASYNC_TIMEOUT_MS, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable, workflowAwaitedAsyncResultPath } from "../background/async-execution.ts";
|
|
55
56
|
import { isScheduledRunAction, type ScheduledRunAction } from "../background/scheduled-runs.ts";
|
|
56
57
|
import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
|
|
57
58
|
import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
|
|
@@ -84,6 +85,13 @@ import { applySteeringRecoveryAgentConfig, buildRevivedAsyncTask, resolveAsyncRe
|
|
|
84
85
|
import { deliverCheckpointDecisionRequest, deliverInterruptRequest, readRevivalBriefs, requestAsyncSteer, type SteerDeliveryMode } from "../background/control-channel.ts";
|
|
85
86
|
import { updateSteeringTarget, waitForSteeringAction } from "../background/steering.ts";
|
|
86
87
|
import { steerAsyncRun } from "./async-steering-action.ts";
|
|
88
|
+
import {
|
|
89
|
+
removeWorkflowForegroundSteeringRoute,
|
|
90
|
+
resolveWorkflowForegroundSteeringTarget,
|
|
91
|
+
steerWorkflowForegroundTarget,
|
|
92
|
+
workflowForegroundSteeringDir,
|
|
93
|
+
workflowForegroundSteeringLaunchOptions,
|
|
94
|
+
} from "./workflow-foreground-steering.ts";
|
|
87
95
|
import { stopAsyncRun } from "./async-stop-action.ts";
|
|
88
96
|
import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
|
|
89
97
|
import { resolveAsyncRootResultPath, waitForImportedAsyncRoot } from "../background/chain-root-attachment.ts";
|
|
@@ -403,6 +411,7 @@ function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefine
|
|
|
403
411
|
function removeForegroundControlIfIdle(state: SubagentState, runId: string): boolean {
|
|
404
412
|
const control = state.foregroundControls.get(runId);
|
|
405
413
|
if (control && (!foregroundSchedulingSettled(control) || (control.activeChildren?.size ?? 0) > 0)) return false;
|
|
414
|
+
if (control) removeWorkflowForegroundSteeringRoute(control);
|
|
406
415
|
state.foregroundControls.delete(runId);
|
|
407
416
|
if (state.lastForegroundControlId === runId) state.lastForegroundControlId = null;
|
|
408
417
|
return true;
|
|
@@ -1450,7 +1459,7 @@ async function resumeAsyncRun(input: {
|
|
|
1450
1459
|
inheritProjectContext: recoveryDescriptor.inheritProjectContext,
|
|
1451
1460
|
inheritSkills: recoveryDescriptor.inheritSkills,
|
|
1452
1461
|
source: "project",
|
|
1453
|
-
filePath: recoveryDescriptor.agentFilePath ?? path.join(recoveryDescriptor.cwd, "
|
|
1462
|
+
filePath: recoveryDescriptor.agentFilePath ?? path.join(getProjectSubagentsDir(recoveryDescriptor.cwd), "recovery-agent"),
|
|
1454
1463
|
} : undefined);
|
|
1455
1464
|
if (!agentConfig) {
|
|
1456
1465
|
return {
|
|
@@ -2096,7 +2105,17 @@ function applySingleAgentLaunchDefaults(params: SubagentParamsLike, agents: Agen
|
|
|
2096
2105
|
|
|
2097
2106
|
export const DEFAULT_FOREGROUND_TIMEOUT_MS = 30 * 60 * 1000;
|
|
2098
2107
|
|
|
2099
|
-
|
|
2108
|
+
// Async single-agent runs also need a wall-clock backstop: a child whose bash
|
|
2109
|
+
// tool blocks forever (e.g. a background process inheriting the terminal with
|
|
2110
|
+
// no bash `timeout` arg) would otherwise hang the parent indefinitely with
|
|
2111
|
+
// zero signal. Same generous default as foreground; explicit timeoutMs/
|
|
2112
|
+
// maxRuntimeMs and agent-level defaultTimeoutMs remain authoritative.
|
|
2113
|
+
//
|
|
2114
|
+
// Deliberately NOT applied at the workflow level: async scripted workflows
|
|
2115
|
+
// stay unbounded as a whole, while each runner child has its own deadline.
|
|
2116
|
+
export { DEFAULT_ASYNC_TIMEOUT_MS };
|
|
2117
|
+
|
|
2118
|
+
export function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?: number): { timeoutMs?: number; error?: string } {
|
|
2100
2119
|
const rawTimeout = params.timeoutMs;
|
|
2101
2120
|
const rawMaxRuntime = params.maxRuntimeMs;
|
|
2102
2121
|
if (rawTimeout === undefined && rawMaxRuntime === undefined) {
|
|
@@ -2115,6 +2134,21 @@ function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?:
|
|
|
2115
2134
|
return timeoutMs === undefined ? {} : { timeoutMs };
|
|
2116
2135
|
}
|
|
2117
2136
|
|
|
2137
|
+
/**
|
|
2138
|
+
* Resolve the effective launch timeout for a single-agent run, applying the
|
|
2139
|
+
* async/foreground default when neither the caller nor the agent set one.
|
|
2140
|
+
*
|
|
2141
|
+
* The async default is deliberately applied only to plain single-agent
|
|
2142
|
+
* launches. Composite launches keep their top-level execution unbounded when
|
|
2143
|
+
* no timeout is set; their runner children resolve separate deadlines.
|
|
2144
|
+
* Exported so the executor wiring is directly testable.
|
|
2145
|
+
*/
|
|
2146
|
+
export function resolveSingleAgentLaunchTimeout(params: SubagentParamsLike, async: boolean): { timeoutMs?: number; error?: string } {
|
|
2147
|
+
const isComposite = (params.chain?.length ?? 0) > 0 || (params.tasks?.length ?? 0) > 0 || params.workflowScript !== undefined;
|
|
2148
|
+
const defaultTimeoutMs = !async ? DEFAULT_FOREGROUND_TIMEOUT_MS : isComposite ? undefined : DEFAULT_ASYNC_TIMEOUT_MS;
|
|
2149
|
+
return resolveForegroundTimeout(params, defaultTimeoutMs);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2118
2152
|
function resolveToolBudget(
|
|
2119
2153
|
raw: unknown,
|
|
2120
2154
|
label = "toolBudget",
|
|
@@ -3155,6 +3189,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
3155
3189
|
const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskText, compactOptional<Parameters<typeof runSync>[4]>({
|
|
3156
3190
|
permissions: input.permissions,
|
|
3157
3191
|
parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
|
|
3192
|
+
...workflowForegroundSteeringLaunchOptions(input.foregroundControl, index),
|
|
3158
3193
|
context: input.contextPolicy.contextForAgent(task.agent),
|
|
3159
3194
|
cwd: taskCwd,
|
|
3160
3195
|
signal: input.signal,
|
|
@@ -3830,8 +3865,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3830
3865
|
// Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
|
|
3831
3866
|
// absolute paths pass through; relative paths resolve against the child cwd.
|
|
3832
3867
|
const reads = readsOverride !== undefined ? readsOverride : agentConfig.defaultReads ?? false;
|
|
3833
|
-
const
|
|
3834
|
-
|
|
3868
|
+
const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, effectiveCwd) : [];
|
|
3869
|
+
const readsInstruction = readPaths.length > 0
|
|
3870
|
+
? `[Read from: ${readPaths.join(", ")}]\n\n`
|
|
3835
3871
|
: "";
|
|
3836
3872
|
task = readsInstruction + task;
|
|
3837
3873
|
task = injectSingleOutputInstruction(task, outputPath, agentConfig);
|
|
@@ -3875,6 +3911,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3875
3911
|
r = await runSync(ctx.cwd, agents, params.agent!, task, compactOptional<Parameters<typeof runSync>[4]>({
|
|
3876
3912
|
permissions: deps.config.permissions,
|
|
3877
3913
|
parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
|
|
3914
|
+
...workflowForegroundSteeringLaunchOptions(foregroundControl, 0),
|
|
3878
3915
|
context: data.contextPolicy.contextForAgent(params.agent!),
|
|
3879
3916
|
cwd: effectiveCwd,
|
|
3880
3917
|
signal,
|
|
@@ -4483,10 +4520,24 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4483
4520
|
void Promise.resolve().then(async () => {
|
|
4484
4521
|
const workflowResults: SingleResult[] = [];
|
|
4485
4522
|
const { action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = workflowRequest;
|
|
4523
|
+
const workflowSteps = new Map<string, NonNullable<AsyncStatus["steps"]>[number]>();
|
|
4524
|
+
let projectedTraceLength = 0;
|
|
4525
|
+
let projectedTraceTail: NonNullable<Details["workflow"]>["trace"][number] | undefined;
|
|
4486
4526
|
const updateTrace = (trace: NonNullable<Details["workflow"]>["trace"]) => {
|
|
4487
4527
|
status.workflow = { ...(status.workflow ?? { emits: [], console: [] }), trace };
|
|
4488
|
-
|
|
4489
|
-
|
|
4528
|
+
const rebuild = trace.length < projectedTraceLength
|
|
4529
|
+
|| (projectedTraceLength > 0 && trace[projectedTraceLength - 1] !== projectedTraceTail);
|
|
4530
|
+
if (rebuild) {
|
|
4531
|
+
workflowSteps.clear();
|
|
4532
|
+
for (const step of status.steps ?? []) {
|
|
4533
|
+
if (step.workflowKey) workflowSteps.set(step.workflowKey, step);
|
|
4534
|
+
}
|
|
4535
|
+
projectedTraceLength = 0;
|
|
4536
|
+
}
|
|
4537
|
+
for (let index = projectedTraceLength; index < trace.length; index += 1) {
|
|
4538
|
+
const entry = trace[index]!;
|
|
4539
|
+
if (entry.operation !== "run") continue;
|
|
4540
|
+
const existing = workflowSteps.get(entry.key);
|
|
4490
4541
|
if (entry.state === "reused" && existing) continue;
|
|
4491
4542
|
const mapped = entry.state === "started" || entry.state === "reused" ? "running" : entry.state === "completed" ? "completed" : "failed";
|
|
4492
4543
|
if (existing) {
|
|
@@ -4497,9 +4548,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4497
4548
|
if (entry.durationMs === undefined) delete existing.durationMs;
|
|
4498
4549
|
else existing.durationMs = entry.durationMs;
|
|
4499
4550
|
} else {
|
|
4500
|
-
|
|
4551
|
+
const step: NonNullable<AsyncStatus["steps"]>[number] = { agent: entry.agent ?? entry.key, label: entry.key, workflowKey: entry.key, parentWorkflowRunId: workflowRunId, status: mapped, startedAt: Date.now() };
|
|
4552
|
+
status.steps?.push(step);
|
|
4553
|
+
workflowSteps.set(entry.key, step);
|
|
4501
4554
|
}
|
|
4502
4555
|
}
|
|
4556
|
+
projectedTraceLength = trace.length;
|
|
4557
|
+
projectedTraceTail = trace.at(-1);
|
|
4503
4558
|
projectWorkflowActivity();
|
|
4504
4559
|
persist();
|
|
4505
4560
|
appendWorkflowEvent({ type: "subagent.workflow.trace", trace });
|
|
@@ -5089,6 +5144,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5089
5144
|
try {
|
|
5090
5145
|
const location = resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results);
|
|
5091
5146
|
const runId = location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir);
|
|
5147
|
+
const directoryStatus = location.asyncDir ? readStatus(location.asyncDir) : null;
|
|
5148
|
+
if (directoryStatus?.mode === "workflow") {
|
|
5149
|
+
const route = resolveWorkflowForegroundSteeringTarget({ state: deps.state, workflowRunId: directoryStatus.runId || runId, asyncDirRoot: DIRS.async });
|
|
5150
|
+
if (!route.ok) return { content: [{ type: "text", text: route.message }], isError: true, details: { mode: "management", results: [] } };
|
|
5151
|
+
return steerWorkflowForegroundTarget({ target: route.target, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal });
|
|
5152
|
+
}
|
|
5092
5153
|
if (location.asyncDir) {
|
|
5093
5154
|
const unsupported = externalRunnerControlError(location.asyncDir, "steer");
|
|
5094
5155
|
if (unsupported) return unsupported;
|
|
@@ -5124,8 +5185,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5124
5185
|
return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
|
|
5125
5186
|
}
|
|
5126
5187
|
if (resolved?.kind === "nested") return steerNestedRun(omitUndefinedProperties({ target: resolved, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal }));
|
|
5127
|
-
if (resolved?.kind === "foreground")
|
|
5188
|
+
if (resolved?.kind === "foreground") {
|
|
5189
|
+
const route = resolveWorkflowForegroundSteeringTarget({ state: deps.state, childRunId: resolved.id, asyncDirRoot: DIRS.async });
|
|
5190
|
+
if (!route.ok) return { content: [{ type: "text", text: route.message }], isError: true, details: { mode: "management", results: [] } };
|
|
5191
|
+
return steerWorkflowForegroundTarget({ target: route.target, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal });
|
|
5192
|
+
}
|
|
5128
5193
|
if (resolved?.kind !== "async") return { content: [{ type: "text", text: `No async run found for '${targetRunId}'.` }], isError: true, details: { mode: "management", results: [] } };
|
|
5194
|
+
const resolvedStatus = resolved.location.asyncDir ? readStatus(resolved.location.asyncDir) : null;
|
|
5195
|
+
if (resolvedStatus?.mode === "workflow") {
|
|
5196
|
+
const route = resolveWorkflowForegroundSteeringTarget({ state: deps.state, workflowRunId: resolvedStatus.runId || resolved.id, asyncDirRoot: DIRS.async });
|
|
5197
|
+
if (!route.ok) return { content: [{ type: "text", text: route.message }], isError: true, details: { mode: "management", results: [] } };
|
|
5198
|
+
return steerWorkflowForegroundTarget({ target: route.target, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal });
|
|
5199
|
+
}
|
|
5129
5200
|
if (resolved.location.asyncDir) {
|
|
5130
5201
|
const unsupported = externalRunnerControlError(resolved.location.asyncDir, "steer");
|
|
5131
5202
|
if (unsupported) return unsupported;
|
|
@@ -5432,9 +5503,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5432
5503
|
if (externalAgent && (!effectiveAsync || effectiveParams.foregroundOnly === true)) {
|
|
5433
5504
|
return buildRequestedModeError(effectiveParams, `Agent '${externalAgent.name}' uses runner.type='external-cli', which currently supports async/background execution only. Omit async or pass async:true; clarify and foregroundOnly are unsupported.`);
|
|
5434
5505
|
}
|
|
5435
|
-
const foregroundTimeout =
|
|
5506
|
+
const foregroundTimeout = resolveSingleAgentLaunchTimeout(
|
|
5436
5507
|
effectiveParams,
|
|
5437
|
-
effectiveAsync
|
|
5508
|
+
effectiveAsync,
|
|
5438
5509
|
);
|
|
5439
5510
|
if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
|
|
5440
5511
|
const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
|
|
@@ -5584,6 +5655,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5584
5655
|
const foregroundDescription = effectiveParams.task?.trim()
|
|
5585
5656
|
|| effectiveParams.tasks?.[0]?.task?.trim()
|
|
5586
5657
|
|| (effectiveParams.chain ? firstRawChainTask(effectiveParams.chain)?.trim() : undefined);
|
|
5658
|
+
const parentWorkflowStatus = effectiveParams.workflowParentRunId
|
|
5659
|
+
? readStatus(path.join(DIRS.async, effectiveParams.workflowParentRunId))
|
|
5660
|
+
: null;
|
|
5661
|
+
const workflowSteeringDir = effectiveParams.workflowParentRunId
|
|
5662
|
+
&& requestSessionId
|
|
5663
|
+
&& deps.state.workflowControllers?.has(effectiveParams.workflowParentRunId)
|
|
5664
|
+
&& parentWorkflowStatus?.mode === "workflow"
|
|
5665
|
+
&& (parentWorkflowStatus.state === "running" || parentWorkflowStatus.state === "queued")
|
|
5666
|
+
&& parentWorkflowStatus.sessionId === requestSessionId
|
|
5667
|
+
? workflowForegroundSteeringDir(DIRS.async, effectiveParams.workflowParentRunId, runId)
|
|
5668
|
+
: undefined;
|
|
5587
5669
|
const foregroundControl: ForegroundRunControl | undefined = effectiveAsync
|
|
5588
5670
|
? undefined
|
|
5589
5671
|
: compactOptional<ForegroundRunControl>({
|
|
@@ -5592,6 +5674,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5592
5674
|
mode: foregroundMode,
|
|
5593
5675
|
...(effectiveParams.workflowParentRunId ? { parentWorkflowRunId: effectiveParams.workflowParentRunId } : {}),
|
|
5594
5676
|
...(effectiveParams.workflowKey ? { workflowKey: effectiveParams.workflowKey } : {}),
|
|
5677
|
+
workflowSteeringDir,
|
|
5595
5678
|
startedAt: Date.now(),
|
|
5596
5679
|
updatedAt: Date.now(),
|
|
5597
5680
|
cwd: effectiveCwd,
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
5
|
+
import type { Details, ForegroundRunControl, SubagentState } from "../../shared/types.ts";
|
|
6
|
+
import { readStatus } from "../../shared/utils.ts";
|
|
7
|
+
import {
|
|
8
|
+
consumeSteerAckFromDir,
|
|
9
|
+
readSteerCapability,
|
|
10
|
+
steerAcksDir,
|
|
11
|
+
steerCapabilityPath,
|
|
12
|
+
stepSteerInboxDir,
|
|
13
|
+
writeSteerRequestToExistingDir,
|
|
14
|
+
type SteerDeliveryMode,
|
|
15
|
+
type SteerRequest,
|
|
16
|
+
} from "../background/control-channel.ts";
|
|
17
|
+
|
|
18
|
+
export interface WorkflowForegroundSteeringTarget {
|
|
19
|
+
control: ForegroundRunControl;
|
|
20
|
+
workflowRunId: string;
|
|
21
|
+
sourceRunId: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type WorkflowForegroundSteeringResolution =
|
|
25
|
+
| { ok: true; target: WorkflowForegroundSteeringTarget }
|
|
26
|
+
| { ok: false; message: string };
|
|
27
|
+
|
|
28
|
+
function activeWorkflowError(state: SubagentState, workflowRunId: string, asyncDirRoot: string): string | undefined {
|
|
29
|
+
if (!state.currentSessionId) return "Workflow steering requires an active parent session.";
|
|
30
|
+
if (!state.workflowControllers?.has(workflowRunId)) return `Workflow '${workflowRunId}' has no live foreground child.`;
|
|
31
|
+
const status = readStatus(path.join(asyncDirRoot, workflowRunId));
|
|
32
|
+
if (!status || status.mode !== "workflow" || (status.state !== "running" && status.state !== "queued")) {
|
|
33
|
+
return `Workflow '${workflowRunId}' has no live foreground child.`;
|
|
34
|
+
}
|
|
35
|
+
if (status.sessionId !== state.currentSessionId) return `Workflow '${workflowRunId}' was not found in the active session.`;
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function controlIsLiveInWorkflow(control: ForegroundRunControl, workflowRunId: string, sessionId: string): boolean {
|
|
40
|
+
return control.parentWorkflowRunId === workflowRunId
|
|
41
|
+
&& control.sessionId === sessionId
|
|
42
|
+
&& Boolean(control.workflowSteeringDir)
|
|
43
|
+
&& (control.activeChildren?.size ?? 0) > 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveWorkflowForegroundSteeringTarget(input: {
|
|
47
|
+
state: SubagentState;
|
|
48
|
+
childRunId?: string;
|
|
49
|
+
workflowRunId?: string;
|
|
50
|
+
asyncDirRoot: string;
|
|
51
|
+
}): WorkflowForegroundSteeringResolution {
|
|
52
|
+
const { state, childRunId, asyncDirRoot } = input;
|
|
53
|
+
if (childRunId) {
|
|
54
|
+
const control = state.foregroundControls.get(childRunId);
|
|
55
|
+
if (!control?.parentWorkflowRunId) return { ok: false, message: `Foreground run '${childRunId}' is not a live workflow-owned child.` };
|
|
56
|
+
const workflowRunId = control.parentWorkflowRunId;
|
|
57
|
+
const workflowError = activeWorkflowError(state, workflowRunId, asyncDirRoot);
|
|
58
|
+
if (workflowError) return { ok: false, message: workflowError };
|
|
59
|
+
if (!controlIsLiveInWorkflow(control, workflowRunId, state.currentSessionId!)) {
|
|
60
|
+
return { ok: false, message: `Foreground run '${childRunId}' is not a live workflow-owned child in the active session.` };
|
|
61
|
+
}
|
|
62
|
+
return { ok: true, target: { control, workflowRunId, sourceRunId: childRunId } };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const workflowRunId = input.workflowRunId;
|
|
66
|
+
if (!workflowRunId) return { ok: false, message: "Workflow steering requires a workflow or child run id." };
|
|
67
|
+
const workflowError = activeWorkflowError(state, workflowRunId, asyncDirRoot);
|
|
68
|
+
if (workflowError) return { ok: false, message: workflowError };
|
|
69
|
+
const controls = [...state.foregroundControls.values()].filter((control) => controlIsLiveInWorkflow(control, workflowRunId, state.currentSessionId!));
|
|
70
|
+
if (controls.length === 0) return { ok: false, message: `Workflow '${workflowRunId}' has no live foreground child.` };
|
|
71
|
+
if (controls.length > 1) return { ok: false, message: `Workflow '${workflowRunId}' has ${controls.length} live foreground children; steer a child run id instead.` };
|
|
72
|
+
return { ok: true, target: { control: controls[0]!, workflowRunId, sourceRunId: workflowRunId } };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function managementError(message: string): AgentToolResult<Details> {
|
|
76
|
+
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function steerWorkflowForegroundTarget(input: {
|
|
80
|
+
target: WorkflowForegroundSteeringTarget;
|
|
81
|
+
message: string;
|
|
82
|
+
mode?: SteerDeliveryMode;
|
|
83
|
+
index?: number;
|
|
84
|
+
signal?: AbortSignal;
|
|
85
|
+
ackTimeoutMs?: number;
|
|
86
|
+
}): Promise<AgentToolResult<Details>> {
|
|
87
|
+
const { control, sourceRunId } = input.target;
|
|
88
|
+
const routeDir = control.workflowSteeringDir;
|
|
89
|
+
if (!routeDir || !fs.existsSync(routeDir)) return managementError(`Foreground run '${control.runId}' has no live workflow steering route.`);
|
|
90
|
+
const activeIndexes = [...(control.activeChildren?.keys() ?? [])].sort((left, right) => left - right);
|
|
91
|
+
const index = input.index ?? (activeIndexes.length === 1 ? activeIndexes[0] : undefined);
|
|
92
|
+
if (index === undefined) {
|
|
93
|
+
return managementError(activeIndexes.length === 0
|
|
94
|
+
? `Foreground run '${control.runId}' has no live child session.`
|
|
95
|
+
: `Foreground run '${control.runId}' has ${activeIndexes.length} live child sessions; provide index.`);
|
|
96
|
+
}
|
|
97
|
+
if (!activeIndexes.includes(index)) return managementError(`Foreground run '${control.runId}' child ${index} is not live.`);
|
|
98
|
+
const capability = readSteerCapability(routeDir, index);
|
|
99
|
+
if (capability?.supported === false) return managementError(`Foreground run '${control.runId}' child ${index} does not support steering.`);
|
|
100
|
+
|
|
101
|
+
const request: SteerRequest = {
|
|
102
|
+
type: "steer",
|
|
103
|
+
id: randomUUID(),
|
|
104
|
+
ts: Date.now(),
|
|
105
|
+
message: input.message.trim(),
|
|
106
|
+
...(input.mode && input.mode !== "steer" ? { mode: input.mode } : {}),
|
|
107
|
+
targetIndex: index,
|
|
108
|
+
source: "steer-action",
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
writeSteerRequestToExistingDir(stepSteerInboxDir(routeDir, index), request);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (typeof error === "object" && error !== null && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
114
|
+
return managementError(`Foreground run '${control.runId}' has no live workflow steering route.`);
|
|
115
|
+
}
|
|
116
|
+
return managementError(`Failed to queue steering for foreground run ${control.runId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const deadline = Date.now() + (input.ackTimeoutMs ?? 3_000);
|
|
120
|
+
let ack;
|
|
121
|
+
let routeRemoved = false;
|
|
122
|
+
while (Date.now() <= deadline) {
|
|
123
|
+
ack = consumeSteerAckFromDir(steerAcksDir(routeDir, index), request.id);
|
|
124
|
+
if (ack || input.signal?.aborted) break;
|
|
125
|
+
if (!fs.existsSync(routeDir)) {
|
|
126
|
+
routeRemoved = true;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
await new Promise<void>((resolve) => setTimeout(resolve, Math.min(50, Math.max(1, deadline - Date.now()))));
|
|
130
|
+
}
|
|
131
|
+
if (routeRemoved || (!ack && !input.signal?.aborted && !fs.existsSync(routeDir))) {
|
|
132
|
+
return managementError(`Foreground run '${control.runId}' has no live child session.`);
|
|
133
|
+
}
|
|
134
|
+
const target = ack?.state === "delivered"
|
|
135
|
+
? { index, state: "delivered" as const, deliveredAt: ack.ts }
|
|
136
|
+
: ack?.state === "queued"
|
|
137
|
+
? { index, state: "queued" as const }
|
|
138
|
+
: ack?.state === "failed"
|
|
139
|
+
? { index, state: "failed" as const, reason: ack.message }
|
|
140
|
+
: { index, state: "pending" as const };
|
|
141
|
+
const steering = {
|
|
142
|
+
requestId: request.id,
|
|
143
|
+
state: ack?.state === "delivered" ? "delivered" as const : ack?.state === "failed" ? "failed" as const : "pending" as const,
|
|
144
|
+
deliveryStatus: ack?.state === "delivered" ? "delivered" as const : "queued" as const,
|
|
145
|
+
sourceRunId,
|
|
146
|
+
targets: [target],
|
|
147
|
+
};
|
|
148
|
+
if (input.signal?.aborted) {
|
|
149
|
+
return { content: [{ type: "text", text: `Steering pending for foreground run ${control.runId} (request ${request.id}); caller aborted before acknowledgment.` }], details: { mode: "management", results: [], steering } };
|
|
150
|
+
}
|
|
151
|
+
if (ack?.state === "delivered") {
|
|
152
|
+
return { content: [{ type: "text", text: `Steering delivered for foreground run ${control.runId} (request ${request.id}).` }], details: { mode: "management", results: [], steering } };
|
|
153
|
+
}
|
|
154
|
+
if (ack?.state === "queued") {
|
|
155
|
+
return { content: [{ type: "text", text: `Steering queued for foreground run ${control.runId} (request ${request.id}).` }], details: { mode: "management", results: [], steering } };
|
|
156
|
+
}
|
|
157
|
+
if (ack?.state === "failed") {
|
|
158
|
+
return { content: [{ type: "text", text: `Steering failed for foreground run ${control.runId} (request ${request.id}): ${ack.message}` }], isError: true, details: { mode: "management", results: [], steering } };
|
|
159
|
+
}
|
|
160
|
+
return { content: [{ type: "text", text: `Steering pending for foreground run ${control.runId} (request ${request.id}); no acknowledgment was received.` }], details: { mode: "management", results: [], steering } };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function workflowForegroundSteeringDir(asyncDirRoot: string, workflowRunId: string, childRunId: string): string {
|
|
164
|
+
return path.join(asyncDirRoot, workflowRunId, "control", "workflow-foreground", childRunId);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function removeWorkflowForegroundSteeringRoute(control: ForegroundRunControl): void {
|
|
168
|
+
if (!control.workflowSteeringDir) return;
|
|
169
|
+
try {
|
|
170
|
+
fs.rmSync(control.workflowSteeringDir, { recursive: true, force: true });
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.warn(`[pi-subagents] Failed to remove workflow foreground steering route '${control.workflowSteeringDir}': ${error instanceof Error ? error.message : String(error)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function workflowForegroundSteeringLaunchOptions(control: ForegroundRunControl | undefined, index: number): Pick<import("../../shared/types.ts").RunSyncOptions, "steerInboxDir" | "steerCapabilityPath" | "steerAckDir"> {
|
|
177
|
+
if (!control?.workflowSteeringDir) return {};
|
|
178
|
+
const steerInboxDir = stepSteerInboxDir(control.workflowSteeringDir, index);
|
|
179
|
+
const steerAckDir = steerAcksDir(control.workflowSteeringDir, index);
|
|
180
|
+
fs.mkdirSync(steerInboxDir, { recursive: true });
|
|
181
|
+
fs.mkdirSync(steerAckDir, { recursive: true });
|
|
182
|
+
return {
|
|
183
|
+
steerInboxDir,
|
|
184
|
+
steerCapabilityPath: steerCapabilityPath(control.workflowSteeringDir, index),
|
|
185
|
+
steerAckDir,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -49,7 +49,7 @@ const DYNAMIC_EXPAND_FROM_KEYS = new Set(["output", "path"]);
|
|
|
49
49
|
const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outputSchema", "cwd", "output", "outputMode", "reads", "progress", "skill", "model", "toolBudget", "acceptance", "agentContract", "gateOn"]);
|
|
50
50
|
const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
|
|
51
51
|
...DYNAMIC_PARALLEL_KEYS,
|
|
52
|
-
"outputName", "structured", "inheritProjectContext", "inheritSkills", "skills", "outputPath", "namespaceOutputPath", "maxSubagentDepth", "waitToolEnabled",
|
|
52
|
+
"outputName", "structured", "inheritProjectContext", "inheritSkills", "skills", "outputPath", "namespaceOutputPath", "maxSubagentDepth", "timeoutMs", "waitToolEnabled",
|
|
53
53
|
"structuredOutput", "structuredOutputSchema", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "capabilityCeiling", "completionGuard", "systemPrompt",
|
|
54
54
|
"systemPromptMode", "thinking", "modelCandidates", "sessionFile", "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "parentSessionId", "launchResolvedExtensions",
|
|
55
55
|
]);
|
|
@@ -191,7 +191,8 @@ function defaultScopeWarn(violation: ModelScopeViolation): void {
|
|
|
191
191
|
*
|
|
192
192
|
* An explicitly requested model string is resolved via {@link resolveModelCandidate}.
|
|
193
193
|
* When `options.scope.enforce` is on, an out-of-scope resolved model throws for
|
|
194
|
-
* an explicit (`source: "explicit"`) request and warns for an inherited one
|
|
194
|
+
* an explicit (`source: "explicit"`) request and warns for an inherited one,
|
|
195
|
+
* unless strict scope enforcement makes inherited violations hard errors.
|
|
195
196
|
*/
|
|
196
197
|
export function resolveSubagentModelOverride(
|
|
197
198
|
requestedModel: string | boolean | undefined,
|
|
@@ -245,7 +246,7 @@ export function resolveEffectiveSubagentModel(
|
|
|
245
246
|
}
|
|
246
247
|
|
|
247
248
|
export interface BuildModelCandidatesOptions {
|
|
248
|
-
/** Fallback models
|
|
249
|
+
/** Fallback models warn by default and throw when strict scope enforcement is enabled. */
|
|
249
250
|
scope?: ModelScopeConfig;
|
|
250
251
|
onWarn?: (violation: ModelScopeViolation) => void;
|
|
251
252
|
}
|
|
@@ -265,9 +266,12 @@ export function buildModelCandidates(
|
|
|
265
266
|
if (!raw) continue;
|
|
266
267
|
const normalized = resolveModelCandidate(raw.trim(), availableModels, preferredProvider);
|
|
267
268
|
if (!normalized || seen.has(normalized)) continue;
|
|
268
|
-
if (index > 0 && options?.scope?.enforce) {
|
|
269
|
+
if ((index > 0 || options?.scope?.strict === true) && options?.scope?.enforce) {
|
|
269
270
|
const violation = checkModelScope(normalized, options.scope, "inherited");
|
|
270
|
-
if (violation)
|
|
271
|
+
if (violation) {
|
|
272
|
+
if (violation.severity === "error") throw new Error(violation.message);
|
|
273
|
+
(options.onWarn ?? defaultScopeWarn)(violation);
|
|
274
|
+
}
|
|
271
275
|
}
|
|
272
276
|
seen.add(normalized);
|
|
273
277
|
candidates.push(normalized);
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* where the model came from: an explicit caller-supplied model (`--model`,
|
|
7
7
|
* tool-call `model`, or a TUI clarify pick) is a hard error, while a model
|
|
8
8
|
* inherited from agent frontmatter / `defaultModel` / the parent session only
|
|
9
|
-
* emits a warning so existing configurations keep working.
|
|
9
|
+
* emits a warning so existing configurations keep working. Optional strict
|
|
10
|
+
* enforcement makes inherited models hard errors too.
|
|
10
11
|
*
|
|
11
12
|
* The decision logic ({@link checkModelScope}) is a pure function of its
|
|
12
13
|
* inputs so it can be unit-tested without touching the filesystem or config.
|
|
@@ -16,6 +17,8 @@ import { splitKnownThinkingSuffix } from "../../shared/model-info.ts";
|
|
|
16
17
|
|
|
17
18
|
export interface ModelScopeConfig {
|
|
18
19
|
enforce?: boolean;
|
|
20
|
+
/** Reject inherited and fallback models outside the allowlist instead of warning. */
|
|
21
|
+
strict?: boolean;
|
|
19
22
|
/** Glob-style allow patterns (only `*` is special), matched against `provider/id`. */
|
|
20
23
|
allow?: string[];
|
|
21
24
|
}
|
|
@@ -67,7 +70,7 @@ export function checkModelScope(
|
|
|
67
70
|
if (allow.some((pattern) => matchesScopePattern(model, pattern))) return undefined;
|
|
68
71
|
|
|
69
72
|
const baseModel = stripThinkingSuffix(model);
|
|
70
|
-
const severity: ModelScopeViolation["severity"] = source === "explicit" ? "error" : "warn";
|
|
73
|
+
const severity: ModelScopeViolation["severity"] = source === "explicit" || scope.strict === true ? "error" : "warn";
|
|
71
74
|
return {
|
|
72
75
|
model: baseModel,
|
|
73
76
|
severity,
|
|
@@ -102,6 +105,13 @@ export function parseModelScopeConfig(
|
|
|
102
105
|
config.enforce = input.enforce;
|
|
103
106
|
}
|
|
104
107
|
|
|
108
|
+
if ("strict" in input) {
|
|
109
|
+
if (typeof input.strict !== "boolean") {
|
|
110
|
+
throw new Error(`Subagent settings in '${meta.filePath}' have invalid 'modelScope.strict'; expected a boolean.`);
|
|
111
|
+
}
|
|
112
|
+
config.strict = input.strict;
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
if ("allow" in input) {
|
|
106
116
|
if (!Array.isArray(input.allow)) {
|
|
107
117
|
throw new Error(`Subagent settings in '${meta.filePath}' have invalid 'modelScope.allow'; expected an array of strings.`);
|
|
@@ -40,6 +40,7 @@ export interface RunnerSubagentStep {
|
|
|
40
40
|
outputMode?: "inline" | "file-only";
|
|
41
41
|
sessionFile?: string;
|
|
42
42
|
maxSubagentDepth?: number;
|
|
43
|
+
timeoutMs?: number;
|
|
43
44
|
waitToolEnabled?: boolean;
|
|
44
45
|
structuredOutput?: {
|
|
45
46
|
schema: import("../../shared/types.ts").JsonSchemaObject;
|
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { resolveAuthorityDecision, type AuthorityPolicyConfig } from "../../policy/authority.ts";
|
|
6
|
+
import { PROJECT_SUBAGENTS_RELATIVE_DIR } from "../../shared/artifacts.ts";
|
|
6
7
|
|
|
7
8
|
export interface WorktreeSetup {
|
|
8
9
|
cwd: string;
|
|
@@ -135,9 +136,9 @@ function resolveRepoState(cwd: string): RepoState {
|
|
|
135
136
|
const cwdRelative = resolveRepoCwdRelative(cwd);
|
|
136
137
|
const toplevel = runGitChecked(cwd, ["rev-parse", "--show-toplevel"]).trim();
|
|
137
138
|
|
|
138
|
-
// pi-subagents writes durable runtime state under .pi
|
|
139
|
+
// pi-subagents writes durable runtime state under .pi/subagents/ by default;
|
|
139
140
|
// that state must not make managed isolation unusable for later runs.
|
|
140
|
-
const status = runGitChecked(toplevel, ["status", "--porcelain", "--",
|
|
141
|
+
const status = runGitChecked(toplevel, ["status", "--porcelain", "--", `:!${PROJECT_SUBAGENTS_RELATIVE_DIR}`]);
|
|
141
142
|
if (status.trim().length > 0) {
|
|
142
143
|
throw new Error("worktree isolation requires a clean git working tree. Commit or stash changes first.");
|
|
143
144
|
}
|
package/src/shared/artifacts.ts
CHANGED
|
@@ -3,19 +3,19 @@ import * as path from "node:path";
|
|
|
3
3
|
import { CHAIN_RUNS_DIR, TEMP_ARTIFACTS_DIR, type ArtifactPaths, type ArtifactDirPreference } from "./types.ts";
|
|
4
4
|
import { getAgentDir } from "./utils.ts";
|
|
5
5
|
const CLEANUP_MARKER_FILE = ".last-cleanup";
|
|
6
|
-
const
|
|
6
|
+
export const PROJECT_SUBAGENTS_RELATIVE_DIR = ".pi/subagents";
|
|
7
7
|
|
|
8
8
|
const PROJECT_ARTIFACT_PATHS = [
|
|
9
|
-
`${
|
|
10
|
-
`${
|
|
11
|
-
`${
|
|
12
|
-
`${
|
|
13
|
-
`${
|
|
14
|
-
`${
|
|
15
|
-
`${
|
|
16
|
-
`${
|
|
17
|
-
`${
|
|
18
|
-
`${
|
|
9
|
+
`${PROJECT_SUBAGENTS_RELATIVE_DIR}/artifacts/output.md`,
|
|
10
|
+
`${PROJECT_SUBAGENTS_RELATIVE_DIR}/artifacts/run_worker_input.md`,
|
|
11
|
+
`${PROJECT_SUBAGENTS_RELATIVE_DIR}/artifacts/run_worker_output.md`,
|
|
12
|
+
`${PROJECT_SUBAGENTS_RELATIVE_DIR}/artifacts/run_worker.jsonl`,
|
|
13
|
+
`${PROJECT_SUBAGENTS_RELATIVE_DIR}/artifacts/run_worker_transcript.jsonl`,
|
|
14
|
+
`${PROJECT_SUBAGENTS_RELATIVE_DIR}/artifacts/run_worker_meta.json`,
|
|
15
|
+
`${PROJECT_SUBAGENTS_RELATIVE_DIR}/artifacts/progress/run/progress.md`,
|
|
16
|
+
`${PROJECT_SUBAGENTS_RELATIVE_DIR}/artifacts/outputs/output.md`,
|
|
17
|
+
`${PROJECT_SUBAGENTS_RELATIVE_DIR}/artifacts/outputs/run/output.md`,
|
|
18
|
+
`${PROJECT_SUBAGENTS_RELATIVE_DIR}/chain-runs/run.json`,
|
|
19
19
|
];
|
|
20
20
|
|
|
21
21
|
function globMatchesPath(pattern: string, filePath: string): boolean {
|
|
@@ -59,7 +59,7 @@ function normalizePattern(pattern: string): string {
|
|
|
59
59
|
|
|
60
60
|
function patternMatchesArtifactPath(pattern: string, artifactPath: string): boolean {
|
|
61
61
|
const normalized = normalizePattern(pattern);
|
|
62
|
-
return normalized ===
|
|
62
|
+
return normalized === PROJECT_SUBAGENTS_RELATIVE_DIR
|
|
63
63
|
|| normalized === "*"
|
|
64
64
|
|| artifactPath.startsWith(`${normalized}/`)
|
|
65
65
|
|| globMatchesPath(normalized, artifactPath);
|
|
@@ -127,11 +127,11 @@ export function getProjectArtifactPackagingWarning(cwd: string): string | undefi
|
|
|
127
127
|
const ignorePath = fs.existsSync(npmIgnorePath) ? npmIgnorePath : path.join(cwd, ".gitignore");
|
|
128
128
|
if (filesIncludeArtifacts === undefined && ignoreFileExcludesProjectArtifacts(ignorePath)) return undefined;
|
|
129
129
|
|
|
130
|
-
return "Project-scoped subagent artifacts can be included when this package is published. Add '.pi
|
|
130
|
+
return "Project-scoped subagent artifacts can be included when this package is published. Add '.pi/subagents/' to .npmignore, restrict package.json files, or set artifactDir to 'session' or 'temp'.";
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
export function getProjectSubagentsDir(cwd: string): string {
|
|
134
|
-
return path.join(cwd,
|
|
134
|
+
return path.join(cwd, PROJECT_SUBAGENTS_RELATIVE_DIR);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
export function getProjectArtifactsDir(cwd: string): string {
|