pi-squad 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -1
- package/package.json +2 -1
- package/src/inline-input.ts +139 -0
- package/src/planner.ts +2 -3
- package/src/skills/squad-plan/SKILL.md +103 -0
- package/src/skills/squad-plan/validate-spec.mjs +80 -0
- package/src/skills/squad-supervisor/SKILL.md +1 -0
- package/src/tools-registration.ts +40 -23
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.19.1] - 2026-07-18
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- `squad-plan` validator now works from an installed package: Node refuses TypeScript type stripping under `node_modules`, so `validate-spec.mjs` stages the real validator sources in a temp directory before importing them. Validation output is byte-identical.
|
|
15
|
+
|
|
16
|
+
## [0.19.0] - 2026-07-18
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `squad-plan` skill: authoring guide for inline plans and strict v1 file specifications, an error→fix map, and a bundled `validate-spec.mjs` that runs the exact tool validator and prints the ready-to-use `specSha256`.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- The `squad` tool now accepts JSON-encoded strings for `tasks`, `agents`, and `config` and decodes them with precise per-field errors, so transports that stringify structured arguments no longer fail valid plans (`tasks: must be array`). Verified live: a real Opus 4.8 session emitted stringified tasks unprompted and the squad started correctly.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Agent `model`/`thinking` now follow configuration unless the user explicitly requests otherwise: the planner prompt, squad tool schema/guidelines, and skills all instruct omitting overrides so agent definitions and `/squad defaults` apply.
|
|
29
|
+
|
|
10
30
|
## [0.18.0] - 2026-07-18
|
|
11
31
|
|
|
12
32
|
### Added
|
|
@@ -64,7 +84,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
64
84
|
|
|
65
85
|
- Kept failed-review rework in the original squad, preserving task ownership and durable session continuity.
|
|
66
86
|
|
|
67
|
-
[Unreleased]: https://github.com/picassio/pi-squad/compare/v0.
|
|
87
|
+
[Unreleased]: https://github.com/picassio/pi-squad/compare/v0.19.1...HEAD
|
|
88
|
+
[0.19.1]: https://github.com/picassio/pi-squad/compare/v0.19.0...v0.19.1
|
|
89
|
+
[0.19.0]: https://github.com/picassio/pi-squad/compare/v0.18.0...v0.19.0
|
|
68
90
|
[0.18.0]: https://github.com/picassio/pi-squad/compare/v0.17.2...v0.18.0
|
|
69
91
|
[0.17.2]: https://github.com/picassio/pi-squad/compare/v0.17.1...v0.17.2
|
|
70
92
|
[0.17.1]: https://github.com/picassio/pi-squad/compare/v0.17.0...v0.17.1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-squad",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "Multi-agent collaboration extension for pi — task decomposition, dependency management, parallel execution, TUI panel",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"src/index.ts"
|
|
12
12
|
],
|
|
13
13
|
"skills": [
|
|
14
|
+
"src/skills/squad-plan",
|
|
14
15
|
"src/skills/squad-supervisor",
|
|
15
16
|
"src/skills/squad-backend-dev",
|
|
16
17
|
"src/skills/squad-code-review",
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inline-input.ts — tolerant decoding for inline squad-start arguments.
|
|
3
|
+
*
|
|
4
|
+
* Some tool transports (MCP bridges, JSON-over-JSON encoders) deliver the
|
|
5
|
+
* structured `tasks`/`agents`/`config` fields as JSON-encoded strings instead
|
|
6
|
+
* of arrays/objects. The squad tool accepts both and coerces strings back to
|
|
7
|
+
* structures with precise errors, so a correct plan never fails on transport
|
|
8
|
+
* shape alone. Semantic plan validation (ids, dependencies, cycles, agents)
|
|
9
|
+
* still happens in plan-rules/startSquad after coercion.
|
|
10
|
+
*/
|
|
11
|
+
import type { InlineSquadStart } from "./runtime.js";
|
|
12
|
+
|
|
13
|
+
export type InlineCoercion =
|
|
14
|
+
| { ok: true; value: InlineSquadStart }
|
|
15
|
+
| { ok: false; error: string };
|
|
16
|
+
|
|
17
|
+
type InlineTask = NonNullable<InlineSquadStart["tasks"]>[number];
|
|
18
|
+
type InlineAgents = NonNullable<InlineSquadStart["agents"]>;
|
|
19
|
+
type InlineConfig = NonNullable<InlineSquadStart["config"]>;
|
|
20
|
+
|
|
21
|
+
function parseIfString(value: unknown, field: string): { ok: true; value: unknown } | { ok: false; error: string } {
|
|
22
|
+
if (typeof value !== "string") return { ok: true, value };
|
|
23
|
+
try {
|
|
24
|
+
return { ok: true, value: JSON.parse(value) };
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return { ok: false, error: `${field} arrived as a JSON string that is not valid JSON (${(error as Error).message}). Send a real ${field === "tasks" ? "array" : "object"}, or a JSON-encoded one.` };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
31
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function coerceTasks(input: unknown): { ok: true; value: InlineTask[] | undefined } | { ok: false; error: string } {
|
|
35
|
+
if (input === undefined) return { ok: true, value: undefined };
|
|
36
|
+
if (!Array.isArray(input)) return { ok: false, error: "tasks must be an array of task objects (or a JSON-encoded array)." };
|
|
37
|
+
const tasks: InlineTask[] = [];
|
|
38
|
+
for (let i = 0; i < input.length; i++) {
|
|
39
|
+
const raw = input[i];
|
|
40
|
+
if (!isPlainObject(raw)) return { ok: false, error: `tasks[${i}] must be an object.` };
|
|
41
|
+
for (const key of ["id", "title", "agent"] as const) {
|
|
42
|
+
if (typeof raw[key] !== "string" || raw[key].length === 0) {
|
|
43
|
+
return { ok: false, error: `tasks[${i}].${key} must be a nonempty string.` };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (raw.description !== undefined && typeof raw.description !== "string") {
|
|
47
|
+
return { ok: false, error: `tasks[${i}].description must be a string when present.` };
|
|
48
|
+
}
|
|
49
|
+
if (raw.depends !== undefined && (!Array.isArray(raw.depends) || !raw.depends.every((d) => typeof d === "string"))) {
|
|
50
|
+
return { ok: false, error: `tasks[${i}].depends must be an array of task-id strings when present.` };
|
|
51
|
+
}
|
|
52
|
+
if (raw.inheritContext !== undefined && typeof raw.inheritContext !== "boolean") {
|
|
53
|
+
return { ok: false, error: `tasks[${i}].inheritContext must be a boolean when present.` };
|
|
54
|
+
}
|
|
55
|
+
tasks.push({
|
|
56
|
+
id: raw.id as string,
|
|
57
|
+
title: raw.title as string,
|
|
58
|
+
agent: raw.agent as string,
|
|
59
|
+
...(raw.description !== undefined ? { description: raw.description as string } : {}),
|
|
60
|
+
...(raw.depends !== undefined ? { depends: raw.depends as string[] } : {}),
|
|
61
|
+
...(raw.inheritContext !== undefined ? { inheritContext: raw.inheritContext as boolean } : {}),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, value: tasks };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function coerceAgents(input: unknown): { ok: true; value: InlineAgents | undefined } | { ok: false; error: string } {
|
|
68
|
+
if (input === undefined) return { ok: true, value: undefined };
|
|
69
|
+
if (!isPlainObject(input)) return { ok: false, error: "agents must be an object mapping agent names to overrides (or a JSON-encoded object)." };
|
|
70
|
+
const agents: InlineAgents = {};
|
|
71
|
+
for (const [name, raw] of Object.entries(input)) {
|
|
72
|
+
if (!isPlainObject(raw)) return { ok: false, error: `agents.${name} must be an object.` };
|
|
73
|
+
if (raw.model !== undefined && typeof raw.model !== "string") return { ok: false, error: `agents.${name}.model must be a string when present.` };
|
|
74
|
+
if (raw.thinking !== undefined && typeof raw.thinking !== "string") return { ok: false, error: `agents.${name}.thinking must be a string when present.` };
|
|
75
|
+
agents[name] = {
|
|
76
|
+
...(raw.model !== undefined ? { model: raw.model as string } : {}),
|
|
77
|
+
...(raw.thinking !== undefined ? { thinking: raw.thinking as string } : {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, value: agents };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function coerceConfig(input: unknown): { ok: true; value: InlineConfig | undefined } | { ok: false; error: string } {
|
|
84
|
+
if (input === undefined) return { ok: true, value: undefined };
|
|
85
|
+
if (!isPlainObject(input)) return { ok: false, error: "config must be an object (or a JSON-encoded object)." };
|
|
86
|
+
if (input.maxConcurrency !== undefined && typeof input.maxConcurrency !== "number") {
|
|
87
|
+
return { ok: false, error: "config.maxConcurrency must be a number when present." };
|
|
88
|
+
}
|
|
89
|
+
if (input.autoUnblock !== undefined && typeof input.autoUnblock !== "boolean") {
|
|
90
|
+
return { ok: false, error: "config.autoUnblock must be a boolean when present." };
|
|
91
|
+
}
|
|
92
|
+
if (input.maxRetries !== undefined && typeof input.maxRetries !== "number") {
|
|
93
|
+
return { ok: false, error: "config.maxRetries must be a number when present." };
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
value: {
|
|
98
|
+
...(input.maxConcurrency !== undefined ? { maxConcurrency: input.maxConcurrency as number } : {}),
|
|
99
|
+
...(input.autoUnblock !== undefined ? { autoUnblock: input.autoUnblock as boolean } : {}),
|
|
100
|
+
...(input.maxRetries !== undefined ? { maxRetries: input.maxRetries as number } : {}),
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Decode inline squad-start input, accepting JSON-encoded structured fields. */
|
|
106
|
+
export function coerceInlineSquadStart(raw: {
|
|
107
|
+
goal: string;
|
|
108
|
+
agents?: unknown;
|
|
109
|
+
tasks?: unknown;
|
|
110
|
+
config?: unknown;
|
|
111
|
+
}): InlineCoercion {
|
|
112
|
+
if (typeof raw.goal !== "string" || raw.goal.trim().length === 0) {
|
|
113
|
+
return { ok: false, error: "goal must be a nonempty string." };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const tasksParsed = parseIfString(raw.tasks, "tasks");
|
|
117
|
+
if (!tasksParsed.ok) return tasksParsed;
|
|
118
|
+
const agentsParsed = parseIfString(raw.agents, "agents");
|
|
119
|
+
if (!agentsParsed.ok) return agentsParsed;
|
|
120
|
+
const configParsed = parseIfString(raw.config, "config");
|
|
121
|
+
if (!configParsed.ok) return configParsed;
|
|
122
|
+
|
|
123
|
+
const tasks = coerceTasks(tasksParsed.value);
|
|
124
|
+
if (!tasks.ok) return tasks;
|
|
125
|
+
const agents = coerceAgents(agentsParsed.value);
|
|
126
|
+
if (!agents.ok) return agents;
|
|
127
|
+
const config = coerceConfig(configParsed.value);
|
|
128
|
+
if (!config.ok) return config;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
ok: true,
|
|
132
|
+
value: {
|
|
133
|
+
goal: raw.goal,
|
|
134
|
+
...(agents.value !== undefined ? { agents: agents.value } : {}),
|
|
135
|
+
...(tasks.value !== undefined ? { tasks: tasks.value } : {}),
|
|
136
|
+
...(config.value !== undefined ? { config: config.value } : {}),
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
package/src/planner.ts
CHANGED
|
@@ -251,8 +251,7 @@ Respond with a JSON object (and nothing else outside the JSON):
|
|
|
251
251
|
\`\`\`json
|
|
252
252
|
{
|
|
253
253
|
"agents": {
|
|
254
|
-
"agent-name": {}
|
|
255
|
-
"agent-name": { "model": "override-model-if-needed", "thinking": "high" }
|
|
254
|
+
"agent-name": {}
|
|
256
255
|
},
|
|
257
256
|
"tasks": [
|
|
258
257
|
{
|
|
@@ -269,7 +268,7 @@ Respond with a JSON object (and nothing else outside the JSON):
|
|
|
269
268
|
## Rules
|
|
270
269
|
${PLAN_STRUCTURE_RULES}
|
|
271
270
|
- Only reference agents that exist in the Available Agents list
|
|
272
|
-
- Agent
|
|
271
|
+
- Agent entries stay empty ({}) so each agent uses its configured model/thinking (agent definition, then squad defaults). Set "model" (a pi model id) or "thinking" (off, minimal, low, medium, high, xhigh, max) ONLY when the goal explicitly requests a specific model or thinking level
|
|
273
272
|
|
|
274
273
|
## Task Descriptions
|
|
275
274
|
${TASK_DESCRIPTION_GUIDE}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: squad-plan
|
|
3
|
+
description: Author valid pi-squad plans — inline task arrays and strict v1 file specifications. Use when creating a squad plan, writing a squad spec JSON, choosing between inline tasks and specFile, computing specSha256, or debugging squad validation errors (must be array, SPEC_MALFORMED, SPEC_HASH_MISMATCH, Plan rejected).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Squad Plan Authoring
|
|
7
|
+
|
|
8
|
+
Two ways to start a squad. Pick one:
|
|
9
|
+
|
|
10
|
+
| Form | When |
|
|
11
|
+
|---|---|
|
|
12
|
+
| **Inline** `squad({ goal, tasks?, agents?, config? })` | Normal plans. Goal + tasks fit comfortably in a tool call. |
|
|
13
|
+
| **File spec** `squad({ specFile, specSha256 })` | Large contracts, many tasks, exact-bytes requirements, or when your transport keeps mangling inline arguments. Children mechanically attest full spec reading. |
|
|
14
|
+
|
|
15
|
+
## Planner rules (both forms)
|
|
16
|
+
|
|
17
|
+
- 3–7 tasks is usually right (hard limit 128 in file specs; >9 warns).
|
|
18
|
+
- First task(s) have empty `depends`. No cycles; every dependency must name an existing task id.
|
|
19
|
+
- When tasks share an interface (API, schema, format), add a design/contract task first and make consumers depend on it.
|
|
20
|
+
- Add a final QA/verification task for user-facing changes.
|
|
21
|
+
- Task ids: short kebab-case (`setup-db`, `auth-middleware`). File specs enforce `^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$`.
|
|
22
|
+
- Structure descriptions as: Goal (outcome first), Context (files to read), Output (deliverable), Boundaries (what must not change), Verify (proving command).
|
|
23
|
+
- Every task's `agent` must name an existing agent definition (defaults: architect, backend, debugger, devops, docs, frontend, fullstack, qa, researcher, reviewer, security).
|
|
24
|
+
- **Model/thinking come from configuration.** Omit inline `agents` overrides and keep file-spec entries at `{ "model": null, "thinking": null }` unless the user explicitly asked for a specific model or thinking level. Configured values resolve as: squad-plan override (only if set) → agent definition (`~/.pi/squad/agents/*.json` or project `.pi/squad/agents/`) → `/squad defaults` policy in `~/.pi/squad/settings.json`.
|
|
25
|
+
|
|
26
|
+
## Inline form
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"goal": "Complete user outcome/acceptance contract, preserved for review",
|
|
31
|
+
"tasks": [
|
|
32
|
+
{ "id": "api-contract", "title": "Define API contract", "agent": "architect", "depends": [] },
|
|
33
|
+
{ "id": "build-api", "title": "Implement API", "agent": "backend", "depends": ["api-contract"],
|
|
34
|
+
"description": "Goal: ... Context: ... Output: ... Boundaries: ... Verify: ..." },
|
|
35
|
+
{ "id": "qa", "title": "Verify end to end", "agent": "qa", "depends": ["build-api"] }
|
|
36
|
+
],
|
|
37
|
+
"config": { "maxConcurrency": 2 }
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- `tasks`, `agents`, and `config` must be real JSON structures. If your transport stringifies them, pi-squad also accepts JSON-encoded strings for these three fields and decodes them — but prefer real structures.
|
|
42
|
+
- Omitting `tasks` runs the planner agent on `goal`.
|
|
43
|
+
- `Plan rejected:` errors list exact structural problems (duplicate ids, unknown dependency, cycle, no entry task). Fix and resubmit; warnings (`⚠️`) don't block.
|
|
44
|
+
|
|
45
|
+
## File-spec form (strict v1)
|
|
46
|
+
|
|
47
|
+
Validation is strict and fail-closed: unknown keys, missing keys, duplicate JSON keys, a UTF-8 BOM, invalid UTF-8, or a wrong hash all reject before anything is scheduled. **Every field below is required — including empty ones.**
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"schemaVersion": 1,
|
|
52
|
+
"goal": "Complete acceptance contract...",
|
|
53
|
+
"tasks": [
|
|
54
|
+
{
|
|
55
|
+
"id": "api-contract",
|
|
56
|
+
"title": "Define API contract",
|
|
57
|
+
"description": "Goal: ... Verify: ...",
|
|
58
|
+
"agent": "architect",
|
|
59
|
+
"depends": [],
|
|
60
|
+
"inheritContext": false,
|
|
61
|
+
"artifactRefs": []
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"agents": {
|
|
65
|
+
"architect": { "model": null, "thinking": null }
|
|
66
|
+
},
|
|
67
|
+
"config": { "maxConcurrency": 2, "autoUnblock": true, "maxRetries": 2 },
|
|
68
|
+
"artifacts": []
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Rules that trip people up:
|
|
73
|
+
|
|
74
|
+
- Every agent used by a task must appear in `agents`, each entry exactly `{ "model": string|null, "thinking": null|"off"|"minimal"|"low"|"medium"|"high"|"xhigh"|"max" }`. Use `null`/`null` (configured defaults) unless the user explicitly requested a specific model or thinking level.
|
|
75
|
+
- `config` needs all three keys: `maxConcurrency` (1–64), `autoUnblock` (boolean), `maxRetries` (0–20).
|
|
76
|
+
- Each task needs all seven keys, even when `description` is `""` and `artifactRefs` is `[]`.
|
|
77
|
+
- No Base64 blobs or oversized embedded content (spec ≤ 1 MiB; embedded text budgets enforced). Reference big content via `artifacts`: absolute `path`, exact lowercase `sha256`, exact `bytes`, nonempty `purpose`.
|
|
78
|
+
- `specSha256` is the SHA-256 of the exact file bytes, 64 lowercase hex chars. Write the file first, hash second, never edit afterward.
|
|
79
|
+
|
|
80
|
+
## Workflow: write → validate → call
|
|
81
|
+
|
|
82
|
+
1. Write the spec to a file (UTF-8, no BOM). Generate it with a script (`JSON.stringify`) rather than by hand to avoid duplicate keys and escaping mistakes.
|
|
83
|
+
2. Validate with the bundled validator — it runs the exact same code the squad tool runs and prints the ready-to-use hash:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
node <skill-dir>/validate-spec.mjs /path/to/spec.v1.json
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
On success it prints `VALID`, the `specSha256`, and the exact `squad(...)` call. On failure it prints the same `SPEC_MALFORMED`/`SPEC_TOO_LARGE`/`ARTIFACT_*` error the tool would return — fix and re-run.
|
|
90
|
+
|
|
91
|
+
3. Call `squad({ specFile, specSha256 })` with the printed values. Do not restate the contract inline.
|
|
92
|
+
|
|
93
|
+
## Error → fix map
|
|
94
|
+
|
|
95
|
+
| Error | Fix |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `tasks: must be array` (tool validation) | Transport stringified your array — send a real array, or pass the JSON string (accepted), or use a file spec. |
|
|
98
|
+
| `SPEC_HASH_MISMATCH` | File changed after hashing, or hash uppercase/wrong. Re-run the validator; use its printed hash. |
|
|
99
|
+
| `SPEC_MALFORMED: unknown/requires ...` | Add every required key exactly; remove extras. See the full example above. |
|
|
100
|
+
| `SPEC_MALFORMED: duplicate task id / invalid dependency / cycle` | Fix the task graph; dependencies reference ids in this spec only. |
|
|
101
|
+
| `SPEC_MALFORMED: task X uses undeclared agent Y` | Add Y to `agents` with `{ "model": null, "thinking": null }`. |
|
|
102
|
+
| `SPEC_TOO_LARGE` | Move large content to `artifacts` (path + sha256 + bytes) and reference via `artifactRefs`. |
|
|
103
|
+
| `Plan rejected:` (inline) | Structural plan errors listed verbatim — apply each fix. |
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* validate-spec.mjs — validate a pi-squad strict v1 file spec and print the
|
|
4
|
+
* exact specSha256 to pass to the squad tool.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node validate-spec.mjs <spec.v1.json>
|
|
7
|
+
*
|
|
8
|
+
* This runs the SAME validator the squad tool uses (src/file-spec.ts), so a
|
|
9
|
+
* VALID result here is exactly what squad({ specFile, specSha256 }) accepts.
|
|
10
|
+
*/
|
|
11
|
+
import { registerHooks } from "node:module";
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { copyFileSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
|
|
18
|
+
if (!process.features.typescript) {
|
|
19
|
+
console.error(
|
|
20
|
+
"This validator needs Node.js with type stripping: Node >= 23.6, or Node >= 22.6 run as\n" +
|
|
21
|
+
" node --experimental-strip-types validate-spec.mjs <spec.v1.json>",
|
|
22
|
+
);
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The extension sources use ".js" specifiers resolved by Pi; map them to the
|
|
27
|
+
// on-disk ".ts" files when loading the real validator outside Pi.
|
|
28
|
+
registerHooks({
|
|
29
|
+
resolve(specifier, context, nextResolve) {
|
|
30
|
+
if (specifier.startsWith(".") && specifier.endsWith(".js")) {
|
|
31
|
+
try {
|
|
32
|
+
return nextResolve(specifier, context);
|
|
33
|
+
} catch {
|
|
34
|
+
return nextResolve(specifier.replace(/\.js$/, ".ts"), context);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return nextResolve(specifier, context);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const specArg = process.argv[2];
|
|
42
|
+
if (!specArg) {
|
|
43
|
+
console.error("Usage: node validate-spec.mjs <spec.v1.json>");
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
const specPath = resolve(process.cwd(), specArg);
|
|
47
|
+
|
|
48
|
+
// Node refuses type stripping for files under node_modules, which is exactly
|
|
49
|
+
// where an installed pi-squad lives. Import the real validator from a temp
|
|
50
|
+
// copy outside node_modules; its only runtime deps are node builtins.
|
|
51
|
+
const sourceDir = fileURLToPath(new URL("../..", import.meta.url));
|
|
52
|
+
const stageDir = mkdtempSync(join(tmpdir(), "pi-squad-validate-"));
|
|
53
|
+
for (const file of ["file-spec.ts", "types.ts"]) {
|
|
54
|
+
copyFileSync(join(sourceDir, file), join(stageDir, file));
|
|
55
|
+
}
|
|
56
|
+
let prepareSpec;
|
|
57
|
+
try {
|
|
58
|
+
({ prepareSpec } = await import(pathToFileURL(join(stageDir, "file-spec.ts")).href));
|
|
59
|
+
} finally {
|
|
60
|
+
rmSync(stageDir, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const raw = readFileSync(specPath);
|
|
65
|
+
const sha256 = createHash("sha256").update(raw).digest("hex");
|
|
66
|
+
// Validation with the self-computed hash: the hash gate passes trivially and
|
|
67
|
+
// every structural/strictness rule still runs.
|
|
68
|
+
const prepared = prepareSpec(specPath, sha256, process.cwd());
|
|
69
|
+
console.log("VALID");
|
|
70
|
+
console.log(`specFile: ${specPath}`);
|
|
71
|
+
console.log(`specSha256: ${prepared.sha256}`);
|
|
72
|
+
console.log(`bytes: ${prepared.raw.length}`);
|
|
73
|
+
console.log(`tasks: ${prepared.spec.tasks.length}`);
|
|
74
|
+
console.log(`agents: ${Object.keys(prepared.spec.agents).join(", ")}`);
|
|
75
|
+
console.log("");
|
|
76
|
+
console.log(`squad({ specFile: ${JSON.stringify(specPath)}, specSha256: ${JSON.stringify(prepared.sha256)} })`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(`INVALID: ${error.message}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
@@ -29,6 +29,7 @@ Providing `tasks` yourself skips the planner agent — so you must apply its rul
|
|
|
29
29
|
- When tasks share an interface (API endpoints, schema, data formats), create a design/contract task FIRST and make consumers depend on it
|
|
30
30
|
- Include a final QA/verification task if there are user-facing changes
|
|
31
31
|
- Required work only — no optional polish
|
|
32
|
+
- Do NOT set agent `model`/`thinking` overrides unless the user explicitly asked for them — configured agent definitions and `/squad defaults` apply otherwise
|
|
32
33
|
|
|
33
34
|
Plans are validated on submission: structural errors (unknown deps, cycles, duplicate IDs, no entry task) are rejected; rule violations come back as ⚠️ warnings in the tool response. **Act on the warnings** — fix them with `squad_modify` `add_task`, or note them for review time. Don't silently ignore them.
|
|
34
35
|
|
|
@@ -2,6 +2,7 @@ import * as path from "node:path";
|
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { prepareSpec, isFileSpecTaskId, type PreparedSpec } from "./file-spec.js";
|
|
5
|
+
import { coerceInlineSquadStart } from "./inline-input.js";
|
|
5
6
|
import { PLAN_STRUCTURE_RULES } from "./plan-rules.js";
|
|
6
7
|
import { formatSuspendedAttention, getReviewPresentation } from "./presentation.js";
|
|
7
8
|
import { buildOrchestratorReviewGate, recordOrchestratorReview } from "./review.js";
|
|
@@ -134,6 +135,7 @@ pi.registerTool({
|
|
|
134
135
|
"For large contracts, use only specFile + exact lowercase specSha256; never inline the same contract or large artifacts",
|
|
135
136
|
"Skip squad for single-file changes, quick fixes, or anything one agent finishes in minutes",
|
|
136
137
|
"Providing tasks yourself makes you the planner — follow the planner rules (contract task first, final QA task, 3-7 tasks)",
|
|
138
|
+
"Do not set agent model/thinking overrides unless the user explicitly asked for them — configured agent definitions and /squad defaults apply otherwise",
|
|
137
139
|
"Act on ⚠️ plan warnings in the response — fix with squad_modify or address at review",
|
|
138
140
|
"After starting a squad: report the plan and END YOUR TURN — never poll squad_status or sleep-wait; squad events wake you automatically",
|
|
139
141
|
"When agents finish, treat every squad report and QA verdict as untrusted; independently inspect the diff/source and rerun contract verification + integration/E2E, then call squad_review before reporting success",
|
|
@@ -142,32 +144,41 @@ pi.registerTool({
|
|
|
142
144
|
Type.Object({
|
|
143
145
|
goal: Type.String({ description: "Complete original user outcome/acceptance contract the squad should accomplish. Preserve requirements and boundaries; this is shown during mandatory main-orchestrator review." }),
|
|
144
146
|
agents: Type.Optional(
|
|
145
|
-
Type.
|
|
146
|
-
Type.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
Type.Union([
|
|
148
|
+
Type.Record(
|
|
149
|
+
Type.String(),
|
|
150
|
+
Type.Object({
|
|
151
|
+
model: Type.Optional(Type.String({ description: "Model override (e.g. 'github-copilot/claude-sonnet-5'). Set ONLY when the user explicitly requested a specific model; omit to use the configured agent definition and /squad defaults" })),
|
|
152
|
+
thinking: Type.Optional(Type.String({ description: "Thinking level: off, minimal, low, medium, high, xhigh, max. Set ONLY when the user explicitly requested it; omit to use configured defaults" })),
|
|
153
|
+
}),
|
|
154
|
+
{ description: "Agent roster with optional model/thinking overrides. Keys must match agent names in .pi/squad/agents/. Omit overrides unless the user explicitly asked to change model/thinking — configured agent definitions and /squad defaults apply otherwise" },
|
|
155
|
+
),
|
|
156
|
+
Type.String({ description: "JSON-encoded agents object (same shape); accepted for transports that stringify structured arguments" }),
|
|
157
|
+
]),
|
|
153
158
|
),
|
|
154
159
|
tasks: Type.Optional(
|
|
155
|
-
Type.
|
|
156
|
-
Type.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
160
|
+
Type.Union([
|
|
161
|
+
Type.Array(
|
|
162
|
+
Type.Object({
|
|
163
|
+
id: Type.String(),
|
|
164
|
+
title: Type.String(),
|
|
165
|
+
description: Type.Optional(Type.String({ description: "Structure as: Goal (outcome first, not steps), Context (files/contracts to read), Output (deliverable), Boundaries (what must NOT change), Verify (command that proves it works). Include only the parts that help." })),
|
|
166
|
+
agent: Type.String(),
|
|
167
|
+
depends: Type.Optional(Type.Array(Type.String())),
|
|
168
|
+
inheritContext: Type.Optional(Type.Boolean({ description: "Fork the current pi session so the agent inherits this conversation's full context. Use ONLY when the task depends on decisions/details discussed here that can't be restated briefly. Costly (agent pays the whole history as input each turn) and auto-skipped when the session exceeds 50% of the agent model's context window — prefer restating key context in the description." })),
|
|
169
|
+
}),
|
|
170
|
+
{ description: "Pre-defined task breakdown. If provided, skips the planner agent. Scope tasks to required work only — no optional polish." },
|
|
171
|
+
),
|
|
172
|
+
Type.String({ description: "JSON-encoded tasks array (same item shape); accepted for transports that stringify structured arguments" }),
|
|
173
|
+
]),
|
|
166
174
|
),
|
|
167
175
|
config: Type.Optional(
|
|
168
|
-
Type.
|
|
169
|
-
|
|
170
|
-
|
|
176
|
+
Type.Union([
|
|
177
|
+
Type.Object({
|
|
178
|
+
maxConcurrency: Type.Optional(Type.Number({ description: "Max parallel agents (default: 2)" })),
|
|
179
|
+
}),
|
|
180
|
+
Type.String({ description: "JSON-encoded config object (same shape); accepted for transports that stringify structured arguments" }),
|
|
181
|
+
]),
|
|
171
182
|
),
|
|
172
183
|
}, { additionalProperties: false }),
|
|
173
184
|
Type.Object({
|
|
@@ -195,7 +206,13 @@ pi.registerTool({
|
|
|
195
206
|
prepared = prepareSpec(params.specFile, params.specSha256, ctx.cwd);
|
|
196
207
|
effective = { goal: prepared.spec.goal, agents: prepared.spec.agents, tasks: prepared.spec.tasks, config: prepared.spec.config };
|
|
197
208
|
} else {
|
|
198
|
-
|
|
209
|
+
// Some transports stringify structured arguments; decode tolerantly with
|
|
210
|
+
// precise errors instead of failing a correct plan on transport shape.
|
|
211
|
+
const coerced = coerceInlineSquadStart(params);
|
|
212
|
+
if (!coerced.ok) {
|
|
213
|
+
return { content: [{ type: "text" as const, text: `Invalid squad input: ${coerced.error} No squad was started.` }], details: undefined };
|
|
214
|
+
}
|
|
215
|
+
effective = coerced.value;
|
|
199
216
|
}
|
|
200
217
|
const baseId = store.makeTaskId(effective.goal) || `squad-${prepared?.sha256.slice(0, 12)}`;
|
|
201
218
|
const squadId = store.squadExists(baseId) ? `${baseId}-${Date.now().toString(36)}` : baseId;
|