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 +12 -1
- package/README.md +26 -9
- package/package.json +1 -1
- package/src/index.ts +4 -4
- package/src/window-placement.ts +31 -14
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.
|
|
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>-
|
|
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-
|
|
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-
|
|
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>-
|
|
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
|
|
401
|
-
|
|
402
|
-
resumes
|
|
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
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,
|
|
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 =
|
|
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: () =>
|
|
1369
|
+
placement: () => windowSettings(getAgentDir()).placement,
|
|
1370
1370
|
roster: rosterFor,
|
|
1371
1371
|
admit: (agent, ctx) => admit(agent, currentRoster(ctx)),
|
|
1372
1372
|
authorize: (id, ctx) => {
|
package/src/window-placement.ts
CHANGED
|
@@ -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
|
|
3
|
-
// opens
|
|
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" ||
|
|
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}
|
|
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
|
|
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))
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
}
|