pi-squad 0.16.7 → 0.17.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/README.md CHANGED
@@ -395,3 +395,7 @@ src/
395
395
  ## License
396
396
 
397
397
  MIT
398
+
399
+ ## File-based squad specifications
400
+
401
+ Large contracts may be started without inlining them: `squad({ specFile, specSha256 })`. The strict v1 JSON contract, size/artifact policy, canonical-byte publication, child-only `squad_spec_read` protocol, tool guard, durable task attestation, and legacy compatibility are specified in [`docs/file-spec-and-full-read-attestation-contract.md`](docs/file-spec-and-full-read-attestation-contract.md). The SHA-256 must be lowercase and match the exact file bytes; malformed, oversized, or artifact-integrity failures are rejected before scheduling.
@@ -0,0 +1,204 @@
1
+ # Exact cancellation, review presentation, and suspended-stall attention contract
2
+
3
+ Status: implementation contract (no implementation in this note)
4
+
5
+ ## Non-negotiable invariants
6
+
7
+ 1. A destructive tool call never infers its squad target. `squad_modify({ action: "cancel" })` requires a non-empty exact `squadId`.
8
+ 2. A scheduler is owned/addressed by its persisted squad ID. Acting on squad A never stops, deletes, focuses, or renders scheduler B.
9
+ 3. When the widget is enabled, its target equals the active/focused squad ID, or both are `null`. A cancelled focused squad is never left displayed.
10
+ 4. Task execution completion and independent acceptance remain separate. A failed review stays `squad.status === "review"`, retains immutable evidence in `squad.review`, and is not accepted.
11
+ 5. Suspension is an explicit pause. Reconciliation, restart, dependency changes, and attention delivery never resume a suspended task. Only an explicit resume operation may do that.
12
+ 6. Task-owned sessions, task mailboxes, review evidence/history, and complete message/evidence text are unchanged and must not be truncated semantically.
13
+ 7. `resume_task` is idempotent with respect to already-live exact work: it never demotes an `in_progress` task, replaces its child, or queues a duplicate run.
14
+
15
+ ## 1. Exact-ID whole-squad cancellation
16
+
17
+ ### Tool contract
18
+
19
+ For `squad_modify` with `action: "cancel"`:
20
+
21
+ - `squadId` is required for this action even if the schema remains conditionally optional for other actions.
22
+ - Missing ID returns an error such as: `cancel requires exact squadId; no squad was changed.` It must not consult `activeSquadId`, latest squad, project candidates, or widget state.
23
+ - Unknown ID returns: `Squad '<supplied-id>' not found; no squad was changed.`
24
+ - Validate/load the persisted target before touching any scheduler or focus state.
25
+ - With a live target scheduler: stop only that scheduler, then remove only `schedulers.get(squadId)`.
26
+ - Without a live target scheduler: perform the same persisted cancellation transition safely. If scheduler lifecycle cleanup is reused, construct it for the exact ID without focusing it and without registering it as another squad's active scheduler. Existing `in_progress -> suspended` stop semantics may be preserved.
27
+ - Persist the target squad's existing cancellation terminal representation (`failed`) after cleanup. This contract does not add a new squad status and does not alter task cancellation semantics.
28
+ - Success names the target: `Squad '<squadId>' cancelled.` Any result/error after an ID is supplied also echoes that ID.
29
+ - Repeating cancellation for the same already-terminal target is safe/idempotent and still names that target; it must not affect another squad.
30
+
31
+ Task-level `cancel_task` remains its existing neutral, dependency-safe contract; this section is only whole-squad destructive cancellation.
32
+
33
+ ### Focus/widget synchronization
34
+
35
+ After the persisted transition, enforce one helper-level invariant rather than updating globals ad hoc:
36
+
37
+ - If `activeSquadId === cancelledId`: set `activeSquadId = null`, set `widgetState.squadId = null`, and request a widget update so both widget and status line clear.
38
+ - If another valid squad B is focused: preserve B and set/retain `widgetState.squadId = B`; do not focus A merely because an absent scheduler had to be reconstructed.
39
+ - If globals were already inconsistent and either points at A, repair them to the valid focused B or clear both. Never leave `widgetState.squadId` pointing at A or at a squad different from `activeSquadId`.
40
+ - Always request an update after focus repair. Clearing must reset/ignore the prior widget cache key so a same-shaped future squad can render.
41
+
42
+ Interactive `/squad cancel` may use the visibly focused squad, but it must capture that exact ID before awaiting, cancel only it, notify `Squad '<id>' cancelled`, then apply the same focus/widget repair. If there is no visible focus, it is a no-op with `No focused squad to cancel`.
43
+
44
+ ## 2. Review-state presentation
45
+
46
+ The persisted lifecycle does not change:
47
+
48
+ | Squad/task execution | Active review | Acceptance meaning |
49
+ |---|---|---|
50
+ | all relevant tasks done | `pending` | candidate awaits independent review |
51
+ | all relevant tasks done | `failed` | candidate was rejected; same-squad rework required |
52
+ | rework starts | absent; failed attempt appended unchanged to `reviewHistory` | executing rework |
53
+ | fresh execution settles | fresh `pending` | new candidate awaits review |
54
+ | pass/pass-with-issues | `passed`, squad `done` | accepted |
55
+
56
+ Required presentation in compact widget, status line/tool output, squad list/selection, and detail panel:
57
+
58
+ - Pending: `◆ REVIEW PENDING · independent review required` (warning style).
59
+ - Failed: `✗ REVIEW FAILED · awaiting same-squad rework` (error style).
60
+ - Failed must never be rendered as the generic `review required` label used for pending.
61
+ - Execution progress (for example `3/3`) remains truthful but must be adjacent to the acceptance label; it must not imply acceptance.
62
+ - `squad_status` must render an explicit acceptance line using the labels above. Detail/list surfaces must use distinct pending/failed icon/label text.
63
+ - Failed detail may show actionable issues, but if it does it must preserve every issue in the detail view; do not slice the semantic list or issue bodies. Width/viewport clipping may be visual only, with full data still navigable.
64
+
65
+ Widget render invalidation must include every persisted value that changes visible output, at minimum `squad.status` and `squad.review?.status ?? "none"` (and issue/verdict fields if rendered). A review `pending -> failed` update must repaint even when task statuses, turns, and progress are unchanged. Clearing/focus changes also invalidate independently of this cache.
66
+
67
+ ## 3. Durable level-triggered suspended-stall attention
68
+
69
+ ### Derived stall predicate
70
+
71
+ Evaluate after normal mailbox recovery and blocked/pending normalization, and before/with completion derivation. Ignore cancelled tasks for scheduling and stall denominators, consistent with current lifecycle rules.
72
+
73
+ Definitions for one persisted squad DAG:
74
+
75
+ - `S`: all exact task IDs whose status is `suspended`, sorted lexically.
76
+ - A task is runnable/live if it is `in_progress`, or `pending` with every dependency `done`.
77
+ - `suspensionBlocked(t)`: `t.status === "blocked"` and at least one currently unsatisfied dependency has a transitive dependency path ending at a task in `S`.
78
+ - Terminal for this predicate: `done`, `failed`, or `cancelled` (cancelled is excluded from the relevant set, but remains visible historically).
79
+
80
+ A **suspended stall** is active iff:
81
+
82
+ 1. `S` is non-empty;
83
+ 2. there is no runnable/live non-cancelled task; and
84
+ 3. every non-cancelled task not in `S` is terminal or `suspensionBlocked`.
85
+
86
+ This covers suspended-only squads and DAGs whose remaining nonterminal work is blocked directly or transitively by suspended ancestors. It does not fire while independent runnable work exists. A blocked task with no transitive suspended ancestor is a different stall and must not be mislabeled.
87
+
88
+ ### Persisted attention/outbox state
89
+
90
+ Add optional squad-owned metadata (names may vary only if semantics remain exact):
91
+
92
+ ```ts
93
+ interface SuspendedStallAttention {
94
+ kind: "suspended_stall";
95
+ fingerprint: string; // canonical hash/string of sorted S + sorted blockedTaskIds
96
+ suspendedTaskIds: string[]; // sorted, complete, never abbreviated
97
+ blockedTaskIds: string[]; // sorted suspension-blocked descendants, complete
98
+ detectedAt: string;
99
+ delivery: "pending" | "delivered";
100
+ deliveredAt: string | null;
101
+ }
102
+
103
+ interface Squad {
104
+ suspendedStallAttention?: SuspendedStallAttention;
105
+ }
106
+ ```
107
+
108
+ Rules:
109
+
110
+ - On false -> true, atomically persist a new `pending` record before emitting an in-memory scheduler event.
111
+ - Repeated reconcile with the same canonical fingerprint is a no-op: do not rewrite timestamps and do not emit again.
112
+ - If the exact suspended/blocked sets change while still stalled, persist one new semantic episode/fingerprint and permit one updated wake containing the new exact IDs.
113
+ - On predicate false (including explicit resume making progress possible), clear the active record. If the same tasks are explicitly suspended again later, that is a new episode and may notify once.
114
+ - Delivery handling is an outbox: only a `pending` record emits `suspended_stall`; the extension sends the main-session follow-up and then marks that exact fingerprint `delivered`. Marking must compare the fingerprint so an older callback cannot acknowledge a newer episode.
115
+ - Scheduler reconstruction/start calls reconcile and therefore recreates missing attention or emits a still-pending persisted record. A delivered record is still rendered/injected as active attention but is not re-emitted on restart or timer reconcile.
116
+ - The unavoidable host boundary is handled as an outbox: a process failure between host acceptance and acknowledgement can retry once after restart; stable `customType` plus squad/fingerprint should be supplied if the host supports dedupe. Normal transitions/reconcile/restart must never create duplicate wakeups.
117
+
118
+ ### Wake content and durable visibility
119
+
120
+ The wake must name the exact squad and include the complete sorted suspended IDs and complete blocked-descendant IDs. Required actionable text:
121
+
122
+ ```text
123
+ [squad] SUSPENDED WORK NEEDS ACTION in '<squadId>'.
124
+ Suspended task IDs: <all IDs>
125
+ Blocked by suspended work: <all IDs, or none>
126
+ No task was resumed automatically.
127
+ Resume intentionally with squad_modify { action: "resume_task", squadId: "<squadId>", taskId: "<exact-task-id>" } for each task you choose.
128
+ ```
129
+
130
+ No `slice`, `... +N`, or shortened semantic payload is allowed in the wake, injected context, or status/detail data. Terminal-width clipping is presentation-only.
131
+
132
+ While the record is active:
133
+
134
+ - `before_agent_start` injects it project-wide even if another squad is focused, so durable attention survives a missed edge notification.
135
+ - `squad_status` and the detail panel show `SUSPENDED — explicit resume required` plus all exact IDs.
136
+ - The compact widget shows an attention indicator; it need not fit all IDs, but detail/status must preserve them. Its cache key includes the attention fingerprint and delivery-independent active state.
137
+ - Attention never calls `resume()` or `resumeTask()`.
138
+
139
+ ### Explicit resumption
140
+
141
+ The recommended recovery is one exact call per intended task:
142
+
143
+ ```json
144
+ { "action": "resume_task", "squadId": "<exact squad>", "taskId": "<exact suspended task>" }
145
+ ```
146
+
147
+ `resume_task` retains the task-owned session/mailbox behavior and immutable failed-review history. It changes only the exact task plus existing descendant invalidation/scheduling rules. Reconcile then clears or replaces attention from current persisted state. If another suspended cut still prevents progress, a new exact-set episode is allowed; otherwise normal scheduling resumes. No descendant is marked successful merely because its ancestor resumed.
148
+
149
+ Before any status write, descendant invalidation, or scheduling, `Scheduler.resumeTask(taskId)` must load the exact task and check both durable and live state. If `task.status === "in_progress"` and `pool.isRunning(taskId)`, it is an idempotent no-op: preserve `in_progress`, the same child process, session, mailbox, messages, and active review/rework state; do not enqueue/spawn another run. The tool must return truthful text naming both identities, for example `Task '<taskId>' is already running in squad '<squadId>'; no duplicate resume was started.` A safe explicit rejection with the same names is also conforming. A contradictory state (`in_progress` without a live child, or live child with non-`in_progress` durable status) must be reconciled by the existing recovery policy, never by blindly writing `pending` under a live child. This guard specifically covers a completed task reopened by `squad_message` on its original session followed immediately by redundant `resume_task`.
150
+
151
+ ## 4. Edge-case matrix
152
+
153
+ | Case | Required outcome |
154
+ |---|---|
155
+ | A focused/running, cancel tool omits ID | Reject; A and every other squad unchanged; widget remains A. |
156
+ | A focused, cancel tool explicitly targets B | Stop/persist only B; A scheduler/focus/widget remain A; result names B. |
157
+ | Explicit B has no live scheduler | Persist cancellation for B without focusing/stopping A; result names B. |
158
+ | Explicit focused A cancelled | Only A stops; focus/widget/status clear; result names A. |
159
+ | Interactive cancel on visible A | Cancel captured A, name A, clear/refresh correctly. |
160
+ | Unknown explicit ID | No scheduler/focus/store mutation; error echoes supplied ID. |
161
+ | Review pending -> failed with tasks still 3/3 | Compact, status, list, detail repaint to `REVIEW FAILED`; progress may remain 3/3. |
162
+ | Repeated widget update after failed review | Cache does not suppress first changed render; later identical updates may coalesce. |
163
+ | One suspended task, all others done | Persist/wake once; no auto-resume. |
164
+ | Suspended root -> blocked child -> blocked grandchild | Wake once with root in suspended IDs and both descendants in blocked IDs. |
165
+ | Suspended root plus unrelated runnable task | No suspended-stall wake until unrelated work ceases to be runnable/live. |
166
+ | Suspended task plus unrelated irreducible blocked component | Predicate false for suspended-stall; existing generic failure/stall handling may apply. |
167
+ | Reconcile timer repeats unchanged stall | Same persisted fingerprint; zero additional wakeups. |
168
+ | Scheduler reconstructed/restarted in unchanged delivered stall | Attention remains visible/injected; no duplicate wake. |
169
+ | Restart with pending outbox record | Emit it once, acknowledge matching fingerprint after send. |
170
+ | Resume one of several suspended tasks | Never resume the others; clear/update attention by the newly derived exact sets. |
171
+ | Resume enables work | Scheduler runs normally; active attention clears. |
172
+ | Message reopens completed task and its child is live; redundant exact `resume_task` follows | Idempotent already-running result names squad/task; durable state stays `in_progress`; same child/session/mailbox continues; no duplicate spawn/queue. |
173
+ | Same task explicitly suspended again later | New false->true episode; one new wake is valid. |
174
+ | Cancelled tasks coexist with suspension | Cancelled tasks remain visible but do not satisfy dependencies or affect the suspended-stall denominator. |
175
+
176
+ ## 5. Verification plan
177
+
178
+ ### Focused unit tests
179
+
180
+ 1. Predicate tests: suspended-only; suspended with all done; direct/transitive blocked descendants; unrelated runnable suppression; unrelated blocked-component suppression; cancelled exclusion.
181
+ 2. Attention reducer/outbox tests: persist-before-emit; canonical sorting; unchanged reconcile dedupe; changed exact set creates one new episode; false clears; stale acknowledgement cannot acknowledge a newer fingerprint.
182
+ 3. Widget/render tests: pending and failed labels differ; `pending -> failed` repaints with unchanged task state; focus clear removes widget/status; attention fingerprint invalidates cache.
183
+
184
+ ### Extension integration/regression tests
185
+
186
+ 1. Create concurrent squads A and B with independent fake schedulers. Focus A; omitted tool cancellation rejects; explicit B cancellation changes only B and names B; A remains displayed/running.
187
+ 2. Repeat with B persisted but no scheduler. Assert no implicit focus and no stop/delete of A.
188
+ 3. Cancel focused A via tool and via `/squad cancel`; assert exact named notification and widget/status clear.
189
+ 4. Record a failed review after a pending widget render; assert compact/status/detail/list say `REVIEW FAILED · awaiting same-squad rework`, evidence remains byte-for-byte in active review, and rework archives it unchanged.
190
+ 5. Live transition into suspended-only and suspended-blocking DAG stalls sends exactly one follow-up with all exact IDs and exact-squad `resume_task` guidance.
191
+ 6. Call reconcile repeatedly and advance the 60-second path; assert no duplicate wake for the same fingerprint.
192
+ 7. Reconstruct extension/scheduler from persisted pending attention; assert one delivery. Reconstruct from delivered attention; assert durable prompt/status visibility and no duplicate follow-up.
193
+ 8. Explicitly resume one exact task; assert its original session/mailbox is reused, other suspended tasks remain suspended, attention updates/clears correctly, and blocked descendants schedule only under existing dependency rules.
194
+ 9. Reopen a completed task through `squad_message`, wait until its original-session child is live and persisted `in_progress`, then immediately call exact `resume_task`. Assert truthful already-running text names squad/task, status never becomes `pending`, child spawn count remains one, session/mailbox identity is unchanged, and one eventual `agent_settled` produces exactly one completion/review transition.
195
+
196
+ Run the full native suite after focused tests (`npm test`), focused TypeScript checks used by the repository, extension-load smoke if available, and `git diff --check`. No test may rely only on emitted text: assert persisted squad/task state, scheduler map/focus state, widget/status calls, and notification counts.
197
+
198
+ ## 6. Implementation boundaries
199
+
200
+ - Do not change task-owned session identity or mailbox routing/retention.
201
+ - Do not mutate, replace, or truncate failed review evidence; archive only through existing same-squad rework behavior.
202
+ - Do not auto-unpause or infer consent from restart/reconcile/dependency edits.
203
+ - Do not add cascading task cancellation or make cancelled dependencies successful.
204
+ - Prefer small shared helpers for exact target resolution, focus/widget synchronization, review presentation, and the pure suspended-stall derivation; avoid unrelated lifecycle refactors.
@@ -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.