pi-squire 0.17.0 → 0.18.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,16 @@ follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.18.0] - 2026-11-01
10
+
11
+ ### Changed
12
+
13
+ - New root companion sessions deliberately default to `<project>-delegates`
14
+ instead of `<project>-delegate-session`. Set `companionSuffix` in
15
+ `<agent dir>/squire/settings.json` to use another suffix, such as `playpen`.
16
+ Existing companions and saved identities keep their names, including for
17
+ nested launches and resumes.
18
+
9
19
  ## [0.17.0] - 2026-11-01
10
20
 
11
21
  ### Breaking
@@ -998,7 +1008,8 @@ author's dotfiles; the changes below are against that copy.
998
1008
 
999
1009
  - The published file list includes `child.ts`, which `index.ts` imports.
1000
1010
 
1001
- [Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.17.0...HEAD
1011
+ [Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.18.0...HEAD
1012
+ [0.18.0]: https://github.com/gvanderclay/pi-squire/compare/v0.17.0...v0.18.0
1002
1013
  [0.17.0]: https://github.com/gvanderclay/pi-squire/compare/v0.16.0...v0.17.0
1003
1014
  [0.16.0]: https://github.com/gvanderclay/pi-squire/compare/v0.15.0...v0.16.0
1004
1015
  [0.15.0]: https://github.com/gvanderclay/pi-squire/compare/v0.14.0...v0.15.0
package/README.md CHANGED
@@ -358,14 +358,14 @@ same-id Squire owner and obsolete local/shared resume chains are refused.
358
358
  ## Where delegate windows open
359
359
 
360
360
  By default a delegate's window opens in a shared **project companion session**
361
- named `<project>-delegate-session` on your existing tmux server. The owning
361
+ named `<project>-delegates` on your existing tmux server. The owning
362
362
  root Pi session's project chooses it, not the parent tmux session or the
363
363
  delegate's working directory. Launching into another repository or creating a
364
364
  worktree there still uses the root's companion; resuming a delegate does too.
365
365
 
366
366
  Nested launches inherit the original root project: A → coordinator in B →
367
367
  worker in C all select A's companion. The preferred base name (for example,
368
- `A-delegate-session`) is saved as `rootPlacement` in the existing durable
368
+ `A-delegates`) is saved as `rootPlacement` in the existing durable
369
369
  delegation identity and shared tree records, not recomputed from B/C or the
370
370
  containing tmux session. Reopening the **same** coordinator session without its
371
371
  launch environment restores that preference; it can launch or directly resume
@@ -384,7 +384,7 @@ uses the root working-directory basename instead.
384
384
  Project names preserve ASCII letters (including case), digits, hyphens and
385
385
  underscores. Runs of other characters become one hyphen, edge hyphens are
386
386
  trimmed, and an empty name becomes `project`: `Client API` uses
387
- `Client-API-delegate-session`.
387
+ `Client-API-delegates`.
388
388
 
389
389
  Multiple roots share a companion even across parent tmux sessions. Distinct
390
390
  paths with the same basename, separate clones named alike, and names that
@@ -394,12 +394,13 @@ each root.
394
394
 
395
395
  pi-squire verifies the tmux session option `@squire-companion` before reusing
396
396
  an occupied name. An unmarked session is left untouched; numbered alternatives
397
- start at `<project>-delegate-session-2`, skipping unmarked alternatives too.
397
+ start at `<project>-<suffix>-2`, skipping unmarked alternatives too.
398
398
  Later launches reuse an existing managed alternative even if a lower name
399
399
  becomes free. Unexpected duplicate managed candidates use the first in numeric
400
- order, without moving windows or repairing sessions. Existing legacy companions
401
- and live windows are neither renamed nor migrated; only new launches and
402
- resumes use project naming.
400
+ order, without moving windows or repairing sessions. Existing companions,
401
+ live windows and saved identities keep their names, including the former
402
+ `<project>-delegate-session` default. Nested launches and resumes retain the
403
+ saved preference; companions are not renamed or migrated.
403
404
 
404
405
  - The session is created on demand, by the launch that needs it, and tmux
405
406
  removes it with its last window, so it does not outlive its delegates.
@@ -416,6 +417,22 @@ resumes use project naming.
416
417
  up each time, so it follows a window you move to another session, which is
417
418
  still found, closed and resumed.
418
419
 
420
+ To use a different suffix, set `companionSuffix` in
421
+ `<agent dir>/squire/settings.json`:
422
+
423
+ ```json
424
+ { "companionSuffix": "playpen" }
425
+ ```
426
+
427
+ This names a new root's companion `<project>-playpen`. The default suffix is
428
+ `delegates`. A suffix must contain 1 to 32 ASCII letters, digits, underscores
429
+ or hyphens, with no leading or trailing hyphen. It cannot be entirely numeric
430
+ or end in a hyphen followed by digits, such as `2` or `x-10`, because those
431
+ names overlap with numbered alternatives. An invalid suffix refuses the launch
432
+ with an error naming the file, before the task is sent or a window opens.
433
+ The suffix is read with `windowPlacement` at launch. It does not replace a
434
+ saved root preference for nested launches or resumes.
435
+
419
436
  To open windows in your own session, as before, set `windowPlacement` in
420
437
  `<agent dir>/squire/settings.json`, beside `worktreeRoot`:
421
438
 
@@ -1076,7 +1093,7 @@ add one.
1076
1093
 
1077
1094
  `<agent dir>/squire/` holds the optional parent guidance file and
1078
1095
  `settings.json` (`maxDepth`, `maxConcurrentDelegates`, `maxDelegatesPerRoot`,
1079
- `windowPlacement`, `worktreeRoot`, `delegateTools`, `toolBundles`,
1096
+ `windowPlacement`, `companionSuffix`, `worktreeRoot`, `delegateTools`, `toolBundles`,
1080
1097
  `delegateWidgetPlacement` and `delegateWidgetMaxEntries`); the
1081
1098
  agents live in `<agent dir>/agents/` (see [Agents](#agents)).
1082
1099
  `delegation_prompt.md` is Markdown, inserted unchanged as the
@@ -1967,7 +1984,7 @@ Other extensions may rely on:
1967
1984
  `<agent dir>/agents/<name>.md` and, in a trusted project,
1968
1985
  `<cwd>/.pi/agents/<name>.md`;
1969
1986
  - the `maxDepth`, `maxConcurrentDelegates`, `maxDelegatesPerRoot`, `delegateTools`, `toolBundles`,
1970
- `windowPlacement`, `worktreeRoot`, `delegateWidgetPlacement` and
1987
+ `windowPlacement`, `companionSuffix`, `worktreeRoot`, `delegateWidgetPlacement` and
1971
1988
  `delegateWidgetMaxEntries` settings in
1972
1989
  `<agent dir>/squire/settings.json`;
1973
1990
  - the optional `<agent dir>/squire/delegation_prompt.md` file and the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-squire",
3
- "version": "0.17.0",
3
+ "version": "0.18.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",
package/src/index.ts CHANGED
@@ -100,7 +100,7 @@ import { registerTrustControl } from "./trust-control.ts";
100
100
  import { createProactive, createUsageClient, type Proactive, type UsageClient } from "./usage.ts";
101
101
  import { registerDelegateWidget } from "./widget.ts";
102
102
  import { registerActivityRuntime } from "./widget-activity-runtime.ts";
103
- import { assertRootPlacement, placementProject, type WindowPlacement, windowPlacement } from "./window-placement.ts";
103
+ import { assertRootPlacement, placementProject, type WindowPlacement, windowSettings } from "./window-placement.ts";
104
104
  import {
105
105
  normalizeWorkspaceValue,
106
106
  prepareDirectory,
@@ -1147,11 +1147,11 @@ export default function delegate(
1147
1147
  if (!tmux.insideTmux()) throw new Error(NOT_IN_TMUX);
1148
1148
  await requireTmuxVersion(tmux);
1149
1149
  // Read once, before anything is created or mailed; the launch keeps these values.
1150
- const placement = windowPlacement(getAgentDir());
1150
+ const { placement, companionSuffix } = windowSettings(getAgentDir());
1151
1151
  const parentIdentity = identity.current();
1152
1152
  const project = parentIdentity
1153
1153
  ? { companion: parentIdentity.rootPlacement, notices: [] }
1154
- : placementProject(ctx.cwd);
1154
+ : placementProject(ctx.cwd, companionSuffix);
1155
1155
  assertRootPlacement(project.companion);
1156
1156
  const own = identity.child(start.agent, ctx, project.companion);
1157
1157
  if (parentIdentity) assertParentPlacement(readTree(own.root), own);
@@ -1366,7 +1366,7 @@ export default function delegate(
1366
1366
  delegateSchema: DelegateSchema,
1367
1367
  notInTmux: NOT_IN_TMUX,
1368
1368
  startPolling: tracking.startPolling,
1369
- placement: () => windowPlacement(getAgentDir()),
1369
+ placement: () => windowSettings(getAgentDir()).placement,
1370
1370
  roster: rosterFor,
1371
1371
  admit: (agent, ctx) => admit(agent, currentRoster(ctx)),
1372
1372
  authorize: (id, ctx) => {
@@ -1,6 +1,6 @@
1
1
  // Call-time choice of where delegate windows open. `companion` (the default)
2
- // opens them in a shared project session; `current`
3
- // opens them in the parent's own session, as before. Read from
2
+ // opens them in a shared project session named `<project>-<companionSuffix>`
3
+ // (default suffix `delegates`); `current` opens in the parent's own session. Read from
4
4
  // `<agent dir>/squire/settings.json` at every launch, like `worktreeRoot`.
5
5
  import { readFileSync } from "node:fs";
6
6
  import { basename, join } from "node:path";
@@ -9,21 +9,25 @@ import { inspectGit } from "./git.ts";
9
9
 
10
10
  export type WindowPlacement = "companion" | "current";
11
11
 
12
+ const DEFAULT_SUFFIX = "delegates";
13
+ const safeName = /^[A-Za-z0-9_](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?$/;
14
+ const numberedEnding = /(^|-)[0-9]+$/;
15
+
12
16
  /** A preferred base, never a selected numbered companion or a tmux locator. */
13
17
  export function assertRootPlacement(value: unknown): asserts value is string {
14
- if (typeof value !== "string" || !/^[A-Za-z0-9_](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?-delegate-session$/.test(value))
18
+ if (typeof value !== "string" || !safeName.test(value) || !value.includes("-") || numberedEnding.test(value))
15
19
  throw new Error(
16
20
  "invalid or missing rootPlacement in delegation identity; original root companion preference is required",
17
21
  );
18
22
  }
19
23
 
20
24
  /** The owning root's project, never the delegate's chosen working directory. */
21
- export function placementProject(cwd: string): { companion: string; notices: string[] } {
25
+ export function placementProject(cwd: string, suffix = DEFAULT_SUFFIX): { companion: string; notices: string[] } {
22
26
  const git = inspectGit(cwd);
23
27
  const project = basename(git.kind === "git" ? git.identity.mainRoot : cwd);
24
28
  const normalized = project.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "project";
25
29
  return {
26
- companion: `${normalized}-delegate-session`,
30
+ companion: `${normalized}-${suffix}`,
27
31
  notices:
28
32
  git.kind === "unknown"
29
33
  ? [
@@ -38,19 +42,32 @@ export function placementProject(cwd: string): { companion: string; notices: str
38
42
  * This reader defaults malformed/unreadable files to companion; mandatory
39
43
  * tree-cap admission separately refuses those files for every launch/resume.
40
44
  */
41
- export function windowPlacement(agentDir: string): WindowPlacement {
45
+ export function windowSettings(agentDir: string): { placement: WindowPlacement; companionSuffix: string } {
42
46
  const file = join(agentDir, "squire", "settings.json");
43
47
  let parsed: unknown;
44
48
  try {
45
49
  parsed = JSON.parse(readFileSync(file, "utf8"));
46
50
  } catch {
47
- return "companion";
51
+ return { placement: "companion", companionSuffix: DEFAULT_SUFFIX };
48
52
  }
49
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return "companion";
50
- const value = (parsed as Record<string, unknown>).windowPlacement;
51
- if (value === undefined || value === "companion") return "companion";
52
- if (value === "current") return "current";
53
- throw new Error(
54
- `invalid window placement settings ${file}: windowPlacement must be "companion" or "current", not ${JSON.stringify(value)}`,
55
- );
53
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
54
+ return { placement: "companion", companionSuffix: DEFAULT_SUFFIX };
55
+ const { windowPlacement: placement = "companion", companionSuffix = DEFAULT_SUFFIX } = parsed as Record<
56
+ string,
57
+ unknown
58
+ >;
59
+ if (placement !== "companion" && placement !== "current")
60
+ throw new Error(
61
+ `invalid window placement settings ${file}: windowPlacement must be "companion" or "current", not ${JSON.stringify(placement)}`,
62
+ );
63
+ if (
64
+ typeof companionSuffix !== "string" ||
65
+ companionSuffix.length > 32 ||
66
+ !safeName.test(companionSuffix) ||
67
+ numberedEnding.test(companionSuffix)
68
+ )
69
+ throw new Error(
70
+ `invalid window placement settings ${file}: companionSuffix must be 1 to 32 ASCII letters, digits, underscores or hyphens, with no edge hyphen or numeric final segment, not ${JSON.stringify(companionSuffix)}`,
71
+ );
72
+ return { placement, companionSuffix };
56
73
  }