@oh-my-pi/pi-coding-agent 13.19.0 → 14.0.3

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 (205) hide show
  1. package/CHANGELOG.md +277 -2
  2. package/package.json +86 -20
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +91 -0
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +83 -125
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -5
  28. package/src/commit/agentic/index.ts +3 -4
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/validation.ts +1 -1
  31. package/src/commit/analysis/conventional.ts +4 -4
  32. package/src/commit/analysis/summary.ts +3 -3
  33. package/src/commit/changelog/generate.ts +4 -4
  34. package/src/commit/map-reduce/map-phase.ts +4 -4
  35. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  36. package/src/commit/pipeline.ts +3 -4
  37. package/src/config/model-registry.ts +17 -3
  38. package/src/config/prompt-templates.ts +44 -226
  39. package/src/config/resolve-config-value.ts +4 -2
  40. package/src/config/settings-schema.ts +54 -2
  41. package/src/config/settings.ts +25 -26
  42. package/src/dap/client.ts +674 -0
  43. package/src/dap/config.ts +150 -0
  44. package/src/dap/defaults.json +211 -0
  45. package/src/dap/index.ts +4 -0
  46. package/src/dap/session.ts +1255 -0
  47. package/src/dap/types.ts +600 -0
  48. package/src/debug/log-viewer.ts +3 -2
  49. package/src/discovery/builtin.ts +1 -2
  50. package/src/discovery/codex.ts +2 -2
  51. package/src/discovery/github.ts +2 -1
  52. package/src/discovery/helpers.ts +2 -2
  53. package/src/discovery/opencode.ts +2 -2
  54. package/src/edit/diff.ts +818 -0
  55. package/src/edit/index.ts +309 -0
  56. package/src/edit/line-hash.ts +67 -0
  57. package/src/edit/modes/chunk.ts +454 -0
  58. package/src/{patch → edit/modes}/hashline.ts +741 -361
  59. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  60. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  61. package/src/{patch → edit}/normalize.ts +97 -76
  62. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  63. package/src/exec/bash-executor.ts +4 -2
  64. package/src/exec/idle-timeout-watchdog.ts +126 -0
  65. package/src/exec/non-interactive-env.ts +5 -0
  66. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +2 -2
  67. package/src/extensibility/custom-commands/bundled/review/index.ts +36 -15
  68. package/src/extensibility/custom-commands/loader.ts +1 -2
  69. package/src/extensibility/custom-tools/loader.ts +34 -11
  70. package/src/extensibility/extensions/loader.ts +9 -4
  71. package/src/extensibility/extensions/runner.ts +24 -1
  72. package/src/extensibility/extensions/types.ts +1 -1
  73. package/src/extensibility/hooks/loader.ts +5 -6
  74. package/src/extensibility/hooks/types.ts +1 -1
  75. package/src/extensibility/plugins/doctor.ts +2 -1
  76. package/src/extensibility/slash-commands.ts +3 -7
  77. package/src/index.ts +2 -1
  78. package/src/internal-urls/docs-index.generated.ts +11 -11
  79. package/src/ipy/executor.ts +58 -17
  80. package/src/ipy/gateway-coordinator.ts +6 -4
  81. package/src/ipy/kernel.ts +45 -22
  82. package/src/ipy/runtime.ts +2 -2
  83. package/src/lsp/client.ts +7 -4
  84. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  85. package/src/lsp/config.ts +20 -4
  86. package/src/lsp/defaults.json +688 -154
  87. package/src/lsp/index.ts +234 -45
  88. package/src/lsp/lspmux.ts +2 -2
  89. package/src/lsp/startup-events.ts +13 -0
  90. package/src/lsp/types.ts +12 -1
  91. package/src/lsp/utils.ts +8 -1
  92. package/src/main.ts +102 -46
  93. package/src/memories/index.ts +4 -5
  94. package/src/modes/acp/acp-agent.ts +563 -163
  95. package/src/modes/acp/acp-event-mapper.ts +9 -1
  96. package/src/modes/acp/acp-mode.ts +4 -2
  97. package/src/modes/components/agent-dashboard.ts +3 -4
  98. package/src/modes/components/diff.ts +6 -7
  99. package/src/modes/components/read-tool-group.ts +6 -12
  100. package/src/modes/components/session-observer-overlay.ts +21 -12
  101. package/src/modes/components/settings-defs.ts +5 -0
  102. package/src/modes/components/tool-execution.ts +1 -1
  103. package/src/modes/components/welcome.ts +1 -1
  104. package/src/modes/controllers/btw-controller.ts +2 -2
  105. package/src/modes/controllers/command-controller.ts +3 -2
  106. package/src/modes/controllers/input-controller.ts +12 -8
  107. package/src/modes/index.ts +20 -2
  108. package/src/modes/interactive-mode.ts +94 -37
  109. package/src/modes/rpc/host-tools.ts +186 -0
  110. package/src/modes/rpc/rpc-client.ts +178 -13
  111. package/src/modes/rpc/rpc-mode.ts +73 -3
  112. package/src/modes/rpc/rpc-types.ts +53 -1
  113. package/src/modes/theme/theme.ts +80 -8
  114. package/src/modes/types.ts +2 -2
  115. package/src/prompts/review-request.md +6 -0
  116. package/src/prompts/system/system-prompt.md +2 -1
  117. package/src/prompts/tools/chunk-edit.md +223 -0
  118. package/src/prompts/tools/debug.md +43 -0
  119. package/src/prompts/tools/grep.md +3 -0
  120. package/src/prompts/tools/lsp.md +5 -5
  121. package/src/prompts/tools/read-chunk.md +17 -0
  122. package/src/prompts/tools/read.md +19 -5
  123. package/src/sdk.ts +190 -154
  124. package/src/secrets/obfuscator.ts +1 -1
  125. package/src/session/agent-session.ts +306 -256
  126. package/src/session/agent-storage.ts +12 -12
  127. package/src/session/compaction/branch-summarization.ts +3 -3
  128. package/src/session/compaction/compaction.ts +5 -6
  129. package/src/session/compaction/utils.ts +3 -3
  130. package/src/session/history-storage.ts +62 -19
  131. package/src/session/messages.ts +3 -3
  132. package/src/session/session-dump-format.ts +203 -0
  133. package/src/session/session-storage.ts +4 -2
  134. package/src/session/streaming-output.ts +1 -1
  135. package/src/session/tool-choice-queue.ts +213 -0
  136. package/src/slash-commands/builtin-registry.ts +56 -8
  137. package/src/ssh/connection-manager.ts +2 -2
  138. package/src/ssh/sshfs-mount.ts +5 -5
  139. package/src/stt/downloader.ts +4 -4
  140. package/src/stt/recorder.ts +4 -4
  141. package/src/stt/transcriber.ts +2 -2
  142. package/src/system-prompt.ts +21 -13
  143. package/src/task/agents.ts +5 -6
  144. package/src/task/commands.ts +2 -5
  145. package/src/task/executor.ts +4 -4
  146. package/src/task/index.ts +3 -4
  147. package/src/task/template.ts +2 -2
  148. package/src/task/worktree.ts +4 -4
  149. package/src/tools/ask.ts +2 -3
  150. package/src/tools/ast-edit.ts +7 -7
  151. package/src/tools/ast-grep.ts +7 -7
  152. package/src/tools/auto-generated-guard.ts +36 -41
  153. package/src/tools/await-tool.ts +2 -2
  154. package/src/tools/bash.ts +5 -23
  155. package/src/tools/browser.ts +4 -5
  156. package/src/tools/calculator.ts +2 -3
  157. package/src/tools/cancel-job.ts +2 -2
  158. package/src/tools/checkpoint.ts +3 -3
  159. package/src/tools/debug.ts +1007 -0
  160. package/src/tools/exit-plan-mode.ts +2 -3
  161. package/src/tools/fetch.ts +67 -3
  162. package/src/tools/find.ts +4 -5
  163. package/src/tools/fs-cache-invalidation.ts +5 -0
  164. package/src/tools/gemini-image.ts +13 -5
  165. package/src/tools/gh.ts +10 -11
  166. package/src/tools/grep.ts +57 -9
  167. package/src/tools/index.ts +44 -22
  168. package/src/tools/inspect-image.ts +4 -4
  169. package/src/tools/output-meta.ts +1 -1
  170. package/src/tools/python.ts +19 -6
  171. package/src/tools/read.ts +198 -67
  172. package/src/tools/render-mermaid.ts +2 -3
  173. package/src/tools/render-utils.ts +20 -6
  174. package/src/tools/renderers.ts +3 -1
  175. package/src/tools/report-tool-issue.ts +80 -0
  176. package/src/tools/resolve.ts +70 -39
  177. package/src/tools/search-tool-bm25.ts +2 -2
  178. package/src/tools/ssh.ts +2 -2
  179. package/src/tools/todo-write.ts +2 -2
  180. package/src/tools/tool-timeouts.ts +1 -0
  181. package/src/tools/write.ts +5 -6
  182. package/src/tui/tree-list.ts +3 -1
  183. package/src/utils/clipboard.ts +80 -0
  184. package/src/utils/commit-message-generator.ts +2 -3
  185. package/src/utils/edit-mode.ts +49 -0
  186. package/src/utils/file-display-mode.ts +6 -5
  187. package/src/utils/file-mentions.ts +8 -7
  188. package/src/utils/git.ts +4 -4
  189. package/src/utils/image-loading.ts +98 -0
  190. package/src/utils/title-generator.ts +2 -3
  191. package/src/utils/tools-manager.ts +6 -6
  192. package/src/web/scrapers/choosealicense.ts +1 -1
  193. package/src/web/search/index.ts +3 -3
  194. package/src/autoresearch/command-initialize.md +0 -34
  195. package/src/patch/diff.ts +0 -433
  196. package/src/patch/index.ts +0 -888
  197. package/src/patch/parser.ts +0 -532
  198. package/src/patch/types.ts +0 -292
  199. package/src/prompts/agents/oracle.md +0 -77
  200. package/src/tools/pending-action.ts +0 -49
  201. package/src/utils/child-process.ts +0 -88
  202. package/src/utils/frontmatter.ts +0 -117
  203. package/src/utils/image-input.ts +0 -274
  204. package/src/utils/mime.ts +0 -53
  205. package/src/utils/prompt-format.ts +0 -170
@@ -8,14 +8,15 @@ import type { ToolDefinition } from "../../extensibility/extensions";
8
8
  import type { Theme } from "../../modes/theme/theme";
9
9
  import { replaceTabs, truncateToWidth } from "../../tools/render-utils";
10
10
  import * as git from "../../utils/git";
11
- import { getAutoresearchFingerprintMismatchError, pathMatchesContractPath } from "../contract";
12
- import { getCurrentAutoresearchBranch, parseWorkDirDirtyPaths } from "../git";
11
+ import { applyAutoresearchContractToExperimentState } from "../apply-contract-to-state";
12
+ import { loadAutoresearchScriptSnapshot, pathMatchesContractPath, readAutoresearchContract } from "../contract";
13
+ import { computeRunModifiedPaths, getCurrentAutoresearchBranch, parseWorkDirDirtyPathsWithStatus } from "../git";
13
14
  import {
14
- AUTORESEARCH_COMMITTABLE_FILES,
15
15
  formatNum,
16
16
  inferMetricUnitFromName,
17
17
  isAutoresearchCommittableFile,
18
18
  isAutoresearchLocalStatePath,
19
+ isAutoresearchShCommand,
19
20
  isBetter,
20
21
  mergeAsi,
21
22
  readPendingRunSummary,
@@ -61,7 +62,14 @@ const logExperimentSchema = Type.Object({
61
62
  ),
62
63
  force: Type.Optional(
63
64
  Type.Boolean({
64
- description: "Allow introducing new secondary metrics.",
65
+ description:
66
+ "When true: skip ASI field requirements and allow keeping a run whose primary metric regressed versus the best kept run.",
67
+ }),
68
+ ),
69
+ skip_restore: Type.Optional(
70
+ Type.Boolean({
71
+ description:
72
+ "When true and status is discard/crash/checks_failed: skip reverting the working tree to HEAD. Useful when the experiment did not modify tracked files or you want to preserve the current state.",
65
73
  }),
66
74
  ),
67
75
  asi: Type.Optional(
@@ -71,11 +79,6 @@ const logExperimentSchema = Type.Object({
71
79
  ),
72
80
  });
73
81
 
74
- interface PreservedFile {
75
- content: Buffer;
76
- path: string;
77
- }
78
-
79
82
  interface KeepCommitResult {
80
83
  error?: string;
81
84
  note?: string;
@@ -102,10 +105,26 @@ export function createLogExperimentTool(
102
105
  const runtime = options.getRuntime(ctx);
103
106
  const state = runtime.state;
104
107
  const workDir = resolveWorkDir(ctx.cwd);
105
- const fingerprintError = getAutoresearchFingerprintMismatchError(state.segmentFingerprint, workDir);
106
- if (fingerprintError) {
108
+
109
+ const contractResult = readAutoresearchContract(workDir);
110
+ const scriptSnapshot = loadAutoresearchScriptSnapshot(workDir);
111
+ const contractErrors = [...contractResult.errors, ...scriptSnapshot.errors];
112
+ if (contractErrors.length > 0) {
107
113
  return {
108
- content: [{ type: "text", text: `Error: ${fingerprintError}` }],
114
+ content: [{ type: "text", text: `Error: ${contractErrors.join(" ")}` }],
115
+ };
116
+ }
117
+ const benchmarkForSync = contractResult.contract.benchmark;
118
+ if (benchmarkForSync.command && !isAutoresearchShCommand(benchmarkForSync.command)) {
119
+ return {
120
+ content: [
121
+ {
122
+ type: "text",
123
+ text:
124
+ "Error: Benchmark.command in autoresearch.md must invoke `autoresearch.sh` directly before logging. " +
125
+ "Fix autoresearch.md or move the workload into autoresearch.sh.",
126
+ },
127
+ ],
109
128
  };
110
129
  }
111
130
 
@@ -116,6 +135,10 @@ export function createLogExperimentTool(
116
135
  content: [{ type: "text", text: "Error: no unlogged run is available. Run run_experiment first." }],
117
136
  };
118
137
  }
138
+
139
+ applyAutoresearchContractToExperimentState(contractResult.contract, state);
140
+ const logPreamble =
141
+ "Refreshed session fields from autoresearch.md before logging (benchmark, scope, constraints).\n\n";
119
142
  runtime.lastRunSummary = pendingRun;
120
143
  runtime.lastRunAsi = pendingRun.parsedAsi;
121
144
  runtime.lastRunChecks =
@@ -170,22 +193,20 @@ export function createLogExperimentTool(
170
193
  };
171
194
  }
172
195
 
196
+ const forceLoose = params.force === true;
173
197
  const secondaryMetrics = buildSecondaryMetrics(params.metrics, pendingRun.parsedMetrics, state.metricName);
174
- const validationError = validateSecondaryMetrics(state, secondaryMetrics, params.force ?? false);
175
- if (validationError) {
176
- return {
177
- content: [{ type: "text", text: `Error: ${validationError}` }],
178
- };
179
- }
180
198
 
181
199
  const mergedAsi = mergeAsi(runtime.lastRunAsi, sanitizeAsi(params.asi));
182
- const asiValidationError = validateAsiRequirements(mergedAsi, params.status);
183
- if (asiValidationError) {
184
- return {
185
- content: [{ type: "text", text: `Error: ${asiValidationError}` }],
186
- };
200
+ if (!forceLoose) {
201
+ const asiValidationError = validateAsiRequirements(mergedAsi, params.status);
202
+ if (asiValidationError) {
203
+ return {
204
+ content: [{ type: "text", text: `Error: ${asiValidationError}` }],
205
+ };
206
+ }
187
207
  }
188
208
 
209
+ const preRunDirtyPaths = pendingRun.preRunDirtyPaths;
189
210
  let keepScopeValidation: { committablePaths: string[] } | undefined;
190
211
  if (params.status === "keep") {
191
212
  const scopeValidation = await validateKeepPaths(options, workDir, state);
@@ -196,6 +217,7 @@ export function createLogExperimentTool(
196
217
  }
197
218
  const currentBestMetric = findBestKeptMetric(state.results, state.currentSegment, state.bestDirection);
198
219
  if (
220
+ !forceLoose &&
199
221
  currentBestMetric !== null &&
200
222
  params.metric !== currentBestMetric &&
201
223
  !isBetter(params.metric, currentBestMetric, state.bestDirection)
@@ -250,8 +272,8 @@ export function createLogExperimentTool(
250
272
  };
251
273
  }
252
274
  gitNote = commitResult.note ?? null;
253
- } else {
254
- const revertResult = await revertFailedExperiment(options, workDir);
275
+ } else if (!params.skip_restore) {
276
+ const revertResult = await revertFailedExperiment(options, workDir, preRunDirtyPaths);
255
277
  if (revertResult.error) {
256
278
  return {
257
279
  content: [{ type: "text", text: `Error: ${revertResult.error}` }],
@@ -309,7 +331,7 @@ export function createLogExperimentTool(
309
331
  runtime.lastAutoResumePendingRunNumber = null;
310
332
 
311
333
  const currentSegmentRuns = currentResults(state.results, state.currentSegment).length;
312
- const text = buildLogText(state, experiment, currentSegmentRuns, wallClockSeconds, gitNote);
334
+ const text = logPreamble + buildLogText(state, experiment, currentSegmentRuns, wallClockSeconds, gitNote);
313
335
  if (state.maxExperiments !== null && currentSegmentRuns >= state.maxExperiments) {
314
336
  runtime.autoresearchMode = false;
315
337
  options.pi.appendEntry(
@@ -432,23 +454,6 @@ export function validateAsiRequirements(asi: ASIData | undefined, status: Experi
432
454
  return null;
433
455
  }
434
456
 
435
- function validateSecondaryMetrics(state: ExperimentState, metrics: NumericMetricMap, force: boolean): string | null {
436
- if (state.secondaryMetrics.length === 0) return null;
437
- const knownNames = new Set(state.secondaryMetrics.map(metric => metric.name));
438
- const providedNames = new Set(Object.keys(metrics));
439
-
440
- const missing = [...knownNames].filter(name => !providedNames.has(name));
441
- if (missing.length > 0) {
442
- return `missing secondary metrics: ${missing.join(", ")}`;
443
- }
444
-
445
- const newMetrics = [...providedNames].filter(name => !knownNames.has(name));
446
- if (newMetrics.length > 0 && !force) {
447
- return `new secondary metrics require force=true: ${newMetrics.join(", ")}`;
448
- }
449
- return null;
450
- }
451
-
452
457
  function registerSecondaryMetrics(state: ExperimentState, metrics: NumericMetricMap): void {
453
458
  for (const name of Object.keys(metrics)) {
454
459
  if (state.secondaryMetrics.some(metric => metric.name === name)) continue;
@@ -547,36 +552,11 @@ async function commitKeptExperiment(
547
552
  async function revertFailedExperiment(
548
553
  options: AutoresearchToolFactoryOptions,
549
554
  workDir: string,
555
+ preRunDirtyPaths: string[],
550
556
  ): Promise<KeepCommitResult> {
551
- const preservedFiles = preserveAutoresearchFiles(workDir);
552
- try {
553
- await git.restore(workDir, { files: ["."], source: "HEAD", staged: true, worktree: true });
554
- } catch (err) {
555
- restoreAutoresearchFiles(preservedFiles);
556
- return {
557
- error: `git restore failed: ${err instanceof Error ? err.message : String(err)}`,
558
- };
559
- }
560
- try {
561
- await git.clean(workDir, { paths: ["."] });
562
- } catch (err) {
563
- restoreAutoresearchFiles(preservedFiles);
564
- return {
565
- error: `git clean failed: ${err instanceof Error ? err.message : String(err)}`,
566
- };
567
- }
568
- try {
569
- await git.clean(workDir, { ignoredOnly: true, paths: ["."] });
570
- } catch (err) {
571
- restoreAutoresearchFiles(preservedFiles);
572
- return {
573
- error: `git clean -X failed: ${err instanceof Error ? err.message : String(err)}`,
574
- };
575
- }
576
- restoreAutoresearchFiles(preservedFiles);
577
- let dirtyStatus = "";
557
+ let statusText: string;
578
558
  try {
579
- dirtyStatus = await git.status(workDir, {
559
+ statusText = await git.status(workDir, {
580
560
  pathspecs: ["."],
581
561
  porcelainV1: true,
582
562
  untrackedFiles: "all",
@@ -584,45 +564,37 @@ async function revertFailedExperiment(
584
564
  });
585
565
  } catch (err) {
586
566
  return {
587
- error: `git status failed after cleanup: ${err instanceof Error ? err.message : String(err)}`,
567
+ error: `git status failed: ${err instanceof Error ? err.message : String(err)}`,
588
568
  };
589
569
  }
570
+
590
571
  const workDirPrefix = await readGitWorkDirPrefix(options, workDir);
591
- const remainingDirtyPaths = parseWorkDirDirtyPaths(dirtyStatus, workDirPrefix).filter(
592
- relativePath => !isAutoresearchLocalStatePath(relativePath),
593
- );
594
- if (remainingDirtyPaths.length > 0) {
595
- return {
596
- error:
597
- "Autoresearch cleanup left the worktree dirty. Resolve these paths before continuing: " +
598
- remainingDirtyPaths.join(", "),
599
- };
572
+ const { tracked, untracked } = computeRunModifiedPaths(preRunDirtyPaths, statusText, workDirPrefix);
573
+ const totalReverted = tracked.length + untracked.length;
574
+ if (totalReverted === 0) {
575
+ return { note: "nothing to revert" };
600
576
  }
601
- return { note: "reverted changes" };
602
- }
603
577
 
604
- function preserveAutoresearchFiles(workDir: string): PreservedFile[] {
605
- const files: PreservedFile[] = [];
606
- for (const relativePath of [...AUTORESEARCH_COMMITTABLE_FILES, "autoresearch.jsonl"]) {
607
- const absolutePath = path.join(workDir, relativePath);
608
- if (!fs.existsSync(absolutePath)) continue;
609
- files.push({
610
- content: fs.readFileSync(absolutePath),
611
- path: absolutePath,
612
- });
613
- }
614
- const localStateDir = path.join(workDir, ".autoresearch");
615
- if (fs.existsSync(localStateDir)) {
616
- collectDirectoryFiles(localStateDir, files);
578
+ if (tracked.length > 0) {
579
+ try {
580
+ await git.restore(workDir, { files: tracked, source: "HEAD", staged: true, worktree: true });
581
+ } catch (err) {
582
+ return {
583
+ error: `git restore failed: ${err instanceof Error ? err.message : String(err)}`,
584
+ };
585
+ }
617
586
  }
618
- return files;
619
- }
620
587
 
621
- function restoreAutoresearchFiles(files: PreservedFile[]): void {
622
- for (const file of files) {
623
- fs.mkdirSync(path.dirname(file.path), { recursive: true });
624
- fs.writeFileSync(file.path, file.content);
588
+ for (const filePath of untracked) {
589
+ const absolutePath = path.join(workDir, filePath);
590
+ try {
591
+ fs.rmSync(absolutePath, { force: true, recursive: true });
592
+ } catch {
593
+ // Best-effort removal of untracked files
594
+ }
625
595
  }
596
+
597
+ return { note: `reverted ${totalReverted} file${totalReverted === 1 ? "" : "s"}` };
626
598
  }
627
599
 
628
600
  function mergeStdoutStderr(result: { stderr: string; stdout: string }): string {
@@ -652,40 +624,26 @@ async function validateKeepPaths(
652
624
 
653
625
  const workDirPrefix = await readGitWorkDirPrefix(options, workDir);
654
626
  const committablePaths: string[] = [];
655
- for (const normalizedPath of parseWorkDirDirtyPaths(statusText, workDirPrefix)) {
656
- if (isAutoresearchLocalStatePath(normalizedPath)) {
627
+ for (const entry of parseWorkDirDirtyPathsWithStatus(statusText, workDirPrefix)) {
628
+ if (isAutoresearchLocalStatePath(entry.path)) {
657
629
  continue;
658
630
  }
659
- if (isAutoresearchCommittableFile(normalizedPath)) {
660
- committablePaths.push(normalizedPath);
631
+ if (isAutoresearchCommittableFile(entry.path)) {
632
+ committablePaths.push(entry.path);
661
633
  continue;
662
634
  }
663
- if (state.offLimits.some(spec => pathMatchesContractPath(normalizedPath, spec))) {
664
- return `cannot keep this run because ${normalizedPath} is listed under Off Limits in autoresearch.md`;
635
+ if (state.offLimits.some(spec => pathMatchesContractPath(entry.path, spec))) {
636
+ return `cannot keep this run because ${entry.path} is listed under Off Limits in autoresearch.md`;
665
637
  }
666
- if (!state.scopePaths.some(spec => pathMatchesContractPath(normalizedPath, spec))) {
667
- return `cannot keep this run because ${normalizedPath} is outside Files in Scope`;
638
+ if (!state.scopePaths.some(spec => pathMatchesContractPath(entry.path, spec))) {
639
+ return `cannot keep this run because ${entry.path} is outside Files in Scope`;
668
640
  }
669
- committablePaths.push(normalizedPath);
641
+ committablePaths.push(entry.path);
670
642
  }
671
643
 
672
644
  return { committablePaths };
673
645
  }
674
646
 
675
- function collectDirectoryFiles(directory: string, files: PreservedFile[]): void {
676
- for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
677
- const absolutePath = path.join(directory, entry.name);
678
- if (entry.isDirectory()) {
679
- collectDirectoryFiles(absolutePath, files);
680
- continue;
681
- }
682
- files.push({
683
- content: fs.readFileSync(absolutePath),
684
- path: absolutePath,
685
- });
686
- }
687
- }
688
-
689
647
  async function updateRunMetadata(
690
648
  runDirectory: string | null,
691
649
  metadata: {
@@ -8,7 +8,8 @@ import type { ToolDefinition } from "../../extensibility/extensions";
8
8
  import type { Theme } from "../../modes/theme/theme";
9
9
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, truncateTail } from "../../session/streaming-output";
10
10
  import { replaceTabs, shortenPath, truncateToWidth } from "../../tools/render-utils";
11
- import { getAutoresearchFingerprintMismatchError } from "../contract";
11
+ import * as git from "../../utils/git";
12
+ import { parseWorkDirDirtyPaths } from "../git";
12
13
  import {
13
14
  EXPERIMENT_MAX_BYTES,
14
15
  EXPERIMENT_MAX_LINES,
@@ -16,6 +17,7 @@ import {
16
17
  formatNum,
17
18
  getAutoresearchRunDirectory,
18
19
  getNextAutoresearchRunNumber,
20
+ isAutoresearchLocalStatePath,
19
21
  isAutoresearchShCommand,
20
22
  killTree,
21
23
  parseAsiLines,
@@ -40,6 +42,12 @@ const runExperimentSchema = Type.Object({
40
42
  description: "Timeout in seconds for autoresearch.checks.sh. Defaults to 300.",
41
43
  }),
42
44
  ),
45
+ force: Type.Optional(
46
+ Type.Boolean({
47
+ description:
48
+ "When true, allow a command that differs from the segment benchmark command and skip the rule that autoresearch.sh must be invoked directly when that script exists.",
49
+ }),
50
+ ),
43
51
  });
44
52
 
45
53
  interface ProcessExecutionResult {
@@ -87,14 +95,9 @@ export function createRunExperimentTool(
87
95
  const workDir = resolveWorkDir(ctx.cwd);
88
96
  const checksPath = path.join(workDir, "autoresearch.checks.sh");
89
97
  const autoresearchScriptPath = path.join(workDir, "autoresearch.sh");
90
- const fingerprintError = getAutoresearchFingerprintMismatchError(state.segmentFingerprint, workDir);
91
- if (fingerprintError) {
92
- return {
93
- content: [{ type: "text", text: `Error: ${fingerprintError}` }],
94
- };
95
- }
96
98
 
97
- if (state.benchmarkCommand && params.command.trim() !== state.benchmarkCommand) {
99
+ const forceCommand = params.force === true;
100
+ if (!forceCommand && state.benchmarkCommand && params.command.trim() !== state.benchmarkCommand) {
98
101
  return {
99
102
  content: [
100
103
  {
@@ -107,7 +110,7 @@ export function createRunExperimentTool(
107
110
  };
108
111
  }
109
112
 
110
- if (fs.existsSync(autoresearchScriptPath) && !isAutoresearchShCommand(params.command)) {
113
+ if (!forceCommand && fs.existsSync(autoresearchScriptPath) && !isAutoresearchShCommand(params.command)) {
111
114
  return {
112
115
  content: [
113
116
  {
@@ -156,6 +159,17 @@ export function createRunExperimentTool(
156
159
  const checksLogPath = path.join(runDirectory, "checks.log");
157
160
  const runJsonPath = path.join(runDirectory, "run.json");
158
161
  await fs.promises.mkdir(runDirectory, { recursive: true });
162
+
163
+ const preRunStatus = await git.status(workDir, {
164
+ porcelainV1: true,
165
+ untrackedFiles: "all",
166
+ z: true,
167
+ });
168
+ const workDirPrefix = await git.show.prefix(workDir);
169
+ const preRunDirtyPaths = parseWorkDirDirtyPaths(preRunStatus, workDirPrefix).filter(
170
+ p => !isAutoresearchLocalStatePath(p),
171
+ );
172
+
159
173
  runtime.lastRunChecks = null;
160
174
  runtime.lastRunDuration = null;
161
175
  runtime.lastRunAsi = null;
@@ -171,6 +185,7 @@ export function createRunExperimentTool(
171
185
  benchmarkLogPath,
172
186
  checksLogPath,
173
187
  command: params.command,
188
+ preRunDirtyPaths,
174
189
  startedAt: new Date().toISOString(),
175
190
  },
176
191
  null,
@@ -287,6 +302,7 @@ export function createRunExperimentTool(
287
302
  parsedAsi,
288
303
  metricName: state.metricName,
289
304
  metricUnit: state.metricUnit,
305
+ preRunDirtyPaths,
290
306
  truncation: llmTruncation.truncated ? llmTruncation : undefined,
291
307
  fullOutputPath: execution.logPath,
292
308
  };
@@ -300,6 +316,7 @@ export function createRunExperimentTool(
300
316
  parsedMetrics,
301
317
  parsedPrimary,
302
318
  passed: resultDetails.passed,
319
+ preRunDirtyPaths,
303
320
  runDirectory,
304
321
  runNumber,
305
322
  };
@@ -329,6 +346,7 @@ export function createRunExperimentTool(
329
346
  parsedMetrics,
330
347
  parsedPrimary,
331
348
  parsedAsi,
349
+ preRunDirtyPaths,
332
350
  truncation: resultDetails.truncation,
333
351
  fullOutputPath: resultDetails.fullOutputPath,
334
352
  },
@@ -337,8 +355,28 @@ export function createRunExperimentTool(
337
355
  ),
338
356
  );
339
357
 
358
+ const commandWarnings: string[] = [];
359
+ if (forceCommand) {
360
+ if (state.benchmarkCommand && params.command.trim() !== state.benchmarkCommand) {
361
+ commandWarnings.push(
362
+ `Warning: command override (force=true). Segment benchmark is ${state.benchmarkCommand}; ran ${params.command}.`,
363
+ );
364
+ }
365
+ if (fs.existsSync(autoresearchScriptPath) && !isAutoresearchShCommand(params.command)) {
366
+ commandWarnings.push(
367
+ "Warning: autoresearch.sh exists but the command was not a direct autoresearch.sh invocation (force=true).",
368
+ );
369
+ }
370
+ }
371
+ const warningPrefix = commandWarnings.length > 0 ? `${commandWarnings.join("\n")}\n\n` : "";
372
+
340
373
  return {
341
- content: [{ type: "text", text: buildRunText(resultDetails, llmTruncation.content, state.bestMetric) }],
374
+ content: [
375
+ {
376
+ type: "text",
377
+ text: warningPrefix + buildRunText(resultDetails, llmTruncation.content, state.bestMetric),
378
+ },
379
+ ],
342
380
  details: resultDetails,
343
381
  };
344
382
  },
@@ -64,7 +64,6 @@ export interface ExperimentState {
64
64
  scopePaths: string[];
65
65
  offLimits: string[];
66
66
  constraints: string[];
67
- segmentFingerprint: string | null;
68
67
  }
69
68
 
70
69
  export interface RunExperimentProgressDetails {
@@ -96,6 +95,7 @@ export interface RunDetails {
96
95
  parsedAsi: ASIData | null;
97
96
  metricName: string;
98
97
  metricUnit: string;
98
+ preRunDirtyPaths: string[];
99
99
  truncation?: TruncationResult;
100
100
  fullOutputPath?: string;
101
101
  }
@@ -122,6 +122,7 @@ export interface PendingRunSummary {
122
122
  parsedMetrics: NumericMetricMap | null;
123
123
  parsedPrimary: number | null;
124
124
  passed: boolean;
125
+ preRunDirtyPaths: string[];
125
126
  runDirectory: string;
126
127
  runNumber: number;
127
128
  }
@@ -165,7 +166,6 @@ export interface AutoresearchJsonConfigEntry {
165
166
  scopePaths?: string[];
166
167
  offLimits?: string[];
167
168
  constraints?: string[];
168
- segmentFingerprint?: string;
169
169
  }
170
170
 
171
171
  export interface AutoresearchJsonRunEntry {
@@ -114,8 +114,10 @@ async function loadImpl<T>(
114
114
  const results = await Promise.all(
115
115
  providers.map(async provider => {
116
116
  try {
117
- const result = await logger.timeAsync(`capability:${capability.id}:${provider.id}`, () =>
118
- provider.load(ctx),
117
+ const result = await logger.time(
118
+ `capability:${capability.id}:${provider.id}`,
119
+ provider.load.bind(provider),
120
+ ctx,
119
121
  );
120
122
  return { provider, result };
121
123
  } catch (error) {
@@ -4,12 +4,11 @@
4
4
  import * as fs from "node:fs";
5
5
  import * as path from "node:path";
6
6
  import type { ImageContent } from "@oh-my-pi/pi-ai";
7
- import { getProjectDir, isEnoent } from "@oh-my-pi/pi-utils";
7
+ import { getProjectDir, isEnoent, readImageMetadata } from "@oh-my-pi/pi-utils";
8
8
  import chalk from "chalk";
9
9
  import { resolveReadPath } from "../tools/path-utils";
10
10
  import { formatBytes } from "../tools/render-utils";
11
11
  import { formatDimensionNote, resizeImage } from "../utils/image-resize";
12
- import { detectSupportedImageMimeTypeFromFile } from "../utils/mime";
13
12
 
14
13
  // Keep CLI startup responsive and avoid OOM when users pass huge files.
15
14
  // If a file exceeds these limits, we include it as a path-only <file/> block.
@@ -42,7 +41,8 @@ export async function processFileArguments(fileArgs: string[], options?: Process
42
41
  process.exit(1);
43
42
  }
44
43
 
45
- const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
44
+ const imageMetadata = await readImageMetadata(absolutePath);
45
+ const mimeType = imageMetadata?.mimeType;
46
46
  const maxBytes = mimeType ? MAX_CLI_IMAGE_BYTES : MAX_CLI_TEXT_BYTES;
47
47
  if (stat.size > maxBytes) {
48
48
  console.error(
@@ -4,7 +4,7 @@
4
4
  * Handles `omp grep` subcommand for testing grep tool on Windows.
5
5
  */
6
6
  import * as path from "node:path";
7
- import { grep } from "@oh-my-pi/pi-natives";
7
+ import { GrepOutputMode, grep } from "@oh-my-pi/pi-natives";
8
8
  import { APP_NAME } from "@oh-my-pi/pi-utils";
9
9
  import chalk from "chalk";
10
10
 
@@ -14,7 +14,7 @@ export interface GrepCommandArgs {
14
14
  glob?: string;
15
15
  limit: number;
16
16
  context: number;
17
- mode: "content" | "filesWithMatches" | "count";
17
+ mode: GrepOutputMode;
18
18
  gitignore: boolean;
19
19
  }
20
20
 
@@ -32,7 +32,7 @@ export function parseGrepArgs(args: string[]): GrepCommandArgs | undefined {
32
32
  path: ".",
33
33
  limit: 20,
34
34
  context: 2,
35
- mode: "content",
35
+ mode: GrepOutputMode.Content,
36
36
  gitignore: true,
37
37
  };
38
38
 
@@ -47,9 +47,9 @@ export function parseGrepArgs(args: string[]): GrepCommandArgs | undefined {
47
47
  } else if (arg === "--context" || arg === "-C") {
48
48
  result.context = parseInt(args[++i], 10);
49
49
  } else if (arg === "--files" || arg === "-f") {
50
- result.mode = "filesWithMatches";
50
+ result.mode = GrepOutputMode.FilesWithMatches;
51
51
  } else if (arg === "--count" || arg === "-c") {
52
- result.mode = "count";
52
+ result.mode = GrepOutputMode.Count;
53
53
  } else if (arg === "--no-gitignore") {
54
54
  result.gitignore = false;
55
55
  } else if (!arg.startsWith("-")) {
@@ -89,7 +89,7 @@ export async function runGrepCommand(cmd: GrepCommandArgs): Promise<void> {
89
89
  glob: cmd.glob,
90
90
  mode: cmd.mode,
91
91
  maxCount: cmd.limit,
92
- context: cmd.mode === "content" ? cmd.context : undefined,
92
+ context: cmd.mode === GrepOutputMode.Content ? cmd.context : undefined,
93
93
  hidden: true,
94
94
  gitignore: cmd.gitignore,
95
95
  });
@@ -105,7 +105,7 @@ export async function runGrepCommand(cmd: GrepCommandArgs): Promise<void> {
105
105
  for (const match of result.matches) {
106
106
  const displayPath = match.path.replace(/\\/g, "/");
107
107
 
108
- if (cmd.mode === "content") {
108
+ if (cmd.mode === GrepOutputMode.Content) {
109
109
  if (match.contextBefore) {
110
110
  for (const ctx of match.contextBefore) {
111
111
  console.log(chalk.dim(`${displayPath}-${ctx.lineNumber}- ${ctx.line}`));
@@ -118,7 +118,7 @@ export async function runGrepCommand(cmd: GrepCommandArgs): Promise<void> {
118
118
  }
119
119
  }
120
120
  console.log("");
121
- } else if (cmd.mode === "count") {
121
+ } else if (cmd.mode === GrepOutputMode.Count) {
122
122
  console.log(`${chalk.cyan(displayPath)}: ${match.matchCount ?? 0} matches`);
123
123
  } else {
124
124
  console.log(chalk.cyan(displayPath));
@@ -0,0 +1,78 @@
1
+ /**
2
+ * CLI handler for `omp grievances` — view reported tool issues from auto-QA.
3
+ */
4
+ import { Database } from "bun:sqlite";
5
+ import chalk from "chalk";
6
+ import { getAutoQaDbPath } from "../tools/report-tool-issue";
7
+
8
+ interface GrievanceRow {
9
+ id: number;
10
+ model: string;
11
+ version: string;
12
+ tool: string;
13
+ report: string;
14
+ }
15
+
16
+ export interface ListGrievancesOptions {
17
+ limit: number;
18
+ tool?: string;
19
+ json: boolean;
20
+ }
21
+
22
+ function openDb(): Database | null {
23
+ try {
24
+ const db = new Database(getAutoQaDbPath(), { readonly: true });
25
+ return db;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ export async function listGrievances(options: ListGrievancesOptions): Promise<void> {
32
+ const db = openDb();
33
+ if (!db) {
34
+ if (options.json) {
35
+ console.log("[]");
36
+ } else {
37
+ console.log(
38
+ chalk.dim("No grievances database found. Enable auto-QA with PI_AUTO_QA=1 or the dev.autoqa setting."),
39
+ );
40
+ }
41
+ return;
42
+ }
43
+
44
+ try {
45
+ let rows: GrievanceRow[];
46
+ if (options.tool) {
47
+ rows = db
48
+ .prepare("SELECT id, model, version, tool, report FROM grievances WHERE tool = ? ORDER BY id DESC LIMIT ?")
49
+ .all(options.tool, options.limit) as GrievanceRow[];
50
+ } else {
51
+ rows = db
52
+ .prepare("SELECT id, model, version, tool, report FROM grievances ORDER BY id DESC LIMIT ?")
53
+ .all(options.limit) as GrievanceRow[];
54
+ }
55
+
56
+ if (options.json) {
57
+ console.log(JSON.stringify(rows, null, 2));
58
+ return;
59
+ }
60
+
61
+ if (rows.length === 0) {
62
+ console.log(chalk.dim("No grievances recorded yet."));
63
+ return;
64
+ }
65
+
66
+ for (const row of rows) {
67
+ console.log(
68
+ `${chalk.dim(`#${row.id}`)} ${chalk.cyan(row.tool)} ${chalk.dim(`(${row.model} v${row.version})`)}`,
69
+ );
70
+ console.log(` ${row.report}`);
71
+ console.log();
72
+ }
73
+
74
+ console.log(chalk.dim(`Showing ${rows.length} most recent${options.tool ? ` for ${options.tool}` : ""}`));
75
+ } finally {
76
+ db.close();
77
+ }
78
+ }