pi-squire 0.8.1 → 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,39 @@ 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
+
9
42
  ## [0.8.1] - 2026-10-20
10
43
 
11
44
  ### Fixed
@@ -285,7 +318,8 @@ author's dotfiles; the changes below are against that copy.
285
318
 
286
319
  - The published file list includes `child.ts`, which `index.ts` imports.
287
320
 
288
- [Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.8.1...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
289
323
  [0.8.1]: https://github.com/gvanderclay/pi-squire/compare/v0.8.0...v0.8.1
290
324
  [0.8.0]: https://github.com/gvanderclay/pi-squire/compare/v0.7.0...v0.8.0
291
325
  [0.7.0]: https://github.com/gvanderclay/pi-squire/compare/v0.6.0...v0.7.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.1",
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";
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
  }