pi-squad 0.17.0 → 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 +173 -93
- package/src/panel/squad-panel.ts +16 -0
- package/src/panel/squad-widget.ts +20 -9
- 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,9 +20,9 @@ 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
|
-
import { setupSquadWidget, type SquadWidgetState } from "./panel/squad-widget.js";
|
|
25
|
+
import { setupSquadWidget, type SquadWidgetControls, type SquadWidgetState } from "./panel/squad-widget.js";
|
|
26
26
|
import * as store from "./store.js";
|
|
27
27
|
import { debug, logError } from "./logger.js";
|
|
28
28
|
import { buildCompletionSummary, buildFailureSummary } from "./report.js";
|
|
@@ -41,19 +41,34 @@ 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 */
|
|
55
62
|
const widgetState: SquadWidgetState = { squadId: null, enabled: true };
|
|
56
|
-
let widgetControls:
|
|
63
|
+
let widgetControls: SquadWidgetControls | null = null;
|
|
64
|
+
|
|
65
|
+
/** Keep main-session focus, compact widget, and detail panel targeting identical. */
|
|
66
|
+
function focusSquad(squadId: string | null): void {
|
|
67
|
+
activeSquadId = squadId;
|
|
68
|
+
widgetState.squadId = squadId;
|
|
69
|
+
if (squadId && squadEnabled) widgetState.enabled = true;
|
|
70
|
+
widgetControls?.refreshNow();
|
|
71
|
+
}
|
|
57
72
|
|
|
58
73
|
/** Format completion against active work while retaining cancelled history. */
|
|
59
74
|
function formatTaskProgress(tasks: Task[]): string {
|
|
@@ -197,11 +212,10 @@ function getActiveScheduler(): Scheduler | null {
|
|
|
197
212
|
|
|
198
213
|
/** Restore the single focus/widget invariant after destructive exact cancellation. */
|
|
199
214
|
function repairFocusAfterCancellation(cancelledId: string): void {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
widgetControls?.requestUpdate();
|
|
215
|
+
const nextFocus = activeSquadId === cancelledId || (activeSquadId !== null && !store.loadSquad(activeSquadId))
|
|
216
|
+
? null
|
|
217
|
+
: activeSquadId;
|
|
218
|
+
focusSquad(nextFocus);
|
|
205
219
|
}
|
|
206
220
|
|
|
207
221
|
function activeSuspendedAttentionForProject(cwd: string): Array<{ squadId: string; attention: SuspendedStallAttention }> {
|
|
@@ -218,10 +232,7 @@ function ensureScheduler(pi: ExtensionAPI, squadId: string, skillPaths: string[]
|
|
|
218
232
|
schedulers.set(squadId, scheduler);
|
|
219
233
|
wireSchedulerEvents(pi, scheduler, squadId);
|
|
220
234
|
}
|
|
221
|
-
|
|
222
|
-
widgetState.squadId = squadId;
|
|
223
|
-
widgetState.enabled = true;
|
|
224
|
-
widgetControls?.requestUpdate();
|
|
235
|
+
focusSquad(squadId);
|
|
225
236
|
return scheduler;
|
|
226
237
|
}
|
|
227
238
|
|
|
@@ -251,6 +262,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
251
262
|
// File-spec children load only the non-recursive reader and fail-closed guard.
|
|
252
263
|
if (process.env.PI_SQUAD_CHILD === "1") { registerChildSpecReader(pi); return; }
|
|
253
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
|
+
|
|
254
270
|
// Wire main-session thinking lookup (needs `pi`, guarded against stale API)
|
|
255
271
|
getMainSessionThinking = () => {
|
|
256
272
|
try {
|
|
@@ -286,6 +302,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
286
302
|
return true;
|
|
287
303
|
};
|
|
288
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
|
+
|
|
289
332
|
// =========================================================================
|
|
290
333
|
// Context Injection — give main agent awareness of squad state
|
|
291
334
|
// =========================================================================
|
|
@@ -302,15 +345,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
302
345
|
const durablePrompts = [...pendingReviewGates.map(({ gate }) => gate), ...suspendedAttention];
|
|
303
346
|
if (!squadEnabled) {
|
|
304
347
|
if (durablePrompts.length === 0) return;
|
|
305
|
-
|
|
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") };
|
|
306
352
|
}
|
|
307
353
|
|
|
308
354
|
// When a squad is active, inject its status
|
|
309
355
|
if (activeSquadId) {
|
|
310
356
|
const squad = store.loadSquad(activeSquadId);
|
|
311
357
|
if (!squad) {
|
|
312
|
-
|
|
313
|
-
widgetState.squadId = null;
|
|
358
|
+
focusSquad(null);
|
|
314
359
|
if (durablePrompts.length > 0) {
|
|
315
360
|
return { systemPrompt: event.systemPrompt + "\n\n" + durablePrompts.join("\n\n") };
|
|
316
361
|
}
|
|
@@ -448,7 +493,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
448
493
|
]),
|
|
449
494
|
|
|
450
495
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
451
|
-
if (!squadEnabled) return
|
|
496
|
+
if (!squadEnabled) return disabledToolResult();
|
|
452
497
|
if (!uiCtx) uiCtx = ctx;
|
|
453
498
|
|
|
454
499
|
// Check if the user cancelled before we start
|
|
@@ -487,6 +532,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
487
532
|
}),
|
|
488
533
|
|
|
489
534
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
535
|
+
if (!squadEnabled) return disabledToolResult();
|
|
490
536
|
let id = params.squadId || activeSquadId;
|
|
491
537
|
|
|
492
538
|
// If no active squad, find the most recent one for this project
|
|
@@ -567,6 +613,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
567
613
|
}),
|
|
568
614
|
|
|
569
615
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
616
|
+
if (!squadEnabled) return disabledToolResult();
|
|
570
617
|
let id = params.squadId;
|
|
571
618
|
if (!id && activeSquadId && store.loadSquad(activeSquadId)?.status === "review") {
|
|
572
619
|
id = activeSquadId;
|
|
@@ -587,6 +634,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
587
634
|
const attestationScheduler = schedulers.get(id) ?? new Scheduler(id, squadSkillPaths, schedulerSpawnContext);
|
|
588
635
|
if (!schedulers.has(id)) { schedulers.set(id, attestationScheduler); wireSchedulerEvents(pi, attestationScheduler, id); }
|
|
589
636
|
const invalidAttestations = await attestationScheduler.auditSpecAttestations();
|
|
637
|
+
if (!squadEnabled) return disabledToolResult();
|
|
590
638
|
if (invalidAttestations.length > 0) {
|
|
591
639
|
void attestationScheduler.start();
|
|
592
640
|
return { content: [{ type: "text" as const, text: `Review rejected: invalid canonical spec attestation for task(s): ${invalidAttestations.join(", ")}. Work was reopened.` }], details: undefined };
|
|
@@ -631,6 +679,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
631
679
|
}),
|
|
632
680
|
|
|
633
681
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
682
|
+
if (!squadEnabled) return disabledToolResult();
|
|
634
683
|
if (!activeSquadId) {
|
|
635
684
|
return { content: [{ type: "text" as const, text: "No active squad." }], details: undefined };
|
|
636
685
|
}
|
|
@@ -711,6 +760,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
711
760
|
}),
|
|
712
761
|
|
|
713
762
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
763
|
+
if (!squadEnabled) return disabledToolResult();
|
|
714
764
|
if (params.action === "cancel") {
|
|
715
765
|
if (!params.squadId?.trim()) {
|
|
716
766
|
return { content: [{ type: "text" as const, text: "cancel requires exact squadId; no squad was changed." }], details: undefined };
|
|
@@ -756,7 +806,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
756
806
|
if (!activeScheduler) {
|
|
757
807
|
return { content: [{ type: "text" as const, text: `Squad '${squadId}' has no active scheduler. Use resume, add_task, or resume_task to reconstruct it.` }], details: undefined };
|
|
758
808
|
}
|
|
759
|
-
|
|
809
|
+
focusSquad(squadId);
|
|
760
810
|
|
|
761
811
|
switch (params.action) {
|
|
762
812
|
case "add_task": {
|
|
@@ -880,8 +930,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
880
930
|
// =========================================================================
|
|
881
931
|
|
|
882
932
|
pi.on("session_start", async (_event, ctx) => {
|
|
933
|
+
// Re-read before touching focus, widgets, schedulers, or persisted work.
|
|
934
|
+
squadEnabled = store.loadSquadSettings().enabled;
|
|
883
935
|
uiCtx = ctx;
|
|
884
936
|
|
|
937
|
+
// A session replacement may not deliver shutdown first. Never carry a
|
|
938
|
+
// focused squad across projects, and never seed a new widget from stale state.
|
|
939
|
+
widgetControls?.dispose();
|
|
940
|
+
widgetControls = null;
|
|
941
|
+
registerTerminalPanelToggle(ctx);
|
|
942
|
+
if (!squadEnabled) {
|
|
943
|
+
widgetState.enabled = false;
|
|
944
|
+
focusSquad(null);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
widgetState.enabled = true;
|
|
948
|
+
const focused = activeSquadId ? store.loadSquad(activeSquadId) : null;
|
|
949
|
+
focusSquad(focused?.cwd === ctx.cwd ? activeSquadId : null);
|
|
950
|
+
|
|
885
951
|
// Install component-based widget
|
|
886
952
|
if (ctx.hasUI) {
|
|
887
953
|
widgetControls = setupSquadWidget(ctx, widgetState);
|
|
@@ -948,12 +1014,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
948
1014
|
}
|
|
949
1015
|
await scheduler.start();
|
|
950
1016
|
}
|
|
951
|
-
if (pendingMailSquads.length > 0)
|
|
952
|
-
activeSquadId = pendingMailSquads[0].id;
|
|
953
|
-
widgetState.squadId = activeSquadId;
|
|
954
|
-
widgetState.enabled = true;
|
|
955
|
-
widgetControls?.requestUpdate();
|
|
956
|
-
}
|
|
1017
|
+
if (pendingMailSquads.length > 0) focusSquad(pendingMailSquads[0].id);
|
|
957
1018
|
|
|
958
1019
|
// Notify about paused squads only if they have real completed work
|
|
959
1020
|
const paused = store.findActiveSquads()
|
|
@@ -979,10 +1040,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
979
1040
|
.filter((s) => s.cwd === ctx.cwd && s.status === "review");
|
|
980
1041
|
if (pendingReviews.length > 0) {
|
|
981
1042
|
const squad = pendingReviews.sort((a, b) => b.created.localeCompare(a.created))[0];
|
|
982
|
-
|
|
983
|
-
widgetState.squadId = squad.id;
|
|
984
|
-
widgetState.enabled = true;
|
|
985
|
-
widgetControls?.requestUpdate();
|
|
1043
|
+
focusSquad(squad.id);
|
|
986
1044
|
const tasks = store.loadAllTasks(squad.id);
|
|
987
1045
|
pi.sendMessage({
|
|
988
1046
|
customType: "squad-review-required",
|
|
@@ -991,43 +1049,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
991
1049
|
});
|
|
992
1050
|
}
|
|
993
1051
|
|
|
994
|
-
// Register Ctrl+Q terminal input handler for panel toggle
|
|
995
|
-
if (ctx.hasUI) {
|
|
996
|
-
ctx.ui.onTerminalInput((data) => {
|
|
997
|
-
if (data === "\x11") {
|
|
998
|
-
// If overlay is already open, let the panel's own handler deal with it
|
|
999
|
-
if (overlayOpen) return undefined;
|
|
1000
|
-
|
|
1001
|
-
// Auto-pick a squad if none active
|
|
1002
|
-
if (!activeSquadId) {
|
|
1003
|
-
const latest = store.findLatestSquad(ctx.cwd)
|
|
1004
|
-
|| store.listSquads().map((id) => store.loadSquad(id)).filter((s): s is Squad => s !== null).sort((a, b) => b.created.localeCompare(a.created))[0];
|
|
1005
|
-
if (latest) {
|
|
1006
|
-
activateSquadView(latest.id, ctx);
|
|
1007
|
-
} else {
|
|
1008
|
-
ctx.ui.notify("No squads found. Use /squad or the squad tool.", "info");
|
|
1009
|
-
return { consume: true };
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
if (activeSquadId) {
|
|
1014
|
-
openPanel(pi, ctx, schedulers.get(activeSquadId) || new Scheduler(activeSquadId, squadSkillPaths, schedulerSpawnContext), activeSquadId);
|
|
1015
|
-
}
|
|
1016
|
-
return { consume: true };
|
|
1017
|
-
}
|
|
1018
|
-
return undefined;
|
|
1019
|
-
});
|
|
1020
|
-
}
|
|
1021
1052
|
});
|
|
1022
1053
|
|
|
1023
1054
|
pi.on("session_shutdown", async () => {
|
|
1055
|
+
focusSquad(null);
|
|
1024
1056
|
widgetControls?.dispose();
|
|
1025
1057
|
widgetControls = null;
|
|
1026
1058
|
for (const [id, sched] of schedulers) {
|
|
1027
1059
|
await sched.stop();
|
|
1028
1060
|
}
|
|
1029
1061
|
schedulers.clear();
|
|
1030
|
-
activeSquadId = null;
|
|
1031
1062
|
uiCtx = null;
|
|
1032
1063
|
});
|
|
1033
1064
|
|
|
@@ -1058,7 +1089,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1058
1089
|
return subs.filter((s) => s.value.startsWith(prefix));
|
|
1059
1090
|
},
|
|
1060
1091
|
handler: async (args, ctx) => {
|
|
1061
|
-
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+/);
|
|
1062
1098
|
const sub = parts[0] || "select";
|
|
1063
1099
|
|
|
1064
1100
|
switch (sub) {
|
|
@@ -1252,8 +1288,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1252
1288
|
|
|
1253
1289
|
case "clear": {
|
|
1254
1290
|
if (activeSquadId) schedulers.delete(activeSquadId);
|
|
1255
|
-
|
|
1256
|
-
widgetState.squadId = null;
|
|
1291
|
+
focusSquad(null);
|
|
1257
1292
|
widgetControls?.dispose();
|
|
1258
1293
|
ctx.ui.notify("Squad view cleared", "info");
|
|
1259
1294
|
return;
|
|
@@ -1274,9 +1309,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1274
1309
|
await sched.stop();
|
|
1275
1310
|
}
|
|
1276
1311
|
schedulers.clear();
|
|
1277
|
-
|
|
1278
|
-
widgetState.squadId = null;
|
|
1279
|
-
widgetControls?.requestUpdate();
|
|
1312
|
+
focusSquad(null);
|
|
1280
1313
|
|
|
1281
1314
|
let count = 0;
|
|
1282
1315
|
for (const id of allSquadIds) {
|
|
@@ -1312,9 +1345,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1312
1345
|
await sched.stop();
|
|
1313
1346
|
}
|
|
1314
1347
|
schedulers.clear();
|
|
1315
|
-
|
|
1316
|
-
widgetState.squadId = null;
|
|
1317
|
-
widgetControls?.requestUpdate();
|
|
1348
|
+
focusSquad(null);
|
|
1318
1349
|
let count = 0;
|
|
1319
1350
|
for (const id of allSquadIds) {
|
|
1320
1351
|
fs.rmSync(store.getSquadDir(id), { recursive: true, force: true });
|
|
@@ -1332,11 +1363,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1332
1363
|
await sched.stop();
|
|
1333
1364
|
schedulers.delete(squad.id);
|
|
1334
1365
|
}
|
|
1335
|
-
if (activeSquadId === squad.id)
|
|
1336
|
-
activeSquadId = null;
|
|
1337
|
-
widgetState.squadId = null;
|
|
1338
|
-
widgetControls?.requestUpdate();
|
|
1339
|
-
}
|
|
1366
|
+
if (activeSquadId === squad.id) focusSquad(null);
|
|
1340
1367
|
fs.rmSync(store.getSquadDir(squad.id), { recursive: true, force: true });
|
|
1341
1368
|
ctx.ui.notify(`Deleted: ${squad.id}`, "info");
|
|
1342
1369
|
}
|
|
@@ -1345,24 +1372,48 @@ export default function (pi: ExtensionAPI) {
|
|
|
1345
1372
|
}
|
|
1346
1373
|
|
|
1347
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
|
+
}
|
|
1348
1384
|
squadEnabled = true;
|
|
1385
|
+
widgetState.enabled = true;
|
|
1386
|
+
if (!wasEnabled) widgetState.squadId = null;
|
|
1387
|
+
if (!widgetControls && uiCtx?.hasUI) widgetControls = setupSquadWidget(uiCtx, widgetState);
|
|
1349
1388
|
widgetControls?.requestUpdate();
|
|
1350
|
-
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");
|
|
1351
1390
|
return;
|
|
1352
1391
|
}
|
|
1353
1392
|
|
|
1354
1393
|
case "disable": {
|
|
1355
|
-
squadEnabled
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
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;
|
|
1359
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();
|
|
1360
1410
|
schedulers.clear();
|
|
1361
|
-
activeSquadId = null;
|
|
1362
|
-
widgetState.squadId = null;
|
|
1363
1411
|
widgetState.enabled = false;
|
|
1364
|
-
|
|
1365
|
-
|
|
1412
|
+
widgetState.squadId = null;
|
|
1413
|
+
focusSquad(null);
|
|
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");
|
|
1366
1417
|
return;
|
|
1367
1418
|
}
|
|
1368
1419
|
|
|
@@ -1627,19 +1678,19 @@ async function pickSquad(
|
|
|
1627
1678
|
* Does NOT start a scheduler (view-only unless squad needs resuming).
|
|
1628
1679
|
*/
|
|
1629
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
|
+
}
|
|
1630
1685
|
const squad = store.loadSquad(squadId);
|
|
1631
1686
|
if (!squad) {
|
|
1632
1687
|
ctx.ui.notify(`Squad '${squadId}' not found`, "error");
|
|
1633
1688
|
return;
|
|
1634
1689
|
}
|
|
1635
1690
|
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
// render, so just updating the state and requesting a render is enough.
|
|
1640
|
-
widgetState.squadId = squadId;
|
|
1641
|
-
widgetState.enabled = true;
|
|
1642
|
-
widgetControls?.requestUpdate();
|
|
1691
|
+
// Selection is one atomic focus operation: panel, widget, status, and tools
|
|
1692
|
+
// must all target this exact squad before control returns to the caller.
|
|
1693
|
+
focusSquad(squadId);
|
|
1643
1694
|
|
|
1644
1695
|
// Compact notification — widget already shows full task details.
|
|
1645
1696
|
// Avoid large multi-line notifications that can break TUI layout.
|
|
@@ -1660,6 +1711,9 @@ function forceWidgetUpdate(): void {
|
|
|
1660
1711
|
/** Wire one scheduler to durable main-session notifications. */
|
|
1661
1712
|
function wireSchedulerEvents(pi: ExtensionAPI, scheduler: Scheduler, squadId: string): void {
|
|
1662
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;
|
|
1663
1717
|
forceWidgetUpdate();
|
|
1664
1718
|
switch (event.type) {
|
|
1665
1719
|
case "squad_review_required": {
|
|
@@ -1742,19 +1796,43 @@ function openPanel(
|
|
|
1742
1796
|
scheduler: Scheduler,
|
|
1743
1797
|
squadId: string,
|
|
1744
1798
|
): void {
|
|
1799
|
+
if (!squadEnabled) {
|
|
1800
|
+
ctx.ui.notify(DISABLED_GUIDANCE, "warning");
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1745
1803
|
if (overlayOpen) return;
|
|
1804
|
+
// Opening details also establishes authoritative focus. This covers callers
|
|
1805
|
+
// that reconstructed or targeted a scheduler without going through selector UI.
|
|
1806
|
+
focusSquad(squadId);
|
|
1746
1807
|
overlayOpen = true;
|
|
1747
1808
|
|
|
1748
1809
|
// The promise resolves when the panel calls done()
|
|
1749
1810
|
const panelPromise = ctx.ui.custom<SquadPanelResult>(
|
|
1750
1811
|
(tui, theme, _kb, done) => {
|
|
1751
|
-
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();
|
|
1752
1822
|
|
|
1753
1823
|
// Wire up message sending from panel
|
|
1754
1824
|
panel.onSendMessage = async (taskId: string, _prefill: string) => {
|
|
1825
|
+
if (!squadEnabled) {
|
|
1826
|
+
ctx.ui.notify(DISABLED_GUIDANCE, "warning");
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1755
1829
|
const task = store.loadTask(squadId, taskId);
|
|
1756
1830
|
const agentName = task?.agent || taskId;
|
|
1757
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
|
+
}
|
|
1758
1836
|
if (input) {
|
|
1759
1837
|
let panelSched = schedulers.get(squadId);
|
|
1760
1838
|
if (!panelSched) {
|
|
@@ -1788,9 +1866,11 @@ function openPanel(
|
|
|
1788
1866
|
// When panel closes (done() called), clean up
|
|
1789
1867
|
panelPromise.then(() => {
|
|
1790
1868
|
overlayOpen = false;
|
|
1869
|
+
closeOverlay = null;
|
|
1791
1870
|
forceWidgetUpdate();
|
|
1792
1871
|
}).catch(() => {
|
|
1793
1872
|
overlayOpen = false;
|
|
1873
|
+
closeOverlay = null;
|
|
1794
1874
|
});
|
|
1795
1875
|
}
|
|
1796
1876
|
|
|
@@ -1841,6 +1921,10 @@ async function startSquad(
|
|
|
1841
1921
|
}
|
|
1842
1922
|
}
|
|
1843
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
|
+
|
|
1844
1928
|
// Merge agent roster
|
|
1845
1929
|
const agents: Record<string, SquadAgentEntry> = { ...plan.agents };
|
|
1846
1930
|
if (params.agents) {
|
|
@@ -1914,12 +1998,8 @@ async function startSquad(
|
|
|
1914
1998
|
// Start scheduler
|
|
1915
1999
|
const scheduler = new Scheduler(squadId, skillPaths, schedulerSpawnContext);
|
|
1916
2000
|
schedulers.set(squadId, scheduler);
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
// Activate widget for this squad
|
|
1920
|
-
widgetState.squadId = squadId;
|
|
1921
|
-
widgetState.enabled = true;
|
|
1922
|
-
widgetControls?.requestUpdate();
|
|
2001
|
+
// Activate panel/widget/tool focus as one invariant.
|
|
2002
|
+
focusSquad(squadId);
|
|
1923
2003
|
|
|
1924
2004
|
// Wire up completion/escalation notifications to main agent.
|
|
1925
2005
|
wireSchedulerEvents(pi, scheduler, squadId);
|
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);
|
|
@@ -43,6 +43,12 @@ export interface SquadWidgetState {
|
|
|
43
43
|
enabled: boolean;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export interface SquadWidgetControls {
|
|
47
|
+
requestUpdate: () => void;
|
|
48
|
+
refreshNow: () => void;
|
|
49
|
+
dispose: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
/**
|
|
47
53
|
* Set up the squad widget. Returns control functions.
|
|
48
54
|
*
|
|
@@ -53,11 +59,8 @@ export interface SquadWidgetState {
|
|
|
53
59
|
export function setupSquadWidget(
|
|
54
60
|
ctx: { ui: { setWidget: Function; setStatus: Function; [key: string]: any }; hasUI?: boolean },
|
|
55
61
|
state: SquadWidgetState,
|
|
56
|
-
): {
|
|
57
|
-
requestUpdate: () =>
|
|
58
|
-
dispose: () => void;
|
|
59
|
-
} {
|
|
60
|
-
if (!ctx.hasUI) return { requestUpdate: () => {}, dispose: () => {} };
|
|
62
|
+
): SquadWidgetControls {
|
|
63
|
+
if (!ctx.hasUI) return { requestUpdate: () => {}, refreshNow: () => {}, dispose: () => {} };
|
|
61
64
|
|
|
62
65
|
let durationTimer: ReturnType<typeof setInterval> | null = null;
|
|
63
66
|
let renderTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -269,9 +272,14 @@ export function setupSquadWidget(
|
|
|
269
272
|
}
|
|
270
273
|
}
|
|
271
274
|
|
|
275
|
+
function refreshNow(): void {
|
|
276
|
+
if (renderTimer) { clearTimeout(renderTimer); renderTimer = null; }
|
|
277
|
+
render();
|
|
278
|
+
manageDurationTimer();
|
|
279
|
+
}
|
|
280
|
+
|
|
272
281
|
// Initial render
|
|
273
|
-
|
|
274
|
-
manageDurationTimer();
|
|
282
|
+
refreshNow();
|
|
275
283
|
|
|
276
284
|
return {
|
|
277
285
|
requestUpdate(): void {
|
|
@@ -279,14 +287,17 @@ export function setupSquadWidget(
|
|
|
279
287
|
if (renderTimer) return;
|
|
280
288
|
renderTimer = setTimeout(() => {
|
|
281
289
|
renderTimer = null;
|
|
282
|
-
|
|
283
|
-
manageDurationTimer();
|
|
290
|
+
refreshNow();
|
|
284
291
|
}, 50);
|
|
285
292
|
},
|
|
293
|
+
refreshNow,
|
|
286
294
|
dispose(): void {
|
|
287
295
|
if (durationTimer) { clearInterval(durationTimer); durationTimer = null; }
|
|
288
296
|
if (renderTimer) { clearTimeout(renderTimer); renderTimer = null; }
|
|
289
297
|
lastCacheKey = "";
|
|
298
|
+
currentLines = [];
|
|
299
|
+
widgetInstalled = false;
|
|
300
|
+
widgetComponent = null;
|
|
290
301
|
ctx.ui.setWidget("squad-tasks", undefined);
|
|
291
302
|
ctx.ui.setStatus("squad", undefined);
|
|
292
303
|
},
|
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: {
|