@oh-my-pi/pi-coding-agent 13.18.0 → 14.0.2

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 (235) hide show
  1. package/CHANGELOG.md +316 -1
  2. package/package.json +86 -24
  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 +116 -30
  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 +123 -178
  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 -8
  28. package/src/commit/agentic/index.ts +22 -26
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/tools/git-file-diff.ts +3 -6
  31. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  32. package/src/commit/agentic/tools/git-overview.ts +6 -9
  33. package/src/commit/agentic/tools/index.ts +6 -8
  34. package/src/commit/agentic/tools/propose-commit.ts +4 -7
  35. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  36. package/src/commit/agentic/tools/split-commit.ts +4 -4
  37. package/src/commit/agentic/validation.ts +1 -1
  38. package/src/commit/analysis/conventional.ts +4 -4
  39. package/src/commit/analysis/summary.ts +3 -3
  40. package/src/commit/changelog/generate.ts +4 -4
  41. package/src/commit/changelog/index.ts +5 -9
  42. package/src/commit/map-reduce/map-phase.ts +4 -4
  43. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  44. package/src/commit/pipeline.ts +13 -16
  45. package/src/config/keybindings.ts +7 -6
  46. package/src/config/prompt-templates.ts +44 -226
  47. package/src/config/resolve-config-value.ts +4 -2
  48. package/src/config/settings-schema.ts +98 -2
  49. package/src/config/settings.ts +25 -26
  50. package/src/dap/client.ts +674 -0
  51. package/src/dap/config.ts +150 -0
  52. package/src/dap/defaults.json +211 -0
  53. package/src/dap/index.ts +4 -0
  54. package/src/dap/session.ts +1255 -0
  55. package/src/dap/types.ts +600 -0
  56. package/src/debug/log-viewer.ts +3 -2
  57. package/src/discovery/builtin.ts +1 -2
  58. package/src/discovery/codex.ts +2 -2
  59. package/src/discovery/github.ts +2 -1
  60. package/src/discovery/helpers.ts +2 -2
  61. package/src/discovery/opencode.ts +2 -2
  62. package/src/edit/diff.ts +818 -0
  63. package/src/edit/index.ts +309 -0
  64. package/src/edit/line-hash.ts +67 -0
  65. package/src/edit/modes/chunk.ts +454 -0
  66. package/src/{patch → edit/modes}/hashline.ts +741 -361
  67. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  68. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  69. package/src/{patch → edit}/normalize.ts +97 -76
  70. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  71. package/src/exec/bash-executor.ts +4 -2
  72. package/src/exec/idle-timeout-watchdog.ts +126 -0
  73. package/src/exec/non-interactive-env.ts +5 -0
  74. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
  75. package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
  76. package/src/extensibility/custom-commands/loader.ts +1 -2
  77. package/src/extensibility/custom-tools/loader.ts +34 -11
  78. package/src/extensibility/custom-tools/types.ts +1 -1
  79. package/src/extensibility/extensions/loader.ts +9 -4
  80. package/src/extensibility/extensions/runner.ts +24 -1
  81. package/src/extensibility/extensions/types.ts +4 -2
  82. package/src/extensibility/hooks/loader.ts +5 -6
  83. package/src/extensibility/hooks/types.ts +2 -2
  84. package/src/extensibility/plugins/doctor.ts +2 -1
  85. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
  86. package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
  87. package/src/extensibility/slash-commands.ts +3 -7
  88. package/src/index.ts +3 -1
  89. package/src/internal-urls/docs-index.generated.ts +11 -11
  90. package/src/ipy/executor.ts +58 -17
  91. package/src/ipy/gateway-coordinator.ts +6 -4
  92. package/src/ipy/kernel.ts +45 -22
  93. package/src/ipy/runtime.ts +2 -2
  94. package/src/lsp/client.ts +7 -4
  95. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  96. package/src/lsp/config.ts +2 -2
  97. package/src/lsp/defaults.json +688 -154
  98. package/src/lsp/index.ts +234 -45
  99. package/src/lsp/lspmux.ts +2 -2
  100. package/src/lsp/startup-events.ts +13 -0
  101. package/src/lsp/types.ts +12 -1
  102. package/src/lsp/utils.ts +8 -1
  103. package/src/main.ts +125 -47
  104. package/src/memories/index.ts +4 -5
  105. package/src/modes/acp/acp-agent.ts +563 -163
  106. package/src/modes/acp/acp-event-mapper.ts +9 -1
  107. package/src/modes/acp/acp-mode.ts +4 -2
  108. package/src/modes/components/agent-dashboard.ts +3 -4
  109. package/src/modes/components/diff.ts +6 -7
  110. package/src/modes/components/footer.ts +9 -29
  111. package/src/modes/components/hook-editor.ts +3 -3
  112. package/src/modes/components/hook-selector.ts +6 -1
  113. package/src/modes/components/read-tool-group.ts +6 -12
  114. package/src/modes/components/session-observer-overlay.ts +472 -0
  115. package/src/modes/components/settings-defs.ts +24 -0
  116. package/src/modes/components/status-line.ts +15 -61
  117. package/src/modes/components/tool-execution.ts +1 -1
  118. package/src/modes/components/welcome.ts +1 -1
  119. package/src/modes/controllers/btw-controller.ts +2 -2
  120. package/src/modes/controllers/command-controller.ts +4 -2
  121. package/src/modes/controllers/event-controller.ts +59 -2
  122. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  123. package/src/modes/controllers/input-controller.ts +15 -8
  124. package/src/modes/controllers/selector-controller.ts +26 -0
  125. package/src/modes/index.ts +20 -2
  126. package/src/modes/interactive-mode.ts +278 -69
  127. package/src/modes/rpc/host-tools.ts +186 -0
  128. package/src/modes/rpc/rpc-client.ts +178 -13
  129. package/src/modes/rpc/rpc-mode.ts +73 -3
  130. package/src/modes/rpc/rpc-types.ts +53 -1
  131. package/src/modes/session-observer-registry.ts +146 -0
  132. package/src/modes/shared.ts +0 -42
  133. package/src/modes/theme/theme.ts +80 -8
  134. package/src/modes/types.ts +4 -2
  135. package/src/modes/utils/keybinding-matchers.ts +9 -0
  136. package/src/prompts/system/custom-system-prompt.md +5 -0
  137. package/src/prompts/system/system-prompt.md +8 -1
  138. package/src/prompts/tools/chunk-edit.md +219 -0
  139. package/src/prompts/tools/debug.md +43 -0
  140. package/src/prompts/tools/grep.md +3 -0
  141. package/src/prompts/tools/lsp.md +5 -5
  142. package/src/prompts/tools/read-chunk.md +17 -0
  143. package/src/prompts/tools/read.md +19 -5
  144. package/src/sdk.ts +216 -165
  145. package/src/secrets/index.ts +1 -1
  146. package/src/secrets/obfuscator.ts +25 -17
  147. package/src/session/agent-session.ts +381 -286
  148. package/src/session/agent-storage.ts +12 -12
  149. package/src/session/compaction/branch-summarization.ts +3 -3
  150. package/src/session/compaction/compaction.ts +5 -6
  151. package/src/session/compaction/utils.ts +3 -3
  152. package/src/session/history-storage.ts +62 -19
  153. package/src/session/messages.ts +3 -3
  154. package/src/session/session-dump-format.ts +203 -0
  155. package/src/session/session-manager.ts +15 -5
  156. package/src/session/session-storage.ts +4 -2
  157. package/src/session/streaming-output.ts +1 -1
  158. package/src/session/tool-choice-queue.ts +213 -0
  159. package/src/slash-commands/builtin-registry.ts +56 -8
  160. package/src/ssh/connection-manager.ts +2 -2
  161. package/src/ssh/sshfs-mount.ts +5 -5
  162. package/src/stt/downloader.ts +4 -4
  163. package/src/stt/recorder.ts +4 -4
  164. package/src/stt/transcriber.ts +2 -2
  165. package/src/system-prompt.ts +25 -13
  166. package/src/task/agents.ts +5 -6
  167. package/src/task/commands.ts +2 -5
  168. package/src/task/executor.ts +32 -4
  169. package/src/task/index.ts +91 -82
  170. package/src/task/template.ts +2 -2
  171. package/src/task/types.ts +25 -0
  172. package/src/task/worktree.ts +131 -149
  173. package/src/tools/ask.ts +2 -3
  174. package/src/tools/ast-edit.ts +7 -7
  175. package/src/tools/ast-grep.ts +7 -7
  176. package/src/tools/auto-generated-guard.ts +36 -41
  177. package/src/tools/await-tool.ts +2 -2
  178. package/src/tools/bash.ts +5 -23
  179. package/src/tools/browser.ts +4 -5
  180. package/src/tools/calculator.ts +2 -3
  181. package/src/tools/cancel-job.ts +2 -2
  182. package/src/tools/checkpoint.ts +3 -3
  183. package/src/tools/debug.ts +1007 -0
  184. package/src/tools/exit-plan-mode.ts +3 -3
  185. package/src/tools/fetch.ts +67 -3
  186. package/src/tools/find.ts +4 -5
  187. package/src/tools/fs-cache-invalidation.ts +5 -0
  188. package/src/tools/gemini-image.ts +13 -5
  189. package/src/tools/gh.ts +130 -308
  190. package/src/tools/grep.ts +57 -9
  191. package/src/tools/index.ts +44 -22
  192. package/src/tools/inspect-image.ts +4 -4
  193. package/src/tools/output-meta.ts +1 -1
  194. package/src/tools/python.ts +19 -6
  195. package/src/tools/read.ts +211 -146
  196. package/src/tools/render-mermaid.ts +2 -3
  197. package/src/tools/render-utils.ts +20 -6
  198. package/src/tools/renderers.ts +3 -1
  199. package/src/tools/report-tool-issue.ts +80 -0
  200. package/src/tools/resolve.ts +70 -39
  201. package/src/tools/search-tool-bm25.ts +2 -2
  202. package/src/tools/ssh.ts +2 -2
  203. package/src/tools/todo-write.ts +2 -2
  204. package/src/tools/tool-timeouts.ts +1 -0
  205. package/src/tools/write.ts +5 -6
  206. package/src/tui/tree-list.ts +3 -1
  207. package/src/utils/clipboard.ts +80 -0
  208. package/src/utils/commit-message-generator.ts +2 -3
  209. package/src/utils/edit-mode.ts +49 -0
  210. package/src/utils/external-editor.ts +11 -5
  211. package/src/utils/file-display-mode.ts +6 -5
  212. package/src/utils/file-mentions.ts +8 -7
  213. package/src/utils/git.ts +1400 -0
  214. package/src/utils/image-loading.ts +98 -0
  215. package/src/utils/title-generator.ts +2 -3
  216. package/src/utils/tools-manager.ts +6 -6
  217. package/src/web/scrapers/choosealicense.ts +1 -1
  218. package/src/web/search/index.ts +3 -3
  219. package/src/web/search/render.ts +6 -4
  220. package/src/autoresearch/command-initialize.md +0 -34
  221. package/src/commit/git/errors.ts +0 -9
  222. package/src/commit/git/index.ts +0 -210
  223. package/src/commit/git/operations.ts +0 -54
  224. package/src/patch/diff.ts +0 -433
  225. package/src/patch/index.ts +0 -888
  226. package/src/patch/parser.ts +0 -532
  227. package/src/patch/types.ts +0 -292
  228. package/src/prompts/agents/oracle.md +0 -77
  229. package/src/tools/gh-cli.ts +0 -125
  230. package/src/tools/pending-action.ts +0 -49
  231. package/src/utils/child-process.ts +0 -88
  232. package/src/utils/frontmatter.ts +0 -117
  233. package/src/utils/image-input.ts +0 -274
  234. package/src/utils/mime.ts +0 -53
  235. package/src/utils/prompt-format.ts +0 -170
@@ -1,4 +1,3 @@
1
- import * as crypto from "node:crypto";
2
1
  import * as fs from "node:fs";
3
2
  import * as path from "node:path";
4
3
  import type { AutoresearchBenchmarkContract, AutoresearchContract, MetricDirection } from "./types";
@@ -76,49 +75,6 @@ export function validateAutoresearchContract(contract: AutoresearchContract): st
76
75
  return errors;
77
76
  }
78
77
 
79
- export function buildAutoresearchSegmentFingerprint(
80
- contract: AutoresearchContract,
81
- scripts: {
82
- benchmarkScript: string;
83
- checksScript: string | null;
84
- },
85
- ): string {
86
- const payload = {
87
- benchmark: contract.benchmark,
88
- scopePaths: contract.scopePaths,
89
- offLimits: contract.offLimits,
90
- constraints: contract.constraints,
91
- scripts,
92
- };
93
- return crypto.createHash("sha256").update(JSON.stringify(payload)).digest("hex");
94
- }
95
-
96
- export function getAutoresearchFingerprintMismatchError(
97
- stateFingerprint: string | null,
98
- workDir: string,
99
- ): string | null {
100
- if (!stateFingerprint) {
101
- return "The current segment has no fingerprint metadata. Re-run init_experiment before continuing.";
102
- }
103
-
104
- const contractResult = readAutoresearchContract(workDir);
105
- const scriptSnapshot = loadAutoresearchScriptSnapshot(workDir);
106
- const errors = [...contractResult.errors, ...scriptSnapshot.errors];
107
- if (errors.length > 0) {
108
- return `${errors.join(" ")} Re-run init_experiment after fixing the workspace contract.`;
109
- }
110
-
111
- const currentFingerprint = buildAutoresearchSegmentFingerprint(contractResult.contract, {
112
- benchmarkScript: scriptSnapshot.benchmarkScript,
113
- checksScript: scriptSnapshot.checksScript,
114
- });
115
- if (currentFingerprint === stateFingerprint) {
116
- return null;
117
- }
118
-
119
- return "autoresearch.md, autoresearch.sh, or autoresearch.checks.sh changed since the current segment was initialized. Re-run init_experiment before continuing.";
120
- }
121
-
122
78
  export function loadAutoresearchScriptSnapshot(workDir: string): AutoresearchScriptSnapshot {
123
79
  const benchmarkScriptPath = path.join(workDir, "autoresearch.sh");
124
80
  const checksScriptPath = path.join(workDir, "autoresearch.checks.sh");
@@ -1,6 +1,5 @@
1
- import { matchesKey, Text, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
1
+ import { matchesKey, replaceTabs, Text, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
2
2
  import type { Theme } from "../modes/theme/theme";
3
- import { replaceTabs } from "../tools/render-utils";
4
3
  import { formatElapsed, formatNum, isBetter } from "./helpers";
5
4
  import { currentResults, findBaselineMetric, findBaselineRunNumber, findBaselineSecondary } from "./state";
6
5
  import type { AutoresearchRuntime, DashboardController, ExperimentResult, ExperimentState } from "./types";
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI } from "../extensibility/extensions";
2
+ import * as git from "../utils/git";
2
3
  import { isAutoresearchLocalStatePath, normalizeAutoresearchPath } from "./helpers";
3
4
 
4
5
  const AUTORESEARCH_BRANCH_PREFIX = "autoresearch/";
@@ -17,9 +18,8 @@ export interface EnsureAutoresearchBranchSuccess {
17
18
 
18
19
  export type EnsureAutoresearchBranchResult = EnsureAutoresearchBranchFailure | EnsureAutoresearchBranchSuccess;
19
20
 
20
- export async function getCurrentAutoresearchBranch(api: ExtensionAPI, workDir: string): Promise<string | null> {
21
- const currentBranchResult = await api.exec("git", ["branch", "--show-current"], { cwd: workDir, timeout: 5_000 });
22
- const currentBranch = currentBranchResult.stdout.trim();
21
+ export async function getCurrentAutoresearchBranch(_api: ExtensionAPI, workDir: string): Promise<string | null> {
22
+ const currentBranch = (await git.branch.current(workDir)) ?? "";
23
23
  return currentBranch.startsWith(AUTORESEARCH_BRANCH_PREFIX) ? currentBranch : null;
24
24
  }
25
25
 
@@ -28,28 +28,30 @@ export async function ensureAutoresearchBranch(
28
28
  workDir: string,
29
29
  goal: string | null,
30
30
  ): Promise<EnsureAutoresearchBranchResult> {
31
- const repoRootResult = await api.exec("git", ["rev-parse", "--show-toplevel"], { cwd: workDir, timeout: 5_000 });
32
- if (repoRootResult.code !== 0) {
31
+ const repoRoot = await git.repo.root(workDir);
32
+ if (!repoRoot) {
33
33
  return {
34
34
  error: "Autoresearch requires a git repository so it can isolate experiments and revert failed runs safely.",
35
35
  ok: false,
36
36
  };
37
37
  }
38
- const repoRoot = repoRootResult.stdout.trim() || workDir;
39
38
 
40
- const dirtyPathsResult = await api.exec("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], {
41
- cwd: repoRoot,
42
- timeout: 5_000,
43
- });
44
- if (dirtyPathsResult.code !== 0) {
39
+ let dirtyPathsOutput: string;
40
+ try {
41
+ dirtyPathsOutput = await git.status(repoRoot, {
42
+ porcelainV1: true,
43
+ untrackedFiles: "all",
44
+ z: true,
45
+ });
46
+ } catch (err) {
45
47
  return {
46
- error: `Unable to inspect git status before starting autoresearch: ${mergeStdoutStderr(dirtyPathsResult).trim() || `exit ${dirtyPathsResult.code}`}`,
48
+ error: `Unable to inspect git status before starting autoresearch: ${err instanceof Error ? err.message : String(err)}`,
47
49
  ok: false,
48
50
  };
49
51
  }
50
52
 
51
53
  const workDirPrefix = await readGitWorkDirPrefix(api, workDir);
52
- const unsafeDirtyPaths = collectUnsafeDirtyPaths(dirtyPathsResult.stdout, workDirPrefix);
54
+ const unsafeDirtyPaths = collectUnsafeDirtyPaths(dirtyPathsOutput, workDirPrefix);
53
55
  const currentBranch = await getCurrentAutoresearchBranch(api, workDir);
54
56
  if (currentBranch) {
55
57
  if (unsafeDirtyPaths.length > 0) {
@@ -66,12 +68,11 @@ export async function ensureAutoresearchBranch(
66
68
  }
67
69
 
68
70
  const branchName = await allocateBranchName(api, workDir, goal);
69
- const checkoutResult = await api.exec("git", ["checkout", "-b", branchName], { cwd: workDir, timeout: 10_000 });
70
- if (checkoutResult.code !== 0) {
71
+ try {
72
+ await git.branch.checkoutNew(workDir, branchName);
73
+ } catch (err) {
71
74
  return {
72
- error:
73
- `Failed to create autoresearch branch ${branchName}: ` +
74
- `${mergeStdoutStderr(checkoutResult).trim() || `exit ${checkoutResult.code}`}`,
75
+ error: `Failed to create autoresearch branch ${branchName}: ${err instanceof Error ? err.message : String(err)}`,
75
76
  ok: false,
76
77
  };
77
78
  }
@@ -109,11 +110,12 @@ export function relativizeGitPathToWorkDir(repoRelativePath: string, workDirPref
109
110
  }
110
111
 
111
112
  async function readGitWorkDirPrefix(api: ExtensionAPI, workDir: string): Promise<string> {
112
- const prefixResult = await api.exec("git", ["rev-parse", "--show-prefix"], { cwd: workDir, timeout: 5_000 });
113
- if (prefixResult.code !== 0) {
113
+ void api;
114
+ try {
115
+ return await git.show.prefix(workDir);
116
+ } catch {
114
117
  return "";
115
118
  }
116
- return prefixResult.stdout.trim();
117
119
  }
118
120
 
119
121
  export function parseDirtyPaths(statusOutput: string): string[] {
@@ -180,11 +182,8 @@ async function allocateBranchName(api: ExtensionAPI, workDir: string, goal: stri
180
182
  }
181
183
 
182
184
  async function branchExists(api: ExtensionAPI, workDir: string, branchName: string): Promise<boolean> {
183
- const result = await api.exec("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], {
184
- cwd: workDir,
185
- timeout: 5_000,
186
- });
187
- return result.code === 0;
185
+ void api;
186
+ return git.ref.exists(workDir, `refs/heads/${branchName}`);
188
187
  }
189
188
 
190
189
  function slugifyGoal(goal: string | null): string {
@@ -204,10 +203,6 @@ function currentDateStamp(): string {
204
203
  return `${year}${month}${day}`;
205
204
  }
206
205
 
207
- function mergeStdoutStderr(result: { stderr: string; stdout: string }): string {
208
- return `${result.stdout}${result.stderr}`;
209
- }
210
-
211
206
  function addDirtyPath(paths: Set<string>, rawPath: string): void {
212
207
  const normalizedPath = normalizeStatusPath(rawPath);
213
208
  if (normalizedPath.length === 0) return;
@@ -241,3 +236,94 @@ function collectUnsafeDirtyPaths(statusOutput: string, workDirPrefix: string): s
241
236
  }
242
237
  return unsafeDirtyPaths;
243
238
  }
239
+
240
+ export interface DirtyPathEntry {
241
+ path: string;
242
+ untracked: boolean;
243
+ }
244
+
245
+ export function parseDirtyPathsWithStatus(statusOutput: string): DirtyPathEntry[] {
246
+ if (statusOutput.includes("\0")) {
247
+ return parseDirtyPathsNulWithStatus(statusOutput);
248
+ }
249
+ return parseDirtyPathsLinesWithStatus(statusOutput);
250
+ }
251
+
252
+ function parseDirtyPathsNulWithStatus(statusOutput: string): DirtyPathEntry[] {
253
+ const seen = new Set<string>();
254
+ const results: DirtyPathEntry[] = [];
255
+ let index = 0;
256
+ while (index + 3 <= statusOutput.length) {
257
+ const statusToken = statusOutput.slice(index, index + 3);
258
+ index += 3;
259
+ const pathEnd = statusOutput.indexOf("\0", index);
260
+ if (pathEnd < 0) break;
261
+ const firstPath = statusOutput.slice(index, pathEnd);
262
+ index = pathEnd + 1;
263
+ const untracked = statusToken.trim().startsWith("??");
264
+ addDirtyPathEntry(seen, results, firstPath, untracked);
265
+ if (isRenameOrCopy(statusToken)) {
266
+ const secondPathEnd = statusOutput.indexOf("\0", index);
267
+ if (secondPathEnd < 0) break;
268
+ const secondPath = statusOutput.slice(index, secondPathEnd);
269
+ index = secondPathEnd + 1;
270
+ addDirtyPathEntry(seen, results, secondPath, false);
271
+ }
272
+ }
273
+ return results;
274
+ }
275
+
276
+ function parseDirtyPathsLinesWithStatus(statusOutput: string): DirtyPathEntry[] {
277
+ const seen = new Set<string>();
278
+ const results: DirtyPathEntry[] = [];
279
+ for (const line of statusOutput.split("\n")) {
280
+ const trimmedLine = line.trimEnd();
281
+ if (trimmedLine.length < 4) continue;
282
+ const statusToken = trimmedLine.slice(0, 3);
283
+ const rawPath = trimmedLine.slice(3).trim();
284
+ if (rawPath.length === 0) continue;
285
+ const untracked = statusToken.trim().startsWith("??");
286
+ const renameParts = rawPath.split(" -> ");
287
+ for (const renamePart of renameParts) {
288
+ addDirtyPathEntry(seen, results, renamePart, untracked);
289
+ }
290
+ }
291
+ return results;
292
+ }
293
+
294
+ function addDirtyPathEntry(seen: Set<string>, results: DirtyPathEntry[], rawPath: string, untracked: boolean): void {
295
+ const normalizedPath = normalizeStatusPath(rawPath);
296
+ if (normalizedPath.length === 0 || seen.has(normalizedPath)) return;
297
+ seen.add(normalizedPath);
298
+ results.push({ path: normalizedPath, untracked });
299
+ }
300
+
301
+ export function parseWorkDirDirtyPathsWithStatus(statusOutput: string, workDirPrefix: string): DirtyPathEntry[] {
302
+ const results: DirtyPathEntry[] = [];
303
+ for (const entry of parseDirtyPathsWithStatus(statusOutput)) {
304
+ const relativePath = relativizeGitPathToWorkDir(entry.path, workDirPrefix);
305
+ if (relativePath === null) continue;
306
+ results.push({ path: relativePath, untracked: entry.untracked });
307
+ }
308
+ return results;
309
+ }
310
+
311
+ export function computeRunModifiedPaths(
312
+ preRunDirtyPaths: string[],
313
+ currentStatusOutput: string,
314
+ workDirPrefix: string,
315
+ ): { tracked: string[]; untracked: string[] } {
316
+ const preRunSet = new Set(preRunDirtyPaths);
317
+ const tracked: string[] = [];
318
+ const untracked: string[] = [];
319
+ for (const entry of parseWorkDirDirtyPathsWithStatus(currentStatusOutput, workDirPrefix)) {
320
+ if (preRunSet.has(entry.path)) continue;
321
+ if (isAutoresearchLocalStatePath(entry.path)) continue;
322
+ if (entry.untracked) {
323
+ untracked.push(entry.path);
324
+ } else {
325
+ tracked.push(entry.path);
326
+ }
327
+ }
328
+ return { tracked, untracked };
329
+ }
@@ -269,6 +269,45 @@ export async function readPendingRunSummary(
269
269
  return null;
270
270
  }
271
271
 
272
+ export async function abandonUnloggedAutoresearchRuns(
273
+ workDir: string,
274
+ loggedRunNumbers: ReadonlySet<number>,
275
+ ): Promise<number> {
276
+ const runsDir = path.join(workDir, ".autoresearch", "runs");
277
+ let entries: fs.Dirent[];
278
+ try {
279
+ entries = await fs.promises.readdir(runsDir, { withFileTypes: true });
280
+ } catch (error) {
281
+ if (isEnoent(error)) return 0;
282
+ throw error;
283
+ }
284
+
285
+ let abandoned = 0;
286
+ const stamp = new Date().toISOString();
287
+ for (const entry of entries) {
288
+ if (!entry.isDirectory()) continue;
289
+ const directoryName = entry.name;
290
+ const runDirectory = path.join(runsDir, directoryName);
291
+ const runJsonPath = path.join(runDirectory, "run.json");
292
+ let parsed: unknown;
293
+ try {
294
+ parsed = await Bun.file(runJsonPath).json();
295
+ } catch (error) {
296
+ if (isEnoent(error)) continue;
297
+ throw error;
298
+ }
299
+
300
+ const pending = parsePendingRunSummary(parsed, runDirectory, directoryName, loggedRunNumbers);
301
+ if (!pending) continue;
302
+
303
+ const existing = typeof parsed === "object" && parsed !== null ? (parsed as Record<string, unknown>) : {};
304
+ await Bun.write(runJsonPath, JSON.stringify({ ...existing, abandonedAt: stamp }, null, 2));
305
+ abandoned += 1;
306
+ }
307
+
308
+ return abandoned;
309
+ }
310
+
272
311
  export function readConfig(cwd: string): AutoresearchConfig {
273
312
  const configPath = path.join(cwd, "autoresearch.config.json");
274
313
  try {
@@ -326,6 +365,7 @@ function parsePendingRunSummary(
326
365
  ): PendingRunSummary | null {
327
366
  if (typeof value !== "object" || value === null) return null;
328
367
  const candidate = value as {
368
+ abandonedAt?: unknown;
329
369
  checks?: { durationSeconds?: unknown; passed?: unknown; timedOut?: unknown };
330
370
  completedAt?: unknown;
331
371
  command?: unknown;
@@ -335,6 +375,7 @@ function parsePendingRunSummary(
335
375
  parsedAsi?: unknown;
336
376
  parsedMetrics?: unknown;
337
377
  parsedPrimary?: unknown;
378
+ preRunDirtyPaths?: unknown;
338
379
  runNumber?: unknown;
339
380
  status?: unknown;
340
381
  timedOut?: unknown;
@@ -342,6 +383,9 @@ function parsePendingRunSummary(
342
383
  if (candidate.loggedAt !== undefined || candidate.status !== undefined) {
343
384
  return null;
344
385
  }
386
+ if (typeof candidate.abandonedAt === "string" && candidate.abandonedAt.trim().length > 0) {
387
+ return null;
388
+ }
345
389
 
346
390
  const command = typeof candidate.command === "string" ? candidate.command : "";
347
391
  const runNumber =
@@ -389,6 +433,10 @@ function parsePendingRunSummary(
389
433
  : null;
390
434
  const checksTimedOut = candidate.checks?.timedOut === true;
391
435
 
436
+ const preRunDirtyPaths = Array.isArray(candidate.preRunDirtyPaths)
437
+ ? candidate.preRunDirtyPaths.filter((item): item is string => typeof item === "string")
438
+ : [];
439
+
392
440
  return {
393
441
  checksDurationSeconds,
394
442
  checksPass,
@@ -399,6 +447,7 @@ function parsePendingRunSummary(
399
447
  parsedMetrics,
400
448
  parsedPrimary,
401
449
  passed: exitCode === 0 && !timedOut && checksPass !== false,
450
+ preRunDirtyPaths,
402
451
  runDirectory,
403
452
  runNumber,
404
453
  };
@@ -1,9 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
4
- import { renderPromptTemplate } from "../config/prompt-templates";
4
+ import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import type { ExtensionContext, ExtensionFactory } from "../extensibility/extensions";
6
- import commandInitializeTemplate from "./command-initialize.md" with { type: "text" };
7
6
  import commandResumeTemplate from "./command-resume.md" with { type: "text" };
8
7
  import { pathMatchesContractPath } from "./contract";
9
8
  import { createDashboardController } from "./dashboard";
@@ -12,7 +11,6 @@ import {
12
11
  formatNum,
13
12
  isAutoresearchCommittableFile,
14
13
  isAutoresearchLocalStatePath,
15
- isAutoresearchShCommand,
16
14
  normalizeAutoresearchPath,
17
15
  readMaxExperiments,
18
16
  readPendingRunSummary,
@@ -37,18 +35,6 @@ import type { AutoresearchRuntime, ChecksResult, ExperimentResult, PendingRunSum
37
35
 
38
36
  const EXPERIMENT_TOOL_NAMES = ["init_experiment", "run_experiment", "log_experiment"];
39
37
 
40
- interface AutoresearchSetupInput {
41
- intent: string;
42
- benchmarkCommand: string;
43
- metricName: string;
44
- metricUnit: string;
45
- direction: "lower" | "higher";
46
- secondaryMetrics: string[];
47
- scopePaths: string[];
48
- offLimits: string[];
49
- constraints: string[];
50
- }
51
-
52
38
  export const createAutoresearchExtension: ExtensionFactory = api => {
53
39
  const runtimeStore = createRuntimeStore();
54
40
  const dashboard = createDashboardController();
@@ -109,17 +95,6 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
109
95
  api.on("tool_call", (event, ctx) => {
110
96
  const runtime = getRuntime(ctx);
111
97
  if (!runtime.autoresearchMode) return;
112
- if (event.toolName === "bash") {
113
- const command = typeof event.input.command === "string" ? event.input.command : "";
114
- const validationError = validateAutoresearchBashCommand(command);
115
- if (validationError) {
116
- return {
117
- block: true,
118
- reason: validationError,
119
- };
120
- }
121
- return;
122
- }
123
98
  if (event.toolName !== "write" && event.toolName !== "edit" && event.toolName !== "ast_edit") return;
124
99
 
125
100
  const rawPaths = getGuardedToolPaths(event.toolName, event.input);
@@ -151,14 +126,17 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
151
126
  });
152
127
 
153
128
  api.registerCommand("autoresearch", {
154
- description: "Start, stop, or clear builtin autoresearch mode.",
129
+ description: "Toggle builtin autoresearch mode, or pass off / clear, or a goal message.",
155
130
  getArgumentCompletions(argumentPrefix: string): AutocompleteItem[] | null {
156
131
  if (argumentPrefix.includes(" ")) return null;
132
+ const normalized = argumentPrefix.trim().toLowerCase();
133
+ // No suggestions for an empty argument prefix so Tab after "/autoresearch " does not
134
+ // force-complete into off/clear; bare command submit toggles like /plan.
135
+ if (normalized.length === 0) return null;
157
136
  const completions: AutocompleteItem[] = [
158
137
  { label: "off", value: "off", description: "Leave autoresearch mode" },
159
138
  { label: "clear", value: "clear", description: "Delete autoresearch.jsonl and leave autoresearch mode" },
160
139
  ];
161
- const normalized = argumentPrefix.trim().toLowerCase();
162
140
  const filtered = completions.filter(item => item.label.startsWith(normalized));
163
141
  return filtered.length > 0 ? filtered : null;
164
142
  },
@@ -171,6 +149,15 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
171
149
  return;
172
150
  }
173
151
 
152
+ if (trimmed === "" && runtime.autoresearchMode) {
153
+ setMode(ctx, false, runtime.goal, "off");
154
+ dashboard.updateWidget(ctx, runtime);
155
+ const experimentTools = new Set(EXPERIMENT_TOOL_NAMES);
156
+ await api.setActiveTools(api.getActiveTools().filter(name => !experimentTools.has(name)));
157
+ ctx.ui.notify("Autoresearch mode disabled", "info");
158
+ return;
159
+ }
160
+
174
161
  if (trimmed === "off") {
175
162
  setMode(ctx, false, runtime.goal, "off");
176
163
  dashboard.updateWidget(ctx, runtime);
@@ -227,7 +214,7 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
227
214
  dashboard.updateWidget(ctx, runtime);
228
215
  await api.setActiveTools([...new Set([...api.getActiveTools(), ...EXPERIMENT_TOOL_NAMES])]);
229
216
  api.sendUserMessage(
230
- renderPromptTemplate(commandResumeTemplate, {
217
+ prompt.render(commandResumeTemplate, {
231
218
  autoresearch_md_path: autoresearchMdPath,
232
219
  branch_status_line: branchResult.created
233
220
  ? `Created and checked out dedicated git branch \`${branchResult.branchName}\` before resuming.`
@@ -239,57 +226,21 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
239
226
  return;
240
227
  }
241
228
 
242
- const setup = await promptForAutoresearchSetup(
243
- ctx,
244
- trimmed || runtime.goal || "what should autoresearch improve?",
245
- );
246
- if (!setup) return;
247
-
248
- const branchResult = await ensureAutoresearchBranch(api, workDir, setup.intent);
229
+ const branchGoal = trimmed.length > 0 ? trimmed : null;
230
+ const branchResult = await ensureAutoresearchBranch(api, workDir, branchGoal);
249
231
  if (!branchResult.ok) {
250
232
  ctx.ui.notify(branchResult.error, "error");
251
233
  return;
252
234
  }
253
235
 
254
- setMode(ctx, true, setup.intent, "on");
255
- runtime.state.name = setup.intent;
256
- runtime.state.metricName = setup.metricName;
257
- runtime.state.metricUnit = setup.metricUnit;
258
- runtime.state.bestDirection = setup.direction;
259
- runtime.state.secondaryMetrics = setup.secondaryMetrics.map(name => ({ name, unit: "" }));
260
- runtime.state.benchmarkCommand = setup.benchmarkCommand;
261
- runtime.state.scopePaths = [...setup.scopePaths];
262
- runtime.state.offLimits = [...setup.offLimits];
263
- runtime.state.constraints = [...setup.constraints];
236
+ setMode(ctx, true, branchGoal, "on");
264
237
  dashboard.updateWidget(ctx, runtime);
265
238
  await api.setActiveTools([...new Set([...api.getActiveTools(), ...EXPERIMENT_TOOL_NAMES])]);
266
- api.sendUserMessage(
267
- renderPromptTemplate(commandInitializeTemplate, {
268
- branch_status_line: branchResult.created
269
- ? `Created and checked out dedicated git branch \`${branchResult.branchName}\`.`
270
- : `Using dedicated git branch \`${branchResult.branchName}\`.`,
271
- intent: setup.intent,
272
- benchmark_command: setup.benchmarkCommand,
273
- metric_name: setup.metricName,
274
- metric_unit: setup.metricUnit,
275
- direction: setup.direction,
276
- has_secondary_metrics: setup.secondaryMetrics.length > 0,
277
- secondary_metrics: setup.secondaryMetrics,
278
- secondary_metrics_block: formatBulletBlock(
279
- setup.secondaryMetrics,
280
- value => ` - \`${value}\``,
281
- " - `(none)`",
282
- ),
283
- scope_paths: setup.scopePaths,
284
- scope_paths_block: formatBulletBlock(setup.scopePaths, value => ` - \`${value}\``),
285
- has_off_limits: setup.offLimits.length > 0,
286
- off_limits: setup.offLimits,
287
- off_limits_block: formatBulletBlock(setup.offLimits, value => ` - \`${value}\``, " - `(none)`"),
288
- has_constraints: setup.constraints.length > 0,
289
- constraints: setup.constraints,
290
- constraints_block: formatBulletBlock(setup.constraints, value => ` - ${value}`, " - `(none)`"),
291
- }),
292
- );
239
+ if (trimmed.length > 0) {
240
+ api.sendUserMessage(trimmed);
241
+ } else {
242
+ ctx.ui.notify("Autoresearch enabled—describe what to optimize in your next message.", "info");
243
+ }
293
244
  },
294
245
  });
295
246
 
@@ -352,7 +303,7 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
352
303
  api.sendMessage(
353
304
  {
354
305
  customType: "autoresearch-resume",
355
- content: renderPromptTemplate(resumeMessageTemplate, {
306
+ content: prompt.render(resumeMessageTemplate, {
356
307
  autoresearch_md_path: autoresearchMdPath,
357
308
  has_ideas: fs.existsSync(ideasPath),
358
309
  has_pending_run: Boolean(pendingRun),
@@ -394,15 +345,16 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
394
345
  status: result.status,
395
346
  };
396
347
  });
348
+ const hasAutoresearchMd = fs.existsSync(autoresearchMdPath);
397
349
  return {
398
- systemPrompt: renderPromptTemplate(promptTemplate, {
350
+ systemPrompt: prompt.render(promptTemplate, {
399
351
  base_system_prompt: event.systemPrompt,
400
352
  has_goal: goal.trim().length > 0,
401
353
  goal,
354
+ has_autoresearch_md: hasAutoresearchMd,
402
355
  working_dir: workDir,
403
356
  default_metric_name: runtime.state.metricName,
404
357
  metric_name: runtime.state.metricName,
405
- has_autoresearch_md: fs.existsSync(autoresearchMdPath),
406
358
  autoresearch_md_path: autoresearchMdPath,
407
359
  has_checks: fs.existsSync(checksPath),
408
360
  checks_path: checksPath,
@@ -438,93 +390,6 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
438
390
  });
439
391
  };
440
392
 
441
- async function promptForAutoresearchSetup(
442
- ctx: ExtensionContext,
443
- defaultIntent: string,
444
- ): Promise<AutoresearchSetupInput | undefined> {
445
- const intentInput = await ctx.ui.input("Autoresearch Intent", defaultIntent);
446
- if (intentInput === undefined) return undefined;
447
- const intent = intentInput.trim();
448
- if (intent.length === 0) {
449
- ctx.ui.notify("Autoresearch intent is required", "info");
450
- return undefined;
451
- }
452
-
453
- const benchmarkCommandInput = await ctx.ui.input("Benchmark Command", "bash autoresearch.sh");
454
- if (benchmarkCommandInput === undefined) return undefined;
455
- const benchmarkCommand = benchmarkCommandInput.trim();
456
- if (benchmarkCommand.length === 0) {
457
- ctx.ui.notify("Benchmark command is required", "info");
458
- return undefined;
459
- }
460
- if (!isAutoresearchShCommand(benchmarkCommand)) {
461
- ctx.ui.notify("Benchmark command must invoke `autoresearch.sh` directly", "info");
462
- return undefined;
463
- }
464
-
465
- const metricNameInput = await ctx.ui.input("Primary Metric Name", "runtime_ms");
466
- if (metricNameInput === undefined) return undefined;
467
- const metricName = metricNameInput.trim();
468
- if (metricName.length === 0) {
469
- ctx.ui.notify("Primary metric name is required", "info");
470
- return undefined;
471
- }
472
-
473
- const metricUnitInput = await ctx.ui.input("Metric Unit", "ms");
474
- if (metricUnitInput === undefined) return undefined;
475
- const metricUnit = metricUnitInput.trim();
476
-
477
- const directionInput = await ctx.ui.input("Metric Direction", "lower");
478
- if (directionInput === undefined) return undefined;
479
- const normalizedDirection = directionInput.trim().toLowerCase();
480
- if (normalizedDirection !== "lower" && normalizedDirection !== "higher") {
481
- ctx.ui.notify("Metric direction must be `lower` or `higher`", "info");
482
- return undefined;
483
- }
484
-
485
- const secondaryMetricsInput = await ctx.ui.input("Tradeoff Metrics", "");
486
- if (secondaryMetricsInput === undefined) return undefined;
487
-
488
- const scopePathsInput = await ctx.ui.input("Files in Scope", "packages/coding-agent/src/autoresearch");
489
- if (scopePathsInput === undefined) return undefined;
490
- const scopePaths = splitSetupList(scopePathsInput);
491
- if (scopePaths.length === 0) {
492
- ctx.ui.notify("Files in Scope must include at least one path", "info");
493
- return undefined;
494
- }
495
-
496
- const offLimitsInput = await ctx.ui.input("Off Limits", "");
497
- if (offLimitsInput === undefined) return undefined;
498
- const constraintsInput = await ctx.ui.input("Constraints", "");
499
- if (constraintsInput === undefined) return undefined;
500
-
501
- return {
502
- intent,
503
- benchmarkCommand,
504
- metricName,
505
- metricUnit,
506
- direction: normalizedDirection,
507
- secondaryMetrics: splitSetupList(secondaryMetricsInput),
508
- scopePaths,
509
- offLimits: splitSetupList(offLimitsInput),
510
- constraints: splitSetupList(constraintsInput),
511
- };
512
- }
513
-
514
- function splitSetupList(value: string): string[] {
515
- return value
516
- .split(/\r?\n|,/)
517
- .map(entry => entry.trim())
518
- .filter((entry, index, values) => entry.length > 0 && values.indexOf(entry) === index);
519
- }
520
-
521
- function formatBulletBlock(values: string[], renderValue: (value: string) => string, emptyValue = ""): string {
522
- if (values.length === 0) {
523
- return emptyValue;
524
- }
525
- return values.map(renderValue).join("\n");
526
- }
527
-
528
393
  function hasLocalAutoresearchState(workDir: string): boolean {
529
394
  return fs.existsSync(path.join(workDir, "autoresearch.jsonl")) || fs.existsSync(path.join(workDir, ".autoresearch"));
530
395
  }
@@ -667,27 +532,3 @@ function canonicalizeTargetPath(targetPath: string): string {
667
532
  }
668
533
  return path.resolve(canonicalizeExistingPath(currentPath), ...pendingSegments);
669
534
  }
670
-
671
- function validateAutoresearchBashCommand(command: string): string | null {
672
- const trimmed = command.trim();
673
- if (trimmed.length === 0) {
674
- return null;
675
- }
676
- const mutationPatterns = [
677
- /(^|[;&|()]\s*)(?:bash|sh)\b/,
678
- /(^|[;&|()]\s*)(?:python|python3|node|perl|ruby|php)\b/,
679
- /(^|[;&|()]\s*)(?:mv|cp|rm|mkdir|touch|chmod|chown|ln|install|patch)\b/,
680
- /(^|[;&|()]\s*)sed\s+-i\b/,
681
- /(^|[;&|()]\s*)git\s+(?:add|apply|checkout|clean|commit|merge|rebase|reset|restore|revert|stash|switch|worktree)\b/,
682
- /(^|[^<])>>?/,
683
- /\|\s*tee\b/,
684
- /<<<?/,
685
- ];
686
- if (mutationPatterns.some(pattern => pattern.test(trimmed))) {
687
- return (
688
- "Autoresearch only allows read-only shell inspection. " +
689
- "Use write/edit/ast_edit for file changes and run_experiment for benchmark execution."
690
- );
691
- }
692
- return null;
693
- }