@oh-my-pi/pi-coding-agent 16.2.6 → 16.2.8

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 (78) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/cli.js +2781 -2660
  3. package/dist/types/cli/bench-cli.d.ts +3 -3
  4. package/dist/types/commands/bench.d.ts +1 -1
  5. package/dist/types/config/service-tier.d.ts +39 -24
  6. package/dist/types/config/settings-schema.d.ts +92 -36
  7. package/dist/types/edit/hashline/filesystem.d.ts +1 -0
  8. package/dist/types/mcp/config-writer.d.ts +48 -0
  9. package/dist/types/mcp/types.d.ts +3 -0
  10. package/dist/types/session/agent-session.d.ts +33 -13
  11. package/dist/types/session/messages.d.ts +1 -1
  12. package/dist/types/session/session-context.d.ts +2 -7
  13. package/dist/types/session/session-entries.d.ts +2 -2
  14. package/dist/types/session/session-manager.d.ts +2 -2
  15. package/dist/types/session/settings-stream-fn.d.ts +2 -2
  16. package/dist/types/system-prompt.test.d.ts +1 -0
  17. package/dist/types/task/executor.d.ts +6 -6
  18. package/dist/types/task/types.d.ts +6 -0
  19. package/dist/types/task/worktree.d.ts +32 -6
  20. package/dist/types/tiny/title-client.d.ts +5 -1
  21. package/dist/types/tools/index.d.ts +3 -3
  22. package/dist/types/tools/output-schema-validator.d.ts +10 -0
  23. package/dist/types/utils/git.d.ts +17 -0
  24. package/package.json +12 -12
  25. package/src/cli/bench-cli.ts +19 -12
  26. package/src/cli/tiny-models-cli.ts +18 -4
  27. package/src/commands/bench.ts +3 -3
  28. package/src/config/mcp-schema.json +10 -1
  29. package/src/config/service-tier.ts +85 -56
  30. package/src/config/settings-schema.ts +81 -37
  31. package/src/config/settings.ts +47 -0
  32. package/src/discovery/builtin-rules/go-add-cleanup.md +32 -0
  33. package/src/discovery/builtin-rules/go-bench-loop.md +36 -0
  34. package/src/discovery/builtin-rules/go-exp-promoted.md +39 -0
  35. package/src/discovery/builtin-rules/go-ioutil.md +36 -0
  36. package/src/discovery/builtin-rules/go-join-hostport.md +29 -0
  37. package/src/discovery/builtin-rules/go-new-expr.md +44 -0
  38. package/src/discovery/builtin-rules/go-rand-v2.md +40 -0
  39. package/src/discovery/builtin-rules/go-range-int.md +45 -0
  40. package/src/discovery/builtin-rules/index.ts +16 -0
  41. package/src/edit/hashline/filesystem.ts +12 -0
  42. package/src/eval/agent-bridge.ts +4 -2
  43. package/src/internal-urls/docs-index.generated.txt +1 -1
  44. package/src/main.ts +1 -1
  45. package/src/mcp/config-writer.ts +121 -0
  46. package/src/mcp/config.ts +10 -6
  47. package/src/mcp/types.ts +3 -0
  48. package/src/modes/components/extensions/extension-dashboard.ts +46 -0
  49. package/src/modes/components/extensions/state-manager.ts +24 -3
  50. package/src/modes/controllers/event-controller.ts +7 -0
  51. package/src/modes/controllers/tool-args-reveal.ts +1 -1
  52. package/src/modes/utils/transcript-render-helpers.ts +2 -2
  53. package/src/prompts/tools/bash.md +1 -4
  54. package/src/sdk.ts +12 -11
  55. package/src/session/agent-session.ts +294 -82
  56. package/src/session/messages.ts +1 -1
  57. package/src/session/session-context.ts +17 -6
  58. package/src/session/session-entries.ts +2 -2
  59. package/src/session/session-manager.ts +9 -2
  60. package/src/session/settings-stream-fn.ts +12 -2
  61. package/src/slash-commands/builtin-registry.ts +2 -10
  62. package/src/system-prompt.test.ts +158 -0
  63. package/src/system-prompt.ts +69 -26
  64. package/src/task/executor.ts +23 -16
  65. package/src/task/index.ts +7 -5
  66. package/src/task/isolation-runner.ts +15 -1
  67. package/src/task/types.ts +6 -0
  68. package/src/task/worktree.ts +219 -38
  69. package/src/tiny/title-client.ts +19 -13
  70. package/src/tools/grep.ts +19 -1
  71. package/src/tools/index.ts +3 -3
  72. package/src/tools/irc.ts +9 -3
  73. package/src/tools/output-schema-validator.ts +38 -0
  74. package/src/tools/read.ts +28 -28
  75. package/src/tools/yield.ts +52 -15
  76. package/src/utils/file-mentions.ts +10 -1
  77. package/src/utils/git.ts +38 -0
  78. package/src/web/search/providers/duckduckgo.ts +17 -3
@@ -98,6 +98,16 @@ function parseYieldType(value: unknown): string | string[] | undefined {
98
98
  throw new Error("type must be a string or non-empty array of strings");
99
99
  }
100
100
 
101
+ /**
102
+ * Render an incremental yield's `type: [...]` labels as a quoted, comma-separated list for
103
+ * model-facing retry messages — keeps the failed section labelled even when the yield carried
104
+ * multiple labels at once.
105
+ */
106
+ function formatYieldLabels(labels: readonly string[]): string {
107
+ if (labels.length === 0) return '""';
108
+ return labels.map(label => `"${label}"`).join(", ");
109
+ }
110
+
101
111
  /**
102
112
  * Expand a plain-object `data` schema into a strict union that ALSO accepts each
103
113
  * top-level section value (and array element) on its own. Agents that yield
@@ -202,10 +212,12 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
202
212
  lenientArgValidation = true;
203
213
 
204
214
  readonly #validate?: (value: unknown) => JsonSchemaValidationResult;
215
+ readonly #validateSection?: ReadonlyMap<string, (value: unknown) => JsonSchemaValidationResult>;
205
216
  #schemaValidationFailures = 0;
206
217
 
207
218
  constructor(session: ToolSession) {
208
219
  let validate: ((value: unknown) => JsonSchemaValidationResult) | undefined;
220
+ let validateSection: ReadonlyMap<string, (value: unknown) => JsonSchemaValidationResult> | undefined;
209
221
  let parameters: TSchema;
210
222
 
211
223
  try {
@@ -217,6 +229,7 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
217
229
  } = buildOutputValidator(session.outputSchema);
218
230
  if (validator) {
219
231
  validate = value => validator.validate(value);
232
+ validateSection = validator.validateSection;
220
233
  }
221
234
 
222
235
  const schemaHint = formatSchema(normalizedSchema ?? session.outputSchema);
@@ -266,6 +279,7 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
266
279
  }
267
280
 
268
281
  this.#validate = validate;
282
+ this.#validateSection = validateSection;
269
283
  this.parameters = parameters;
270
284
  }
271
285
 
@@ -307,22 +321,25 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
307
321
  if (data === null) {
308
322
  throw new Error("data is required when yield indicates success");
309
323
  }
310
- if (this.#validate && !isIncremental) {
311
- const parsed = this.#validate(data);
312
- if (!parsed.success) {
313
- this.#schemaValidationFailures++;
314
- if (this.#schemaValidationFailures <= MAX_SCHEMA_RETRIES) {
315
- const remaining = MAX_SCHEMA_RETRIES - this.#schemaValidationFailures;
316
- const retryHint =
317
- remaining > 0
318
- ? ` Call yield again with the corrected shape — ${remaining} retry attempt(s) remain before the schema constraint is dropped.`
319
- : " Call yield again with the corrected shape — this is the final retry before the schema constraint is dropped.";
320
- throw new Error(
321
- `Output does not match schema: ${formatAllValidationIssues(parsed.issues)}.${retryHint}`,
322
- );
323
- }
324
- schemaValidationOverridden = true;
324
+ const sectionFailure = isIncremental
325
+ ? this.#validateIncrementalSection(yieldType as string[], data)
326
+ : this.#validate
327
+ ? this.#validate(data)
328
+ : undefined;
329
+ if (sectionFailure && !sectionFailure.success) {
330
+ this.#schemaValidationFailures++;
331
+ if (this.#schemaValidationFailures <= MAX_SCHEMA_RETRIES) {
332
+ const remaining = MAX_SCHEMA_RETRIES - this.#schemaValidationFailures;
333
+ const retryHint =
334
+ remaining > 0
335
+ ? ` Call yield again with the corrected shape — ${remaining} retry attempt(s) remain before the schema constraint is dropped.`
336
+ : " Call yield again with the corrected shape — this is the final retry before the schema constraint is dropped.";
337
+ const scope = isIncremental ? `Section ${formatYieldLabels(yieldType as string[])}` : "Output";
338
+ throw new Error(
339
+ `${scope} does not match schema: ${formatAllValidationIssues(sectionFailure.issues)}.${retryHint}`,
340
+ );
325
341
  }
342
+ schemaValidationOverridden = true;
326
343
  }
327
344
  }
328
345
 
@@ -344,6 +361,26 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
344
361
  },
345
362
  };
346
363
  }
364
+
365
+ /**
366
+ * Validate the `data` payload of an incremental yield (`type: ["<label>", …]`) against
367
+ * the matching property's sub-validator. Returns the first failure across all known labels,
368
+ * or `undefined` when no label is recognised (user-defined section labels stay loose) or
369
+ * when all known labels accept the value. Lets the model see the same retry feedback that
370
+ * the terminal-yield path already produces, instead of leaking the mismatch through to
371
+ * the parent's post-mortem `schema_violation`.
372
+ */
373
+ #validateIncrementalSection(labels: string[], data: unknown): JsonSchemaValidationResult | undefined {
374
+ const subValidators = this.#validateSection;
375
+ if (!subValidators || subValidators.size === 0) return undefined;
376
+ for (const label of labels) {
377
+ const sub = subValidators.get(label);
378
+ if (!sub) continue;
379
+ const parsed = sub(data);
380
+ if (!parsed.success) return parsed;
381
+ }
382
+ return undefined;
383
+ }
347
384
  }
348
385
 
349
386
  // Register subprocess tool handler for extraction + termination.
@@ -10,7 +10,7 @@ import path from "node:path";
10
10
  import { formatHashlineHeader, formatNumberedLines, type SnapshotStore } from "@oh-my-pi/hashline";
11
11
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
12
12
  import type { ImageContent } from "@oh-my-pi/pi-ai";
13
- import { formatAge, formatBytes, readImageMetadata } from "@oh-my-pi/pi-utils";
13
+ import { formatAge, formatBytes, isProbablyBinary, readImageMetadata } from "@oh-my-pi/pi-utils";
14
14
  import { canonicalSnapshotKey } from "../edit/file-snapshot-store";
15
15
  import { normalizeToLF } from "../edit/normalize";
16
16
  import type { FileMentionMessage } from "../session/messages";
@@ -257,6 +257,15 @@ export async function generateFileMentionMessages(
257
257
  });
258
258
  continue;
259
259
  }
260
+ if (await isProbablyBinary(absolutePath)) {
261
+ files.push({
262
+ path: resolvedPath,
263
+ content: `(skipped auto-read: binary file, ${formatBytes(stat.size)})`,
264
+ byteSize: stat.size,
265
+ skippedReason: "binary",
266
+ });
267
+ continue;
268
+ }
260
269
 
261
270
  const content = await Bun.file(absolutePath).text();
262
271
  const snapshotStore = options?.useHashLines ? options.snapshotStore : undefined;
package/src/utils/git.ts CHANGED
@@ -74,8 +74,20 @@ export interface StatusOptions {
74
74
  readonly z?: boolean;
75
75
  }
76
76
 
77
+ export interface CommitAuthor {
78
+ readonly date?: string;
79
+ readonly email: string;
80
+ readonly name: string;
81
+ }
82
+
83
+ export interface CommitDetails {
84
+ readonly author: CommitAuthor;
85
+ readonly message: string;
86
+ }
87
+
77
88
  export interface CommitOptions {
78
89
  readonly allowEmpty?: boolean;
90
+ readonly author?: CommitAuthor;
79
91
  readonly files?: readonly string[];
80
92
  readonly signal?: AbortSignal;
81
93
  }
@@ -91,6 +103,7 @@ export interface PatchOptions {
91
103
  readonly cached?: boolean;
92
104
  readonly check?: boolean;
93
105
  readonly env?: Record<string, string | undefined>;
106
+ readonly threeWay?: boolean;
94
107
  readonly signal?: AbortSignal;
95
108
  }
96
109
 
@@ -359,6 +372,7 @@ function buildApplyArgs(patchPath: string, options: PatchOptions): string[] {
359
372
  const args = ["apply"];
360
373
  if (options.check) args.push("--check");
361
374
  if (options.cached) args.push("--cached");
375
+ if (options.threeWay) args.push("--3way");
362
376
  args.push("--binary", patchPath);
363
377
  return args;
364
378
  }
@@ -1122,6 +1136,10 @@ export const stage = {
1122
1136
  /** Create a commit with the given message (passed via stdin). */
1123
1137
  export async function commit(cwd: string, message: string, options: CommitOptions = {}): Promise<GitCommandResult> {
1124
1138
  const args = ["commit", "-F", "-"];
1139
+ if (options.author) {
1140
+ args.push(`--author=${options.author.name} <${options.author.email}>`);
1141
+ if (options.author.date) args.push(`--date=${options.author.date}`);
1142
+ }
1125
1143
  if (options.allowEmpty) args.push("--allow-empty");
1126
1144
  if (options.files?.length) args.push("--", ...options.files);
1127
1145
  return runChecked(cwd, args, { signal: options.signal, stdin: message });
@@ -1195,6 +1213,19 @@ export const show = Object.assign(
1195
1213
  },
1196
1214
  );
1197
1215
 
1216
+ /** Read commit message and author metadata for replay/rewrite flows. */
1217
+ export async function commitDetails(cwd: string, revision: string, signal?: AbortSignal): Promise<CommitDetails> {
1218
+ const raw = await runText(cwd, ["show", "-s", "--format=%an%x00%ae%x00%aI%x00%B", revision], {
1219
+ readOnly: true,
1220
+ signal,
1221
+ });
1222
+ const [name = "", email = "", date = "", ...messageParts] = raw.split("\0");
1223
+ return {
1224
+ author: { date, email, name },
1225
+ message: messageParts.join("\0").replace(/\n$/, ""),
1226
+ };
1227
+ }
1228
+
1198
1229
  // ════════════════════════════════════════════════════════════════════════════
1199
1230
  // API: log
1200
1231
  // ════════════════════════════════════════════════════════════════════════════
@@ -1212,6 +1243,13 @@ export const log = {
1212
1243
  },
1213
1244
  };
1214
1245
 
1246
+ export const revList = {
1247
+ /** Commits in `base..head`, oldest first. */
1248
+ async range(cwd: string, base: string, head: string, signal?: AbortSignal): Promise<string[]> {
1249
+ return splitLines(await runText(cwd, ["rev-list", "--reverse", `${base}..${head}`], { readOnly: true, signal }));
1250
+ },
1251
+ };
1252
+
1215
1253
  // ════════════════════════════════════════════════════════════════════════════
1216
1254
  // API: branch
1217
1255
  // ════════════════════════════════════════════════════════════════════════════
@@ -35,7 +35,7 @@ const RECENCY_TO_DDG_DF: Record<NonNullable<SearchParams["recency"]>, string> =
35
35
  * the orchestrator can fall through to the next provider with context.
36
36
  */
37
37
  const BROWSER_USER_AGENT =
38
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
38
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
39
39
 
40
40
  interface ParsedResult {
41
41
  title: string;
@@ -130,15 +130,29 @@ async function callDuckDuckGoHtml(params: SearchParams): Promise<string> {
130
130
  const form = new URLSearchParams({ q: params.query, kl: "us-en" });
131
131
  const df = params.recency ? RECENCY_TO_DDG_DF[params.recency] : undefined;
132
132
  if (df) form.set("df", df);
133
+ // Add b: "" parameter as specified in the browser fetch template to match real browser form submission
134
+ form.set("b", "");
133
135
 
134
136
  const response = await (params.fetch ?? fetch)(DUCKDUCKGO_HTML_URL, {
135
137
  method: "POST",
136
138
  body: form.toString(),
137
139
  headers: {
140
+ Accept:
141
+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
142
+ "Accept-Language": "en,en-US;q=0.9",
143
+ "Cache-Control": "max-age=0",
138
144
  "Content-Type": "application/x-www-form-urlencoded",
145
+ Priority: "u=0, i",
146
+ "Sec-Ch-Ua": '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"',
147
+ "Sec-Ch-Ua-Mobile": "?0",
148
+ "Sec-Ch-Ua-Platform": '"macOS"',
149
+ "Sec-Fetch-Dest": "document",
150
+ "Sec-Fetch-Mode": "navigate",
151
+ "Sec-Fetch-Site": "same-origin",
152
+ "Sec-Fetch-User": "?1",
153
+ "Upgrade-Insecure-Requests": "1",
139
154
  "User-Agent": BROWSER_USER_AGENT,
140
- Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
141
- "Accept-Language": "en-US,en;q=0.5",
155
+ Referer: "https://html.duckduckgo.com/",
142
156
  },
143
157
  signal: withHardTimeout(params.signal),
144
158
  });