pi-squire 0.5.1 → 0.7.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 +32 -1
- package/README.md +7 -3
- package/package.json +1 -1
- package/src/index.ts +21 -0
- package/src/tracking.ts +77 -12
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,35 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.7.0] - 2026-10-19
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `delegate`, `delegation_status` and `delegation_close` declare an
|
|
14
|
+
`outputSchema` and return matching `structuredContent`, so a codemode
|
|
15
|
+
script gets objects instead of text. `delegation_status` gives each
|
|
16
|
+
delegation its derived `state` (`running`, `done`, `gone`, `closed`,
|
|
17
|
+
`window-closed` or `unknown`), also added to its details.
|
|
18
|
+
`delegation_close` returns one shape on every path:
|
|
19
|
+
`{ id, windowId, windowName, closed, alreadyClosed, killed }`; its details
|
|
20
|
+
gain the fields each path lacked. The model-facing text is unchanged.
|
|
21
|
+
- The three tools declare MCP-style `annotations`: `delegation_status` is
|
|
22
|
+
read-only and idempotent; `delegation_close` is destructive and idempotent;
|
|
23
|
+
`delegate` is neither, and is the only one that reaches an open world,
|
|
24
|
+
since the delegate it starts calls a remote model and runs tools
|
|
25
|
+
unattended.
|
|
26
|
+
|
|
27
|
+
## [0.6.0] - 2026-10-15
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- Every delegate now starts with `PI_UNATTENDED=1` in its environment, a
|
|
32
|
+
general signal that no human is watching the session. Delegates run as a
|
|
33
|
+
full TUI, so extensions that open a dialog, such as a handoff gate or an
|
|
34
|
+
approval prompt, waited on nobody and stalled the delegate; they can read
|
|
35
|
+
this variable to skip the dialog instead. The test that pins the child's
|
|
36
|
+
exact environment now includes it.
|
|
37
|
+
|
|
9
38
|
## [0.5.1] - 2026-10-14
|
|
10
39
|
|
|
11
40
|
### Changed
|
|
@@ -228,7 +257,9 @@ author's dotfiles; the changes below are against that copy.
|
|
|
228
257
|
|
|
229
258
|
- The published file list includes `child.ts`, which `index.ts` imports.
|
|
230
259
|
|
|
231
|
-
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.
|
|
260
|
+
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.7.0...HEAD
|
|
261
|
+
[0.7.0]: https://github.com/gvanderclay/pi-squire/compare/v0.6.0...v0.7.0
|
|
262
|
+
[0.6.0]: https://github.com/gvanderclay/pi-squire/compare/v0.5.1...v0.6.0
|
|
232
263
|
[0.5.1]: https://github.com/gvanderclay/pi-squire/compare/v0.5.0...v0.5.1
|
|
233
264
|
[0.5.0]: https://github.com/gvanderclay/pi-squire/compare/v0.4.0...v0.5.0
|
|
234
265
|
[0.4.0]: https://github.com/gvanderclay/pi-squire/compare/v0.3.2...v0.4.0
|
package/README.md
CHANGED
|
@@ -178,7 +178,11 @@ variable of its own. `PI_DELEGATE_PARENT` and `PI_DELEGATE_AUTO_EXIT` are set
|
|
|
178
178
|
for the child, never read as configuration by the parent. The window starts
|
|
179
179
|
with the tmux server's environment plus only these from the parent:
|
|
180
180
|
`PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR` (when the parent has it
|
|
181
|
-
set), `PI_DELEGATE_PARENT` and `PI_DELEGATE_AUTO_EXIT
|
|
181
|
+
set), `PI_DELEGATE_PARENT` and `PI_DELEGATE_AUTO_EXIT`, and it always gets `PI_UNATTENDED=1`.
|
|
182
|
+
`PI_UNATTENDED=1` is a general signal, not specific to pi-squire: no human is
|
|
183
|
+
watching this session, even though it runs in a TUI. Other extensions may read
|
|
184
|
+
it to skip a dialog or confirmation instead of waiting on one, and any other
|
|
185
|
+
runner that starts Pi unattended can set it too. Anything else the
|
|
182
186
|
parent process has, such as a `CLAUDE_CONFIG_DIR` set by a shell alias,
|
|
183
187
|
reaches the child only through a `session:launch` contributor. `--model` and
|
|
184
188
|
`--thinking` override the agent's defaults for one call, and an unknown value
|
|
@@ -557,8 +561,8 @@ Other extensions may rely on:
|
|
|
557
561
|
- the `[delegate]` result text and the closed-without-a-result text;
|
|
558
562
|
- `AGENT.md`'s frontmatter keys and its location,
|
|
559
563
|
`<agent dir>/agents/<name>/AGENT.md`;
|
|
560
|
-
- the child environment variables `PI_DELEGATE_PARENT
|
|
561
|
-
`PI_DELEGATE_AUTO_EXIT`.
|
|
564
|
+
- the child environment variables `PI_DELEGATE_PARENT`,
|
|
565
|
+
`PI_DELEGATE_AUTO_EXIT` and `PI_UNATTENDED=1`.
|
|
562
566
|
|
|
563
567
|
A `message:*` provider must:
|
|
564
568
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -99,6 +99,22 @@ function parentTrust(parent: string): string {
|
|
|
99
99
|
/** The flags `/delegate` takes before the task. */
|
|
100
100
|
const FLAGS = ["--model", "--thinking", "--label", "--auto-exit", "--no-auto-exit"] as const;
|
|
101
101
|
|
|
102
|
+
/** The `delegate` tool's structured result, which is also its details. */
|
|
103
|
+
export const DelegateSchema = Type.Object({
|
|
104
|
+
outcome: Type.Literal("started"),
|
|
105
|
+
id: Type.String({ description: "The delegation id that delegation_status and delegation_close take." }),
|
|
106
|
+
agent: Type.String(),
|
|
107
|
+
model: Type.String({ description: "The provider/id it started on, after any fallback." }),
|
|
108
|
+
skipped: Type.Array(Type.Object({ model: Type.String(), reason: Type.String() }), {
|
|
109
|
+
description: "Earlier models in the fallback list that were passed over, and why.",
|
|
110
|
+
}),
|
|
111
|
+
notices: Type.Array(Type.String()),
|
|
112
|
+
thinking: Type.String(),
|
|
113
|
+
autoExit: Type.Boolean(),
|
|
114
|
+
windowId: Type.String(),
|
|
115
|
+
windowName: Type.String(),
|
|
116
|
+
});
|
|
117
|
+
|
|
102
118
|
const NOT_IN_TMUX = "this session is not inside tmux; start it in a tmux pane to open a delegate window";
|
|
103
119
|
|
|
104
120
|
const USAGE =
|
|
@@ -469,6 +485,8 @@ export default function delegate(
|
|
|
469
485
|
PI_CODING_AGENT_DIR: getAgentDir(),
|
|
470
486
|
[PARENT_ENV]: parent,
|
|
471
487
|
[AUTO_EXIT_ENV]: start.autoExit ? "1" : "0",
|
|
488
|
+
// A general signal for any extension: no human watches this session, so don't wait on a dialog.
|
|
489
|
+
PI_UNATTENDED: "1",
|
|
472
490
|
};
|
|
473
491
|
if (start.rest.length > 0) env[FALLBACK_ENV] = start.rest.join(",");
|
|
474
492
|
const sessionDir = process.env[SESSION_DIR_ENV];
|
|
@@ -549,6 +567,9 @@ export default function delegate(
|
|
|
549
567
|
}),
|
|
550
568
|
),
|
|
551
569
|
}),
|
|
570
|
+
outputSchema: DelegateSchema,
|
|
571
|
+
// Opens a new window each call and removes nothing. Open world: the delegate calls a remote model and runs its agent's tools unattended.
|
|
572
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
552
573
|
async execute(
|
|
553
574
|
_toolCallId: string,
|
|
554
575
|
params: {
|
package/src/tracking.ts
CHANGED
|
@@ -37,20 +37,76 @@ export type Tracking = {
|
|
|
37
37
|
stop(): void;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
/** The one result every tool returns. */
|
|
40
|
+
/** The one result every tool returns. `details` doubles as `structuredContent`, so it must match the tool's `outputSchema`; Pi does not check it. */
|
|
41
41
|
export function toolResult(
|
|
42
42
|
payload: string,
|
|
43
|
-
details: unknown
|
|
44
|
-
): { content: { type: "text"; text: string }[]; details: unknown } {
|
|
45
|
-
return { content: [{ type: "text", text: payload }], details };
|
|
43
|
+
details: unknown,
|
|
44
|
+
): { content: { type: "text"; text: string }[]; details: unknown; structuredContent: unknown } {
|
|
45
|
+
return { content: [{ type: "text", text: payload }], details, structuredContent: details };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** Every tool here touches only local state: tmux, the session's entries and files under the agent directory. */
|
|
49
|
+
const CLOSED_WORLD = { openWorldHint: false } as const;
|
|
50
|
+
|
|
51
|
+
const DELEGATION_STATES: readonly DelegationState[] = ["closed", "done", "gone", "running", "window-closed", "unknown"];
|
|
52
|
+
|
|
53
|
+
/** One delegation in `delegation_status`'s structured result; `viewDetails` builds it. */
|
|
54
|
+
const DelegationSchema = Type.Object({
|
|
55
|
+
id: Type.String(),
|
|
56
|
+
name: Type.String(),
|
|
57
|
+
agent: Type.String(),
|
|
58
|
+
model: Type.String(),
|
|
59
|
+
thinking: Type.String(),
|
|
60
|
+
autoExit: Type.Boolean(),
|
|
61
|
+
windowId: Type.String(),
|
|
62
|
+
windowName: Type.String(),
|
|
63
|
+
state: Type.Union(
|
|
64
|
+
DELEGATION_STATES.map((state) => Type.Literal(state)),
|
|
65
|
+
{
|
|
66
|
+
description:
|
|
67
|
+
"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; gone and window-closed turn done if a late reply arrives.",
|
|
68
|
+
},
|
|
69
|
+
),
|
|
70
|
+
closed: Type.Boolean(),
|
|
71
|
+
closedWithoutResult: Type.Boolean(),
|
|
72
|
+
status: Type.Optional(Type.String({ description: "The result envelope's status: done, failed or stopped." })),
|
|
73
|
+
envelopePath: Type.Optional(Type.String()),
|
|
74
|
+
sessionPath: Type.Optional(Type.String()),
|
|
75
|
+
lastActiveAt: Type.Optional(
|
|
76
|
+
Type.String({ description: "ISO time the session file was last written; running only." }),
|
|
77
|
+
),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export const StatusSchema = Type.Object({
|
|
81
|
+
delegations: Type.Array(DelegationSchema),
|
|
82
|
+
marks: Type.Array(
|
|
83
|
+
Type.Object({
|
|
84
|
+
scope: Type.String(),
|
|
85
|
+
source: Type.Union([Type.Literal("reactive"), Type.Literal("proactive")]),
|
|
86
|
+
reason: Type.String(),
|
|
87
|
+
clearsAt: Type.String(),
|
|
88
|
+
}),
|
|
89
|
+
),
|
|
90
|
+
readingErrors: Type.Array(Type.Object({ provider: Type.String(), reason: Type.String(), at: Type.String() })),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const CloseSchema = Type.Object({
|
|
94
|
+
id: Type.String(),
|
|
95
|
+
windowId: Type.String(),
|
|
96
|
+
windowName: Type.String(),
|
|
97
|
+
closed: Type.Literal(true),
|
|
98
|
+
alreadyClosed: Type.Boolean({ description: "An earlier delegation_close had recorded it; nothing was done." }),
|
|
99
|
+
killed: Type.Boolean({ description: "This call killed the delegate's window." }),
|
|
100
|
+
});
|
|
101
|
+
|
|
48
102
|
/** A tool as registered; `execute` takes the tool's own parameters, which the registration call does not check. */
|
|
49
103
|
type ToolDef = {
|
|
50
104
|
name: string;
|
|
51
105
|
label: string;
|
|
52
106
|
description: string;
|
|
53
107
|
parameters: unknown;
|
|
108
|
+
outputSchema: unknown;
|
|
109
|
+
annotations: { readOnlyHint: boolean; destructiveHint: boolean; idempotentHint: boolean; openWorldHint: boolean };
|
|
54
110
|
execute: (...args: never[]) => Promise<ReturnType<typeof toolResult>>;
|
|
55
111
|
};
|
|
56
112
|
|
|
@@ -149,7 +205,7 @@ function viewText({ record, state, name, sessionPath, lastActive }: View): strin
|
|
|
149
205
|
}
|
|
150
206
|
|
|
151
207
|
/** The structured details for one delegation. */
|
|
152
|
-
function viewDetails({ record, name, sessionPath, lastActive }: View): Record<string, unknown> {
|
|
208
|
+
function viewDetails({ record, state, name, sessionPath, lastActive }: View): Record<string, unknown> {
|
|
153
209
|
return {
|
|
154
210
|
id: record.id,
|
|
155
211
|
name,
|
|
@@ -159,6 +215,7 @@ function viewDetails({ record, name, sessionPath, lastActive }: View): Record<st
|
|
|
159
215
|
autoExit: record.autoExit === true,
|
|
160
216
|
windowId: record.windowId,
|
|
161
217
|
windowName: record.windowName,
|
|
218
|
+
state,
|
|
162
219
|
closed: record.closed === true,
|
|
163
220
|
closedWithoutResult: record.gone === true && record.result === undefined && record.closed !== true,
|
|
164
221
|
status: record.result?.status,
|
|
@@ -267,6 +324,8 @@ function statusTool(results: Results, tmux: TmuxClient, readingErrors: () => Rea
|
|
|
267
324
|
parameters: Type.Object({
|
|
268
325
|
id: Type.Optional(Type.String({ description: "One delegation id; omit to report every delegation." })),
|
|
269
326
|
}),
|
|
327
|
+
outputSchema: StatusSchema,
|
|
328
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, ...CLOSED_WORLD },
|
|
270
329
|
async execute(
|
|
271
330
|
_toolCallId: string,
|
|
272
331
|
params: { id?: string },
|
|
@@ -310,6 +369,9 @@ function closeTool(results: Results, tmux: TmuxClient): ToolDef {
|
|
|
310
369
|
parameters: Type.Object({
|
|
311
370
|
id: Type.String({ description: "The delegation id to close." }),
|
|
312
371
|
}),
|
|
372
|
+
outputSchema: CloseSchema,
|
|
373
|
+
// Kills a window; a second close finds it recorded and does nothing.
|
|
374
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, ...CLOSED_WORLD },
|
|
313
375
|
async execute(
|
|
314
376
|
_toolCallId: string,
|
|
315
377
|
params: { id: string },
|
|
@@ -318,8 +380,15 @@ function closeTool(results: Results, tmux: TmuxClient): ToolDef {
|
|
|
318
380
|
_ctx: ExtensionContext,
|
|
319
381
|
): Promise<ReturnType<typeof toolResult>> {
|
|
320
382
|
const [record] = selectRecords(results, params.id);
|
|
321
|
-
|
|
322
|
-
|
|
383
|
+
const closed = (alreadyClosed: boolean, killed: boolean) => ({
|
|
384
|
+
id: record.id,
|
|
385
|
+
windowId: record.windowId,
|
|
386
|
+
windowName: record.windowName,
|
|
387
|
+
closed: true,
|
|
388
|
+
alreadyClosed,
|
|
389
|
+
killed,
|
|
390
|
+
});
|
|
391
|
+
if (record.closed === true) return toolResult(`Delegation ${record.id} is already closed.`, closed(true, false));
|
|
323
392
|
let windows: TmuxWindow[];
|
|
324
393
|
try {
|
|
325
394
|
windows = await tmux.listWindows();
|
|
@@ -339,11 +408,7 @@ function closeTool(results: Results, tmux: TmuxClient): ToolDef {
|
|
|
339
408
|
const how = alive
|
|
340
409
|
? `killed window ${record.windowName} (${record.windowId})`
|
|
341
410
|
: `its window ${record.windowName} was already gone`;
|
|
342
|
-
return toolResult(`Closed delegation ${record.id}: ${how}.`,
|
|
343
|
-
id: record.id,
|
|
344
|
-
windowId: record.windowId,
|
|
345
|
-
killed: alive,
|
|
346
|
-
});
|
|
411
|
+
return toolResult(`Closed delegation ${record.id}: ${how}.`, closed(false, alive));
|
|
347
412
|
},
|
|
348
413
|
};
|
|
349
414
|
}
|