pi-squad 0.16.6 → 0.17.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/README.md +26 -3
- package/docs/behavioral-contract-exact-targeting-review-suspended-attention.md +204 -0
- package/docs/file-spec-and-full-read-attestation-contract.md +412 -0
- package/docs/fleet-bridge.md +79 -0
- package/package.json +2 -1
- package/src/agent-pool.ts +16 -4
- package/src/file-spec.ts +451 -0
- package/src/index.ts +209 -83
- package/src/panel/squad-widget.ts +16 -7
- package/src/panel/task-list.ts +10 -0
- package/src/presentation.ts +38 -0
- package/src/protocol.ts +21 -0
- package/src/review.ts +19 -6
- package/src/scheduler.ts +211 -18
- package/src/skills/squad-supervisor/SKILL.md +6 -1
- package/src/store.ts +39 -2
- package/src/types.ts +18 -1
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
# File-based squad specification and full-read attestation contract
|
|
2
|
+
|
|
3
|
+
Status: implementation contract (v1)
|
|
4
|
+
Target: pi-squad on Pi 0.80.10
|
|
5
|
+
Non-goal: session compaction, summarization, semantic truncation, or proof based on prompts/`grep`/ordinary `read`.
|
|
6
|
+
|
|
7
|
+
## 1. Decisions and invariants
|
|
8
|
+
|
|
9
|
+
1. `squad` gains a mutually exclusive file form: `{ specFile, specSha256 }`. The call does not repeat the goal, tasks, descriptions, source, logs, or Base64.
|
|
10
|
+
2. The caller's SHA-256 is over the **original file bytes**. Those validated bytes, unchanged, become the canonical spec. “Canonical” means the immutable authoritative copy, not JSON reserialization.
|
|
11
|
+
3. A file squad is not schedulable until the source and referenced artifacts validate and the canonical copy plus squad/task state have been atomically published.
|
|
12
|
+
4. Every non-cancelled task in a file squad, including later-added, rework, and retest tasks, requires a valid task-owned read attestation for the squad's one canonical spec before it can become `done`.
|
|
13
|
+
5. A file child starts with a small manifest/bootstrap prompt, not duplicated goal/task contract text. Before attestation, the only executable tool is the child-only `squad_spec_read` tool.
|
|
14
|
+
6. Reading may occur in any chunk order. Completion requires the exact deterministic set once; duplicate valid reads are harmless. Missing, malformed, or tampered chunks never advance coverage.
|
|
15
|
+
7. Legacy inline squads have no `squad.spec`; no reader, guard, or attestation is required. Existing task sessions and mailbox behavior remain unchanged.
|
|
16
|
+
8. No layer silently truncates contract bytes, tool results, task output, or mail. A limit violation is an error directing the caller to artifact references.
|
|
17
|
+
|
|
18
|
+
The transport motivation is the observed 1.34–1.96 MB provider requests and failed large inline squad call described in [[pi-openai-codex-websocket-errors-large-sessions-20260717]].
|
|
19
|
+
|
|
20
|
+
## 2. Public `squad` call schema
|
|
21
|
+
|
|
22
|
+
The registered TypeBox schema MUST be equivalent to this JSON Schema. The two forms are exclusive (`additionalProperties: false` on each branch).
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
27
|
+
"oneOf": [
|
|
28
|
+
{
|
|
29
|
+
"type": "object",
|
|
30
|
+
"additionalProperties": false,
|
|
31
|
+
"required": ["goal"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"goal": { "type": "string" },
|
|
34
|
+
"agents": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"additionalProperties": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"additionalProperties": false,
|
|
39
|
+
"properties": {
|
|
40
|
+
"model": { "type": "string" },
|
|
41
|
+
"thinking": { "enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max"] }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"tasks": {
|
|
46
|
+
"type": "array",
|
|
47
|
+
"items": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"additionalProperties": false,
|
|
50
|
+
"required": ["id", "title", "agent"],
|
|
51
|
+
"properties": {
|
|
52
|
+
"id": { "type": "string" },
|
|
53
|
+
"title": { "type": "string" },
|
|
54
|
+
"description": { "type": "string" },
|
|
55
|
+
"agent": { "type": "string" },
|
|
56
|
+
"depends": { "type": "array", "items": { "type": "string" } },
|
|
57
|
+
"inheritContext": { "type": "boolean" }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"config": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"additionalProperties": false,
|
|
64
|
+
"properties": { "maxConcurrency": { "type": "integer", "minimum": 1 } }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"type": "object",
|
|
70
|
+
"additionalProperties": false,
|
|
71
|
+
"required": ["specFile", "specSha256"],
|
|
72
|
+
"properties": {
|
|
73
|
+
"specFile": { "type": "string", "minLength": 1 },
|
|
74
|
+
"specSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Uppercase hashes are rejected rather than normalized. The file form never invokes the planner and does not remap unknown agents.
|
|
82
|
+
|
|
83
|
+
## 3. Specification schema (`schemaVersion: 1`)
|
|
84
|
+
|
|
85
|
+
Validation is strict. JSON duplicate object keys, a BOM, invalid UTF-8, non-finite/non-JSON values, trailing non-whitespace, and unknown properties are malformed. A duplicate-key-aware parser is required; `JSON.parse` alone is insufficient.
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
90
|
+
"type": "object",
|
|
91
|
+
"additionalProperties": false,
|
|
92
|
+
"required": ["schemaVersion", "goal", "tasks", "agents", "config", "artifacts"],
|
|
93
|
+
"properties": {
|
|
94
|
+
"schemaVersion": { "const": 1 },
|
|
95
|
+
"goal": { "type": "string", "minLength": 1 },
|
|
96
|
+
"tasks": {
|
|
97
|
+
"type": "array",
|
|
98
|
+
"minItems": 1,
|
|
99
|
+
"maxItems": 128,
|
|
100
|
+
"items": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"additionalProperties": false,
|
|
103
|
+
"required": ["id", "title", "description", "agent", "depends", "inheritContext", "artifactRefs"],
|
|
104
|
+
"properties": {
|
|
105
|
+
"id": { "type": "string", "pattern": "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$" },
|
|
106
|
+
"title": { "type": "string", "minLength": 1 },
|
|
107
|
+
"description": { "type": "string" },
|
|
108
|
+
"agent": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$" },
|
|
109
|
+
"depends": { "type": "array", "uniqueItems": true, "items": { "type": "string" } },
|
|
110
|
+
"inheritContext": { "type": "boolean" },
|
|
111
|
+
"artifactRefs": { "type": "array", "uniqueItems": true, "items": { "type": "string" } }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"agents": {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"maxProperties": 64,
|
|
118
|
+
"propertyNames": { "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$" },
|
|
119
|
+
"additionalProperties": {
|
|
120
|
+
"type": "object",
|
|
121
|
+
"additionalProperties": false,
|
|
122
|
+
"required": ["model", "thinking"],
|
|
123
|
+
"properties": {
|
|
124
|
+
"model": { "type": ["string", "null"] },
|
|
125
|
+
"thinking": { "enum": [null, "off", "minimal", "low", "medium", "high", "xhigh", "max"] }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"config": {
|
|
130
|
+
"type": "object",
|
|
131
|
+
"additionalProperties": false,
|
|
132
|
+
"required": ["maxConcurrency", "autoUnblock", "maxRetries"],
|
|
133
|
+
"properties": {
|
|
134
|
+
"maxConcurrency": { "type": "integer", "minimum": 1, "maximum": 64 },
|
|
135
|
+
"autoUnblock": { "type": "boolean" },
|
|
136
|
+
"maxRetries": { "type": "integer", "minimum": 0, "maximum": 20 }
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"artifacts": {
|
|
140
|
+
"type": "array",
|
|
141
|
+
"maxItems": 1024,
|
|
142
|
+
"items": {
|
|
143
|
+
"type": "object",
|
|
144
|
+
"additionalProperties": false,
|
|
145
|
+
"required": ["id", "path", "sha256", "bytes", "purpose"],
|
|
146
|
+
"properties": {
|
|
147
|
+
"id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$" },
|
|
148
|
+
"path": { "type": "string", "minLength": 1 },
|
|
149
|
+
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
150
|
+
"bytes": { "type": "integer", "minimum": 0 },
|
|
151
|
+
"purpose": { "type": "string", "minLength": 1 },
|
|
152
|
+
"mediaType": { "type": "string", "minLength": 1 }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Additional semantic validation:
|
|
161
|
+
|
|
162
|
+
- Task IDs and artifact IDs are unique. Dependencies name existing tasks, exclude self, and form a DAG. Every task agent is a key in `agents` and resolves to a present, enabled agent definition; no fallback/remap.
|
|
163
|
+
- Every `artifactRefs` value names an artifact. Unreferenced artifacts are allowed for squad-wide context.
|
|
164
|
+
- `agents` MUST contain every assigned agent; extra valid roster entries are allowed.
|
|
165
|
+
- Artifact paths contain no NUL and are absolute under the host platform's native rules. Windows accepts drive-rooted and UNC absolute paths, rejects drive-relative paths; POSIX requires `/`. Preserve the exact string in spec bytes; use `path.normalize` only for access/comparison, never rewrite the canonical bytes.
|
|
166
|
+
- `reviewOnComplete` is intentionally absent and remains unconditionally true in persisted runtime config.
|
|
167
|
+
- For file squads, later `squad_modify add_task` IDs MUST satisfy the same task-ID pattern before any path construction. Generated rework/retest IDs must also be validated for the pattern/length (reject rather than truncate or collide).
|
|
168
|
+
- Run existing `validatePlan` structural checks. Its stylistic rules may produce warnings, but schema, graph, identity, size, and hash errors are fatal.
|
|
169
|
+
|
|
170
|
+
## 4. Size and artifact-reference policy
|
|
171
|
+
|
|
172
|
+
All limits are UTF-8 byte limits, checked before scheduling:
|
|
173
|
+
|
|
174
|
+
| Item | Limit |
|
|
175
|
+
|---|---:|
|
|
176
|
+
| canonical spec file | 1,048,576 bytes |
|
|
177
|
+
| `goal` | 65,536 bytes |
|
|
178
|
+
| one task `title` | 1,024 bytes |
|
|
179
|
+
| one task `description` | 131,072 bytes |
|
|
180
|
+
| cumulative `goal` + titles + descriptions + artifact purposes | 524,288 bytes |
|
|
181
|
+
| one artifact path | 32,768 bytes |
|
|
182
|
+
| tasks / agents / artifacts | 128 / 64 / 1024 |
|
|
183
|
+
|
|
184
|
+
Logs, source trees, binaries, images, generated fixtures, and Base64/data-URI payloads do not belong in text fields. Fields exceeding a limit MUST be moved to one or more `artifacts` entries with exact absolute path, byte length, and SHA-256. Reject over-limit input with the measured and allowed byte counts; never slice it. Reject a Base64/data-URI-like run longer than 4,096 characters in `goal`, `title`, `description`, or `purpose`, even when the total field is under its cap. Ordinary prose/source-like text is not rejected merely for being long.
|
|
185
|
+
|
|
186
|
+
At creation, every artifact is opened and verified as a regular, non-symlink file using the same stable-read rules as the source spec. A missing, changed, byte-length-mismatched, or hash-mismatched artifact rejects creation. Artifacts remain external and are not copied. Consumers must hash an artifact immediately before use; a later mutation is an explicit integrity error, never permission to use different bytes.
|
|
187
|
+
|
|
188
|
+
## 5. Stable source read and atomic publication
|
|
189
|
+
|
|
190
|
+
### Source and artifact reads
|
|
191
|
+
|
|
192
|
+
For each input path:
|
|
193
|
+
|
|
194
|
+
1. Resolve a relative `specFile` against the main tool call's `ctx.cwd`; artifact paths must already be absolute. Reject NUL.
|
|
195
|
+
2. `lstat` the final component and reject symbolic links/reparse-point links and non-regular files. Open read-only with `O_NOFOLLOW` where supported. On platforms without it, open then compare pre-open `lstat` identity to `fstat` identity; reject uncertainty or mismatch.
|
|
196
|
+
3. Record identity, size, mtime/ctime, read all bytes from the open descriptor with no library truncation, then `fstat` again. Reject if identity, size, mtime, or ctime changed. Hash the bytes read and compare byte count and lowercase SHA-256.
|
|
197
|
+
4. Decode the spec with a fatal UTF-8 decoder and validate it. Do not reopen by pathname during this operation.
|
|
198
|
+
|
|
199
|
+
Opening the descriptor pins the object against path replacement; the before/after metadata check detects in-place writes. A final symlink is never followed. Parent-directory symlinks are permitted for a user-supplied source path because the opened identity is pinned; all destination components are controlled by pi-squad.
|
|
200
|
+
|
|
201
|
+
### Destination
|
|
202
|
+
|
|
203
|
+
- Add `Squad.spec?: { schemaVersion: 1; sha256: string; bytes: number; path: string; chunkBytes: 32768; chunkCount: number }` where `path` is the absolute canonical path.
|
|
204
|
+
- Derive the squad ID with existing `makeTaskId(goal)` only when it is nonempty and path-safe; otherwise use `squad-<first 12 spec-hash hex>`. Resolve collisions with the existing unique suffix before staging. The final ID must match `^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$` and its resolved directory must be an immediate child of the squad root.
|
|
205
|
+
- Canonical location: `<squad-root>/<squad-id>/spec/spec.v1.json`. After resolving/normalizing, assert it remains beneath that exact squad directory.
|
|
206
|
+
- Build the entire new squad under a sibling staging directory created with exclusive semantics, e.g. `<squad-id>.creating.<uuid>`, mode `0700` where supported. Write canonical bytes with `wx`/mode `0600`, fsync the file, write squad/task JSON atomically, fsync the staging directories, then rename the staging directory to the not-yet-existing final squad directory. Never schedule from staging.
|
|
207
|
+
- If final ID exists, choose the unique ID before staging. Never merge or overwrite. Clean abandoned staging directories only after proving their name/shape and that no published squad references them.
|
|
208
|
+
- After publication, make the canonical file read-only (`0400`/best effort on Windows). Immutability is enforced primarily by exclusive creation and by verifying `bytes` and SHA-256 on every reader/attestation/settlement path. Any mismatch quarantines the squad from scheduling/acceptance.
|
|
209
|
+
- Preserve original bytes exactly, including insignificant JSON whitespace and final newline. `specSha256` and `Squad.spec.sha256` therefore identify exactly what children receive.
|
|
210
|
+
|
|
211
|
+
Creation failures leave no discoverable squad and start no scheduler/child. Error results identify the class (`SPEC_MALFORMED`, `SPEC_HASH_MISMATCH`, `SPEC_UNSTABLE`, `SPEC_TOO_LARGE`, `ARTIFACT_*`, `PLAN_INVALID`, `PUBLISH_FAILED`) without leaking file contents.
|
|
212
|
+
|
|
213
|
+
## 6. Child environment, loading, and bootstrap
|
|
214
|
+
|
|
215
|
+
For a file squad, `AgentPool.spawn` supplies:
|
|
216
|
+
|
|
217
|
+
```text
|
|
218
|
+
PI_SQUAD_CHILD=1
|
|
219
|
+
PI_SQUAD_ID=<exact squad id>
|
|
220
|
+
PI_SQUAD_TASK_ID=<exact task id>
|
|
221
|
+
PI_SQUAD_SPEC_PATH=<absolute canonical path>
|
|
222
|
+
PI_SQUAD_SPEC_SHA256=<64 lowercase hex>
|
|
223
|
+
PI_SQUAD_SPEC_BYTES=<decimal byte length>
|
|
224
|
+
PI_SQUAD_SPEC_CHUNK_BYTES=32768
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Do not place spec content in argv or environment. Values originate from persisted state, not a prompt. The package extension entry behaves as follows:
|
|
228
|
+
|
|
229
|
+
- main process: register existing squad tools;
|
|
230
|
+
- child without all spec variables (legacy): return exactly as today;
|
|
231
|
+
- child with all spec variables: register only the non-recursive reader and guard; never register `squad`, squad commands, scheduler, widget, or nested orchestration.
|
|
232
|
+
|
|
233
|
+
Pi 0.80.10 package loading must remain sufficient; do not require a global settings edit. If an agent definition has a `--tools` allowlist, force-add `squad_spec_read` for a file child. The child prompt contains only the manifest (IDs, hash, bytes, chunk count), reader usage, and dynamic non-spec messages. `buildSquadProtocol`, `buildTaskSection`, and the first RPC prompt MUST NOT inline `goal`, task descriptions, sibling descriptions, or other spec fields in file mode.
|
|
234
|
+
|
|
235
|
+
A task added after creation or generated by rework/retest still reads the same canonical spec. Its dynamic task/mailbox delta may be delivered normally and remains task-owned; it does not alter canonical bytes.
|
|
236
|
+
|
|
237
|
+
## 7. Deterministic UTF-8 chunk protocol
|
|
238
|
+
|
|
239
|
+
Tool name: `squad_spec_read`. It exists only in a file-spec child.
|
|
240
|
+
|
|
241
|
+
Input schema:
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"type": "object",
|
|
246
|
+
"additionalProperties": false,
|
|
247
|
+
"required": ["index"],
|
|
248
|
+
"properties": { "index": { "type": "integer", "minimum": 0 } }
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Chunk construction over canonical raw bytes, with `C = 32768`:
|
|
253
|
+
|
|
254
|
+
```text
|
|
255
|
+
start[0] = 0
|
|
256
|
+
candidate = min(start[i] + C, byteLength)
|
|
257
|
+
while candidate < byteLength and (bytes[candidate] & 0xC0) == 0x80:
|
|
258
|
+
candidate -= 1
|
|
259
|
+
end[i] = candidate
|
|
260
|
+
start[i+1] = end[i]
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The already-validated UTF-8 guarantees progress and decoding. Chunks are contiguous, nonempty, code-point aligned, cover `[0, byteLength)` exactly once, and are independent of newline layout. `chunkCount` is computed from this algorithm, not `ceil(bytes/C)`.
|
|
264
|
+
|
|
265
|
+
Before every read, the tool securely opens the canonical file and verifies whole-file byte length/hash. An out-of-range index or integrity failure is an error and records no coverage. A successful result is:
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"content": [
|
|
270
|
+
{ "type": "text", "text": "{\"version\":1,\"squadId\":\"...\",\"taskId\":\"...\",\"index\":0,\"chunkCount\":2,\"startByte\":0,\"endByteExclusive\":32768,\"chunkBytes\":32768,\"chunkSha256\":\"...\",\"specBytes\":40000,\"specSha256\":\"...\"}" },
|
|
271
|
+
{ "type": "text", "text": "<exact fatal-UTF-8 decoding of raw chunk bytes>" }
|
|
272
|
+
],
|
|
273
|
+
"details": { "same typed metadata as content[0], not the chunk text" }
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The first text block is compact JSON in the fixed key order shown; the second is the exact chunk text. No prefix, suffix, elision, or viewport truncation is permitted in the second block. Per-chunk SHA-256 is over raw chunk bytes; whole-file SHA-256 is over canonical raw bytes.
|
|
278
|
+
|
|
279
|
+
Reads may be requested in any order and concurrently. The reader is idempotent. A duplicate with identical metadata/hash leaves the original delivery record unchanged; a conflicting duplicate invalidates progress and blocks the task.
|
|
280
|
+
|
|
281
|
+
## 8. Tool guard and Pi 0.80.10 parallel semantics
|
|
282
|
+
|
|
283
|
+
The child extension installs a `tool_call` handler. While complete attestation is absent or invalid:
|
|
284
|
+
|
|
285
|
+
- allow only `event.toolName === "squad_spec_read"` with the exact schema;
|
|
286
|
+
- return `{ block: true, reason: "Read every canonical squad spec chunk with squad_spec_read before using other tools." }` for **every** other tool, including `read`, `bash`, `grep`-capable commands, write/edit, browser/network, MCP/custom tools, and any tool added after startup;
|
|
287
|
+
- treat guard exceptions/state parse failures as blocked (fail closed).
|
|
288
|
+
|
|
289
|
+
Pi 0.80.10 guarantees sibling calls in a parallel assistant message are preflighted sequentially and only then execute concurrently. A sibling normal tool cannot rely on sibling reader results because those results do not exist during preflight; it is blocked. All requested chunks may run in parallel, but normal tools become eligible only in a later model turn after delivery commits. Mutating tool input is not used as authorization.
|
|
290
|
+
|
|
291
|
+
Tool execution alone is not delivery proof. Keep an untrusted pending record keyed by `toolCallId`; commit a chunk only when the extension observes the finalized successful tool-result `message_end` for that same call and verifies both returned content blocks byte-for-byte against a fresh canonical read. If the process crashes before commit, the chunk is read again. Partial RPC output, `tool_execution_start`, `tool_result`, and `tool_execution_end` alone do not count.
|
|
292
|
+
|
|
293
|
+
A child may emit text or attempt to settle without tools; Pi has no completion-block return from `tool_call`. Parent settlement enforcement below makes that incapable of completing the task.
|
|
294
|
+
|
|
295
|
+
## 9. Durable coverage and attestation state machine
|
|
296
|
+
|
|
297
|
+
Task-owned paths:
|
|
298
|
+
|
|
299
|
+
```text
|
|
300
|
+
<squad-dir>/<task-id>/spec-read-state.json
|
|
301
|
+
<squad-dir>/<task-id>/spec-read-attestation.json
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Use the store's cross-process lock plus atomic temp-write/fsync/rename discipline. State is bound to `{squadId, taskId, specSha256, specBytes, chunkBytes, chunkCount}`. Mismatch means `INVALID`, never migration to a different spec.
|
|
305
|
+
|
|
306
|
+
States:
|
|
307
|
+
|
|
308
|
+
```text
|
|
309
|
+
NOT_REQUIRED (legacy squad only)
|
|
310
|
+
REQUIRED -> READING -> COMPLETE
|
|
311
|
+
| | |
|
|
312
|
+
+----------+-----------+--> INVALID (identity/hash/schema/conflicting record)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
- `REQUIRED`: zero committed chunks.
|
|
316
|
+
- `READING`: a durable set of valid committed chunk indices. Order is irrelevant.
|
|
317
|
+
- `COMPLETE`: all deterministic indices are present; offsets are contiguous; concatenated raw chunk hashes recompute the whole canonical hash. Atomically write the attestation, then treat the guard as open.
|
|
318
|
+
- `INVALID`: fail closed. A canonical mismatch quarantines the squad. A corrupt progress file may be moved aside for audit and reset to `REQUIRED` only after the canonical file verifies; a conflicting/tampered claimed delivery is never accepted.
|
|
319
|
+
|
|
320
|
+
Exact complete attestation schema:
|
|
321
|
+
|
|
322
|
+
```json
|
|
323
|
+
{
|
|
324
|
+
"version": 1,
|
|
325
|
+
"state": "complete",
|
|
326
|
+
"squadId": "squad-id",
|
|
327
|
+
"taskId": "task-id",
|
|
328
|
+
"specSha256": "64-lowercase-hex",
|
|
329
|
+
"specBytes": 40000,
|
|
330
|
+
"chunkBytes": 32768,
|
|
331
|
+
"chunkCount": 2,
|
|
332
|
+
"chunks": [
|
|
333
|
+
{
|
|
334
|
+
"index": 0,
|
|
335
|
+
"startByte": 0,
|
|
336
|
+
"endByteExclusive": 32768,
|
|
337
|
+
"bytes": 32768,
|
|
338
|
+
"sha256": "64-lowercase-hex",
|
|
339
|
+
"toolCallId": "pi-tool-call-id",
|
|
340
|
+
"deliveredAt": "RFC3339 timestamp"
|
|
341
|
+
}
|
|
342
|
+
],
|
|
343
|
+
"completedAt": "RFC3339 timestamp"
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
`chunks` is stored in ascending index order regardless of delivery order and contains exactly `chunkCount` entries. Unknown properties, duplicate indices, gaps, overlaps, wrong offsets/lengths, wrong hashes, an invalid timestamp, or wrong identity make it invalid. Redundant reads after `COMPLETE` may return content but do not rewrite `completedAt` or the original chunk record.
|
|
348
|
+
|
|
349
|
+
Restart behavior:
|
|
350
|
+
|
|
351
|
+
- Durable committed progress survives child/main crashes. Pending executions do not.
|
|
352
|
+
- A resumed process for the task's immutable Pi session may trust a still-valid complete task attestation after re-verifying the canonical whole hash; it need not reread. This preserves task-owned session semantics while proving that exact session/task previously received all tool results.
|
|
353
|
+
- Incomplete progress resumes at any missing indices. The bootstrap reports the complete sorted missing-index list (never a truncated list).
|
|
354
|
+
- A new task/rework/retest has independent state even when assigned to the same agent.
|
|
355
|
+
|
|
356
|
+
## 10. Parent settlement, review, pause, cancellation, and recovery
|
|
357
|
+
|
|
358
|
+
All completion paths share `validateTaskSpecAttestation(squad, task)`; do not enforce only in one event handler.
|
|
359
|
+
|
|
360
|
+
1. On child `agent_settled`, before extracting/promoting output or unblocking dependents, validate canonical bytes and the task attestation. If missing/invalid, reject candidate completion, append a durable status reason, leave `completed = null`, set the task back to `pending`, and reopen the same task-owned session. Never copy candidate output to `Task.output` as accepted output.
|
|
361
|
+
2. `squad_modify complete_task` performs the same check and rejects without status mutation when required attestation is absent/invalid.
|
|
362
|
+
3. `checkSquadCompletion` may enter review only if every non-cancelled `done` task has a valid attestation. Discovery of a missing/tampered attestation reopens that task and transitively invalidates/re-blocks completed descendants using existing dependency semantics.
|
|
363
|
+
4. `squad_review` revalidates all required attestations and canonical bytes before recording any verdict. On failure it rejects the review and reopens affected work; a stale pending review cannot bypass recovery.
|
|
364
|
+
5. Parent restart reconciliation performs the same audit for file squads in `running`, `failed`, `paused`, or `review`. It does not automatically resume explicitly `suspended` tasks, preserving pause semantics. It does not revive `cancelled` tasks; cancelled tasks are excluded from completion just as today.
|
|
365
|
+
6. Explicit `resume_task` continues the same durable session and coverage state. `pause_task`, whole-squad pause, exact cancellation, mailbox delivery, descendant invalidation, failed-review rework, and main review retain their current targeting and ordering semantics.
|
|
366
|
+
7. If settlement repeatedly occurs without attestation, it repeatedly remains non-completing/recoverable; it can be paused or cancelled normally. It is never converted to `done` merely to stop a loop.
|
|
367
|
+
|
|
368
|
+
A valid attestation proves delivery of bytes into the task's Pi tool-result context, not human-like semantic comprehension. That is the strongest mechanically enforceable claim and is intentionally stronger than prompt instructions or filesystem access logs.
|
|
369
|
+
|
|
370
|
+
## 11. Compatibility and security matrix
|
|
371
|
+
|
|
372
|
+
| Case | Required behavior |
|
|
373
|
+
|---|---|
|
|
374
|
+
| Legacy `{goal,...}` call/task/session | Existing planner, prompts, sessions, mailboxes, and settlement; no attestation required |
|
|
375
|
+
| File call also contains `goal`/`tasks` | Schema rejection; no squad directory/scheduler |
|
|
376
|
+
| Missing/malformed/duplicate-key/invalid-UTF-8 spec | Reject before publish/schedule |
|
|
377
|
+
| Wrong caller hash / uppercase hash | Reject before publish/schedule; no normalization |
|
|
378
|
+
| Oversized embedded contract/Base64/data URI | Reject with measured limit; require artifact ref; never truncate |
|
|
379
|
+
| Missing/wrong/tampered artifact at creation | Reject before publish/schedule |
|
|
380
|
+
| Final-component symlink or non-regular source/artifact | Reject |
|
|
381
|
+
| Path replaced during read | Open descriptor identity remains pinned; mismatch/metadata change rejects |
|
|
382
|
+
| Canonical file changed after publish | Reader, scheduler, completion, and review fail closed/quarantine |
|
|
383
|
+
| Windows drive-relative path / NUL / POSIX relative artifact | Reject; native absolute path rules only |
|
|
384
|
+
| Chunk order `N-1..0` | Allowed; final attestation sorted and full coverage verified |
|
|
385
|
+
| Duplicate identical chunk | Idempotent; no coverage inflation |
|
|
386
|
+
| Missing chunk / out-of-range index | No complete attestation; all normal tools blocked |
|
|
387
|
+
| Tampered chunk metadata/text/hash | No commit; conflicting record invalidates progress |
|
|
388
|
+
| `grep`, ordinary `read`, or `bash cat spec` before completion | Blocked by all-tool preflight guard; cannot count as proof |
|
|
389
|
+
| All reads plus normal tool in one parallel assistant message | Reads may execute; normal sibling was preflighted before results and is blocked |
|
|
390
|
+
| Crash before finalized tool-result `message_end` | Pending chunk does not count; reread after restart |
|
|
391
|
+
| Crash after durable chunk commit | Coverage resumes without losing committed chunks |
|
|
392
|
+
| Complete task session resumed in a new child process | Reverify canonical + attestation; normal tools allowed without redundant reread |
|
|
393
|
+
| Child settles with prose only | Parent rejects/reopens; no output promotion/dependent unblock |
|
|
394
|
+
| Attestation deleted/tampered after task done | Reconciliation/review reopens task and invalidates descendants |
|
|
395
|
+
| Explicitly paused/suspended task on restart | Remains suspended; no auto-resume solely for attestation |
|
|
396
|
+
| Cancelled task without attestation | Remains cancelled and excluded under existing semantics |
|
|
397
|
+
| Added/rework/retest task in file squad | Fresh task-owned attestation required for same canonical spec |
|
|
398
|
+
| Agent tool allowlist omits reader | Spawn force-adds reader; guard remains authoritative |
|
|
399
|
+
| Attempted recursive squad from child | Squad tools not registered; pre-attestation guard would also block them |
|
|
400
|
+
|
|
401
|
+
## 12. Required verification coverage
|
|
402
|
+
|
|
403
|
+
Implementation is incomplete unless tests cover:
|
|
404
|
+
|
|
405
|
+
- schema success/failure, duplicate keys, UTF-8/BOM, hash mismatch, total/field/Base64 limits, artifact refs, graph/agent failures;
|
|
406
|
+
- stable-descriptor reads, symlink rejection, path replacement/in-place mutation, atomic no-partial publication, existing-ID collision, POSIX and mocked/native Windows path cases;
|
|
407
|
+
- multibyte characters on every possible boundary shape, exact concatenated bytes, per-chunk and whole hashes, reverse/random/concurrent order, duplicate/missing/out-of-range/tampered chunks;
|
|
408
|
+
- child extension/RPC package loading on Pi 0.80.10, reader forced into a tool allowlist, all-tool blocking (including grep/bash/read/custom), parallel preflight, textual early settlement;
|
|
409
|
+
- commit only after finalized tool-result delivery, crash before/after commit, main and child restart, incomplete resume, complete same-session resume;
|
|
410
|
+
- missing/tampered attestation at `agent_settled`, manual completion, completion-to-review, and review acceptance; descendant recovery;
|
|
411
|
+
- exact pause/resume/cancellation/mailbox behavior, new/rework/retest tasks, and legacy inline compatibility;
|
|
412
|
+
- `npm test`, child-process RPC integration, and `npm pack --dry-run` proving the child guard/runtime and documentation are included.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# pi-fleet ↔ pi-squad Bridge (cross-extension contract)
|
|
2
|
+
|
|
3
|
+
pi-squad orchestrates **what** gets done (task DAG, QA loop, advisor) with local
|
|
4
|
+
`pi --mode rpc` agents. pi-fleet orchestrates **where** work runs (remote tailnet
|
|
5
|
+
workers, bundles, baselines, budgets). This document defines how they compose —
|
|
6
|
+
and the independence guarantees that composition must never break.
|
|
7
|
+
|
|
8
|
+
## Independence contract (non-negotiable)
|
|
9
|
+
|
|
10
|
+
| Installed | Behavior |
|
|
11
|
+
|---|---|
|
|
12
|
+
| pi-fleet only | Everything works. The bridge global is published but never consumed — zero side effects (FleetManager stays lazy). |
|
|
13
|
+
| pi-squad only | Everything works. Squad feature-detects the bridge, finds nothing, and never offers/uses remote placement. Tasks requesting a `host` fail with an actionable error, not a crash. |
|
|
14
|
+
| Both | Remote placement unlocks (Phase A below). |
|
|
15
|
+
|
|
16
|
+
Rules:
|
|
17
|
+
- **No package dependency in either direction.** The only coupling is a
|
|
18
|
+
version-checked global (`globalThis.__piFleetBridge`) plus this document.
|
|
19
|
+
- Consumers must treat a missing global or `version !== 1` as "fleet absent".
|
|
20
|
+
- pi-fleet publishes at server-mode startup and unpublishes on
|
|
21
|
+
`session_shutdown` (ownership-checked, so a reload's new instance wins).
|
|
22
|
+
- Consumer exceptions are isolated inside the bridge's event dispatch — a
|
|
23
|
+
buggy consumer cannot break fleet event handling.
|
|
24
|
+
|
|
25
|
+
## Bridge API v1 (pi-fleet `src/server/bridge.ts`)
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const bridge = (globalThis as Record<string, unknown>).__piFleetBridge as PiFleetBridgeV1 | undefined;
|
|
29
|
+
if (bridge?.version === 1) { /* fleet available */ }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- `spawnWorker({host, cwd, bundle?, fromBaseline?, maxCost?})` → `{instanceId, host, bundle, staleBaseline?}`
|
|
33
|
+
- `prompt(instanceId, message)` — fire-and-forget; observe via `onEvent`
|
|
34
|
+
- `rpc(instanceId, command, timeoutMs?)` — raw pi RPC passthrough (steer/abort/set_model/get_state…)
|
|
35
|
+
- `onEvent(instanceId, listener)` → unsubscribe — the worker's pi RPC event stream
|
|
36
|
+
- `abort(instanceId)` / `stop(instanceId)` / `status(instanceId)`
|
|
37
|
+
|
|
38
|
+
## Composition phases
|
|
39
|
+
|
|
40
|
+
**Phase 0 (shipped, prompt-level):** the squad supervisor skill teaches the main
|
|
41
|
+
session when to use squad (intra-repo parallelism) vs fleet (machine-bound work)
|
|
42
|
+
vs both side by side. Works with either extension alone.
|
|
43
|
+
|
|
44
|
+
**Phase A (planned, pi-squad side):** `host?` on squad agent entries →
|
|
45
|
+
`agents: { backend: { host: "ab-internal-10", fromBaseline: "api-repo" } }`.
|
|
46
|
+
Squad's AgentPool gains a bridge-backed executor: spawn via `spawnWorker`,
|
|
47
|
+
deliver the squad protocol prompt via `prompt`, map `onEvent` into the existing
|
|
48
|
+
AgentEvent pipeline (same events the local RPC children emit), steer via
|
|
49
|
+
`rpc({type:"steer"})`, kill via `stop`. Scheduler/monitor/advisor/QA operate on
|
|
50
|
+
the event stream and need no changes. Known problems to solve:
|
|
51
|
+
- **Skills**: `--skill` paths don't exist remotely — squad skills must ship in
|
|
52
|
+
the worker's bundle (publish a `squad` bundle) or be inlined into the
|
|
53
|
+
appended system prompt.
|
|
54
|
+
- **System prompt**: fleet spawn has no `--append-system-prompt` passthrough
|
|
55
|
+
yet; deliver the protocol as the first prompt message, or extend the spawn
|
|
56
|
+
frame (fleet-side change, additive).
|
|
57
|
+
- **File coordination**: `modifiedFiles`/sibling-conflict rules are meaningless
|
|
58
|
+
across machines. Remote squad tasks must deliver work as branch/patch
|
|
59
|
+
(fleet's `deliver` mechanism), never assume a shared working tree.
|
|
60
|
+
- **inheritContext**: session forks cannot cross machines. The remote
|
|
61
|
+
equivalent is `fromBaseline` (warm repo context, clone-on-spawn).
|
|
62
|
+
|
|
63
|
+
**Phase B (planned):** unify review — remote squad tasks route fleet
|
|
64
|
+
`task_done` into squad's `handleTaskCompleted`; QA `FAIL` verdict maps to
|
|
65
|
+
`remote_reject`; fleet's own awaiting-review is suppressed for squad-owned
|
|
66
|
+
instances.
|
|
67
|
+
|
|
68
|
+
**Phase C (planned):** budgets — `maxCost` per squad task; squad cost roll-up
|
|
69
|
+
includes remote workers.
|
|
70
|
+
|
|
71
|
+
## Division-of-labor cheat sheet
|
|
72
|
+
|
|
73
|
+
| Situation | Use |
|
|
74
|
+
|---|---|
|
|
75
|
+
| Parallel work inside this repo (backend+frontend+QA share the tree) | squad |
|
|
76
|
+
| Work on another machine/OS/dev-env/repo; blast-radius or cost isolation | fleet |
|
|
77
|
+
| Big feature here + deployment validation on the VM | both, side by side |
|
|
78
|
+
| Warm repo understanding for repeated remote tasks | fleet baselines |
|
|
79
|
+
| Stuck-agent rescue with a stronger model | squad advisor (local), orchestrator review (fleet) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-squad",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Multi-agent collaboration extension for pi — task decomposition, dependency management, parallel execution, TUI panel",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"src",
|
|
26
|
+
"docs",
|
|
26
27
|
"README.md",
|
|
27
28
|
"LICENSE"
|
|
28
29
|
],
|
package/src/agent-pool.ts
CHANGED
|
@@ -146,8 +146,9 @@ export class AgentPool {
|
|
|
146
146
|
sessionDir?: string;
|
|
147
147
|
/** Fork the given session file so a new task inherits main-session context. */
|
|
148
148
|
forkSession?: { file: string; sessionDir: string };
|
|
149
|
+
spec?: { squadId: string; path: string; sha256: string; bytes: number; chunkBytes: number };
|
|
149
150
|
}): Promise<AgentProcess> {
|
|
150
|
-
const { taskId, agentDef, protocolOptions, cwd, skillPaths, resumeSession, sessionDir, forkSession } = options;
|
|
151
|
+
const { taskId, agentDef, protocolOptions, cwd, skillPaths, resumeSession, sessionDir, forkSession, spec } = options;
|
|
151
152
|
|
|
152
153
|
// Kill existing process for this task if any
|
|
153
154
|
if (this.agents.has(taskId)) {
|
|
@@ -161,7 +162,7 @@ export class AgentPool {
|
|
|
161
162
|
fs.writeFileSync(promptFile, systemPrompt, "utf-8");
|
|
162
163
|
|
|
163
164
|
// Build pi CLI args
|
|
164
|
-
const args = buildPiArgs(agentDef, promptFile, skillPaths, { resumeSession, sessionDir, forkSession });
|
|
165
|
+
const args = buildPiArgs(agentDef, promptFile, skillPaths, { resumeSession, sessionDir, forkSession }, Boolean(spec));
|
|
165
166
|
|
|
166
167
|
// Spawn pi process — set env var to prevent recursive squad extension loading
|
|
167
168
|
const invocation = getPiInvocation(["--mode", "rpc", ...args]);
|
|
@@ -169,7 +170,14 @@ export class AgentPool {
|
|
|
169
170
|
const proc = spawn(invocation.command, invocation.args, {
|
|
170
171
|
cwd,
|
|
171
172
|
stdio: ["pipe", "pipe", "pipe"],
|
|
172
|
-
env: { ...process.env, PI_SQUAD_CHILD: "1"
|
|
173
|
+
env: { ...process.env, PI_SQUAD_CHILD: "1", ...(spec ? {
|
|
174
|
+
PI_SQUAD_ID: spec.squadId,
|
|
175
|
+
PI_SQUAD_TASK_ID: taskId,
|
|
176
|
+
PI_SQUAD_SPEC_PATH: spec.path,
|
|
177
|
+
PI_SQUAD_SPEC_SHA256: spec.sha256,
|
|
178
|
+
PI_SQUAD_SPEC_BYTES: String(spec.bytes),
|
|
179
|
+
PI_SQUAD_SPEC_CHUNK_BYTES: String(spec.chunkBytes),
|
|
180
|
+
} : {}) },
|
|
173
181
|
});
|
|
174
182
|
|
|
175
183
|
const activity: AgentActivity = {
|
|
@@ -524,6 +532,7 @@ function buildPiArgs(
|
|
|
524
532
|
sessionDir?: string;
|
|
525
533
|
forkSession?: { file: string; sessionDir: string };
|
|
526
534
|
},
|
|
535
|
+
fileSpec = false,
|
|
527
536
|
): string[] {
|
|
528
537
|
const { resumeSession, sessionDir, forkSession } = sessionOptions;
|
|
529
538
|
if (resumeSession && forkSession) throw new Error("Cannot resume and fork a task session simultaneously");
|
|
@@ -547,7 +556,10 @@ function buildPiArgs(
|
|
|
547
556
|
}
|
|
548
557
|
|
|
549
558
|
if (agentDef.tools && agentDef.tools.length > 0) {
|
|
550
|
-
|
|
559
|
+
const tools = fileSpec && !agentDef.tools.includes("squad_spec_read")
|
|
560
|
+
? [...agentDef.tools, "squad_spec_read"]
|
|
561
|
+
: agentDef.tools;
|
|
562
|
+
args.push("--tools", tools.join(","));
|
|
551
563
|
}
|
|
552
564
|
|
|
553
565
|
for (const skillPath of skillPaths) {
|