pi-squire 0.18.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 +18 -1
- package/README.md +5 -5
- package/package.json +1 -1
- package/src/git.ts +0 -9
- package/src/index.ts +27 -70
- 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/workspace.ts +9 -64
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ 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
|
+
|
|
9
25
|
## [0.18.0] - 2026-11-01
|
|
10
26
|
|
|
11
27
|
### Changed
|
|
@@ -1008,7 +1024,8 @@ author's dotfiles; the changes below are against that copy.
|
|
|
1008
1024
|
|
|
1009
1025
|
- The published file list includes `child.ts`, which `index.ts` imports.
|
|
1010
1026
|
|
|
1011
|
-
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.18.
|
|
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
|
|
1012
1029
|
[0.18.0]: https://github.com/gvanderclay/pi-squire/compare/v0.17.0...v0.18.0
|
|
1013
1030
|
[0.17.0]: https://github.com/gvanderclay/pi-squire/compare/v0.16.0...v0.17.0
|
|
1014
1031
|
[0.16.0]: https://github.com/gvanderclay/pi-squire/compare/v0.15.0...v0.16.0
|
package/README.md
CHANGED
|
@@ -1063,14 +1063,14 @@ limit, it resolves each entry the way Pi resolves `--model`: an exact
|
|
|
1063
1063
|
order the entries are listed. No entry may be a `*` pattern: a `model` or
|
|
1064
1064
|
`fallback` entry with `*` makes the file invalid, and the agent is disabled.
|
|
1065
1065
|
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
|
1068
1068
|
session, and that message and every later one from the same address are
|
|
1069
1069
|
instructions to follow as given. Other sessions are colleagues: the delegate
|
|
1070
1070
|
may answer them, but checks with the parent before doing work the parent did
|
|
1071
|
-
not ask for.
|
|
1072
|
-
|
|
1073
|
-
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.
|
|
1074
1074
|
|
|
1075
1075
|
An agent file that cannot be used is invalid, and its agent is disabled:
|
|
1076
1076
|
frontmatter that is not readable YAML or not a set of fields, an `enabled` or
|
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
|
@@ -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,
|
|
@@ -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/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"]);
|