pi-daddy 0.43.1 → 0.44.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 (221) hide show
  1. package/CHANGELOG.md +93 -31
  2. package/README.md +15 -0
  3. package/THIRD_PARTY_LICENSES/musl-COPYRIGHT.txt +193 -0
  4. package/THIRD_PARTY_LICENSES/zig-MIT.txt +21 -0
  5. package/THIRD_PARTY_NOTICES.md +9 -0
  6. package/dist/cli.d.ts +2 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +10 -5
  9. package/dist/cli.js.map +1 -1
  10. package/dist/executors/activity-session.js +116 -65
  11. package/dist/executors/activity-session.js.map +1 -1
  12. package/dist/executors/captured-execution.d.ts +13 -0
  13. package/dist/executors/captured-execution.d.ts.map +1 -0
  14. package/dist/executors/captured-execution.js +53 -0
  15. package/dist/executors/captured-execution.js.map +1 -0
  16. package/dist/executors/child-final.d.ts +31 -0
  17. package/dist/executors/child-final.d.ts.map +1 -0
  18. package/dist/executors/child-final.js +197 -0
  19. package/dist/executors/child-final.js.map +1 -0
  20. package/dist/executors/executor.d.ts +2 -1
  21. package/dist/executors/executor.d.ts.map +1 -1
  22. package/dist/executors/executor.js +12 -0
  23. package/dist/executors/executor.js.map +1 -1
  24. package/dist/executors/owned-worker.d.ts +18 -0
  25. package/dist/executors/owned-worker.d.ts.map +1 -0
  26. package/dist/executors/owned-worker.js +295 -0
  27. package/dist/executors/owned-worker.js.map +1 -0
  28. package/dist/executors/worker-artifact.d.ts +3 -0
  29. package/dist/executors/worker-artifact.d.ts.map +1 -0
  30. package/dist/executors/worker-artifact.js +3 -0
  31. package/dist/executors/worker-artifact.js.map +1 -0
  32. package/dist/governance/approval-prompt.d.ts +4 -3
  33. package/dist/governance/approval-prompt.d.ts.map +1 -1
  34. package/dist/governance/approval-prompt.js +13 -6
  35. package/dist/governance/approval-prompt.js.map +1 -1
  36. package/dist/governance/captured-worker-record.d.ts +9 -0
  37. package/dist/governance/captured-worker-record.d.ts.map +1 -0
  38. package/dist/governance/captured-worker-record.js +123 -0
  39. package/dist/governance/captured-worker-record.js.map +1 -0
  40. package/dist/governance/lease-record.d.ts +4 -0
  41. package/dist/governance/lease-record.d.ts.map +1 -1
  42. package/dist/governance/lease-record.js +19 -2
  43. package/dist/governance/lease-record.js.map +1 -1
  44. package/dist/governance/ledger-events.d.ts +5 -5
  45. package/dist/governance/ledger-events.d.ts.map +1 -1
  46. package/dist/governance/ledger-v3-validation.js +1 -1
  47. package/dist/governance/ledger-v3-validation.js.map +1 -1
  48. package/dist/governance/workspace-acceptance.d.ts +3 -1
  49. package/dist/governance/workspace-acceptance.d.ts.map +1 -1
  50. package/dist/governance/workspace-acceptance.js +21 -5
  51. package/dist/governance/workspace-acceptance.js.map +1 -1
  52. package/dist/governance/workspace-lease.d.ts.map +1 -1
  53. package/dist/governance/workspace-lease.js +44 -1
  54. package/dist/governance/workspace-lease.js.map +1 -1
  55. package/dist/kernel/authority-text.d.ts +3 -0
  56. package/dist/kernel/authority-text.d.ts.map +1 -0
  57. package/dist/kernel/authority-text.js +22 -0
  58. package/dist/kernel/authority-text.js.map +1 -0
  59. package/dist/kernel/bounded-read-failures.d.ts +9 -0
  60. package/dist/kernel/bounded-read-failures.d.ts.map +1 -0
  61. package/dist/kernel/bounded-read-failures.js +33 -0
  62. package/dist/kernel/bounded-read-failures.js.map +1 -0
  63. package/dist/kernel/bounded-read-sync.d.ts +24 -0
  64. package/dist/kernel/bounded-read-sync.d.ts.map +1 -0
  65. package/dist/kernel/bounded-read-sync.js +75 -0
  66. package/dist/kernel/bounded-read-sync.js.map +1 -0
  67. package/dist/kernel/bounded-read.d.ts +29 -2
  68. package/dist/kernel/bounded-read.d.ts.map +1 -1
  69. package/dist/kernel/bounded-read.js +98 -9
  70. package/dist/kernel/bounded-read.js.map +1 -1
  71. package/dist/kernel/capacity.d.ts +36 -0
  72. package/dist/kernel/capacity.d.ts.map +1 -0
  73. package/dist/kernel/capacity.js +94 -0
  74. package/dist/kernel/capacity.js.map +1 -0
  75. package/dist/kernel/captured-worker-contract.d.ts +38 -0
  76. package/dist/kernel/captured-worker-contract.d.ts.map +1 -0
  77. package/dist/kernel/captured-worker-contract.js +2 -0
  78. package/dist/kernel/captured-worker-contract.js.map +1 -0
  79. package/dist/kernel/catalog.d.ts +12 -3
  80. package/dist/kernel/catalog.d.ts.map +1 -1
  81. package/dist/kernel/catalog.js +28 -8
  82. package/dist/kernel/catalog.js.map +1 -1
  83. package/dist/kernel/chain.d.ts +1 -0
  84. package/dist/kernel/chain.d.ts.map +1 -1
  85. package/dist/kernel/chain.js.map +1 -1
  86. package/dist/kernel/definitions.d.ts +11 -1
  87. package/dist/kernel/definitions.d.ts.map +1 -1
  88. package/dist/kernel/definitions.js +6 -3
  89. package/dist/kernel/definitions.js.map +1 -1
  90. package/dist/kernel/fanout.d.ts +5 -25
  91. package/dist/kernel/fanout.d.ts.map +1 -1
  92. package/dist/kernel/fanout.js +15 -30
  93. package/dist/kernel/fanout.js.map +1 -1
  94. package/dist/kernel/grant-env.d.ts +0 -2
  95. package/dist/kernel/grant-env.d.ts.map +1 -1
  96. package/dist/kernel/grant-env.js +0 -1
  97. package/dist/kernel/grant-env.js.map +1 -1
  98. package/dist/kernel/model-preflight.d.ts +8 -0
  99. package/dist/kernel/model-preflight.d.ts.map +1 -1
  100. package/dist/kernel/model-preflight.js +23 -0
  101. package/dist/kernel/model-preflight.js.map +1 -1
  102. package/dist/kernel/selected-definitions.d.ts +15 -0
  103. package/dist/kernel/selected-definitions.d.ts.map +1 -0
  104. package/dist/kernel/selected-definitions.js +146 -0
  105. package/dist/kernel/selected-definitions.js.map +1 -0
  106. package/dist/kernel/skill-packages.d.ts +4 -2
  107. package/dist/kernel/skill-packages.d.ts.map +1 -1
  108. package/dist/kernel/skill-packages.js +34 -41
  109. package/dist/kernel/skill-packages.js.map +1 -1
  110. package/dist/kernel/skill-resources.d.ts.map +1 -1
  111. package/dist/kernel/skill-resources.js +8 -11
  112. package/dist/kernel/skill-resources.js.map +1 -1
  113. package/dist/kernel/workspace.d.ts +6 -3
  114. package/dist/kernel/workspace.d.ts.map +1 -1
  115. package/dist/kernel/workspace.js +10 -6
  116. package/dist/kernel/workspace.js.map +1 -1
  117. package/dist/products/dashboard-cli.js +1 -1
  118. package/dist/products/dashboard-cli.js.map +1 -1
  119. package/dist/products/dashboard-render.d.ts +1 -2
  120. package/dist/products/dashboard-render.d.ts.map +1 -1
  121. package/dist/products/dashboard-render.js +3 -1
  122. package/dist/products/dashboard-render.js.map +1 -1
  123. package/dist/products/dashboard-session-client.d.ts +1 -2
  124. package/dist/products/dashboard-session-client.d.ts.map +1 -1
  125. package/dist/products/dashboard-session-client.js.map +1 -1
  126. package/dist/products/episode-report.d.ts +12 -8
  127. package/dist/products/episode-report.d.ts.map +1 -1
  128. package/dist/products/episode-report.js +61 -12
  129. package/dist/products/episode-report.js.map +1 -1
  130. package/extensions/approvals.ts +1 -1
  131. package/extensions/chain-plan.ts +9 -3
  132. package/extensions/context-staging.ts +30 -35
  133. package/extensions/dashboard-session-server.ts +1 -2
  134. package/extensions/definition-describe.ts +44 -0
  135. package/extensions/definition-runtime.ts +89 -26
  136. package/extensions/delegate-chain.ts +273 -256
  137. package/extensions/delegation-activation.ts +1 -1
  138. package/extensions/delegation.ts +41 -53
  139. package/extensions/execute-child.ts +167 -134
  140. package/extensions/fanout-outcome.ts +1 -9
  141. package/extensions/grants-command.ts +2 -9
  142. package/extensions/grants.ts +106 -38
  143. package/extensions/init-command.ts +52 -12
  144. package/extensions/reload-environment.ts +21 -2
  145. package/extensions/retired-inputs.ts +7 -0
  146. package/extensions/run-delegation.ts +212 -265
  147. package/extensions/session-capacity.ts +53 -0
  148. package/extensions/session-environment.ts +5 -2
  149. package/extensions/session-model-prompt.ts +33 -32
  150. package/extensions/session-report.ts +28 -11
  151. package/extensions/session.ts +200 -157
  152. package/extensions/workspace-runtime.ts +2 -1
  153. package/native/README.md +25 -0
  154. package/native/linux-x64/worker +0 -0
  155. package/native/linux-x64/worker.sha256 +1 -0
  156. package/native/provenance.json +42 -0
  157. package/native/toolchain.json +18 -0
  158. package/native/worker.c +209 -0
  159. package/package.json +15 -7
  160. package/src/cli.ts +16 -6
  161. package/src/executors/activity-session.ts +120 -66
  162. package/src/executors/captured-execution.ts +61 -0
  163. package/src/executors/child-final.ts +190 -0
  164. package/src/executors/executor.ts +14 -1
  165. package/src/executors/owned-worker.ts +322 -0
  166. package/src/executors/worker-artifact.ts +2 -0
  167. package/src/governance/approval-prompt.ts +18 -8
  168. package/src/governance/captured-worker-record.ts +138 -0
  169. package/src/governance/lease-record.ts +21 -2
  170. package/src/governance/ledger-events.ts +5 -5
  171. package/src/governance/ledger-v3-validation.ts +1 -1
  172. package/src/governance/workspace-acceptance.ts +20 -4
  173. package/src/governance/workspace-lease.ts +59 -1
  174. package/src/kernel/authority-text.ts +17 -0
  175. package/src/kernel/bounded-read-failures.ts +42 -0
  176. package/src/kernel/bounded-read-sync.ts +87 -0
  177. package/src/kernel/bounded-read.ts +128 -11
  178. package/src/kernel/capacity.ts +123 -0
  179. package/src/kernel/captured-worker-contract.ts +29 -0
  180. package/src/kernel/catalog.ts +45 -16
  181. package/src/kernel/chain.ts +1 -0
  182. package/src/kernel/definitions.ts +11 -2
  183. package/src/kernel/fanout.ts +17 -32
  184. package/src/kernel/grant-env.ts +0 -3
  185. package/src/kernel/model-preflight.ts +29 -0
  186. package/src/kernel/selected-definitions.ts +156 -0
  187. package/src/kernel/skill-packages.ts +40 -38
  188. package/src/kernel/skill-resources.ts +8 -9
  189. package/src/kernel/workspace.ts +12 -5
  190. package/src/products/dashboard-cli.ts +1 -1
  191. package/src/products/dashboard-render.ts +6 -2
  192. package/src/products/dashboard-session-client.ts +1 -2
  193. package/src/products/episode-report.ts +80 -17
  194. package/dist/advisors/advisor.d.ts +0 -90
  195. package/dist/advisors/advisor.d.ts.map +0 -1
  196. package/dist/advisors/advisor.js +0 -154
  197. package/dist/advisors/advisor.js.map +0 -1
  198. package/dist/advisors/decider.d.ts +0 -88
  199. package/dist/advisors/decider.d.ts.map +0 -1
  200. package/dist/advisors/decider.js +0 -29
  201. package/dist/advisors/decider.js.map +0 -1
  202. package/dist/advisors/jev.d.ts +0 -41
  203. package/dist/advisors/jev.d.ts.map +0 -1
  204. package/dist/advisors/jev.js +0 -139
  205. package/dist/advisors/jev.js.map +0 -1
  206. package/dist/advisors/settings.d.ts +0 -49
  207. package/dist/advisors/settings.d.ts.map +0 -1
  208. package/dist/advisors/settings.js +0 -99
  209. package/dist/advisors/settings.js.map +0 -1
  210. package/dist/governance/episode-cost-gate.d.ts +0 -31
  211. package/dist/governance/episode-cost-gate.d.ts.map +0 -1
  212. package/dist/governance/episode-cost-gate.js +0 -112
  213. package/dist/governance/episode-cost-gate.js.map +0 -1
  214. package/extensions/advisor-session.ts +0 -67
  215. package/extensions/effort-advice.ts +0 -112
  216. package/extensions/pruning-advice.ts +0 -133
  217. package/src/advisors/advisor.ts +0 -246
  218. package/src/advisors/decider.ts +0 -74
  219. package/src/advisors/jev.ts +0 -163
  220. package/src/advisors/settings.ts +0 -115
  221. package/src/governance/episode-cost-gate.ts +0 -128
@@ -16,7 +16,9 @@
16
16
  * shape, a missing result indistinguishable from an empty one, and the child would have no way to know.
17
17
  */
18
18
  import { randomUUID } from "node:crypto";
19
- import { closeSync, mkdirSync, openSync, readSync, realpathSync, rmSync, statSync } from "node:fs";
19
+ import { mkdirSync, realpathSync, rmSync, statSync } from "node:fs";
20
+ import { BoundedReadCleanupError } from "../src/kernel/bounded-read.ts";
21
+ import { readBoundedTextSync } from "../src/kernel/bounded-read-sync.ts";
20
22
  import { isAbsolute, relative, resolve } from "node:path";
21
23
  import {
22
24
  CONTEXT_RANK,
@@ -31,6 +33,8 @@ import {
31
33
  export interface ParentSession {
32
34
  getSessionFile(): string | undefined;
33
35
  getEntries(): Array<{ id: string; type: string }>;
36
+ /** Public Pi active leaf-to-root ancestry API; unavailable is not an empty conversation. */
37
+ getBranch?(): Array<{ id: string; type: string }>;
34
38
  }
35
39
 
36
40
  export interface StagingInput {
@@ -86,7 +90,10 @@ export function createHandoffStager(input: StagingInput) {
86
90
  for (const path of granted.files ?? [])
87
91
  sections.push({ ...readSection(input.cwd, path), keepRank: CONTEXT_RANK.file });
88
92
  if (granted.mode === "pruned") {
89
- const all = parentTurns(input.parentSession);
93
+ const branch = parentTurns(input.parentSession);
94
+ const all = branch.turns;
95
+ if (branch.unavailable)
96
+ sections.push({ label: "parent turns — incomplete", body: branch.unavailable, keepRank: CONTEXT_RANK.summary });
90
97
  const selection = selectPrunedTurns(all, {
91
98
  ...(granted.turns !== undefined ? { turns: granted.turns } : {}),
92
99
  ...(granted.files !== undefined ? { files: granted.files } : {}),
@@ -113,7 +120,7 @@ export function createHandoffStager(input: StagingInput) {
113
120
  keepRank: (matched.has(turn.id) ? CONTEXT_RANK.fileMatchedTurn : CONTEXT_RANK.recentTurn) + index,
114
121
  });
115
122
  });
116
- if (chosen.length === 0)
123
+ if (chosen.length === 0 && !branch.unavailable)
117
124
  sections.push({ label: "parent turns", body: "(no turn of your parent's session matched the selection)" });
118
125
  }
119
126
 
@@ -193,43 +200,31 @@ function readSection(cwd: string, path: string): ContextSection {
193
200
  if (within.startsWith("..") || within === "" || isAbsolute(within))
194
201
  return refuse("outside this session's working directory");
195
202
  try {
196
- const stats = statSync(absolute);
197
- // A FIFO satisfies `statSync` and then never returns from a read, which would block the whole pi session with
198
- // no watchdog — and the path is model-supplied, so it is reachable rather than theoretical.
199
- if (!stats.isFile()) return refuse("not a regular file");
200
- const note = stats.size > MAX_FILE_BYTES ? ` (first ${MAX_FILE_BYTES} of ${stats.size} bytes)` : "";
201
- return { label: `${path}${note}`, body: readBounded(absolute, MAX_FILE_BYTES) };
203
+ const read = readBoundedTextSync(absolute, {
204
+ maxBytes: MAX_FILE_BYTES,
205
+ prefix: true,
206
+ confinedRoot: root,
207
+ });
208
+ const note = read.truncated ? " (incomplete: bounded file prefix)" : "";
209
+ return { label: `${path}${note}`, body: read.text };
202
210
  } catch (error) {
211
+ if (error instanceof BoundedReadCleanupError) throw error;
203
212
  return { label: path, body: `(could not be read: ${error instanceof Error ? error.message : String(error)})` };
204
213
  }
205
214
  }
206
215
 
207
- /**
208
- * Read at most `budget` BYTES, without pulling the rest of the file into the parent first.
209
- *
210
- * `readFileSync(...).slice(budget)` reads the whole file and then slices by UTF-16 code units, so a large file in
211
- * the repository stalled the parent and the "(first N of M bytes)" label was wrong for any multi-byte content.
212
- */
213
- function readBounded(path: string, budget: number): string {
214
- const handle = openSync(path, "r");
216
+ /** Canonical active branch includes shared ancestry and excludes alternate descendants. */
217
+ function parentTurns(session?: ParentSession): { turns: PrunableTurn[]; unavailable?: string } {
215
218
  try {
216
- const buffer = Buffer.alloc(budget);
217
- const read = readSync(handle, buffer, 0, budget, 0);
218
- return new TextDecoder("utf-8", { fatal: false }).decode(buffer.subarray(0, read)).replace(/\uFFFD+$/, "");
219
- } finally {
220
- closeSync(handle);
221
- }
222
- }
223
-
224
- /** The parent's message turns, reduced to what the selection rule needs. Never pi's own types past this point. */
225
- function parentTurns(session?: ParentSession): PrunableTurn[] {
226
- if (!session) return [];
227
- try {
228
- return session
229
- .getEntries()
230
- .filter((entry) => entry.type === "message")
231
- .map((entry) => ({ id: entry.id, text: JSON.stringify((entry as { message?: unknown }).message ?? entry) }));
232
- } catch {
233
- return [];
219
+ if (!session?.getBranch) throw new Error("active branch API is unavailable");
220
+ const entries = session.getBranch();
221
+ if (!Array.isArray(entries)) throw new Error("active branch did not return entries");
222
+ return {
223
+ turns: entries
224
+ .filter((entry) => entry.type === "message")
225
+ .map((entry) => ({ id: entry.id, text: JSON.stringify((entry as { message?: unknown }).message ?? entry) })),
226
+ };
227
+ } catch (error) {
228
+ return { turns: [], unavailable: `(active branch unavailable; context is incomplete: ${String(error)})` };
234
229
  }
235
230
  }
@@ -75,8 +75,7 @@ async function respond(session: GrantsSession, token: string, line: string): Pro
75
75
  return {
76
76
  ok: true,
77
77
  rows: sessionModelRows(session, [...session.definitions.keys()].sort()),
78
- cost: session.episodeCostGate.cost,
79
- ceiling: session.episodeCostGate.ceiling,
78
+ cost: null, // Live complete episode cost is unavailable; retained usage remains in the episode report.
80
79
  };
81
80
  }
82
81
 
@@ -0,0 +1,44 @@
1
+ /** Read-only handshake with the exact immutable definition dispatched by this extension instance. */
2
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ import { Type } from "typebox";
4
+ import { digestDefinition } from "../src/kernel/definitions.ts";
5
+ import { maySpawnDefinition } from "../src/kernel/delegate.ts";
6
+ import type { GrantsSession } from "./session.ts";
7
+ import { assertDelegationAuthority } from "./delegation-authority.ts";
8
+ export function assertDefinitionIdentity(
9
+ session: GrantsSession,
10
+ spec: { agent?: string; definitionId?: string },
11
+ ): void {
12
+ const definition = spec.agent ? session.definitions.get(spec.agent) : undefined;
13
+ if (spec.definitionId !== undefined && (!definition || definition.definitionId !== spec.definitionId))
14
+ throw Error("definition snapshot changed or is unavailable; call delegate_describe again");
15
+ if (definition?.binding && !spec.definitionId)
16
+ throw Error("Principal delegation requires delegate_describe definitionId");
17
+ }
18
+ export function registerDefinitionDescribe(pi: ExtensionAPI, session: GrantsSession): void {
19
+ pi.registerTool({
20
+ name: "delegate_describe",
21
+ label: "Describe a governed definition",
22
+ description:
23
+ "Read the exact selected definition identity and Principal binding before delegation. Starts no child and grants nothing.",
24
+ parameters: Type.Object({ agent: Type.String() }),
25
+ async execute(_id, params) {
26
+ assertDelegationAuthority(session);
27
+ await session.ensureDefinitions?.();
28
+ if (!maySpawnDefinition(session.ownGrant, params.agent))
29
+ throw Error("definition is outside this session's grant");
30
+ const definition = session.definitions.get(params.agent);
31
+ if (!definition?.definitionId)
32
+ throw Error("selected definition is unavailable; reload after correcting discovery");
33
+ const details = {
34
+ version: 1,
35
+ agent: params.agent,
36
+ sourceHash: definition.sourceHash,
37
+ bodySha256: digestDefinition(definition).sha256,
38
+ binding: definition.binding ?? null,
39
+ definitionId: definition.definitionId,
40
+ };
41
+ return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details };
42
+ },
43
+ });
44
+ }
@@ -2,8 +2,8 @@
2
2
 
3
3
  export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
4
4
  export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
5
- export type ModelSource = "explicit" | "session" | "definition" | "global" | "pi";
6
- export type ThinkingSource = ModelSource | "advisor";
5
+ export type ModelSource = "explicit" | "session" | "definition" | "authored" | "global" | "pi";
6
+ export type ThinkingSource = ModelSource;
7
7
  export interface DefinitionRuntimeChoice {
8
8
  model?: string;
9
9
  thinking?: ThinkingLevel;
@@ -58,36 +58,99 @@ export function resolvedModelOf(value: string | undefined): { provider: string;
58
58
  : null;
59
59
  }
60
60
 
61
+ /** Parse the complete needed list before evaluating availability. No model/thinking cross product. */
62
+ export function authoredRuntimePreferences(raw: unknown): Required<DefinitionRuntimeChoice>[] {
63
+ if (raw === undefined) return [];
64
+ let value = raw;
65
+ if (typeof value === "string") {
66
+ const text = value.startsWith("'") && value.endsWith("'") ? value.slice(1, -1) : value;
67
+ try {
68
+ value = JSON.parse(text);
69
+ } catch {
70
+ throw new Error("runtime-preferences must be a JSON array of model/thinking pairs");
71
+ }
72
+ }
73
+ if (!Array.isArray(value) || value.length > 32)
74
+ throw new Error("runtime-preferences must contain at most 32 ordered pairs");
75
+ return value.map((row, index) => {
76
+ if (
77
+ !isObject(row) ||
78
+ Object.keys(row).some((key) => !["model", "thinking"].includes(key)) ||
79
+ !qualifiedModel(row.model) ||
80
+ !parseThinkingLevel(row.thinking)
81
+ ) {
82
+ throw new Error(`runtime-preferences[${index}] needs an exact provider/model and thinking pair`);
83
+ }
84
+ return { model: row.model as string, thinking: row.thinking as ThinkingLevel };
85
+ });
86
+ }
87
+
88
+ function qualifiedModel(value: unknown): value is string {
89
+ return typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9_.-]*\/\S+$/.test(value);
90
+ }
91
+
61
92
  export function resolveDefinitionRuntime(input: {
62
93
  definition?: string;
63
94
  explicit: { model?: string; thinking?: string };
64
95
  session: ReadonlyMap<string, DefinitionRuntimeChoice>;
65
96
  settings: DefinitionRuntimeSettings;
66
97
  piModel?: string;
67
- advisorThinking?: string;
98
+ piThinking?: string;
99
+ authoredPreferences?: unknown;
100
+ /** Local, passive evidence only. Undefined means no known reason to reject, not remote health. */
101
+ unavailable?: (choice: DefinitionRuntimeChoice) => string | undefined;
68
102
  }): ResolvedDefinitionRuntime {
69
103
  const session = input.definition ? input.session.get(input.definition) : undefined;
70
- const definition = input.definition ? input.settings.definitions.get(input.definition) : undefined;
71
- const modelCandidates: Array<[string | undefined, ModelSource]> = [
72
- [input.explicit.model, "explicit"],
73
- [session?.model, "session"],
74
- [definition?.model, "definition"],
75
- [input.settings.defaults.model, "global"],
76
- [input.piModel, "pi"],
77
- ];
78
- const thinkingCandidates: Array<[string | undefined, ThinkingSource]> = [
79
- [input.explicit.thinking, "explicit"],
80
- [session?.thinking, "session"],
81
- [input.advisorThinking, "advisor"],
82
- [definition?.thinking, "definition"],
83
- [input.settings.defaults.thinking, "global"],
84
- ];
85
- const selectedModel = modelCandidates.find(([value]) => value !== undefined) ?? [undefined, "pi" as const];
86
- const selectedThinking = thinkingCandidates.find(([value]) => value !== undefined) ?? [undefined, "pi" as const];
87
- return {
88
- model: selectedModel[0],
89
- modelSource: selectedModel[1],
90
- thinking: parseThinkingLevel(selectedThinking[0]),
91
- thinkingSource: selectedThinking[1],
92
- };
104
+ const project = input.definition ? input.settings.definitions.get(input.definition) : undefined;
105
+ const fixed = [
106
+ [input.explicit, "explicit"],
107
+ [session, "session"],
108
+ [project, "definition"],
109
+ ] as const;
110
+ const model = fixed.find(([choice]) => choice?.model !== undefined);
111
+ const thinking = fixed.find(([choice]) => choice?.thinking !== undefined);
112
+ const fixedModel = model?.[0]?.model;
113
+ const fixedThinking = thinking?.[0]?.thinking;
114
+ if (fixedModel !== undefined && !qualifiedModel(fixedModel))
115
+ throw new Error("explicit runtime model needs provider/model");
116
+ if (fixedThinking !== undefined && !parseThinkingLevel(fixedThinking))
117
+ throw new Error("explicit runtime thinking is invalid");
118
+ const result = (candidate: DefinitionRuntimeChoice, source: ModelSource): ResolvedDefinitionRuntime => ({
119
+ model: fixedModel ?? candidate.model,
120
+ modelSource: model?.[1] ?? source,
121
+ thinking: parseThinkingLevel(fixedThinking ?? candidate.thinking),
122
+ thinkingSource: thinking?.[1] ?? source,
123
+ });
124
+ // Only a complete higher-priority pair can leave malformed authored preferences unused.
125
+ if (fixedModel !== undefined && fixedThinking !== undefined) {
126
+ const selected = result({}, "pi");
127
+ const reason = input.unavailable?.(selected);
128
+ if (reason) throw new Error(reason);
129
+ return selected;
130
+ }
131
+ const preferences = authoredRuntimePreferences(input.authoredPreferences);
132
+ for (const candidate of preferences) {
133
+ if (fixedModel !== undefined && candidate.model !== fixedModel) continue;
134
+ if (fixedThinking !== undefined && candidate.thinking !== fixedThinking) continue;
135
+ const selected = result(candidate, "authored");
136
+ if (!input.unavailable?.(selected)) return selected;
137
+ }
138
+ // Normal fallback fills only missing fields; an overridden row is never described as authored.
139
+ const normal = [
140
+ [input.settings.defaults, "global"],
141
+ [{ model: input.piModel, thinking: parseThinkingLevel(input.piThinking) }, "pi"],
142
+ ] as const;
143
+ let refusal: string | undefined;
144
+ for (const [candidate, source] of normal) {
145
+ const selected = result(
146
+ { model: candidate.model ?? input.piModel, thinking: candidate.thinking ?? parseThinkingLevel(input.piThinking) },
147
+ source,
148
+ );
149
+ // Attribute a field filled from the current session accurately even when another default wins.
150
+ if (!model && candidate.model === undefined) selected.modelSource = "pi";
151
+ if (!thinking && candidate.thinking === undefined) selected.thinkingSource = "pi";
152
+ refusal = input.unavailable?.(selected);
153
+ if (!refusal) return selected;
154
+ }
155
+ throw new Error(refusal ?? "no locally usable configured runtime pair");
93
156
  }