pi-maestro-teammate 2.8.0 → 2.9.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.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Pi extension for dispatching one or more role-based teammate tasks through a sin
6
6
 
7
7
  ## Breaking Changes In 1.0
8
8
 
9
- > Current version: **2.8.0**. The 1.0 breaking changes below remain in effect; later releases added circuit breaker, retry resilience, quiet state, duration tracking, observe `watch`/`until=completed`, per-workspace mailbox isolation, lifecycle hardening, and explicit model-registry routing without breaking the v1 public import paths.
9
+ > Current version: **2.8.1**, with `pi-maestro-backends@0.1.7`; Backend-Core 0.1.5 and Settings-Core 0.2.3 remain unchanged. Classifier budgets and diagnostics are session-scoped; child compaction preserves the original task, cumulative search budget, and exact result references. See [RELEASE.md](../../RELEASE.md) for dependency changes and verification limits. The 1.0 breaking changes below remain in effect; later releases added circuit breaker, retry resilience, quiet state, duration tracking, observe `watch`/`until=completed`, per-workspace mailbox isolation, lifecycle hardening, and explicit model-registry routing without breaking the v1 public import paths.
10
10
 
11
11
  - Every public `teammate` call requires a non-empty `tasks` array.
12
12
  - Single-agent work is represented by `tasks` with one item.
@@ -31,7 +31,7 @@ From the dispatch prompt, extract:
31
31
  3. **Verify** — read the strongest matches to confirm they answer the target; discard weak hits instead of reporting them.
32
32
  4. **Report** — emit findings per the Output contract below.
33
33
 
34
- Budget: at most two search rounds beyond the initial pass. Stop there and report what remains unresolved rather than widening endlessly.
34
+ Budget: at most two search rounds beyond the initial pass, or the caller's tighter limit. A round is one bounded batch of queries for unresolved targets, not a separate allowance for each symbol or file. Track rounds cumulatively across compact/new_context; preserve consumed rounds, confirmed findings, negative results, and the exact next action in any handoff. Once the target is answered, return immediately. Do not re-search already-read files merely to polish line citations. At the limit, report what remains unresolved rather than widening endlessly.
35
35
 
36
36
  ## Output
37
37
 
package/agents/planner.md CHANGED
@@ -33,9 +33,9 @@ Treat a matched template as governing evidence: cite its entry ID in `## Evidenc
33
33
  ## Process
34
34
 
35
35
  1. **Research** — gather governing knowledge (`maestro search` → `maestro load`), verify repository facts yourself, and delegate at most one bounded nested task when it materially helps. Before designing, check for existing decision documents (`docs/*decision*`, `docs/*architecture*`, `maestro search "<topic> 决策|方案"`); when one exists, cite it in `## Evidence` and treat its locked decisions as settled premises — do not re-litigate them in the Plan.
36
- 2. **Design** — lock the technical decisions, affected interfaces and data flow, error behavior, and rejected alternatives whose trade-offs matter.
37
- 3. **Compose** — write the Plan per the Document Contract below.
38
- 4. **Self-check** — confirm every required section and task field is present, every user requirement traces to a planned outcome, and dependencies form an executable DAG before returning.
36
+ 2. **Design** — select the smallest end-to-end path using existing authorities. Separate user-required outcomes and explicit human-locked safety/compatibility decisions from agent-selected implementation means; select one approach without treating those means as immutable human requirements.
37
+ 3. **Compose** — write the Plan per the Document Contract below, proportionate to the requested outcome. Keep auxiliary diagnostics, provenance, and hardening out of the critical path unless a requirement or confirmed material defect makes them necessary.
38
+ 4. **Self-check** — trace every user requirement to an outcome; justify each blocking dependency with its requirement or confirmed defect, not merely an enumerated risk. Make shared input/authority contracts available before consumers need production-path acceptance. Confirm complete coverage and an executable DAG; identify the first end-to-end acceptance milestone, cumulative correction budget, and when to report a simpler equivalent option or need for redesign/user guidance.
39
39
 
40
40
  ## Output — Document Contract
41
41
 
@@ -46,7 +46,7 @@ Return only Markdown for the Plan, with no preface, commentary, interview log, o
46
46
  3. `## Evidence`: list governing knowledge or spec IDs, verified code entry points with `file:line` anchors, current behavior, and constraints. Separate verified facts from assumptions.
47
47
  4. `## Scope`: list explicit in-scope and out-of-scope boundaries, including compatibility and migration constraints.
48
48
  5. `## Requirements`: provide a table with `ID`, `Requirement / source`, `Planned outcome`, and `Acceptance evidence`. Map every user requirement to one or more planned outcomes.
49
- 6. `## Design`: lock technical decisions, affected interfaces and data flow, and error and failure behavior. A Plan describes exactly one selected approach — never present multiple alternative approaches side by side with a recommendation. When alternatives and their trade-offs matter, reference the decision document that settled them in `## Evidence`, or move the unresolved choice to `## Open Decisions`; do not embed an option comparison in the Plan body.
49
+ 6. `## Design`: specify the selected implementation, affected interfaces and data flow, and error and failure behavior; label explicit human-locked decisions separately from revisable implementation means. Equivalent simplification must preserve required behavior and guarantees; changes to approved scope, explicit locked decisions, or safety boundaries require renewed approval, never silent gate removal. A Plan describes exactly one selected approach — never present multiple alternative approaches side by side with a recommendation. When alternatives and their trade-offs matter, reference the decision document that settled them in `## Evidence`, or move the unresolved choice to `## Open Decisions`; do not embed an option comparison in the Plan body.
50
50
  7. `## Execution Plan`: define ordered, outcome-sized tasks. Every task must contain these fields:
51
51
  - `ID`
52
52
  - `Outcome`
@@ -57,7 +57,7 @@ Return only Markdown for the Plan, with no preface, commentary, interview log, o
57
57
  - `Verification`
58
58
  Dependencies must form an executable DAG and identify safe parallel work. A task is a verifiable outcome, not a command or activity log.
59
59
  8. `## Validation`: specify exact commands or observable checks, expected results, requirement coverage, and relevant regression or integration boundaries.
60
- 9. `## Risks and Recovery`: state concrete risks, mitigations, and rollback or recovery behavior.
60
+ 9. `## Risks and Recovery`: state concrete risks, mitigations, and rollback or recovery behavior. Risks are not automatically acceptance gates; justify any blocking mitigation. Count failed attempts by shared interface/state machine and delivery blocker across workers, task names, approaches, and resets; after three unsuccessful attempts, stop the correction chain and report its effect on the requested outcome plus a simpler equivalent option or need for redesign/user guidance.
61
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
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-maestro-teammate",
3
- "version": "2.8.0",
3
+ "version": "2.9.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": {
@@ -21,6 +21,7 @@
21
21
  ],
22
22
  "scripts": {
23
23
  "test": "node --experimental-transform-types --import ./test/setup.ts --test --test-concurrency=2 \"test/*.test.ts\"",
24
+ "test:host-observers-packed": "node --test test/host-observers-packed.test.mjs",
24
25
  "typecheck": "tsc -p tsconfig.build.json",
25
26
  "build:declarations": "tsc -p tsconfig.declarations.json",
26
27
  "check:declarations": "node scripts/check-declarations.mjs",
@@ -176,7 +177,7 @@
176
177
  },
177
178
  "files": [
178
179
  "bin/*.mjs",
179
- "src/**/*.{ts,json}",
180
+ "src/**/*.{ts,mjs,json}",
180
181
  "types/**/*.d.ts",
181
182
  "agents/",
182
183
  "README.md"
@@ -216,7 +217,7 @@
216
217
  "jiti": "2.7.0",
217
218
  "pi-maestro-settings-core": "0.2.3",
218
219
  "pi-maestro-backend-core": "0.1.5",
219
- "pi-maestro-backends": "0.1.6",
220
+ "pi-maestro-backends": "0.1.7",
220
221
  "pi-maestro-fabric-core": "0.1.0",
221
222
  "proper-lockfile": "4.1.2",
222
223
  "ssh2": "1.17.0",
@@ -56,9 +56,28 @@ export interface JevClient {
56
56
 
57
57
  interface PreparedJevDecision {
58
58
  identity: string;
59
+ effectiveModel?: string;
60
+ /** Present only after native authenticated model resolution. */
61
+ authenticatedModel?: string;
59
62
  decide(): Promise<JevResponse>;
60
63
  }
61
64
 
65
+ export interface ClassifierRuntimeReadiness {
66
+ status: "disabled" | "unknown" | "available" | "unavailable";
67
+ available: boolean;
68
+ reason?: string;
69
+ model?: string;
70
+ }
71
+
72
+ const probes = new WeakMap<JevClient, (signal?: AbortSignal) => Promise<ClassifierRuntimeReadiness>>();
73
+
74
+ /** @internal Authentication-only probe; never submits a model request. */
75
+ export async function probeJevClient(client: JevClient, signal?: AbortSignal): Promise<ClassifierRuntimeReadiness> {
76
+ signal?.throwIfAborted();
77
+ const probe = probes.get(client);
78
+ return probe ? probe(signal) : { status: "unknown", available: false, reason: "Legacy classifier credentials configured but not verified" };
79
+ }
80
+
62
81
  // Keep preparation internal to these clients: cache identity and execution must
63
82
  // use the same resolved native model, even when the host registry changes.
64
83
  const preparations = new WeakMap<JevClient, (request: Omit<JevRequest, "model">) => Promise<PreparedJevDecision>>();
@@ -136,19 +155,55 @@ export interface ClassifierRuntime {
136
155
  classify(model: ClassifierModel<ClassifierApi>, context: ClassifierContext, options?: ModelsClassifierOptions): Promise<ClassifierResult>;
137
156
  }
138
157
 
139
- export function createNativeJevClient(runtime: ClassifierRuntime, options: Pick<JevClientOptions, "endpoint" | "model" | "timeoutMs">): JevClient {
158
+ export interface NativeJevClientOptions {
159
+ /** Absent means auto across authenticated classifier providers. */
160
+ endpoint?: JevEndpoint;
161
+ model?: string;
162
+ timeoutMs?: number;
163
+ }
164
+
165
+ function nativeSignal(options: NativeJevClientOptions, signal?: AbortSignal): AbortSignal {
166
+ const timeout = AbortSignal.timeout(Math.max(1, options.timeoutMs ?? JEV_DEFAULT_TIMEOUT_MS));
167
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
168
+ }
169
+
170
+ /** One resolver for both execution and readiness; registry entries alone are not auth evidence. */
171
+ async function resolveNativeClassifierModel(runtime: ClassifierRuntime, options: NativeJevClientOptions, signal: AbortSignal): Promise<ClassifierModel<ClassifierApi>> {
172
+ signal.throwIfAborted();
173
+ const reference = options.model?.trim() || undefined;
174
+ const slash = reference?.indexOf("/") ?? -1;
175
+ const pinnedProvider = slash > 0 ? reference!.slice(0, slash) : undefined;
176
+ if (options.endpoint && pinnedProvider && pinnedProvider !== options.endpoint) {
177
+ throw new Error("Native classifier unavailable (model provider conflicts with explicit endpoint)");
178
+ }
179
+ const provider = options.endpoint ?? pinnedProvider;
180
+ let abort!: () => void;
181
+ const cancelled = new Promise<never>((_resolve, reject) => {
182
+ abort = () => reject(signal.reason ?? new Error("Classifier probe aborted"));
183
+ signal.addEventListener("abort", abort, { once: true });
184
+ if (signal.aborted) abort();
185
+ });
186
+ let available: readonly ClassifierModel<ClassifierApi>[];
187
+ try {
188
+ available = await Promise.race([runtime.getAvailableOfType("classifier", provider, { signal }), cancelled]);
189
+ signal.throwIfAborted();
190
+ } finally {
191
+ signal.removeEventListener("abort", abort);
192
+ }
193
+ const candidates = available.filter((entry) => !provider || entry.provider === provider);
194
+ // Stable preference within provider tiers: never manufacture a default model.
195
+ const rank = (entry: ClassifierModel<ClassifierApi>) => entry.provider === "typesafe" ? 0 : entry.provider === "openrouter" ? 1 : 2;
196
+ const model = reference
197
+ ? candidates.find((entry) => slash > 0 ? entry.provider === pinnedProvider && entry.id === reference.slice(slash + 1) : entry.id === reference)
198
+ : candidates.reduce<ClassifierModel<ClassifierApi> | undefined>((best, entry) => !best || rank(entry) < rank(best) ? entry : best, undefined);
199
+ if (!model) throw new Error("Native classifier unavailable (no matching authenticated classifier model)");
200
+ return model;
201
+ }
202
+
203
+ export function createNativeJevClient(runtime: ClassifierRuntime, options: NativeJevClientOptions = {}): JevClient {
140
204
  const prepare = async (request: Omit<JevRequest, "model">): Promise<PreparedJevDecision> => {
141
- const signal = AbortSignal.timeout(Math.max(1, options.timeoutMs ?? JEV_DEFAULT_TIMEOUT_MS));
142
- const provider = options.endpoint;
143
- const available = await runtime.getAvailableOfType("classifier", options.model?.includes("/") ? undefined : provider, { signal });
144
- const reference = options.model;
145
- const slash = reference?.indexOf("/") ?? -1;
146
- const model = reference
147
- ? (slash > 0
148
- ? runtime.getModelOfType("classifier", reference.slice(0, slash), reference.slice(slash + 1))
149
- : available.find((entry) => entry.id === reference))
150
- : available[0];
151
- if (!model) throw new Error("Native classifier unavailable (no matching authenticated classifier model)");
205
+ const signal = nativeSignal(options);
206
+ const model = await resolveNativeClassifierModel(runtime, options, signal);
152
207
  const questions: ClassifierContext["questions"] = {};
153
208
  for (const [id, question] of Object.entries(request.questions)) {
154
209
  questions[id] = question.type === "noul"
@@ -159,6 +214,7 @@ export function createNativeJevClient(runtime: ClassifierRuntime, options: Pick<
159
214
  }
160
215
  return {
161
216
  identity: JSON.stringify(["native", options.endpoint, model.provider, model.api, model.baseUrl, model.id]),
217
+ authenticatedModel: `${model.provider}/${model.id}`,
162
218
  async decide() {
163
219
  const result = await runtime.classify(model, { state: { text: request.state }, questions }, { signal });
164
220
  if (result.stopReason !== "stop") throw new Error(result.errorMessage ?? `Native classifier ${result.stopReason}`);
@@ -174,6 +230,10 @@ export function createNativeJevClient(runtime: ClassifierRuntime, options: Pick<
174
230
  };
175
231
  const client: JevClient = { decide: async (request) => (await prepare(request)).decide() };
176
232
  preparations.set(client, prepare);
233
+ probes.set(client, async (parent) => {
234
+ const model = await resolveNativeClassifierModel(runtime, options, nativeSignal(options, parent));
235
+ return { status: "available", available: true, model: `${model.provider}/${model.id}` };
236
+ });
177
237
  return client;
178
238
  }
179
239
 
@@ -204,6 +264,7 @@ export function createJevClient(options: JevClientOptions): JevClient {
204
264
  };
205
265
  preparations.set(client, async (request) => ({
206
266
  identity: JSON.stringify(["http", options.endpoint, url, model]),
267
+ effectiveModel: model,
207
268
  decide: () => client.decide(request),
208
269
  }));
209
270
  return client;
@@ -29,6 +29,8 @@ import {
29
29
  createJevClient,
30
30
  createNativeJevClient,
31
31
  type ClassifierRuntime,
32
+ type ClassifierRuntimeReadiness,
33
+ probeJevClient,
32
34
  prepareJevDecision,
33
35
  resolveJevEndpoint,
34
36
  type JevClient,
@@ -61,7 +63,7 @@ export interface ClassifierConfig {
61
63
  timeoutMs?: number;
62
64
  /** Answer cache TTL (default 10min — error text repeats heavily). */
63
65
  cacheTtlMs?: number;
64
- /** Max JEV calls per configure cycle (default 30). */
66
+ /** Max JEV calls per process-local session + cwd identity (default 30). */
65
67
  maxCallsPerSession?: number;
66
68
  /** domain name → mode. Absent = "off". */
67
69
  domains?: Record<string, ClassifierDomainMode>;
@@ -81,6 +83,9 @@ export interface ClassifierStatus {
81
83
  endpoint?: JevEndpoint;
82
84
  apiKeyPresent: boolean;
83
85
  model?: string;
86
+ runtimeStatus?: ClassifierRuntimeReadiness["status"];
87
+ runtimeReason?: string;
88
+ effectiveModel?: string;
84
89
  callsUsed: number;
85
90
  maxCalls: number;
86
91
  cacheSize: number;
@@ -92,9 +97,22 @@ const DEFAULT_MAX_CALLS = 30;
92
97
 
93
98
  let config: ClassifierConfig = { enabled: false };
94
99
  let client: JevClient | undefined;
95
- let hostBinding: { hostVersion: string; runtime: ClassifierRuntime } | undefined;
100
+ export interface ClassifierRuntimeBinding {
101
+ hostVersion: string;
102
+ runtime: ClassifierRuntime;
103
+ sessionId?: string;
104
+ cwd?: string;
105
+ }
106
+ export type { ClassifierRuntimeReadiness } from "./client.ts";
107
+
108
+ let hostBinding: ClassifierRuntimeBinding | undefined;
96
109
  let unavailableReason = "Classifier host runtime unavailable";
97
- let callsUsed = 0;
110
+ let apiKeyPresent = false;
111
+ let readiness: ClassifierRuntimeReadiness = { status: "disabled", available: false };
112
+ let readinessSequence = 0;
113
+ // Quota survives lifecycle fencing; intentionally no disk persistence.
114
+ let budgetKey = JSON.stringify(["", ""]);
115
+ const budgets = new Map<string, number>();
98
116
  const registry = new Map<string, ClassifyDomain<string, unknown>>();
99
117
  const cache = new Map<string, { response: JevResponse; at: number }>();
100
118
  const pending = new Map<string, Promise<JevResponse>>();
@@ -103,13 +121,13 @@ let generation = {};
103
121
 
104
122
  function fenceGeneration(): void {
105
123
  generation = {};
106
- callsUsed = 0;
124
+ readinessSequence++;
107
125
  cache.clear();
108
126
  pending.clear();
109
127
  }
110
128
 
111
129
  function captureGeneration() {
112
- return { generation, config, client, unavailableReason };
130
+ return { generation, config, client, unavailableReason, budgetKey, legacy: apiKeyPresent };
113
131
  }
114
132
 
115
133
  type Generation = ReturnType<typeof captureGeneration>;
@@ -119,8 +137,9 @@ function assertCurrent(owner: Generation): void {
119
137
  }
120
138
 
121
139
  /** Bind only the current process's host facade, never a child/remote runtime. */
122
- export function bindClassifierRuntime(binding: { hostVersion: string; runtime: ClassifierRuntime }): void {
123
- hostBinding = binding;
140
+ export function bindClassifierRuntime(binding: ClassifierRuntimeBinding): void {
141
+ hostBinding = { ...binding };
142
+ budgetKey = JSON.stringify([binding.sessionId ?? "", binding.cwd ?? ""]);
124
143
  configureClassifier(config);
125
144
  }
126
145
 
@@ -130,6 +149,8 @@ export function unbindClassifierRuntime(runtime: ClassifierRuntime): void {
130
149
  hostBinding = undefined;
131
150
  client = undefined;
132
151
  unavailableReason = "Classifier host runtime unavailable";
152
+ apiKeyPresent = false;
153
+ readiness = config.enabled ? { status: "unavailable", available: false, reason: unavailableReason } : { status: "disabled", available: false };
133
154
  }
134
155
 
135
156
  /** Inject classifier configuration (host loads `.pi/classifier.json` / env). */
@@ -139,10 +160,13 @@ export function configureClassifier(next: ClassifierConfig): void {
139
160
  const runtime = hostBinding?.runtime;
140
161
  const owner = getPiFeatureOwner(hostBinding?.hostVersion ?? next.hostVersion, !!runtime && typeof runtime.classify === "function" && typeof runtime.getAvailableOfType === "function" && typeof runtime.getModelOfType === "function");
141
162
  client = undefined;
163
+ apiKeyPresent = false;
142
164
  unavailableReason = "Classifier host runtime unavailable";
165
+ readiness = config.enabled ? { status: "unavailable", available: false, reason: unavailableReason } : { status: "disabled", available: false };
143
166
  if (!config.enabled) return;
144
167
  if (owner === "native" && runtime) {
145
- client = createNativeJevClient(runtime, { endpoint: next.endpoint ?? "typesafe", model: next.model, timeoutMs: next.timeoutMs });
168
+ client = createNativeJevClient(runtime, { endpoint: next.endpoint, model: next.model, timeoutMs: next.timeoutMs });
169
+ readiness = { status: "unknown", available: false, reason: "Native classifier authentication has not been probed" };
146
170
  return;
147
171
  }
148
172
  if (owner !== "legacy") return;
@@ -160,6 +184,33 @@ export function configureClassifier(next: ClassifierConfig): void {
160
184
  ...(next.fetchFn ? { fetchFn: next.fetchFn } : {}),
161
185
  } as JevClientOptions)
162
186
  : undefined;
187
+ apiKeyPresent = !!resolved;
188
+ readiness = resolved
189
+ ? { status: "unknown", available: false, reason: "Legacy classifier credentials configured but not verified" }
190
+ : { status: "unavailable", available: false, reason: unavailableReason };
191
+ }
192
+
193
+ /** Refresh authentication/model readiness without spending model-call quota. */
194
+ export async function probeClassifierRuntime(signal?: AbortSignal): Promise<ClassifierRuntimeReadiness> {
195
+ const owner = captureGeneration();
196
+ // Legacy probes do not authenticate, so they cannot supersede request evidence.
197
+ const sequence = owner.legacy ? readinessSequence : ++readinessSequence;
198
+ if (!owner.config.enabled || !owner.client) return { ...readiness };
199
+ let result: ClassifierRuntimeReadiness;
200
+ try {
201
+ result = await probeJevClient(owner.client, signal);
202
+ } catch (error) {
203
+ result = { status: "unavailable", available: false, reason: error instanceof Error ? error.message : String(error) };
204
+ }
205
+ if (owner.generation !== generation) {
206
+ return { status: "unknown", available: false, reason: "Classifier readiness superseded by runtime/config/session generation" };
207
+ }
208
+ // A non-verifying legacy probe must preserve same-generation execution evidence.
209
+ if (owner.legacy && result.status === "unknown") result = { ...readiness };
210
+ // Concurrent same-generation consumers may use their own auth result, but
211
+ // only the newest refresh owns the synchronous status projection.
212
+ if (sequence === readinessSequence && !signal?.aborted) readiness = result;
213
+ return { ...result };
163
214
  }
164
215
 
165
216
  export function classifierConfig(): ClassifierConfig {
@@ -173,6 +224,10 @@ export function resetClassifierForTest(): void {
173
224
  client = undefined;
174
225
  hostBinding = undefined;
175
226
  unavailableReason = "Classifier host runtime unavailable";
227
+ apiKeyPresent = false;
228
+ readiness = { status: "disabled", available: false };
229
+ budgets.clear();
230
+ budgetKey = JSON.stringify(["", ""]);
176
231
  registry.clear();
177
232
  }
178
233
 
@@ -196,9 +251,12 @@ export function classifierStatus(): ClassifierStatus {
196
251
  return {
197
252
  enabled: config.enabled === true,
198
253
  ...(config.endpoint ? { endpoint: config.endpoint } : {}),
199
- apiKeyPresent: client !== undefined,
254
+ apiKeyPresent,
200
255
  ...(config.model ? { model: config.model } : {}),
201
- callsUsed,
256
+ runtimeStatus: readiness.status,
257
+ ...(readiness.reason ? { runtimeReason: readiness.reason } : {}),
258
+ ...(readiness.model ? { effectiveModel: readiness.model } : {}),
259
+ callsUsed: budgets.get(budgetKey) ?? 0,
202
260
  maxCalls: config.maxCallsPerSession ?? DEFAULT_MAX_CALLS,
203
261
  cacheSize: cache.size,
204
262
  domains,
@@ -245,26 +303,56 @@ async function jevDecide<D extends string, I>(
245
303
  assertCurrent(owner);
246
304
  if (!owner.client) throw new Error(owner.unavailableReason);
247
305
  const questions = domain.questions();
248
- const prepared = await prepareJevDecision(owner.client, { state, questions });
306
+ // Native preparation is auth evidence. Legacy owns status only after cache,
307
+ // pending or quota admission; a rejected request must not supersede execution.
308
+ let sequence = owner.legacy ? undefined : ++readinessSequence;
309
+ let prepared: Awaited<ReturnType<typeof prepareJevDecision>>;
310
+ try {
311
+ prepared = await prepareJevDecision(owner.client, { state, questions });
312
+ } catch (error) {
313
+ assertCurrent(owner);
314
+ if (sequence === readinessSequence) readiness = { status: "unavailable", available: false, reason: error instanceof Error ? error.message : String(error) };
315
+ throw error;
316
+ }
249
317
  assertCurrent(owner);
318
+ if (prepared.authenticatedModel && sequence === readinessSequence) {
319
+ readiness = { status: "available", available: true, model: prepared.authenticatedModel };
320
+ }
250
321
  const key = createHash("sha256")
251
322
  .update(JSON.stringify([domain.name, prepared.identity, questions, state]))
252
323
  .digest("hex");
324
+ const publishSuccess = (response: JevResponse): JevResponse => {
325
+ assertCurrent(owner);
326
+ if (sequence === readinessSequence) readiness = {
327
+ status: "available", available: true,
328
+ model: prepared.authenticatedModel ?? response.model ?? prepared.effectiveModel,
329
+ };
330
+ return response;
331
+ };
332
+ const publishFailure = (error: unknown): void => {
333
+ if (owner.generation === generation && sequence === readinessSequence) readiness = {
334
+ status: "unavailable", available: false, reason: error instanceof Error ? error.message : String(error),
335
+ ...(prepared.authenticatedModel ? { model: prepared.authenticatedModel } : {}),
336
+ };
337
+ };
253
338
  const entry = cache.get(key);
254
339
  if (entry && Date.now() - entry.at <= (owner.config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS)) {
255
- return entry.response;
340
+ sequence ??= ++readinessSequence;
341
+ return publishSuccess(entry.response);
256
342
  }
257
343
  cache.delete(key);
258
344
  const existing = pending.get(key);
259
345
  if (existing) {
260
- const response = await existing;
261
- assertCurrent(owner);
262
- return response;
346
+ sequence ??= ++readinessSequence;
347
+ try { return publishSuccess(await existing); }
348
+ catch (error) { publishFailure(error); throw error; }
263
349
  }
350
+ const callsUsed = budgets.get(owner.budgetKey) ?? 0;
264
351
  if (callsUsed >= (owner.config.maxCallsPerSession ?? DEFAULT_MAX_CALLS)) {
265
352
  throw new Error("JEV session call budget exhausted");
266
353
  }
267
- callsUsed += 1;
354
+ budgets.set(owner.budgetKey, callsUsed + 1);
355
+ sequence ??= ++readinessSequence;
268
356
  // Reserve before invoking host code, which may re-enter configuration.
269
357
  const decision = Promise.resolve().then(() => {
270
358
  assertCurrent(owner);
@@ -275,7 +363,10 @@ async function jevDecide<D extends string, I>(
275
363
  const response = await decision;
276
364
  assertCurrent(owner);
277
365
  cache.set(key, { response, at: Date.now() });
278
- return response;
366
+ return publishSuccess(response);
367
+ } catch (error) {
368
+ publishFailure(error);
369
+ throw error;
279
370
  } finally {
280
371
  // A late old request must not remove a new generation's reservation.
281
372
  if (owner.generation === generation && pending.get(key) === decision) pending.delete(key);
@@ -271,8 +271,7 @@ import type {
271
271
  } from "../runs/execution.ts";
272
272
  import { createForkSnapshot } from "../runs/fork-snapshot.ts";
273
273
  import {
274
- auxToolCallFallback,
275
- auxToolResultFallback,
274
+ auxToolResultCard,
276
275
  renderCompletionOutboxMessage,
277
276
  renderMonitorResult,
278
277
  renderTeammateCompletionFallbackMessage,
@@ -280,13 +279,14 @@ import {
280
279
  renderTeammateStalledMessage,
281
280
  renderObserveCall,
282
281
  renderObserveResult,
283
- renderQuietTeammateAux,
282
+ teammateCallLine,
284
283
  renderTeammateCall,
285
284
  renderTeammateListCall,
286
285
  renderTeammateListResult,
287
286
  renderTeammateResult,
288
287
  renderTeammateSendCall,
289
288
  renderTeammateSendResult,
289
+ teammateStatusRow,
290
290
  } from "../tui/render.ts";
291
291
  import { AttachOverlay } from "../tui/attach-overlay.ts";
292
292
  import {
@@ -482,7 +482,7 @@ import {
482
482
  registerTeammateChildProxyCaller,
483
483
  resolvedRunLocation,
484
484
  } from "../runs/child-extensions.ts";
485
- import { setQuietMode } from "pi-maestro-settings-core/ui";
485
+ import { isQuietMode, setQuietMode } from "pi-maestro-settings-core/ui";
486
486
  import {
487
487
  aggregateAgentRunPhase,
488
488
  diagnoseAgentRuntime,
@@ -743,7 +743,7 @@ export default function registerTeammateExtension(
743
743
  const content = typeof message.content === "string"
744
744
  ? message.content.replace(/^●\s*/, "")
745
745
  : "agent spawned";
746
- return renderQuietTeammateAux("teammate-started", content, "success", theme as ExtensionContext["ui"]["theme"]);
746
+ return teammateStatusRow("teammate-started", content, "success", theme as ExtensionContext["ui"]["theme"]);
747
747
  },
748
748
  );
749
749
 
@@ -842,7 +842,8 @@ export default function registerTeammateExtension(
842
842
  let classifierRuntime: ExtensionContext["modelRegistry"] | undefined;
843
843
  pi.on("session_start", (_event, ctx) => {
844
844
  classifierRuntime = ctx.modelRegistry;
845
- bindClassifierRuntime({ hostVersion: PI_VERSION, runtime: classifierRuntime });
845
+ bindClassifierRuntime({ hostVersion: PI_VERSION, runtime: classifierRuntime,
846
+ sessionId: ctx.sessionManager.getSessionId(), cwd: ctx.cwd });
846
847
  });
847
848
  pi.on("session_shutdown", () => {
848
849
  if (classifierRuntime) unbindClassifierRuntime(classifierRuntime);
@@ -6928,15 +6929,15 @@ export default function registerTeammateExtension(
6928
6929
  renderCall(args, theme, context) {
6929
6930
  if (context.isPartial === false) return new Text("", 0, 0);
6930
6931
  const lines = typeof args.lines === "number" ? ` · ${args.lines} lines` : "";
6931
- return renderQuietTeammateAux("teammate-watch", `@${String(args.name ?? "?")}${lines}`, "running", theme)
6932
- ?? auxToolCallFallback("teammate-watch", theme);
6932
+ return teammateCallLine(theme, "teammate-watch", `@${String(args.name ?? "?")}${lines}`);
6933
6933
  },
6934
6934
 
6935
6935
  renderResult(result, options, theme, context) {
6936
6936
  if (options.isPartial) return new Text("", 0, 0);
6937
6937
  const failed = context.isError || (result as { isError?: boolean }).isError === true;
6938
- return renderQuietTeammateAux("teammate-watch", failed ? "inspection failed" : "inspected", failed ? "failure" : "success", theme)
6939
- ?? auxToolResultFallback(result, theme);
6938
+ return isQuietMode()
6939
+ ? teammateStatusRow("teammate-watch", failed ? "inspection failed" : "inspected", failed ? "failure" : "success", theme)
6940
+ : auxToolResultCard("teammate-watch", result, theme, options);
6940
6941
  },
6941
6942
  };
6942
6943
 
@@ -6983,16 +6984,16 @@ export default function registerTeammateExtension(
6983
6984
  renderCall(args, theme, context) {
6984
6985
  if (context.isPartial === false) return new Text("", 0, 0);
6985
6986
  const target = args.name ? `@${String(args.name)}` : `${String(args.waitMs ?? 0)}ms`;
6986
- return renderQuietTeammateAux("teammate-wait", target, "running", theme)
6987
- ?? auxToolCallFallback("teammate-wait", theme);
6987
+ return teammateCallLine(theme, "teammate-wait", target);
6988
6988
  },
6989
6989
 
6990
6990
  renderResult(result, options, theme, context) {
6991
6991
  if (options.isPartial) return new Text("", 0, 0);
6992
6992
  const status = result.details?.status ?? "timeout";
6993
6993
  const failed = context.isError || (result as { isError?: boolean }).isError === true;
6994
- return renderQuietTeammateAux("teammate-wait", status, failed ? "failure" : "success", theme)
6995
- ?? auxToolResultFallback(result, theme);
6994
+ return isQuietMode()
6995
+ ? teammateStatusRow("teammate-wait", status, failed ? "failure" : "success", theme)
6996
+ : auxToolResultCard("teammate-wait", result, theme, options);
6996
6997
  },
6997
6998
  };
6998
6999
 
@@ -9637,9 +9638,9 @@ Use list for an attention-first overview, get for one complete normalized window
9637
9638
  details: query,
9638
9639
  };
9639
9640
  },
9640
- renderCall(_args, theme, context) {
9641
+ renderCall(_args: Record<string, unknown>, theme, context) {
9641
9642
  if (context.isPartial === false) return new Text("", 0, 0);
9642
- return auxToolCallFallback("monitor", theme);
9643
+ return teammateCallLine(theme, "monitor", String(_args.action ?? "list"));
9643
9644
  },
9644
9645
  renderResult(result, options, theme, context) {
9645
9646
  return renderMonitorResult(result, options, theme, context.isError);
@@ -9720,15 +9721,15 @@ Use list for an attention-first overview, get for one complete normalized window
9720
9721
  if (context.isPartial === false) return new Text("", 0, 0);
9721
9722
  const action = String(args.action ?? "status");
9722
9723
  const targets = Array.isArray(args.targets) ? (args.targets as string[]).join(", ") : "";
9723
- return renderQuietTeammateAux("teammate-monitor", `${action} ${targets}`, "running", theme)
9724
- ?? auxToolCallFallback("teammate-monitor", theme);
9724
+ return teammateCallLine(theme, "teammate-monitor", `${action} ${targets}`);
9725
9725
  },
9726
9726
 
9727
9727
  renderResult(result, options, theme, context) {
9728
9728
  if (options.isPartial) return new Text("", 0, 0);
9729
9729
  const failed = context.isError || (result as { isError?: boolean }).isError === true;
9730
- return renderQuietTeammateAux("teammate-monitor", failed ? "failed" : "ok", failed ? "failure" : "success", theme)
9731
- ?? auxToolResultFallback(result, theme);
9730
+ return isQuietMode()
9731
+ ? teammateStatusRow("teammate-monitor", failed ? "failed" : "ok", failed ? "failure" : "success", theme)
9732
+ : auxToolResultCard("teammate-monitor", result, theme, options);
9732
9733
  },
9733
9734
  };
9734
9735
 
@@ -9950,13 +9951,13 @@ This lifecycle tool is available only after the user enters Monitor mode with /m
9950
9951
  created.handle,
9951
9952
  );
9952
9953
  },
9953
- renderCall(_args, theme, context) {
9954
+ renderCall(_args: Record<string, unknown>, theme, context) {
9954
9955
  if (context.isPartial === false) return new Text("", 0, 0);
9955
- return auxToolCallFallback("workspace-window", theme);
9956
+ return teammateCallLine(theme, "workspace-window", String(_args.action ?? ""));
9956
9957
  },
9957
9958
  renderResult(result, options, theme) {
9958
9959
  if (options.isPartial) return new Text("", 0, 0);
9959
- return auxToolResultFallback(result, theme);
9960
+ return auxToolResultCard("workspace-window", result, theme, options);
9960
9961
  },
9961
9962
  };
9962
9963
 
@@ -10057,13 +10058,13 @@ This Monitor-only lifecycle tool loads configured target ids without exposing SS
10057
10058
  return result(sanitizeRemoteMonitorError(error, "worker operation"), true);
10058
10059
  }
10059
10060
  },
10060
- renderCall(_args, theme, context) {
10061
+ renderCall(_args: Record<string, unknown>, theme, context) {
10061
10062
  if (context.isPartial === false) return new Text("", 0, 0);
10062
- return auxToolCallFallback("remote-worker", theme);
10063
+ return teammateCallLine(theme, "remote-worker", String(_args.action ?? ""));
10063
10064
  },
10064
10065
  renderResult(result, options, theme) {
10065
10066
  if (options.isPartial) return new Text("", 0, 0);
10066
- return auxToolResultFallback(result, theme);
10067
+ return auxToolResultCard("remote-worker", result, theme, options);
10067
10068
  },
10068
10069
  };
10069
10070
 
@@ -76,6 +76,8 @@ const MONITOR_MODE_CONTEXT = [
76
76
  "## Complete-project orchestration",
77
77
  "For a complete software project, begin with monitor list and relevant lifecycle list tools to inventory existing work and reuse suitable workers. Never dispatch an undivided project objective directly to implementation. If no decision-complete plan exists, create one read-only planning or technical-lead worker to inspect project knowledge and current code and return requirements, architecture, interfaces, risks, acceptance criteria, and a dependency-aware phase plan. Read its exact completion resource, then bring only genuine product or architecture decisions to the user.",
78
78
  "After decisions are locked, execute a phase DAG. Every worker objective must state purpose, owned scope, dependencies, locked interfaces, acceptance criteria, focused verification, and required result resources. Create only runnable workers, parallelize only independent scopes, and keep each shared interface or state machine under one owner until stable. Use explicit phase barriers for dependencies between different windows; never infer dependency completion from timing or message delivery.",
79
+ "Prioritize the smallest end-to-end path to the user-required outcome using existing authorities. Separate explicit locked constraints from agent-selected implementation means; justify every blocking edge with a requirement or confirmed material defect. Auxiliary diagnostics, provenance, or hardening do not automatically gate unrelated delivery. Make shared authorities available before consumers need production-path acceptance; reconcile an unnecessary gate with the root/user instead of bypassing it, and obtain renewed approval for scope, explicit locked-decision, or safety-boundary changes.",
80
+ "Track failed attempts cumulatively by shared interface/state machine and delivery blocker across workers, task names, approaches, and resets. After three unsuccessful attempts, stop the correction chain and report its effect on the requested outcome, evidence, and a simpler equivalent option or need for redesign/user guidance. Reuse an existing execution worker for bounded corrections; do not create a new worker merely to reset the budget. If direct execution is needed, ask the user to exit Monitor first; this control window remains supervision-only.",
79
81
  "At a phase barrier, establish lifecycle state with monitor or observe as appropriate, then read exact completion resources and correlated Flow reports before accepting work; settled windows alone do not prove project success. Reuse still-valid verification evidence, diagnose failed or ambiguous work before retrying, and dispatch integration, review, or verification only after its dependencies pass. The project is ready only when integrated behavior and acceptance criteria are verified. Release or deployment is not implied by implementation: it requires explicit user authorization and a dedicated worker.",
80
82
  "## Tool routing",
81
83
  "Use workspace-window only for local Pi workers. Create only a required worker; native interactive is the default. provider=herdr requires an already-running local Herdr session, supports interactive presentation only, and never starts or stops the Herdr server. Create already delivers the objective, so do not resend it. Retain the returned exact owner target and completion handle, read settled results through its immutable agent:// resource, and close only windows created by this Monitor; never close discovered external peers.",
@@ -109,9 +109,6 @@ import type {
109
109
  NormalizedTask,
110
110
  } from "../runs/execution.ts";
111
111
  import {
112
- auxToolCallFallback,
113
- auxToolResultFallback,
114
- renderQuietTeammateAux,
115
112
  renderTeammateCall,
116
113
  renderTeammateListCall,
117
114
  renderTeammateListResult,