@ferris1225/pi-subagents 4.1.18 → 4.1.21
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/README.md +384 -337
- package/agents/cleaner.md +50 -45
- package/agents/documenter.md +40 -42
- package/agents/explorer.md +40 -45
- package/agents/reviewer.md +82 -82
- package/agents/synthesizer.md +39 -0
- package/agents/worker.md +43 -45
- package/package.json +55 -55
- package/src/agents.ts +25 -5
- package/src/announcements.ts +78 -75
- package/src/background.ts +11 -0
- package/src/completion.ts +19 -9
- package/src/config.ts +3 -10
- package/src/dispatch.ts +817 -647
- package/src/durable.ts +443 -402
- package/src/format.ts +173 -179
- package/src/index.ts +6 -6
- package/src/models.ts +4 -6
- package/src/monitor.ts +56 -5
- package/src/prompt.ts +14 -21
- package/src/recovery.ts +145 -145
- package/src/rpc-run.ts +993 -993
- package/src/runtime.ts +22 -4
- package/src/session-fork.ts +2 -0
- package/src/setup.ts +23 -43
- package/src/spawn.ts +668 -654
- package/src/temp-hygiene.ts +230 -174
- package/src/thread-lifecycle.ts +1487 -1399
- package/src/tools.ts +384 -712
- package/src/widget.ts +195 -157
- package/src/workflow.ts +24 -8
- package/src/worktree.ts +18 -0
package/agents/worker.md
CHANGED
|
@@ -1,45 +1,43 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: worker
|
|
3
|
-
description: Default
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
##
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
Keep the final response comfortably below the 40-line delivery cap unless the result genuinely requires more. Root-cause fixes over patches; no unrelated churn; an unrun check is never a passed check.
|
|
1
|
+
---
|
|
2
|
+
name: worker
|
|
3
|
+
description: Default route for any non-trivial, self-contained code change — implement, fix, refactor, or test, then verify and hand off.
|
|
4
|
+
thinking: high
|
|
5
|
+
# No `tools` field => inherits all tools (full capability).
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a worker agent with full capabilities in an isolated context window. You own a delegated, self-contained task end to end so the main conversation stays clean. You have NOT got the caller's conversation history — the task brief is your source of truth.
|
|
9
|
+
|
|
10
|
+
## Procedure
|
|
11
|
+
|
|
12
|
+
1. **Context.** Read the brief fully. Read referenced files — and referenced images (screenshots, mockups) when the model supports vision — before editing. If critical context is missing, state what an `explorer` should retrieve rather than guessing.
|
|
13
|
+
2. **Plan.** Inspect existing code and conventions first; form the smallest coherent root-cause change that satisfies the brief. Prefer the design that deletes complexity rather than rearranges it, and decompose rather than push a file toward ~1000 lines. For a large task, note files, order, and risks before editing. No unrelated refactors or standalone docs work unless the brief asks.
|
|
14
|
+
3. **Implement.** Preserve the user's work; limit edits to the request plus required validation. Follow the project's error handling, naming, and style. Synchronize existing README/docs/examples/comments directly affected by your change; do not defer obvious drift.
|
|
15
|
+
4. **Verify.** Run the project's format/build/tests when they exist (e.g. `tsc --noEmit`, the test runner). NEVER report an unrun check as passed — report it as unavailable or a pre-existing failure, with the exact error.
|
|
16
|
+
5. **Handoff.** Return only the concrete outcome. Do not repeat the task brief, plan, root-cause investigation, or tool chronology; report only unresolved blockers and checks that remain failed.
|
|
17
|
+
|
|
18
|
+
## Boundaries
|
|
19
|
+
|
|
20
|
+
- Never commit, push, publish, tag, release, or bump a package version. The parent workflow owns the independent review gate and every release action — even when repository instructions normally automate release after green checks.
|
|
21
|
+
- Children are leaf processes: you cannot dispatch sub-agents.
|
|
22
|
+
- When your brief carries reviewer findings, apply their fix instructions: implement each when it is sound; when it is wrong, out of scope, or a sounder fix exists, ship your fix and push back in your report — cite the finding, refute the instruction's reasoning, and describe what you shipped instead. A deviation without reasoning will be re-opened.
|
|
23
|
+
- Do not ask the caller to duplicate downstream roles, and never treat your own verification as the final gate.
|
|
24
|
+
|
|
25
|
+
## Output format
|
|
26
|
+
|
|
27
|
+
## Completed
|
|
28
|
+
|
|
29
|
+
What was done, in a few lines.
|
|
30
|
+
|
|
31
|
+
## Files Changed
|
|
32
|
+
|
|
33
|
+
- `path/to/file.ts` — what changed.
|
|
34
|
+
|
|
35
|
+
## Verification
|
|
36
|
+
|
|
37
|
+
Which checks you ACTUALLY ran and their result (e.g. `tsc --noEmit` clean; `vitest` 12 passed). State explicitly anything you could not run and why.
|
|
38
|
+
|
|
39
|
+
## Notes (only when material)
|
|
40
|
+
|
|
41
|
+
Unresolved blockers, rejected requirements, or decisions the caller must know. For a reviewer handoff: exact paths changed and the key functions/types touched. Omit when nothing actionable.
|
|
42
|
+
|
|
43
|
+
Keep the final response comfortably below the 40-line delivery cap unless the result genuinely requires more.
|
package/package.json
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "4.1.
|
|
4
|
-
"description": "A managed sub-agent team for pi: specialized roles, pre-commit documentation sync, retained threads, auto-fix chains, model fallback, and Git worktree isolation.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"publishConfig": {
|
|
8
|
-
"access": "public"
|
|
9
|
-
},
|
|
10
|
-
"keywords": [
|
|
11
|
-
"pi-package",
|
|
12
|
-
"pi-extension",
|
|
13
|
-
"subagent",
|
|
14
|
-
"sub-agent",
|
|
15
|
-
"delegation",
|
|
16
|
-
"code-cleanup",
|
|
17
|
-
"dead-code"
|
|
18
|
-
],
|
|
19
|
-
"files": [
|
|
20
|
-
"src",
|
|
21
|
-
"agents",
|
|
22
|
-
"README.md",
|
|
23
|
-
"LICENSE"
|
|
24
|
-
],
|
|
25
|
-
"pi": {
|
|
26
|
-
"extensions": [
|
|
27
|
-
"./src/index.ts"
|
|
28
|
-
]
|
|
29
|
-
},
|
|
30
|
-
"scripts": {
|
|
31
|
-
"check": "tsc --noEmit",
|
|
32
|
-
"test": "vitest run tests",
|
|
33
|
-
"prepack": "npm run check && npm test"
|
|
34
|
-
},
|
|
35
|
-
"peerDependencies": {
|
|
36
|
-
"@earendil-works/pi-agent-core": ">=0.84.4",
|
|
37
|
-
"@earendil-works/pi-ai": ">=0.84.4",
|
|
38
|
-
"@earendil-works/pi-coding-agent": ">=0.84.4",
|
|
39
|
-
"@earendil-works/pi-tui": ">=0.84.4",
|
|
40
|
-
"typebox": "*"
|
|
41
|
-
},
|
|
42
|
-
"devDependencies": {
|
|
43
|
-
"@earendil-works/pi-agent-core": "^0.84.4",
|
|
44
|
-
"@earendil-works/pi-ai": "^0.84.4",
|
|
45
|
-
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
46
|
-
"@earendil-works/pi-tui": "^0.84.4",
|
|
47
|
-
"@types/node": "^22.10.0",
|
|
48
|
-
"typebox": "^1.3.9",
|
|
49
|
-
"typescript": "^5.9.0",
|
|
50
|
-
"vitest": "^4.1.0"
|
|
51
|
-
},
|
|
52
|
-
"engines": {
|
|
53
|
-
"node": ">=22.19.0"
|
|
54
|
-
}
|
|
55
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@ferris1225/pi-subagents",
|
|
3
|
+
"version": "4.1.21",
|
|
4
|
+
"description": "A managed sub-agent team for pi: specialized roles, pre-commit documentation sync, retained threads, auto-fix chains, model fallback, and Git worktree isolation.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"pi-package",
|
|
12
|
+
"pi-extension",
|
|
13
|
+
"subagent",
|
|
14
|
+
"sub-agent",
|
|
15
|
+
"delegation",
|
|
16
|
+
"code-cleanup",
|
|
17
|
+
"dead-code"
|
|
18
|
+
],
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"agents",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"pi": {
|
|
26
|
+
"extensions": [
|
|
27
|
+
"./src/index.ts"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"check": "tsc --noEmit",
|
|
32
|
+
"test": "vitest run tests",
|
|
33
|
+
"prepack": "npm run check && npm test"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@earendil-works/pi-agent-core": ">=0.84.4",
|
|
37
|
+
"@earendil-works/pi-ai": ">=0.84.4",
|
|
38
|
+
"@earendil-works/pi-coding-agent": ">=0.84.4",
|
|
39
|
+
"@earendil-works/pi-tui": ">=0.84.4",
|
|
40
|
+
"typebox": "*"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@earendil-works/pi-agent-core": "^0.84.4",
|
|
44
|
+
"@earendil-works/pi-ai": "^0.84.4",
|
|
45
|
+
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
46
|
+
"@earendil-works/pi-tui": "^0.84.4",
|
|
47
|
+
"@types/node": "^22.10.0",
|
|
48
|
+
"typebox": "^1.3.9",
|
|
49
|
+
"typescript": "^5.9.0",
|
|
50
|
+
"vitest": "^4.1.0"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=22.19.0"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/agents.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { dirname, join } from "node:path";
|
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import { THINKING_LEVEL_VALUES, type AgentScope, type ThinkingLevel } from "./config.ts";
|
|
19
|
+
import type { IsolationMode } from "./worktree.ts";
|
|
19
20
|
|
|
20
21
|
export type AgentSource = "builtin" | "user" | "project";
|
|
21
22
|
|
|
@@ -23,21 +24,27 @@ export interface AgentConfig {
|
|
|
23
24
|
name: string;
|
|
24
25
|
description: string;
|
|
25
26
|
tools?: string[];
|
|
27
|
+
/** Model ref this run was routed to; filled in by dispatch, never declared by the agent file. */
|
|
26
28
|
model?: string;
|
|
27
29
|
/** Per-agent default thinking strength (frontmatter `thinking`); config override wins. */
|
|
28
30
|
thinking?: ThinkingLevel;
|
|
31
|
+
/** Role-declared default isolation (frontmatter `isolation`); an explicit
|
|
32
|
+
* per-call request wins, and `worktree` applies to write-capable roles only. */
|
|
33
|
+
isolation?: IsolationMode;
|
|
29
34
|
systemPrompt: string;
|
|
30
35
|
source: AgentSource;
|
|
31
36
|
filePath: string;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
const SHELL_TOOL_NAMES = new Set(["bash", "powershell"]);
|
|
40
|
+
/** The shell that actually fits the host: PowerShell on Windows, Bash elsewhere.
|
|
41
|
+
* Only used to break a tie when the parent has both enabled — a parent running a
|
|
42
|
+
* single shell is followed as configured, whatever it is. */
|
|
43
|
+
const NATIVE_SHELL_TOOL = process.platform === "win32" ? "powershell" : "bash";
|
|
35
44
|
const PI_BUILTIN_TOOL_NAMES = new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"]);
|
|
36
45
|
export const SUBAGENT_TOOL_NAMES = [
|
|
37
46
|
"subagent",
|
|
38
47
|
"subagent_control",
|
|
39
|
-
"subagent_wait",
|
|
40
|
-
"subagent_status",
|
|
41
48
|
"subagent_stop",
|
|
42
49
|
] as const;
|
|
43
50
|
const SUBAGENT_TOOL_NAME_SET = new Set<string>(SUBAGENT_TOOL_NAMES);
|
|
@@ -45,7 +52,13 @@ const SUBAGENT_TOOL_NAME_SET = new Set<string>(SUBAGENT_TOOL_NAMES);
|
|
|
45
52
|
/** Resolve every child against the parent's live tool selection. Roles without
|
|
46
53
|
* an allowlist inherit the complete active set. Explicit lists keep only their
|
|
47
54
|
* declared Pi built-ins, adapt an existing shell slot, and gain active extension/
|
|
48
|
-
* SDK tools. pi-subagents controls are always removed so children stay leaves.
|
|
55
|
+
* SDK tools. pi-subagents controls are always removed so children stay leaves.
|
|
56
|
+
*
|
|
57
|
+
* A declared shell is one slot, so it resolves to one shell: the parent's, and
|
|
58
|
+
* the host-native one when the parent runs both. A child never inherits a shell
|
|
59
|
+
* the parent does not have — Pi's `--tools` allowlist overrides the child's own
|
|
60
|
+
* `defaultTools` setting, so naming a shell the user disabled would hand it a
|
|
61
|
+
* terminal they deliberately turned off. */
|
|
49
62
|
export function resolveAgentTools(
|
|
50
63
|
agent: AgentConfig,
|
|
51
64
|
activeToolNames: readonly string[],
|
|
@@ -53,7 +66,10 @@ export function resolveAgentTools(
|
|
|
53
66
|
const active = [...new Set(activeToolNames)].filter((tool) => !SUBAGENT_TOOL_NAME_SET.has(tool));
|
|
54
67
|
if (!agent.tools) return { ...agent, tools: active };
|
|
55
68
|
|
|
56
|
-
const
|
|
69
|
+
const parentShellTools = active.filter((tool) => SHELL_TOOL_NAMES.has(tool));
|
|
70
|
+
const activeShellTools = parentShellTools.length > 1 && parentShellTools.includes(NATIVE_SHELL_TOOL)
|
|
71
|
+
? [NATIVE_SHELL_TOOL]
|
|
72
|
+
: parentShellTools;
|
|
57
73
|
const tools: string[] = [];
|
|
58
74
|
let shellAdapted = false;
|
|
59
75
|
for (const tool of agent.tools) {
|
|
@@ -133,13 +149,17 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
133
149
|
const thinking = (THINKING_LEVEL_VALUES as readonly string[]).includes(rawThinking ?? "")
|
|
134
150
|
? (rawThinking as ThinkingLevel)
|
|
135
151
|
: undefined;
|
|
152
|
+
const rawIsolation = str(frontmatter.isolation)?.trim();
|
|
153
|
+
const isolation = rawIsolation === "worktree" || rawIsolation === "shared"
|
|
154
|
+
? (rawIsolation as IsolationMode)
|
|
155
|
+
: undefined;
|
|
136
156
|
|
|
137
157
|
agents.push({
|
|
138
158
|
name,
|
|
139
159
|
description,
|
|
140
160
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
141
|
-
model: str(frontmatter.model),
|
|
142
161
|
...(thinking ? { thinking } : {}),
|
|
162
|
+
...(isolation ? { isolation } : {}),
|
|
143
163
|
systemPrompt: body,
|
|
144
164
|
source,
|
|
145
165
|
filePath,
|
package/src/announcements.ts
CHANGED
|
@@ -1,75 +1,78 @@
|
|
|
1
|
-
/** Session-start recovery, stale-config migration, and widget installation. */
|
|
2
|
-
|
|
3
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { loadConfig, saveConfig } from "./config.ts";
|
|
6
|
-
import { availableModelsInScope, filterUnavailableModelOverrides } from "./models.ts";
|
|
7
|
-
import { announceRecoveryRecords } from "./recovery.ts";
|
|
8
|
-
import type { SubagentRuntime } from "./runtime.ts";
|
|
9
|
-
import { installActiveRunsWidget } from "./widget.ts";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* One-time-per-stale-override migration: keep agent model selections Pi still
|
|
13
|
-
* reports as available, drop the rest back to dynamic main-model routing, and
|
|
14
|
-
* tell the user what was removed. Saving the cleaned config is what makes it
|
|
15
|
-
* one-time — the dropped refs no longer exist to re-trigger the notice.
|
|
16
|
-
*/
|
|
17
|
-
async function migrateUnavailableAgentModels(
|
|
18
|
-
ctx: { ui: { notify: (message: string, kind: "info" | "warning" | "error") => void } } & Parameters<typeof availableModelsInScope>[0],
|
|
19
|
-
runtime: SubagentRuntime,
|
|
20
|
-
): Promise<void> {
|
|
21
|
-
try {
|
|
22
|
-
const config = await loadConfig(runtime.configPath);
|
|
23
|
-
const overrides = Object.entries(config.agentModels);
|
|
24
|
-
if (overrides.length === 0) return;
|
|
25
|
-
const { kept, dropped } = filterUnavailableModelOverrides(config.agentModels, availableModelsInScope(ctx));
|
|
26
|
-
if (dropped.length === 0) return;
|
|
27
|
-
await saveConfig({ ...config, agentModels: kept }, runtime.configPath);
|
|
28
|
-
const list = dropped.map(({ agent, ref }) => `${agent}: ${ref}`).join(", ");
|
|
29
|
-
ctx.ui.notify(
|
|
30
|
-
`pi-subagents: removed stale agent model overrides that are no longer available (${list}). Those agents now follow the current main model; run /subagents-setup to re-pick.`,
|
|
31
|
-
"warning",
|
|
32
|
-
);
|
|
33
|
-
} catch {
|
|
34
|
-
/* migration failures are non-fatal */
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function registerAnnouncements(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
39
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
40
|
-
if (!existsSync(runtime.configPath)) {
|
|
41
|
-
ctx.ui.notify(
|
|
42
|
-
"pi-subagents: no configuration yet — run /subagents-setup to pick agents, models, and thinking strengths. Defaults (all
|
|
43
|
-
"info",
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
await announceRecoveryRecords(runtime.configPath, ctx);
|
|
47
|
-
await migrateUnavailableAgentModels(ctx, runtime);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
1
|
+
/** Session-start recovery, stale-config migration, and widget installation. */
|
|
2
|
+
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { loadConfig, saveConfig } from "./config.ts";
|
|
6
|
+
import { availableModelsInScope, filterUnavailableModelOverrides } from "./models.ts";
|
|
7
|
+
import { announceRecoveryRecords } from "./recovery.ts";
|
|
8
|
+
import type { SubagentRuntime } from "./runtime.ts";
|
|
9
|
+
import { installActiveRunsWidget } from "./widget.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One-time-per-stale-override migration: keep agent model selections Pi still
|
|
13
|
+
* reports as available, drop the rest back to dynamic main-model routing, and
|
|
14
|
+
* tell the user what was removed. Saving the cleaned config is what makes it
|
|
15
|
+
* one-time — the dropped refs no longer exist to re-trigger the notice.
|
|
16
|
+
*/
|
|
17
|
+
async function migrateUnavailableAgentModels(
|
|
18
|
+
ctx: { ui: { notify: (message: string, kind: "info" | "warning" | "error") => void } } & Parameters<typeof availableModelsInScope>[0],
|
|
19
|
+
runtime: SubagentRuntime,
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
try {
|
|
22
|
+
const config = await loadConfig(runtime.configPath);
|
|
23
|
+
const overrides = Object.entries(config.agentModels);
|
|
24
|
+
if (overrides.length === 0) return;
|
|
25
|
+
const { kept, dropped } = filterUnavailableModelOverrides(config.agentModels, availableModelsInScope(ctx));
|
|
26
|
+
if (dropped.length === 0) return;
|
|
27
|
+
await saveConfig({ ...config, agentModels: kept }, runtime.configPath);
|
|
28
|
+
const list = dropped.map(({ agent, ref }) => `${agent}: ${ref}`).join(", ");
|
|
29
|
+
ctx.ui.notify(
|
|
30
|
+
`pi-subagents: removed stale agent model overrides that are no longer available (${list}). Those agents now follow the current main model; run /subagents-setup to re-pick.`,
|
|
31
|
+
"warning",
|
|
32
|
+
);
|
|
33
|
+
} catch {
|
|
34
|
+
/* migration failures are non-fatal */
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function registerAnnouncements(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
39
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
40
|
+
if (!existsSync(runtime.configPath)) {
|
|
41
|
+
ctx.ui.notify(
|
|
42
|
+
"pi-subagents: no configuration yet — run /subagents-setup to pick agents, models, and thinking strengths. Defaults (all built-in agents on the main model) apply until then.",
|
|
43
|
+
"info",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
await announceRecoveryRecords(runtime.configPath, ctx);
|
|
47
|
+
await migrateUnavailableAgentModels(ctx, runtime);
|
|
48
|
+
// Restore starts at extension load and session_start fires right behind
|
|
49
|
+
// it, so without this the notice reports whatever the race left behind.
|
|
50
|
+
await runtime.durableRestore;
|
|
51
|
+
if (!runtime.restoredNotified && runtime.restoredRunIds.length > 0) {
|
|
52
|
+
runtime.restoredNotified = true;
|
|
53
|
+
const ids = runtime.restoredRunIds.map((id) => `#${id}`).join(", ");
|
|
54
|
+
ctx.ui.notify(
|
|
55
|
+
`pi-subagents: restored ${runtime.restoredRunIds.length} interrupted thread${runtime.restoredRunIds.length === 1 ? "" : "s"} (${ids}) with retained context. subagent_control resume continues one.`,
|
|
56
|
+
"info",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (ctx.mode !== "tui") return;
|
|
60
|
+
installActiveRunsWidget(ctx);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Compaction failures are otherwise silent in long orchestration sessions
|
|
64
|
+
// where subagent results accumulate; aborted (user-cancelled) compactions
|
|
65
|
+
// are deliberate and not worth a notice.
|
|
66
|
+
pi.on("session_compact_failed", async (event, ctx) => {
|
|
67
|
+
if (event.aborted && !event.errorMessage) return;
|
|
68
|
+
const detail = event.errorMessage ? `: ${event.errorMessage}` : "";
|
|
69
|
+
if (event.willRetry) {
|
|
70
|
+
ctx.ui.notify(`pi-subagents: session compaction failed${detail} — retrying automatically.`, "warning");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
ctx.ui.notify(
|
|
74
|
+
`pi-subagents: session compaction failed${detail}. Long threads may hit context limits soon; run /compact to retry or trim old results.`,
|
|
75
|
+
"error",
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
}
|
package/src/background.ts
CHANGED
|
@@ -86,6 +86,17 @@ export class BackgroundTaskQueue {
|
|
|
86
86
|
return this.concurrency;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/** Tasks still waiting for a free slot (never started). */
|
|
90
|
+
get pendingCount(): number {
|
|
91
|
+
return this.pending.length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Tasks currently holding a slot. Suspended tasks (lane waits, managed
|
|
95
|
+
* workflow continuations) hold none and are excluded. */
|
|
96
|
+
get activeCount(): number {
|
|
97
|
+
return this.active.size;
|
|
98
|
+
}
|
|
99
|
+
|
|
89
100
|
/** Stop counting a running task toward the concurrency limit. Its body keeps
|
|
90
101
|
* running under the same abort signal; completion still releases everything
|
|
91
102
|
* waitForTask/waitForIdle promise. Frees a slot for queued work immediately.
|
package/src/completion.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
|
|
11
|
-
import { formatUsageCompact, sumUsage } from "./monitor.ts";
|
|
11
|
+
import { formatUsageCompact, sumUsage, type RunWaitReason } from "./monitor.ts";
|
|
12
12
|
import type { UsageStats } from "./rpc-run.ts";
|
|
13
13
|
|
|
14
14
|
export interface CompletionBatchTimings {
|
|
@@ -139,9 +139,22 @@ export interface ActiveRunFoot {
|
|
|
139
139
|
agent: string;
|
|
140
140
|
/** Optional content label (task-derived) shown next to the agent name. */
|
|
141
141
|
label?: string;
|
|
142
|
-
/**
|
|
143
|
-
*
|
|
144
|
-
|
|
142
|
+
/** Why a not-yet-executing run is waiting. Stated precisely so a repository
|
|
143
|
+
* lane wait or a starting child is never mistaken for an exhausted pool. */
|
|
144
|
+
wait?: RunWaitReason;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function activeRunWaitTag(wait: RunWaitReason | undefined): string {
|
|
148
|
+
switch (wait) {
|
|
149
|
+
case "process-slot":
|
|
150
|
+
return " (queued, starts when a process slot frees)";
|
|
151
|
+
case "repository-lane":
|
|
152
|
+
return " (waiting for the repository write lane, not for a slot)";
|
|
153
|
+
case "starting":
|
|
154
|
+
return " (starting)";
|
|
155
|
+
default:
|
|
156
|
+
return "";
|
|
157
|
+
}
|
|
145
158
|
}
|
|
146
159
|
|
|
147
160
|
/**
|
|
@@ -156,11 +169,8 @@ export function formatActiveRunsFooter(runs: readonly ActiveRunFoot[], maxListed
|
|
|
156
169
|
if (runs.length === 0) return "";
|
|
157
170
|
const listed = runs.slice(0, maxListed);
|
|
158
171
|
const items = listed
|
|
159
|
-
.map((run) => {
|
|
160
|
-
const tagged = run.queued ? " (queued, starts when a slot frees)" : "";
|
|
161
|
-
return `#${run.id} ${run.agent}${run.label ? `·${run.label}` : ""}${tagged}`;
|
|
162
|
-
})
|
|
172
|
+
.map((run) => `#${run.id} ${run.agent}${run.label ? `·${run.label}` : ""}${activeRunWaitTag(run.wait)}`)
|
|
163
173
|
.join(", ");
|
|
164
174
|
const more = runs.length > listed.length ? `, +${runs.length - listed.length} more` : "";
|
|
165
|
-
return `\n\n⚠ ${runs.length} other run${runs.length === 1 ? "" : "s"} still active: ${items}${more}. Do not conclude the overall task yet —
|
|
175
|
+
return `\n\n⚠ ${runs.length} other run${runs.length === 1 ? "" : "s"} still active: ${items}${more}. Do not conclude the overall task yet — their results wake you automatically.`;
|
|
166
176
|
}
|
package/src/config.ts
CHANGED
|
@@ -13,10 +13,10 @@ import { dirname, join } from "node:path";
|
|
|
13
13
|
import { getAgentDir, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
|
|
15
15
|
/** Full catalog of agents shipped with the package (selectable in /subagents-setup). */
|
|
16
|
-
export const BUILTIN_AGENT_NAMES = ["explorer", "worker", "cleaner", "documenter", "reviewer"] as const;
|
|
16
|
+
export const BUILTIN_AGENT_NAMES = ["explorer", "worker", "cleaner", "documenter", "synthesizer", "reviewer"] as const;
|
|
17
17
|
|
|
18
18
|
/** Agents enabled out of the box on a fresh install. */
|
|
19
|
-
export const DEFAULT_ENABLED_AGENTS: readonly string[] = [
|
|
19
|
+
export const DEFAULT_ENABLED_AGENTS: readonly string[] = [...BUILTIN_AGENT_NAMES];
|
|
20
20
|
|
|
21
21
|
export const AGENT_SCOPE_VALUES = ["user", "project", "both"] as const;
|
|
22
22
|
export type AgentScope = (typeof AGENT_SCOPE_VALUES)[number];
|
|
@@ -45,7 +45,7 @@ export const DEFAULT_IDLE_TIMEOUT_SEC = 90;
|
|
|
45
45
|
export const IDLE_TIMEOUT_SEC_LIMIT = 600;
|
|
46
46
|
|
|
47
47
|
export interface SubagentsConfig {
|
|
48
|
-
/** Agent names that are discoverable and injected. Fresh-install default:
|
|
48
|
+
/** Agent names that are discoverable and injected. Fresh-install default: every built-in agent. */
|
|
49
49
|
enabledAgents: string[];
|
|
50
50
|
/** Per-agent model override, keyed by agent name, as "provider/model-id". */
|
|
51
51
|
agentModels: Record<string, string>;
|
|
@@ -62,8 +62,6 @@ export interface SubagentsConfig {
|
|
|
62
62
|
* is included in the message. Default: 80.
|
|
63
63
|
*/
|
|
64
64
|
maxResultLines: number;
|
|
65
|
-
/** Whether to inject the delegation directive into the parent system prompt. Default: true. */
|
|
66
|
-
proactiveInjection: boolean;
|
|
67
65
|
/** Which agent directories to discover from. Default: "user". */
|
|
68
66
|
agentScope: AgentScope;
|
|
69
67
|
/**
|
|
@@ -80,7 +78,6 @@ export const DEFAULT_CONFIG: SubagentsConfig = {
|
|
|
80
78
|
agentThinkingLevels: {},
|
|
81
79
|
notifyOnReviewPass: false,
|
|
82
80
|
maxResultLines: DEFAULT_MAX_RESULT_LINES,
|
|
83
|
-
proactiveInjection: true,
|
|
84
81
|
agentScope: "user",
|
|
85
82
|
idleTimeoutSec: DEFAULT_IDLE_TIMEOUT_SEC,
|
|
86
83
|
};
|
|
@@ -155,10 +152,6 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
155
152
|
const maxResultLines = clampCount(raw.maxResultLines, MAX_RESULT_LINES_LIMIT);
|
|
156
153
|
if (maxResultLines !== undefined) config.maxResultLines = maxResultLines;
|
|
157
154
|
|
|
158
|
-
if (typeof raw.proactiveInjection === "boolean") {
|
|
159
|
-
config.proactiveInjection = raw.proactiveInjection;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
155
|
if (isAgentScope(raw.agentScope)) {
|
|
163
156
|
config.agentScope = raw.agentScope;
|
|
164
157
|
}
|