gentle-pi 3.5.0 → 3.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.
@@ -9,7 +9,11 @@
9
9
  // the caller's "provider/id" selection through pi's live model registry,
10
10
  // authenticates through the registry's own resolver, and completes exactly
11
11
  // one frozen prompt as a single user message — no systemPrompt, no tools, no
12
- // session, no extension hooks.
12
+ // session, and none of pi's tool/skill/prompt extension hooks. Extension
13
+ // *providers* are the opposite: they are explicitly in scope. Reading the
14
+ // earlier "no extension hooks" wording as "no extension providers" is what
15
+ // produced gentle-shell#1304, where every extension-registered model was
16
+ // unreachable, so the distinction is stated here rather than implied.
13
17
  //
14
18
  // Every I/O seam is injected (`registry`, `complete`, `now`), so this module
15
19
  // runs under tests with no network and no pi process. gentle-pi#311 P2 wires
@@ -22,18 +26,32 @@ import { SAFE_MODEL_ID_PATTERN } from "./model-routing-authority.ts";
22
26
 
23
27
  // ---------------------------------------------------------------------------
24
28
  // Registry seam — a structural subset of pi's live ModelRegistry
25
- // (@earendil-works/pi-coding-agent core/model-registry.ts). Only `find` and
26
- // `getApiKeyAndHeaders` are needed here; the real registry's resolved auth
27
- // carries extra optional fields (`baseUrl`, `env`) that this narrower shape
28
- // simply ignores.
29
+ // (@earendil-works/pi-coding-agent core/model-registry.ts). `find` and
30
+ // `getApiKeyAndHeaders` are required. `getProvider` is optional and selects
31
+ // the dispatch path: when present, the completion goes through the composed
32
+ // provider it returns (which is what reaches extension-registered providers);
33
+ // when absent, it falls back to `deps.complete`, so a test double with no
34
+ // composition layer still satisfies the seam. The real registry's resolved
35
+ // auth carries extra optional fields (`baseUrl`, `env`) that this narrower
36
+ // shape simply ignores.
29
37
  // ---------------------------------------------------------------------------
30
38
 
39
+ /**
40
+ * The single method this module needs from a resolved provider. pi's
41
+ * `Provider.streamSimple` returns an `AssistantMessageEventStream`; only its
42
+ * `result()` is consumed here, so the seam asks for nothing more.
43
+ */
44
+ export interface InProcessReviewerProvider {
45
+ streamSimple(model: Model<Api>, context: Context, options?: SimpleStreamOptions): { result(): Promise<AssistantMessage> };
46
+ }
47
+
31
48
  export interface InProcessReviewerRegistry {
32
49
  find(provider: string, modelId: string): Model<Api> | undefined;
33
50
  getApiKeyAndHeaders(model: Model<Api>): Promise<
34
51
  | { readonly ok: true; readonly apiKey?: string; readonly headers?: ProviderHeaders }
35
52
  | { readonly ok: false; readonly error: string }
36
53
  >;
54
+ getProvider?(provider: string): InProcessReviewerProvider | undefined;
37
55
  }
38
56
 
39
57
  export const INPROCESS_REVIEWER_FAILURE = {
@@ -180,6 +198,33 @@ export async function runInProcessReviewer(request: InProcessReviewerRequest, de
180
198
  );
181
199
  }
182
200
 
201
+ // pi's composed provider is the only layer that honors extension-registered
202
+ // providers (core/provider-composer.ts: `extension.streamSimple` when
203
+ // `model.api === extension.api`). pi-ai's `completeSimple` resolves against
204
+ // its own builtin-only registry and throws for any extension api, so a seam
205
+ // that carries `getProvider` is always preferred; a seam without it at all
206
+ // (a test double with no composition layer) still uses `deps.complete`.
207
+ // A registry that has `getProvider` yet owns no provider for a model its
208
+ // own `find` just resolved is incoherent: refuse, never route past it.
209
+ //
210
+ // The lookup deliberately uses `parsed.provider` — the key `find` was
211
+ // called with — and not `model.provider`, which is a field of the object
212
+ // `find` returned. In pi's registry both reads hit one provider map
213
+ // (core/model-runtime.ts `getProvider`/`getModel` -> pi-ai models.ts, where
214
+ // `getModels(provider)` returns [] for an unknown id), so a resolved model
215
+ // always has a provider under its own key and this branch is unreachable by
216
+ // construction. Keying on the returned field instead would make that
217
+ // guarantee depend on every provider's `getModels()` echoing its own id,
218
+ // which a native or OAuth-modified extension provider is free not to do.
219
+ const provider = deps.registry.getProvider?.(parsed.provider);
220
+ if (deps.registry.getProvider !== undefined && provider === undefined) {
221
+ return refuse(
222
+ INPROCESS_REVIEWER_FAILURE.MODEL_NOT_FOUND,
223
+ `The model registry resolved ${JSON.stringify(request.selection)} for ${request.routingKey} but owns no provider for ${JSON.stringify(parsed.provider)}; reassign ${request.routingKey} to a model whose provider the interactive pi can actually dispatch.`,
224
+ { provider: parsed.provider, api: model.api },
225
+ );
226
+ }
227
+
183
228
  const auth = await deps.registry.getApiKeyAndHeaders(model);
184
229
  // Negation narrowing (`!auth.ok`) does not eliminate the `ok: true` arm of
185
230
  // this discriminated union under this project's `strict: false` tsconfig;
@@ -245,9 +290,12 @@ export async function runInProcessReviewer(request: InProcessReviewerRequest, de
245
290
  return undefined;
246
291
  };
247
292
 
293
+ // `SimpleStreamOptions` is identical on both paths; only the return shape
294
+ // differs (an event stream versus a promise), hence `.result()` — which is
295
+ // exactly what pi-ai's own compat layer does with the same stream.
248
296
  let assistant: AssistantMessage;
249
297
  try {
250
- assistant = await deps.complete(model, context, options);
298
+ assistant = provider === undefined ? await deps.complete(model, context, options) : await provider.streamSimple(model, context, options).result();
251
299
  } catch (error) {
252
300
  return abortRefusal() ?? refuse(INPROCESS_REVIEWER_FAILURE.PROVIDER_FAILED, `Reviewer completion failed for ${request.routingKey}: ${sanitizeErrorExcerpt(error)}`);
253
301
  }
@@ -1032,6 +1032,18 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
1032
1032
  // closed START/STATUS fields this row negotiates. riskEvidence and hint
1033
1033
  // remain dark; neither is proven to reach Pi's negotiated START path.
1034
1034
  "3.5.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
1035
+ // v3.6.0 repeats 3.5.0: the published provider contract bundle is
1036
+ // byte-identical at 1.2.0, both binaries advertise capabilities/v2.6
1037
+ // with only build-identity differences, and no review-integration schema
1038
+ // changed between the v3.5.0 and v3.6.0 tags. riskEvidence and hint
1039
+ // remain dark; neither is proven to reach Pi's negotiated START path.
1040
+ "3.6.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
1041
+ // v3.6.1 repeats 3.6.0: published provider-contract archives are byte-identical
1042
+ // (SHA-256 547b68e172cc87aa297309d61624e5fc2c24d407a494b53eeb5a2b053904352c).
1043
+ // The published v3.6.1 binary advertises capabilities/v2.6, and the tag diff
1044
+ // changes no review-integration schema or capability source. riskEvidence and
1045
+ // hint remain dark because neither is proven in Pi's negotiated START path.
1046
+ "3.6.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
1035
1047
  });
1036
1048
 
1037
1049
  export interface NativeReviewProcessDiagnostics {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gentle-pi",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
5
5
  "license": "MIT",
6
6
  "type": "module",