pi-squad 0.17.1 → 0.17.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/README.md +12 -2
- package/docs/persistent-master-switch-contract.md +148 -0
- package/package.json +1 -1
- package/src/index.ts +134 -40
- package/src/panel/squad-panel.ts +16 -0
- package/src/store.ts +3 -1
- package/src/types.ts +2 -0
package/README.md
CHANGED
|
@@ -90,6 +90,14 @@ squad_modify({ action: "resume_task", squadId: "<exact-squad-id>", taskId: "<exa
|
|
|
90
90
|
|
|
91
91
|
Repeated reconciliation and restart do not create notification storms. A delivered stall remains visible until explicit resumption changes the state; a different suspended/blocked set is a new actionable episode.
|
|
92
92
|
|
|
93
|
+
### Persistent Master Switch
|
|
94
|
+
|
|
95
|
+
`/squad disable` persists a global master switch in `~/.pi/squad/settings.json`. It first saves the disabled state, then stops schedulers, durably suspends in-progress tasks, kills child processes, clears focus, and hides the widget. On later Pi restarts, disabled mode does not reconstruct schedulers, recover mail, resume sessions, normalize tasks, or select a squad.
|
|
96
|
+
|
|
97
|
+
While disabled, all five squad tools and every `/squad` operation fail closed with `/squad enable` guidance; only exact `/squad enable` and the idempotent exact `/squad disable` control commands run. Tool schemas remain registered. Mandatory pending/failed review gates and suspended-stall reminders remain injected because they are durable safety obligations, but they do not authorize squad work while disabled.
|
|
98
|
+
|
|
99
|
+
`/squad enable` persists the enabled state and restores widget availability without selecting a squad or resuming anything. Suspended work remains suspended until an explicit exact-task resume or `/squad resume <squad-id>`.
|
|
100
|
+
|
|
93
101
|
### QA Rework Loop
|
|
94
102
|
|
|
95
103
|
When a QA agent outputs `## Verdict: FAIL`, the scheduler automatically:
|
|
@@ -217,7 +225,7 @@ Full overlay with task list, live activity preview, and scrollable message view.
|
|
|
217
225
|
| `/squad cancel` | Cancel the visibly focused squad; the notification names it and the focused widget/status clear |
|
|
218
226
|
| `/squad clear` | Dismiss widget |
|
|
219
227
|
| `/squad cleanup` | Delete squad data |
|
|
220
|
-
| `/squad enable/disable` |
|
|
228
|
+
| `/squad enable/disable` | Persistently enable/disable all squad execution; enabling never auto-resumes work |
|
|
221
229
|
|
|
222
230
|
## Tools (LLM-callable)
|
|
223
231
|
|
|
@@ -225,6 +233,7 @@ Full overlay with task list, live activity preview, and scrollable message view.
|
|
|
225
233
|
|---|---|
|
|
226
234
|
| `squad` | Start a squad with goal + optional tasks/config |
|
|
227
235
|
| `squad_status` | Check progress, costs, task states |
|
|
236
|
+
| `squad_review` | Record the main orchestrator's independent acceptance review |
|
|
228
237
|
| `squad_message` | Durably message an exact task; completed tasks reopen on their original session |
|
|
229
238
|
| `squad_modify` | Add/cancel/complete/pause/resume tasks or squads, or replace a task's dependencies with `set_dependencies`; exact `squadId` is required for destructive whole-squad `cancel` and recommended for all task actions |
|
|
230
239
|
|
|
@@ -331,7 +340,7 @@ Agents must complete at least one LLM turn and produce either a tool call or a s
|
|
|
331
340
|
|
|
332
341
|
- Every task owns one durable Pi session. New tasks create it under their task directory; stale, suspended, failed, or explicitly reopened tasks resume that same session rather than starting over. A pre-prompt retry may refresh Pi's provisional session ID only while the bound file is the same and its JSONL has not materialized; afterward both file and ID are immutable.
|
|
333
342
|
- Legacy tasks without a session binding migrate on first reopen by creating a task-owned session and seeding its first prompt with the complete persisted multiline message history and prior task output—without truncation.
|
|
334
|
-
- In-progress tasks are **suspended** on orderly shutdown; ordinary orphaned work is paused for explicit resume.
|
|
343
|
+
- In-progress tasks are **suspended** on orderly shutdown; ordinary orphaned work is paused for explicit resume. When the persistent master switch is enabled, a reconstructed scheduler can resume stale `in_progress` state through reconciliation, and startup reconstructs project squads with pending mailbox entries—including `review` or already accepted `done` squads—to resume delivery. Disabled startup performs none of this recovery.
|
|
335
344
|
- A durable message to a completed task clears its prior completion/review state, reopens the squad, keeps the task `in_progress` while its agent is live, and requires a fresh orchestrator review after the final `agent_settled`. Every transitive descendant is re-blocked and reruns in dependency order, so results derived from the reopened dependency cannot remain falsely complete.
|
|
336
345
|
- Failure is never terminal: `resume` recovers failed squads (failed tasks reset to pending), `complete_task` marks recovered work done and schedules dependents, and a 60s reconcile loop re-derives scheduling from persisted state so out-of-band store edits can't strand ready tasks.
|
|
337
346
|
- Mail is acknowledged only after Pi accepts the correlated RPC command. Pending and acknowledged entries remain task-addressed on disk, survive process restart, and remain visible in history. Queue and acknowledgement read/modify/write operations are serialized across processes so concurrent mutations cannot overwrite messages or delivery state.
|
|
@@ -355,6 +364,7 @@ All state in `~/.pi/squad/`. No database, no daemon. Writes are atomic. JSONL re
|
|
|
355
364
|
|
|
356
365
|
```
|
|
357
366
|
~/.pi/squad/
|
|
367
|
+
├── settings.json — persistent master switch, defaults, and advisor settings
|
|
358
368
|
├── agents/ — agent definitions (user-editable)
|
|
359
369
|
├── debug.log — error and debug logging
|
|
360
370
|
└── {squad-id}/
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Persistent squad master-switch contract
|
|
2
|
+
|
|
3
|
+
Status: implementation contract (not current implementation truth)
|
|
4
|
+
|
|
5
|
+
## 1. State and persistence
|
|
6
|
+
|
|
7
|
+
The master switch is global, not project- or session-local.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
interface SquadSettings {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
// existing defaultModel, defaultThinking, and advisor fields remain unchanged
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
DEFAULT_SQUAD_SETTINGS.enabled = true;
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- Persist the value at `~/.pi/squad/settings.json` through the existing atomic settings writer.
|
|
19
|
+
- A missing settings file, a legacy file without `enabled`, or a non-boolean `enabled` uses the backward-compatible default `true`. Other settings keep their existing deep-merge behavior.
|
|
20
|
+
- Every whole-settings write (including `/squad defaults` and `/squad advisor`) must preserve the effective `enabled` value.
|
|
21
|
+
- The extension loads the effective setting before any `session_start` widget installation, focus restoration, orphan cleanup, attestation audit, scheduler construction/start, suspended-stall recovery, mailbox recovery, or squad notification.
|
|
22
|
+
- Tools and the `/squad` command remain statically registered in both states. Registration is not permission to execute.
|
|
23
|
+
- The conditional child-only `squad_spec_read` registration is not a main-session squad execution path; its existing file-spec guard remains unchanged. Disabling the parent kills its child processes.
|
|
24
|
+
|
|
25
|
+
A failed settings write fails the control command. It must not update the in-memory master state or claim success.
|
|
26
|
+
|
|
27
|
+
## 2. Disabled startup
|
|
28
|
+
|
|
29
|
+
When the loaded value is `false`, `session_start` is read-only with respect to squad work:
|
|
30
|
+
|
|
31
|
+
- do not create or start a `Scheduler`;
|
|
32
|
+
- do not audit or rewrite file-spec/task/squad state;
|
|
33
|
+
- do not normalize orphaned `in_progress` tasks;
|
|
34
|
+
- do not recover pending mail, resume sessions, acknowledge attention, or spawn children;
|
|
35
|
+
- do not select/focus a squad, install/show squad widget content, open a panel, or register an operational Ctrl+Q path;
|
|
36
|
+
- do not emit ordinary paused/recovery/completion/failure notifications.
|
|
37
|
+
|
|
38
|
+
The only lifecycle exception is the durable safety injection in section 7. Reading persisted review/attention records for that injection is allowed; reconstructing or mutating work is not.
|
|
39
|
+
|
|
40
|
+
An enabled startup retains existing recovery behavior and contracts. In particular, explicitly `suspended` tasks still never resume merely because Pi restarted.
|
|
41
|
+
|
|
42
|
+
## 3. Public tool gate
|
|
43
|
+
|
|
44
|
+
The enabled check is the first operation in every main-session tool `execute` handler, before target inference, store lookup, focus changes, scheduler construction, mailbox writes, review writes, or other mutations. While disabled, every tool returns a non-throwing result with clear guidance such as:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
pi-squad is disabled. Run /squad enable, then retry this operation; no squad work was changed.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
All five public tools registered in `src/index.ts` are covered:
|
|
51
|
+
|
|
52
|
+
| Tool | Disabled behavior |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `squad` | No planning, publication, scheduler, task, or child creation. |
|
|
55
|
+
| `squad_status` | No latest/active lookup or context refresh; fail closed rather than offering a read-only bypass. |
|
|
56
|
+
| `squad_review` | No attestation scheduler/audit and no review mutation. The durable review gate remains pending/failed and injected. |
|
|
57
|
+
| `squad_message` | No mailbox write, scheduler reconstruction, task reopening, or delivery. |
|
|
58
|
+
| `squad_modify` | Every action fails closed, including add/set-dependencies/cancel/pause/resume/complete and whole-squad actions. |
|
|
59
|
+
|
|
60
|
+
Static schemas/descriptions remain available to the model, but a call cannot execute until enabled.
|
|
61
|
+
|
|
62
|
+
## 4. Slash-command and UI gate
|
|
63
|
+
|
|
64
|
+
The `/squad` handler checks the master state before defaulting, resolving a direct squad ID, reading targets, or opening interactive UI.
|
|
65
|
+
|
|
66
|
+
While disabled:
|
|
67
|
+
|
|
68
|
+
- exact `/squad enable` is allowed;
|
|
69
|
+
- repeated `/squad disable` is an idempotent control-plane no-op that reports the extension is already disabled and points to `/squad enable`; it never reconstructs work;
|
|
70
|
+
- every other form returns one clear disabled notification and performs no squad-work or settings mutation.
|
|
71
|
+
|
|
72
|
+
This covers every registered branch and fallback in `src/index.ts`:
|
|
73
|
+
|
|
74
|
+
| Form | Disabled result |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `/squad`, `/squad select`, `/squad list`, `/squad all`, `/squad <squad-id>` | No selection, focus, or store-driven UI. |
|
|
77
|
+
| `/squad resume [id]` | No scheduler construction or task/squad transition. |
|
|
78
|
+
| `/squad msg ...` | No mailbox or task mutation. |
|
|
79
|
+
| `/squad widget`, `/squad panel`, `/squad clear` | No view activation or panel/widget bypass. |
|
|
80
|
+
| `/squad cancel`, `/squad cleanup [all]` | No stopping/deleting/mutating persisted squads. |
|
|
81
|
+
| `/squad agents`, `/squad defaults`, `/squad advisor` | No agent/settings editor mutation while the master switch is off. |
|
|
82
|
+
|
|
83
|
+
Ctrl+Q and all already-created panel callbacks obey the same gate: while disabled they may consume the input and show enable guidance, but may not resolve/focus a squad, construct/start a scheduler, send mail, pause/resume/cancel tasks, or mutate work. If an overlay is open during disable, close it when supported; regardless, its callbacks must fail closed.
|
|
84
|
+
|
|
85
|
+
## 5. `/squad disable` transition
|
|
86
|
+
|
|
87
|
+
From enabled state, ordering is mandatory:
|
|
88
|
+
|
|
89
|
+
1. Load current settings, set `enabled: false`, and atomically persist them. If this fails, abort without claiming or entering disabled mode.
|
|
90
|
+
2. Set the in-memory master state to false immediately after persistence, so concurrent public paths fail closed during shutdown.
|
|
91
|
+
3. Stop every registered scheduler. Existing `Scheduler.stop()` ordering remains authoritative: stop monitor/reconcile, durably change each `in_progress` task to `suspended`, then kill all child processes. Await every stop/kill before reporting success.
|
|
92
|
+
4. Clear the scheduler registry and active focus. Do not reconstruct an absent scheduler merely to disable it.
|
|
93
|
+
5. Force `widgetState.enabled = false`, clear its squad target/status, request a render clear (or dispose the widget), and prevent late scheduler events from showing it again.
|
|
94
|
+
6. Leave all pending/failed review evidence, review history, mailbox data, sessions, cancellation state, file-spec evidence, and suspended-stall attention intact.
|
|
95
|
+
7. Notify truthfully that pi-squad is disabled and `/squad enable` is required.
|
|
96
|
+
|
|
97
|
+
The operation is idempotent. Repeating it while already disabled does not rewrite tasks, resume work, create schedulers, or duplicate children; it reports the already-disabled state. A task already suspended remains suspended.
|
|
98
|
+
|
|
99
|
+
## 6. `/squad enable` transition and no-auto-resume rule
|
|
100
|
+
|
|
101
|
+
Ordering is symmetric at the persistence boundary:
|
|
102
|
+
|
|
103
|
+
1. Load current settings, set `enabled: true`, and atomically persist them. On failure, remain disabled.
|
|
104
|
+
2. Set the in-memory master state to true.
|
|
105
|
+
3. Restore widget *availability* (`widgetState.enabled = true` and install/request its controls when UI exists), but keep focus cleared unless the user explicitly selects a squad. An empty widget is valid.
|
|
106
|
+
4. Do not construct/start a scheduler, audit work, recover mail, reopen a task, resume a child session, select a squad, clear an attention record, or change any squad/task status.
|
|
107
|
+
5. Notify: pi-squad is enabled; no suspended work was resumed; `/squad select` and an explicit `/squad resume <id>` or exact `squad_modify resume_task` may be required.
|
|
108
|
+
|
|
109
|
+
Repeated enable is idempotent: it converges persisted/in-memory enabled state and widget availability without spawning, selecting, resuming, or duplicating anything.
|
|
110
|
+
|
|
111
|
+
**Explicit consent is required after disable.** Work suspended by disable remains suspended across `/squad enable` and across disabled restarts. Selection is view-only. Only a later explicit resume operation may revive it, using the existing exact-target, task-session, mailbox, descendant-invalidation, and failed-review-history contracts.
|
|
112
|
+
|
|
113
|
+
## 7. Authoritative safety reminders while disabled
|
|
114
|
+
|
|
115
|
+
Master disable suppresses ordinary squad hints/status prompts and operational event notifications, but it must never hide or weaken these persisted safety obligations:
|
|
116
|
+
|
|
117
|
+
1. Every project squad in `review` keeps its complete mandatory orchestrator review gate injected by `before_agent_start`, across focus changes and while disabled. Candidate work remains untrusted and cannot become accepted. Because `squad_review` is fail-closed while disabled, the injected guidance must state that `/squad enable` is required before recording the independent review.
|
|
118
|
+
2. Every active persisted `suspendedStallAttention` keeps its complete exact squad ID, suspended IDs, blocked IDs, and “No task was resumed automatically” reminder injected project-wide while disabled. Guidance must state: enable first, then explicitly resume only intended exact tasks.
|
|
119
|
+
|
|
120
|
+
These are read-only, durable prompt injections, not permission to reconstruct a scheduler, acknowledge an outbox record, auto-resume work, or call a blocked tool. Review evidence and suspended-attention data remain authoritative and untruncated.
|
|
121
|
+
|
|
122
|
+
## 8. Widget and event behavior
|
|
123
|
+
|
|
124
|
+
The master switch outranks the separate session-local `/squad widget` preference.
|
|
125
|
+
|
|
126
|
+
- Disabled means no widget body and no squad status-line content, even if a prior widget toggle was on or a late scheduler event arrives.
|
|
127
|
+
- Disable clears focus and hides immediately after children are quiesced; enable restores the widget mechanism but does not infer a target.
|
|
128
|
+
- Ordinary `before_agent_start` squad hints/active status are absent while disabled; only section 7 safety text remains.
|
|
129
|
+
- Late scheduler callbacks after disable must not emit ordinary review/failure/reply/escalation UI or re-enable/focus the widget. Their durable state must still obey existing cancellation, suspension, and review guards.
|
|
130
|
+
|
|
131
|
+
## 9. Required regression matrix
|
|
132
|
+
|
|
133
|
+
Implementation tests must prove persisted state as well as returned text:
|
|
134
|
+
|
|
135
|
+
1. Missing/legacy settings default enabled; `false` and `true` survive extension/session restart; other settings survive both writes.
|
|
136
|
+
2. Disabled startup creates zero schedulers/children and performs zero orphan, attestation, mailbox, focus, widget, or task/squad mutation.
|
|
137
|
+
3. Each of the five registered public tools fails before lookup/reconstruction/mutation and includes `/squad enable` guidance.
|
|
138
|
+
4. Every slash branch/fallback listed in section 4, default `/squad`, direct-ID activation, Ctrl+Q, and panel actions have no disabled bypass.
|
|
139
|
+
5. Disable persists before stop, suspends live tasks durably, kills all children, clears scheduler/focus, and hides widget; a persistence failure leaves enabled runtime unchanged.
|
|
140
|
+
6. Enable restores widget availability and clear guidance but does not select, reconstruct, recover, or resume work.
|
|
141
|
+
7. Repeated enable/disable causes no duplicate scheduler/child, task rewrite, or auto-resume.
|
|
142
|
+
8. Disabled restart and later enable leave suspended tasks suspended; only explicit exact resume changes them.
|
|
143
|
+
9. Pending/failed review and delivered/pending suspended-attention reminders remain complete and injected while disabled, with enable-first guidance; no review/attention record is mutated.
|
|
144
|
+
10. Existing cancellation, mandatory review, file-spec/attestation, mailbox/session, no-truncation, and suspended-stall suites remain green.
|
|
145
|
+
|
|
146
|
+
## 10. Registration cross-check
|
|
147
|
+
|
|
148
|
+
`src/index.ts` currently exposes exactly five main-session tools (`squad`, `squad_status`, `squad_review`, `squad_message`, `squad_modify`) and one `/squad` command whose branches are `list`, `all`, `select`, `resume`, `widget`, `panel`, `msg`, `cancel`, `clear`, `cleanup`, `enable`, `disable`, `defaults`, `advisor`, `agents`, plus default selection and direct-ID fallback. Sections 3 and 4 cover every registration and branch. The session-start Ctrl+Q panel path and panel callbacks are covered separately because they are public execution surfaces even though they are not slash commands.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { Scheduler, formatSuspendedStallAttention, type SchedulerEvent, type Sch
|
|
|
20
20
|
import { runPlanner } from "./planner.js";
|
|
21
21
|
import { validatePlan, PLAN_STRUCTURE_RULES } from "./plan-rules.js";
|
|
22
22
|
import { ADVISOR_SYSTEM_PROMPT, buildAdvisorConsultText, type AdvisorConsultInput } from "./advisor.js";
|
|
23
|
-
import { completeSimple, type Message, type TextContent } from "@earendil-works/pi-ai";
|
|
23
|
+
import { completeSimple, type Message, type TextContent } from "@earendil-works/pi-ai/compat";
|
|
24
24
|
import { SquadPanel, type SquadPanelResult } from "./panel/squad-panel.js";
|
|
25
25
|
import { setupSquadWidget, type SquadWidgetControls, type SquadWidgetState } from "./panel/squad-widget.js";
|
|
26
26
|
import * as store from "./store.js";
|
|
@@ -41,14 +41,21 @@ interface InlineSquadStart {
|
|
|
41
41
|
config?: { maxConcurrency?: number; autoUnblock?: boolean; maxRetries?: number };
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/** Master switch —
|
|
44
|
+
/** Master switch — loaded from ~/.pi/squad/settings.json before lifecycle work. */
|
|
45
45
|
let squadEnabled = true;
|
|
46
|
+
const DISABLED_GUIDANCE = "pi-squad is disabled. Run /squad enable, then retry this operation; no squad work was changed.";
|
|
47
|
+
const disabledToolResult = () => ({
|
|
48
|
+
content: [{ type: "text" as const, text: DISABLED_GUIDANCE }],
|
|
49
|
+
details: undefined,
|
|
50
|
+
});
|
|
46
51
|
/** Registry of all running schedulers — supports multiple concurrent squads */
|
|
47
52
|
const schedulers = new Map<string, Scheduler>();
|
|
48
53
|
/** The currently viewed/focused squad (for widget, panel, status) */
|
|
49
54
|
let activeSquadId: string | null = null;
|
|
50
55
|
/** Whether an overlay panel is currently open (prevents double-open) */
|
|
51
56
|
let overlayOpen = false;
|
|
57
|
+
/** Close an already-open overlay when the master switch is disabled. */
|
|
58
|
+
let closeOverlay: (() => void) | null = null;
|
|
52
59
|
/** Stored ExtensionContext for widget updates from background scheduler events */
|
|
53
60
|
let uiCtx: import("@earendil-works/pi-coding-agent").ExtensionContext | null = null;
|
|
54
61
|
/** Component-based widget state + controls */
|
|
@@ -59,7 +66,7 @@ let widgetControls: SquadWidgetControls | null = null;
|
|
|
59
66
|
function focusSquad(squadId: string | null): void {
|
|
60
67
|
activeSquadId = squadId;
|
|
61
68
|
widgetState.squadId = squadId;
|
|
62
|
-
if (squadId) widgetState.enabled = true;
|
|
69
|
+
if (squadId && squadEnabled) widgetState.enabled = true;
|
|
63
70
|
widgetControls?.refreshNow();
|
|
64
71
|
}
|
|
65
72
|
|
|
@@ -255,6 +262,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
255
262
|
// File-spec children load only the non-recursive reader and fail-closed guard.
|
|
256
263
|
if (process.env.PI_SQUAD_CHILD === "1") { registerChildSpecReader(pi); return; }
|
|
257
264
|
|
|
265
|
+
// Load the global master switch before any session lifecycle work can run.
|
|
266
|
+
squadEnabled = store.loadSquadSettings().enabled;
|
|
267
|
+
widgetState.enabled = squadEnabled;
|
|
268
|
+
if (!squadEnabled) focusSquad(null);
|
|
269
|
+
|
|
258
270
|
// Wire main-session thinking lookup (needs `pi`, guarded against stale API)
|
|
259
271
|
getMainSessionThinking = () => {
|
|
260
272
|
try {
|
|
@@ -290,6 +302,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
290
302
|
return true;
|
|
291
303
|
};
|
|
292
304
|
|
|
305
|
+
/** Register a dynamically gated Ctrl+Q path; disabled mode can only show guidance. */
|
|
306
|
+
const registerTerminalPanelToggle = (ctx: ExtensionContext): void => {
|
|
307
|
+
if (!ctx.hasUI) return;
|
|
308
|
+
ctx.ui.onTerminalInput((data) => {
|
|
309
|
+
if (data !== "\x11") return undefined;
|
|
310
|
+
if (!squadEnabled) {
|
|
311
|
+
ctx.ui.notify(DISABLED_GUIDANCE, "warning");
|
|
312
|
+
return { consume: true };
|
|
313
|
+
}
|
|
314
|
+
// If overlay is already open, let the panel's own handler deal with it.
|
|
315
|
+
if (overlayOpen) return undefined;
|
|
316
|
+
if (!activeSquadId) {
|
|
317
|
+
const latest = store.findLatestSquad(ctx.cwd)
|
|
318
|
+
|| store.listSquads().map((id) => store.loadSquad(id)).filter((s): s is Squad => s !== null).sort((a, b) => b.created.localeCompare(a.created))[0];
|
|
319
|
+
if (latest) activateSquadView(latest.id, ctx);
|
|
320
|
+
else {
|
|
321
|
+
ctx.ui.notify("No squads found. Use /squad or the squad tool.", "info");
|
|
322
|
+
return { consume: true };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (activeSquadId) {
|
|
326
|
+
openPanel(pi, ctx, schedulers.get(activeSquadId) || new Scheduler(activeSquadId, squadSkillPaths, schedulerSpawnContext), activeSquadId);
|
|
327
|
+
}
|
|
328
|
+
return { consume: true };
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
|
|
293
332
|
// =========================================================================
|
|
294
333
|
// Context Injection — give main agent awareness of squad state
|
|
295
334
|
// =========================================================================
|
|
@@ -306,7 +345,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
306
345
|
const durablePrompts = [...pendingReviewGates.map(({ gate }) => gate), ...suspendedAttention];
|
|
307
346
|
if (!squadEnabled) {
|
|
308
347
|
if (durablePrompts.length === 0) return;
|
|
309
|
-
|
|
348
|
+
const enableFirst = durablePrompts.map((prompt) =>
|
|
349
|
+
`${prompt}\npi-squad is disabled. Run /squad enable first; then perform only the explicit review or exact-task resume described above.`,
|
|
350
|
+
);
|
|
351
|
+
return { systemPrompt: event.systemPrompt + "\n\n" + enableFirst.join("\n\n") };
|
|
310
352
|
}
|
|
311
353
|
|
|
312
354
|
// When a squad is active, inject its status
|
|
@@ -451,7 +493,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
451
493
|
]),
|
|
452
494
|
|
|
453
495
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
454
|
-
if (!squadEnabled) return
|
|
496
|
+
if (!squadEnabled) return disabledToolResult();
|
|
455
497
|
if (!uiCtx) uiCtx = ctx;
|
|
456
498
|
|
|
457
499
|
// Check if the user cancelled before we start
|
|
@@ -490,6 +532,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
490
532
|
}),
|
|
491
533
|
|
|
492
534
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
535
|
+
if (!squadEnabled) return disabledToolResult();
|
|
493
536
|
let id = params.squadId || activeSquadId;
|
|
494
537
|
|
|
495
538
|
// If no active squad, find the most recent one for this project
|
|
@@ -570,6 +613,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
570
613
|
}),
|
|
571
614
|
|
|
572
615
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
616
|
+
if (!squadEnabled) return disabledToolResult();
|
|
573
617
|
let id = params.squadId;
|
|
574
618
|
if (!id && activeSquadId && store.loadSquad(activeSquadId)?.status === "review") {
|
|
575
619
|
id = activeSquadId;
|
|
@@ -590,6 +634,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
590
634
|
const attestationScheduler = schedulers.get(id) ?? new Scheduler(id, squadSkillPaths, schedulerSpawnContext);
|
|
591
635
|
if (!schedulers.has(id)) { schedulers.set(id, attestationScheduler); wireSchedulerEvents(pi, attestationScheduler, id); }
|
|
592
636
|
const invalidAttestations = await attestationScheduler.auditSpecAttestations();
|
|
637
|
+
if (!squadEnabled) return disabledToolResult();
|
|
593
638
|
if (invalidAttestations.length > 0) {
|
|
594
639
|
void attestationScheduler.start();
|
|
595
640
|
return { content: [{ type: "text" as const, text: `Review rejected: invalid canonical spec attestation for task(s): ${invalidAttestations.join(", ")}. Work was reopened.` }], details: undefined };
|
|
@@ -634,6 +679,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
634
679
|
}),
|
|
635
680
|
|
|
636
681
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
682
|
+
if (!squadEnabled) return disabledToolResult();
|
|
637
683
|
if (!activeSquadId) {
|
|
638
684
|
return { content: [{ type: "text" as const, text: "No active squad." }], details: undefined };
|
|
639
685
|
}
|
|
@@ -714,6 +760,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
714
760
|
}),
|
|
715
761
|
|
|
716
762
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
763
|
+
if (!squadEnabled) return disabledToolResult();
|
|
717
764
|
if (params.action === "cancel") {
|
|
718
765
|
if (!params.squadId?.trim()) {
|
|
719
766
|
return { content: [{ type: "text" as const, text: "cancel requires exact squadId; no squad was changed." }], details: undefined };
|
|
@@ -883,12 +930,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
883
930
|
// =========================================================================
|
|
884
931
|
|
|
885
932
|
pi.on("session_start", async (_event, ctx) => {
|
|
933
|
+
// Re-read before touching focus, widgets, schedulers, or persisted work.
|
|
934
|
+
squadEnabled = store.loadSquadSettings().enabled;
|
|
886
935
|
uiCtx = ctx;
|
|
887
936
|
|
|
888
937
|
// A session replacement may not deliver shutdown first. Never carry a
|
|
889
938
|
// focused squad across projects, and never seed a new widget from stale state.
|
|
890
939
|
widgetControls?.dispose();
|
|
891
940
|
widgetControls = null;
|
|
941
|
+
registerTerminalPanelToggle(ctx);
|
|
942
|
+
if (!squadEnabled) {
|
|
943
|
+
widgetState.enabled = false;
|
|
944
|
+
focusSquad(null);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
widgetState.enabled = true;
|
|
892
948
|
const focused = activeSquadId ? store.loadSquad(activeSquadId) : null;
|
|
893
949
|
focusSquad(focused?.cwd === ctx.cwd ? activeSquadId : null);
|
|
894
950
|
|
|
@@ -993,33 +1049,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
993
1049
|
});
|
|
994
1050
|
}
|
|
995
1051
|
|
|
996
|
-
// Register Ctrl+Q terminal input handler for panel toggle
|
|
997
|
-
if (ctx.hasUI) {
|
|
998
|
-
ctx.ui.onTerminalInput((data) => {
|
|
999
|
-
if (data === "\x11") {
|
|
1000
|
-
// If overlay is already open, let the panel's own handler deal with it
|
|
1001
|
-
if (overlayOpen) return undefined;
|
|
1002
|
-
|
|
1003
|
-
// Auto-pick a squad if none active
|
|
1004
|
-
if (!activeSquadId) {
|
|
1005
|
-
const latest = store.findLatestSquad(ctx.cwd)
|
|
1006
|
-
|| store.listSquads().map((id) => store.loadSquad(id)).filter((s): s is Squad => s !== null).sort((a, b) => b.created.localeCompare(a.created))[0];
|
|
1007
|
-
if (latest) {
|
|
1008
|
-
activateSquadView(latest.id, ctx);
|
|
1009
|
-
} else {
|
|
1010
|
-
ctx.ui.notify("No squads found. Use /squad or the squad tool.", "info");
|
|
1011
|
-
return { consume: true };
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
if (activeSquadId) {
|
|
1016
|
-
openPanel(pi, ctx, schedulers.get(activeSquadId) || new Scheduler(activeSquadId, squadSkillPaths, schedulerSpawnContext), activeSquadId);
|
|
1017
|
-
}
|
|
1018
|
-
return { consume: true };
|
|
1019
|
-
}
|
|
1020
|
-
return undefined;
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
1052
|
});
|
|
1024
1053
|
|
|
1025
1054
|
pi.on("session_shutdown", async () => {
|
|
@@ -1060,7 +1089,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1060
1089
|
return subs.filter((s) => s.value.startsWith(prefix));
|
|
1061
1090
|
},
|
|
1062
1091
|
handler: async (args, ctx) => {
|
|
1063
|
-
const
|
|
1092
|
+
const requested = args.trim();
|
|
1093
|
+
if (!squadEnabled && requested !== "enable" && requested !== "disable") {
|
|
1094
|
+
ctx.ui.notify(DISABLED_GUIDANCE, "warning");
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
const parts = requested.split(/\s+/);
|
|
1064
1098
|
const sub = parts[0] || "select";
|
|
1065
1099
|
|
|
1066
1100
|
switch (sub) {
|
|
@@ -1338,22 +1372,48 @@ export default function (pi: ExtensionAPI) {
|
|
|
1338
1372
|
}
|
|
1339
1373
|
|
|
1340
1374
|
case "enable": {
|
|
1375
|
+
const wasEnabled = squadEnabled;
|
|
1376
|
+
try {
|
|
1377
|
+
const settings = store.loadSquadSettings();
|
|
1378
|
+
settings.enabled = true;
|
|
1379
|
+
store.saveSquadSettings(settings);
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
ctx.ui.notify(`Could not enable pi-squad: ${(error as Error).message}`, "error");
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1341
1384
|
squadEnabled = true;
|
|
1385
|
+
widgetState.enabled = true;
|
|
1386
|
+
if (!wasEnabled) widgetState.squadId = null;
|
|
1387
|
+
if (!widgetControls && uiCtx?.hasUI) widgetControls = setupSquadWidget(uiCtx, widgetState);
|
|
1342
1388
|
widgetControls?.requestUpdate();
|
|
1343
|
-
ctx.ui.notify("pi-squad enabled
|
|
1389
|
+
ctx.ui.notify("pi-squad enabled. No suspended work was resumed; use /squad select and an explicit /squad resume <id> or exact resume_task if needed.", "info");
|
|
1344
1390
|
return;
|
|
1345
1391
|
}
|
|
1346
1392
|
|
|
1347
1393
|
case "disable": {
|
|
1348
|
-
squadEnabled
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
await sched.stop();
|
|
1394
|
+
if (!squadEnabled) {
|
|
1395
|
+
ctx.ui.notify("pi-squad is already disabled. Run /squad enable to use squad operations.", "info");
|
|
1396
|
+
return;
|
|
1352
1397
|
}
|
|
1398
|
+
try {
|
|
1399
|
+
const settings = store.loadSquadSettings();
|
|
1400
|
+
settings.enabled = false;
|
|
1401
|
+
store.saveSquadSettings(settings);
|
|
1402
|
+
} catch (error) {
|
|
1403
|
+
ctx.ui.notify(`Could not disable pi-squad: ${(error as Error).message}`, "error");
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
// The persisted state is authoritative before shutdown begins.
|
|
1407
|
+
squadEnabled = false;
|
|
1408
|
+
closeOverlay?.();
|
|
1409
|
+
for (const sched of schedulers.values()) await sched.stop();
|
|
1353
1410
|
schedulers.clear();
|
|
1354
1411
|
widgetState.enabled = false;
|
|
1412
|
+
widgetState.squadId = null;
|
|
1355
1413
|
focusSquad(null);
|
|
1356
|
-
|
|
1414
|
+
widgetControls?.dispose();
|
|
1415
|
+
widgetControls = null;
|
|
1416
|
+
ctx.ui.notify("pi-squad disabled. Running work was durably suspended; run /squad enable before any squad operation.", "info");
|
|
1357
1417
|
return;
|
|
1358
1418
|
}
|
|
1359
1419
|
|
|
@@ -1618,6 +1678,10 @@ async function pickSquad(
|
|
|
1618
1678
|
* Does NOT start a scheduler (view-only unless squad needs resuming).
|
|
1619
1679
|
*/
|
|
1620
1680
|
function activateSquadView(squadId: string, ctx: import("@earendil-works/pi-coding-agent").ExtensionContext | import("@earendil-works/pi-coding-agent").ExtensionCommandContext): void {
|
|
1681
|
+
if (!squadEnabled) {
|
|
1682
|
+
ctx.ui.notify(DISABLED_GUIDANCE, "warning");
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1621
1685
|
const squad = store.loadSquad(squadId);
|
|
1622
1686
|
if (!squad) {
|
|
1623
1687
|
ctx.ui.notify(`Squad '${squadId}' not found`, "error");
|
|
@@ -1647,6 +1711,9 @@ function forceWidgetUpdate(): void {
|
|
|
1647
1711
|
/** Wire one scheduler to durable main-session notifications. */
|
|
1648
1712
|
function wireSchedulerEvents(pi: ExtensionAPI, scheduler: Scheduler, squadId: string): void {
|
|
1649
1713
|
scheduler.onEvent((event: SchedulerEvent) => {
|
|
1714
|
+
// Durable scheduler state remains authoritative, but disabled mode suppresses
|
|
1715
|
+
// late operational UI, acknowledgements, focus, and ordinary notifications.
|
|
1716
|
+
if (!squadEnabled) return;
|
|
1650
1717
|
forceWidgetUpdate();
|
|
1651
1718
|
switch (event.type) {
|
|
1652
1719
|
case "squad_review_required": {
|
|
@@ -1729,6 +1796,10 @@ function openPanel(
|
|
|
1729
1796
|
scheduler: Scheduler,
|
|
1730
1797
|
squadId: string,
|
|
1731
1798
|
): void {
|
|
1799
|
+
if (!squadEnabled) {
|
|
1800
|
+
ctx.ui.notify(DISABLED_GUIDANCE, "warning");
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1732
1803
|
if (overlayOpen) return;
|
|
1733
1804
|
// Opening details also establishes authoritative focus. This covers callers
|
|
1734
1805
|
// that reconstructed or targeted a scheduler without going through selector UI.
|
|
@@ -1738,13 +1809,30 @@ function openPanel(
|
|
|
1738
1809
|
// The promise resolves when the panel calls done()
|
|
1739
1810
|
const panelPromise = ctx.ui.custom<SquadPanelResult>(
|
|
1740
1811
|
(tui, theme, _kb, done) => {
|
|
1741
|
-
const panel = new SquadPanel(
|
|
1812
|
+
const panel = new SquadPanel(
|
|
1813
|
+
tui,
|
|
1814
|
+
theme,
|
|
1815
|
+
scheduler,
|
|
1816
|
+
squadId,
|
|
1817
|
+
done,
|
|
1818
|
+
() => squadEnabled,
|
|
1819
|
+
() => ctx.ui.notify(DISABLED_GUIDANCE, "warning"),
|
|
1820
|
+
);
|
|
1821
|
+
closeOverlay = () => panel.close();
|
|
1742
1822
|
|
|
1743
1823
|
// Wire up message sending from panel
|
|
1744
1824
|
panel.onSendMessage = async (taskId: string, _prefill: string) => {
|
|
1825
|
+
if (!squadEnabled) {
|
|
1826
|
+
ctx.ui.notify(DISABLED_GUIDANCE, "warning");
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1745
1829
|
const task = store.loadTask(squadId, taskId);
|
|
1746
1830
|
const agentName = task?.agent || taskId;
|
|
1747
1831
|
const input = await ctx.ui.input(`Message to ${agentName}`, "Type your message...");
|
|
1832
|
+
if (!squadEnabled) {
|
|
1833
|
+
ctx.ui.notify(DISABLED_GUIDANCE, "warning");
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1748
1836
|
if (input) {
|
|
1749
1837
|
let panelSched = schedulers.get(squadId);
|
|
1750
1838
|
if (!panelSched) {
|
|
@@ -1778,9 +1866,11 @@ function openPanel(
|
|
|
1778
1866
|
// When panel closes (done() called), clean up
|
|
1779
1867
|
panelPromise.then(() => {
|
|
1780
1868
|
overlayOpen = false;
|
|
1869
|
+
closeOverlay = null;
|
|
1781
1870
|
forceWidgetUpdate();
|
|
1782
1871
|
}).catch(() => {
|
|
1783
1872
|
overlayOpen = false;
|
|
1873
|
+
closeOverlay = null;
|
|
1784
1874
|
});
|
|
1785
1875
|
}
|
|
1786
1876
|
|
|
@@ -1831,6 +1921,10 @@ async function startSquad(
|
|
|
1831
1921
|
}
|
|
1832
1922
|
}
|
|
1833
1923
|
|
|
1924
|
+
// A planner may take long enough for /squad disable to run concurrently.
|
|
1925
|
+
// Re-check before publishing any squad or constructing a scheduler.
|
|
1926
|
+
if (!squadEnabled) return disabledToolResult();
|
|
1927
|
+
|
|
1834
1928
|
// Merge agent roster
|
|
1835
1929
|
const agents: Record<string, SquadAgentEntry> = { ...plan.agents };
|
|
1836
1930
|
if (params.agents) {
|
package/src/panel/squad-panel.ts
CHANGED
|
@@ -41,6 +41,8 @@ export class SquadPanel implements Component, Focusable {
|
|
|
41
41
|
private done: (result: SquadPanelResult) => void;
|
|
42
42
|
private scheduler: Scheduler;
|
|
43
43
|
private squadId: string;
|
|
44
|
+
private isEnabled: () => boolean;
|
|
45
|
+
private onDisabled: () => void;
|
|
44
46
|
|
|
45
47
|
/** Callback for sending messages — set by the extension */
|
|
46
48
|
onSendMessage?: (taskId: string, message: string) => Promise<void>;
|
|
@@ -69,12 +71,16 @@ export class SquadPanel implements Component, Focusable {
|
|
|
69
71
|
scheduler: Scheduler,
|
|
70
72
|
squadId: string,
|
|
71
73
|
done: (result: SquadPanelResult) => void,
|
|
74
|
+
isEnabled: () => boolean = () => true,
|
|
75
|
+
onDisabled: () => void = () => {},
|
|
72
76
|
) {
|
|
73
77
|
this.tui = tui;
|
|
74
78
|
this.theme = theme;
|
|
75
79
|
this.scheduler = scheduler;
|
|
76
80
|
this.squadId = squadId;
|
|
77
81
|
this.done = done;
|
|
82
|
+
this.isEnabled = isEnabled;
|
|
83
|
+
this.onDisabled = onDisabled;
|
|
78
84
|
|
|
79
85
|
this.taskListView = new TaskListView(theme, squadId);
|
|
80
86
|
this.messageView = new MessageView(theme, squadId);
|
|
@@ -112,6 +118,12 @@ export class SquadPanel implements Component, Focusable {
|
|
|
112
118
|
// =========================================================================
|
|
113
119
|
|
|
114
120
|
handleInput(data: string): void {
|
|
121
|
+
if (!this.isEnabled()) {
|
|
122
|
+
this.onDisabled();
|
|
123
|
+
this.finish({ action: "close" });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
115
127
|
// Ctrl+Q or q: close panel
|
|
116
128
|
if (data === "\x11") {
|
|
117
129
|
this.finish({ action: "close" });
|
|
@@ -316,6 +328,10 @@ export class SquadPanel implements Component, Focusable {
|
|
|
316
328
|
// Lifecycle
|
|
317
329
|
// =========================================================================
|
|
318
330
|
|
|
331
|
+
close(): void {
|
|
332
|
+
this.finish({ action: "close" });
|
|
333
|
+
}
|
|
334
|
+
|
|
319
335
|
dispose(): void {
|
|
320
336
|
if (this.renderTimeout) {
|
|
321
337
|
clearTimeout(this.renderTimeout);
|
package/src/store.ts
CHANGED
|
@@ -62,12 +62,14 @@ export function getSquadSettingsPath(): string {
|
|
|
62
62
|
return path.join(SQUAD_HOME, "settings.json");
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/** Load global squad settings, merged over defaults (advisor merged deep) */
|
|
65
|
+
/** Load global squad settings, merged over defaults (advisor merged deep). */
|
|
66
66
|
export function loadSquadSettings(): SquadSettings {
|
|
67
67
|
const loaded = readJson<Partial<SquadSettings>>(getSquadSettingsPath());
|
|
68
68
|
return {
|
|
69
69
|
...DEFAULT_SQUAD_SETTINGS,
|
|
70
70
|
...(loaded || {}),
|
|
71
|
+
// Legacy, missing, and malformed values remain backward-compatible.
|
|
72
|
+
enabled: typeof loaded?.enabled === "boolean" ? loaded.enabled : DEFAULT_SQUAD_SETTINGS.enabled,
|
|
71
73
|
advisor: { ...DEFAULT_SQUAD_SETTINGS.advisor, ...(loaded?.advisor || {}) },
|
|
72
74
|
};
|
|
73
75
|
}
|
package/src/types.ts
CHANGED
|
@@ -44,6 +44,7 @@ export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
|
44
44
|
* - any other string: an explicit model id (defaultModel) or thinking level (defaultThinking)
|
|
45
45
|
*/
|
|
46
46
|
export interface SquadSettings {
|
|
47
|
+
enabled: boolean;
|
|
47
48
|
defaultModel: string;
|
|
48
49
|
defaultThinking: string;
|
|
49
50
|
advisor: {
|
|
@@ -56,6 +57,7 @@ export interface SquadSettings {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export const DEFAULT_SQUAD_SETTINGS: SquadSettings = {
|
|
60
|
+
enabled: true,
|
|
59
61
|
defaultModel: "main",
|
|
60
62
|
defaultThinking: "main",
|
|
61
63
|
advisor: {
|