pi-maestro-teammate 2.4.0 → 2.6.0

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.
Files changed (58) hide show
  1. package/README.md +10 -4
  2. package/agents/analyst.md +0 -1
  3. package/agents/explorer.md +0 -1
  4. package/agents/general.md +0 -1
  5. package/agents/planner.md +2 -2
  6. package/agents/research.md +0 -1
  7. package/agents/verifier.md +0 -1
  8. package/agents/workflow.md +0 -1
  9. package/package.json +3 -3
  10. package/src/completion-outbox/coordinator.ts +39 -10
  11. package/src/completion-outbox/registry.ts +24 -1
  12. package/src/extension/index.ts +297 -143
  13. package/src/extension/teammate-core.ts +46 -24
  14. package/src/extension/teammate-helpers.ts +24 -0
  15. package/src/extension/teammate-proxy.ts +112 -33
  16. package/src/models/model-routing.ts +131 -43
  17. package/src/public/v1/completion-durability.ts +2 -0
  18. package/src/public/v1/model-routing.ts +4 -0
  19. package/src/public/v1/ssh-hosts.ts +560 -209
  20. package/src/public/v1/supervision.ts +9 -0
  21. package/src/remote/acp-client-operations.ts +1 -1
  22. package/src/remote/acp-driver.ts +2 -2
  23. package/src/remote/pi-rpc-driver.ts +1 -1
  24. package/src/remote/ssh.ts +48 -2
  25. package/src/runs/execution-infra.ts +29 -5
  26. package/src/runs/execution.ts +166 -39
  27. package/src/runs/pi-subprocess-attempt.ts +3279 -2960
  28. package/src/runs/recovery-protocol.ts +98 -0
  29. package/src/runs/retry.ts +10 -0
  30. package/src/runs/runtime-actor.ts +8 -3
  31. package/src/shared/types.ts +36 -2
  32. package/src/supervision/advisor-runtime.ts +156 -0
  33. package/src/transcript/session-history.ts +1 -1
  34. package/src/tui/attach-overlay.ts +8 -0
  35. package/src/tui/locale-catalog-model.ts +20 -0
  36. package/src/tui/locale-catalog-profiles.ts +4 -4
  37. package/src/tui/model-mapping-overlay.ts +70 -101
  38. package/src/tui/render.ts +16 -12
  39. package/types/completion-outbox/coordinator.d.ts +1 -0
  40. package/types/completion-outbox/registry.d.ts +1 -0
  41. package/types/extension/teammate-core.d.ts +3 -3
  42. package/types/extension/teammate-helpers.d.ts +3 -0
  43. package/types/models/model-routing.d.ts +27 -6
  44. package/types/public/v1/completion-durability.d.ts +2 -0
  45. package/types/public/v1/model-routing.d.ts +1 -0
  46. package/types/public/v1/ssh-hosts.d.ts +45 -1
  47. package/types/public/v1/supervision.d.ts +2 -0
  48. package/types/remote/ssh.d.ts +3 -0
  49. package/types/runs/execution-infra.d.ts +14 -3
  50. package/types/runs/pi-subprocess-attempt.d.ts +3 -1
  51. package/types/runs/recovery-protocol.d.ts +60 -0
  52. package/types/runs/retry.d.ts +10 -0
  53. package/types/shared/types.d.ts +28 -2
  54. package/types/supervision/advisor-runtime.d.ts +14 -0
  55. package/types/transcript/session-history.d.ts +1 -1
  56. package/types/tui/locale-catalog-model.d.ts +20 -0
  57. package/types/tui/locale-catalog-profiles.d.ts +4 -4
  58. package/types/tui/model-mapping-overlay.d.ts +3 -0
package/README.md CHANGED
@@ -189,12 +189,12 @@ project .pi/agents > project .agents > ~/.agents > legacy user directory > bundl
189
189
 
190
190
  Configure task-type defaults with `Alt+M` or `/teammate-models`. The Control Center's **Profiles** tab is the saved-template system: it manages multiple named routing Profiles shared by every project. Its **Routing** tab edits the active global Profile and combines built-in task types, discovered agent types, and types already present in that Profile.
191
191
 
192
- Switch the current project's template directly by stable Profile ID or display name. With no argument, the command opens the Control Center on **Profiles**:
192
+ Switch the current project's template directly by stable Profile ID or display name. With no argument, the same command opens the full Control Center:
193
193
 
194
194
  ```text
195
- /teammate-model fast-lane
196
- /teammate-model Fast Lane
197
- /teammate-model
195
+ /teammate-models fast-lane
196
+ /teammate-models Fast Lane
197
+ /teammate-models
198
198
  ```
199
199
 
200
200
  Command completion lists every saved template and marks the project-active and global-default entries. If display names are duplicated, use the stable ID. Direct switching disables but preserves project overrides, matching the Profiles UI.
@@ -359,6 +359,12 @@ observe({ action: "watch", targets: [{ kind: "teammate", id: "reviewer" }], time
359
359
 
360
360
  A durable, per-workspace-isolated message queue backing cross-session delivery (staging → ready → claimed → accepted, atomic writes + idempotent receipts). Cold resume stays synchronous when the mailbox is authoritative; Windows file-lock renames retry automatically and orphaned state records are garbage-collected. External consumers (the Flow host) integrate through the `pi-maestro-teammate/v1/mailbox` subpath: the extension publishes a live `MailboxHostRegistry` on the shared-process bridge key `Symbol.for("pi-maestro-teammate.mailbox-registry")` (durable `enqueueTaskNotification`, per-recipient `pendingCount`, and `negotiate` capability v1/v2, with `taskId`-keyed dedup); `pi-maestro-flow` consumes it via `mailboxRegistry()` (see `packages/pi-maestro-flow/src/extension/index.ts`) and the contract is covered by `test/mailbox-registry.test.ts`.
361
361
 
362
+ ## Advisor Compatibility
363
+
364
+ When installed by itself, `pi-maestro-teammate` keeps the low-frequency standalone Advisor. Configure its defaults in `.pi/settings.json` under `monitor.advisor` (or the top-level `advisor` key), or with `PI_ADVISOR`, `PI_ADVISOR_COOLDOWN_MS`, and `PI_ADVISOR_MAX_REVIEWS`. `/advisor on|off` is a current-session override; `/advisor status` shows the effective state.
365
+
366
+ When `pi-maestro-flow` is also loaded, its `pi-maestro-flow/advisor` runtime is the canonical owner and this package's priority-10 fallback sleeps, even when the Flow Advisor is disabled. A process-wide registry and one dynamic `/advisor` broker prevent duplicate commands, double dispatch, and stale delivery in either package load order. Flow reads canonical `.pi/advisor.json` first, otherwise the legacy settings above, then defaults, and finally the same environment overrides. See the Flow Advisor guide for manual/hybrid mode, automatic review budgets, and `reviewEveryToolResults: 0`.
367
+
362
368
  ## Runtime
363
369
 
364
370
  - Foreground dispatch is the default and returns child results directly.
package/agents/analyst.md CHANGED
@@ -4,7 +4,6 @@ description: "Read-only technical analysis and review specialist. Use for techni
4
4
  systemPromptMode: replace
5
5
  inheritProjectContext: false
6
6
  thinking: high
7
- taskType: analysis
8
7
  tools: read, grep, find, ls
9
8
  inheritSkills: false
10
9
  ---
@@ -3,7 +3,6 @@ name: explorer
3
3
  description: "Read-only codebase discovery and call-chain tracing specialist. Use when you need file:line answers fast; not for analysis, planning, or implementation."
4
4
  systemPromptMode: replace
5
5
  thinking: low
6
- taskType: explore
7
6
  tools: read, grep, find, ls
8
7
  inheritProjectContext: false
9
8
  inheritSkills: false
package/agents/general.md CHANGED
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: general
3
3
  description: "General-purpose teammate for direct implementation, analysis, and verification. Use for direct work in the project; not for read-only discovery, planning, or DAG orchestration."
4
- taskType: development
5
4
  systemPromptMode: append
6
5
  inheritProjectContext: true
7
6
  tools: read, grep, find, ls, bash, edit, write
package/agents/planner.md CHANGED
@@ -4,7 +4,6 @@ description: "Read-only architecture and execution planning specialist. Use to p
4
4
  systemPromptMode: replace
5
5
  inheritProjectContext: true
6
6
  thinking: high
7
- taskType: planning
8
7
  tools: read, grep, find, ls
9
8
  inheritSkills: false
10
9
  ---
@@ -59,7 +58,8 @@ Return only Markdown for the Plan, with no preface, commentary, interview log, o
59
58
  Dependencies must form an executable DAG and identify safe parallel work. A task is a verifiable outcome, not a command or activity log.
60
59
  8. `## Validation`: specify exact commands or observable checks, expected results, requirement coverage, and relevant regression or integration boundaries.
61
60
  9. `## Risks and Recovery`: state concrete risks, mitigations, and rollback or recovery behavior.
62
- 10. `## Open Decisions`: list unresolved user-owned decisions. Write `None` only after evidence-based review; a Plan with unresolved decisions is not confirmation-ready.
61
+ 10. `## Knowledge Outcome`: require an end-of-execution assessment after implementation and verification. Name plausible candidate topics only when the work may produce a reusable non-obvious pitfall, failure lesson, trade-off, or prescriptive constraint; otherwise write `None expected` with a concrete reason. This section predicts what to assess—it must not fabricate knowledge in advance. Actual staging happens after approval in Act/Run mode, and the executor must explicitly report zero candidates when no result meets the project's knowledge quality bar.
62
+ 11. `## Open Decisions`: list unresolved user-owned decisions. Write `None` only after evidence-based review; a Plan with unresolved decisions is not confirmation-ready.
63
63
 
64
64
  Execution ownership: after the Plan is approved, implementation defaults to the project's `general-executor` agent (fallback: `general` when that role is not discovered). State this default in the Plan's Execution Plan section when it helps, and shape each task so a generic executor can consume it without rediscovery — concrete outcome, bounded scope, named files, acceptance criteria, and verification commands. Do not assume a workflow-task pipeline (`.task/TASK-*.json`) exists; the Plan must be executable by `general-executor` from the Plan text alone.
65
65
 
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: research
3
3
  description: "Read-only project knowledge and external web research specialist. Use for source-grounded answers from knowledge bases or the web; not for code discovery or implementation."
4
- taskType: analysis
5
4
  thinking: high
6
5
  systemPromptMode: replace
7
6
  inheritProjectContext: true
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: verifier
3
3
  description: "Independent read-only fallback verifier for Goal completion claims. Use only when a Goal declares no acceptance commands; not for ordinary review."
4
- taskType: verification
5
4
  thinking: low
6
5
  systemPromptMode: replace
7
6
  inheritProjectContext: false
@@ -4,7 +4,6 @@ description: "Decomposes complex problems and dispatches dependency-aware teamma
4
4
  systemPromptMode: replace
5
5
  inheritProjectContext: true
6
6
  thinking: high
7
- taskType: planning
8
7
  tools: read, grep, find, ls, teammate, teammate-send, teammate-list, observe
9
8
  inheritSkills: false
10
9
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-maestro-teammate",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "description": "Pi extension — teammate agent dispatch with DAG task graphs, RPC messaging, and compact TUI",
5
5
  "type": "module",
6
6
  "engines": {
@@ -195,8 +195,8 @@
195
195
  "cross-spawn": "7.0.6",
196
196
  "jiti": "2.7.0",
197
197
  "pi-maestro-settings-core": "0.2.1",
198
- "pi-maestro-backend-core": "*",
199
- "pi-maestro-backends": "*",
198
+ "pi-maestro-backend-core": "0.1.3",
199
+ "pi-maestro-backends": "0.1.3",
200
200
  "proper-lockfile": "4.1.2",
201
201
  "ssh2": "1.17.0",
202
202
  "zod": "4.4.3"
@@ -150,6 +150,7 @@ export class CompletionDeliveryCoordinator {
150
150
  readonly #operations = new Set<Promise<unknown>>();
151
151
  #binding: CompletionSessionBinding | undefined;
152
152
  #unsubscribe: (() => void) | undefined;
153
+ #accepting = true;
153
154
  #disposed = false;
154
155
  // Throttle GC attempts inside reconcile(). tryGc() also persists a cross-process
155
156
  // page/expiry fence, but this local guard prevents one coordinator from
@@ -169,8 +170,12 @@ export class CompletionDeliveryCoordinator {
169
170
  });
170
171
  }
171
172
 
172
- async beginDispatch(seed: CompletionDispatchSeed): Promise<CompletionDispatchDurability> {
173
- if (!this.#enabled()) return { durable: false };
173
+ beginDispatch(seed: CompletionDispatchSeed): Promise<CompletionDispatchDurability> {
174
+ return this.#track(this.#beginDispatch(seed));
175
+ }
176
+
177
+ async #beginDispatch(seed: CompletionDispatchSeed): Promise<CompletionDispatchDurability> {
178
+ if (!this.#accepting || this.#disposed || !this.#enabled()) return { durable: false };
174
179
  const existing = this.#dispatches.get(seed.dispatchId);
175
180
  if (existing) {
176
181
  if (existing.handle.reservationId !== seed.reservationId
@@ -181,21 +186,42 @@ export class CompletionDeliveryCoordinator {
181
186
  }
182
187
  const provider = this.registry.current();
183
188
  if (!provider) return { durable: false };
184
- await this.store.reserve(seed);
189
+ // Acquire the cross-extension ownership pin before the first await. Flow
190
+ // may retire its current provider as soon as shutdown begins, but capture
191
+ // must retain this exact generation until the admitted begin either commits
192
+ // into #dispatches or rolls back.
193
+ const releaseProviderPin = this.registry.pinDispatch(seed.dispatchId, provider);
194
+ let reserved = false;
185
195
  try {
196
+ await this.store.reserve(seed);
197
+ reserved = true;
186
198
  const handle = await provider.beginDispatch(seed);
187
- // Pin the provider instance in the shared registry as well as this
188
- // coordinator. Flow publication capture resolves stage/commit through
189
- // this dispatch ownership fence even after a provider reload.
190
- const releaseProviderPin = this.registry.pinDispatch(seed.dispatchId, provider);
199
+ if (!this.#accepting || this.#disposed) {
200
+ await provider.abandonDispatch({
201
+ dispatchId: seed.dispatchId,
202
+ reservationId: seed.reservationId,
203
+ reason: "completion coordinator stopped admission during beginDispatch",
204
+ abandonedAt: this.#now(),
205
+ }).catch(() => undefined);
206
+ await this.store.releaseReservation(seed.target, seed.reservationId).catch(() => undefined);
207
+ releaseProviderPin();
208
+ return { durable: false };
209
+ }
191
210
  this.#dispatches.set(seed.dispatchId, { handle, seed, provider, releaseProviderPin });
192
211
  return { durable: true, handle };
193
212
  } catch (error) {
194
- await this.store.releaseReservation(seed.target, seed.reservationId).catch(() => undefined);
213
+ if (reserved) {
214
+ await this.store.releaseReservation(seed.target, seed.reservationId).catch(() => undefined);
215
+ }
216
+ releaseProviderPin();
195
217
  throw error;
196
218
  }
197
219
  }
198
220
 
221
+ stopAdmission(): void {
222
+ this.#accepting = false;
223
+ }
224
+
199
225
  async requireNotification(input: CompletionNotificationRequirement): Promise<void> {
200
226
  if (!this.#dispatches.has(input.dispatchId)) return;
201
227
  const provider = this.#pinnedProvider(input.dispatchId);
@@ -368,6 +394,7 @@ export class CompletionDeliveryCoordinator {
368
394
  }
369
395
 
370
396
  dispose(): void {
397
+ this.#accepting = false;
371
398
  this.#disposed = true;
372
399
  this.#binding = undefined;
373
400
  this.#acceptedByHost.clear();
@@ -595,8 +622,10 @@ export class CompletionDeliveryCoordinator {
595
622
  if (pinned) return pinned.provider;
596
623
  const recovered = this.#recoveredProviderPins.get(dispatchId);
597
624
  if (recovered) return recovered.provider;
598
- const provider = this.registry.providerForDispatch(dispatchId) ?? this.registry.current();
599
- if (!provider) throw new Error("Completion durability provider became unavailable.");
625
+ const provider = this.registry.providerForDispatch(dispatchId);
626
+ if (!provider) {
627
+ throw new Error(`Completion dispatch ${dispatchId} has no pinned durability provider.`);
628
+ }
600
629
  return provider;
601
630
  }
602
631
 
@@ -20,6 +20,7 @@ export class CompletionDurabilityRegistryImpl implements CompletionDurabilityReg
20
20
  provider: CompletionDurabilityProvider;
21
21
  owners: Set<symbol>;
22
22
  }>();
23
+ readonly #providerIdleWaiters = new Map<CompletionDurabilityProvider, Set<() => void>>();
23
24
 
24
25
  current(): CompletionDurabilityProvider | undefined {
25
26
  return this.#provider;
@@ -52,10 +53,24 @@ export class CompletionDurabilityRegistryImpl implements CompletionDurabilityReg
52
53
  released = true;
53
54
  const active = this.#dispatchPins.get(dispatchId);
54
55
  if (!active || active.provider !== provider || !active.owners.delete(owner)) return;
55
- if (active.owners.size === 0) this.#dispatchPins.delete(dispatchId);
56
+ if (active.owners.size === 0) {
57
+ this.#dispatchPins.delete(dispatchId);
58
+ this.#resolveProviderIdle(provider);
59
+ }
56
60
  };
57
61
  }
58
62
 
63
+ waitForProviderIdle(provider: CompletionDurabilityProvider): Promise<void> {
64
+ if (![...this.#dispatchPins.values()].some((pin) => pin.provider === provider)) {
65
+ return Promise.resolve();
66
+ }
67
+ return new Promise<void>((resolve) => {
68
+ const waiters = this.#providerIdleWaiters.get(provider) ?? new Set<() => void>();
69
+ waiters.add(resolve);
70
+ this.#providerIdleWaiters.set(provider, waiters);
71
+ });
72
+ }
73
+
59
74
  register(provider: CompletionDurabilityProvider): () => void {
60
75
  if (!provider || typeof provider !== "object") {
61
76
  throw new TypeError("Completion durability provider must be an object.");
@@ -80,6 +95,14 @@ export class CompletionDurabilityRegistryImpl implements CompletionDurabilityReg
80
95
  return () => this.#listeners.delete(listener);
81
96
  }
82
97
 
98
+ #resolveProviderIdle(provider: CompletionDurabilityProvider): void {
99
+ if ([...this.#dispatchPins.values()].some((pin) => pin.provider === provider)) return;
100
+ const waiters = this.#providerIdleWaiters.get(provider);
101
+ if (!waiters) return;
102
+ this.#providerIdleWaiters.delete(provider);
103
+ for (const resolve of waiters) resolve();
104
+ }
105
+
83
106
  #notify(): void {
84
107
  const snapshot = this.snapshot();
85
108
  for (const listener of this.#listeners) {