pi-squire 0.17.0 → 0.18.1
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 +29 -1
- package/README.md +31 -14
- package/package.json +1 -1
- package/src/git.ts +0 -9
- package/src/index.ts +31 -74
- package/src/render.ts +1 -1
- package/src/resume.ts +7 -18
- package/src/snapshot.ts +4 -27
- package/src/tracking-attempt.ts +5 -50
- package/src/tracking.ts +14 -34
- package/src/tree-close.ts +1 -1
- package/src/window-placement.ts +31 -14
- package/src/workspace.ts +9 -64
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,32 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.18.1] - 2026-11-01
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- The delegation tools put about 70% less text in a model's context: in a
|
|
14
|
+
codemode root their declarations drop from about 5,100 estimated tokens to
|
|
15
|
+
about 1,400. The descriptions and parameter notes are shorter, and the output
|
|
16
|
+
schemas carry no field descriptions. `workspace`, `workspaceSnapshot`, and a
|
|
17
|
+
workspace attempt's `plan` and `attempt` are declared by their discriminant
|
|
18
|
+
and directory only, with their other fields left open; the structured data
|
|
19
|
+
itself is unchanged. Tests that pinned the old wording are updated.
|
|
20
|
+
- A delegate's appended prompt is its agent body and working-directory line
|
|
21
|
+
only. The paragraph naming the parent's address and the final-message rule
|
|
22
|
+
are in the per-run `squire_delegate_task` section alone, where they were
|
|
23
|
+
already repeated.
|
|
24
|
+
|
|
25
|
+
## [0.18.0] - 2026-11-01
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- New root companion sessions deliberately default to `<project>-delegates`
|
|
30
|
+
instead of `<project>-delegate-session`. Set `companionSuffix` in
|
|
31
|
+
`<agent dir>/squire/settings.json` to use another suffix, such as `playpen`.
|
|
32
|
+
Existing companions and saved identities keep their names, including for
|
|
33
|
+
nested launches and resumes.
|
|
34
|
+
|
|
9
35
|
## [0.17.0] - 2026-11-01
|
|
10
36
|
|
|
11
37
|
### Breaking
|
|
@@ -998,7 +1024,9 @@ author's dotfiles; the changes below are against that copy.
|
|
|
998
1024
|
|
|
999
1025
|
- The published file list includes `child.ts`, which `index.ts` imports.
|
|
1000
1026
|
|
|
1001
|
-
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.
|
|
1027
|
+
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.18.1...HEAD
|
|
1028
|
+
[0.18.1]: https://github.com/gvanderclay/pi-squire/compare/v0.18.0...v0.18.1
|
|
1029
|
+
[0.18.0]: https://github.com/gvanderclay/pi-squire/compare/v0.17.0...v0.18.0
|
|
1002
1030
|
[0.17.0]: https://github.com/gvanderclay/pi-squire/compare/v0.16.0...v0.17.0
|
|
1003
1031
|
[0.16.0]: https://github.com/gvanderclay/pi-squire/compare/v0.15.0...v0.16.0
|
|
1004
1032
|
[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
|
|
|
@@ -1046,14 +1063,14 @@ limit, it resolves each entry the way Pi resolves `--model`: an exact
|
|
|
1046
1063
|
order the entries are listed. No entry may be a `*` pattern: a `model` or
|
|
1047
1064
|
`fallback` entry with `*` makes the file invalid, and the agent is disabled.
|
|
1048
1065
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1066
|
+
Each run of the delegate also gets a system-prompt section naming the parent
|
|
1067
|
+
session's address. The delegate's task arrives as a message from that
|
|
1051
1068
|
session, and that message and every later one from the same address are
|
|
1052
1069
|
instructions to follow as given. Other sessions are colleagues: the delegate
|
|
1053
1070
|
may answer them, but checks with the parent before doing work the parent did
|
|
1054
|
-
not ask for.
|
|
1055
|
-
|
|
1056
|
-
the
|
|
1071
|
+
not ask for. Its final message reaches the parent on its own. The section names
|
|
1072
|
+
no provider, package or tool, so it holds whatever `message:*` provider
|
|
1073
|
+
delivers the task. `prompt_mode: replace` appends the body as written.
|
|
1057
1074
|
|
|
1058
1075
|
An agent file that cannot be used is invalid, and its agent is disabled:
|
|
1059
1076
|
frontmatter that is not readable YAML or not a set of fields, an `enabled` or
|
|
@@ -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/git.ts
CHANGED
|
@@ -6,8 +6,6 @@ import { spawnSync } from "node:child_process";
|
|
|
6
6
|
import { realpathSync } from "node:fs";
|
|
7
7
|
import { basename, dirname, resolve } from "node:path";
|
|
8
8
|
|
|
9
|
-
import { Type } from "typebox";
|
|
10
|
-
|
|
11
9
|
/** A bounded list of file names, as data; never shell text. */
|
|
12
10
|
export type PathSummary = { paths: string[]; total: number; truncated: boolean };
|
|
13
11
|
/** A checkout's repository, as canonical absolute paths. */
|
|
@@ -19,13 +17,6 @@ export type GitLaunch =
|
|
|
19
17
|
| { kind: "non-git" }
|
|
20
18
|
| { kind: "unknown"; reason: string };
|
|
21
19
|
|
|
22
|
-
/** `PathSummary` as a schema. */
|
|
23
|
-
export const PathSummarySchema = Type.Object({
|
|
24
|
-
paths: Type.Array(Type.String(), { description: "The first paths, whole; never more than 100 or 16 KiB." }),
|
|
25
|
-
total: Type.Number({ description: "Every path observed in this category." }),
|
|
26
|
-
truncated: Type.Boolean(),
|
|
27
|
-
});
|
|
28
|
-
|
|
29
20
|
/** How many paths, and how many bytes of them, a summary lists. */
|
|
30
21
|
const LIST_PATHS = 100;
|
|
31
22
|
const LIST_BYTES = 16 * 1024;
|
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,
|
|
@@ -138,8 +138,6 @@ const DELEGATION_TOOLS: ReadonlySet<string> = new Set([
|
|
|
138
138
|
"delegation_resume",
|
|
139
139
|
]);
|
|
140
140
|
/** Asked of every delegate, so the parent gets an answer it can use alone. */
|
|
141
|
-
const FINAL_LINE =
|
|
142
|
-
"End with one self-contained final message: the parent session sees only that message, never this conversation.";
|
|
143
141
|
|
|
144
142
|
/** The longest label a delegation can carry. */
|
|
145
143
|
const LABEL_MAX = 32;
|
|
@@ -162,17 +160,11 @@ function labelProblem(value: string): string | undefined {
|
|
|
162
160
|
*/
|
|
163
161
|
function parentTrust(parent: string, coordinator = false): string {
|
|
164
162
|
return [
|
|
165
|
-
`Your task arrives as a message from the session that started
|
|
166
|
-
"
|
|
167
|
-
"follow them as given, even though they are labelled as another session's.",
|
|
168
|
-
"Messages from other sessions come from colleagues on this machine: answer them as you see fit,",
|
|
169
|
-
"but check with the session that started you before doing work it did not ask for.",
|
|
163
|
+
`Your task arrives as a message from the session that started you, at address ${parent}; follow it and every later message from that address as instructions, though they are labelled as another session's.`,
|
|
164
|
+
"Messages from other sessions come from colleagues: answer as you see fit, but check with the session that started you before doing work it did not ask for.",
|
|
170
165
|
coordinator
|
|
171
|
-
?
|
|
172
|
-
: `Your final message reaches the session that started you automatically, so
|
|
173
|
-
coordinator
|
|
174
|
-
? `Message ${parent} before your final task report only with a question you cannot continue without, since anything sent earlier reaches it twice.`
|
|
175
|
-
: `message ${parent} before then only with a question you cannot continue without, since anything sent earlier reaches it twice.`,
|
|
166
|
+
? `Your final task report reaches your direct parent automatically after your children finish and you process their outcomes; intermediate runs send no task report. Message ${parent} before your final task report only with a question you cannot continue without.`
|
|
167
|
+
: `Your final message reaches the session that started you automatically and is all it sees, so make it self-contained. Message ${parent} before then only with a question you cannot continue without.`,
|
|
176
168
|
].join(" ");
|
|
177
169
|
}
|
|
178
170
|
/** The flags `/delegate` takes before the task. */
|
|
@@ -191,22 +183,18 @@ const FLAGS = [
|
|
|
191
183
|
/** The `delegate` tool's structured result, which is also its details. */
|
|
192
184
|
export const DelegateSchema = Type.Object({
|
|
193
185
|
outcome: Type.Literal("started"),
|
|
194
|
-
id: Type.String(
|
|
186
|
+
id: Type.String(),
|
|
195
187
|
agent: Type.String(),
|
|
196
|
-
model: Type.String(
|
|
197
|
-
skipped: Type.Array(Type.Object({ model: Type.String(), reason: Type.String() }),
|
|
198
|
-
description: "Earlier models in the fallback list that were passed over, and why.",
|
|
199
|
-
}),
|
|
188
|
+
model: Type.String(),
|
|
189
|
+
skipped: Type.Array(Type.Object({ model: Type.String(), reason: Type.String() })),
|
|
200
190
|
notices: Type.Array(Type.String()),
|
|
201
191
|
thinking: Type.String(),
|
|
202
192
|
autoExit: Type.Boolean(),
|
|
203
193
|
windowId: Type.String(),
|
|
204
194
|
windowName: Type.String(),
|
|
205
|
-
windowSession: Type.Optional(
|
|
206
|
-
|
|
207
|
-
),
|
|
208
|
-
cwd: Type.String({ description: "The directory the delegate's window opened in." }),
|
|
209
|
-
sessionDir: Type.String({ description: "The session-storage directory the delegate was told to use." }),
|
|
195
|
+
windowSession: Type.Optional(Type.String()),
|
|
196
|
+
cwd: Type.String(),
|
|
197
|
+
sessionDir: Type.String(),
|
|
210
198
|
workspace: WorkspaceSchema,
|
|
211
199
|
});
|
|
212
200
|
|
|
@@ -652,23 +640,19 @@ function toolSelection(agent: Agent, parentTools: readonly string[], extra: read
|
|
|
652
640
|
return [...new Set([...base, ...extra])].filter((name) => !DELEGATION_TOOLS.has(name));
|
|
653
641
|
}
|
|
654
642
|
|
|
655
|
-
/** The delegate's appended prompt: the agent's body
|
|
656
|
-
function delegatePrompt(agent: Agent,
|
|
657
|
-
|
|
658
|
-
? "End the task with one self-contained final report to your direct parent after your child tasks finish and you process their outcomes."
|
|
659
|
-
: FINAL_LINE;
|
|
660
|
-
const tail = `${final}\n\n${parentTrust(parent, coordinator)}${managedContext}`;
|
|
661
|
-
if (agent.prompt === "") return tail;
|
|
643
|
+
/** The delegate's appended prompt: the agent's body and where it works; the run-time section carries the parent's address. */
|
|
644
|
+
function delegatePrompt(agent: Agent, managedContext: string): string {
|
|
645
|
+
if (agent.prompt === "") return managedContext.trimStart();
|
|
662
646
|
// `replace` is how every delegate was built before `prompt_mode`, byte for byte.
|
|
663
647
|
const body =
|
|
664
648
|
agent.promptMode === "replace" ? agent.prompt : `<agent_instructions>\n${agent.prompt}\n</agent_instructions>`;
|
|
665
|
-
return `${body}
|
|
649
|
+
return `${body}${managedContext}`;
|
|
666
650
|
}
|
|
667
651
|
|
|
668
652
|
/** The `delegate` tool's description: what delegation does, and the roster as it stands. */
|
|
669
653
|
function toolDescription(agents: readonly Agent[]): string {
|
|
670
654
|
const intro =
|
|
671
|
-
"
|
|
655
|
+
"Start a delegate Pi session on a self-contained task in a background tmux window. Its result arrives later as a message that starts your next turn, so keep working or end your turn; do not poll delegation_status or mail it just to wait.";
|
|
672
656
|
if (agents.length === 0) {
|
|
673
657
|
return `${intro}\n\nNo agents are defined yet. Add ${whereAgentsGo()}.`;
|
|
674
658
|
}
|
|
@@ -678,11 +662,7 @@ function toolDescription(agents: readonly Agent[]): string {
|
|
|
678
662
|
"Agents:",
|
|
679
663
|
...agents.map(rosterLine),
|
|
680
664
|
"",
|
|
681
|
-
"
|
|
682
|
-
"`model` and `thinking` override the agent's defaults for this call, except a field the agent locks; an unknown value is refused with close matches. `label` names the delegate's window and session; give one that says what the task is. `delegation_status` reports a delegation, and `delegation_close` ends one. `delegation_resume` continues an exited delegate in its own session with a follow-up task, for work that needs the context it already built.",
|
|
683
|
-
"`auto_exit` (default the agent's, normally true) closes the delegate's window once it finishes. Set it false when you mean to keep talking to the delegate by mail after its result; the user can also keep a window open by typing in it.",
|
|
684
|
-
"",
|
|
685
|
-
"Writing the task: the delegate sees nothing of this session, so the task must stand alone. State the question, the decision its answer feeds, what is already known or ruled out, and the files or paths to start from. When the work produces a report, name the path to write it to and ask for the conclusion back, not the contents.",
|
|
665
|
+
"The delegate sees only the task and its agent prompt. State the question, the decision it feeds, what is known or ruled out, and where to start. For a long report, name a file to write and ask for the conclusion back.",
|
|
686
666
|
].join("\n");
|
|
687
667
|
}
|
|
688
668
|
|
|
@@ -994,9 +974,7 @@ export default function delegate(
|
|
|
994
974
|
: workspace.mode === "existing"
|
|
995
975
|
? `\n\nThe session that started you chose your working directory: ${workspace.cwd}`
|
|
996
976
|
: "";
|
|
997
|
-
const prompt = writePromptFile(
|
|
998
|
-
delegatePrompt(start.agent, parent, managedContext, grantsNesting(start.agent, own.depth)),
|
|
999
|
-
);
|
|
977
|
+
const prompt = writePromptFile(delegatePrompt(start.agent, managedContext));
|
|
1000
978
|
const argv = [
|
|
1001
979
|
...parentCommand(),
|
|
1002
980
|
...session,
|
|
@@ -1147,11 +1125,11 @@ export default function delegate(
|
|
|
1147
1125
|
if (!tmux.insideTmux()) throw new Error(NOT_IN_TMUX);
|
|
1148
1126
|
await requireTmuxVersion(tmux);
|
|
1149
1127
|
// Read once, before anything is created or mailed; the launch keeps these values.
|
|
1150
|
-
const placement =
|
|
1128
|
+
const { placement, companionSuffix } = windowSettings(getAgentDir());
|
|
1151
1129
|
const parentIdentity = identity.current();
|
|
1152
1130
|
const project = parentIdentity
|
|
1153
1131
|
? { companion: parentIdentity.rootPlacement, notices: [] }
|
|
1154
|
-
: placementProject(ctx.cwd);
|
|
1132
|
+
: placementProject(ctx.cwd, companionSuffix);
|
|
1155
1133
|
assertRootPlacement(project.companion);
|
|
1156
1134
|
const own = identity.child(start.agent, ctx, project.companion);
|
|
1157
1135
|
if (parentIdentity) assertParentPlacement(readTree(own.root), own);
|
|
@@ -1366,7 +1344,7 @@ export default function delegate(
|
|
|
1366
1344
|
delegateSchema: DelegateSchema,
|
|
1367
1345
|
notInTmux: NOT_IN_TMUX,
|
|
1368
1346
|
startPolling: tracking.startPolling,
|
|
1369
|
-
placement: () =>
|
|
1347
|
+
placement: () => windowSettings(getAgentDir()).placement,
|
|
1370
1348
|
roster: rosterFor,
|
|
1371
1349
|
admit: (agent, ctx) => admit(agent, currentRoster(ctx)),
|
|
1372
1350
|
authorize: (id, ctx) => {
|
|
@@ -1526,53 +1504,32 @@ export default function delegate(
|
|
|
1526
1504
|
label: "Delegate",
|
|
1527
1505
|
description,
|
|
1528
1506
|
parameters: Type.Object({
|
|
1529
|
-
agent: Type.String({ description: "Roster agent name
|
|
1530
|
-
task: Type.String(
|
|
1531
|
-
|
|
1532
|
-
}),
|
|
1533
|
-
model: Type.Optional(
|
|
1534
|
-
Type.String({
|
|
1535
|
-
description:
|
|
1536
|
-
"provider/id model for this call; defaults to the agent's, or this session's when it names none. Not used when the agent locks model.",
|
|
1537
|
-
}),
|
|
1538
|
-
),
|
|
1539
|
-
thinking: Type.Optional(
|
|
1540
|
-
Type.String({
|
|
1541
|
-
description: `Thinking level (${THINKING_LEVELS.join(", ")}); defaults to the agent's, or this session's when it names none. Not used when the agent locks thinking.`,
|
|
1542
|
-
}),
|
|
1543
|
-
),
|
|
1507
|
+
agent: Type.String({ description: "Roster agent name." }),
|
|
1508
|
+
task: Type.String(),
|
|
1509
|
+
model: Type.Optional(Type.String({ description: "provider/id; defaults to the agent's." })),
|
|
1510
|
+
thinking: Type.Optional(Type.String({ description: `${THINKING_LEVELS.join(", ")}; defaults to the agent's.` })),
|
|
1544
1511
|
label: Type.Optional(
|
|
1545
1512
|
Type.String({
|
|
1546
|
-
description: `
|
|
1513
|
+
description: `Window and session name suffix: letters, digits, _ or -, at most ${LABEL_MAX}.`,
|
|
1547
1514
|
}),
|
|
1548
1515
|
),
|
|
1549
1516
|
cwd: Type.Optional(
|
|
1550
|
-
Type.Union([Type.String(), Type.Null()], {
|
|
1551
|
-
description:
|
|
1552
|
-
"An existing directory to run the delegate in: absolute, relative to this session's working directory, or starting with ~/. Omit to run in this session's working directory. The directory is used as it is; nothing is created or cleaned up. Mutually exclusive with worktree.",
|
|
1553
|
-
}),
|
|
1517
|
+
Type.Union([Type.String(), Type.Null()], { description: "Existing directory to run in. Not with worktree." }),
|
|
1554
1518
|
),
|
|
1555
1519
|
worktree: Type.Optional(
|
|
1556
1520
|
Type.Union([Type.String(), Type.Null()], {
|
|
1557
|
-
description:
|
|
1558
|
-
"Create a new managed worktree and local branch with this literal branch name, from this repository's committed HEAD or explicit worktree_base. Source dirt is not copied. Mutually exclusive with cwd.",
|
|
1521
|
+
description: "New branch name: creates a managed worktree from HEAD or worktree_base. Not with cwd.",
|
|
1559
1522
|
}),
|
|
1560
1523
|
),
|
|
1561
1524
|
worktree_source: Type.Optional(
|
|
1562
|
-
Type.Union([Type.String(), Type.Null()], {
|
|
1563
|
-
description:
|
|
1564
|
-
"Existing Git checkout to create from; relative to this session or starting with ~/. Requires worktree.",
|
|
1565
|
-
}),
|
|
1525
|
+
Type.Union([Type.String(), Type.Null()], { description: "Git checkout to create the worktree from." }),
|
|
1566
1526
|
),
|
|
1567
1527
|
worktree_base: Type.Optional(
|
|
1568
|
-
Type.Union([Type.String(), Type.Null()], {
|
|
1569
|
-
description: "Local commit-ish to create the branch from; requires worktree. No fetch is performed.",
|
|
1570
|
-
}),
|
|
1528
|
+
Type.Union([Type.String(), Type.Null()], { description: "Local commit-ish for the worktree branch." }),
|
|
1571
1529
|
),
|
|
1572
1530
|
auto_exit: Type.Optional(
|
|
1573
1531
|
Type.Boolean({
|
|
1574
|
-
description:
|
|
1575
|
-
"Close the delegate's window once it finishes; defaults to the agent's setting. False keeps it open to talk to it by mail after its result.",
|
|
1532
|
+
description: "Close the window when it finishes; defaults to the agent's. False keeps it open for mail.",
|
|
1576
1533
|
}),
|
|
1577
1534
|
),
|
|
1578
1535
|
}),
|
package/src/render.ts
CHANGED
|
@@ -281,7 +281,7 @@ export function renderDelegateResult(
|
|
|
281
281
|
const text = result.content.map((part) => part.text ?? "").join("\n");
|
|
282
282
|
return new Text(context.isError ? theme.fg("error", text) : text, 0, 0);
|
|
283
283
|
}
|
|
284
|
-
const started = details as Static<typeof DelegateSchema
|
|
284
|
+
const started = details as Static<typeof DelegateSchema> & { workspace: Workspace };
|
|
285
285
|
const lines = [
|
|
286
286
|
`${theme.fg("accent", "●")} ${theme.fg("toolTitle", theme.bold(started.windowName))}${theme.fg(
|
|
287
287
|
"dim",
|
package/src/resume.ts
CHANGED
|
@@ -171,7 +171,7 @@ async function ensureExited(record: StartedRecorded, tmux: TmuxClient): Promise<
|
|
|
171
171
|
function resultSchema(delegateSchema: TObject): TObject {
|
|
172
172
|
return Type.Object({
|
|
173
173
|
...delegateSchema.properties,
|
|
174
|
-
resumedFrom: Type.String(
|
|
174
|
+
resumedFrom: Type.String(),
|
|
175
175
|
});
|
|
176
176
|
}
|
|
177
177
|
|
|
@@ -391,26 +391,15 @@ function resumeTool(deps: ResumeDeps): ToolDef {
|
|
|
391
391
|
name: "delegation_resume",
|
|
392
392
|
label: "Resume delegation",
|
|
393
393
|
description:
|
|
394
|
-
"
|
|
394
|
+
"Continue an exited direct child in its own session, so it keeps the context it built, with a follow-up task in a new window. It is recorded as a new delegation whose result arrives as a message. For unrelated work, use delegate. Refuses a running delegation (mail it instead), an open window, an older link of a resumed chain (it names the newest) and a missing working directory.",
|
|
395
395
|
parameters: Type.Object({
|
|
396
|
-
id: Type.String({ description: "The delegation
|
|
397
|
-
task: Type.String(
|
|
398
|
-
|
|
399
|
-
}),
|
|
400
|
-
model: Type.Optional(
|
|
401
|
-
Type.String({
|
|
402
|
-
description:
|
|
403
|
-
"provider/id to resume on; used as given. Omit to continue on the model the delegate finished on, or its agent's next fallback when that one is unavailable. Not used when the agent locks model: the agent's current model applies.",
|
|
404
|
-
}),
|
|
405
|
-
),
|
|
396
|
+
id: Type.String({ description: "The newest delegation id of the child." }),
|
|
397
|
+
task: Type.String(),
|
|
398
|
+
model: Type.Optional(Type.String({ description: "provider/id; defaults to the model it finished on." })),
|
|
406
399
|
thinking: Type.Optional(
|
|
407
|
-
Type.String({
|
|
408
|
-
description: `Thinking level (${THINKING_LEVELS.join(", ")}); defaults to the level the delegate last ran on, from its session. Not used when the agent locks thinking: the agent's current level applies.`,
|
|
409
|
-
}),
|
|
410
|
-
),
|
|
411
|
-
auto_exit: Type.Optional(
|
|
412
|
-
Type.Boolean({ description: "Close the window once it finishes; defaults to the delegation's setting." }),
|
|
400
|
+
Type.String({ description: `${THINKING_LEVELS.join(", ")}; defaults to the level it last ran on.` }),
|
|
413
401
|
),
|
|
402
|
+
auto_exit: Type.Optional(Type.Boolean({ description: "Defaults to the delegation's setting." })),
|
|
414
403
|
}),
|
|
415
404
|
outputSchema: resultSchema(deps.delegateSchema),
|
|
416
405
|
// Opens a window and removes nothing but an exited window of the old run; each call starts another run.
|
package/src/snapshot.ts
CHANGED
|
@@ -16,11 +16,9 @@ import {
|
|
|
16
16
|
type GitPosition,
|
|
17
17
|
onePath,
|
|
18
18
|
type PathSummary,
|
|
19
|
-
PathSummarySchema,
|
|
20
19
|
pendingPaths,
|
|
21
20
|
summarize,
|
|
22
21
|
} from "./git.ts";
|
|
23
|
-
import { PositionSchema } from "./workspace.ts";
|
|
24
22
|
|
|
25
23
|
/**
|
|
26
24
|
* A checkout as observed at one moment: its position and head against the
|
|
@@ -41,31 +39,10 @@ export type WorkspaceSnapshot =
|
|
|
41
39
|
pending: { staged: PathSummary; unstaged: PathSummary; untracked: PathSummary; conflicted: PathSummary };
|
|
42
40
|
};
|
|
43
41
|
|
|
44
|
-
/** `WorkspaceSnapshot` as a schema
|
|
45
|
-
export const WorkspaceSnapshotSchema = Type.
|
|
46
|
-
[
|
|
47
|
-
|
|
48
|
-
Type.Object({ kind: Type.Literal("unknown"), reason: Type.String() }),
|
|
49
|
-
Type.Object({
|
|
50
|
-
kind: Type.Literal("git"),
|
|
51
|
-
position: PositionSchema,
|
|
52
|
-
head: Type.String(),
|
|
53
|
-
baseline: Type.String({ description: "The launch commit the changes are counted from." }),
|
|
54
|
-
ahead: Type.Number({ description: "Commits reachable from head but not from the baseline." }),
|
|
55
|
-
workingState: Type.Union([Type.Literal("clean"), Type.Literal("dirty")]),
|
|
56
|
-
committed: PathSummarySchema,
|
|
57
|
-
pending: Type.Object({
|
|
58
|
-
staged: PathSummarySchema,
|
|
59
|
-
unstaged: PathSummarySchema,
|
|
60
|
-
untracked: PathSummarySchema,
|
|
61
|
-
conflicted: PathSummarySchema,
|
|
62
|
-
}),
|
|
63
|
-
}),
|
|
64
|
-
],
|
|
65
|
-
{
|
|
66
|
-
description:
|
|
67
|
-
"The checkout observed now: git (changes since the launch commit, committed and pending), non-git, or unknown with a reason.",
|
|
68
|
-
},
|
|
42
|
+
/** `WorkspaceSnapshot` as a schema for `delegation_status`: the discriminant, the rest open. */
|
|
43
|
+
export const WorkspaceSnapshotSchema = Type.Object(
|
|
44
|
+
{ kind: Type.Union([Type.Literal("git"), Type.Literal("non-git"), Type.Literal("unknown")]) },
|
|
45
|
+
{ additionalProperties: true },
|
|
69
46
|
);
|
|
70
47
|
|
|
71
48
|
/** Reporting limits for one snapshot; never evidence for deleting anything. */
|
package/src/tracking-attempt.ts
CHANGED
|
@@ -1,46 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import { WorktreePlanSchema } from "./workspace.ts";
|
|
3
2
|
|
|
4
3
|
const ATTEMPT_PHASES = ["planned", "created", "rolled-back", "retained", "interrupted"] as const;
|
|
5
|
-
|
|
6
|
-
const ResourceSchema = Type.Union(
|
|
7
|
-
[
|
|
8
|
-
Type.Object({ kind: Type.Literal("none") }),
|
|
9
|
-
Type.Object({ kind: Type.Literal("branch"), branch: Type.String(), head: Type.String() }),
|
|
10
|
-
Type.Object({ kind: Type.Literal("worktree"), cwd: Type.String(), branch: Type.String(), head: Type.String() }),
|
|
11
|
-
Type.Object({
|
|
12
|
-
kind: Type.Literal("unknown"),
|
|
13
|
-
reason: Type.String(),
|
|
14
|
-
observedPaths: Type.Array(Type.String()),
|
|
15
|
-
observedRefs: Type.Array(Type.String()),
|
|
16
|
-
}),
|
|
17
|
-
],
|
|
18
|
-
{ description: "What is known to exist for a launch that has no window, as facts." },
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
const AttemptStateSchema = Type.Union([
|
|
22
|
-
Type.Object({ phase: Type.Literal("planned"), resources: ResourceSchema }),
|
|
23
|
-
Type.Object({ phase: Type.Literal("created"), resources: ResourceSchema }),
|
|
24
|
-
Type.Object({
|
|
25
|
-
phase: Type.Literal("rolled-back"),
|
|
26
|
-
resources: Type.Object({ kind: Type.Literal("none") }),
|
|
27
|
-
cause: Type.String(),
|
|
28
|
-
}),
|
|
29
|
-
Type.Object({
|
|
30
|
-
phase: Type.Literal("retained"),
|
|
31
|
-
resources: ResourceSchema,
|
|
32
|
-
cause: Type.String(),
|
|
33
|
-
recovery: Type.Array(Type.String(), {
|
|
34
|
-
description: "Non-forced commands that remove exactly the resources known to remain.",
|
|
35
|
-
}),
|
|
36
|
-
}),
|
|
37
|
-
Type.Object({
|
|
38
|
-
phase: Type.Literal("interrupted"),
|
|
39
|
-
resources: ResourceSchema,
|
|
40
|
-
lastPhase: Type.Union([Type.Literal("planned"), Type.Literal("created")]),
|
|
41
|
-
reason: Type.String(),
|
|
42
|
-
}),
|
|
43
|
-
]);
|
|
4
|
+
const PHASE = Type.Union(ATTEMPT_PHASES.map((phase) => Type.Literal(phase)));
|
|
44
5
|
|
|
45
6
|
/** A managed launch that has no window: never running, never closable, with no window id. */
|
|
46
7
|
export const AttemptSchema = Type.Object({
|
|
@@ -52,14 +13,8 @@ export const AttemptSchema = Type.Object({
|
|
|
52
13
|
thinking: Type.String(),
|
|
53
14
|
name: Type.Optional(Type.String()),
|
|
54
15
|
sessionDir: Type.String(),
|
|
55
|
-
plan:
|
|
56
|
-
attempt:
|
|
57
|
-
requestId: Type.Optional(Type.String(
|
|
58
|
-
state:
|
|
59
|
-
ATTEMPT_PHASES.map((phase) => Type.Literal(phase)),
|
|
60
|
-
{
|
|
61
|
-
description:
|
|
62
|
-
"planned, created: a launch in progress. rolled-back: failed before a window and everything was removed. retained: failed before a window and something remains (see attempt.recovery). interrupted: the parent ended mid-launch; what exists is unknown. No window was opened.",
|
|
63
|
-
},
|
|
64
|
-
),
|
|
16
|
+
plan: Type.Object({ branch: Type.String(), destination: Type.String() }, { additionalProperties: true }),
|
|
17
|
+
attempt: Type.Object({ phase: PHASE }, { additionalProperties: true }),
|
|
18
|
+
requestId: Type.Optional(Type.String()),
|
|
19
|
+
state: PHASE,
|
|
65
20
|
});
|
package/src/tracking.ts
CHANGED
|
@@ -71,46 +71,30 @@ const StartedSchema = Type.Object({
|
|
|
71
71
|
root: Type.Optional(Type.String()),
|
|
72
72
|
depth: Type.Optional(Type.Number()),
|
|
73
73
|
accounting: Type.Optional(Type.String()),
|
|
74
|
-
canResume: Type.Boolean(
|
|
75
|
-
description: "Direct-parent authority only; current admission and exited-session checks still apply.",
|
|
76
|
-
}),
|
|
74
|
+
canResume: Type.Boolean(),
|
|
77
75
|
kind: Type.Literal("started"),
|
|
78
76
|
id: Type.String(),
|
|
79
77
|
name: Type.String(),
|
|
80
78
|
agent: Type.String(),
|
|
81
|
-
model: Type.String(
|
|
82
|
-
finishedModel: Type.Optional(
|
|
83
|
-
Type.String({ description: "The model its result came from, read from its session file; done only." }),
|
|
84
|
-
),
|
|
79
|
+
model: Type.String(),
|
|
80
|
+
finishedModel: Type.Optional(Type.String()),
|
|
85
81
|
thinking: Type.String(),
|
|
86
82
|
autoExit: Type.Boolean(),
|
|
87
|
-
resumedFrom: Type.Optional(Type.String(
|
|
83
|
+
resumedFrom: Type.Optional(Type.String()),
|
|
88
84
|
windowId: Type.String(),
|
|
89
85
|
windowName: Type.String(),
|
|
90
|
-
windowSession: Type.Optional(
|
|
91
|
-
|
|
92
|
-
),
|
|
93
|
-
cwd: Type.Optional(
|
|
94
|
-
Type.String({ description: "The directory its window opened in; absent for records from before it was kept." }),
|
|
95
|
-
),
|
|
96
|
-
sessionDir: Type.Optional(Type.String({ description: "The session-storage directory it was told to use." })),
|
|
86
|
+
windowSession: Type.Optional(Type.String()),
|
|
87
|
+
cwd: Type.Optional(Type.String()),
|
|
88
|
+
sessionDir: Type.Optional(Type.String()),
|
|
97
89
|
workspace: Type.Optional(WorkspaceSchema),
|
|
98
90
|
workspaceSnapshot: WorkspaceSnapshotSchema,
|
|
99
|
-
state: Type.Union(
|
|
100
|
-
STARTED_STATES.map((state) => Type.Literal(state)),
|
|
101
|
-
{
|
|
102
|
-
description:
|
|
103
|
-
"running: window open, no result. done: a result arrived (see status, envelopePath). gone: the window went with no result; a late reply makes it done. closed: delegation_close recorded it. window-closed: the window is gone. unknown: tmux could not be asked. done and closed are final, though a later delegation_resume continues one as a new delegation; gone and window-closed turn done if a late reply arrives.",
|
|
104
|
-
},
|
|
105
|
-
),
|
|
91
|
+
state: Type.Union(STARTED_STATES.map((state) => Type.Literal(state))),
|
|
106
92
|
closed: Type.Boolean(),
|
|
107
93
|
closedWithoutResult: Type.Boolean(),
|
|
108
|
-
status: Type.Optional(Type.String(
|
|
94
|
+
status: Type.Optional(Type.String()),
|
|
109
95
|
envelopePath: Type.Optional(Type.String()),
|
|
110
96
|
sessionPath: Type.Optional(Type.String()),
|
|
111
|
-
lastActiveAt: Type.Optional(
|
|
112
|
-
Type.String({ description: "ISO time the session file was last written; running only." }),
|
|
113
|
-
),
|
|
97
|
+
lastActiveAt: Type.Optional(Type.String()),
|
|
114
98
|
});
|
|
115
99
|
|
|
116
100
|
const DelegationSchema = Type.Union([StartedSchema, AttemptSchema]);
|
|
@@ -297,10 +281,8 @@ function statusTool(results: Results, tmux: TmuxClient, readingErrors: () => Rea
|
|
|
297
281
|
name: "delegation_status",
|
|
298
282
|
label: "Delegation status",
|
|
299
283
|
description:
|
|
300
|
-
"
|
|
301
|
-
parameters: Type.Object({
|
|
302
|
-
id: Type.Optional(Type.String({ description: "One delegation id; omit to report every delegation." })),
|
|
303
|
-
}),
|
|
284
|
+
"Report this session's descendant delegations, or one by id: state, window, model, paths and workspace. Results arrive as messages on their own, so call it only for a delegation that seems stuck or to find its files. An open window does not mean the task is unfinished; only a result means done. gone and window-closed turn done if a late reply arrives.",
|
|
285
|
+
parameters: Type.Object({ id: Type.Optional(Type.String()) }),
|
|
304
286
|
outputSchema: StatusSchema,
|
|
305
287
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, ...CLOSED_WORLD },
|
|
306
288
|
async execute(
|
|
@@ -375,10 +357,8 @@ function closeTool(results: Results, tmux: TmuxClient, assertActive: () => void)
|
|
|
375
357
|
name: "delegation_close",
|
|
376
358
|
label: "Close delegation",
|
|
377
359
|
description:
|
|
378
|
-
"
|
|
379
|
-
parameters: Type.Object({
|
|
380
|
-
id: Type.String({ description: "The delegation id to close." }),
|
|
381
|
-
}),
|
|
360
|
+
"Close a descendant and its recorded subtree: fence new launches and kill its verified windows, keeping worktrees, branches and session files. Check closed and complete; partial failures stay fenced for a retry.",
|
|
361
|
+
parameters: Type.Object({ id: Type.String() }),
|
|
382
362
|
outputSchema: CloseSchema,
|
|
383
363
|
// Kills only verified windows; repeats retry unresolved members and preserve confirmed facts.
|
|
384
364
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, ...CLOSED_WORLD },
|
package/src/tree-close.ts
CHANGED
|
@@ -23,7 +23,7 @@ export const CloseSchema = Type.Object({
|
|
|
23
23
|
id: Type.String(),
|
|
24
24
|
windowId: Type.Optional(Type.String()),
|
|
25
25
|
windowName: Type.Optional(Type.String()),
|
|
26
|
-
closed: Type.Boolean(
|
|
26
|
+
closed: Type.Boolean(),
|
|
27
27
|
complete: Type.Boolean(),
|
|
28
28
|
alreadyClosed: Type.Boolean(),
|
|
29
29
|
killed: Type.Boolean(),
|
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
|
}
|
package/src/workspace.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
ProjectTrustStore,
|
|
19
19
|
} from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { Type } from "typebox";
|
|
21
|
-
import { askGit, type GitLaunch, inspectGit
|
|
21
|
+
import { askGit, type GitLaunch, inspectGit } from "./git.ts";
|
|
22
22
|
import { localTrust, type TrustDecision, type TrustResolver, trustChoices } from "./trust-prompt.ts";
|
|
23
23
|
import type { ManagedWorkspace } from "./worktree.ts";
|
|
24
24
|
|
|
@@ -41,69 +41,14 @@ export type DirectoryWorkspace = {
|
|
|
41
41
|
git: GitLaunch;
|
|
42
42
|
};
|
|
43
43
|
export type Workspace = DirectoryWorkspace | ManagedWorkspace;
|
|
44
|
-
/** `
|
|
45
|
-
export const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
mainRoot: Type.String(),
|
|
53
|
-
commonDir: Type.String(),
|
|
54
|
-
checkoutRoot: Type.String(),
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const CheckoutSchema = Type.Object({
|
|
58
|
-
kind: Type.Literal("git"),
|
|
59
|
-
identity: IdentitySchema,
|
|
60
|
-
position: PositionSchema,
|
|
61
|
-
head: Type.Union([Type.String(), Type.Null()]),
|
|
62
|
-
baseline: Type.Union([Type.String(), Type.Null()], {
|
|
63
|
-
description: "The commit checked out at launch; null when the checkout had no commit yet.",
|
|
64
|
-
}),
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const CWD_DESCRIPTION = "The directory the delegate's window opened in.";
|
|
68
|
-
|
|
69
|
-
/** `Workspace` as a schema, for the tools' structured outputs. */
|
|
70
|
-
export const WorkspaceSchema = Type.Union([
|
|
71
|
-
Type.Object({
|
|
72
|
-
mode: Type.Union([Type.Literal("inherited"), Type.Literal("existing")]),
|
|
73
|
-
cwd: Type.String({ description: CWD_DESCRIPTION }),
|
|
74
|
-
git: Type.Union(
|
|
75
|
-
[
|
|
76
|
-
CheckoutSchema,
|
|
77
|
-
Type.Object({ kind: Type.Literal("non-git") }),
|
|
78
|
-
Type.Object({ kind: Type.Literal("unknown"), reason: Type.String() }),
|
|
79
|
-
],
|
|
80
|
-
{ description: "The checkout at launch: git, non-git, or unknown when Git could not be asked." },
|
|
81
|
-
),
|
|
82
|
-
}),
|
|
83
|
-
Type.Object({
|
|
84
|
-
mode: Type.Literal("managed"),
|
|
85
|
-
cwd: Type.String({ description: CWD_DESCRIPTION }),
|
|
86
|
-
git: CheckoutSchema,
|
|
87
|
-
sourceCwd: Type.String({ description: "The checkout the worktree was created from." }),
|
|
88
|
-
source: IdentitySchema,
|
|
89
|
-
createdBranch: Type.String({ description: "The branch created for the worktree, as originally named." }),
|
|
90
|
-
requestedBase: Type.String(),
|
|
91
|
-
baseCommit: Type.String({ description: "The commit the branch was created at." }),
|
|
92
|
-
sourceDirty: PathSummarySchema,
|
|
93
|
-
}),
|
|
94
|
-
]);
|
|
95
|
-
|
|
96
|
-
/** `WorktreePlan` as a schema: what an attempt intended. */
|
|
97
|
-
export const WorktreePlanSchema = Type.Object({
|
|
98
|
-
sourceCwd: Type.String(),
|
|
99
|
-
source: IdentitySchema,
|
|
100
|
-
destination: Type.String(),
|
|
101
|
-
branch: Type.String(),
|
|
102
|
-
requestedBase: Type.String(),
|
|
103
|
-
baseCommit: Type.String(),
|
|
104
|
-
sourceDirty: PathSummarySchema,
|
|
105
|
-
trusted: Type.Boolean(),
|
|
106
|
-
});
|
|
44
|
+
/** `Workspace` as a schema for the tools' structured outputs: the discriminant and directory, the rest open to keep tool declarations short. */
|
|
45
|
+
export const WorkspaceSchema = Type.Object(
|
|
46
|
+
{
|
|
47
|
+
mode: Type.Union([Type.Literal("inherited"), Type.Literal("existing"), Type.Literal("managed")]),
|
|
48
|
+
cwd: Type.String(),
|
|
49
|
+
},
|
|
50
|
+
{ additionalProperties: true },
|
|
51
|
+
);
|
|
107
52
|
|
|
108
53
|
/** Optional inputs some models fill with these instead of leaving them out. */
|
|
109
54
|
const OMITTED = new Set(["", "null", "none", "false", "undefined"]);
|