pi-squire 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,51 @@ follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.0] - 2026-10-21
10
+
11
+ ### Breaking
12
+
13
+ - Agent definitions now live under `<agent dir>/squire/agents/<name>/AGENT.md`,
14
+ and only there. The old `<agent dir>/agents/<name>/AGENT.md` location is no
15
+ longer read, with no fallback or dual discovery, so an agent left there is
16
+ neither listed nor launched.
17
+ - Pi 1.3.0 or later is now required. The raised floor supports structured
18
+ per-run system-prompt sections and `before_agent_start` preparation for runs
19
+ started by an idle custom-message result, so parent guidance and roster
20
+ metadata are current before each new run.
21
+
22
+ ### Added
23
+
24
+ - Optional parent delegation guidance: Markdown in
25
+ `<agent dir>/squire/delegation_prompt.md` is inserted unchanged into the
26
+ `squire_parent_delegation` system-prompt section before each new run, only
27
+ while the `delegate` tool is active and not hidden. Missing, empty or deleted
28
+ files add no guidance; a read failure warns with the path, removes stale
29
+ guidance for that run, and leaves delegation working. The package ships no
30
+ default delegation or approval policy.
31
+ - The `delegate` tool description now refreshes from the current roster before
32
+ each new run, including a run woken by an idle delegate result. A changed
33
+ agent description, addition or removal shows up in the run being prepared;
34
+ unchanged metadata is not re-registered.
35
+
36
+ ### Changed
37
+
38
+ - The `delegate` tool now says to keep working or end the turn rather than poll
39
+ or send a blocking mail request merely to wait for a result, while a
40
+ substantive follow-up remains supported.
41
+
42
+ ## [0.8.1] - 2026-10-20
43
+
44
+ ### Fixed
45
+
46
+ - A 429 whose text says the endpoint is unavailable, such as OpenCode Go's
47
+ `429: Endpoint is unavailable`, no longer marks the provider as
48
+ usage-limited. It is an outage, not a used-up quota, so `/delegate` keeps
49
+ launching on the provider instead of refusing it for five minutes or more.
50
+ Pi has already retried the request by then. A running delegate that hits
51
+ it moves to its next fallback model and carries on, as it does on a usage
52
+ limit.
53
+
9
54
  ## [0.8.0] - 2026-10-20
10
55
 
11
56
  ### Added
@@ -273,7 +318,9 @@ author's dotfiles; the changes below are against that copy.
273
318
 
274
319
  - The published file list includes `child.ts`, which `index.ts` imports.
275
320
 
276
- [Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.8.0...HEAD
321
+ [Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.9.0...HEAD
322
+ [0.9.0]: https://github.com/gvanderclay/pi-squire/compare/v0.8.1...v0.9.0
323
+ [0.8.1]: https://github.com/gvanderclay/pi-squire/compare/v0.8.0...v0.8.1
277
324
  [0.8.0]: https://github.com/gvanderclay/pi-squire/compare/v0.7.0...v0.8.0
278
325
  [0.7.0]: https://github.com/gvanderclay/pi-squire/compare/v0.6.0...v0.7.0
279
326
  [0.6.0]: https://github.com/gvanderclay/pi-squire/compare/v0.5.1...v0.6.0
package/README.md CHANGED
@@ -10,13 +10,12 @@ answers with one message, which the parent session shows as a result.
10
10
 
11
11
  You need:
12
12
 
13
- - Pi 0.80.5 or later, on macOS or Linux (not Windows). Pi 1.0.0 is the version
14
- pi-squire is tested with. The floor is the first installable release with
15
- the `agent_settled` event (added in 0.80.4, which was never published to
16
- npm); `--exclude-tools` needs 0.77.0 and `--session-id`
17
- 0.76.0. The extension uses the `pi.events` bus, `pi.registerCommand` and
18
- `pi.registerTool`; Pi supplies the peer dependencies
19
- `@earendil-works/pi-coding-agent` and `typebox`.
13
+ - Pi 1.3.0 or later, on macOS or Linux (not Windows). Pi 1.3.0 is the version
14
+ pi-squire is tested with. It provides the per-run `before_agent_start`
15
+ preparation for idle custom-message results, the structured system-prompt
16
+ options the parent guidance uses, and the extension APIs the package relies
17
+ on. Pi supplies the peer dependencies `@earendil-works/pi-coding-agent` and
18
+ `typebox`.
20
19
  - tmux 3.0 or later, with the Pi session running in a tmux pane. That is the
21
20
  first version with `new-window -e`. A window is opened in the pane's
22
21
  session, so a client attached elsewhere does not matter.
@@ -45,8 +44,8 @@ The package has no runtime dependencies and no build step.
45
44
 
46
45
  The package ships no agents, so create one first. `<agent dir>` is Pi's agent
47
46
  directory: `~/.pi/agent`, or `PI_CODING_AGENT_DIR` when that is set. Save this
48
- as `<agent dir>/agents/scout/AGENT.md`, replacing `provider/model-id` with a
49
- model your Pi can use:
47
+ as `<agent dir>/squire/agents/scout/AGENT.md`, replacing
48
+ `provider/model-id` with a model your Pi can use:
50
49
 
51
50
  ```markdown
52
51
  ---
@@ -60,8 +59,10 @@ You research one question at a time and answer concisely, citing the files
60
59
  you read.
61
60
  ```
62
61
 
63
- The roster is read at call time, so no reload is needed. Then, from a Pi
64
- session running in tmux, hand it a task:
62
+ The roster is read at call time, so no reload is needed. Delegation guidance
63
+ for the parent is optional: save Markdown as
64
+ `<agent dir>/squire/delegation_prompt.md`, and it is read before each run.
65
+ Then, from a Pi session running in tmux, hand it a task:
65
66
 
66
67
  ```text
67
68
  /delegate scout --label first Which files define the command-line entry point?
@@ -75,10 +76,11 @@ closes its own window ([auto-exit](#auto-exit)).
75
76
  ## How a delegation works
76
77
 
77
78
  `/delegate <agent> [--model <provider/id>] [--thinking <level>] [--label <name>] [--auto-exit | --no-auto-exit] <task…>`
78
- reads the agent from `<agent dir>/agents/<name>/AGENT.md`, writes the task to
79
- the delegate's inbox through `message:send`, emits `session:launch` so other
80
- extensions can shape the launch, then opens a detached window running the
81
- parent's own Pi in the parent's working directory and agent root. The agent's
79
+ reads the agent from `<agent dir>/squire/agents/<name>/AGENT.md`, writes
80
+ the task to the delegate's inbox through `message:send`, emits
81
+ `session:launch` so other extensions can shape the launch, then opens a
82
+ detached window running the parent's own Pi in the parent's working directory
83
+ and agent root. The agent's
82
84
  prompt reaches the child as a path to a private file under the system
83
85
  temporary folder, not as text on the command line.
84
86
 
@@ -112,8 +114,9 @@ temporary folder, not as text on the command line.
112
114
 
113
115
  ## Agents
114
116
 
115
- The roster is read at call time from `<agent dir>/agents/<name>/AGENT.md`, one
116
- directory per agent, so a new agent is usable without a reload. Frontmatter
117
+ The roster is read at call time from
118
+ `<agent dir>/squire/agents/<name>/AGENT.md`, one directory per agent, so a new
119
+ agent is usable without a reload. Frontmatter
117
120
  carries `description`, `model` (`provider/id`) and `thinking`, and optionally
118
121
  `auto-exit` (`true` or `false`, default `true`; see [Auto-exit](#auto-exit)),
119
122
  `exclude-tools`, `tools`, `extensions` and `fallback`. The body is the delegate's system prompt,
@@ -173,8 +176,23 @@ add one.
173
176
 
174
177
  ## Configuration
175
178
 
176
- None beyond the roster. `pi-squire` reads no settings file and no environment
177
- variable of its own. `PI_DELEGATE_PARENT` and `PI_DELEGATE_AUTO_EXIT` are set
179
+ `<agent dir>/squire/` holds the roster and the optional parent guidance file.
180
+ `delegation_prompt.md` is Markdown, inserted unchanged as the
181
+ `squire_parent_delegation` system-prompt section before each new run, including
182
+ a run started by an idle delegate result. It is not a conversation or user
183
+ message, is not duplicated in the `delegate` tool description or guidelines,
184
+ and is not sent to the child. The guidance is added only while the `delegate`
185
+ tool is active and not hidden; when delegation is inactive or hidden, the
186
+ section is removed and tool selection is left as it was. A missing or whitespace-only
187
+ file adds no section. Editing the file replaces the section at the next run;
188
+ deleting it removes the section. A read failure other than a missing file
189
+ warns with the path, removes stale guidance for that run, and leaves otherwise
190
+ valid delegation working. The package ships no default delegation or approval
191
+ policy.
192
+
193
+ Refresh happens before a new run, not before every model request in a tool
194
+ loop, and the extension does not watch the filesystem. `pi-squire` reads no
195
+ settings file and no environment variable of its own. `PI_DELEGATE_PARENT` and `PI_DELEGATE_AUTO_EXIT` are set
178
196
  for the child, never read as configuration by the parent. The window starts
179
197
  with the tmux server's environment plus only these from the parent:
180
198
  `PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR` (when the parent has it
@@ -213,13 +231,18 @@ and its excluded tools.
213
231
 
214
232
  The model picks an agent from these lines without reading files. The
215
233
  description also tells the model to turn `auto_exit` off when it means to keep
216
- talking to the delegate by mail after its result. A tool's description is
217
- fixed when it is registered, so the description is rebuilt at every session
218
- start, and a roster change shows up in the next session. Pi cannot list the
219
- session's tools while extensions load, so the description built then skips the
220
- `exclude-tools` check, and the one built at session start applies it. The
221
- roster itself is read at call time, and an unknown agent, model or thinking
222
- level is refused with close matches and nothing starts.
234
+ talking to the delegate by mail after its result. It says a task must stand
235
+ alone, that results arrive automatically, and that the parent should keep
236
+ working or end its turn rather than poll or send a blocking mail request
237
+ merely to wait; a substantive follow-up remains fine. The description is
238
+ rebuilt at session start and before each new run when the exposed roster
239
+ changes, so an edit, addition or removal shows up in the run being prepared,
240
+ including a run woken by an idle delegate result. Unchanged metadata is not
241
+ re-registered. Pi cannot list the session's tools while extensions load, so
242
+ the description built then skips the `exclude-tools` check, and the one built
243
+ after startup applies it. The roster itself is still read at call time, and an
244
+ unknown agent, model or thinking level is refused with close matches and
245
+ nothing starts.
223
246
 
224
247
  `delegation_status` reports one state per delegation:
225
248
 
@@ -485,7 +508,7 @@ has been sent and just before it opens the delegate's window.
485
508
  | --- | --- |
486
509
  | `args` | the child `pi`'s complete argument list, launcher first |
487
510
  | `env` | the child's environment, as `KEY`/`value` pairs |
488
- | `agent` | the agent's name, as in `<agent dir>/agents/<name>/AGENT.md`; read it, do not change it |
511
+ | `agent` | the agent's name, as in `<agent dir>/squire/agents/<name>/AGENT.md`; read it, do not change it |
489
512
 
490
513
  Listeners may only append to `args` and add keys to `env`; there is no veto,
491
514
  and `pi-squire` passes both to tmux unchanged. Do all work synchronously: the
@@ -520,10 +543,6 @@ to the provider.
520
543
  Inside a delegate, `pi-squire` also takes over the task: a `request` whose
521
544
  `from` is the parent's address. It sets `handled` and sends the task as a user
522
545
  prompt labelled `[delegate]`, so the run starts the way a typed prompt does.
523
- Pi 1.0.0 starts a turn for an injected custom message without preparing the
524
- system prompt, so a fresh delegate's first request would otherwise carry no
525
- context files, skills or role paragraph
526
- ([earendil-works/pi#5581](https://github.com/earendil-works/pi/issues/5581)).
527
546
  The provider still arms the automatic answer for a request a listener took
528
547
  over. Every other message to the delegate is left to the provider. `pi-squire` reads `from` and requires
529
548
  it to be the sender's address, which for a delegate is its session id; a reply
@@ -549,6 +568,16 @@ written just before a window closed is never mistaken for no reply. **If
549
568
  because a waiting reply cannot be ruled out. The provider's README carries the
550
569
  contract.
551
570
 
571
+ ## Testing limits
572
+
573
+ The automated tests drive the extension through a fake Pi session. They check
574
+ the registered tool definitions, the structured prompt options a simulated run
575
+ preparation receives, file handling, visibility gating and the existing launch
576
+ and result paths. They do not prove actual Pi event scheduling, final provider
577
+ requests, compaction, cache behavior, or model obedience. The idle-result wake
578
+ is simulated by dispatching the next preparation; the suite does not assert
579
+ that the runtime emits it.
580
+
552
581
  ## Compatibility
553
582
 
554
583
  While pi-squire is below 1.0, breaking any contract below bumps the minor
@@ -565,7 +594,9 @@ Other extensions may rely on:
565
594
  - the `/delegate` syntax and flags;
566
595
  - the `[delegate]` result text and the closed-without-a-result text;
567
596
  - `AGENT.md`'s frontmatter keys and its location,
568
- `<agent dir>/agents/<name>/AGENT.md`;
597
+ `<agent dir>/squire/agents/<name>/AGENT.md`;
598
+ - the optional `<agent dir>/squire/delegation_prompt.md` file and the
599
+ `squire_parent_delegation` system-prompt section;
569
600
  - the child environment variables `PI_DELEGATE_PARENT`,
570
601
  `PI_DELEGATE_AUTO_EXIT` and `PI_UNATTENDED=1`.
571
602
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-squire",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Starts a delegate Pi session in a background tmux window, sends it the task over message:*, and shows the parent its result",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -44,14 +44,14 @@
44
44
  "access": "public"
45
45
  },
46
46
  "peerDependencies": {
47
- "@earendil-works/pi-coding-agent": "*",
47
+ "@earendil-works/pi-coding-agent": ">=1.3.0",
48
48
  "@earendil-works/pi-tui": "*",
49
49
  "typebox": "*"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@biomejs/biome": "2.5.14",
53
- "@earendil-works/pi-coding-agent": "1.0.0",
54
- "@earendil-works/pi-tui": "1.0.0",
53
+ "@earendil-works/pi-coding-agent": "1.3.0",
54
+ "@earendil-works/pi-tui": "1.3.0",
55
55
  "@types/node": "26.6.3",
56
56
  "typebox": "1.3.27",
57
57
  "typescript": "7.0.2"
package/src/agents.ts CHANGED
@@ -1,4 +1,4 @@
1
- // The parent's agent roster: `<agent dir>/agents/<name>/AGENT.md`, read at
1
+ // The parent's agent roster: `<agent dir>/squire/agents/<name>/AGENT.md`, read at
2
2
  // call time so a new agent is usable without a reload. Frontmatter carries
3
3
  // `description`, `model` and `thinking`, and optionally `auto-exit` (default
4
4
  // true) and `exclude-tools` (tools the delegate goes without, passed to its Pi
@@ -179,14 +179,14 @@ function flag(value: unknown): boolean | undefined | null {
179
179
  }
180
180
 
181
181
  /**
182
- * Read every agent under `<agentDir>/agents/`, in name order. A directory
182
+ * Read every agent under `<agentDir>/squire/agents/`, in name order. A directory
183
183
  * with no `AGENT.md` is not an agent and is passed over quietly; an
184
184
  * `AGENT.md` that cannot be used is warned about and left out. `tools` are
185
185
  * the session's tool names, which `exclude-tools` is checked against;
186
186
  * undefined skips that check, as before the session's tools are known.
187
187
  */
188
188
  export function readRoster(agentDir: string, tools?: readonly string[]): Roster {
189
- const dir = join(agentDir, "agents");
189
+ const dir = join(agentDir, "squire", "agents");
190
190
  let names: string[];
191
191
  try {
192
192
  names = readdirSync(dir).sort();
package/src/child.ts CHANGED
@@ -13,11 +13,11 @@
13
13
  // edxeth/pi-subagents' `auto-exit` (README "Child lifecycle", `cf6dbf4`).
14
14
  //
15
15
  // It also takes over the parent's task request on `message:inbound` and sends
16
- // it as a user prompt. Pi 1.0.0 starts a custom-message turn without preparing
17
- // the system prompt, so a fresh delegate's first request would carry no
18
- // AGENTS.md, skills or role addendum (earendil-works/pi#5581). The provider
19
- // still arms the automatic answer for a request a listener took over.
20
- // ponytail: drop the takeover once #5581 lands and the minimum Pi has it.
16
+ // it as a user prompt, so the task starts through the same path as a typed
17
+ // prompt, labelled `[delegate]`. Pi 1.2.0 already prepares the system prompt
18
+ // for custom-message runs (earendil-works/pi#5581); this takeover remains the
19
+ // package's pinned startup delivery path. The provider still arms the
20
+ // automatic answer for a request a listener took over.
21
21
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
22
22
 
23
23
  import { activeMark, classify, type Response, recordLimit } from "./limits.ts";
@@ -84,27 +84,39 @@ function registerTakeover(pi: ExtensionAPI, parent: string): void {
84
84
  }
85
85
 
86
86
  /**
87
- * A usage limit moves the run to the next fallback that is usable and not
88
- * marked, and continues it in this session; each fallback is tried once.
89
- * Returns the setter for the model whose usage limit ended the last run.
87
+ * A usage limit or an unavailable endpoint moves the run to the next fallback
88
+ * that is usable and not marked, and continues it in this session; each
89
+ * fallback is tried once. Returns the setter for what ended the last run, as
90
+ * `<provider/model> <what happened>`.
90
91
  */
91
- function registerFallback(pi: ExtensionAPI): (model: string | undefined) => void {
92
+ function registerFallback(pi: ExtensionAPI): (failure: string | undefined) => void {
92
93
  /** The models still to move to on a usage limit, in order. */
93
94
  const fallback = (process.env[FALLBACK_ENV] ?? "").split(",").filter((model) => model.includes("/"));
94
- let limited: string | undefined;
95
+ let failure: string | undefined;
95
96
  pi.on("agent_before_settle", async (_event, ctx) => {
96
- const from = limited;
97
- limited = undefined;
97
+ const from = failure;
98
+ failure = undefined;
98
99
  const next = from === undefined ? undefined : await switchModel(pi, ctx, fallback);
99
100
  if (next === undefined) return;
100
- const content = `[delegate] ${from} hit a usage limit, so this session switched to ${next}. Carry on with the task from where it stopped.`;
101
+ const content = `[delegate] ${from}, so this session switched to ${next}. Carry on with the task from where it stopped.`;
101
102
  return { continue: true, entries: [{ type: "custom_message", customType: "delegate", content, display: true }] };
102
103
  });
103
- return (model) => {
104
- limited = model;
104
+ return (value) => {
105
+ failure = value;
105
106
  };
106
107
  }
107
108
 
109
+ /** Why a failed run should move to a fallback, as `<provider/model> <what happened>`; undefined when it should not. */
110
+ function fallbackReason(last: Message | undefined): string | undefined {
111
+ const error = last?.errorMessage;
112
+ if (typeof error !== "string") return undefined;
113
+ const model = `${String(last?.provider)}/${String(last?.model)}`;
114
+ if (classify(error) !== undefined) return `${model} hit a usage limit`;
115
+ // Pi has already retried an unavailable endpoint; it marks nothing, but this run moves on.
116
+ if (/unavailable/i.test(error)) return `${model} was unavailable (${error})`;
117
+ return undefined;
118
+ }
119
+
108
120
  /** This delegate's footer icon, picked once per load. */
109
121
  const ICON = Math.random() < 0.5 ? "👦" : "👧";
110
122
 
@@ -120,7 +132,7 @@ function showStatus(ctx: ExtensionContext, armed: boolean): void {
120
132
 
121
133
  export function registerChild(pi: ExtensionAPI, parent: string): void {
122
134
  registerTakeover(pi, parent);
123
- const setLimited = registerFallback(pi);
135
+ const setFailure = registerFallback(pi);
124
136
 
125
137
  let armed = process.env[AUTO_EXIT_ENV] === "1";
126
138
  /** Whether the run that just ended finished without the user stopping it. */
@@ -162,11 +174,7 @@ export function registerChild(pi: ExtensionAPI, parent: string): void {
162
174
  const stopped = last?.stopReason === "aborted" || ctx.signal?.aborted === true;
163
175
  if (stopped) disarm(ctx);
164
176
  if (!stopped && last?.stopReason === "error") recordUsageLimit(last, ctx, response);
165
- setLimited(
166
- !stopped && typeof last?.errorMessage === "string" && classify(last.errorMessage) !== undefined
167
- ? `${String(last.provider)}/${String(last.model)}`
168
- : undefined,
169
- );
177
+ setFailure(stopped ? undefined : fallbackReason(last));
170
178
  // A failure ends the work like a completion: the parent gets the error, the session file stays.
171
179
  completed = !stopped;
172
180
  });
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // `/delegate <agent> [--model <provider/id>] [--thinking <level>]
5
5
  // [--label <name>] [--auto-exit | --no-auto-exit] <task…>`
6
- // reads the agent from `<agent dir>/agents/<name>/AGENT.md`, writes the task
6
+ // reads the agent from `<agent dir>/squire/agents/<name>/AGENT.md`, writes the task
7
7
  // to the delegate's inbox through `message:send`, emits `session:launch` so
8
8
  // listeners can add arguments and environment, then opens a window in the
9
9
  // parent's working directory running the parent's own Pi. The window and the
@@ -34,7 +34,7 @@
34
34
  // The `session:launch` contract this package provides, and the `message:*`
35
35
  // hooks it consumes, live in this package's README.
36
36
  import { randomUUID } from "node:crypto";
37
- import { mkdtempSync, writeFileSync } from "node:fs";
37
+ import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
38
38
  import { tmpdir } from "node:os";
39
39
  import { basename, join } from "node:path";
40
40
 
@@ -54,6 +54,10 @@ import { createTmuxClient, type TmuxClient } from "./tmux.ts";
54
54
  import { createTracking, toolResult } from "./tracking.ts";
55
55
  import { createProactive, createUsageClient, type Proactive, type UsageClient } from "./usage.ts";
56
56
 
57
+ /** The optional parent guidance file under `<agent dir>/squire/`. */
58
+ const PARENT_PROMPT = "delegation_prompt.md";
59
+ /** The parent system-prompt section this extension owns. */
60
+ const PARENT_SECTION = "squire_parent_delegation";
57
61
  /** The hook a provider of `message:*` answers with the written request. */
58
62
  const SEND = "message:send";
59
63
  /** The hook emitted just before a delegate's window opens. */
@@ -325,13 +329,33 @@ function thinkingProblem(value: string): string | undefined {
325
329
  }`;
326
330
  }
327
331
 
332
+ /** Read `<agent dir>/squire/delegation_prompt.md`; missing or whitespace-only means no guidance. */
333
+ function readParentPrompt(): { text?: string; error?: string } {
334
+ const path = join(getAgentDir(), "squire", PARENT_PROMPT);
335
+ try {
336
+ const text = readFileSync(path, "utf8");
337
+ return text.trim() === "" ? {} : { text };
338
+ } catch (err) {
339
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return {};
340
+ return {
341
+ error: `could not read ${path}: ${(err as Error).message}; parent delegation guidance is off for this run`,
342
+ };
343
+ }
344
+ }
345
+
346
+ /** Whether this run exposes `delegate` after tool selection and hidden declarations. */
347
+ function delegateVisible(options: { selectedTools: string[]; hiddenTools: string[] }): boolean {
348
+ return options.selectedTools.includes("delegate") && !options.hiddenTools.includes("delegate");
349
+ }
350
+
328
351
  /** The `delegate` tool's description: what delegation does, and the roster as it stands. */
329
352
  function toolDescription(agents: readonly Agent[]): string {
330
353
  const intro =
331
- "Hand a self-contained task to a delegate Pi session in a background tmux window, so it works while you do not. Each call starts the delegation at once; the delegate runs in this session's working directory and agent root and reports back later as one message. While it runs, keep working or end your turn: its result starts your next turn on its own, so there is no need to ask the delegate for it or poll delegation_status. Use it when the work is self-contained and you would rather keep your own context for it.";
354
+ "Hand a self-contained task to a delegate Pi session in a background tmux window, so it works while you do not. Each call starts the delegation at once; the delegate runs in this session's working directory and agent root and reports back later as one message. While it runs, keep working or end your turn: its result starts your next turn on its own. Do not poll delegation_status or send a blocking mail request merely to wait for the result; send a substantive follow-up only when you have something to add or ask. Use it when the work is self-contained and you would rather keep your own context for it.";
332
355
  if (agents.length === 0) {
333
356
  return `${intro}\n\nNo agents are defined yet. Add ${join(
334
357
  getAgentDir(),
358
+ "squire",
335
359
  "agents",
336
360
  "<name>",
337
361
  "AGENT.md",
@@ -411,6 +435,7 @@ export default function delegate(
411
435
  throw new Error(
412
436
  `no agents are defined; add ${join(
413
437
  getAgentDir(),
438
+ "squire",
414
439
  "agents",
415
440
  "<name>",
416
441
  "AGENT.md",
@@ -541,11 +566,13 @@ export default function delegate(
541
566
  }
542
567
  }
543
568
 
544
- /** The tool's description is fixed at registration, so it is rebuilt at every session start. */
545
- const delegateTool = () => ({
569
+ /** The roster description last registered, so unchanged metadata is not registered again. */
570
+ let registeredDescription: string | undefined;
571
+
572
+ const delegateTool = (description: string) => ({
546
573
  name: "delegate",
547
574
  label: "Delegate",
548
- description: toolDescription(currentRoster().agents),
575
+ description,
549
576
  parameters: Type.Object({
550
577
  agent: Type.String({ description: "Roster agent name to run." }),
551
578
  task: Type.String({
@@ -619,11 +646,32 @@ export default function delegate(
619
646
  },
620
647
  });
621
648
 
649
+ const registerDelegate = (description: string): void => {
650
+ pi.registerTool(delegateTool(description) as never);
651
+ registeredDescription = description;
652
+ };
653
+
622
654
  pi.on("session_start", (_event, ctx) => {
623
655
  registry = ctx.modelRegistry;
624
656
  tracking.restore(ctx);
625
- // The roster is read at call time; the description can only be fixed here.
626
- pi.registerTool(delegateTool() as never);
657
+ // The roster is read at call time; the description is rebuilt here and before each run.
658
+ registerDelegate(toolDescription(currentRoster().agents));
659
+ });
660
+
661
+ // One roster/prompt snapshot prepares this run: refresh the description only when
662
+ // it changed, and replace only this extension's prompt section from the same snapshot.
663
+ pi.on("before_agent_start", (event, ctx) => {
664
+ const description = toolDescription(currentRoster().agents);
665
+ const prompt = readParentPrompt();
666
+ const visible = delegateVisible(event.systemPromptOptions);
667
+ // Report a read failure even when guidance cannot be used, so it does not go unseen.
668
+ if (prompt.error !== undefined) ctx.ui.notify(`delegate: ${prompt.error}`, "warning");
669
+ if (visible && description !== registeredDescription) registerDelegate(description);
670
+ if (!visible || prompt.error !== undefined || prompt.text === undefined) {
671
+ delete event.systemPromptOptions.sections[PARENT_SECTION];
672
+ return;
673
+ }
674
+ event.systemPromptOptions.sections[PARENT_SECTION] = prompt.text;
627
675
  });
628
676
 
629
677
  pi.on("session_shutdown", () => {
@@ -680,6 +728,6 @@ export default function delegate(
680
728
  },
681
729
  });
682
730
 
683
- pi.registerTool(delegateTool() as never);
731
+ registerDelegate(toolDescription(currentRoster().agents));
684
732
  tracking.registerTools();
685
733
  }
package/src/limits.ts CHANGED
@@ -145,10 +145,12 @@ export function headerReset(response: Response | undefined, now = Date.now()): n
145
145
 
146
146
  /**
147
147
  * Whether an error message (Pi's `<status>: <body>`) is a usage limit, and
148
- * the reset time the text states, if any. Overloaded and 5xx errors never are.
148
+ * the reset time the text states, if any. Overloaded, unavailable and 5xx
149
+ * errors never are: OpenCode Go sends "429: Endpoint is unavailable" for an
150
+ * outage, with quota left.
149
151
  */
150
152
  export function classify(message: string, now = Date.now()): { resetAt?: number } | undefined {
151
- if (/overloaded/i.test(message) || /^\s*5\d\d\b/.test(message)) return undefined;
153
+ if (/overloaded|unavailable/i.test(message) || /^\s*5\d\d\b/.test(message)) return undefined;
152
154
  if (!LIMIT_PATTERN.test(message) && !/^\s*429\b/.test(message)) return undefined;
153
155
  const resetAt = statedReset(message, now);
154
156
  return resetAt !== undefined && resetAt > now ? { resetAt } : {};