pi-subagents 0.45.2 → 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.
Files changed (70) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +2 -0
  3. package/docs/agents.md +342 -0
  4. package/docs/configuration.md +328 -0
  5. package/docs/extension-api.md +308 -0
  6. package/docs/missions.md +119 -0
  7. package/docs/models.md +192 -0
  8. package/docs/observability.md +174 -0
  9. package/docs/tool-reference.md +343 -0
  10. package/docs/watchdog.md +176 -0
  11. package/docs/workflows.md +163 -0
  12. package/package.json +4 -2
  13. package/skills/pi-subagents/references/execution-controls.md +6 -6
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
  15. package/src/agents/agents.ts +17 -8
  16. package/src/agents/frontmatter.ts +7 -3
  17. package/src/agents/skills.ts +2 -9
  18. package/src/api/project-panes.ts +30 -0
  19. package/src/extension/config.ts +18 -1
  20. package/src/extension/fanout-child.ts +5 -4
  21. package/src/extension/index.ts +66 -19
  22. package/src/extension/rpc.ts +3 -6
  23. package/src/extension/schemas.ts +28 -7
  24. package/src/extension/subagent-guide.ts +39 -0
  25. package/src/extension/tool-description.ts +30 -12
  26. package/src/inspectors/herdr/project-panes.ts +459 -63
  27. package/src/missions/actions.ts +25 -2
  28. package/src/missions/lifecycle.ts +21 -2
  29. package/src/missions/store.ts +79 -2
  30. package/src/missions/types.ts +33 -0
  31. package/src/missions/workflow-state.ts +19 -13
  32. package/src/runs/background/async-execution.ts +17 -6
  33. package/src/runs/background/async-job-tracker.ts +15 -0
  34. package/src/runs/background/async-resume.ts +19 -3
  35. package/src/runs/background/async-status.ts +6 -1
  36. package/src/runs/background/completion-replay.ts +267 -0
  37. package/src/runs/background/control-channel.ts +36 -0
  38. package/src/runs/background/result-watcher.ts +28 -6
  39. package/src/runs/background/scheduled-runs.ts +2 -1
  40. package/src/runs/background/stale-run-reconciler.ts +2 -21
  41. package/src/runs/background/subagent-runner.ts +47 -6
  42. package/src/runs/background/wait-completions.ts +39 -5
  43. package/src/runs/background/wait-subscriptions.ts +18 -3
  44. package/src/runs/foreground/async-steering-action.ts +1 -1
  45. package/src/runs/foreground/chain-execution.ts +3 -0
  46. package/src/runs/foreground/execution.ts +7 -0
  47. package/src/runs/foreground/foreground-history.ts +137 -0
  48. package/src/runs/foreground/subagent-executor.ts +403 -54
  49. package/src/runs/foreground/workflow-foreground-steering.ts +187 -0
  50. package/src/runs/shared/dynamic-fanout.ts +1 -1
  51. package/src/runs/shared/model-fallback.ts +8 -4
  52. package/src/runs/shared/model-scope.ts +12 -2
  53. package/src/runs/shared/parallel-utils.ts +1 -0
  54. package/src/runs/shared/worktree.ts +3 -2
  55. package/src/shared/artifacts.ts +14 -14
  56. package/src/shared/display-text.ts +100 -0
  57. package/src/shared/fork-context.ts +13 -0
  58. package/src/shared/formatters.ts +4 -6
  59. package/src/shared/prompt-resources.ts +51 -0
  60. package/src/shared/settings.ts +15 -2
  61. package/src/shared/types.ts +41 -2
  62. package/src/shared/utf8.ts +11 -0
  63. package/src/shared/utils.ts +43 -33
  64. package/src/slash/prompt-workflows.ts +2 -15
  65. package/src/slash/slash-commands.ts +22 -2
  66. package/src/tui/fleet-status.ts +22 -12
  67. package/src/tui/fleet.ts +135 -25
  68. package/src/tui/render.ts +150 -33
  69. package/src/watchdog/change-signature.ts +4 -3
  70. package/src/workflows/scripted-workflow.ts +167 -10
@@ -0,0 +1,163 @@
1
+ # Workflows and orchestration
2
+
3
+ How to compose subagents: the recommended pattern, packaged prompt shortcuts, scripted workflows, direct commands, worktree isolation, and child-to-parent coordination.
4
+
5
+ ## Recommended orchestration pattern
6
+
7
+ Use orchestration as parent-agent guidance, not as a runtime workflow mode. For implementation work, the recommended loop is:
8
+
9
+ ```text
10
+ clarify → scout → worker → fresh reviewers → worker
11
+ ```
12
+
13
+ Packaged `worker`, `oracle`, and `advisor` default to forked context when a launch omits `context`; pass `context: "fresh"` when you intentionally want a fresh child run.
14
+
15
+ Child-safety boundaries are enforced at runtime:
16
+
17
+ - Spawned child sessions do not receive the bundled `pi-subagents` skill.
18
+ - Forked child context filtering removes parent-only subagent artifacts (including old hidden orchestration-instruction messages, slash/status/control messages, and prior parent `subagent` tool-call/tool-result history) while preserving ordinary prose and unrelated tool calls/results.
19
+ - By default, children do not register the `subagent` tool and receive boundary instructions that they are not the parent orchestrator and must not propose or run subagents.
20
+ - The explicit exception is an agent whose resolved builtin `tools` includes `subagent`; that child gets a child-safe `subagent` tool for the fanout work the parent assigned, still bounded by `maxSubagentDepth`.
21
+
22
+ ## Prompt shortcuts
23
+
24
+ The package includes reusable prompt templates for common workflows. You do not need them, but they are handy when you want the same shape every time:
25
+
26
+ | Prompt | Use it for |
27
+ |--------|------------|
28
+ | `/parallel-review` | Launch fresh-context reviewers with distinct angles, then synthesize what to fix. |
29
+ | `/review-loop` | Run parent-controlled worker, reviewer, and fix-worker cycles until clean or capped. |
30
+ | `/parallel-research` | Combine `researcher` and `scout` for external evidence, local code context, and practical tradeoffs. |
31
+ | `/gather-context-and-clarify` | Scout/research first, then ask the user the clarification questions that matter. |
32
+ | `/parallel-cleanup` | Run review-only cleanup passes after implementation. |
33
+
34
+ Add `autofix` to `/parallel-review` or `/parallel-cleanup` to apply only the synthesized fixes worth doing now after reviewers return.
35
+
36
+ ## Scripted workflows (workflowScript)
37
+
38
+ All model-facing subagent execution is expressed through `workflowScript` in the `subagent` tool. Use stable keys and ordinary JavaScript for one child, sequence, and parallelism. Scripts are ordinary JavaScript statement bodies. Use an explicit `return` for a useful result:
39
+
40
+ ```js
41
+ subagent({ workflowScript: `
42
+ const scan = await runs.run("scan", { agent: "scout", task: "Scan the codebase" });
43
+ const reviews = await runs.all([
44
+ { key: "correctness", agent: "reviewer", task: "Review correctness: " + scan.output },
45
+ { key: "tests", agent: "reviewer", task: "Review tests: " + scan.output }
46
+ ]);
47
+ return reviews.map(result => result.output);
48
+ ` });
49
+ ```
50
+
51
+ For long task text with Markdown fences or shell blocks, use quoted lines instead of a raw template literal:
52
+
53
+ ````js
54
+ const task = [
55
+ "Run this command:",
56
+ "```bash",
57
+ "npm test",
58
+ "```"
59
+ ].join("\n");
60
+ return runs.run("test", { agent: "worker", task });
61
+ ````
62
+
63
+ A plain workflow creates one enclosing mission by default. Its children do not create separate missions. The result exposes the id as `details.missionId`, and human-readable output ends with `Mission: <id> (<status>)`. Pass `mission:false` for an ephemeral workflow with no mission or durable `state` global.
64
+
65
+ For watched same-repo workflows, pass `async:false` to show the live in-chat workflow card. `chatProgress` can force `off` or `live-card` when the automatic policy is not what you want. Foreground workflows default to a 30-minute timeout; async workflows have no default timeout. See the [tool reference](tool-reference.md) for the full parameter list.
66
+
67
+ The legacy `/chain`, `/parallel`, and `/run-chain` commands are not registered.
68
+
69
+ ## Direct commands
70
+
71
+ Use `/run <agent> [task] [--bg] [--fork]` for one child.
72
+
73
+ ## Worktree isolation
74
+
75
+ Scripted workflows can give each writing child a separate managed git worktree by setting `worktree: true` on each `runs.run` / `runs.all` item:
76
+
77
+ ```javascript
78
+ const [api, ui] = await runs.all([
79
+ { key: "api", agent: "worker", task: "Implement the API", worktree: true },
80
+ { key: "ui", agent: "worker", task: "Implement the UI", worktree: true }
81
+ ]);
82
+ return { api: api.artifactPaths, ui: ui.artifactPaths };
83
+ ```
84
+
85
+ Each child uses the existing worktree lifecycle: it branches from clean HEAD, journals ownership before launch, captures a patch and handoff manifest, then removes cleanly captured temporary worktrees and branches. The handoff manifest path remains available in the child's `artifactPaths`; return or emit it when the orchestrator needs to apply or inspect the patches. `runs.ref` stays concise and intentionally omits full paths.
86
+
87
+ A top-level `{ workflowScript, worktree: true }` makes isolation the default for every workflow child. An individual child can override that default with `worktree: false`. Keep one writer when parallel writes are not intentionally isolated.
88
+
89
+ Configure the worktree base directory and setup hook in [configuration.md](configuration.md).
90
+
91
+ ## Supervisor coordination (child asks parent)
92
+
93
+ Child agents can talk back to the parent Pi session without installing `pi-intercom`. `pi-subagents` provides the child-facing `contact_supervisor` tool and the parent-facing `subagent_supervisor({ action: "reply" })` path natively. If no external `pi-intercom` tool owns the `intercom` name, the native channel also exposes `intercom` as a compatibility fallback.
94
+
95
+ Use it for work where the child might need a decision instead of guessing:
96
+
97
+ ```text
98
+ Run this implementation in the background. If the worker gets blocked or needs a product decision, have it ask me through intercom.
99
+ ```
100
+
101
+ ```text
102
+ Ask oracle to review this plan. If it sees a decision I need to make, have it ask me instead of assuming.
103
+ ```
104
+
105
+ The child uses one dedicated coordination tool, `contact_supervisor`, with a `reason`:
106
+
107
+ - `need_decision` — blocking decisions or clarification
108
+ - `interview_request` — structured input
109
+ - `progress_update` — short non-blocking updates when a discovery changes the plan
110
+
111
+ Children should not ask for clarification when the only conflict is review-only/no-edit versus progress-writing or artifact-writing instructions; no-edit wins.
112
+
113
+ The parent replies with `subagent_supervisor({ action: "reply", replyTo, message })` or checks pending requests with `subagent_supervisor({ action: "pending" })`. Supervisor messages are scoped to the exact Pi session id that spawned the child. A second Pi session in the same repository does not receive those requests.
114
+
115
+ Child-side routine completion handoffs are not expected. If a child appears stalled, needs-attention notices show up in the parent session with useful next actions, such as checking `subagent({ action: "status" })`, interrupting the run, or nudging the child.
116
+
117
+ If messages do not show up, run `/subagents-doctor`. Advanced users can tune the bridge with `intercomBridge` in [configuration.md](configuration.md).
118
+
119
+ ## Recursion guard
120
+
121
+ Subagents can call `subagent` only when their resolved builtin tools explicitly include `subagent`. That is meant for delegated fanout agents, not ordinary worker/reviewer children. A depth guard prevents unbounded nesting.
122
+
123
+ By default, nesting is limited to two levels: main session → subagent → sub-subagent. Deeper calls are blocked with guidance to complete the current task directly. Nested runs appear in the parent status widget and `status` output as a tree, and `status`, `interrupt`, and `resume` can target a nested run by its id.
124
+
125
+ Configure the limit with:
126
+
127
+ 1. `PI_SUBAGENT_MAX_DEPTH` before starting Pi
128
+ 2. `config.maxSubagentDepth`
129
+ 3. `maxSubagentDepth` in agent frontmatter, which can only tighten the inherited limit
130
+
131
+ ```bash
132
+ export PI_SUBAGENT_MAX_DEPTH=3
133
+ export PI_SUBAGENT_MAX_DEPTH=1
134
+ export PI_SUBAGENT_MAX_DEPTH=0
135
+ ```
136
+
137
+ `PI_SUBAGENT_DEPTH` is internal and propagated automatically. Do not set it manually.
138
+
139
+ ## Prompt-template integration
140
+
141
+ `pi-subagents` includes a native prompt-workflow adapter for reusable subagent prompt templates, so you do not need `pi-prompt-template-model` for the common subagent workflow path.
142
+
143
+ Create a prompt in `.pi/prompts/` or `~/.pi/agent/prompts/`:
144
+
145
+ ```md
146
+ ---
147
+ description: Take a screenshot
148
+ model: claude-sonnet-4-20250514
149
+ subagent: browser-screenshoter
150
+ cwd: /tmp/screenshots
151
+ ---
152
+ Use url in the prompt to take screenshot: $@
153
+ ```
154
+
155
+ Then run it through the native adapter:
156
+
157
+ ```text
158
+ /prompt-workflow take-screenshot https://example.com
159
+ ```
160
+
161
+ The adapter delegates to the named subagent, applies `model`, `skill`, `cwd`, and fork/fresh context metadata, and supports runtime overrides such as `--subagent reviewer`, `--fork`, `--fresh`, and `--bg`.
162
+
163
+ Prompt templates with `chain:` frontmatter are translated into `workflowScript` and launched through `/prompt-workflow`; `/chain-prompts` is no longer registered.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.45.2",
3
+ "version": "0.47.0",
4
4
  "description": "Pi extension for single-agent delegation and scripted multi-agent workflows",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -15,7 +15,8 @@
15
15
  "./control-channel": "./src/api/control-channel.ts",
16
16
  "./intercom-bridge": "./src/api/intercom-bridge.ts",
17
17
  "./pi-args": "./src/api/pi-args.ts",
18
- "./shared-types": "./src/api/shared-types.ts"
18
+ "./shared-types": "./src/api/shared-types.ts",
19
+ "./project-panes": "./src/api/project-panes.ts"
19
20
  },
20
21
  "repository": {
21
22
  "type": "git",
@@ -44,6 +45,7 @@
44
45
  "agents/",
45
46
  "skills/**/*",
46
47
  "prompts/**/*",
48
+ "docs/**/*",
47
49
  "README.md",
48
50
  "CHANGELOG.md"
49
51
  ],
@@ -68,11 +68,11 @@ subagent({
68
68
  })
69
69
  ```
70
70
 
71
- Scripts run in a timed worker with only `runs.run`, `runs.all`, `runs.status`, `runs.ref/refs`, `emit`, captured `console`, and standard JavaScript. Mission-attached workflows also get `await state.get(key)` and `await state.set(key, value)` for durable JSON state shared across workflows on the same mission; `mission: false` workflows have no `state` global. Stable keys are required. Child launches follow ordinary single-agent execution controls. Give each child a distinct decision and output path when reports must outlive the workflow, then consume the aggregate workflow result before opening individual reports.
71
+ Scripts run in a timed worker with only `runs.run`, `runs.all`, `runs.status`, `runs.ref/refs`, `prompts.render`, `emit`, captured `console`, and standard JavaScript. `await prompts.render("package:<name>" | "user:<name>" | "project:<name>", vars?)` reads a named Markdown fragment through the host resolver, applies simple scalar `{{name}}` interpolation, and returns plain task text. It does not give the script filesystem access. Pass the rendered text explicitly as `task` to `runs.run`. Mission-attached workflows also get `await state.get(key)` and `await state.set(key, value)` for durable JSON state shared across workflows on the same mission; `mission: false` workflows have no `state` global. Stable keys are required. Child launches follow ordinary single-agent execution controls. Give each child a distinct decision and output path when reports must outlive the workflow, then consume the aggregate workflow result before opening individual reports.
72
72
 
73
73
  For one host-run verification command, pass `gate: "npm test"` on a `runs.run`/`runs.all` item (or at the top level as a workflow default). It is shorthand for verified acceptance with that single command: the runtime executes it on the host, records the result as evidence, and memoizes it per tracked workspace state and effective environment. `gate` cannot be combined with `acceptance`; use explicit `acceptance.verify` for multiple commands or custom criteria.
74
74
 
75
- Completed workflow children from this parent session stay addressable as retained children. `subagent({ action: "children.list" })` lists up to the last 10 with run ids, and a later workflow continues one with `runs.run(key, { resume: "<run-id>", task: "follow-up" })`. `resume` and `agent` are mutually exclusive, the revived child keeps its stored agent/model/tool contract, and `gate` is rejected on retained resume items.
75
+ Completed workflow children from this parent session stay addressable as retained children. `subagent({ action: "children.list" })` lists up to the last 10 with run ids, and a later workflow continues one with `runs.run(key, { resume: "<run-id>", task: "follow-up" })`. Inside `workflowScript`, awaiting that call waits for the revived child to finish and returns its completed output and new `runId`; top-level `{ action: "resume" }` remains detached. A follow-up loop can render each task with `await prompts.render(...)`. Assign each returned child result back to the loop variable because every resume can return a new retained `runId`; always resume the latest returned id. `resume` and `agent` are mutually exclusive, the revived child keeps its stored agent/model/tool contract, and `gate` is rejected on retained resume items.
76
76
 
77
77
  ### Async/background
78
78
 
@@ -158,7 +158,7 @@ A cooperating terminal runtime can register read-only external records through `
158
158
 
159
159
  ### Scheduled subagent runs
160
160
 
161
- Schedules are durable project records under `.pi-subagents/schedules/`. They are enabled by default; set `{ "scheduledRuns": { "enabled": false } }` in `~/.pi/agent/extensions/subagent/config.json` to disable them. Only schedule explicit work the user asked for.
161
+ Schedules are durable project records under `.pi/subagents/schedules/`. They are enabled by default; set `{ "scheduledRuns": { "enabled": false } }` in `~/.pi/agent/extensions/subagent/config.json` to disable them. Only schedule explicit work the user asked for.
162
162
 
163
163
  ```typescript
164
164
  // One-shot reviewer
@@ -235,7 +235,7 @@ The subagent watchdog is an **opt-in** adversarial change reviewer. It is not th
235
235
 
236
236
  When enabled, it reviews actual repo edits at safe `agent_end` boundaries only if
237
237
  the final worktree state changed during that turn. Unchanged or reverted diffs and
238
- generated `.pi-subagents/` / temp artifacts do not trigger review. Writing children
238
+ generated `.pi/subagents/` / temp artifacts do not trigger review. Writing children
239
239
  can review their own worktree; the parent can still review the aggregate diff after
240
240
  child changes land. Enabled watchdogs also run changed-file TypeScript/JavaScript
241
241
  LSP diagnostics before the model pass when `typescript-language-server` is available.
@@ -298,7 +298,7 @@ Routing rule:
298
298
  - Several projects with independent work: one async `workflowScript` whose child keys include repo slugs and whose child calls set explicit `cwd`; keep publication and merge decisions serial per repo.
299
299
  - Different project, substantial or long-running work: open a project-owned Herdr pane rooted there when a separate visible project session is useful, then give that project Pi session a narrow mission/result contract. Do not model it as ordinary child nesting, and do not expect existing headless runs to move into the pane.
300
300
 
301
- Project panes run a separate Pi session from the target directory. Subagents launched inside that pane use that project's config, agents, skills, files, git state, and mission records. The pane binding lives under `<projectRoot>/.pi-subagents/project-panes/herdr.json`. For ordinary headless delegation to another repo, prefer explicit `cwd` first; reserve project panes for visible or persistent project ownership.
301
+ Project panes run a separate Pi session from the target directory. Subagents launched inside that pane use that project's config, agents, skills, files, git state, and mission records. The pane binding lives under `<projectRoot>/.pi/subagents/project-panes/herdr.json`. For ordinary headless delegation to another repo, prefer explicit `cwd` first; reserve project panes for visible or persistent project ownership.
302
302
 
303
303
  ```typescript
304
304
  subagent({ action: "mission.create", mission: { title: "Ship auth refresh", objective: "Implement and validate refresh handling" } })
@@ -340,7 +340,7 @@ single-writer pattern instead.
340
340
 
341
341
  Git worktrees start from tracked files, so ignored or untracked build state
342
342
  such as `node_modules` may be absent. The clean-check ignores pi-subagents'
343
- own `.pi-subagents/` runtime state, including default mission records, but still
343
+ own `.pi/subagents/` runtime state, including default mission records, but still
344
344
  rejects ordinary source/config changes. `pi-subagents` attempts to symlink the
345
345
  root checkout's `node_modules` into each managed worktree when it exists, but
346
346
  agents should still treat dependency setup as an explicit bootstrap step before
@@ -28,7 +28,7 @@ subagent({ action: "refine.show", agent: "reviewer" })
28
28
  subagent({ action: "refine.rollback", agent: "reviewer" })
29
29
  ```
30
30
 
31
- `refine` builds a bounded project-local guidance overlay for one agent from recent run evidence, using a fresh read-only proposal child; validated guidance is stored under `.pi-subagents/refinements/<agent>.md` with revision snapshots and is injected into that agent's child system prompt for this project. `refine.show` prints the current overlay and history; `refine.rollback` restores the previous revision. Guidance that tries to override safety, policy, tool, output, acceptance, developer, or system instructions is rejected. `/subagents-refine <agent>` is the slash equivalent.
31
+ `refine` builds a bounded project-local guidance overlay for one agent from recent run evidence, using a fresh read-only proposal child; validated guidance is stored under `.pi/subagents/refinements/<agent>.md` with revision snapshots and is injected into that agent's child system prompt for this project. `refine.show` prints the current overlay and history; `refine.rollback` restores the previous revision. Guidance that tries to override safety, policy, tool, output, acceptance, developer, or system instructions is rejected. `/subagents-refine <agent>` is the slash equivalent.
32
32
 
33
33
  ### Create an agent
34
34
 
@@ -659,16 +659,25 @@ function findConfiguredProjectRoot(cwd: string): string | null {
659
659
  const nearestRoot = candidates[0];
660
660
  if (!nearestRoot) return null;
661
661
 
662
- const nearestMode = readProjectRootResolution(nearestRoot);
663
- if (nearestMode === "nearest") return nearestRoot;
664
-
665
- const gitRoot = findNearestGitRoot(cwd);
666
- const gitProjectRoot = gitRoot ? candidates.find((candidate) => path.resolve(candidate) === path.resolve(gitRoot)) : undefined;
667
- if (gitProjectRoot && (nearestMode === "git-root" || readProjectRootResolution(gitProjectRoot) === "git-root")) {
668
- return gitProjectRoot;
662
+ let policyRoot: string | undefined;
663
+ let policyRootIndex = -1;
664
+ for (const [index, candidate] of candidates.entries()) {
665
+ const mode = readProjectRootResolution(candidate);
666
+ if (mode === "nearest") return nearestRoot;
667
+ if (mode === "git-root") {
668
+ policyRoot = candidate;
669
+ policyRootIndex = index;
670
+ break;
671
+ }
669
672
  }
673
+ if (!policyRoot) return nearestRoot;
670
674
 
671
- return nearestRoot;
675
+ const gitRoot = findNearestGitRoot(cwd);
676
+ const gitProjectRoot = gitRoot
677
+ ? candidates.slice(policyRootIndex).find((candidate) => path.resolve(candidate) === path.resolve(gitRoot))
678
+ : undefined;
679
+ const configuredGitRoot = fs.existsSync(path.join(policyRoot, ".git")) ? policyRoot : undefined;
680
+ return gitProjectRoot ?? configuredGitRoot ?? nearestRoot;
672
681
  }
673
682
 
674
683
  function getUserAgentSettingsPath(): string {
@@ -83,12 +83,13 @@ export function parseFrontmatter(content: string): { frontmatter: Record<string,
83
83
  let currentBlockLines: string[] | null = null;
84
84
  let currentIndent: number | null = null;
85
85
  let currentFolded = false;
86
+ let currentLiteral = false;
86
87
 
87
88
  for (const line of lines) {
88
89
  const indent = line.search(/\S|$/); // position of first non-whitespace char
89
90
  const trimmed = line.trim();
90
91
 
91
- if (currentKey !== null && currentBlockLines !== null && (indent > (currentIndent ?? 0) || (currentFolded && trimmed === ""))) {
92
+ if (currentKey !== null && currentBlockLines !== null && (indent > (currentIndent ?? 0) || ((currentFolded || currentLiteral) && trimmed === ""))) {
92
93
  // This line is part of the current block value
93
94
  currentBlockLines.push(line);
94
95
  continue;
@@ -109,6 +110,7 @@ export function parseFrontmatter(content: string): { frontmatter: Record<string,
109
110
  currentBlockLines = null;
110
111
  currentIndent = null;
111
112
  currentFolded = false;
113
+ currentLiteral = false;
112
114
  }
113
115
 
114
116
  const match = line.match(/^([\w-]+):\s*(.*)$/);
@@ -119,13 +121,15 @@ export function parseFrontmatter(content: string): { frontmatter: Record<string,
119
121
  const isQuoted = (rawValue.startsWith('"') && rawValue.endsWith('"')) || (rawValue.startsWith("'") && rawValue.endsWith("'"));
120
122
  const value = isQuoted ? rawValue.slice(1, -1) : rawValue;
121
123
  const isFolded = !isQuoted && (rawValue === ">" || rawValue === ">-");
124
+ const isLiteral = !isQuoted && (rawValue === "|" || rawValue === "|-");
122
125
 
123
- if (value === "" || isFolded) {
124
- // Key with empty value or folded block indicator — defer storing until we see indent
126
+ if (value === "" || isFolded || isLiteral) {
127
+ // Key with empty value or block scalar indicator — defer storing until we see indent
125
128
  currentKey = key;
126
129
  currentBlockLines = [];
127
130
  currentIndent = indent;
128
131
  currentFolded = isFolded;
132
+ currentLiteral = isLiteral;
129
133
  } else {
130
134
  // Simple key: value
131
135
  frontmatter[key] = value;
@@ -6,6 +6,7 @@ import { execSync } from "node:child_process";
6
6
  import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
+ import { parseFrontmatter } from "./frontmatter.ts";
9
10
  import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
10
11
 
11
12
  export type SkillSource =
@@ -395,15 +396,7 @@ function chooseHigherPrioritySkill(existing: CachedSkillEntry | undefined, candi
395
396
  }
396
397
 
397
398
  function parseSkillDescription(content: string): string | undefined {
398
- const normalized = content.replace(/\r\n/g, "\n");
399
- if (!normalized.startsWith("---")) return undefined;
400
-
401
- const endIndex = normalized.indexOf("\n---", 3);
402
- if (endIndex === -1) return undefined;
403
-
404
- const frontmatter = normalized.slice(3, endIndex).trim();
405
- const match = frontmatter.match(/^description:\s*(.+)$/m);
406
- return match?.[1]?.trim().replace(/^['\"]|['\"]$/g, "");
399
+ return parseFrontmatter(content).frontmatter.description;
407
400
  }
408
401
 
409
402
  function maybeReadSkillDescription(filePath: string): string | undefined {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Public project-owned Herdr pane lifecycle API for other Pi extensions.
3
+ *
4
+ * This is the supported extension-to-extension surface. Callers must not
5
+ * import `src/inspectors/herdr/*` directly.
6
+ */
7
+ export {
8
+ PROJECT_PANES_API_VERSION,
9
+ PROJECT_PANE_TRUST_STATUS,
10
+ createProjectPaneManager,
11
+ openProjectPane,
12
+ getProjectPaneStatus,
13
+ closeProjectPane,
14
+ readProjectPaneBinding,
15
+ projectPaneBindingPath,
16
+ type ProjectPaneManager,
17
+ type ProjectPaneManagerOptions,
18
+ type ProjectPaneCommandClient,
19
+ type OpenProjectPaneOptions,
20
+ type GetProjectPaneStatusOptions,
21
+ type CloseProjectPaneOptions,
22
+ type OpenProjectPaneData,
23
+ type ProjectPaneStatusData,
24
+ type CloseProjectPaneData,
25
+ type ProjectPaneRuntime,
26
+ type ProjectPaneError,
27
+ type ProjectPaneErrorCode,
28
+ type ProjectPaneResult,
29
+ type HerdrProjectPaneBinding,
30
+ } from "../inspectors/herdr/project-panes.ts";
@@ -1,13 +1,14 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import type { ArtifactDirPreference, ExtensionConfig } from "../shared/types.ts";
4
+ import { FLEET_KEYBINDING_ACTIONS, type ArtifactDirPreference, type ExtensionConfig } from "../shared/types.ts";
5
5
  import { validateMissionStoreConfig } from "../missions/store.ts";
6
6
  import { validateAuthorityPolicy } from "../policy/authority.ts";
7
7
  import { getAgentDir } from "../shared/utils.ts";
8
8
  import { validatePermissionConfig } from "../runs/shared/permissions.ts";
9
9
 
10
10
  const ARTIFACT_DIR_PREFERENCES = new Set<ArtifactDirPreference>(["project", "session", "temp"]);
11
+ const FLEET_KEYBINDING_ACTION_SET = new Set<string>(FLEET_KEYBINDING_ACTIONS);
11
12
 
12
13
  export function resolveScheduledStoreRoot(value: string): string {
13
14
  const expanded = value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
@@ -24,14 +25,30 @@ function validateScheduledRunsConfig(value: unknown): void {
24
25
  resolveScheduledStoreRoot(storeRoot);
25
26
  }
26
27
 
28
+ function validateFleetKeybindingsConfig(value: unknown): void {
29
+ if (value === undefined) return;
30
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.fleetKeybindings must be a JSON object");
31
+ for (const [action, bindings] of Object.entries(value)) {
32
+ if (!FLEET_KEYBINDING_ACTION_SET.has(action)) throw new Error(`config.fleetKeybindings.${action} is not a supported Fleet action`);
33
+ if (!Array.isArray(bindings) || bindings.length === 0) throw new Error(`config.fleetKeybindings.${action} must be a non-empty array of strings`);
34
+ for (const binding of bindings) {
35
+ if (typeof binding !== "string" || !binding.trim()) throw new Error(`config.fleetKeybindings.${action} entries must be non-empty strings`);
36
+ }
37
+ }
38
+ }
39
+
27
40
  function validateConfig(config: Record<string, unknown>): void {
28
41
  if (config.artifactDir !== undefined && !ARTIFACT_DIR_PREFERENCES.has(config.artifactDir as ArtifactDirPreference)) {
29
42
  throw new Error(`config.artifactDir must be "project", "session", or "temp"`);
30
43
  }
44
+ if (config.legacyChainControls !== undefined && typeof config.legacyChainControls !== "boolean") {
45
+ throw new Error("config.legacyChainControls must be a boolean");
46
+ }
31
47
  validateMissionStoreConfig(config.missions);
32
48
  validateAuthorityPolicy(config.authorityPolicy);
33
49
  validatePermissionConfig(config.permissions);
34
50
  validateScheduledRunsConfig(config.scheduledRuns);
51
+ validateFleetKeybindingsConfig(config.fleetKeybindings);
35
52
  }
36
53
 
37
54
  export function getConfigPath(): string {
@@ -10,7 +10,7 @@ import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi
10
10
  import { readNestedControlRequests, resolveNestedRouteFromEnv, type NestedRoute, writeNestedControlResult } from "../runs/shared/nested-events.ts";
11
11
  import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom.ts";
12
12
  import { resolveSubagentIntercomTarget } from "../intercom/intercom-bridge.ts";
13
- import { SubagentParams } from "./schemas.ts";
13
+ import { createSubagentParamsSchema } from "./schemas.ts";
14
14
  import { loadConfig, resolveAsyncByDefault } from "./config.ts";
15
15
  import { type Details, type SubagentState } from "../shared/types.ts";
16
16
 
@@ -171,15 +171,16 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
171
171
  allowMutatingManagementActions: false,
172
172
  });
173
173
 
174
- const tool: ToolDefinition<typeof SubagentParams, Details> = {
174
+ const params = createSubagentParamsSchema(config);
175
+ const tool: ToolDefinition<typeof params, Details> = {
175
176
  name: "subagent",
176
177
  label: "Subagent",
177
178
  description: [
178
179
  "Delegate to subagents from child-safe fanout mode.",
179
- "Allowed management/control actions: list, get, status, interrupt, resume, steer, append-step, doctor.",
180
+ `Allowed management/control actions: list, get, status, interrupt, resume, steer${config.legacyChainControls === true ? ", append-step" : ""}, doctor.`,
180
181
  "Mutating management actions (create, update, delete, eject, disable, enable, reset, grant-spawn-budget) are blocked in this mode.",
181
182
  ].join("\n"),
182
- parameters: SubagentParams,
183
+ parameters: params,
183
184
  execute(id, params, signal, onUpdate, ctx) {
184
185
  return executor.executePublic(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx);
185
186
  },
@@ -27,7 +27,7 @@ import { cleanupOldChainDirs } from "../shared/settings.ts";
27
27
  import { clearLegacyResultAnimationTimer, renderSubagentResult, renderSubagentSummary } from "../tui/render.ts";
28
28
  import { openSubagentFleet } from "../tui/fleet.ts";
29
29
  import { SubagentFleetStatus, resolveFleetViewPlacement } from "../tui/fleet-status.ts";
30
- import { SubagentParams } from "./schemas.ts";
30
+ import { createSubagentParamsSchema } from "./schemas.ts";
31
31
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
32
32
  import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
33
33
  import { createResultWatcher } from "../runs/background/result-watcher.ts";
@@ -37,7 +37,7 @@ import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template
37
37
  import { registerMainWatchdog } from "../watchdog/register-main.ts";
38
38
  import { registerSlashSubagentBridge } from "../slash/slash-bridge.ts";
39
39
  import { createNativeSupervisorChannel } from "../intercom/native-supervisor-channel.ts";
40
- import { registerHerdrStatusBridge } from "../integrations/herdr-status.ts";
40
+ import { registerHerdrStatusBridge, type HerdrStatusRun } from "../integrations/herdr-status.ts";
41
41
  import { registerSubagentRpcBridge } from "./rpc.ts";
42
42
  import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
43
43
  import { inspectSubagentStatus } from "../runs/background/run-status.ts";
@@ -53,7 +53,7 @@ import { formatDuration, shortenPath } from "../shared/formatters.ts";
53
53
  import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
54
54
  import { buildSubagentToolDescription } from "./tool-description.ts";
55
55
  import { collectGoalContinuationNotices } from "../missions/goal-driver.ts";
56
- import { syncMissionFromAsyncCompletion } from "../missions/lifecycle.ts";
56
+ import { restoreForegroundRunHistory } from "../runs/foreground/foreground-history.ts";
57
57
  import { resolveMissionStoreLocation } from "../missions/store.ts";
58
58
  import { listRetainedChildren } from "../runs/background/retained-children.ts";
59
59
  import {
@@ -309,6 +309,36 @@ class SubagentControlNoticeComponent implements Component {
309
309
  }
310
310
  }
311
311
 
312
+ export function projectActiveHerdrRuns(state: SubagentState): HerdrStatusRun[] {
313
+ const active = (status: string) => status === "queued" || status === "running";
314
+ const foregroundChildrenByWorkflow = new Map<string, Array<{ agent: string; needsAttention: boolean }>>();
315
+ for (const control of state.foregroundControls.values()) {
316
+ if (!control.parentWorkflowRunId) continue;
317
+ const children = control.activeChildren?.size
318
+ ? [...control.activeChildren.values()].map((child) => ({
319
+ agent: child.agent,
320
+ needsAttention: child.currentActivityState === "needs_attention",
321
+ }))
322
+ : control.currentAgent
323
+ ? [{ agent: control.currentAgent, needsAttention: control.currentActivityState === "needs_attention" }]
324
+ : [];
325
+ if (children.length === 0) continue;
326
+ const existing = foregroundChildrenByWorkflow.get(control.parentWorkflowRunId) ?? [];
327
+ existing.push(...children);
328
+ foregroundChildrenByWorkflow.set(control.parentWorkflowRunId, existing);
329
+ }
330
+ return [...state.asyncJobs.values()]
331
+ .filter((job) => active(job.status))
332
+ .map((job) => {
333
+ const children = job.mode === "workflow" ? foregroundChildrenByWorkflow.get(job.asyncId) : undefined;
334
+ return {
335
+ id: job.asyncId,
336
+ agents: children?.length ? children.map((child) => child.agent) : job.agents,
337
+ needsAttention: job.activityState === "needs_attention" || children?.some((child) => child.needsAttention),
338
+ };
339
+ });
340
+ }
341
+
312
342
  export default function registerSubagentExtension(pi: ExtensionAPI): void {
313
343
  if (process.env[SUBAGENT_CHILD_ENV] === "1") {
314
344
  return;
@@ -362,6 +392,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
362
392
  lastUiContext: null,
363
393
  poller: null,
364
394
  completionSeen: new Map(),
395
+ widgetsSuspended: false,
365
396
  watcher: null,
366
397
  watcherRestartTimer: null,
367
398
  resultFileCoalescer: {
@@ -378,7 +409,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
378
409
  ? new SubagentFleetStatus(state, async (itemKey) => {
379
410
  const ctx = state.lastUiContext;
380
411
  if (!ctx?.hasUI) return;
381
- await openSubagentFleet(ctx, state, { initialKey: itemKey, asyncDirRoot: DIRS.async, resultsDir: DIRS.results });
412
+ await openSubagentFleet(ctx, state, { initialKey: itemKey, asyncDirRoot: DIRS.async, resultsDir: DIRS.results, fleetKeybindings: config.fleetKeybindings });
382
413
  }, { placement: fleetViewPlacement })
383
414
  : undefined;
384
415
  let executorScheduled: ((id: string, params: SubagentParamsLike, signal: AbortSignal, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
@@ -536,11 +567,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
536
567
  });
537
568
 
538
569
 
539
- const tool: ToolDefinition<typeof SubagentParams, Details> = {
570
+ const parameters = createSubagentParamsSchema(config);
571
+ const tool: ToolDefinition<typeof parameters, Details> = {
540
572
  name: "subagent",
541
573
  label: "Subagent",
542
574
  description: buildSubagentToolDescription(config),
543
- parameters: SubagentParams,
575
+ parameters,
544
576
 
545
577
  execute(id, params, signal, onUpdate, ctx) {
546
578
  return executeSubagentCollapsed(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx);
@@ -603,7 +635,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
603
635
  }
604
636
  });
605
637
 
606
- registerSlashCommands(pi, state);
638
+ registerSlashCommands(pi, state, { fleetKeybindings: config.fleetKeybindings });
607
639
 
608
640
  const eventUnsubscribeStoreKey = "__piSubagentEventUnsubscribes";
609
641
  const controlNoticeSeenStoreKey = "__piSubagentVisibleControlNotices";
@@ -621,13 +653,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
621
653
  const existingVisibleControlNotices = globalStore[controlNoticeSeenStoreKey];
622
654
  const visibleControlNotices = existingVisibleControlNotices instanceof Set ? existingVisibleControlNotices as Set<string> : new Set<string>();
623
655
  globalStore[controlNoticeSeenStoreKey] = visibleControlNotices;
624
- const activeHerdrRuns = () => [...state.asyncJobs.values()]
625
- .filter((job) => job.status === "queued" || job.status === "running")
626
- .map((job) => ({
627
- id: job.asyncId,
628
- agents: job.agents,
629
- needsAttention: job.activityState === "needs_attention",
630
- }));
656
+ const activeHerdrRuns = () => projectActiveHerdrRuns(state);
631
657
  const herdrStatusBridge = registerHerdrStatusBridge({
632
658
  events: pi.events,
633
659
  getRuns: activeHerdrRuns,
@@ -653,11 +679,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
653
679
  const asyncCompleteHandler = (payload: unknown) => {
654
680
  handleComplete(payload);
655
681
  scheduledRunManager.handleAsyncCompletion(payload);
656
- try {
657
- syncMissionFromAsyncCompletion(payload);
658
- } catch (error) {
659
- console.error("Failed to update mission from async completion:", error);
660
- }
661
682
  fleetStatus?.refresh();
662
683
  };
663
684
  const eventUnsubscribes = [
@@ -694,7 +715,22 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
694
715
  }
695
716
  };
696
717
 
718
+ const suspendWidgetsForCompaction = () => {
719
+ if (state.widgetsSuspended) return;
720
+ state.widgetsSuspended = true;
721
+ if (state.lastUiContext?.hasUI) state.lastUiContext.ui.setWidget(WIDGET_KEY, undefined);
722
+ fleetStatus?.refresh();
723
+ };
724
+ const resumeWidgetsAfterCompaction = () => {
725
+ if (!state.widgetsSuspended) return;
726
+ state.widgetsSuspended = false;
727
+ const ctx = state.lastUiContext;
728
+ if (ctx?.hasUI) refreshWidget(ctx);
729
+ fleetStatus?.refresh();
730
+ };
731
+
697
732
  const resetSessionState = (ctx: ExtensionContext, recovering: boolean) => {
733
+ state.widgetsSuspended = false;
698
734
  state.baseCwd = ctx.cwd;
699
735
  goalTurnId = 0;
700
736
  state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
@@ -722,6 +758,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
722
758
  state.foregroundControls.clear();
723
759
  state.lastForegroundControlId = null;
724
760
  resetJobs(ctx);
761
+ restoreForegroundRunHistory(state, { resultsDir: DIRS.results });
725
762
  restoreActiveJobs(ctx);
726
763
  scheduledRunManager.bindSession(ctx);
727
764
  restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
@@ -732,9 +769,18 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
732
769
  };
733
770
 
734
771
  pi.on("agent_start", () => {
772
+ resumeWidgetsAfterCompaction();
735
773
  herdrStatusBridge.agentStarted();
736
774
  });
737
775
 
776
+ pi.on("agent_settled", () => {
777
+ resumeWidgetsAfterCompaction();
778
+ });
779
+
780
+ pi.on("session_before_compact", (event) => {
781
+ if (event.reason !== "manual") suspendWidgetsForCompaction();
782
+ });
783
+
738
784
  pi.on("session_compact", () => {
739
785
  const hasActiveAsyncWork = [...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running");
740
786
  if (!hasActiveAsyncWork || state.lastUiContext?.hasUI !== true) return;
@@ -760,6 +806,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
760
806
  });
761
807
 
762
808
  pi.on("session_shutdown", async () => {
809
+ state.widgetsSuspended = false;
763
810
  stopResultWatcher();
764
811
  state.currentSessionId = null;
765
812
  state.parentSessionFile = null;