@oh-my-pi/pi-coding-agent 13.18.0 → 13.19.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.
- package/CHANGELOG.md +50 -0
- package/package.json +7 -11
- package/src/autoresearch/git.ts +25 -30
- package/src/autoresearch/tools/log-experiment.ts +61 -74
- package/src/commit/agentic/agent.ts +0 -3
- package/src/commit/agentic/index.ts +19 -22
- package/src/commit/agentic/tools/git-file-diff.ts +3 -6
- package/src/commit/agentic/tools/git-hunk.ts +3 -3
- package/src/commit/agentic/tools/git-overview.ts +6 -9
- package/src/commit/agentic/tools/index.ts +6 -8
- package/src/commit/agentic/tools/propose-commit.ts +4 -7
- package/src/commit/agentic/tools/recent-commits.ts +3 -3
- package/src/commit/agentic/tools/split-commit.ts +4 -4
- package/src/commit/changelog/index.ts +5 -9
- package/src/commit/pipeline.ts +10 -12
- package/src/config/keybindings.ts +7 -6
- package/src/config/settings-schema.ts +44 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +4 -16
- package/src/extensibility/custom-commands/bundled/review/index.ts +43 -41
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -1
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
- package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
- package/src/index.ts +1 -0
- package/src/main.ts +24 -2
- package/src/modes/components/footer.ts +9 -29
- package/src/modes/components/hook-editor.ts +3 -3
- package/src/modes/components/hook-selector.ts +6 -1
- package/src/modes/components/session-observer-overlay.ts +472 -0
- package/src/modes/components/settings-defs.ts +19 -0
- package/src/modes/components/status-line.ts +15 -61
- package/src/modes/controllers/command-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +59 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +26 -0
- package/src/modes/interactive-mode.ts +195 -43
- package/src/modes/session-observer-registry.ts +146 -0
- package/src/modes/shared.ts +0 -42
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/keybinding-matchers.ts +9 -0
- package/src/prompts/system/custom-system-prompt.md +5 -0
- package/src/prompts/system/system-prompt.md +6 -0
- package/src/sdk.ts +28 -13
- package/src/secrets/index.ts +1 -1
- package/src/secrets/obfuscator.ts +24 -16
- package/src/session/agent-session.ts +75 -30
- package/src/session/session-manager.ts +15 -5
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +28 -0
- package/src/task/index.ts +88 -78
- package/src/task/types.ts +25 -0
- package/src/task/worktree.ts +127 -145
- package/src/tools/exit-plan-mode.ts +1 -0
- package/src/tools/gh.ts +120 -297
- package/src/tools/read.ts +13 -79
- package/src/utils/external-editor.ts +11 -5
- package/src/utils/git.ts +1400 -0
- package/src/web/search/render.ts +6 -4
- package/src/commit/git/errors.ts +0 -9
- package/src/commit/git/index.ts +0 -210
- package/src/commit/git/operations.ts +0 -54
- package/src/tools/gh-cli.ts +0 -125
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.19.0] - 2026-04-05
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added idle auto-compaction settings and scheduling so sessions can compact after inactive turns without auto-continuing.
|
|
9
|
+
- Added `onExternalEditor` callback to extension UI dialog options for handling external editor shortcut in select dialogs
|
|
10
|
+
- Added external editor shortcut support in plan review selector, allowing users to open and edit the plan in their configured editor
|
|
11
|
+
- Added `matchesAppExternalEditor` keybinding matcher to detect external editor shortcut (Ctrl+G or configured binding)
|
|
12
|
+
- Added `trimTrailingNewline` option to `openInEditor` function to preserve trailing newlines when editing files
|
|
13
|
+
- Added GitHub CLI utilities to git module (`utils/git.github`) with `available()`, `run()`, `json()`, and `text()` methods for GitHub CLI operations
|
|
14
|
+
- Exported git utilities from main package entry point for use by extensions
|
|
15
|
+
- Added comprehensive git utility module (`utils/git`) with organized namespaces for common git operations (branch, commit, diff, log, patch, ref, stage, status, head, repository)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Changed idle compaction settings (`compaction.idleThresholdTokens` and `compaction.idleTimeoutSeconds`) from enum to numeric type for flexible configuration
|
|
20
|
+
- Modified secret obfuscation to deobfuscate restored session messages for local display while keeping outbound LLM messages obfuscated
|
|
21
|
+
- Updated stash pop operation to preserve staged changes with `--index` flag when restoring after task branch merges
|
|
22
|
+
- Changed secret placeholders to deterministic hash-style redaction tokens and deobfuscated assistant output for local display.
|
|
23
|
+
- Updated hook editor and hook selector components to use `matchesAppExternalEditor` matcher for consistent external editor keybinding detection
|
|
24
|
+
- Modified plan review flow to read the latest plan content from disk before approval, allowing changes made in external editor to be reflected
|
|
25
|
+
- Enhanced plan review help text to dynamically display the configured external editor keybinding
|
|
26
|
+
- Refactored git operations to use centralized utility module instead of `ControlledGit` class throughout codebase
|
|
27
|
+
- Replaced `ControlledGit` dependency injection pattern with direct `cwd` parameter in commit agent tools
|
|
28
|
+
- Migrated git HEAD resolution in footer and status-line components to use new synchronous and asynchronous utilities
|
|
29
|
+
- Updated git status summary calculation in status-line component to use new git utility API
|
|
30
|
+
- Simplified git branch operations in task execution and cleanup to use new utility functions
|
|
31
|
+
- Refactored patch application logic in task worktree to use new git patch utilities
|
|
32
|
+
|
|
33
|
+
### Removed
|
|
34
|
+
|
|
35
|
+
- Removed `gh-cli.ts` module; GitHub CLI functionality now available via `utils/git.github`
|
|
36
|
+
- Removed `ControlledGit` class and associated git wrapper infrastructure from `commit/git` module
|
|
37
|
+
- Removed `mergeStdoutStderr` helper function from autoresearch git utilities
|
|
38
|
+
- Removed `findGitHeadPathAsync` and `findGitHeadPathSync` from modes/shared module (replaced by git utilities)
|
|
39
|
+
- Removed `./commit/git` export from package.json (internal diff parsing still available via `./commit/git/*`)
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Fixed idle compaction timer to properly cancel when event controller is disposed, preventing memory leaks
|
|
44
|
+
- Fixed session resumption to preserve the last non-empty session when starting a fresh session
|
|
45
|
+
- Fixed stash detection to use git ref resolution instead of output parsing for reliable stash state tracking
|
|
46
|
+
- Fixed isolated task merge-back to preserve task outputs on merge failure and stash dirty worktrees before cherry-pick.
|
|
47
|
+
- Fixed web search source rendering to truncate long title, metadata, and URL lines before they overflow the UI.
|
|
48
|
+
- Fixed PR checkout tool to resolve symlinks in worktree paths, ensuring consistent path references in results and metadata
|
|
49
|
+
- Fixed `read` output for file-backed internal URLs like `local://...` to include hashline prefixes in hashline edit mode, preserving usable line refs for follow-up edits
|
|
50
|
+
- Fixed the plan review selector to support the external editor shortcut for opening and updating the current plan from the approval screen
|
|
51
|
+
|
|
5
52
|
## [13.18.0] - 2026-04-02
|
|
6
53
|
### Breaking Changes
|
|
7
54
|
|
|
@@ -354,6 +401,9 @@
|
|
|
354
401
|
|
|
355
402
|
- Fixed resumed and session-switched GitHub Copilot/OpenAI Responses conversations replaying stale assistant native history from older saved sessions by sanitizing persisted assistant replay metadata on rehydration and resetting provider session state across live session boundaries ([#505](https://github.com/can1357/oh-my-pi/issues/505))
|
|
356
403
|
|
|
404
|
+
### Added
|
|
405
|
+
|
|
406
|
+
- Session observer overlay (`Ctrl+S`): view running subagent sessions with a picker and read-only transcript showing thinking, text, tool calls, and results
|
|
357
407
|
## [13.14.0] - 2026-03-20
|
|
358
408
|
|
|
359
409
|
### Added
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.
|
|
4
|
+
"version": "13.19.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
44
44
|
"@mozilla/readability": "^0.6",
|
|
45
|
-
"@oh-my-pi/omp-stats": "13.
|
|
46
|
-
"@oh-my-pi/pi-agent-core": "13.
|
|
47
|
-
"@oh-my-pi/pi-ai": "13.
|
|
48
|
-
"@oh-my-pi/pi-natives": "13.
|
|
49
|
-
"@oh-my-pi/pi-tui": "13.
|
|
50
|
-
"@oh-my-pi/pi-utils": "13.
|
|
45
|
+
"@oh-my-pi/omp-stats": "13.19.0",
|
|
46
|
+
"@oh-my-pi/pi-agent-core": "13.19.0",
|
|
47
|
+
"@oh-my-pi/pi-ai": "13.19.0",
|
|
48
|
+
"@oh-my-pi/pi-natives": "13.19.0",
|
|
49
|
+
"@oh-my-pi/pi-tui": "13.19.0",
|
|
50
|
+
"@oh-my-pi/pi-utils": "13.19.0",
|
|
51
51
|
"@sinclair/typebox": "^0.34",
|
|
52
52
|
"@xterm/headless": "^6.0",
|
|
53
53
|
"ajv": "^8.18",
|
|
@@ -146,10 +146,6 @@
|
|
|
146
146
|
"types": "./src/commit/changelog/*.ts",
|
|
147
147
|
"import": "./src/commit/changelog/*.ts"
|
|
148
148
|
},
|
|
149
|
-
"./commit/git": {
|
|
150
|
-
"types": "./src/commit/git/index.ts",
|
|
151
|
-
"import": "./src/commit/git/index.ts"
|
|
152
|
-
},
|
|
153
149
|
"./commit/git/*": {
|
|
154
150
|
"types": "./src/commit/git/*.ts",
|
|
155
151
|
"import": "./src/commit/git/*.ts"
|
package/src/autoresearch/git.ts
CHANGED
|
@@ -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(
|
|
21
|
-
const
|
|
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
|
|
32
|
-
if (
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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: ${
|
|
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(
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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;
|
|
@@ -7,6 +7,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
7
7
|
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
|
+
import * as git from "../../utils/git";
|
|
10
11
|
import { getAutoresearchFingerprintMismatchError, pathMatchesContractPath } from "../contract";
|
|
11
12
|
import { getCurrentAutoresearchBranch, parseWorkDirDirtyPaths } from "../git";
|
|
12
13
|
import {
|
|
@@ -493,7 +494,7 @@ function validateObservedStatus(
|
|
|
493
494
|
}
|
|
494
495
|
|
|
495
496
|
async function commitKeptExperiment(
|
|
496
|
-
|
|
497
|
+
_options: AutoresearchToolFactoryOptions,
|
|
497
498
|
workDir: string,
|
|
498
499
|
state: ExperimentState,
|
|
499
500
|
experiment: ExperimentResult,
|
|
@@ -503,25 +504,15 @@ async function commitKeptExperiment(
|
|
|
503
504
|
return { note: "nothing to commit" };
|
|
504
505
|
}
|
|
505
506
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
});
|
|
510
|
-
if (addResult.code !== 0) {
|
|
507
|
+
try {
|
|
508
|
+
await git.stage.files(workDir, scopeValidation.committablePaths);
|
|
509
|
+
} catch (err) {
|
|
511
510
|
return {
|
|
512
|
-
error: `git add failed: ${
|
|
511
|
+
error: `git add failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
513
512
|
};
|
|
514
513
|
}
|
|
515
514
|
|
|
516
|
-
|
|
517
|
-
"git",
|
|
518
|
-
["diff", "--cached", "--quiet", "--", ...scopeValidation.committablePaths],
|
|
519
|
-
{
|
|
520
|
-
cwd: workDir,
|
|
521
|
-
timeout: 10_000,
|
|
522
|
-
},
|
|
523
|
-
);
|
|
524
|
-
if (diffResult.code === 0) {
|
|
515
|
+
if (!(await git.diff.has(workDir, { cached: true, files: scopeValidation.committablePaths }))) {
|
|
525
516
|
return { note: "nothing to commit" };
|
|
526
517
|
}
|
|
527
518
|
|
|
@@ -533,32 +524,23 @@ async function commitKeptExperiment(
|
|
|
533
524
|
payload[name] = value;
|
|
534
525
|
}
|
|
535
526
|
const commitMessage = `${experiment.description}\n\nResult: ${JSON.stringify(payload)}`;
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
);
|
|
544
|
-
if (commitResult.code !== 0) {
|
|
527
|
+
let commitResultText = "";
|
|
528
|
+
try {
|
|
529
|
+
const commitResult = await git.commit(workDir, commitMessage, {
|
|
530
|
+
files: scopeValidation.committablePaths,
|
|
531
|
+
});
|
|
532
|
+
commitResultText = mergeStdoutStderr(commitResult);
|
|
533
|
+
} catch (err) {
|
|
545
534
|
return {
|
|
546
|
-
error: `git commit failed: ${
|
|
535
|
+
error: `git commit failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
547
536
|
};
|
|
548
537
|
}
|
|
549
538
|
|
|
550
|
-
const
|
|
551
|
-
cwd: workDir,
|
|
552
|
-
timeout: 5_000,
|
|
553
|
-
});
|
|
554
|
-
const newCommit = revParseResult.stdout.trim();
|
|
539
|
+
const newCommit = (await git.head.short(workDir, 7)) ?? "";
|
|
555
540
|
if (newCommit.length >= 7) {
|
|
556
541
|
experiment.commit = newCommit;
|
|
557
542
|
}
|
|
558
|
-
const summaryLine =
|
|
559
|
-
mergeStdoutStderr(commitResult)
|
|
560
|
-
.split("\n")
|
|
561
|
-
.find(line => line.trim().length > 0) ?? "committed";
|
|
543
|
+
const summaryLine = commitResultText.split("\n").find(line => line.trim().length > 0) ?? "committed";
|
|
562
544
|
return { note: summaryLine.trim() };
|
|
563
545
|
}
|
|
564
546
|
|
|
@@ -567,44 +549,46 @@ async function revertFailedExperiment(
|
|
|
567
549
|
workDir: string,
|
|
568
550
|
): Promise<KeepCommitResult> {
|
|
569
551
|
const preservedFiles = preserveAutoresearchFiles(workDir);
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
);
|
|
575
|
-
const cleanResult = await options.pi.exec("git", ["clean", "-fd", "--", "."], { cwd: workDir, timeout: 10_000 });
|
|
576
|
-
const cleanIgnoredResult = await options.pi.exec("git", ["clean", "-fdX", "--", "."], {
|
|
577
|
-
cwd: workDir,
|
|
578
|
-
timeout: 10_000,
|
|
579
|
-
});
|
|
580
|
-
restoreAutoresearchFiles(preservedFiles);
|
|
581
|
-
if (restoreResult.code !== 0) {
|
|
552
|
+
try {
|
|
553
|
+
await git.restore(workDir, { files: ["."], source: "HEAD", staged: true, worktree: true });
|
|
554
|
+
} catch (err) {
|
|
555
|
+
restoreAutoresearchFiles(preservedFiles);
|
|
582
556
|
return {
|
|
583
|
-
error: `git restore failed: ${
|
|
557
|
+
error: `git restore failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
584
558
|
};
|
|
585
559
|
}
|
|
586
|
-
|
|
560
|
+
try {
|
|
561
|
+
await git.clean(workDir, { paths: ["."] });
|
|
562
|
+
} catch (err) {
|
|
563
|
+
restoreAutoresearchFiles(preservedFiles);
|
|
587
564
|
return {
|
|
588
|
-
error: `git clean failed: ${
|
|
565
|
+
error: `git clean failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
589
566
|
};
|
|
590
567
|
}
|
|
591
|
-
|
|
568
|
+
try {
|
|
569
|
+
await git.clean(workDir, { ignoredOnly: true, paths: ["."] });
|
|
570
|
+
} catch (err) {
|
|
571
|
+
restoreAutoresearchFiles(preservedFiles);
|
|
592
572
|
return {
|
|
593
|
-
error: `git clean -X failed: ${
|
|
573
|
+
error: `git clean -X failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
594
574
|
};
|
|
595
575
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
576
|
+
restoreAutoresearchFiles(preservedFiles);
|
|
577
|
+
let dirtyStatus = "";
|
|
578
|
+
try {
|
|
579
|
+
dirtyStatus = await git.status(workDir, {
|
|
580
|
+
pathspecs: ["."],
|
|
581
|
+
porcelainV1: true,
|
|
582
|
+
untrackedFiles: "all",
|
|
583
|
+
z: true,
|
|
584
|
+
});
|
|
585
|
+
} catch (err) {
|
|
602
586
|
return {
|
|
603
|
-
error: `git status failed after cleanup: ${
|
|
587
|
+
error: `git status failed after cleanup: ${err instanceof Error ? err.message : String(err)}`,
|
|
604
588
|
};
|
|
605
589
|
}
|
|
606
590
|
const workDirPrefix = await readGitWorkDirPrefix(options, workDir);
|
|
607
|
-
const remainingDirtyPaths = parseWorkDirDirtyPaths(
|
|
591
|
+
const remainingDirtyPaths = parseWorkDirDirtyPaths(dirtyStatus, workDirPrefix).filter(
|
|
608
592
|
relativePath => !isAutoresearchLocalStatePath(relativePath),
|
|
609
593
|
);
|
|
610
594
|
if (remainingDirtyPaths.length > 0) {
|
|
@@ -654,21 +638,21 @@ async function validateKeepPaths(
|
|
|
654
638
|
return "Files in Scope is empty for the current segment. Re-run init_experiment after fixing autoresearch.md.";
|
|
655
639
|
}
|
|
656
640
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
return `git status failed: ${
|
|
641
|
+
let statusText: string;
|
|
642
|
+
try {
|
|
643
|
+
statusText = await git.status(workDir, {
|
|
644
|
+
pathspecs: ["."],
|
|
645
|
+
porcelainV1: true,
|
|
646
|
+
untrackedFiles: "all",
|
|
647
|
+
z: true,
|
|
648
|
+
});
|
|
649
|
+
} catch (err) {
|
|
650
|
+
return `git status failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
667
651
|
}
|
|
668
652
|
|
|
669
653
|
const workDirPrefix = await readGitWorkDirPrefix(options, workDir);
|
|
670
654
|
const committablePaths: string[] = [];
|
|
671
|
-
for (const normalizedPath of parseWorkDirDirtyPaths(
|
|
655
|
+
for (const normalizedPath of parseWorkDirDirtyPaths(statusText, workDirPrefix)) {
|
|
672
656
|
if (isAutoresearchLocalStatePath(normalizedPath)) {
|
|
673
657
|
continue;
|
|
674
658
|
}
|
|
@@ -808,9 +792,12 @@ function buildLogText(
|
|
|
808
792
|
}
|
|
809
793
|
|
|
810
794
|
async function readGitWorkDirPrefix(options: AutoresearchToolFactoryOptions, workDir: string): Promise<string> {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
795
|
+
void options;
|
|
796
|
+
try {
|
|
797
|
+
return await git.show.prefix(workDir);
|
|
798
|
+
} catch {
|
|
799
|
+
return "";
|
|
800
|
+
}
|
|
814
801
|
}
|
|
815
802
|
|
|
816
803
|
function truncateAsiValue(value: ASIData[string]): string {
|
|
@@ -2,7 +2,6 @@ import { INTENT_FIELD, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
|
2
2
|
import type { Api, Model } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { Markdown } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import type { ControlledGit } from "../../commit/git";
|
|
6
5
|
import typesDescriptionPrompt from "../../commit/prompts/types-description.md" with { type: "text" };
|
|
7
6
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
8
7
|
import { renderPromptTemplate } from "../../config/prompt-templates";
|
|
@@ -18,7 +17,6 @@ import { createCommitTools } from "./tools";
|
|
|
18
17
|
|
|
19
18
|
export interface CommitAgentInput {
|
|
20
19
|
cwd: string;
|
|
21
|
-
git: ControlledGit;
|
|
22
20
|
model: Model<Api>;
|
|
23
21
|
thinkingLevel?: ThinkingLevel;
|
|
24
22
|
settings: Settings;
|
|
@@ -46,7 +44,6 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
|
|
|
46
44
|
const spawns = "quick_task";
|
|
47
45
|
const tools = createCommitTools({
|
|
48
46
|
cwd: input.cwd,
|
|
49
|
-
git: input.git,
|
|
50
47
|
authStorage: input.authStorage,
|
|
51
48
|
modelRegistry: input.modelRegistry,
|
|
52
49
|
settings: input.settings,
|
|
@@ -4,7 +4,6 @@ import { $env, getProjectDir, isEnoent } from "@oh-my-pi/pi-utils";
|
|
|
4
4
|
import { applyChangelogProposals } from "../../commit/changelog";
|
|
5
5
|
import { detectChangelogBoundaries } from "../../commit/changelog/detect";
|
|
6
6
|
import { parseUnreleasedSection } from "../../commit/changelog/parse";
|
|
7
|
-
import { ControlledGit } from "../../commit/git";
|
|
8
7
|
import { formatCommitMessage } from "../../commit/message";
|
|
9
8
|
import { resolvePrimaryModel, resolveSmolModel } from "../../commit/model-selection";
|
|
10
9
|
import type { CommitCommandArgs, ConventionalAnalysis } from "../../commit/types";
|
|
@@ -12,6 +11,7 @@ import { ModelRegistry } from "../../config/model-registry";
|
|
|
12
11
|
import { renderPromptTemplate } from "../../config/prompt-templates";
|
|
13
12
|
import { Settings } from "../../config/settings";
|
|
14
13
|
import { discoverAuthStorage, discoverContextFiles } from "../../sdk";
|
|
14
|
+
import * as git from "../../utils/git";
|
|
15
15
|
import { type ExistingChangelogEntries, runCommitAgentSession } from "./agent";
|
|
16
16
|
import { generateFallbackProposal } from "./fallback";
|
|
17
17
|
import splitConfirmPrompt from "./prompts/split-confirm.md" with { type: "text" };
|
|
@@ -20,25 +20,24 @@ import { computeDependencyOrder } from "./topo-sort";
|
|
|
20
20
|
import { detectTrivialChange } from "./trivial";
|
|
21
21
|
|
|
22
22
|
interface CommitExecutionContext {
|
|
23
|
-
|
|
23
|
+
cwd: string;
|
|
24
24
|
dryRun: boolean;
|
|
25
25
|
push: boolean;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
|
|
29
29
|
const cwd = getProjectDir();
|
|
30
|
-
const git = new ControlledGit(cwd);
|
|
31
30
|
const [settings, authStorage] = await Promise.all([Settings.init({ cwd }), discoverAuthStorage()]);
|
|
32
31
|
|
|
33
32
|
process.stdout.write("● Resolving model...\n");
|
|
34
33
|
const modelRegistry = new ModelRegistry(authStorage);
|
|
35
34
|
await modelRegistry.refresh();
|
|
36
35
|
const stagedFilesPromise = (async () => {
|
|
37
|
-
let stagedFiles = await git.
|
|
36
|
+
let stagedFiles = await git.diff.changedFiles(cwd, { cached: true });
|
|
38
37
|
if (stagedFiles.length === 0) {
|
|
39
38
|
process.stdout.write("No staged changes detected, staging all changes...\n");
|
|
40
|
-
await git.
|
|
41
|
-
stagedFiles = await git.
|
|
39
|
+
await git.stage.files(cwd);
|
|
40
|
+
stagedFiles = await git.diff.changedFiles(cwd, { cached: true });
|
|
42
41
|
}
|
|
43
42
|
return stagedFiles;
|
|
44
43
|
})();
|
|
@@ -66,8 +65,8 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
|
|
|
66
65
|
const [changelogBoundaries, contextFiles, numstat, diff] = await Promise.all([
|
|
67
66
|
args.noChangelog ? [] : detectChangelogBoundaries(cwd, stagedFiles),
|
|
68
67
|
discoverContextFiles(cwd),
|
|
69
|
-
git.
|
|
70
|
-
git.
|
|
68
|
+
git.diff.numstat(cwd, { cached: true }),
|
|
69
|
+
git.diff(cwd, { cached: true }),
|
|
71
70
|
]);
|
|
72
71
|
const changelogTargets = changelogBoundaries.map(boundary => boundary.changelogPath);
|
|
73
72
|
if (!args.noChangelog) {
|
|
@@ -93,7 +92,7 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
|
|
|
93
92
|
if (forceFallback) {
|
|
94
93
|
process.stdout.write("● Forcing fallback commit generation...\n");
|
|
95
94
|
const fallbackProposal = generateFallbackProposal(numstat);
|
|
96
|
-
await runSingleCommit(fallbackProposal, {
|
|
95
|
+
await runSingleCommit(fallbackProposal, { cwd, dryRun: args.dryRun, push: args.push });
|
|
97
96
|
return;
|
|
98
97
|
}
|
|
99
98
|
|
|
@@ -110,7 +109,7 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
|
|
|
110
109
|
summary: trivialChange.summary,
|
|
111
110
|
warnings: [],
|
|
112
111
|
};
|
|
113
|
-
await runSingleCommit(trivialProposal, {
|
|
112
|
+
await runSingleCommit(trivialProposal, { cwd, dryRun: args.dryRun, push: args.push });
|
|
114
113
|
return;
|
|
115
114
|
}
|
|
116
115
|
|
|
@@ -129,7 +128,6 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
|
|
|
129
128
|
try {
|
|
130
129
|
commitState = await runCommitAgentSession({
|
|
131
130
|
cwd,
|
|
132
|
-
git,
|
|
133
131
|
model: agentModel,
|
|
134
132
|
thinkingLevel: agentThinkingLevel,
|
|
135
133
|
settings,
|
|
@@ -169,7 +167,6 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
|
|
|
169
167
|
}
|
|
170
168
|
process.stdout.write("● Applying changelog entries...\n");
|
|
171
169
|
const updated = await applyChangelogProposals({
|
|
172
|
-
git,
|
|
173
170
|
cwd,
|
|
174
171
|
proposals: commitState.changelogProposal.entries,
|
|
175
172
|
dryRun: args.dryRun,
|
|
@@ -188,13 +185,13 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
|
|
|
188
185
|
}
|
|
189
186
|
|
|
190
187
|
if (commitState.proposal) {
|
|
191
|
-
await runSingleCommit(commitState.proposal, {
|
|
188
|
+
await runSingleCommit(commitState.proposal, { cwd, dryRun: args.dryRun, push: args.push });
|
|
192
189
|
return;
|
|
193
190
|
}
|
|
194
191
|
|
|
195
192
|
if (commitState.splitProposal) {
|
|
196
193
|
await runSplitCommit(commitState.splitProposal, {
|
|
197
|
-
|
|
194
|
+
cwd,
|
|
198
195
|
dryRun: args.dryRun,
|
|
199
196
|
push: args.push,
|
|
200
197
|
additionalFiles: updatedChangelogFiles,
|
|
@@ -215,10 +212,10 @@ async function runSingleCommit(proposal: CommitProposal, ctx: CommitExecutionCon
|
|
|
215
212
|
process.stdout.write(`${commitMessage}\n`);
|
|
216
213
|
return;
|
|
217
214
|
}
|
|
218
|
-
await
|
|
215
|
+
await git.commit(ctx.cwd, commitMessage);
|
|
219
216
|
process.stdout.write("Commit created.\n");
|
|
220
217
|
if (ctx.push) {
|
|
221
|
-
await
|
|
218
|
+
await git.push(ctx.cwd);
|
|
222
219
|
process.stdout.write("Pushed to remote.\n");
|
|
223
220
|
}
|
|
224
221
|
}
|
|
@@ -233,7 +230,7 @@ async function runSplitCommit(
|
|
|
233
230
|
if (ctx.additionalFiles && ctx.additionalFiles.length > 0) {
|
|
234
231
|
appendFilesToLastCommit(plan, ctx.additionalFiles);
|
|
235
232
|
}
|
|
236
|
-
const stagedFiles = await
|
|
233
|
+
const stagedFiles = await git.diff.changedFiles(ctx.cwd, { cached: true });
|
|
237
234
|
const plannedFiles = new Set(plan.commits.flatMap(commit => commit.changes.map(change => change.path)));
|
|
238
235
|
const missingFiles = stagedFiles.filter(file => !plannedFiles.has(file));
|
|
239
236
|
if (missingFiles.length > 0) {
|
|
@@ -270,10 +267,10 @@ async function runSplitCommit(
|
|
|
270
267
|
throw new Error(order.error);
|
|
271
268
|
}
|
|
272
269
|
|
|
273
|
-
await
|
|
270
|
+
await git.stage.reset(ctx.cwd);
|
|
274
271
|
for (const commitIndex of order) {
|
|
275
272
|
const commit = plan.commits[commitIndex];
|
|
276
|
-
await
|
|
273
|
+
await git.stage.hunks(ctx.cwd, commit.changes);
|
|
277
274
|
const analysis: ConventionalAnalysis = {
|
|
278
275
|
type: commit.type,
|
|
279
276
|
scope: commit.scope,
|
|
@@ -281,12 +278,12 @@ async function runSplitCommit(
|
|
|
281
278
|
issueRefs: commit.issueRefs,
|
|
282
279
|
};
|
|
283
280
|
const message = formatCommitMessage(analysis, commit.summary);
|
|
284
|
-
await
|
|
285
|
-
await
|
|
281
|
+
await git.commit(ctx.cwd, message);
|
|
282
|
+
await git.stage.reset(ctx.cwd);
|
|
286
283
|
}
|
|
287
284
|
process.stdout.write("Split commits created.\n");
|
|
288
285
|
if (ctx.push) {
|
|
289
|
-
await
|
|
286
|
+
await git.push(ctx.cwd);
|
|
290
287
|
process.stdout.write("Pushed to remote.\n");
|
|
291
288
|
}
|
|
292
289
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { CommitAgentState } from "../../../commit/agentic/state";
|
|
3
|
-
import type { ControlledGit } from "../../../commit/git";
|
|
4
3
|
import type { CustomTool } from "../../../extensibility/custom-tools/types";
|
|
4
|
+
import * as git from "../../../utils/git";
|
|
5
5
|
|
|
6
6
|
const TARGET_TOKENS = 30000;
|
|
7
7
|
const CHARS_PER_TOKEN = 4;
|
|
@@ -136,10 +136,7 @@ const gitFileDiffSchema = Type.Object({
|
|
|
136
136
|
staged: Type.Optional(Type.Boolean({ description: "Use staged changes (default: true)" })),
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
export function createGitFileDiffTool(
|
|
140
|
-
git: ControlledGit,
|
|
141
|
-
state: CommitAgentState,
|
|
142
|
-
): CustomTool<typeof gitFileDiffSchema> {
|
|
139
|
+
export function createGitFileDiffTool(cwd: string, state: CommitAgentState): CustomTool<typeof gitFileDiffSchema> {
|
|
143
140
|
return {
|
|
144
141
|
name: "git_file_diff",
|
|
145
142
|
label: "Git File Diff",
|
|
@@ -167,7 +164,7 @@ export function createGitFileDiffTool(
|
|
|
167
164
|
|
|
168
165
|
if (uncachedFiles.length > 0) {
|
|
169
166
|
for (const file of uncachedFiles) {
|
|
170
|
-
const diff = await git.
|
|
167
|
+
const diff = await git.diff(cwd, { cached: staged, files: [file] });
|
|
171
168
|
if (diff) {
|
|
172
169
|
diffs.set(file, diff);
|
|
173
170
|
state.diffCache.set(cacheKey(file), diff);
|