pi-subagents 0.81.0 → 0.82.1
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 +55 -0
- package/README.md +1 -1
- package/bundle/agents.js +3 -3
- package/bundle/child-tool-plan.js +2 -2
- package/bundle/{chunk-L5ZHE4L4.js → chunk-3JCDWF4V.js} +4 -4
- package/bundle/{chunk-LSRUTHT7.js → chunk-43SCHIMD.js} +1 -1
- package/bundle/{chunk-VJHOELQP.js → chunk-46XKCPJR.js} +193 -81
- package/bundle/{chunk-G5DKFLLI.js → chunk-4SDU2BZX.js} +15 -10
- package/bundle/{chunk-2ZSNH26H.js → chunk-4Y2ROR7L.js} +7 -7
- package/bundle/{chunk-V2JWDB5U.js → chunk-52UBALJL.js} +2 -2
- package/bundle/{chunk-3DC5KBVP.js → chunk-5TYMSZX3.js} +1 -1
- package/bundle/{chunk-NNLASTQE.js → chunk-6XJB7PIX.js} +20 -6
- package/bundle/{chunk-ZBC44JJ3.js → chunk-CIZQXJ5Q.js} +22 -9
- package/bundle/{chunk-KSWQVGDW.js → chunk-FXUXCX2K.js} +225 -18
- package/bundle/{chunk-563BHAZ5.js → chunk-G4GZD3OJ.js} +35 -26
- package/bundle/{chunk-ARRKBB2B.js → chunk-GJECHSAR.js} +2 -2
- package/bundle/{chunk-V3BIUPLU.js → chunk-IVAE5Z6Q.js} +256 -52
- package/bundle/{chunk-V6MR5TOQ.js → chunk-JYUKW2SY.js} +25 -19
- package/bundle/{chunk-BJN5LUQY.js → chunk-OYF6AFDY.js} +1 -1
- package/bundle/{chunk-CEKS3TFO.js → chunk-PBK2I7WY.js} +6 -6
- package/bundle/{chunk-TUWLXTAM.js → chunk-PDZWJCHD.js} +681 -320
- package/bundle/{chunk-EYNJ6VL6.js → chunk-PLCCBLPY.js} +2 -2
- package/bundle/{chunk-XMFLCKMQ.js → chunk-QE4AGIMG.js} +339 -132
- package/bundle/{chunk-74RT5YQE.js → chunk-RMQFMMAT.js} +123 -33
- package/bundle/{chunk-WSBNENFS.js → chunk-UCPP4GNU.js} +14 -4
- package/bundle/{chunk-2XYWN7QJ.js → chunk-UTT254JE.js} +2 -2
- package/bundle/{chunk-BPS2AGBZ.js → chunk-VSUSLDNW.js} +93 -52
- package/bundle/{chunk-ZP3HODFH.js → chunk-XSWOTIA3.js} +1 -1
- package/bundle/{chunk-3IF2PGW7.js → chunk-YKEWIB4M.js} +1 -1
- package/bundle/{chunk-26C3WLYQ.js → chunk-ZIV4J4HG.js} +498 -274
- package/bundle/control-channel.js +2 -2
- package/bundle/index.js +1 -1
- package/bundle/intercom-bridge.js +2 -2
- package/bundle/preflight.js +8 -8
- package/bundle/project-panes.js +3 -3
- package/docs/agents.md +37 -7
- package/docs/configuration.md +9 -1
- package/docs/tool-reference.md +3 -1
- package/docs/workflows.md +16 -0
- package/package.json +2 -2
- package/skills/council-mode/SKILL.md +1 -4
- package/skills/pi-subagents/SKILL.md +130 -107
- package/skills/pi-subagents/references/orchestration.md +124 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +96 -295
- package/src/agents/agent-management.js +3 -3
- package/src/agents/agent-memory.js +3 -1
- package/src/agents/agents.js +6 -6
- package/src/agents/frontmatter.d.ts +2 -0
- package/src/agents/frontmatter.js +15 -1
- package/src/agents/skills.js +2 -1
- package/src/extension/config.js +3 -0
- package/src/extension/herdr-pi-bridge.js +17 -10
- package/src/extension/index.js +82 -2
- package/src/extension/reply-workflow-script.js +5 -2
- package/src/extension/schemas.js +3 -3
- package/src/extension/subagent-guide.js +1 -2
- package/src/intercom/native-supervisor-channel.js +20 -0
- package/src/missions/lifecycle.js +7 -1
- package/src/runs/background/active-async-capacity.d.ts +1 -0
- package/src/runs/background/active-async-capacity.js +9 -3
- package/src/runs/background/async-execution.js +20 -10
- package/src/runs/background/async-job-tracker.d.ts +6 -1
- package/src/runs/background/async-job-tracker.js +9 -4
- package/src/runs/background/async-status.d.ts +4 -0
- package/src/runs/background/async-status.js +22 -3
- package/src/runs/background/await-async-run.js +3 -7
- package/src/runs/background/boot-id.d.ts +12 -0
- package/src/runs/background/boot-id.js +47 -0
- package/src/runs/background/interrupted-elsewhere.d.ts +22 -0
- package/src/runs/background/interrupted-elsewhere.js +68 -0
- package/src/runs/background/notify.d.ts +2 -0
- package/src/runs/background/notify.js +13 -0
- package/src/runs/background/result-delivery-ownership.d.ts +6 -0
- package/src/runs/background/result-delivery-ownership.js +19 -1
- package/src/runs/background/result-watcher.d.ts +3 -1
- package/src/runs/background/result-watcher.js +24 -8
- package/src/runs/background/run-child-session.d.ts +1 -6
- package/src/runs/background/run-child-session.js +1 -1
- package/src/runs/background/run-status.js +2 -1
- package/src/runs/background/stale-run-reconciler.d.ts +25 -0
- package/src/runs/background/stale-run-reconciler.js +52 -13
- package/src/runs/background/subagent-runner-bootstrap.js +3 -0
- package/src/runs/background/subagent-runner.d.ts +4 -0
- package/src/runs/background/subagent-runner.js +13 -5
- package/src/runs/background/subagent-wait.d.ts +1 -1
- package/src/runs/background/subagent-wait.js +6 -1
- package/src/runs/background/terminal-run-index.d.ts +2 -0
- package/src/runs/background/terminal-run-index.js +44 -3
- package/src/runs/foreground/async-stop-action.js +3 -1
- package/src/runs/foreground/execution.js +6 -3
- package/src/runs/foreground/foreground-history.d.ts +16 -2
- package/src/runs/foreground/foreground-history.js +133 -31
- package/src/runs/foreground/subagent-executor.d.ts +11 -0
- package/src/runs/foreground/subagent-executor.js +277 -74
- package/src/runs/shared/child-launch.d.ts +2 -2
- package/src/runs/shared/child-session.d.ts +3 -2
- package/src/runs/shared/child-session.js +51 -8
- package/src/runs/shared/child-tool-plan.d.ts +7 -0
- package/src/runs/shared/child-tool-plan.js +21 -6
- package/src/runs/shared/herdr-connection.d.ts +17 -8
- package/src/runs/shared/herdr-connection.js +19 -7
- package/src/runs/shared/herdr-external-adapters.d.ts +3 -2
- package/src/runs/shared/herdr-external-adapters.js +1 -1
- package/src/runs/shared/herdr-machine.d.ts +12 -2
- package/src/runs/shared/herdr-machine.js +55 -9
- package/src/runs/shared/herdr-pane-host.d.ts +88 -0
- package/src/runs/shared/herdr-pane-host.js +155 -0
- package/src/runs/shared/herdr-placed-run.d.ts +27 -150
- package/src/runs/shared/herdr-placed-run.js +48 -395
- package/src/runs/shared/local-machine-access.d.ts +5 -0
- package/src/runs/shared/local-machine-access.js +28 -0
- package/src/runs/shared/orca-progress-tabs.d.ts +0 -1
- package/src/runs/shared/orca-progress-tabs.js +1 -24
- package/src/runs/shared/parallel-utils.d.ts +1 -1
- package/src/runs/shared/placed-run-owner.d.ts +49 -0
- package/src/runs/shared/placed-run-owner.js +93 -0
- package/src/runs/shared/placement-host.d.ts +99 -0
- package/src/runs/shared/placement-host.js +157 -0
- package/src/runs/shared/rex-machine.d.ts +25 -0
- package/src/runs/shared/rex-machine.js +83 -0
- package/src/runs/shared/rex-pane-host.d.ts +62 -0
- package/src/runs/shared/rex-pane-host.js +327 -0
- package/src/shared/completion-owner.d.ts +5 -0
- package/src/shared/completion-owner.js +5 -0
- package/src/shared/session-identity.d.ts +2 -0
- package/src/shared/session-identity.js +26 -0
- package/src/shared/types.d.ts +45 -6
- package/src/tui/fleet.d.ts +1 -0
- package/src/tui/fleet.js +25 -15
- package/src/tui/render.d.ts +6 -0
- package/src/tui/render.js +3 -3
- package/src/tui/subagent-messages.js +1 -0
- package/src/workflows/scripted-workflow.d.ts +1 -1
- package/src/workflows/workflow-auto-resume.d.ts +32 -0
- package/src/workflows/workflow-auto-resume.js +102 -0
- package/src/workflows/workflow-launch-record.d.ts +27 -0
- package/src/workflows/workflow-launch-record.js +48 -0
- package/src/workflows/workflow-resume-claim.d.ts +82 -0
- package/src/workflows/workflow-resume-claim.js +210 -0
- package/src/workflows/workflow-resume.d.ts +56 -0
- package/src/workflows/workflow-resume.js +106 -0
- package/src/workflows/workflow-reuse.d.ts +15 -9
- package/src/workflows/workflow-reuse.js +77 -14
- package/src/workflows/workflow-revival.d.ts +2 -0
- package/src/workflows/workflow-revival.js +2 -1
- package/src/workflows/workflow-stop-cause.d.ts +6 -0
- package/src/workflows/workflow-stop-cause.js +8 -0
- package/skills/pi-subagents/references/constraints-and-recipes.md +0 -74
- package/skills/pi-subagents/references/execution-controls.md +0 -563
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -165
- package/skills/pi-subagents/references/multi-lane-orchestration.md +0 -61
- package/skills/pi-subagents/references/review-and-validation.md +0 -73
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { normalizePublicSubagentExecution } from "../extension/public-execution.js";
|
|
4
|
+
import { reconcileAsyncRun } from "../runs/background/stale-run-reconciler.js";
|
|
5
|
+
import { ownsRun } from "../runs/shared/run-ownership.js";
|
|
6
|
+
import { readStatus } from "../shared/utils.js";
|
|
7
|
+
import { readWorkflowLaunchRecord } from "./workflow-launch-record.js";
|
|
8
|
+
import { claimWorkflowResume, refusalSentence, relaunchByHandText, resumeByHandText } from "./workflow-resume-claim.js";
|
|
9
|
+
import { workflowScriptDigest } from "./workflow-reuse.js";
|
|
10
|
+
import { isRunId } from "./workflow-revival.js";
|
|
11
|
+
import { isReusableWorkflowStopCause, WORKFLOW_STOP_CAUSE_OWNER_EXITED } from "./workflow-stop-cause.js";
|
|
12
|
+
/** A run may be resumed again after at most this many resumes that followed Pi exits (auto-resume only). */
|
|
13
|
+
const MAX_WORKFLOW_RESUME_ATTEMPTS = 3;
|
|
14
|
+
export function workflowResumeRefusal(code, reason, runId) {
|
|
15
|
+
const byHand = code === "launch-missing" || code === "launch-invalid" || code === "structured" || code === "digest" || code === "authority"
|
|
16
|
+
? relaunchByHandText(runId)
|
|
17
|
+
: code === "capacity" || code === "attempts" || code === "launch-failed" ? resumeByHandText(runId) : "";
|
|
18
|
+
return { code, reason, byHand };
|
|
19
|
+
}
|
|
20
|
+
/** The tool error for a resume by id; the resume form is not repeated to a caller that just tried it. */
|
|
21
|
+
export function manualWorkflowResumeRefusalText(runId, refusal) {
|
|
22
|
+
return `Cannot resume workflow '${runId}': ${refusalSentence(refusal.reason, refusal.byHand === relaunchByHandText(runId) ? refusal.byHand : "")}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The run id a resume action relaunches: no message, task, chain or index, and an id naming a
|
|
26
|
+
* top-level workflow run. Anything else is a child resume.
|
|
27
|
+
*/
|
|
28
|
+
export function workflowResumeTargetId(asyncDirRoot, params) {
|
|
29
|
+
if (params.message !== undefined || params.task !== undefined || params.index !== undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (Array.isArray(params.chain) && params.chain.length > 0)
|
|
32
|
+
return undefined;
|
|
33
|
+
const id = params.id ?? params.runId;
|
|
34
|
+
if (!isRunId(id))
|
|
35
|
+
return undefined;
|
|
36
|
+
let status;
|
|
37
|
+
try {
|
|
38
|
+
status = readStatus(path.join(asyncDirRoot, id));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return status?.mode === "workflow" && status.parentWorkflowRunId === undefined ? id : undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Checks a stopped workflow run can be resumed and claims it for a new run id. Synchronous and
|
|
47
|
+
* cheap: the launch itself happens in the executor.
|
|
48
|
+
* `newRunId` presets the new run's id: a reload claims a run again under the id of the launch it cancelled.
|
|
49
|
+
*/
|
|
50
|
+
export function prepareWorkflowResume(input) {
|
|
51
|
+
const { asyncDirRoot, sourceRunId, trigger } = input;
|
|
52
|
+
const refuse = (code, reason) => ({ ok: false, ...workflowResumeRefusal(code, reason, sourceRunId) });
|
|
53
|
+
const sourceAsyncDir = path.join(asyncDirRoot, sourceRunId);
|
|
54
|
+
// Only the session that owns the run (itself or one it replaced) may resume it, and nothing is
|
|
55
|
+
// written into another session's run, so this comes before the reconcile.
|
|
56
|
+
const stored = readStatus(sourceAsyncDir);
|
|
57
|
+
if (stored && !ownsRun(input.currentSessionId, stored.sessionId))
|
|
58
|
+
return refuse("not-owned", "it was started in another session; reopen that session to resume it");
|
|
59
|
+
// Session start has already reconciled this session's runs; a resume by id may come later.
|
|
60
|
+
if (trigger === "manual")
|
|
61
|
+
reconcileAsyncRun(sourceAsyncDir);
|
|
62
|
+
const status = readStatus(sourceAsyncDir);
|
|
63
|
+
if (!status || status.mode !== "workflow" || !isReusableWorkflowStopCause(status.workflow?.stopCause))
|
|
64
|
+
return refuse("not-interrupted", "not interrupted; pass message to revive one of its children");
|
|
65
|
+
// The attempt the resumed run records: only resumes that follow a Pi exit count.
|
|
66
|
+
const attempt = (status.workflow?.resumeOrigin?.attempt ?? 0) + (status.workflow?.stopCause === WORKFLOW_STOP_CAUSE_OWNER_EXITED ? 1 : 0);
|
|
67
|
+
if (trigger === "auto" && attempt > MAX_WORKFLOW_RESUME_ATTEMPTS)
|
|
68
|
+
return refuse("attempts", `${MAX_WORKFLOW_RESUME_ATTEMPTS} resumes already followed Pi exits`);
|
|
69
|
+
const read = readWorkflowLaunchRecord(sourceAsyncDir);
|
|
70
|
+
if (!read.ok)
|
|
71
|
+
return read.missing ? refuse("launch-missing", "launch.json is missing (the run started before launches were saved)") : refuse("launch-invalid", `launch.json is invalid: ${read.error}`);
|
|
72
|
+
const record = read.record;
|
|
73
|
+
if (record.source === "structured")
|
|
74
|
+
return refuse("structured", "launched from tasks/chain");
|
|
75
|
+
if (record.source === "script" && (typeof record.params.workflowScript !== "string" || workflowScriptDigest(record.params.workflowScript) !== record.scriptDigest)) {
|
|
76
|
+
return refuse("launch-invalid", "launch.json is invalid: its script does not match its scriptDigest");
|
|
77
|
+
}
|
|
78
|
+
const normalized = normalizePublicSubagentExecution(record.params);
|
|
79
|
+
if (!normalized.ok)
|
|
80
|
+
return refuse("launch-invalid", `launch.json is invalid: ${normalized.error}`);
|
|
81
|
+
const newRunId = input.newRunId ?? randomUUID();
|
|
82
|
+
const claim = claimWorkflowResume(sourceAsyncDir, { newRunId, trigger, now: input.now });
|
|
83
|
+
if (claim.kind === "resumed")
|
|
84
|
+
return refuse("claim-resumed", `resumed as ${claim.newRunId}; wait on it or stop it`);
|
|
85
|
+
if (claim.kind === "busy")
|
|
86
|
+
return refuse("claim-busy", `being resumed by pid ${claim.pid}`);
|
|
87
|
+
if (claim.kind === "unreadable")
|
|
88
|
+
return refuse("claim-busy", "its resume claim cannot be read; wait or remove it");
|
|
89
|
+
return {
|
|
90
|
+
ok: true,
|
|
91
|
+
prepared: {
|
|
92
|
+
sourceRunId,
|
|
93
|
+
sourceAsyncDir,
|
|
94
|
+
newRunId,
|
|
95
|
+
trigger,
|
|
96
|
+
params: {
|
|
97
|
+
...record.params,
|
|
98
|
+
async: true,
|
|
99
|
+
reuseFrom: sourceRunId,
|
|
100
|
+
...(status.admissionCapabilityCeiling ? { capabilityCeiling: status.admissionCapabilityCeiling } : {}),
|
|
101
|
+
},
|
|
102
|
+
expectedScriptDigest: record.scriptDigest,
|
|
103
|
+
resumeOrigin: { sourceRunId, attempt },
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { type WorkflowResumeClaimOutcome } from "./workflow-resume-claim.ts";
|
|
1
2
|
import { type WorkflowScriptChildResult } from "./scripted-workflow.ts";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export declare function workflowRuntimeReplacedRelaunchNotice(runId: string): string;
|
|
3
|
+
import { WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED } from "./workflow-stop-cause.ts";
|
|
4
|
+
/** What a runtime replacement left; the notice's resume line says how the run continues. */
|
|
5
|
+
export declare const WORKFLOW_RUNTIME_REPLACED_STOP_NOTICE = "Async children that were still running keep running, and finished children are kept.";
|
|
6
6
|
/** Abort reason for workflow controllers torn down by runtime replacement; carries the cause as data, not text. */
|
|
7
7
|
export declare function runtimeReplacedAbortReason(): Error;
|
|
8
|
-
export declare function workflowStopCause(reason: unknown):
|
|
8
|
+
export declare function workflowStopCause(reason: unknown): typeof WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED | undefined;
|
|
9
9
|
export declare function workflowScriptDigest(script: string): string;
|
|
10
10
|
/** Hash of the canonical launch params the script host uses to detect duplicate keys. */
|
|
11
11
|
export declare function workflowChildFingerprint(params: Record<string, unknown>): string;
|
|
@@ -40,16 +40,22 @@ type WorkflowReuseMatch = {
|
|
|
40
40
|
kind: "started";
|
|
41
41
|
runId: string;
|
|
42
42
|
};
|
|
43
|
+
/** Why a relaunch of `runId` is refused when another relaunch already continues it. */
|
|
44
|
+
export declare function claimedWorkflowRunRefusal(runId: string, claim: Exclude<WorkflowResumeClaimOutcome, {
|
|
45
|
+
kind: "owner";
|
|
46
|
+
}>, named: boolean): string;
|
|
43
47
|
/**
|
|
44
48
|
* The newest terminal workflow of this session with the same script and args,
|
|
45
|
-
* when that run stopped because its runtime was replaced.
|
|
49
|
+
* when that run stopped because its runtime was replaced or its Pi process exited.
|
|
50
|
+
* Throws when another relaunch already continues that run.
|
|
46
51
|
*/
|
|
47
52
|
export declare function findWorkflowReuseSource(asyncDirRoot: string, sessionId: string, scriptDigest: string, argsDigest: string | undefined): WorkflowReuseSource | undefined;
|
|
48
53
|
/**
|
|
49
|
-
* The
|
|
50
|
-
* must match: each child is still reused only when its key and launch params match.
|
|
54
|
+
* The interrupted workflow run named by `options.reuseFrom`, from any session. Only the script
|
|
55
|
+
* must match: each child is still reused only when its key and launch params match. The run is
|
|
56
|
+
* reconciled first, so a run whose Pi process died is marked `owner-exited` before it is read.
|
|
51
57
|
*/
|
|
52
|
-
export declare function readNamedWorkflowReuseSource(asyncDirRoot: string, runId: string, scriptDigest: string, cwd: string): WorkflowReuseSource;
|
|
58
|
+
export declare function readNamedWorkflowReuseSource(asyncDirRoot: string, runId: string, scriptDigest: string, cwd: string, claimOwnerRunId?: string): WorkflowReuseSource;
|
|
53
59
|
/** A settled match is reusable only when that child succeeded; failed or stopped children run again. */
|
|
54
60
|
export declare function matchWorkflowReuse(source: WorkflowReuseSource, key: string, fingerprint: string): WorkflowReuseMatch | undefined;
|
|
55
61
|
export {};
|
|
@@ -2,14 +2,15 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { readRecentTerminalRunIndex } from "../runs/background/terminal-run-index.js";
|
|
5
|
+
import { reconcileAsyncRun } from "../runs/background/stale-run-reconciler.js";
|
|
5
6
|
import { readStatus } from "../shared/utils.js";
|
|
7
|
+
import { workflowResumeClaimState } from "./workflow-resume-claim.js";
|
|
8
|
+
import { isRunId } from "./workflow-revival.js";
|
|
6
9
|
import { workflowRunParamsFingerprint } from "./scripted-workflow.js";
|
|
7
|
-
|
|
8
|
-
const WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED = "runtime-replaced";
|
|
10
|
+
import { isReusableWorkflowStopCause, WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED } from "./workflow-stop-cause.js";
|
|
9
11
|
const WORKFLOW_CHILD_JOURNAL_FILE = "workflow-children.jsonl";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
12
|
+
/** What a runtime replacement left; the notice's resume line says how the run continues. */
|
|
13
|
+
export const WORKFLOW_RUNTIME_REPLACED_STOP_NOTICE = "Async children that were still running keep running, and finished children are kept.";
|
|
13
14
|
/** Abort reason for workflow controllers torn down by runtime replacement; carries the cause as data, not text. */
|
|
14
15
|
export function runtimeReplacedAbortReason() {
|
|
15
16
|
return Object.assign(new Error("Workflow stopped because the extension session was replaced or reloaded."), { workflowStopCause: WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED });
|
|
@@ -67,9 +68,63 @@ function readWorkflowChildJournal(runId, workflowAsyncDir) {
|
|
|
67
68
|
}
|
|
68
69
|
return source;
|
|
69
70
|
}
|
|
71
|
+
const WORKFLOW_REUSE_CHAIN_MAX_LINKS = 16;
|
|
72
|
+
/**
|
|
73
|
+
* The head run's journal plus the journals of the runs it was relaunched from (`workflow.reusedFrom`).
|
|
74
|
+
* A child's start record is written only once it gets a concurrency slot, so a key the head never
|
|
75
|
+
* reached is still in an earlier run's journal. For each key the newest run that recorded it wins. A
|
|
76
|
+
* status file is parsed, not validated, so a corrupt `reusedFrom` may be any JSON value.
|
|
77
|
+
*/
|
|
78
|
+
function readWorkflowReuseChain(asyncDirRoot, headRunId, headStatus) {
|
|
79
|
+
const source = readWorkflowChildJournal(headRunId, path.join(asyncDirRoot, headRunId));
|
|
80
|
+
const visited = new Set([headRunId]);
|
|
81
|
+
let next = headStatus.workflow?.reusedFrom;
|
|
82
|
+
for (let links = 0; isRunId(next) && links < WORKFLOW_REUSE_CHAIN_MAX_LINKS && !visited.has(next); links++) {
|
|
83
|
+
visited.add(next);
|
|
84
|
+
const asyncDir = path.join(asyncDirRoot, next);
|
|
85
|
+
let status;
|
|
86
|
+
try {
|
|
87
|
+
status = readStatus(asyncDir);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
if (status?.mode !== "workflow" || status.workflow?.scriptDigest !== headStatus.workflow?.scriptDigest)
|
|
93
|
+
break;
|
|
94
|
+
const link = readWorkflowChildJournal(next, asyncDir);
|
|
95
|
+
for (const key of new Set([...link.started.keys(), ...link.settled.keys()])) {
|
|
96
|
+
if (source.started.has(key) || source.settled.has(key))
|
|
97
|
+
continue;
|
|
98
|
+
const started = link.started.get(key);
|
|
99
|
+
const settled = link.settled.get(key);
|
|
100
|
+
if (started)
|
|
101
|
+
source.started.set(key, started);
|
|
102
|
+
if (settled)
|
|
103
|
+
source.settled.set(key, settled);
|
|
104
|
+
}
|
|
105
|
+
next = status.workflow?.reusedFrom;
|
|
106
|
+
}
|
|
107
|
+
return source;
|
|
108
|
+
}
|
|
109
|
+
/** Why a relaunch of `runId` is refused when another relaunch already continues it. */
|
|
110
|
+
export function claimedWorkflowRunRefusal(runId, claim, named) {
|
|
111
|
+
const run = `${named ? "options.reuseFrom: workflow" : "Workflow"} run '${runId}'`;
|
|
112
|
+
if (claim.kind === "resumed")
|
|
113
|
+
return `${run} was resumed as ${claim.newRunId}; wait on it or stop it.`;
|
|
114
|
+
if (claim.kind === "busy")
|
|
115
|
+
return `${run} is being resumed by pid ${claim.pid}; wait for it.`;
|
|
116
|
+
return `${run} has a resume claim that cannot be read; wait or remove it.`;
|
|
117
|
+
}
|
|
118
|
+
function assertWorkflowRunNotClaimed(asyncDir, runId, named, claimOwnerRunId) {
|
|
119
|
+
const claim = workflowResumeClaimState(asyncDir);
|
|
120
|
+
if (claim.kind === "none" || (claim.kind !== "unreadable" && claim.newRunId === claimOwnerRunId))
|
|
121
|
+
return;
|
|
122
|
+
throw new Error(claimedWorkflowRunRefusal(runId, claim, named));
|
|
123
|
+
}
|
|
70
124
|
/**
|
|
71
125
|
* The newest terminal workflow of this session with the same script and args,
|
|
72
|
-
* when that run stopped because its runtime was replaced.
|
|
126
|
+
* when that run stopped because its runtime was replaced or its Pi process exited.
|
|
127
|
+
* Throws when another relaunch already continues that run.
|
|
73
128
|
*/
|
|
74
129
|
export function findWorkflowReuseSource(asyncDirRoot, sessionId, scriptDigest, argsDigest) {
|
|
75
130
|
let runIds;
|
|
@@ -90,28 +145,36 @@ export function findWorkflowReuseSource(asyncDirRoot, sessionId, scriptDigest, a
|
|
|
90
145
|
}
|
|
91
146
|
if (status?.mode !== "workflow" || status.workflow?.scriptDigest !== scriptDigest || status.workflow.argsDigest !== argsDigest)
|
|
92
147
|
continue;
|
|
93
|
-
|
|
148
|
+
if (!isReusableWorkflowStopCause(status.workflow.stopCause))
|
|
149
|
+
return undefined;
|
|
150
|
+
assertWorkflowRunNotClaimed(asyncDir, runId, false);
|
|
151
|
+
return readWorkflowReuseChain(asyncDirRoot, runId, status);
|
|
94
152
|
}
|
|
95
153
|
return undefined;
|
|
96
154
|
}
|
|
97
155
|
/**
|
|
98
|
-
* The
|
|
99
|
-
* must match: each child is still reused only when its key and launch params match.
|
|
156
|
+
* The interrupted workflow run named by `options.reuseFrom`, from any session. Only the script
|
|
157
|
+
* must match: each child is still reused only when its key and launch params match. The run is
|
|
158
|
+
* reconciled first, so a run whose Pi process died is marked `owner-exited` before it is read.
|
|
100
159
|
*/
|
|
101
|
-
export function readNamedWorkflowReuseSource(asyncDirRoot, runId, scriptDigest, cwd) {
|
|
160
|
+
export function readNamedWorkflowReuseSource(asyncDirRoot, runId, scriptDigest, cwd, claimOwnerRunId) {
|
|
102
161
|
// A user-supplied id: it must name one run directory, never a path.
|
|
103
|
-
const
|
|
162
|
+
const runName = isRunId(runId);
|
|
163
|
+
if (runName)
|
|
164
|
+
reconcileAsyncRun(path.join(asyncDirRoot, runId));
|
|
165
|
+
const status = runName ? readStatus(path.join(asyncDirRoot, runId)) : null;
|
|
104
166
|
if (!status)
|
|
105
167
|
throw new Error(`options.reuseFrom: workflow run '${runId}' was not found.`);
|
|
106
168
|
if (status.mode !== "workflow")
|
|
107
169
|
throw new Error(`options.reuseFrom: run '${runId}' is not a workflow run.`);
|
|
108
|
-
if (status.workflow?.stopCause
|
|
109
|
-
throw new Error(`options.reuseFrom: workflow run '${runId}' did not stop because its session was replaced, so its children cannot be reused.`);
|
|
170
|
+
if (!isReusableWorkflowStopCause(status.workflow?.stopCause))
|
|
171
|
+
throw new Error(`options.reuseFrom: workflow run '${runId}' did not stop because its session was replaced or its Pi process exited, so its children cannot be reused.`);
|
|
110
172
|
if (status.workflow.scriptDigest !== scriptDigest)
|
|
111
173
|
throw new Error(`options.reuseFrom: workflow run '${runId}' ran a different workflow script.`);
|
|
112
174
|
if (status.cwd !== cwd)
|
|
113
175
|
throw new Error(`options.reuseFrom: workflow run '${runId}' ran in '${status.cwd}', not '${cwd}'.`);
|
|
114
|
-
|
|
176
|
+
assertWorkflowRunNotClaimed(path.join(asyncDirRoot, runId), runId, true, claimOwnerRunId);
|
|
177
|
+
return readWorkflowReuseChain(asyncDirRoot, runId, status);
|
|
115
178
|
}
|
|
116
179
|
/** A settled match is reusable only when that child succeeded; failed or stopped children run again. */
|
|
117
180
|
export function matchWorkflowReuse(source, key, fingerprint) {
|
|
@@ -9,6 +9,8 @@ export interface WorkflowKeyRevival {
|
|
|
9
9
|
/** State of the latest revived run; undefined when its status is unreadable. */
|
|
10
10
|
state?: AsyncStatus["state"];
|
|
11
11
|
}
|
|
12
|
+
/** A run directory name, never a path. */
|
|
13
|
+
export declare function isRunId(value: unknown): value is string;
|
|
12
14
|
/**
|
|
13
15
|
* Links a detached revival to the workflow key of its source run when the source failed and is
|
|
14
16
|
* an async workflow child or an earlier revival of one. Throws when the source status cannot be
|
|
@@ -13,7 +13,8 @@ export const REVIVAL_ORIGIN_FILE = "workflow-revival-origin.json";
|
|
|
13
13
|
/** Recording refuses a deeper link, so readers bounded by this always reach the chain's end. */
|
|
14
14
|
const MAX_REVIVAL_DEPTH = 16;
|
|
15
15
|
const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
16
|
-
|
|
16
|
+
/** A run directory name, never a path. */
|
|
17
|
+
export function isRunId(value) {
|
|
17
18
|
return typeof value === "string" && value.length > 0 && value.length <= 255 && path.basename(value) === value && value !== "." && value !== "..";
|
|
18
19
|
}
|
|
19
20
|
function readLink(file) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The extension runtime that owned the workflow was replaced (/reload, resume, project switch). */
|
|
2
|
+
export declare const WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED = "runtime-replaced";
|
|
3
|
+
/** Stale-run reconciliation found the Pi process that ran the workflow dead (crash, kill, reboot). */
|
|
4
|
+
export declare const WORKFLOW_STOP_CAUSE_OWNER_EXITED = "owner-exited";
|
|
5
|
+
export type WorkflowStopCause = typeof WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED | typeof WORKFLOW_STOP_CAUSE_OWNER_EXITED;
|
|
6
|
+
export declare function isReusableWorkflowStopCause(cause: unknown): cause is WorkflowStopCause;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Leaf module with no imports, so any module can use it without an import cycle.
|
|
2
|
+
/** The extension runtime that owned the workflow was replaced (/reload, resume, project switch). */
|
|
3
|
+
export const WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED = "runtime-replaced";
|
|
4
|
+
/** Stale-run reconciliation found the Pi process that ran the workflow dead (crash, kill, reboot). */
|
|
5
|
+
export const WORKFLOW_STOP_CAUSE_OWNER_EXITED = "owner-exited";
|
|
6
|
+
export function isReusableWorkflowStopCause(cause) {
|
|
7
|
+
return cause === WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED || cause === WORKFLOW_STOP_CAUSE_OWNER_EXITED;
|
|
8
|
+
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# Pi Subagents: Constraints And Recipes
|
|
2
|
-
|
|
3
|
-
This file is a detailed reference loaded from `skills/pi-subagents/SKILL.md`.
|
|
4
|
-
|
|
5
|
-
## Important Constraints
|
|
6
|
-
|
|
7
|
-
- **Explicit forking requires a persisted parent session.** If the current session
|
|
8
|
-
does not have a persisted session file or current leaf, explicit `context: "fork"`
|
|
9
|
-
fails. An agent-level `defaultContext: fork` is a preference: packaged `oracle`
|
|
10
|
-
and `advisor` fall back to `fresh` when those fork preconditions are not
|
|
11
|
-
met yet. Use `context: "fresh"` when you do not want a fork even after the parent
|
|
12
|
-
session exists.
|
|
13
|
-
- **Packaged workers start fresh.** `worker` defaults to fresh context so its brief,
|
|
14
|
-
not the parent's unfinished agenda, controls the implementation. Pass explicit
|
|
15
|
-
`context: "fork"` when inherited conversation history is required.
|
|
16
|
-
- **Forked runs inherit parent history.** They are branched threads, not fresh
|
|
17
|
-
filtered contexts. Use fresh context for adversarial reviewers unless the user explicitly asks for forked context.
|
|
18
|
-
- **Default subagent nesting depth is 2.** Deeper recursive delegation is blocked
|
|
19
|
-
unless configured otherwise.
|
|
20
|
-
- **Attention signals are not lifecycle state.** `needs_attention` means no activity has been observed past the configured threshold. `paused` means the child turn was intentionally interrupted or is awaiting direction; it is not the same as `failed`.
|
|
21
|
-
- **Intercom asks are blocking.** A session can only maintain one pending outbound
|
|
22
|
-
ask wait state at a time.
|
|
23
|
-
- **Keep conversational authority clear.** Advisory subagents should not silently
|
|
24
|
-
become second decision-makers.
|
|
25
|
-
- **Respect the fixed authority policy.** `authorityPolicy` is a small `auto` / `confirm` / `forbid` map for supported operational actions. Worktree discard, destructive cleanup, and spawn-budget grants default to confirmation; stop, steer, and schedule creation remain automatic. Use `worktree.discard` with the durable `handoffPath`; confirm-required actions refuse safely without an interactive UI and retained paths include manual Git recovery commands.
|
|
26
|
-
|
|
27
|
-
Runtime config can change orchestration behavior. `intercomBridge.resultDelivery: false` disables only external acknowledged grouped-result delivery when native parent notifications own completion; supervisor asks/progress stay active, and enabled transport failures are still reported. `asyncByDefault` and `forceTopLevelAsync` affect whether launches detach; `waitTool` can make direct `bg_wait()` calls return immediately while headless auto-drain remains active, and its effective value is propagated to child runtimes; `globalConcurrencyLimit` bounds concurrent fanout, while a positive `maxSubagentSpawnsPerSession` optionally caps cumulative launches (`0` or unset is unlimited). Status and doctor report the budget; static work preflights declared capacity; only the settled root interactive parent can use `grant-spawn-budget` after native confirmation, with total grants bounded by the original cap. Compaction does not reset usage or grants; `singleRunOutputBaseDir` and `worktreeBaseDir` route outputs and worktrees; `completionBatch` groups async notifications. `artifactDir` is `session` (default), `project`, or `temp` and chooses where subagent artifacts are stored. Set `asyncWidget: false` to hide the above-editor background-run widget when a companion footer or dashboard owns that space (fleet inspector remains available). Per-run `artifacts: false` disables artifact capture for that launch. Async status and result artifacts include `lifecycleArtifactVersion` and fields such as `workflowGraph`, `steps`, `results`, `totalTokens`, `totalCost`, `turnCount`, `toolCount`, and nested `children`. Prefer these artifacts and `status` views over scraping terminal output.
|
|
28
|
-
|
|
29
|
-
### Keep report artifacts out of the repository root
|
|
30
|
-
|
|
31
|
-
Treat lane reports, review notes, council pass reports, and gate logs as scratch unless the user explicitly asks to keep them. Prefer `output: false` and the aggregate workflow result for short reports. When a later step needs a file, use the runtime-managed output artifact by setting a stable child key plus a relative `output` path such as `plans/deploy.md`; relative child outputs are saved under the run artifact directory, not the project root. Do not put `reports/...`, `*-report.json`, or similar repo-root paths in child task text.
|
|
32
|
-
|
|
33
|
-
For durable evidence, copy only the final summary to session memory, a PR body/comment, a mission artifact, or a user-approved docs path outside the repo. After the PR, issue, or gate reaches a terminal state, delete or move scratch reports from the active worktree before reporting completion. Keep a project `.gitignore` entry for ad-hoc report patterns only as a safety net; it is not the cleanup mechanism.
|
|
34
|
-
|
|
35
|
-
## Best Practices
|
|
36
|
-
|
|
37
|
-
- Run subagents asynchronously by default; direct one-child execution is enough for one bounded task, while a workflow script (a ```` ```js workflow ```` block plus `subagent({ workflow: true })`) is the composition surface for JavaScript control flow and data-dependent branching. Use `async: false` only when the parent must block. See `references/execution-controls.md` → Async/background for wait semantics.
|
|
38
|
-
- For a predeclared broad plan split into visible narrow stages, use `runs.lanes([...])` inside a workflow script; use raw `runs.run(...)`/`runs.all(...)` for conditional or rolling flows. See [`execution-controls.md`](execution-controls.md#parallel-sequential-lanes).
|
|
39
|
-
- Keep one writer per cwd/worktree. Parallelize reading, review, and validation; concurrent writers need isolated worktrees. Give every child a cold-start packet with its goal, target/ref, authority, context, success criteria, validation, output, and stop rules.
|
|
40
|
-
- Keep tasks narrow and standalone; do not rely on issue numbers, broad globs, or supervisor round-trips to supply missing context.
|
|
41
|
-
- Keep authority with the parent. Escalate unapproved product, scope, architecture, merge, credential, or release decisions; checks, receipts, and review bots are evidence, not authority.
|
|
42
|
-
- Use `fresh` context for adversarial review. `fork` is a persisted, history-inheriting branch; see `references/execution-controls.md` for its preconditions.
|
|
43
|
-
- Use a same-session oracle follow-up only when its first answer leaves a material tradeoff. Treat `needs_attention` as a control signal, not failure, and do not interrupt a child merely because it is quiet during tools, tests, or reasoning.
|
|
44
|
-
- Use `/name` when intercom targeting needs a stable session name.
|
|
45
|
-
|
|
46
|
-
## Workflow selection
|
|
47
|
-
|
|
48
|
-
This reference keeps cross-cutting policy and failure handling. Load the matching domain reference for detail:
|
|
49
|
-
|
|
50
|
-
| Need | Read |
|
|
51
|
-
| --- | --- |
|
|
52
|
-
| Execution syntax, lifecycle, async/wait, missions, controls, watchdog, or worktrees | [`references/execution-controls.md`](execution-controls.md) |
|
|
53
|
-
| Role choice, prompt contracts, review/research/cleanup techniques, or model tiering | [`references/prompting-and-roles.md`](prompting-and-roles.md) |
|
|
54
|
-
| Fresh review, validation, gate failures, finding disposition, and final delivery checks | [`references/review-and-validation.md`](review-and-validation.md) |
|
|
55
|
-
| Independent lanes, repositories, worktrees, and handoffs | [`references/multi-lane-orchestration.md`](multi-lane-orchestration.md) |
|
|
56
|
-
| Agent management, file authoring, prompt integration, or RPC | [`references/management-authoring-rpc.md`](management-authoring-rpc.md) |
|
|
57
|
-
|
|
58
|
-
After delegation is operator-authorized, choose the smallest recipe that earns
|
|
59
|
-
its overhead. Recipes select a shape; they do not authorize delegation:
|
|
60
|
-
|
|
61
|
-
- **Recon → plan → implement:** run one focused `scout`, then one `worker` that consumes its findings.
|
|
62
|
-
- **Implementation:** clarify scope and acceptance, record user-owned decisions and seam/validation contracts, and use a bounded scout, writer, or fresh reviewer only where the requested delegation benefits from that stage. Keep one writer, inspect direct evidence, and require every added stage to earn its overhead. Split large work into serial milestones instead of a writer swarm; do not stop at review without disposition.
|
|
63
|
-
- **Parallel analysis:** fan out only independent read/review/validation work, or isolate each writer in its own worktree. Never run concurrent writers in one checkout.
|
|
64
|
-
|
|
65
|
-
## Error Handling
|
|
66
|
-
|
|
67
|
-
- **Unknown agent:** the error lists the available agents and the directories searched; check scope/precedence and author new orchestration as a workflow script, not legacy chains.
|
|
68
|
-
- **Setup, discovery, or intercom confusion:** run `subagent({ action: "doctor" })`.
|
|
69
|
-
- **Max subagent depth exceeded:** flatten the workflow or raise `maxSubagentDepth` in config.
|
|
70
|
-
- **Missing session file for a fork:** persist the parent session before using `context: "fork"`.
|
|
71
|
-
- **Intercom already waiting for a reply:** resolve the pending ask before starting another.
|
|
72
|
-
- **Parallel output-path conflict:** give each task a distinct output path, or disable output where no artifact is needed.
|
|
73
|
-
- **Worktree launch failure:** ensure the git tree is clean and task cwd overrides match the shared cwd.
|
|
74
|
-
- **Child fails before starting:** inspect `subagent({ action: "status", id: "..." })`, artifact metadata, output logs, and `doctor`; loader errors usually appear in child logs.
|