@mgiles/perk 2.2.0 → 3.0.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 (129) hide show
  1. package/README.md +1 -1
  2. package/extension/adapters/planAdapterPlannotator.ts +132 -91
  3. package/extension/adapters/planAdapterTombell.ts +5 -3
  4. package/extension/doors/address.ts +235 -37
  5. package/extension/doors/annotationPush.ts +924 -0
  6. package/extension/doors/auditWaveTools.ts +352 -0
  7. package/extension/doors/ciExecutor.ts +220 -55
  8. package/extension/doors/draftReviewWaveTools.ts +384 -0
  9. package/extension/doors/harvestWaveTools.ts +279 -0
  10. package/extension/doors/land.ts +1 -0
  11. package/extension/doors/learn.ts +238 -24
  12. package/extension/doors/learnFactory.ts +33 -7
  13. package/extension/doors/lifecycleGates.ts +1 -1
  14. package/extension/doors/objectiveReviewBrowser.ts +514 -0
  15. package/extension/doors/objectiveStack.ts +1143 -0
  16. package/extension/doors/planReviewBrowser.ts +453 -0
  17. package/extension/doors/plannotatorHandoff.ts +120 -49
  18. package/extension/doors/prReview.ts +203 -22
  19. package/extension/doors/prReviewBrowser.ts +31 -23
  20. package/extension/doors/prReviewDynamic.ts +256 -0
  21. package/extension/doors/prReviewTerminal.ts +6 -11
  22. package/extension/doors/reviewWaveTools.ts +374 -0
  23. package/extension/doors/selfcheck.ts +1 -1
  24. package/extension/doors/submit.ts +86 -10
  25. package/extension/factories/gistAuthor.ts +4 -4
  26. package/extension/factories/gistDraft.ts +1 -1
  27. package/extension/factories/gistSave.ts +2 -2
  28. package/extension/factories/objective.ts +7 -7
  29. package/extension/factories/objectiveAuthor.ts +5 -4
  30. package/extension/factories/objectiveDraft.ts +61 -6
  31. package/extension/factories/objectivePlan.ts +163 -8
  32. package/extension/factories/objectiveSave.ts +16 -1
  33. package/extension/factories/planMode.ts +5 -3
  34. package/extension/factories/planReview.ts +131 -36
  35. package/extension/factories/planTitle.ts +27 -9
  36. package/extension/hunkFeedback/inbox.ts +389 -0
  37. package/extension/hunkFeedback/perkFeedback.ts +364 -0
  38. package/extension/hunkFeedback/receiver.ts +296 -0
  39. package/extension/hunkFeedback/store.ts +533 -0
  40. package/extension/index.ts +116 -44
  41. package/extension/substrate/cache.ts +75 -22
  42. package/extension/substrate/coldDoor.ts +2 -3
  43. package/extension/substrate/config.ts +42 -14
  44. package/extension/substrate/git.ts +10 -0
  45. package/extension/substrate/providers.ts +4 -33
  46. package/extension/substrate/resources.ts +11 -0
  47. package/extension/substrate/result.ts +13 -5
  48. package/extension/substrate/sessionData.ts +3 -3
  49. package/extension/substrate/sessionPointers.ts +3 -3
  50. package/extension/substrate/structuredOutput.ts +78 -19
  51. package/extension/substrate/terminalLaunch.ts +1 -1
  52. package/extension/substrate/toolGating.ts +206 -26
  53. package/extension/substrate/workflowState.ts +6 -0
  54. package/extension/surfaces/footerProvider.ts +6 -6
  55. package/extension/surfaces/surfaces.ts +98 -282
  56. package/extension/vendor/btw/btw.ts +57 -12
  57. package/extension/waves/adversarialReviewWave.ts +164 -0
  58. package/extension/waves/auditWave.ts +312 -0
  59. package/extension/waves/draftReviewWave.ts +189 -0
  60. package/extension/waves/harvestWave.ts +399 -0
  61. package/extension/waves/learnWave.ts +155 -0
  62. package/extension/waves/memoryAdapter.ts +139 -0
  63. package/extension/waves/objectiveExplorerWave.ts +120 -0
  64. package/extension/waves/prReviewDynamicWave.ts +696 -0
  65. package/extension/waves/prReviewWave.ts +274 -0
  66. package/extension/waves/reportWave.ts +749 -0
  67. package/extension/waves/reviewClassifierWave.ts +109 -0
  68. package/extension/waves/rpcAdapter.ts +264 -0
  69. package/extension/worker/readOnlySession.ts +3 -3
  70. package/extension/worker/worker.ts +71 -93
  71. package/extension/workerMain.ts +4 -6
  72. package/package.json +20 -5
  73. package/prompts/README.md +19 -0
  74. package/prompts/_fixtures/live.yaml +71 -72
  75. package/prompts/common/resume-advisory.md +1 -0
  76. package/prompts/contexts/adapters/plannotator-gist.md +13 -0
  77. package/prompts/contexts/adapters/plannotator-objective.md +1 -2
  78. package/prompts/contexts/adapters/plannotator-plan.md +1 -2
  79. package/prompts/contexts/gist-authoring.md +6 -20
  80. package/prompts/contexts/objective-authoring.md +6 -18
  81. package/prompts/stages/address/action.md +4 -4
  82. package/prompts/stages/address/preview.md +3 -3
  83. package/prompts/stages/audit.md +18 -0
  84. package/prompts/stages/conflict-resolution.md +1 -1
  85. package/prompts/stages/gist-author/seed.md +4 -4
  86. package/prompts/stages/gist-save.md +1 -1
  87. package/prompts/stages/implement.md +3 -1
  88. package/prompts/stages/learn-code.md +1 -1
  89. package/prompts/stages/learn-docs.md +2 -2
  90. package/prompts/stages/learn-harvest.md +15 -0
  91. package/prompts/stages/learn-orchestrate.md +7 -5
  92. package/prompts/stages/objective-author/adopt.md +5 -3
  93. package/prompts/stages/objective-author/file.md +5 -4
  94. package/prompts/stages/objective-author/seed.md +5 -4
  95. package/prompts/stages/objective-land.md +7 -0
  96. package/prompts/stages/objective-plan/guidance.md +2 -2
  97. package/prompts/stages/objective-plan/seed.md +5 -1
  98. package/prompts/stages/objective-recover.md +8 -0
  99. package/prompts/stages/objective-replan.md +4 -2
  100. package/prompts/stages/objective-review-browser.md +9 -0
  101. package/prompts/stages/objective-sync.md +6 -0
  102. package/prompts/stages/plan-from/adopt.md +2 -2
  103. package/prompts/stages/plan-from/file.md +2 -2
  104. package/prompts/stages/plan-review-browser.md +9 -0
  105. package/prompts/stages/pr-review-browser/active.md +6 -6
  106. package/prompts/stages/pr-review-browser/foreign.md +6 -6
  107. package/prompts/stages/pr-review-dynamic.md +7 -0
  108. package/prompts/stages/pr-review-terminal/active.md +7 -7
  109. package/prompts/stages/pr-review-terminal/foreign.md +7 -7
  110. package/prompts/stages/pr-review.md +7 -6
  111. package/prompts/stages/replan.md +3 -3
  112. package/shared/README.md +2 -2
  113. package/shared/bindings.yaml +15 -0
  114. package/shared/contracts-history.md +11 -0
  115. package/shared/contracts.md +3713 -227
  116. package/shared/providers.yaml +27 -60
  117. package/shared/registry.yaml +32 -8
  118. package/shared/schemas/outputs/objective-doctor.schema.json +449 -0
  119. package/shared/schemas/outputs/objective-stack-land.schema.json +732 -0
  120. package/shared/schemas/outputs/objective-stack-recover.schema.json +397 -0
  121. package/shared/schemas/outputs/objective-stack-status.schema.json +577 -0
  122. package/shared/schemas/outputs/objective-stack-sync.schema.json +184 -0
  123. package/shared/schemas/outputs/plan-save.schema.json +12 -0
  124. package/shared/schemas/outputs/pr-submit.schema.json +177 -1
  125. package/extension/adapters/todoAdapterJuicesharp.ts +0 -102
  126. package/extension/checkpoints/checkpoints.ts +0 -550
  127. package/extension/checkpoints/planSteps.ts +0 -108
  128. package/extension/doors/askUser.ts +0 -198
  129. package/prompts/contexts/adapters/juicesharp-todo.md +0 -7
@@ -2,13 +2,12 @@
2
2
  //
3
3
  // Twin of perk/substrate/providers.py: both planes parse the SAME bundled file (no codegen). This is the
4
4
  // THIRD parsed cross-plane contract (after registry.yaml and bindings.yaml). It is the SUPPORTED
5
- // SET — the catalog of plan/todo/askuser/footer/web providers perk knows how to wire —
5
+ // SET — the catalog of plan/footer/web providers perk knows how to wire —
6
6
  // distinct from the per-repo SELECTION (the flat `[providers]` table in .perk/config.toml).
7
7
  //
8
8
  // The Python CLI is the authoritative validator (perk/substrate/providers.py); this side does a thin
9
9
  // structural parse only — no deep content validation here. `resolveProviders` is CONSUMED by
10
- // `planMode` (plan-seam registration-time vacating when a foreign plan provider is selected); the
11
- // todo-seam runtime deferral is likewise live.
10
+ // `planMode` (plan-seam registration-time vacating when a foreign plan provider is selected).
12
11
 
13
12
  import { readFileSync } from "node:fs";
14
13
  import { join } from "node:path";
@@ -24,12 +23,10 @@ export interface Provider {
24
23
  packageFilter?: Record<string, unknown>;
25
24
  }
26
25
 
27
- export const PROVIDER_SEAMS = ["plan", "todo", "askuser", "footer", "web"] as const;
26
+ export const PROVIDER_SEAMS = ["plan", "footer", "web"] as const;
28
27
 
29
28
  /** The bundled reference provider ids (the behavior-preserving no-config defaults per seam). */
30
29
  export const PERK_PLAN_PROVIDER_ID = "perk-plan";
31
- export const PERK_CHECKPOINTS_PROVIDER_ID = "perk-checkpoints";
32
- export const PERK_ASK_USER_PROVIDER_ID = "perk-ask-user";
33
30
  export const PERK_FOOTER_PROVIDER_ID = "perk-footer";
34
31
 
35
32
  /** The foreign `@tombell/pi-plan` plan-provider id. */
@@ -38,12 +35,6 @@ export const TOMBELL_PLAN_PROVIDER_ID = "tombell-plan";
38
35
  /** The foreign `@plannotator/pi-extension` plan-provider id (augment-posture adapter selection check). */
39
36
  export const PLANNOTATOR_PLAN_PROVIDER_ID = "plannotator-plan";
40
37
 
41
- /** The foreign `@juicesharp/rpiv-todo` todo-provider id. */
42
- export const JUICESHARP_TODO_PROVIDER_ID = "juicesharp-todo";
43
-
44
- /** The foreign `@juicesharp/rpiv-ask-user-question` askuser-provider id (vacate-only interface seam). */
45
- export const JUICESHARP_ASK_USER_PROVIDER_ID = "juicesharp-ask-user";
46
-
47
38
  /** The foreign `pi-powerline-footer` footer-provider id (vacate-only interface seam). */
48
39
  export const POWERLINE_FOOTER_PROVIDER_ID = "powerline-footer";
49
40
 
@@ -106,8 +97,6 @@ export function loadProviders(): Provider[] {
106
97
  */
107
98
  export interface ResolvedProviders {
108
99
  plan: Provider;
109
- todo: Provider;
110
- askuser: Provider;
111
100
  footer: Provider;
112
101
  web: Provider;
113
102
  issues: string[];
@@ -124,20 +113,6 @@ export interface ResolvedProviders {
124
113
  */
125
114
  const REFERENCE_FALLBACKS: Record<(typeof PROVIDER_SEAMS)[number], Provider> = {
126
115
  plan: { id: PERK_PLAN_PROVIDER_ID, seam: "plan", package: null, adapter: null, default: true },
127
- todo: {
128
- id: PERK_CHECKPOINTS_PROVIDER_ID,
129
- seam: "todo",
130
- package: null,
131
- adapter: null,
132
- default: true,
133
- },
134
- askuser: {
135
- id: PERK_ASK_USER_PROVIDER_ID,
136
- seam: "askuser",
137
- package: null,
138
- adapter: null,
139
- default: true,
140
- },
141
116
  footer: {
142
117
  id: PERK_FOOTER_PROVIDER_ID,
143
118
  seam: "footer",
@@ -183,8 +158,6 @@ function byId(set: Provider[]): Map<string, Provider> {
183
158
  export function resolveProviders(
184
159
  selection: {
185
160
  plan?: string;
186
- todo?: string;
187
- askuser?: string;
188
161
  footer?: string;
189
162
  web?: string;
190
163
  },
@@ -205,7 +178,7 @@ export function resolveProviders(
205
178
  return def;
206
179
  };
207
180
 
208
- const resolveSeam = (seam: "plan" | "todo" | "askuser" | "footer" | "web"): Provider => {
181
+ const resolveSeam = (seam: "plan" | "footer" | "web"): Provider => {
209
182
  const selected = selection[seam];
210
183
  if (selected == null) return requireDefault(seam);
211
184
  const provider = ids.get(selected);
@@ -222,8 +195,6 @@ export function resolveProviders(
222
195
 
223
196
  return {
224
197
  plan: resolveSeam("plan"),
225
- todo: resolveSeam("todo"),
226
- askuser: resolveSeam("askuser"),
227
198
  footer: resolveSeam("footer"),
228
199
  web: resolveSeam("web"),
229
200
  issues,
@@ -39,3 +39,14 @@ export function perkVersion(): string {
39
39
  return "0.0.0";
40
40
  }
41
41
  }
42
+
43
+ /**
44
+ * The vintage-stamp filter over a perkVersion() value: the version, or undefined for the
45
+ * "0.0.0" failure sentinel. A `perk_version` stamp claims an EXACT session vintage
46
+ * (contracts §8.3 — the session-audit read side treats it as the exact basis), so the
47
+ * self-confessed-unknown sentinel must not masquerade as one; omitting the stamp leaves the
48
+ * session on the honest timestamp-estimate arm.
49
+ */
50
+ export function versionStamp(version: string): string | undefined {
51
+ return version === "0.0.0" ? undefined : version;
52
+ }
@@ -55,18 +55,26 @@ export function ok<D extends object>(
55
55
  * `failFor(ctx, scope, label)` when the content label differs from the report scope). Each call
56
56
  * reports loudly (`report(ctx, scope, "error", message, { alsoLog: true })`) and returns the
57
57
  * canonical soft failure: content `"<label> failed: <message>"`, details
58
- * `{ ok: false, error: message, error_type: errorType }`, no `terminate`.
58
+ * `{ ok: false, error: message, error_type: errorType }`, no `terminate`. `X` is the
59
+ * `FailDetails` extras hook: `failFor<X>(…)` lets a call attach module-specific fail details
60
+ * (spread AFTER `error`/`error_type`) — e.g. the learn wave's attempt receipts. Extras stay
61
+ * optional, so existing extras-free call sites are unchanged.
59
62
  */
60
- export function failFor(
63
+ export function failFor<X extends object = Record<never, never>>(
61
64
  target: ReportTarget,
62
65
  scope: string,
63
66
  label: string = scope,
64
- ): (message: string, errorType: string) => FailResult {
65
- return (message, errorType) => {
67
+ ): (message: string, errorType: string, extras?: X) => FailResult<X> {
68
+ return (message, errorType, extras) => {
66
69
  report(target, scope, "error", message, { alsoLog: true });
67
70
  return {
68
71
  content: [{ type: "text", text: `${label} failed: ${message}` }],
69
- details: { ok: false, error: message, error_type: errorType },
72
+ details: {
73
+ ok: false,
74
+ error: message,
75
+ error_type: errorType,
76
+ ...(extras ?? {}),
77
+ } as FailDetails<X>,
70
78
  };
71
79
  };
72
80
  }
@@ -33,10 +33,10 @@
33
33
  // loadable under `node --test`; accepts a minimal structural ctx (`BranchSource & { cwd }`).
34
34
 
35
35
  import { createHash } from "node:crypto";
36
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
36
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
37
37
  import { join, relative } from "node:path";
38
38
  import type { ReportTarget } from "../surfaces/report.ts";
39
- import { sessionDataDir } from "./cache.ts";
39
+ import { atomicWriteFileSync, sessionDataDir } from "./cache.ts";
40
40
  import {
41
41
  appendWorkflowState,
42
42
  type BranchSource,
@@ -115,7 +115,7 @@ export function writeSessionData(
115
115
  if (dir === null) return null;
116
116
  const path = join(dir, name);
117
117
  try {
118
- writeFileSync(path, content, "utf8");
118
+ atomicWriteFileSync(path, content);
119
119
  } catch (error) {
120
120
  console.error(`perk: warning: could not write session data ${path}: ${error}`);
121
121
  return null;
@@ -13,9 +13,9 @@
13
13
  // wedge the save/launch/drive it rides on. Node builtins + cache.ts + git.ts only (loads under
14
14
  // `node --test`).
15
15
 
16
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
17
17
  import { basename, join } from "node:path";
18
- import { runScratchDir } from "./cache.ts";
18
+ import { atomicWriteFileSync, runScratchDir } from "./cache.ts";
19
19
  import { mainCheckoutRoot } from "./git.ts";
20
20
 
21
21
  export const SESSION_POINTERS_FILE = "session-pointers.json";
@@ -121,7 +121,7 @@ export function recordSessionPointer(
121
121
  record[klass][site] = pointer;
122
122
  const dir = runScratchDir(root, runId);
123
123
  mkdirSync(dir, { recursive: true });
124
- writeFileSync(sessionPointersPath(root, runId), serialize(record), "utf8");
124
+ atomicWriteFileSync(sessionPointersPath(root, runId), serialize(record));
125
125
  return true;
126
126
  } catch (error) {
127
127
  console.error(`perk: warning: could not record session pointer (${klass}.${site}): ${error}`);
@@ -2,16 +2,19 @@
2
2
  //
3
3
  // pi-ai has no dedicated JSON-mode; structured output is done via tool calling. This module wraps
4
4
  // that idiom into two pure, dependency-light, NEVER-throwing helpers:
5
- // - `resolveModelAuth(ctx)` reuses the session's configured + authenticated model (the sanctioned
6
- // `ModelRegistry.getApiKeyAndHeaders` path), and
7
- // - `completeStructured(opts)` builds a single-tool `Context`, calls `complete`, and validates the
8
- // returned tool-call arguments against a TypeBox schema.
5
+ // - `resolveModelAuth(ctx)` reuses the session's configured + authenticated model (the compat
6
+ // `ModelRegistry.getApiKeyAndHeaders` path — the fallback for hosts without registry
7
+ // dispatch), and
8
+ // - `completeStructured(opts)` builds a single-tool `Context`, completes it (via the injected
9
+ // registry `dispatch` when the host provides one, else the compat `complete`), and validates
10
+ // the returned tool-call arguments against a TypeBox schema.
9
11
  // Both report failure via a soft `{ ok:false, error }` outcome — no throws ever reach the caller, so
10
12
  // every consumer can stay fail-safe with a deterministic fallback. The first consumer is
11
13
  // `extension/factories/planTitle.ts` (LLM-generated plan-issue titles).
12
14
 
13
15
  import {
14
16
  type Api,
17
+ type AssistantMessage,
15
18
  type Context,
16
19
  type Model,
17
20
  type Static,
@@ -24,21 +27,54 @@ import {
24
27
  // keeps the types. Pi's extension loader aliases both the root and /compat to the compat entry.
25
28
  import { complete } from "@earendil-works/pi-ai/compat";
26
29
 
27
- /** Structurally-minimal slice of `ExtensionContext` needed to reuse the session's model + auth. */
30
+ /**
31
+ * Registry dispatch (mirrors pi ≥ 0.84's `ModelRegistry.complete`): pi owns final request
32
+ * assembly — resolved auth, nullable headers, credential-resolved `baseUrl`, provider `env` —
33
+ * end to end. Feature-detect it (`typeof … === "function"`); absent on older hosts.
34
+ */
35
+ export type ModelDispatch = (
36
+ model: Model<Api>,
37
+ context: Context,
38
+ options: { signal?: AbortSignal; timeoutMs?: number },
39
+ ) => Promise<AssistantMessage>;
40
+
41
+ /**
42
+ * Structurally-minimal slice of `ExtensionContext` needed to reuse the session's model + auth.
43
+ * Header values mirror pi-ai's `ProviderHeaders` (`string | null` — null is a header-deletion
44
+ * marker); `baseUrl`/`env` mirror pi 0.84's `ResolvedRequestAuth`.
45
+ */
28
46
  export interface ModelAuthContext {
29
47
  model: Model<Api> | undefined;
30
48
  modelRegistry: {
31
- getApiKeyAndHeaders(
32
- model: Model<Api>,
33
- ): Promise<
34
- { ok: true; apiKey?: string; headers?: Record<string, string> } | { ok: false; error: string }
49
+ getApiKeyAndHeaders(model: Model<Api>): Promise<
50
+ | {
51
+ ok: true;
52
+ apiKey?: string;
53
+ headers?: Record<string, string | null>;
54
+ baseUrl?: string;
55
+ env?: Record<string, string>;
56
+ }
57
+ | { ok: false; error: string }
35
58
  >;
59
+ /** pi ≥ 0.84 registry dispatch; absent on older hosts (feature-detected, never assumed). */
60
+ complete?(
61
+ model: Model<Api>,
62
+ context: Context,
63
+ options?: { signal?: AbortSignal; timeoutMs?: number },
64
+ ): Promise<AssistantMessage>;
36
65
  };
37
66
  }
38
67
 
39
68
  /** Resolved model + auth, or a soft failure (no model / unresolved auth). */
40
69
  export type ResolvedModelAuth =
41
- | { ok: true; model: Model<Api>; apiKey?: string; headers?: Record<string, string> }
70
+ | {
71
+ ok: true;
72
+ model: Model<Api>;
73
+ apiKey?: string;
74
+ headers?: Record<string, string | null>;
75
+ baseUrl?: string;
76
+ env?: Record<string, string>;
77
+ }
42
78
  | { ok: false; error: string };
43
79
 
44
80
  /** The generic structured-output outcome — soft success/failure, never a throw. */
@@ -60,7 +96,14 @@ export async function resolveModelAuth(ctx: ModelAuthContext): Promise<ResolvedM
60
96
  try {
61
97
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
62
98
  if (!auth.ok) return { ok: false, error: auth.error };
63
- return { ok: true, model, apiKey: auth.apiKey, headers: auth.headers };
99
+ return {
100
+ ok: true,
101
+ model,
102
+ apiKey: auth.apiKey,
103
+ headers: auth.headers,
104
+ baseUrl: auth.baseUrl,
105
+ env: auth.env,
106
+ };
64
107
  } catch (err) {
65
108
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
66
109
  }
@@ -79,8 +122,17 @@ export interface CompleteStructuredOptions<S extends TSchema> {
79
122
  instruction: string;
80
123
  /** The payload (e.g. the document to summarize/classify). */
81
124
  input: string;
125
+ /**
126
+ * Registry dispatch (pi ≥ 0.84). When present it REPLACES the compat `complete` +
127
+ * apiKey/headers assembly below — pi owns auth end to end, including the credential-resolved
128
+ * `baseUrl` the fallback path cannot carry.
129
+ */
130
+ dispatch?: ModelDispatch;
82
131
  apiKey?: string;
83
- headers?: Record<string, string>;
132
+ /** `string | null` mirrors pi-ai's `ProviderHeaders` — a null value deletes a default header. */
133
+ headers?: Record<string, string | null>;
134
+ /** Provider-scoped environment values (pi 0.84 `ResolvedRequestAuth.env`), fallback path only. */
135
+ env?: Record<string, string>;
84
136
  signal?: AbortSignal;
85
137
  timeoutMs?: number;
86
138
  }
@@ -114,14 +166,21 @@ export async function completeStructured<S extends TSchema>(
114
166
  tools: [tool],
115
167
  };
116
168
 
117
- let msg: Awaited<ReturnType<typeof complete>>;
169
+ // Primary: registry dispatch (pi ≥ 0.84 owns auth/headers/baseUrl/env). Fallback: the compat
170
+ // `complete` with caller-resolved auth — it forwards apiKey/headers/env but has NO `baseUrl`
171
+ // option, so a credential-resolved endpoint is silently dropped on old hosts (the fallback's
172
+ // named, pre-existing limitation; the dispatch path is the fix).
173
+ let msg: AssistantMessage;
118
174
  try {
119
- msg = await complete(opts.model, context, {
120
- apiKey: opts.apiKey,
121
- headers: opts.headers,
122
- signal: opts.signal,
123
- timeoutMs: opts.timeoutMs,
124
- });
175
+ msg = opts.dispatch
176
+ ? await opts.dispatch(opts.model, context, { signal: opts.signal, timeoutMs: opts.timeoutMs })
177
+ : await complete(opts.model, context, {
178
+ apiKey: opts.apiKey,
179
+ headers: opts.headers,
180
+ env: opts.env,
181
+ signal: opts.signal,
182
+ timeoutMs: opts.timeoutMs,
183
+ });
125
184
  } catch (err) {
126
185
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
127
186
  }
@@ -51,7 +51,7 @@ function interactiveShellWrap(
51
51
  command: string,
52
52
  ): string {
53
53
  const shell =
54
- env.SHELL !== undefined && env.SHELL.startsWith("/")
54
+ env.SHELL?.startsWith("/") === true
55
55
  ? env.SHELL
56
56
  : platform === "darwin"
57
57
  ? "/bin/zsh"