jeopi-cli 16.4.2 → 16.4.4

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 (148) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/cli.js +3033 -2991
  3. package/dist/types/advisor/advise-tool.d.ts +5 -1
  4. package/dist/types/advisor/config.d.ts +4 -3
  5. package/dist/types/advisor/runtime.d.ts +15 -0
  6. package/dist/types/cli/args.d.ts +2 -0
  7. package/dist/types/cli/update-cli.d.ts +4 -1
  8. package/dist/types/cli/usage-error.d.ts +4 -0
  9. package/dist/types/commit/agentic/agent.d.ts +1 -0
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/model-resolver.d.ts +7 -1
  12. package/dist/types/config/models-config.d.ts +1 -1
  13. package/dist/types/config/settings-schema.d.ts +3 -13
  14. package/dist/types/eval/py/tool-bridge.d.ts +1 -0
  15. package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
  16. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  17. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  18. package/dist/types/internal-urls/memory-protocol.d.ts +6 -6
  19. package/dist/types/internal-urls/types.d.ts +7 -0
  20. package/dist/types/irc/bus.d.ts +4 -0
  21. package/dist/types/lsp/deferred-diagnostics.d.ts +11 -0
  22. package/dist/types/modes/components/move-overlay.d.ts +1 -1
  23. package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
  24. package/dist/types/modes/components/welcome.d.ts +4 -0
  25. package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
  26. package/dist/types/modes/orchestrate.d.ts +1 -1
  27. package/dist/types/modes/rpc/host-tools.d.ts +2 -0
  28. package/dist/types/modes/rpc/rpc-mode.d.ts +27 -7
  29. package/dist/types/modes/ultrathink.d.ts +1 -1
  30. package/dist/types/modes/utils/transcript-render-helpers.d.ts +9 -0
  31. package/dist/types/modes/workflow.d.ts +1 -1
  32. package/dist/types/session/agent-session.d.ts +6 -0
  33. package/dist/types/session/auth-broker-config.d.ts +2 -2
  34. package/dist/types/tools/bash-skill-urls.d.ts +3 -2
  35. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -1
  36. package/dist/types/tools/browser/cmux/rpc.d.ts +20 -0
  37. package/dist/types/tools/browser/launch.d.ts +1 -0
  38. package/dist/types/tools/browser/run-cancellation.d.ts +17 -2
  39. package/dist/types/tools/browser/run-output.d.ts +25 -0
  40. package/dist/types/tools/browser/tab-worker.d.ts +22 -0
  41. package/dist/types/utils/changelog.d.ts +35 -1
  42. package/package.json +12 -12
  43. package/src/advisor/__tests__/advisor.test.ts +13 -0
  44. package/src/advisor/__tests__/config.test.ts +36 -0
  45. package/src/advisor/advise-tool.ts +6 -1
  46. package/src/advisor/config.ts +11 -8
  47. package/src/advisor/runtime.ts +127 -0
  48. package/src/autolearn/controller.ts +15 -3
  49. package/src/cli/args.ts +12 -0
  50. package/src/cli/flag-tables.ts +21 -7
  51. package/src/cli/update-cli.ts +63 -13
  52. package/src/cli/usage-error.ts +7 -0
  53. package/src/commands/acp.ts +11 -2
  54. package/src/commands/launch.ts +12 -3
  55. package/src/commit/agentic/agent.ts +4 -0
  56. package/src/commit/agentic/index.ts +46 -21
  57. package/src/config/config-file.ts +30 -12
  58. package/src/config/model-registry.ts +7 -7
  59. package/src/config/model-resolver.ts +24 -6
  60. package/src/config/models-config.ts +1 -1
  61. package/src/config/settings-schema.ts +3 -15
  62. package/src/edit/index.ts +10 -76
  63. package/src/edit/renderer.ts +13 -10
  64. package/src/eval/__tests__/agent-bridge.test.ts +19 -14
  65. package/src/eval/__tests__/bridge-timeout.test.ts +60 -0
  66. package/src/eval/executor-base.ts +84 -7
  67. package/src/eval/js/shared/rewrite-imports.ts +31 -13
  68. package/src/eval/py/tool-bridge.ts +13 -14
  69. package/src/exec/non-interactive-env.ts +0 -1
  70. package/src/export/ttsr.ts +0 -3
  71. package/src/extensibility/custom-tools/loader.ts +70 -37
  72. package/src/extensibility/custom-tools/types.ts +3 -0
  73. package/src/extensibility/extensions/runner.ts +3 -0
  74. package/src/extensibility/extensions/types.ts +3 -0
  75. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +9 -1
  76. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +30 -3
  77. package/src/extensibility/plugins/manager.ts +21 -0
  78. package/src/internal-urls/docs-index.generated.txt +1 -1
  79. package/src/internal-urls/memory-protocol.ts +13 -8
  80. package/src/internal-urls/skill-protocol.ts +1 -1
  81. package/src/internal-urls/types.ts +7 -0
  82. package/src/irc/bus.ts +48 -18
  83. package/src/lsp/client.ts +7 -1
  84. package/src/lsp/deferred-diagnostics.ts +66 -0
  85. package/src/main.ts +13 -13
  86. package/src/mcp/tool-bridge.ts +57 -6
  87. package/src/mcp/transports/stdio.test.ts +45 -1
  88. package/src/mcp/transports/stdio.ts +6 -3
  89. package/src/modes/acp/acp-agent.ts +73 -2
  90. package/src/modes/acp/acp-event-mapper.ts +5 -0
  91. package/src/modes/acp/acp-mode.ts +11 -0
  92. package/src/modes/components/__tests__/move-overlay.test.ts +16 -1
  93. package/src/modes/components/advisor-config.ts +15 -7
  94. package/src/modes/components/chat-transcript-builder.ts +31 -11
  95. package/src/modes/components/move-overlay.ts +2 -3
  96. package/src/modes/components/plan-review-overlay.ts +7 -0
  97. package/src/modes/components/tips.txt +1 -1
  98. package/src/modes/components/welcome.ts +13 -14
  99. package/src/modes/controllers/command-controller.ts +8 -10
  100. package/src/modes/controllers/event-controller.ts +58 -42
  101. package/src/modes/controllers/input-controller.ts +9 -6
  102. package/src/modes/controllers/selector-controller.ts +3 -2
  103. package/src/modes/controllers/tan-command-controller.ts +40 -1
  104. package/src/modes/interactive-mode.ts +13 -0
  105. package/src/modes/magic-keyword-boundary.ts +23 -0
  106. package/src/modes/orchestrate.ts +6 -5
  107. package/src/modes/print-mode.ts +9 -0
  108. package/src/modes/rpc/host-tools.ts +13 -0
  109. package/src/modes/rpc/rpc-mode.ts +130 -56
  110. package/src/modes/ultrathink.ts +6 -5
  111. package/src/modes/utils/transcript-render-helpers.ts +44 -0
  112. package/src/modes/utils/ui-helpers.ts +16 -1
  113. package/src/modes/workflow.ts +5 -4
  114. package/src/prompts/system/eager-task.md +2 -2
  115. package/src/prompts/system/tan-context-switch.md +17 -0
  116. package/src/prompts/tools/browser.md +5 -5
  117. package/src/sdk.ts +3 -0
  118. package/src/session/agent-session.ts +267 -36
  119. package/src/session/auth-broker-config.ts +4 -4
  120. package/src/session/snapcompact-inline.ts +3 -19
  121. package/src/session/streaming-output.ts +40 -12
  122. package/src/slash-commands/builtin-registry.ts +8 -8
  123. package/src/task/executor.ts +8 -0
  124. package/src/task/render.ts +51 -44
  125. package/src/tools/bash-skill-urls.ts +7 -4
  126. package/src/tools/bash.ts +30 -33
  127. package/src/tools/browser/cmux/cmux-tab.ts +49 -50
  128. package/src/tools/browser/cmux/rpc.ts +50 -0
  129. package/src/tools/browser/launch.ts +8 -4
  130. package/src/tools/browser/run-cancellation.ts +44 -5
  131. package/src/tools/browser/run-output.ts +76 -0
  132. package/src/tools/browser/tab-worker.ts +280 -133
  133. package/src/tools/eval-render.ts +16 -14
  134. package/src/tools/glob.ts +20 -6
  135. package/src/tools/grep.ts +1 -0
  136. package/src/tools/image-gen.ts +3 -4
  137. package/src/tools/irc.ts +27 -13
  138. package/src/tools/path-utils.ts +6 -1
  139. package/src/tools/read.ts +14 -7
  140. package/src/tools/write.ts +22 -5
  141. package/src/tools/yield.ts +29 -1
  142. package/src/utils/changelog.ts +106 -9
  143. package/src/utils/git.ts +0 -1
  144. package/src/utils/title-generator.ts +72 -7
  145. package/src/web/search/index.ts +14 -6
  146. package/src/web/search/providers/perplexity.ts +18 -2
  147. package/dist/types/tools/bash-command-fixup.d.ts +0 -11
  148. package/src/tools/bash-command-fixup.ts +0 -37
@@ -112,6 +112,19 @@ async function getBunGlobalBinDir(): Promise<string | undefined> {
112
112
  }
113
113
  }
114
114
 
115
+ async function getNpmGlobalBinDir(): Promise<string | undefined> {
116
+ if (!$which("npm")) return undefined;
117
+ try {
118
+ const result = await $`npm prefix -g`.quiet().nothrow();
119
+ if (result.exitCode !== 0) return undefined;
120
+ const prefix = result.text().trim();
121
+ if (prefix.length === 0) return undefined;
122
+ return process.platform === "win32" ? prefix : path.join(prefix, "bin");
123
+ } catch {
124
+ return undefined;
125
+ }
126
+ }
127
+
115
128
  async function getHomebrewFormulaPrefix(): Promise<string | undefined> {
116
129
  if (!$which("brew")) return undefined;
117
130
  for (const formula of [HOMEBREW_FORMULA, APP_NAME]) {
@@ -192,26 +205,36 @@ function isPathInDirectory(filePath: string, directoryPath: string): boolean {
192
205
  return isPathInDirectoryLexical(resolvedFile, dirReal);
193
206
  }
194
207
 
195
- type UpdateMethod = "brew" | "mise" | "bun" | "binary";
208
+ type UpdateMethod = "brew" | "mise" | "bun" | "npm" | "binary";
196
209
 
197
210
  interface UpdateMethodResolutionOptions {
198
211
  homebrewPrefix?: string;
199
212
  miseBinDirs?: readonly string[];
200
213
  miseDataDir?: string;
214
+ npmBinDir?: string;
201
215
  }
202
216
 
203
- type UpdateTarget = { method: "brew" } | { method: "mise" } | { method: "bun" } | { method: "binary"; path: string };
217
+ type UpdateTarget =
218
+ | { method: "brew" }
219
+ | { method: "mise" }
220
+ | { method: "bun" }
221
+ | { method: "npm" }
222
+ | { method: "binary"; path: string };
204
223
 
205
224
  function resolveUpdateMethod(
206
225
  jeopiPath: string,
207
226
  bunBinDir: string | undefined,
208
227
  options: UpdateMethodResolutionOptions = {},
209
228
  ): UpdateMethod {
210
- const { homebrewPrefix, miseBinDirs = [], miseDataDir } = options;
229
+ const { homebrewPrefix, miseBinDirs = [], miseDataDir, npmBinDir } = options;
230
+ const launcherExtension = path.extname(jeopiPath).toLowerCase();
231
+ const isWindowsScriptLauncher =
232
+ launcherExtension === ".cmd" || launcherExtension === ".ps1" || launcherExtension === ".bat";
211
233
  if (homebrewPrefix && isPathInDirectory(jeopiPath, path.join(homebrewPrefix, "bin"))) return "brew";
212
234
  if (miseBinDirs.some(dir => isPathInDirectory(jeopiPath, dir))) return "mise";
213
235
  if (miseDataDir && isPathInDirectory(jeopiPath, path.join(miseDataDir, "shims"))) return "mise";
214
236
  if (bunBinDir && isPathInDirectory(jeopiPath, bunBinDir)) return "bun";
237
+ if ((npmBinDir && isPathInDirectory(jeopiPath, npmBinDir)) || isWindowsScriptLauncher) return "npm";
215
238
  return "binary";
216
239
  }
217
240
 
@@ -224,6 +247,7 @@ export function resolveUpdateMethodForTest(
224
247
  }
225
248
  async function resolveUpdateTarget(): Promise<UpdateTarget> {
226
249
  const bunBinDir = await getBunGlobalBinDir();
250
+ const npmBinDir = await getNpmGlobalBinDir();
227
251
  const homebrewPrefix = await getHomebrewFormulaPrefix();
228
252
  const miseAvailable = $which("mise") !== undefined;
229
253
  const miseBinDirs = miseAvailable ? await getMiseBinDirs() : [];
@@ -231,7 +255,7 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
231
255
  const jeopiPath = resolveJeopiPath();
232
256
 
233
257
  if (jeopiPath) {
234
- const method = resolveUpdateMethod(jeopiPath, bunBinDir, { homebrewPrefix, miseBinDirs, miseDataDir });
258
+ const method = resolveUpdateMethod(jeopiPath, bunBinDir, { homebrewPrefix, miseBinDirs, miseDataDir, npmBinDir });
235
259
  if (method === "binary") return { method, path: jeopiPath };
236
260
  return { method };
237
261
  }
@@ -874,6 +898,15 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
874
898
  }
875
899
  }
876
900
 
901
+ function buildVersionedPackageInstallArgs(expectedVersion: string, nativeTag: string): string[] {
902
+ const args = [`${NPM_PACKAGE}@${expectedVersion}`, `${NATIVES_PACKAGE}@${expectedVersion}`];
903
+ const leafPackage = NATIVE_LEAF_PACKAGES.get(nativeTag);
904
+ if (leafPackage) {
905
+ args.push(`${leafPackage}@${expectedVersion}`);
906
+ }
907
+ return args;
908
+ }
909
+
877
910
  /**
878
911
  * Build the bun argv used to globally install a specific jeopi version.
879
912
  *
@@ -905,19 +938,23 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
905
938
  * See #1824.
906
939
  */
907
940
  export function buildBunInstallArgs(expectedVersion: string, nativeTag: string = currentNativeTag()): string[] {
908
- const args = [
941
+ return [
909
942
  "install",
910
943
  "-g",
911
944
  "--no-cache",
912
945
  `--registry=${NPM_REGISTRY}`,
913
- `${NPM_PACKAGE}@${expectedVersion}`,
914
- `${NATIVES_PACKAGE}@${expectedVersion}`,
946
+ ...buildVersionedPackageInstallArgs(expectedVersion, nativeTag),
947
+ ];
948
+ }
949
+
950
+ /** Build the npm argv used to update npm-managed global installs. */
951
+ export function buildNpmInstallArgs(expectedVersion: string, nativeTag: string = currentNativeTag()): string[] {
952
+ return [
953
+ "install",
954
+ "-g",
955
+ `--registry=${NPM_REGISTRY}`,
956
+ ...buildVersionedPackageInstallArgs(expectedVersion, nativeTag),
915
957
  ];
916
- const leafPackage = NATIVE_LEAF_PACKAGES.get(nativeTag);
917
- if (leafPackage) {
918
- args.push(`${leafPackage}@${expectedVersion}`);
919
- }
920
- return args;
921
958
  }
922
959
 
923
960
  export function buildHomebrewUpdateArgs(force: boolean): string[] {
@@ -933,7 +970,7 @@ export function buildMiseForceInstallArgs(expectedVersion: string): string[] {
933
970
  }
934
971
 
935
972
  /**
936
- * Update via bun package manager.
973
+ * Update via package manager.
937
974
  */
938
975
  async function updateViaBun(expectedVersion: string): Promise<void> {
939
976
  console.log(chalk.dim("Updating via bun..."));
@@ -954,6 +991,17 @@ async function updateViaBun(expectedVersion: string): Promise<void> {
954
991
  }
955
992
  }
956
993
 
994
+ async function updateViaNpm(expectedVersion: string): Promise<void> {
995
+ console.log(chalk.dim("Updating via npm..."));
996
+ const args = buildNpmInstallArgs(expectedVersion);
997
+ const result = await $`npm ${args}`.nothrow();
998
+ if (result.exitCode !== 0) {
999
+ throw new Error(`npm install failed with exit code ${result.exitCode}`);
1000
+ }
1001
+
1002
+ await printVerification(expectedVersion);
1003
+ }
1004
+
957
1005
  async function updateViaHomebrew(expectedVersion: string, force: boolean): Promise<void> {
958
1006
  console.log(chalk.dim("Updating Homebrew formulae..."));
959
1007
  const update = await $`brew update`.nothrow();
@@ -1089,6 +1137,8 @@ export async function runUpdateCommand(opts: { force: boolean; check: boolean })
1089
1137
  await updateViaMise(release.version, opts.force);
1090
1138
  } else if (target.method === "bun") {
1091
1139
  await updateViaBun(release.version);
1140
+ } else if (target.method === "npm") {
1141
+ await updateViaNpm(release.version);
1092
1142
  } else {
1093
1143
  await updateViaBinaryAt(target.path, release.version);
1094
1144
  }
@@ -0,0 +1,7 @@
1
+ /** Command-line input error that should be rendered without a stack trace. */
2
+ export class CliUsageError extends Error {
3
+ constructor(message: string) {
4
+ super(message);
5
+ this.name = "CliUsageError";
6
+ }
7
+ }
@@ -5,7 +5,7 @@
5
5
  * ACP terminal-auth flag asks the same command to open the interactive TUI.
6
6
  */
7
7
  import { Command } from "jeopi-utils/cli";
8
- import { parseArgs } from "../cli/args";
8
+ import { type Args as ParsedArgs, parseArgs, reportCliUsageError } from "../cli/args";
9
9
  import { runRootCommand } from "../main";
10
10
  import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth";
11
11
 
@@ -15,7 +15,16 @@ export default class Acp extends Command {
15
15
 
16
16
  async run(): Promise<void> {
17
17
  const { args, terminalAuth } = prepareAcpTerminalAuthArgs(this.argv);
18
- const parsed = parseArgs(args);
18
+ let parsed: ParsedArgs;
19
+ try {
20
+ parsed = parseArgs(args);
21
+ } catch (error) {
22
+ if (reportCliUsageError(error)) {
23
+ process.exitCode = 2;
24
+ return;
25
+ }
26
+ throw error;
27
+ }
19
28
  if (!terminalAuth) {
20
29
  parsed.mode = "acp";
21
30
  }
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { APP_NAME } from "jeopi-utils";
6
6
  import { Args, Command, Flags } from "jeopi-utils/cli";
7
- import { parseArgs } from "../cli/args";
7
+ import { type Args as ParsedArgs, parseArgs, reportCliUsageError } from "../cli/args";
8
8
  import { runRootCommand } from "../main";
9
9
  import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth";
10
10
  import { CLI_THINKING_LEVELS } from "../thinking";
@@ -140,7 +140,7 @@ export default class Index extends Command {
140
140
  description: "Include thinking blocks in print mode text output",
141
141
  }),
142
142
  "max-time": Flags.string({
143
- description: "Stop the session after this many seconds",
143
+ description: "Stop the session after this duration (e.g., 600, 10m, 1h)",
144
144
  }),
145
145
  // `--auto-approve` / `--yolo`: declared here so oclif's auto-generated `--help` lists it.
146
146
  // Runtime parsing happens in `cli/args.ts parseArgs` (line 176 in that file) — `runRootCommand`
@@ -176,7 +176,16 @@ export default class Index extends Command {
176
176
 
177
177
  async run(): Promise<void> {
178
178
  const { args } = prepareAcpTerminalAuthArgs(this.argv);
179
- const parsed = parseArgs(args);
179
+ let parsed: ParsedArgs;
180
+ try {
181
+ parsed = parseArgs(args);
182
+ } catch (error) {
183
+ if (reportCliUsageError(error)) {
184
+ process.exitCode = 2;
185
+ return;
186
+ }
187
+ throw error;
188
+ }
180
189
  await runRootCommand(parsed, args);
181
190
  }
182
191
  }
@@ -29,6 +29,7 @@ export interface CommitAgentInput {
29
29
  requireChangelog: boolean;
30
30
  diffText?: string;
31
31
  existingChangelogEntries?: ExistingChangelogEntries[];
32
+ onComplete?: (state: CommitAgentState) => Promise<void> | void;
32
33
  }
33
34
 
34
35
  export interface ExistingChangelogEntries {
@@ -175,6 +176,9 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
175
176
  });
176
177
  }
177
178
 
179
+ if (input.onComplete) {
180
+ await input.onComplete(state);
181
+ }
178
182
  return state;
179
183
  } finally {
180
184
  unsubscribe();
@@ -6,7 +6,7 @@ import { detectChangelogBoundaries } from "../../commit/changelog/detect";
6
6
  import { parseUnreleasedSection } from "../../commit/changelog/parse";
7
7
  import { formatCommitMessage } from "../../commit/message";
8
8
  import { resolvePrimaryModel, resolveSmolModel } from "../../commit/model-selection";
9
- import type { CommitCommandArgs, ConventionalAnalysis } from "../../commit/types";
9
+ import type { CommitCommandArgs, ConventionalAnalysis, NumstatEntry } from "../../commit/types";
10
10
  import { ModelRegistry } from "../../config/model-registry";
11
11
  import { Settings } from "../../config/settings";
12
12
  import { discoverAuthStorage, discoverContextFiles } from "../../sdk";
@@ -121,11 +121,10 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
121
121
  }
122
122
 
123
123
  process.stdout.write("● Starting commit agent...\n");
124
- let commitState: CommitAgentState;
125
- let usedFallback = false;
124
+ let agentSessionCompleted = false;
126
125
 
127
126
  try {
128
- commitState = await runCommitAgentSession({
127
+ await runCommitAgentSession({
129
128
  cwd,
130
129
  model: agentModel,
131
130
  thinkingLevel: agentThinkingLevel,
@@ -138,42 +137,67 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
138
137
  requireChangelog: !args.noChangelog && changelogTargets.length > 0,
139
138
  diffText: diff,
140
139
  existingChangelogEntries,
140
+ onComplete: async commitState => {
141
+ agentSessionCompleted = true;
142
+ await completeAgentCommitState(commitState, {
143
+ cwd,
144
+ dryRun: args.dryRun,
145
+ push: args.push,
146
+ noChangelog: args.noChangelog,
147
+ changelogTargets,
148
+ numstat,
149
+ });
150
+ },
141
151
  });
152
+ return;
142
153
  } catch (error) {
154
+ if (agentSessionCompleted) {
155
+ throw error;
156
+ }
143
157
  const errorMessage = error instanceof Error ? error.message : String(error);
144
158
  process.stderr.write(`Agent error: ${errorMessage}\n`);
145
159
  if (error instanceof Error && error.stack && $env.DEBUG) {
146
160
  process.stderr.write(`${error.stack}\n`);
147
161
  }
148
162
  process.stdout.write("● Using fallback commit generation...\n");
149
- commitState = { proposal: generateFallbackProposal(numstat) };
150
- usedFallback = true;
163
+ const fallbackProposal = generateFallbackProposal(numstat);
164
+ await runSingleCommit(fallbackProposal, { cwd, dryRun: args.dryRun, push: args.push });
165
+ return;
151
166
  }
167
+ }
152
168
 
153
- if (!usedFallback && !commitState.proposal && !commitState.splitProposal) {
169
+ async function completeAgentCommitState(
170
+ commitState: CommitAgentState,
171
+ ctx: CommitExecutionContext & {
172
+ noChangelog: boolean;
173
+ changelogTargets: string[];
174
+ numstat: NumstatEntry[];
175
+ },
176
+ ): Promise<void> {
177
+ let usedFallback = false;
178
+ if (!commitState.proposal && !commitState.splitProposal) {
154
179
  if ($env.PI_COMMIT_NO_FALLBACK?.toLowerCase() !== "true") {
155
180
  process.stdout.write("● Agent did not provide proposal, using fallback...\n");
156
- commitState.proposal = generateFallbackProposal(numstat);
181
+ commitState.proposal = generateFallbackProposal(ctx.numstat);
157
182
  usedFallback = true;
158
183
  }
159
184
  }
160
185
 
161
186
  let updatedChangelogFiles: string[] = [];
162
- if (!args.noChangelog && changelogTargets.length > 0 && !usedFallback) {
187
+ if (!ctx.noChangelog && ctx.changelogTargets.length > 0 && !usedFallback) {
163
188
  if (!commitState.changelogProposal) {
164
- process.stderr.write("Commit agent did not provide changelog entries.\n");
165
- return;
189
+ throw new Error("Commit agent did not provide changelog entries.");
166
190
  }
167
191
  process.stdout.write("● Applying changelog entries...\n");
168
192
  const updated = await applyChangelogProposals({
169
- cwd,
193
+ cwd: ctx.cwd,
170
194
  proposals: commitState.changelogProposal.entries,
171
- dryRun: args.dryRun,
195
+ dryRun: ctx.dryRun,
172
196
  onProgress: message => {
173
197
  process.stdout.write(` ├─ ${message}\n`);
174
198
  },
175
199
  });
176
- updatedChangelogFiles = updated.map(filePath => path.relative(cwd, filePath));
200
+ updatedChangelogFiles = updated.map(filePath => path.relative(ctx.cwd, filePath));
177
201
  if (updated.length > 0) {
178
202
  for (const filePath of updated) {
179
203
  process.stdout.write(` └─ ${filePath}\n`);
@@ -184,21 +208,21 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
184
208
  }
185
209
 
186
210
  if (commitState.proposal) {
187
- await runSingleCommit(commitState.proposal, { cwd, dryRun: args.dryRun, push: args.push });
211
+ await runSingleCommit(commitState.proposal, ctx);
188
212
  return;
189
213
  }
190
214
 
191
215
  if (commitState.splitProposal) {
192
216
  await runSplitCommit(commitState.splitProposal, {
193
- cwd,
194
- dryRun: args.dryRun,
195
- push: args.push,
217
+ cwd: ctx.cwd,
218
+ dryRun: ctx.dryRun,
219
+ push: ctx.push,
196
220
  additionalFiles: updatedChangelogFiles,
197
221
  });
198
222
  return;
199
223
  }
200
224
 
201
- process.stderr.write("Commit agent did not provide a proposal.\n");
225
+ throw new Error("Commit agent did not provide a proposal.");
202
226
  }
203
227
 
204
228
  async function runSingleCommit(proposal: CommitProposal, ctx: CommitExecutionContext): Promise<void> {
@@ -211,6 +235,7 @@ async function runSingleCommit(proposal: CommitProposal, ctx: CommitExecutionCon
211
235
  process.stdout.write(`${commitMessage}\n`);
212
236
  return;
213
237
  }
238
+ process.stdout.write("● Creating commit...\n");
214
239
  await git.commit(ctx.cwd, commitMessage);
215
240
  process.stdout.write("Commit created.\n");
216
241
  if (ctx.push) {
@@ -233,8 +258,7 @@ async function runSplitCommit(
233
258
  const plannedFiles = new Set(plan.commits.flatMap(commit => commit.changes.map(change => change.path)));
234
259
  const missingFiles = stagedFiles.filter(file => !plannedFiles.has(file));
235
260
  if (missingFiles.length > 0) {
236
- process.stderr.write(`Split commit plan missing staged files: ${missingFiles.join(", ")}\n`);
237
- return;
261
+ throw new Error(`Split commit plan missing staged files: ${missingFiles.join(", ")}`);
238
262
  }
239
263
 
240
264
  if (ctx.dryRun) {
@@ -266,6 +290,7 @@ async function runSplitCommit(
266
290
  throw new Error(order.error);
267
291
  }
268
292
 
293
+ process.stdout.write("● Creating split commits...\n");
269
294
  const stagedDiff = await git.diff(ctx.cwd, { cached: true });
270
295
  await git.stage.reset(ctx.cwd);
271
296
  for (const commitIndex of order) {
@@ -11,9 +11,9 @@ interface ConfigSchemaError {
11
11
  }
12
12
 
13
13
  /**
14
- * Module-private cache of (jsonPath, ymlPath) pairs we already migrated this
15
- * process. Prevents `ConfigFile.relocate()` / repeated `tryLoad()` calls from
16
- * re-running the migration over and over on the boot path.
14
+ * Module-private cache of JSON → YAML migrations this process already ran.
15
+ * Prevents `ConfigFile.relocate()` / repeated `tryLoad()` calls from re-running
16
+ * the migration over and over on the boot path.
17
17
  */
18
18
  const migratedPaths = new Set<string>();
19
19
 
@@ -123,6 +123,7 @@ export type LoadResult<T> =
123
123
 
124
124
  export class ConfigFile<T> implements IConfigFile<T> {
125
125
  readonly #basePath: string;
126
+ readonly #yamlFallbackPath: string | null;
126
127
  readonly #jsonMigrationPath: string | null;
127
128
  #cache?: LoadResult<T>;
128
129
  #auxValidate?: (value: T) => void;
@@ -134,13 +135,17 @@ export class ConfigFile<T> implements IConfigFile<T> {
134
135
  ) {
135
136
  this.#basePath = configPath;
136
137
  if (configPath.endsWith(".yml")) {
138
+ this.#yamlFallbackPath = `${configPath.slice(0, -4)}.yaml`;
137
139
  this.#jsonMigrationPath = `${configPath.slice(0, -4)}.json`;
138
140
  } else if (configPath.endsWith(".yaml")) {
141
+ this.#yamlFallbackPath = null;
139
142
  this.#jsonMigrationPath = `${configPath.slice(0, -5)}.json`;
140
143
  } else if (configPath.endsWith(".json") || configPath.endsWith(".jsonc")) {
144
+ this.#yamlFallbackPath = null;
141
145
  // JSON configs are still supported without migration.
142
146
  this.#jsonMigrationPath = null;
143
147
  } else {
148
+ this.#yamlFallbackPath = null;
144
149
  throw new Error(`Invalid config file path: ${configPath}`);
145
150
  }
146
151
  }
@@ -150,9 +155,21 @@ export class ConfigFile<T> implements IConfigFile<T> {
150
155
  * Sync callers (tests, settings init) hit this implicitly via {@link tryLoad}.
151
156
  */
152
157
  #ensureMigrated(): void {
153
- if (this.#jsonMigrationPath) {
154
- migrateJsonToYml(this.#jsonMigrationPath, this.#basePath);
158
+ if (!this.#jsonMigrationPath) return;
159
+ if (this.#yamlFallbackPath && !fs.existsSync(this.#basePath) && fs.existsSync(this.#yamlFallbackPath)) {
160
+ return;
155
161
  }
162
+ migrateJsonToYml(this.#jsonMigrationPath, this.#basePath);
163
+ }
164
+
165
+ #resolveReadPath(): string {
166
+ if (fs.existsSync(this.#basePath)) {
167
+ return this.#basePath;
168
+ }
169
+ if (this.#yamlFallbackPath && fs.existsSync(this.#yamlFallbackPath)) {
170
+ return this.#yamlFallbackPath;
171
+ }
172
+ return this.#basePath;
156
173
  }
157
174
 
158
175
  relocate(configPath?: string): ConfigFile<T> {
@@ -165,7 +182,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
165
182
 
166
183
  getMtimeMs(): number | null {
167
184
  try {
168
- return fs.statSync(this.path()).mtimeMs;
185
+ return fs.statSync(this.#resolveReadPath()).mtimeMs;
169
186
  } catch (err) {
170
187
  if (isEnoent(err)) return null;
171
188
  throw err;
@@ -211,12 +228,13 @@ export class ConfigFile<T> implements IConfigFile<T> {
211
228
  #parseContent(content: string): LoadResult<T> {
212
229
  try {
213
230
  let parsed: unknown;
214
- if (this.#basePath.endsWith(".json") || this.#basePath.endsWith(".jsonc")) {
231
+ const readPath = this.#resolveReadPath();
232
+ if (readPath.endsWith(".json") || readPath.endsWith(".jsonc")) {
215
233
  parsed = JSONC.parse(content);
216
- } else if (this.#basePath.endsWith(".yml") || this.#basePath.endsWith(".yaml")) {
234
+ } else if (readPath.endsWith(".yml") || readPath.endsWith(".yaml")) {
217
235
  parsed = YAML.parse(content);
218
236
  } else {
219
- throw new Error(`Invalid config file path: ${this.#basePath}`);
237
+ throw new Error(`Invalid config file path: ${readPath}`);
220
238
  }
221
239
 
222
240
  const checked = this.schema(parsed);
@@ -255,7 +273,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
255
273
 
256
274
  let content: string;
257
275
  try {
258
- content = fs.readFileSync(this.path(), "utf-8").trim();
276
+ content = fs.readFileSync(this.#resolveReadPath(), "utf-8").trim();
259
277
  } catch (error) {
260
278
  if (isEnoent(error)) {
261
279
  return this.#storeCache({ status: "not-found" });
@@ -275,7 +293,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
275
293
 
276
294
  let content: string;
277
295
  try {
278
- content = (await Bun.file(this.path()).text()).trim();
296
+ content = (await Bun.file(this.#resolveReadPath()).text()).trim();
279
297
  } catch (error) {
280
298
  if (isEnoent(error)) {
281
299
  return this.#storeCache({ status: "not-found" });
@@ -306,7 +324,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
306
324
  }
307
325
 
308
326
  path(): string {
309
- return this.#basePath;
327
+ return this.#resolveReadPath();
310
328
  }
311
329
 
312
330
  invalidate() {
@@ -234,7 +234,7 @@ export interface ProviderDiscoveryState {
234
234
  error?: string;
235
235
  }
236
236
 
237
- /** Result of loading custom models from models.json */
237
+ /** Result of loading custom models config. */
238
238
  interface CustomModelsResult {
239
239
  models?: CustomModelOverlay[];
240
240
  overrides?: Map<string, ProviderOverride>;
@@ -271,7 +271,7 @@ interface CommandApiKeyResolution {
271
271
  value?: string;
272
272
  }
273
273
  /**
274
- * Resolve a models.yml secret/config value to an actual value.
274
+ * Resolve a models.yml/models.yaml secret/config value to an actual value.
275
275
  * `!cmd` runs a shell command and returns trimmed stdout, otherwise env vars are
276
276
  * checked first and the input falls back to a literal value.
277
277
  */
@@ -784,7 +784,7 @@ export class ModelRegistry {
784
784
  }
785
785
 
786
786
  /**
787
- * Reload models from disk (built-in + custom from models.json).
787
+ * Reload models from disk (built-in + custom config).
788
788
  */
789
789
  async refresh(strategy: ModelRefreshStrategy = "online-if-uncached"): Promise<void> {
790
790
  this.#reloadStaticModels();
@@ -889,7 +889,7 @@ export class ModelRegistry {
889
889
  #reloadStaticModels(): void {
890
890
  const currentMtime = this.#modelsConfigFile.getMtimeMs();
891
891
  if (currentMtime !== null && currentMtime === this.#lastStaticLoadMtime) {
892
- // models.json unchanged since last load; reloading would be redundant.
892
+ // Models config unchanged since last load; reloading would be redundant.
893
893
  return;
894
894
  }
895
895
  this.#modelsConfigFile.invalidate();
@@ -913,14 +913,14 @@ export class ModelRegistry {
913
913
  }
914
914
 
915
915
  /**
916
- * Get any error from loading models.json (undefined if no error).
916
+ * Get any error from loading custom models config (undefined if no error).
917
917
  */
918
918
  getError(): ConfigError | undefined {
919
919
  return this.#configError;
920
920
  }
921
921
 
922
922
  #loadModels() {
923
- // Load custom models from models.json first (to know which providers to override)
923
+ // Load custom config first (to know which providers to override).
924
924
  const {
925
925
  models: customModels = [],
926
926
  overrides = new Map(),
@@ -1823,7 +1823,7 @@ export class ModelRegistry {
1823
1823
 
1824
1824
  /**
1825
1825
  * Get all models (built-in + custom).
1826
- * If models.json had errors, returns only built-in models.
1826
+ * If custom config had errors, returns only built-in models.
1827
1827
  */
1828
1828
  getAll(): Model<Api>[] {
1829
1829
  return this.#models;
@@ -1149,20 +1149,32 @@ export function resolveModelOverride(
1149
1149
  modelPatterns: string[],
1150
1150
  modelRegistry: ModelLookupRegistry,
1151
1151
  settings?: Settings,
1152
- ): { model?: Model<Api>; thinkingLevel?: ConfiguredThinkingLevel; explicitThinkingLevel: boolean } {
1152
+ ): {
1153
+ model?: Model<Api>;
1154
+ thinkingLevel?: ConfiguredThinkingLevel;
1155
+ explicitThinkingLevel: boolean;
1156
+ warning?: string;
1157
+ } {
1153
1158
  if (modelPatterns.length === 0) return { explicitThinkingLevel: false };
1154
1159
  const availableModels = modelRegistry.getAvailable();
1155
1160
  const matchPreferences = getModelMatchPreferences(settings);
1161
+ let warning: string | undefined;
1156
1162
  for (const pattern of modelPatterns) {
1157
- const { model, thinkingLevel, explicitThinkingLevel } = resolveModelRoleValue(pattern, availableModels, {
1163
+ const {
1164
+ model,
1165
+ thinkingLevel,
1166
+ explicitThinkingLevel,
1167
+ warning: patternWarning,
1168
+ } = resolveModelRoleValue(pattern, availableModels, {
1158
1169
  settings,
1159
1170
  matchPreferences,
1160
1171
  });
1161
1172
  if (model) {
1162
- return { model, thinkingLevel, explicitThinkingLevel };
1173
+ return { model, thinkingLevel, explicitThinkingLevel, warning: patternWarning };
1163
1174
  }
1175
+ if (!warning && patternWarning) warning = patternWarning;
1164
1176
  }
1165
- return { explicitThinkingLevel: false };
1177
+ return { explicitThinkingLevel: false, warning };
1166
1178
  }
1167
1179
 
1168
1180
  /**
@@ -1176,6 +1188,10 @@ export function resolveModelOverride(
1176
1188
  * `modelRoles.task` pointing at an unqualified id whose only available
1177
1189
  * provider variant has no configured credentials — see #985).
1178
1190
  *
1191
+ * `sessionId` is forwarded to `getApiKey` so that session-sticky OAuth
1192
+ * credentials resolve correctly during the pre-flight auth check. Without it,
1193
+ * providers with multiple OAuth accounts may return `undefined` even though
1194
+ * the credential is usable once the subagent session starts — see #5325.
1179
1195
  * Keyless-by-design providers (llama.cpp, ollama, lm-studio) advertise the
1180
1196
  * `kNoAuth` sentinel from `getApiKey` to signal that they do not require
1181
1197
  * credentials. Those are treated as authenticated here so an explicitly
@@ -1191,18 +1207,20 @@ export async function resolveModelOverrideWithAuthFallback(
1191
1207
  parentActiveModelPattern: string | undefined,
1192
1208
  modelRegistry: ModelLookupRegistry & Pick<ModelRegistry, "getApiKey">,
1193
1209
  settings?: Settings,
1210
+ sessionId?: string,
1194
1211
  ): Promise<{
1195
1212
  model?: Model<Api>;
1196
1213
  thinkingLevel?: ConfiguredThinkingLevel;
1197
1214
  explicitThinkingLevel: boolean;
1198
1215
  authFallbackUsed: boolean;
1216
+ warning?: string;
1199
1217
  }> {
1200
1218
  const primary = resolveModelOverride(modelPatterns, modelRegistry, settings);
1201
1219
  if (!primary.model || !parentActiveModelPattern) {
1202
1220
  return { ...primary, authFallbackUsed: false };
1203
1221
  }
1204
1222
 
1205
- const primaryKey = await modelRegistry.getApiKey(primary.model);
1223
+ const primaryKey = await modelRegistry.getApiKey(primary.model, sessionId);
1206
1224
  if (primaryKey === kNoAuth || isAuthenticated(primaryKey)) {
1207
1225
  return { ...primary, authFallbackUsed: false };
1208
1226
  }
@@ -1214,7 +1232,7 @@ export async function resolveModelOverrideWithAuthFallback(
1214
1232
  if (modelsAreEqual(fallback.model, primary.model)) {
1215
1233
  return { ...primary, authFallbackUsed: false };
1216
1234
  }
1217
- const fallbackKey = await modelRegistry.getApiKey(fallback.model);
1235
+ const fallbackKey = await modelRegistry.getApiKey(fallback.model, sessionId);
1218
1236
  if (!isAuthenticated(fallbackKey)) {
1219
1237
  return { ...primary, authFallbackUsed: false };
1220
1238
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * models.json config file handle and provider configuration validation.
2
+ * Custom model/provider config file handle and validation.
3
3
  */
4
4
 
5
5
  import type { Api, ModelSpec } from "jeopi-ai/types";