@oh-my-pi/pi-coding-agent 14.5.14 → 14.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/package.json +7 -7
  3. package/src/autoresearch/command-resume.md +5 -8
  4. package/src/autoresearch/git.ts +41 -51
  5. package/src/autoresearch/helpers.ts +43 -359
  6. package/src/autoresearch/index.ts +281 -273
  7. package/src/autoresearch/prompt-setup.md +43 -0
  8. package/src/autoresearch/prompt.md +52 -193
  9. package/src/autoresearch/resume-message.md +2 -8
  10. package/src/autoresearch/state.ts +59 -166
  11. package/src/autoresearch/storage.ts +687 -0
  12. package/src/autoresearch/tools/init-experiment.ts +201 -290
  13. package/src/autoresearch/tools/log-experiment.ts +304 -517
  14. package/src/autoresearch/tools/run-experiment.ts +117 -296
  15. package/src/autoresearch/tools/update-notes.ts +116 -0
  16. package/src/autoresearch/types.ts +16 -66
  17. package/src/config/settings-schema.ts +1 -1
  18. package/src/config/settings.ts +20 -1
  19. package/src/cursor.ts +1 -1
  20. package/src/edit/index.ts +9 -31
  21. package/src/edit/line-hash.ts +70 -43
  22. package/src/edit/modes/hashline.lark +26 -0
  23. package/src/edit/modes/hashline.ts +898 -1099
  24. package/src/edit/modes/patch.ts +0 -7
  25. package/src/edit/modes/replace.ts +0 -4
  26. package/src/edit/renderer.ts +22 -20
  27. package/src/edit/streaming.ts +8 -28
  28. package/src/eval/eval.lark +24 -30
  29. package/src/eval/js/context-manager.ts +5 -162
  30. package/src/eval/js/prelude.txt +0 -12
  31. package/src/eval/parse.ts +129 -129
  32. package/src/eval/py/prelude.py +1 -219
  33. package/src/export/html/template.generated.ts +1 -1
  34. package/src/export/html/template.js +2 -2
  35. package/src/internal-urls/docs-index.generated.ts +1 -1
  36. package/src/modes/components/session-observer-overlay.ts +5 -2
  37. package/src/modes/components/status-line/segments.ts +1 -1
  38. package/src/modes/components/status-line.ts +3 -5
  39. package/src/modes/components/tree-selector.ts +4 -5
  40. package/src/modes/components/welcome.ts +11 -1
  41. package/src/modes/controllers/command-controller.ts +2 -6
  42. package/src/modes/controllers/event-controller.ts +1 -2
  43. package/src/modes/controllers/extension-ui-controller.ts +3 -15
  44. package/src/modes/controllers/input-controller.ts +0 -1
  45. package/src/modes/controllers/selector-controller.ts +1 -1
  46. package/src/modes/interactive-mode.ts +5 -7
  47. package/src/prompts/system/system-prompt.md +14 -38
  48. package/src/prompts/tools/ast-edit.md +8 -8
  49. package/src/prompts/tools/ast-grep.md +10 -10
  50. package/src/prompts/tools/eval.md +13 -31
  51. package/src/prompts/tools/find.md +2 -1
  52. package/src/prompts/tools/hashline.md +66 -57
  53. package/src/prompts/tools/search.md +2 -2
  54. package/src/session/session-manager.ts +17 -13
  55. package/src/tools/ast-edit.ts +141 -44
  56. package/src/tools/ast-grep.ts +112 -36
  57. package/src/tools/eval.ts +2 -53
  58. package/src/tools/find.ts +16 -15
  59. package/src/tools/path-utils.ts +36 -196
  60. package/src/tools/search.ts +56 -35
  61. package/src/utils/edit-mode.ts +2 -11
  62. package/src/utils/file-display-mode.ts +1 -1
  63. package/src/utils/git.ts +17 -0
  64. package/src/utils/session-color.ts +0 -12
  65. package/src/utils/title-generator.ts +22 -38
  66. package/src/autoresearch/apply-contract-to-state.ts +0 -24
  67. package/src/autoresearch/contract.ts +0 -288
  68. package/src/edit/modes/atom.lark +0 -29
  69. package/src/edit/modes/atom.ts +0 -1773
  70. package/src/prompts/tools/atom.md +0 -150
@@ -1,29 +1,9 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { isEnoent } from "@oh-my-pi/pi-utils";
4
- import { parseCommandArgs } from "../utils/command-args";
5
- import type {
6
- ASIData,
7
- ASIValue,
8
- AutoresearchConfig,
9
- MetricDirection,
10
- NumericMetricMap,
11
- PendingRunSummary,
12
- } from "./types";
1
+ import type { ASIData, ASIValue, MetricDirection, NumericMetricMap } from "./types";
13
2
 
14
3
  export const METRIC_LINE_PREFIX = "METRIC";
15
4
  export const ASI_LINE_PREFIX = "ASI";
16
5
  export const EXPERIMENT_MAX_LINES = 10;
17
6
  export const EXPERIMENT_MAX_BYTES = 4 * 1024;
18
- export const AUTORESEARCH_COMMITTABLE_FILES = [
19
- "autoresearch.md",
20
- "autoresearch.program.md",
21
- "autoresearch.sh",
22
- "autoresearch.checks.sh",
23
- "autoresearch.ideas.md",
24
- ] as const;
25
- export const AUTORESEARCH_LOCAL_STATE_FILES = ["autoresearch.jsonl"] as const;
26
- export const AUTORESEARCH_LOCAL_STATE_DIRECTORIES = [".autoresearch"] as const;
27
7
 
28
8
  const DENIED_KEY_NAMES = new Set(["__proto__", "constructor", "prototype"]);
29
9
 
@@ -120,51 +100,6 @@ export function formatElapsed(milliseconds: number): string {
120
100
  return `${seconds}s`;
121
101
  }
122
102
 
123
- export function getAutoresearchRunDirectory(workDir: string, runNumber: number): string {
124
- return path.join(workDir, ".autoresearch", "runs", String(runNumber).padStart(4, "0"));
125
- }
126
-
127
- export function getNextAutoresearchRunNumber(workDir: string, lastRunNumber: number | null): number {
128
- const runsDirectory = path.join(workDir, ".autoresearch", "runs");
129
- let maxRunNumber = lastRunNumber ?? 0;
130
- try {
131
- for (const entry of fs.readdirSync(runsDirectory, { withFileTypes: true })) {
132
- if (!entry.isDirectory()) continue;
133
- const runNumber = Number.parseInt(entry.name, 10);
134
- if (Number.isFinite(runNumber)) {
135
- maxRunNumber = Math.max(maxRunNumber, runNumber);
136
- }
137
- }
138
- } catch (error) {
139
- if (!isEnoent(error)) {
140
- throw error;
141
- }
142
- }
143
- return maxRunNumber + 1;
144
- }
145
-
146
- export function normalizeAutoresearchPath(relativePath: string): string {
147
- const normalized = relativePath.replaceAll("\\", "/").trim();
148
- if (normalized === "." || normalized === "./") return ".";
149
- return normalized.replace(/^\.\/+/, "").replace(/\/+$/, "");
150
- }
151
-
152
- export function isAutoresearchCommittableFile(relativePath: string): boolean {
153
- const normalized = normalizeAutoresearchPath(relativePath);
154
- return AUTORESEARCH_COMMITTABLE_FILES.some(candidate => candidate === normalized);
155
- }
156
-
157
- export function isAutoresearchLocalStatePath(relativePath: string): boolean {
158
- const normalized = normalizeAutoresearchPath(relativePath);
159
- if (AUTORESEARCH_LOCAL_STATE_FILES.some(candidate => candidate === normalized)) {
160
- return true;
161
- }
162
- return AUTORESEARCH_LOCAL_STATE_DIRECTORIES.some(candidate => {
163
- const normalizedCandidate = normalizeAutoresearchPath(candidate);
164
- return normalized === normalizedCandidate || normalized.startsWith(`${normalizedCandidate}/`);
165
- });
166
- }
167
-
168
103
  export function killTree(pid: number, signal: NodeJS.Signals | number = "SIGTERM"): void {
169
104
  try {
170
105
  process.kill(-pid, signal);
@@ -177,47 +112,6 @@ export function killTree(pid: number, signal: NodeJS.Signals | number = "SIGTERM
177
112
  }
178
113
  }
179
114
 
180
- export function isAutoresearchShCommand(command: string): boolean {
181
- let normalized = command.trim();
182
- normalized = normalized.replace(/^(?:\w+=\S*\s+)+/, "");
183
-
184
- let previous = "";
185
- while (previous !== normalized) {
186
- previous = normalized;
187
- normalized = normalized.replace(/^(?:env|time|nice|nohup)(?:\s+-\S+(?:\s+\d+)?)?\s+/, "");
188
- }
189
- if (/[;&|<>]/.test(normalized)) {
190
- return false;
191
- }
192
-
193
- const tokens = parseCommandArgs(normalized);
194
- if (tokens.length === 0) return false;
195
-
196
- let index = 0;
197
- if (tokens[index] === "bash" || tokens[index] === "sh") {
198
- index += 1;
199
- while (index < tokens.length && tokens[index]?.startsWith("-")) {
200
- if (tokens[index]?.includes("c")) {
201
- return false;
202
- }
203
- index += 1;
204
- }
205
- }
206
-
207
- const scriptToken = tokens[index];
208
- if (!scriptToken || !/^(?:\.\/|\/[\w/.-]*\/)?autoresearch\.sh$/.test(scriptToken)) {
209
- return false;
210
- }
211
-
212
- for (const token of tokens.slice(index + 1)) {
213
- if (token === "&&" || token === "||" || token === ";" || token === "|" || token === ">" || token === "<") {
214
- return false;
215
- }
216
- }
217
-
218
- return true;
219
- }
220
-
221
115
  export function isBetter(current: number, best: number, direction: MetricDirection): boolean {
222
116
  return direction === "lower" ? current < best : current > best;
223
117
  }
@@ -231,287 +125,77 @@ export function inferMetricUnitFromName(name: string): string {
231
125
  return "";
232
126
  }
233
127
 
234
- export async function readPendingRunSummary(
235
- workDir: string,
236
- loggedRunNumbers: ReadonlySet<number> = new Set<number>(),
237
- ): Promise<PendingRunSummary | null> {
238
- const runsDir = path.join(workDir, ".autoresearch", "runs");
239
- let entries: fs.Dirent[];
240
- try {
241
- entries = await fs.promises.readdir(runsDir, { withFileTypes: true });
242
- } catch (error) {
243
- if (isEnoent(error)) return null;
244
- throw error;
245
- }
246
-
247
- const runDirectories = entries
248
- .filter(entry => entry.isDirectory())
249
- .map(entry => entry.name)
250
- .sort((left, right) => right.localeCompare(left));
251
-
252
- for (const directoryName of runDirectories) {
253
- const runDirectory = path.join(runsDir, directoryName);
254
- const runJsonPath = path.join(runDirectory, "run.json");
255
- let parsed: unknown;
256
- try {
257
- parsed = await Bun.file(runJsonPath).json();
258
- } catch (error) {
259
- if (isEnoent(error)) continue;
260
- throw error;
261
- }
262
-
263
- const pendingRun = parsePendingRunSummary(parsed, runDirectory, directoryName, loggedRunNumbers);
264
- if (pendingRun) {
265
- return pendingRun;
266
- }
267
- }
268
-
269
- return null;
270
- }
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
-
311
- export function readConfig(cwd: string): AutoresearchConfig {
312
- const configPath = path.join(cwd, "autoresearch.config.json");
313
- try {
314
- const raw = fs.readFileSync(configPath, "utf8");
315
- const parsed = JSON.parse(raw) as unknown;
316
- if (typeof parsed !== "object" || parsed === null) return {};
317
- const candidate = parsed as { maxIterations?: unknown; workingDir?: unknown };
318
- const config: AutoresearchConfig = {};
319
- if (typeof candidate.maxIterations === "number" && Number.isFinite(candidate.maxIterations)) {
320
- config.maxIterations = candidate.maxIterations;
321
- }
322
- if (typeof candidate.workingDir === "string" && candidate.workingDir.trim().length > 0) {
323
- config.workingDir = candidate.workingDir;
324
- }
325
- return config;
326
- } catch (error) {
327
- if (isEnoent(error)) return {};
328
- return {};
329
- }
128
+ export function normalizePathSpec(value: string): string {
129
+ const trimmed = value.trim().replaceAll("\\", "/");
130
+ if (trimmed === "" || trimmed === "." || trimmed === "./") return ".";
131
+ const collapsed = trimmed.replace(/^\.\/+/, "").replace(/\/+$/, "");
132
+ return collapsed.length === 0 ? "." : collapsed;
330
133
  }
331
134
 
332
- export function readMaxExperiments(cwd: string): number | null {
333
- const value = readConfig(cwd).maxIterations;
334
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
335
- return Math.floor(value);
135
+ export function pathMatchesSpec(pathValue: string, specValue: string): boolean {
136
+ const normalizedPath = normalizePathSpec(pathValue);
137
+ const normalizedSpec = normalizePathSpec(specValue);
138
+ if (normalizedSpec === ".") return true;
139
+ return normalizedPath === normalizedSpec || normalizedPath.startsWith(`${normalizedSpec}/`);
336
140
  }
337
141
 
338
- export function resolveWorkDir(cwd: string): string {
339
- const configured = readConfig(cwd).workingDir;
340
- if (!configured) return cwd;
341
- return path.isAbsolute(configured) ? configured : path.resolve(cwd, configured);
342
- }
343
-
344
- export function validateWorkDir(cwd: string): string | null {
345
- const workDir = resolveWorkDir(cwd);
346
- try {
347
- const stat = fs.statSync(workDir);
348
- if (!stat.isDirectory()) {
349
- return `workingDir ${workDir} is not a directory.`;
350
- }
351
- return null;
352
- } catch (error) {
353
- if (isEnoent(error)) {
354
- return `workingDir ${workDir} does not exist.`;
355
- }
356
- return `workingDir ${workDir} is unavailable.`;
357
- }
358
- }
359
-
360
- function parsePendingRunSummary(
361
- value: unknown,
362
- runDirectory: string,
363
- directoryName: string,
364
- loggedRunNumbers: ReadonlySet<number>,
365
- ): PendingRunSummary | null {
366
- if (typeof value !== "object" || value === null) return null;
367
- const candidate = value as {
368
- abandonedAt?: unknown;
369
- checks?: { durationSeconds?: unknown; passed?: unknown; timedOut?: unknown };
370
- completedAt?: unknown;
371
- command?: unknown;
372
- durationSeconds?: unknown;
373
- exitCode?: unknown;
374
- loggedAt?: unknown;
375
- parsedAsi?: unknown;
376
- parsedMetrics?: unknown;
377
- parsedPrimary?: unknown;
378
- preRunDirtyPaths?: unknown;
379
- runNumber?: unknown;
380
- status?: unknown;
381
- timedOut?: unknown;
382
- };
383
- if (candidate.loggedAt !== undefined || candidate.status !== undefined) {
384
- return null;
385
- }
386
- if (typeof candidate.abandonedAt === "string" && candidate.abandonedAt.trim().length > 0) {
387
- return null;
388
- }
389
-
390
- const command = typeof candidate.command === "string" ? candidate.command : "";
391
- const runNumber =
392
- typeof candidate.runNumber === "number" && Number.isFinite(candidate.runNumber)
393
- ? candidate.runNumber
394
- : parseInt(directoryName, 10);
395
- if (!Number.isFinite(runNumber)) return null;
396
- if (loggedRunNumbers.has(runNumber)) return null;
397
-
398
- const hasCompletedMetadata =
399
- typeof candidate.completedAt === "string" ||
400
- candidate.exitCode !== undefined ||
401
- candidate.timedOut !== undefined ||
402
- candidate.durationSeconds !== undefined ||
403
- candidate.checks !== undefined ||
404
- candidate.parsedPrimary !== undefined ||
405
- candidate.parsedMetrics !== undefined ||
406
- candidate.parsedAsi !== undefined;
407
- if (!hasCompletedMetadata) {
408
- return null;
142
+ export function dedupeStrings(values: readonly string[]): string[] {
143
+ const out: string[] = [];
144
+ const seen = new Set<string>();
145
+ for (const value of values) {
146
+ const trimmed = value.trim();
147
+ if (trimmed.length === 0 || seen.has(trimmed)) continue;
148
+ seen.add(trimmed);
149
+ out.push(trimmed);
409
150
  }
410
-
411
- const checksPass =
412
- typeof candidate.checks?.passed === "boolean"
413
- ? candidate.checks.passed
414
- : typeof candidate.checks?.timedOut === "boolean" && candidate.checks.timedOut
415
- ? false
416
- : null;
417
- const exitCode =
418
- typeof candidate.exitCode === "number" && Number.isFinite(candidate.exitCode) ? candidate.exitCode : null;
419
- const timedOut = candidate.timedOut === true;
420
- const durationSeconds =
421
- typeof candidate.durationSeconds === "number" && Number.isFinite(candidate.durationSeconds)
422
- ? candidate.durationSeconds
423
- : null;
424
- const parsedPrimary =
425
- typeof candidate.parsedPrimary === "number" && Number.isFinite(candidate.parsedPrimary)
426
- ? candidate.parsedPrimary
427
- : null;
428
- const parsedAsi = cloneAsiData(candidate.parsedAsi);
429
- const parsedMetrics = cloneNumericMetricMap(candidate.parsedMetrics);
430
- const checksDurationSeconds =
431
- typeof candidate.checks?.durationSeconds === "number" && Number.isFinite(candidate.checks.durationSeconds)
432
- ? candidate.checks.durationSeconds
433
- : null;
434
- const checksTimedOut = candidate.checks?.timedOut === true;
435
-
436
- const preRunDirtyPaths = Array.isArray(candidate.preRunDirtyPaths)
437
- ? candidate.preRunDirtyPaths.filter((item): item is string => typeof item === "string")
438
- : [];
439
-
440
- return {
441
- checksDurationSeconds,
442
- checksPass,
443
- checksTimedOut,
444
- command,
445
- durationSeconds,
446
- parsedAsi,
447
- parsedMetrics,
448
- parsedPrimary,
449
- passed: exitCode === 0 && !timedOut && checksPass !== false,
450
- preRunDirtyPaths,
451
- runDirectory,
452
- runNumber,
453
- };
151
+ return out;
454
152
  }
455
153
 
456
- function cloneNumericMetricMap(value: unknown): NumericMetricMap | null {
457
- if (typeof value !== "object" || value === null) return null;
458
- const metrics = value as { [key: string]: unknown };
459
- const clone: NumericMetricMap = {};
460
- for (const [key, entryValue] of Object.entries(metrics)) {
154
+ export function ensureNumericMetricMap(value: NumericMetricMap | undefined): NumericMetricMap {
155
+ if (!value) return {};
156
+ const out: NumericMetricMap = {};
157
+ for (const [key, entryValue] of Object.entries(value)) {
461
158
  if (DENIED_KEY_NAMES.has(key)) continue;
462
159
  if (typeof entryValue === "number" && Number.isFinite(entryValue)) {
463
- clone[key] = entryValue;
160
+ out[key] = entryValue;
464
161
  }
465
162
  }
466
- return Object.keys(clone).length > 0 ? clone : null;
163
+ return out;
467
164
  }
468
165
 
469
- function cloneAsiData(value: unknown): ASIData | null {
470
- if (typeof value !== "object" || value === null) return null;
471
- const candidate = value as { [key: string]: unknown };
472
- const clone: ASIData = {};
473
- for (const [key, entryValue] of Object.entries(candidate)) {
166
+ export function sanitizeAsi(value: { [key: string]: unknown } | undefined): ASIData | undefined {
167
+ if (!value) return undefined;
168
+ const result: ASIData = {};
169
+ for (const [key, entryValue] of Object.entries(value)) {
474
170
  if (DENIED_KEY_NAMES.has(key)) continue;
475
- const sanitized = clonePendingAsiValue(entryValue);
171
+ const sanitized = sanitizeAsiValue(entryValue);
476
172
  if (sanitized !== undefined) {
477
- clone[key] = sanitized;
173
+ result[key] = sanitized;
478
174
  }
479
175
  }
480
- return Object.keys(clone).length > 0 ? clone : null;
176
+ return Object.keys(result).length > 0 ? result : undefined;
481
177
  }
482
178
 
483
- function clonePendingAsiValue(value: unknown): ASIValue | undefined {
179
+ function sanitizeAsiValue(value: unknown): ASIValue | undefined {
484
180
  if (value === null) return null;
485
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
486
- return value;
487
- }
181
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
488
182
  if (Array.isArray(value)) {
489
183
  const items = value
490
- .map(entry => clonePendingAsiValue(entry))
491
- .filter((entry): entry is NonNullable<typeof entry> => entry !== undefined);
184
+ .map(item => sanitizeAsiValue(item))
185
+ .filter((item): item is NonNullable<typeof item> => item !== undefined);
492
186
  return items;
493
187
  }
494
188
  if (typeof value === "object") {
495
- const candidate = value as { [key: string]: unknown };
496
- const clone: { [key: string]: ASIValue } = {};
497
- for (const [key, entryValue] of Object.entries(candidate)) {
189
+ const objectValue = value as { [key: string]: unknown };
190
+ const result: ASIData = {};
191
+ for (const [key, entryValue] of Object.entries(objectValue)) {
498
192
  if (DENIED_KEY_NAMES.has(key)) continue;
499
- const sanitized = clonePendingAsiValue(entryValue);
193
+ const sanitized = sanitizeAsiValue(entryValue);
500
194
  if (sanitized !== undefined) {
501
- clone[key] = sanitized;
195
+ result[key] = sanitized;
502
196
  }
503
197
  }
504
- return clone;
198
+ return result;
505
199
  }
506
200
  return undefined;
507
201
  }
508
-
509
- export function collectLoggedRunNumbers(results: readonly { runNumber: number | null }[]): Set<number> {
510
- const runNumbers = new Set<number>();
511
- for (const result of results) {
512
- if (result.runNumber !== null) {
513
- runNumbers.add(result.runNumber);
514
- }
515
- }
516
- return runNumbers;
517
- }