squadrant 0.19.0 → 0.19.2
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/dist/index.js +1341 -346
- package/dist/index.js.map +1 -1
- package/dist/squadrantd.js +1097 -249
- package/dist/squadrantd.js.map +1 -1
- package/package.json +1 -1
- package/plugin/skills/captain-ops/SKILL.md +4 -3
- package/scripts/control-event-table.mjs +227 -0
- package/templates/captain.claude.md +1 -0
- package/templates/captain.generic.md +1 -0
- package/templates/crew.claude.md +2 -0
- package/templates/crew.generic.md +2 -0
- package/templates/crew.opencode.md +2 -0
package/package.json
CHANGED
|
@@ -214,7 +214,7 @@ You don't have an Agent Team or `TaskCreate`/`TaskUpdate` tools — those were C
|
|
|
214
214
|
|
|
215
215
|
If you ever need a bounded check (not a loop), use a fixed counter (≤ 3 attempts with a sleep between), or watch the mailbox seq — never an unbounded `until` loop.
|
|
216
216
|
|
|
217
|
-
### Handling CREW IDLE
|
|
217
|
+
### Handling CREW IDLE / BLOCKED
|
|
218
218
|
|
|
219
219
|
CREW IDLE is **ambiguous** — the watchdog did not detect a heartbeat, which can happen when:
|
|
220
220
|
- **(a)** The crew finished but never ran `squadrant crew signal done` (issue #278 — common for claude/opencode before the completion-protocol fix).
|
|
@@ -226,12 +226,13 @@ On CREW IDLE, do a **single on-demand spot-check** (allowed — not a polling lo
|
|
|
226
226
|
| Spot-check shows | Captain action |
|
|
227
227
|
|-----------------|----------------|
|
|
228
228
|
| Completed work (PR opened, commits pushed, results reported) but no CREW DONE | Treat as the #278 case — review, then follow the HUMAN REVIEW GATE contract: surface the diff and wait for operator go-ahead; never merge unprompted. If not actually done, **re-task**: send the next instruction via `crew send` (the #148 re-open flow). |
|
|
229
|
-
| Crew asked a question or is waiting for a decision | Respond via `crew send`. Do NOT terminalize — it will signal done after the next turn. |
|
|
229
|
+
| Crew asked a question or is waiting for a decision (plain text, no rendered options) | Respond via `crew send`. Do NOT terminalize — it will signal done after the next turn. |
|
|
230
|
+
| Crew has an **open AskUserQuestion/permission prompt** (a rendered `❯ 1. …` option list) | `crew send` will correctly **refuse** to touch it — don't fight the refusal or fall back to `crew close` + re-spawn. Read the options with `squadrant crew read <project> <name>`, then answer deliberately with `squadrant crew answer <project> <name> <option>` (#592). Never guess with a bare Enter. |
|
|
230
231
|
| Still mid-task / transient idle | Leave it; wait for the next daemon event. |
|
|
231
232
|
|
|
232
233
|
**Do not re-send the original task** if the crew appears to have completed it — that triggers a duplicate run. Read the crew screen or diff first, then decide: terminalize vs re-task vs leave.
|
|
233
234
|
|
|
234
|
-
This is the captain-side backstop: even if the completion-protocol imperative is skipped, the lifecycle still terminalizes because the captain classifies intent instead of letting the task strand at IDLE.
|
|
235
|
+
This is the captain-side backstop: even if the completion-protocol imperative is skipped, the lifecycle still terminalizes because the captain classifies intent instead of letting the task strand at IDLE. The same BLOCKED-on-a-real-prompt case applies whether the daemon fires an explicit CREW BLOCKED event or you only notice via a spot-check on IDLE.
|
|
235
236
|
|
|
236
237
|
When a crew sends you a status message via `squadrant runtime send <project> "<message>"`, it lands in your captain pane. Acknowledge, then update your handoff if a meaningful decision was made.
|
|
237
238
|
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Generates docs/generated/control-events.md: every ControlEvent variant vs its
|
|
3
|
+
// producers (emit sites) and consumers (state-machine case, reduce.ts allowlist,
|
|
4
|
+
// telegram formatter), plus registered LifecycleSource count vs CLAUDE.md's claim.
|
|
5
|
+
//
|
|
6
|
+
// `--check` regenerates the table in memory and exits non-zero if it differs
|
|
7
|
+
// from what's on disk, OR if a ControlEvent variant has zero shipped producers
|
|
8
|
+
// and is not in KNOWN_ZERO_PRODUCER below. That's what makes a new zombie event
|
|
9
|
+
// or doc drift fail `pnpm test` instead of sitting unnoticed.
|
|
10
|
+
//
|
|
11
|
+
// Plain Node, no dependencies. See docs/specs/2026-08-29-event-architecture-design.md §1.5, §11.
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync, readdirSync, existsSync } from "node:fs";
|
|
14
|
+
import { join, relative } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
18
|
+
const CONTROL_TYPES_FILE = join(ROOT, "packages/shared/src/types/control.ts");
|
|
19
|
+
const STATE_MACHINE_FILE = join(ROOT, "packages/core/src/state-machine.ts");
|
|
20
|
+
const REDUCE_FILE = join(ROOT, "packages/core/src/daemon/reduce.ts");
|
|
21
|
+
const TELEGRAM_FORMAT_FILE = join(ROOT, "packages/core/src/telegram/format.ts");
|
|
22
|
+
const SQUADRANTD_FILE = join(ROOT, "packages/cli/src/squadrantd.ts");
|
|
23
|
+
const CLAUDE_MD_FILE = join(ROOT, "CLAUDE.md");
|
|
24
|
+
const OUT_FILE = join(ROOT, "docs/generated/control-events.md");
|
|
25
|
+
const PACKAGES_DIR = join(ROOT, "packages");
|
|
26
|
+
|
|
27
|
+
// Variants verified (2026-08-29 event-architecture design, §1.5) to have a
|
|
28
|
+
// state-machine case, a reduce.ts allowlist entry, and (for task.idle) a
|
|
29
|
+
// telegram formatter — but zero shipped producers. Removing an entry here
|
|
30
|
+
// without giving it a real producer is a lie; adding one back is a real
|
|
31
|
+
// regression. Either way this list must be a deliberate edit, not silent.
|
|
32
|
+
const KNOWN_ZERO_PRODUCER = ["heartbeat", "task.idle", "task.reconcile-failed"];
|
|
33
|
+
|
|
34
|
+
function escapeRe(s) {
|
|
35
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function walkSourceFiles(dir, out = []) {
|
|
39
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
40
|
+
if (entry.name === "__tests__" || entry.name === "dist" || entry.name === "node_modules") continue;
|
|
41
|
+
const full = join(dir, entry.name);
|
|
42
|
+
if (entry.isDirectory()) walkSourceFiles(full, out);
|
|
43
|
+
else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx"))) out.push(full);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function extractVariants(controlTypesText) {
|
|
49
|
+
const variants = [];
|
|
50
|
+
const re = /^\s*\|\s*\{\s*type:\s*"([^"]+)"/gm;
|
|
51
|
+
let m;
|
|
52
|
+
while ((m = re.exec(controlTypesText))) variants.push(m[1]);
|
|
53
|
+
return variants;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Producer = a line constructing `{ type: "<variant>" ... }` as an object
|
|
57
|
+
// literal (an emit site). Excludes the union definition itself and
|
|
58
|
+
// `Extract<ControlEvent, { type: "..." }>` type-level narrowing, which reads
|
|
59
|
+
// like construction but declares nothing.
|
|
60
|
+
function findProducers(files, variant) {
|
|
61
|
+
const re = new RegExp(`type:\\s*["']${escapeRe(variant)}["']`);
|
|
62
|
+
const hits = [];
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
if (file === CONTROL_TYPES_FILE) continue;
|
|
65
|
+
const lines = readFileSync(file, "utf8").split("\n");
|
|
66
|
+
for (let i = 0; i < lines.length; i++) {
|
|
67
|
+
const line = lines[i];
|
|
68
|
+
if (line.includes("Extract<")) continue;
|
|
69
|
+
if (re.test(line)) hits.push(`${relative(ROOT, file)}:${i + 1}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return hits;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function hasCase(text, variant) {
|
|
76
|
+
return new RegExp(`case\\s+["']${escapeRe(variant)}["']\\s*:`).test(text);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function extractKnownEventTypes(reduceText) {
|
|
80
|
+
const m = reduceText.match(/KNOWN_EVENT_TYPES[^=]*=\s*new Set\(\[([\s\S]*?)\]\)/);
|
|
81
|
+
if (!m) throw new Error("KNOWN_EVENT_TYPES not found in reduce.ts");
|
|
82
|
+
return new Set([...m[1].matchAll(/"([^"]+)"/g)].map((x) => x[1]));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function extractLifecycleSources(squadrantdText) {
|
|
86
|
+
const block = squadrantdText.match(/ctx\.lifecycleSources\s*=\s*\[([\s\S]*?)\];/);
|
|
87
|
+
if (!block) throw new Error("ctx.lifecycleSources assignment not found in squadrantd.ts");
|
|
88
|
+
const varNames = [...block[1].matchAll(/(\w+)/g)].map((x) => x[1]);
|
|
89
|
+
return varNames.map((varName) => {
|
|
90
|
+
const decl = squadrantdText.match(new RegExp(`const\\s+${escapeRe(varName)}\\s*=\\s*new\\s+(\\w+)\\(`));
|
|
91
|
+
return decl ? decl[1] : varName;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function extractClaudeMdClaim(claudeMdText) {
|
|
96
|
+
const m = claudeMdText.match(/(\d+)\s+`LifecycleSource`\s+implementations/);
|
|
97
|
+
return m ? Number(m[1]) : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildTable() {
|
|
101
|
+
const controlTypesText = readFileSync(CONTROL_TYPES_FILE, "utf8");
|
|
102
|
+
const stateMachineText = readFileSync(STATE_MACHINE_FILE, "utf8");
|
|
103
|
+
const reduceText = readFileSync(REDUCE_FILE, "utf8");
|
|
104
|
+
const telegramText = readFileSync(TELEGRAM_FORMAT_FILE, "utf8");
|
|
105
|
+
const squadrantdText = readFileSync(SQUADRANTD_FILE, "utf8");
|
|
106
|
+
const claudeMdText = existsSync(CLAUDE_MD_FILE) ? readFileSync(CLAUDE_MD_FILE, "utf8") : "";
|
|
107
|
+
|
|
108
|
+
const variants = extractVariants(controlTypesText);
|
|
109
|
+
const sourceFiles = walkSourceFiles(PACKAGES_DIR);
|
|
110
|
+
const knownEventTypes = extractKnownEventTypes(reduceText);
|
|
111
|
+
|
|
112
|
+
const rows = variants.map((variant) => {
|
|
113
|
+
const producers = findProducers(sourceFiles, variant);
|
|
114
|
+
const zombie = producers.length === 0;
|
|
115
|
+
const allowedZombie = KNOWN_ZERO_PRODUCER.includes(variant);
|
|
116
|
+
return {
|
|
117
|
+
variant,
|
|
118
|
+
producers,
|
|
119
|
+
stateMachine: hasCase(stateMachineText, variant),
|
|
120
|
+
reduceAllowlist: knownEventTypes.has(variant),
|
|
121
|
+
telegramFormatter: hasCase(telegramText, variant),
|
|
122
|
+
zombie,
|
|
123
|
+
allowedZombie,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const unauthorizedZombies = rows.filter((r) => r.zombie && !r.allowedZombie).map((r) => r.variant);
|
|
128
|
+
const staleAllowlistEntries = KNOWN_ZERO_PRODUCER.filter(
|
|
129
|
+
(v) => !rows.some((r) => r.variant === v && r.zombie),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const registeredSources = extractLifecycleSources(squadrantdText);
|
|
133
|
+
const claudeMdClaim = extractClaudeMdClaim(claudeMdText);
|
|
134
|
+
|
|
135
|
+
const lines = [];
|
|
136
|
+
lines.push("<!-- GENERATED by scripts/control-event-table.mjs — do not hand-edit. -->");
|
|
137
|
+
lines.push("<!-- Regenerate: node scripts/control-event-table.mjs -->");
|
|
138
|
+
lines.push("");
|
|
139
|
+
lines.push("# ControlEvent producer/consumer table");
|
|
140
|
+
lines.push("");
|
|
141
|
+
lines.push(
|
|
142
|
+
"Every `ControlEvent` variant (`packages/shared/src/types/control.ts`) against its shipped " +
|
|
143
|
+
"producers and consumers. ⚠ marks a variant with zero shipped producers — see " +
|
|
144
|
+
"`docs/specs/2026-08-29-event-architecture-design.md` §1.5 / §11.",
|
|
145
|
+
);
|
|
146
|
+
lines.push("");
|
|
147
|
+
lines.push("| Variant | Producers | state-machine.ts | reduce.ts allowlist | telegram formatter |");
|
|
148
|
+
lines.push("|---|---|---|---|---|");
|
|
149
|
+
for (const r of rows) {
|
|
150
|
+
const variantCell = r.zombie ? `⚠ \`${r.variant}\`` : `\`${r.variant}\``;
|
|
151
|
+
const producerCell = r.zombie ? "0 (known zombie)" : String(r.producers.length);
|
|
152
|
+
lines.push(
|
|
153
|
+
`| ${variantCell} | ${producerCell} | ${r.stateMachine ? "✓" : "—"} | ${
|
|
154
|
+
r.reduceAllowlist ? "✓" : "—"
|
|
155
|
+
} | ${r.telegramFormatter ? "✓" : "—"} |`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
lines.push("");
|
|
159
|
+
lines.push("## Producer sites");
|
|
160
|
+
lines.push("");
|
|
161
|
+
for (const r of rows) {
|
|
162
|
+
if (r.producers.length === 0) continue;
|
|
163
|
+
lines.push(`- \`${r.variant}\``);
|
|
164
|
+
for (const hit of r.producers) lines.push(` - ${hit}`);
|
|
165
|
+
}
|
|
166
|
+
lines.push("");
|
|
167
|
+
lines.push("## Known zero-producer variants (allowlisted)");
|
|
168
|
+
lines.push("");
|
|
169
|
+
if (KNOWN_ZERO_PRODUCER.length === 0) {
|
|
170
|
+
lines.push("(none)");
|
|
171
|
+
} else {
|
|
172
|
+
for (const v of KNOWN_ZERO_PRODUCER) lines.push(`- \`${v}\``);
|
|
173
|
+
}
|
|
174
|
+
lines.push("");
|
|
175
|
+
lines.push("## LifecycleSource implementations");
|
|
176
|
+
lines.push("");
|
|
177
|
+
lines.push(`Registered in \`packages/cli/src/squadrantd.ts\` (\`ctx.lifecycleSources\`): **${registeredSources.length}**`);
|
|
178
|
+
lines.push("");
|
|
179
|
+
for (const cls of registeredSources) lines.push(`- \`${cls}\``);
|
|
180
|
+
lines.push("");
|
|
181
|
+
lines.push(
|
|
182
|
+
`CLAUDE.md claims: **${claudeMdClaim === null ? "not found" : claudeMdClaim}** ${
|
|
183
|
+
claudeMdClaim === registeredSources.length ? "(matches ✓)" : "(⚠ DRIFT — does not match)"
|
|
184
|
+
}`,
|
|
185
|
+
);
|
|
186
|
+
lines.push("");
|
|
187
|
+
|
|
188
|
+
const content = lines.join("\n");
|
|
189
|
+
return { content, unauthorizedZombies, staleAllowlistEntries, registeredSources, claudeMdClaim };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function main() {
|
|
193
|
+
const check = process.argv.includes("--check");
|
|
194
|
+
const { content, unauthorizedZombies, staleAllowlistEntries } = buildTable();
|
|
195
|
+
|
|
196
|
+
let failed = false;
|
|
197
|
+
|
|
198
|
+
if (unauthorizedZombies.length > 0) {
|
|
199
|
+
console.error(
|
|
200
|
+
`control-event-table: new zero-producer ControlEvent variant(s) not in KNOWN_ZERO_PRODUCER: ${unauthorizedZombies.join(", ")}`,
|
|
201
|
+
);
|
|
202
|
+
console.error("Either add a producer, or deliberately add the variant to KNOWN_ZERO_PRODUCER in scripts/control-event-table.mjs.");
|
|
203
|
+
failed = true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (staleAllowlistEntries.length > 0) {
|
|
207
|
+
console.error(
|
|
208
|
+
`control-event-table: KNOWN_ZERO_PRODUCER lists variant(s) that now have a producer — remove from the allowlist: ${staleAllowlistEntries.join(", ")}`,
|
|
209
|
+
);
|
|
210
|
+
failed = true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (check) {
|
|
214
|
+
const onDisk = existsSync(OUT_FILE) ? readFileSync(OUT_FILE, "utf8") : null;
|
|
215
|
+
if (onDisk !== content) {
|
|
216
|
+
console.error(`control-event-table: ${OUT_FILE} is stale. Run: node scripts/control-event-table.mjs`);
|
|
217
|
+
failed = true;
|
|
218
|
+
}
|
|
219
|
+
process.exit(failed ? 1 : 0);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
writeFileSync(OUT_FILE, content);
|
|
223
|
+
console.log(`control-event-table: wrote ${relative(ROOT, OUT_FILE)}`);
|
|
224
|
+
process.exit(failed ? 1 : 0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
main();
|
|
@@ -9,6 +9,7 @@ You are a **project captain** for Squadrant. You lead ONE project. You are a **c
|
|
|
9
9
|
3. Even a one-line fix gets a crew session. You plan, delegate, review.
|
|
10
10
|
4. **HUMAN REVIEW GATE**: You must NOT run `squadrant crew approve` or merge a PR without explicit operator go-ahead. The default is pause-and-show-the-diff. Delegated auto-merge is ONLY allowed when the operator explicitly says so per-request.
|
|
11
11
|
5. **ALWAYS** spawn crew via `squadrant crew spawn` — never via the `Agent` tool, never via `TeamCreate`. Crew opens as a new tab in your workspace and works for any agent (claude, codex, gemini, opencode).
|
|
12
|
+
6. If a question you asked resolves without a human answer (e.g. a synthetic 'No response after Ns — continued without an answer' from an AFK timeout), never treat it as approval. Take only the safe, reversible option; if none exists, take no action and report that you are still waiting.
|
|
12
13
|
|
|
13
14
|
## ALWAYS do on session start
|
|
14
15
|
|
|
@@ -23,6 +23,7 @@ You are a project captain coordinating work via cmux workspaces. You are a coord
|
|
|
23
23
|
```
|
|
24
24
|
5. **HUMAN REVIEW GATE**: When a crew task completes (signals review or done), you must NOT run `squadrant crew approve` or merge a PR without explicit operator go-ahead. The default is pause-and-show-the-diff. Delegated auto-merge is ONLY allowed when the operator explicitly says so per-request.
|
|
25
25
|
6. Record learnings (script: `~/.config/squadrant/scripts/record-learning.sh`).
|
|
26
|
+
7. If a question you asked resolves without a human answer (e.g. a synthetic 'No response after Ns — continued without an answer' from an AFK timeout), never treat it as approval. Take only the safe, reversible option; if none exists, take no action and report that you are still waiting.
|
|
26
27
|
|
|
27
28
|
## Crew Spawning
|
|
28
29
|
|
package/templates/crew.claude.md
CHANGED
|
@@ -10,6 +10,8 @@ You are a crew member working on a specific task within a git worktree.
|
|
|
10
10
|
4. You do NOT create Agent Teams (no nested teams).
|
|
11
11
|
5. When your task is complete, report back to your captain.
|
|
12
12
|
6. Commit your work to your worktree branch frequently.
|
|
13
|
+
7. Never write to a captain's memory directory or MEMORY.md — propose durable learnings in your done message instead.
|
|
14
|
+
8. If a question you asked resolves without a human answer (e.g. a synthetic 'No response after Ns — continued without an answer' from an AFK timeout), never treat it as approval. Take only the safe, reversible option; if none exists, take no action and report that you are still waiting.
|
|
13
15
|
|
|
14
16
|
## Your Worktree
|
|
15
17
|
|
|
@@ -10,6 +10,8 @@ If asked "who are you?", answer that you are a crew member working on an assigne
|
|
|
10
10
|
2. You are a single agent session working alone on your task. Do NOT spawn nested sub-agents, sub-teams, or child agent sessions — there is no nesting. Complete the work yourself in this session.
|
|
11
11
|
3. When your task is complete, commit your work and report back.
|
|
12
12
|
4. Commit your work frequently with descriptive messages.
|
|
13
|
+
5. Never write to a captain's memory directory or MEMORY.md — propose durable learnings in your done message instead.
|
|
14
|
+
6. If a question you asked resolves without a human answer (e.g. a synthetic 'No response after Ns — continued without an answer' from an AFK timeout), never treat it as approval. Take only the safe, reversible option; if none exists, take no action and report that you are still waiting.
|
|
13
15
|
|
|
14
16
|
## Your Worktree
|
|
15
17
|
|
|
@@ -10,6 +10,8 @@ If asked "who are you?", answer that you are a crew member working on an assigne
|
|
|
10
10
|
2. You are a single agent session working alone on your task. Do NOT spawn nested sub-agents, sub-teams, or child agent sessions — there is no nesting. Complete the work yourself in this session.
|
|
11
11
|
3. When your task is complete, commit your work and report back.
|
|
12
12
|
4. Commit your work frequently with descriptive messages.
|
|
13
|
+
5. Never write to a captain's memory directory or MEMORY.md — propose durable learnings in your done message instead.
|
|
14
|
+
6. If a question you asked resolves without a human answer (e.g. a synthetic 'No response after Ns — continued without an answer' from an AFK timeout), never treat it as approval. Take only the safe, reversible option; if none exists, take no action and report that you are still waiting.
|
|
13
15
|
|
|
14
16
|
## Your Worktree
|
|
15
17
|
|