@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.
- package/CHANGELOG.md +316 -1
- package/package.json +86 -24
- package/scripts/format-prompts.ts +2 -2
- package/src/autoresearch/apply-contract-to-state.ts +24 -0
- package/src/autoresearch/contract.ts +0 -44
- package/src/autoresearch/dashboard.ts +1 -2
- package/src/autoresearch/git.ts +116 -30
- package/src/autoresearch/helpers.ts +49 -0
- package/src/autoresearch/index.ts +28 -187
- package/src/autoresearch/prompt.md +26 -9
- package/src/autoresearch/state.ts +0 -6
- package/src/autoresearch/tools/init-experiment.ts +202 -117
- package/src/autoresearch/tools/log-experiment.ts +123 -178
- package/src/autoresearch/tools/run-experiment.ts +48 -10
- package/src/autoresearch/types.ts +2 -2
- package/src/capability/index.ts +4 -2
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/grep-cli.ts +8 -8
- package/src/cli/grievances-cli.ts +78 -0
- package/src/cli/read-cli.ts +67 -0
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/update-cli.ts +3 -3
- package/src/cli.ts +2 -0
- package/src/commands/grep.ts +6 -1
- package/src/commands/grievances.ts +20 -0
- package/src/commands/read.ts +33 -0
- package/src/commit/agentic/agent.ts +5 -8
- package/src/commit/agentic/index.ts +22 -26
- package/src/commit/agentic/tools/analyze-file.ts +3 -3
- 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/agentic/validation.ts +1 -1
- package/src/commit/analysis/conventional.ts +4 -4
- package/src/commit/analysis/summary.ts +3 -3
- package/src/commit/changelog/generate.ts +4 -4
- package/src/commit/changelog/index.ts +5 -9
- package/src/commit/map-reduce/map-phase.ts +4 -4
- package/src/commit/map-reduce/reduce-phase.ts +4 -4
- package/src/commit/pipeline.ts +13 -16
- package/src/config/keybindings.ts +7 -6
- package/src/config/prompt-templates.ts +44 -226
- package/src/config/resolve-config-value.ts +4 -2
- package/src/config/settings-schema.ts +98 -2
- package/src/config/settings.ts +25 -26
- package/src/dap/client.ts +674 -0
- package/src/dap/config.ts +150 -0
- package/src/dap/defaults.json +211 -0
- package/src/dap/index.ts +4 -0
- package/src/dap/session.ts +1255 -0
- package/src/dap/types.ts +600 -0
- package/src/debug/log-viewer.ts +3 -2
- package/src/discovery/builtin.ts +1 -2
- package/src/discovery/codex.ts +2 -2
- package/src/discovery/github.ts +2 -1
- package/src/discovery/helpers.ts +2 -2
- package/src/discovery/opencode.ts +2 -2
- package/src/edit/diff.ts +818 -0
- package/src/edit/index.ts +309 -0
- package/src/edit/line-hash.ts +67 -0
- package/src/edit/modes/chunk.ts +454 -0
- package/src/{patch → edit/modes}/hashline.ts +741 -361
- package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
- package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
- package/src/{patch → edit}/normalize.ts +97 -76
- package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
- package/src/exec/bash-executor.ts +4 -2
- package/src/exec/idle-timeout-watchdog.ts +126 -0
- package/src/exec/non-interactive-env.ts +5 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
- package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
- package/src/extensibility/custom-commands/loader.ts +1 -2
- package/src/extensibility/custom-tools/loader.ts +34 -11
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/loader.ts +9 -4
- package/src/extensibility/extensions/runner.ts +24 -1
- package/src/extensibility/extensions/types.ts +4 -2
- package/src/extensibility/hooks/loader.ts +5 -6
- package/src/extensibility/hooks/types.ts +2 -2
- package/src/extensibility/plugins/doctor.ts +2 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
- package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
- package/src/extensibility/slash-commands.ts +3 -7
- package/src/index.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/ipy/executor.ts +58 -17
- package/src/ipy/gateway-coordinator.ts +6 -4
- package/src/ipy/kernel.ts +45 -22
- package/src/ipy/runtime.ts +2 -2
- package/src/lsp/client.ts +7 -4
- package/src/lsp/clients/lsp-linter-client.ts +4 -4
- package/src/lsp/config.ts +2 -2
- package/src/lsp/defaults.json +688 -154
- package/src/lsp/index.ts +234 -45
- package/src/lsp/lspmux.ts +2 -2
- package/src/lsp/startup-events.ts +13 -0
- package/src/lsp/types.ts +12 -1
- package/src/lsp/utils.ts +8 -1
- package/src/main.ts +125 -47
- package/src/memories/index.ts +4 -5
- package/src/modes/acp/acp-agent.ts +563 -163
- package/src/modes/acp/acp-event-mapper.ts +9 -1
- package/src/modes/acp/acp-mode.ts +4 -2
- package/src/modes/components/agent-dashboard.ts +3 -4
- package/src/modes/components/diff.ts +6 -7
- 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/read-tool-group.ts +6 -12
- package/src/modes/components/session-observer-overlay.ts +472 -0
- package/src/modes/components/settings-defs.ts +24 -0
- package/src/modes/components/status-line.ts +15 -61
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/btw-controller.ts +2 -2
- package/src/modes/controllers/command-controller.ts +4 -2
- 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 +15 -8
- package/src/modes/controllers/selector-controller.ts +26 -0
- package/src/modes/index.ts +20 -2
- package/src/modes/interactive-mode.ts +278 -69
- package/src/modes/rpc/host-tools.ts +186 -0
- package/src/modes/rpc/rpc-client.ts +178 -13
- package/src/modes/rpc/rpc-mode.ts +73 -3
- package/src/modes/rpc/rpc-types.ts +53 -1
- package/src/modes/session-observer-registry.ts +146 -0
- package/src/modes/shared.ts +0 -42
- package/src/modes/theme/theme.ts +80 -8
- package/src/modes/types.ts +4 -2
- 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 +8 -1
- package/src/prompts/tools/chunk-edit.md +219 -0
- package/src/prompts/tools/debug.md +43 -0
- package/src/prompts/tools/grep.md +3 -0
- package/src/prompts/tools/lsp.md +5 -5
- package/src/prompts/tools/read-chunk.md +17 -0
- package/src/prompts/tools/read.md +19 -5
- package/src/sdk.ts +216 -165
- package/src/secrets/index.ts +1 -1
- package/src/secrets/obfuscator.ts +25 -17
- package/src/session/agent-session.ts +381 -286
- package/src/session/agent-storage.ts +12 -12
- package/src/session/compaction/branch-summarization.ts +3 -3
- package/src/session/compaction/compaction.ts +5 -6
- package/src/session/compaction/utils.ts +3 -3
- package/src/session/history-storage.ts +62 -19
- package/src/session/messages.ts +3 -3
- package/src/session/session-dump-format.ts +203 -0
- package/src/session/session-manager.ts +15 -5
- package/src/session/session-storage.ts +4 -2
- package/src/session/streaming-output.ts +1 -1
- package/src/session/tool-choice-queue.ts +213 -0
- package/src/slash-commands/builtin-registry.ts +56 -8
- package/src/ssh/connection-manager.ts +2 -2
- package/src/ssh/sshfs-mount.ts +5 -5
- package/src/stt/downloader.ts +4 -4
- package/src/stt/recorder.ts +4 -4
- package/src/stt/transcriber.ts +2 -2
- package/src/system-prompt.ts +25 -13
- package/src/task/agents.ts +5 -6
- package/src/task/commands.ts +2 -5
- package/src/task/executor.ts +32 -4
- package/src/task/index.ts +91 -82
- package/src/task/template.ts +2 -2
- package/src/task/types.ts +25 -0
- package/src/task/worktree.ts +131 -149
- package/src/tools/ask.ts +2 -3
- package/src/tools/ast-edit.ts +7 -7
- package/src/tools/ast-grep.ts +7 -7
- package/src/tools/auto-generated-guard.ts +36 -41
- package/src/tools/await-tool.ts +2 -2
- package/src/tools/bash.ts +5 -23
- package/src/tools/browser.ts +4 -5
- package/src/tools/calculator.ts +2 -3
- package/src/tools/cancel-job.ts +2 -2
- package/src/tools/checkpoint.ts +3 -3
- package/src/tools/debug.ts +1007 -0
- package/src/tools/exit-plan-mode.ts +3 -3
- package/src/tools/fetch.ts +67 -3
- package/src/tools/find.ts +4 -5
- package/src/tools/fs-cache-invalidation.ts +5 -0
- package/src/tools/gemini-image.ts +13 -5
- package/src/tools/gh.ts +130 -308
- package/src/tools/grep.ts +57 -9
- package/src/tools/index.ts +44 -22
- package/src/tools/inspect-image.ts +4 -4
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/python.ts +19 -6
- package/src/tools/read.ts +211 -146
- package/src/tools/render-mermaid.ts +2 -3
- package/src/tools/render-utils.ts +20 -6
- package/src/tools/renderers.ts +3 -1
- package/src/tools/report-tool-issue.ts +80 -0
- package/src/tools/resolve.ts +70 -39
- package/src/tools/search-tool-bm25.ts +2 -2
- package/src/tools/ssh.ts +2 -2
- package/src/tools/todo-write.ts +2 -2
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/tools/write.ts +5 -6
- package/src/tui/tree-list.ts +3 -1
- package/src/utils/clipboard.ts +80 -0
- package/src/utils/commit-message-generator.ts +2 -3
- package/src/utils/edit-mode.ts +49 -0
- package/src/utils/external-editor.ts +11 -5
- package/src/utils/file-display-mode.ts +6 -5
- package/src/utils/file-mentions.ts +8 -7
- package/src/utils/git.ts +1400 -0
- package/src/utils/image-loading.ts +98 -0
- package/src/utils/title-generator.ts +2 -3
- package/src/utils/tools-manager.ts +6 -6
- package/src/web/scrapers/choosealicense.ts +1 -1
- package/src/web/search/index.ts +3 -3
- package/src/web/search/render.ts +6 -4
- package/src/autoresearch/command-initialize.md +0 -34
- 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/patch/diff.ts +0 -433
- package/src/patch/index.ts +0 -888
- package/src/patch/parser.ts +0 -532
- package/src/patch/types.ts +0 -292
- package/src/prompts/agents/oracle.md +0 -77
- package/src/tools/gh-cli.ts +0 -125
- package/src/tools/pending-action.ts +0 -49
- package/src/utils/child-process.ts +0 -88
- package/src/utils/frontmatter.ts +0 -117
- package/src/utils/image-input.ts +0 -274
- package/src/utils/mime.ts +0 -53
- package/src/utils/prompt-format.ts +0 -170
package/src/task/worktree.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { Dirent } from "node:fs";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
4
|
+
import * as path from "node:path";
|
|
5
5
|
import { projfsOverlayStart, projfsOverlayStop } from "@oh-my-pi/pi-natives";
|
|
6
|
-
import { getWorktreeDir, isEnoent, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { $which, getWorktreeDir, isEnoent, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { $ } from "bun";
|
|
8
|
+
import * as git from "../utils/git";
|
|
8
9
|
|
|
9
10
|
/** Baseline state for a single git repository. */
|
|
10
11
|
export interface RepoBaseline {
|
|
@@ -27,14 +28,11 @@ export function getEncodedProjectName(cwd: string): string {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export async function getRepoRoot(cwd: string): Promise<string> {
|
|
30
|
-
const
|
|
31
|
-
if (result.exitCode !== 0) {
|
|
32
|
-
throw new Error("Git repository not found for isolated task execution.");
|
|
33
|
-
}
|
|
34
|
-
const repoRoot = result.text().trim();
|
|
31
|
+
const repoRoot = await git.repo.root(cwd);
|
|
35
32
|
if (!repoRoot) {
|
|
36
|
-
throw new Error("Git repository
|
|
33
|
+
throw new Error("Git repository not found for isolated task execution.");
|
|
37
34
|
}
|
|
35
|
+
|
|
38
36
|
return repoRoot;
|
|
39
37
|
}
|
|
40
38
|
|
|
@@ -54,26 +52,16 @@ export async function ensureWorktree(baseCwd: string, id: string): Promise<strin
|
|
|
54
52
|
const encodedProject = getEncodedProjectName(repoRoot);
|
|
55
53
|
const worktreeDir = getWorktreeDir(encodedProject, id);
|
|
56
54
|
await fs.mkdir(path.dirname(worktreeDir), { recursive: true });
|
|
57
|
-
await
|
|
55
|
+
await git.worktree.tryRemove(repoRoot, worktreeDir);
|
|
58
56
|
await fs.rm(worktreeDir, { recursive: true, force: true });
|
|
59
|
-
await
|
|
57
|
+
await git.worktree.add(repoRoot, worktreeDir, "HEAD", { detach: true });
|
|
60
58
|
return worktreeDir;
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
/** Find nested git repositories (non-submodule) under the given root. */
|
|
64
62
|
async function discoverNestedRepos(repoRoot: string): Promise<string[]> {
|
|
65
63
|
// Get submodule paths so we can exclude them
|
|
66
|
-
const
|
|
67
|
-
.cwd(repoRoot)
|
|
68
|
-
.quiet()
|
|
69
|
-
.nothrow()
|
|
70
|
-
.text();
|
|
71
|
-
const submodulePaths = new Set(
|
|
72
|
-
submoduleRaw
|
|
73
|
-
.split("\n")
|
|
74
|
-
.map(l => l.trim())
|
|
75
|
-
.filter(Boolean),
|
|
76
|
-
);
|
|
64
|
+
const submodulePaths = new Set(await git.ls.submodules(repoRoot));
|
|
77
65
|
|
|
78
66
|
// Find all .git dirs/files that aren't the root or known submodules
|
|
79
67
|
const result: string[] = [];
|
|
@@ -109,14 +97,10 @@ async function discoverNestedRepos(repoRoot: string): Promise<string[]> {
|
|
|
109
97
|
}
|
|
110
98
|
|
|
111
99
|
async function captureRepoBaseline(repoRoot: string): Promise<RepoBaseline> {
|
|
112
|
-
const headCommit = (await
|
|
113
|
-
const staged = await
|
|
114
|
-
const unstaged = await
|
|
115
|
-
const
|
|
116
|
-
const untracked = untrackedRaw
|
|
117
|
-
.split("\n")
|
|
118
|
-
.map(line => line.trim())
|
|
119
|
-
.filter(line => line.length > 0);
|
|
100
|
+
const headCommit = (await git.head.sha(repoRoot)) ?? "";
|
|
101
|
+
const staged = await git.diff(repoRoot, { binary: true, cached: true });
|
|
102
|
+
const unstaged = await git.diff(repoRoot, { binary: true });
|
|
103
|
+
const untracked = await git.ls.untracked(repoRoot);
|
|
120
104
|
return { repoRoot, headCommit, staged, unstaged, untracked };
|
|
121
105
|
}
|
|
122
106
|
|
|
@@ -131,35 +115,10 @@ export async function captureBaseline(repoRoot: string): Promise<WorktreeBaselin
|
|
|
131
115
|
return { root, nested };
|
|
132
116
|
}
|
|
133
117
|
|
|
134
|
-
async function writeTempPatchFile(patch: string): Promise<string> {
|
|
135
|
-
const tempPath = path.join(os.tmpdir(), `omp-task-patch-${Snowflake.next()}.patch`);
|
|
136
|
-
await Bun.write(tempPath, patch);
|
|
137
|
-
return tempPath;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function applyPatch(
|
|
141
|
-
cwd: string,
|
|
142
|
-
patch: string,
|
|
143
|
-
options?: { cached?: boolean; env?: Record<string, string> },
|
|
144
|
-
): Promise<void> {
|
|
145
|
-
if (!patch.trim()) return;
|
|
146
|
-
const tempPath = await writeTempPatchFile(patch);
|
|
147
|
-
try {
|
|
148
|
-
const command = options?.cached ? $`git apply --cached --binary ${tempPath}` : $`git apply --binary ${tempPath}`;
|
|
149
|
-
let runner = command.cwd(cwd).quiet();
|
|
150
|
-
if (options?.env) {
|
|
151
|
-
runner = runner.env(options.env);
|
|
152
|
-
}
|
|
153
|
-
await runner;
|
|
154
|
-
} finally {
|
|
155
|
-
await fs.rm(tempPath, { force: true });
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
118
|
async function applyRepoBaseline(worktreeDir: string, rb: RepoBaseline, sourceRoot: string): Promise<void> {
|
|
160
|
-
await
|
|
161
|
-
await
|
|
162
|
-
await
|
|
119
|
+
await git.patch.applyText(worktreeDir, rb.staged, { cached: true });
|
|
120
|
+
await git.patch.applyText(worktreeDir, rb.staged);
|
|
121
|
+
await git.patch.applyText(worktreeDir, rb.unstaged);
|
|
163
122
|
|
|
164
123
|
for (const entry of rb.untracked) {
|
|
165
124
|
const source = path.join(sourceRoot, entry);
|
|
@@ -193,15 +152,12 @@ export async function applyBaseline(worktreeDir: string, baseline: WorktreeBasel
|
|
|
193
152
|
// Commit baseline state so captureRepoDeltaPatch can cleanly subtract it.
|
|
194
153
|
// Without this, `git add -A && git commit` by the task would include
|
|
195
154
|
// baseline untracked files in the diff-tree output.
|
|
196
|
-
|
|
197
|
-
await
|
|
198
|
-
|
|
199
|
-
if (hasChanges) {
|
|
200
|
-
await $`git add -A`.cwd(nestedDir).quiet();
|
|
201
|
-
await $`git commit -m omp-baseline --allow-empty`.cwd(nestedDir).quiet();
|
|
155
|
+
if ((await git.status(nestedDir)).trim().length > 0) {
|
|
156
|
+
await git.stage.files(nestedDir);
|
|
157
|
+
await git.commit(nestedDir, "omp-baseline", { allowEmpty: true });
|
|
202
158
|
// Update baseline to reflect the committed state — prevents double-apply
|
|
203
159
|
// in captureRepoDeltaPatch's temp-index path
|
|
204
|
-
entry.baseline.headCommit = (await
|
|
160
|
+
entry.baseline.headCommit = (await git.head.sha(nestedDir)) ?? "";
|
|
205
161
|
entry.baseline.staged = "";
|
|
206
162
|
entry.baseline.unstaged = "";
|
|
207
163
|
entry.baseline.untracked = [];
|
|
@@ -209,32 +165,9 @@ export async function applyBaseline(worktreeDir: string, baseline: WorktreeBasel
|
|
|
209
165
|
}
|
|
210
166
|
}
|
|
211
167
|
|
|
212
|
-
async function applyPatchToIndex(cwd: string, patch: string, indexFile: string): Promise<void> {
|
|
213
|
-
if (!patch.trim()) return;
|
|
214
|
-
const tempPath = await writeTempPatchFile(patch);
|
|
215
|
-
try {
|
|
216
|
-
await $`git apply --cached --binary ${tempPath}`
|
|
217
|
-
.cwd(cwd)
|
|
218
|
-
.env({
|
|
219
|
-
GIT_INDEX_FILE: indexFile,
|
|
220
|
-
})
|
|
221
|
-
.quiet();
|
|
222
|
-
} finally {
|
|
223
|
-
await fs.rm(tempPath, { force: true });
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async function listUntracked(cwd: string): Promise<string[]> {
|
|
228
|
-
const raw = await $`git ls-files --others --exclude-standard`.cwd(cwd).quiet().text();
|
|
229
|
-
return raw
|
|
230
|
-
.split("\n")
|
|
231
|
-
.map(line => line.trim())
|
|
232
|
-
.filter(line => line.length > 0);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
168
|
async function captureRepoDeltaPatch(repoDir: string, rb: RepoBaseline): Promise<string> {
|
|
236
169
|
// Check if HEAD advanced (task committed changes)
|
|
237
|
-
const currentHead = (await
|
|
170
|
+
const currentHead = (await git.head.sha(repoDir)) ?? "";
|
|
238
171
|
const headAdvanced = currentHead && currentHead !== rb.headCommit;
|
|
239
172
|
|
|
240
173
|
if (headAdvanced) {
|
|
@@ -242,28 +175,31 @@ async function captureRepoDeltaPatch(repoDir: string, rb: RepoBaseline): Promise
|
|
|
242
175
|
const parts: string[] = [];
|
|
243
176
|
|
|
244
177
|
// Committed changes since baseline
|
|
245
|
-
const committedDiff = await
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
.text();
|
|
178
|
+
const committedDiff = await git.diff.tree(repoDir, rb.headCommit, currentHead, {
|
|
179
|
+
allowFailure: true,
|
|
180
|
+
binary: true,
|
|
181
|
+
});
|
|
250
182
|
if (committedDiff.trim()) parts.push(committedDiff);
|
|
251
183
|
|
|
252
184
|
// Uncommitted changes on top of the new HEAD
|
|
253
|
-
const staged = await
|
|
254
|
-
const unstaged = await
|
|
185
|
+
const staged = await git.diff(repoDir, { binary: true, cached: true });
|
|
186
|
+
const unstaged = await git.diff(repoDir, { binary: true });
|
|
255
187
|
if (staged.trim()) parts.push(staged);
|
|
256
188
|
if (unstaged.trim()) parts.push(unstaged);
|
|
257
189
|
|
|
258
190
|
// New untracked files (relative to both baseline and current tracking)
|
|
259
|
-
const currentUntracked = await
|
|
191
|
+
const currentUntracked = await git.ls.untracked(repoDir);
|
|
260
192
|
const baselineUntracked = new Set(rb.untracked);
|
|
261
193
|
const newUntracked = currentUntracked.filter(entry => !baselineUntracked.has(entry));
|
|
262
194
|
if (newUntracked.length > 0) {
|
|
263
195
|
const nullPath = getGitNoIndexNullPath();
|
|
264
196
|
const untrackedDiffs = await Promise.all(
|
|
265
197
|
newUntracked.map(entry =>
|
|
266
|
-
|
|
198
|
+
git.diff(repoDir, {
|
|
199
|
+
allowFailure: true,
|
|
200
|
+
binary: true,
|
|
201
|
+
noIndex: { left: nullPath, right: entry },
|
|
202
|
+
}),
|
|
267
203
|
),
|
|
268
204
|
);
|
|
269
205
|
parts.push(...untrackedDiffs.filter(d => d.trim()));
|
|
@@ -275,12 +211,23 @@ async function captureRepoDeltaPatch(repoDir: string, rb: RepoBaseline): Promise
|
|
|
275
211
|
// HEAD unchanged: use temp index approach (subtracts baseline from delta)
|
|
276
212
|
const tempIndex = path.join(os.tmpdir(), `omp-task-index-${Snowflake.next()}`);
|
|
277
213
|
try {
|
|
278
|
-
await
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
214
|
+
await git.readTree(repoDir, rb.headCommit, {
|
|
215
|
+
env: { GIT_INDEX_FILE: tempIndex },
|
|
216
|
+
});
|
|
217
|
+
await git.patch.applyText(repoDir, rb.staged, {
|
|
218
|
+
cached: true,
|
|
219
|
+
env: { GIT_INDEX_FILE: tempIndex },
|
|
220
|
+
});
|
|
221
|
+
await git.patch.applyText(repoDir, rb.unstaged, {
|
|
222
|
+
cached: true,
|
|
223
|
+
env: { GIT_INDEX_FILE: tempIndex },
|
|
224
|
+
});
|
|
225
|
+
const diff = await git.diff(repoDir, {
|
|
226
|
+
binary: true,
|
|
227
|
+
env: { GIT_INDEX_FILE: tempIndex },
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const currentUntracked = await git.ls.untracked(repoDir);
|
|
284
231
|
const baselineUntracked = new Set(rb.untracked);
|
|
285
232
|
const newUntracked = currentUntracked.filter(entry => !baselineUntracked.has(entry));
|
|
286
233
|
|
|
@@ -289,7 +236,11 @@ async function captureRepoDeltaPatch(repoDir: string, rb: RepoBaseline): Promise
|
|
|
289
236
|
const nullPath = getGitNoIndexNullPath();
|
|
290
237
|
const untrackedDiffs = await Promise.all(
|
|
291
238
|
newUntracked.map(entry =>
|
|
292
|
-
|
|
239
|
+
git.diff(repoDir, {
|
|
240
|
+
allowFailure: true,
|
|
241
|
+
binary: true,
|
|
242
|
+
noIndex: { left: nullPath, right: entry },
|
|
243
|
+
}),
|
|
293
244
|
),
|
|
294
245
|
);
|
|
295
246
|
return `${diff}${diff && !diff.endsWith("\n") ? "\n" : ""}${untrackedDiffs.join("\n")}`;
|
|
@@ -355,29 +306,25 @@ export async function applyNestedPatches(
|
|
|
355
306
|
|
|
356
307
|
const combinedDiff = repoPatches.map(p => p.patch).join("\n");
|
|
357
308
|
for (const { patch } of repoPatches) {
|
|
358
|
-
await
|
|
309
|
+
await git.patch.applyText(nestedDir, patch);
|
|
359
310
|
}
|
|
360
311
|
|
|
361
312
|
// Commit so nested repo history reflects the task changes
|
|
362
|
-
|
|
363
|
-
await $`git --no-optional-locks status --porcelain`.cwd(nestedDir).quiet().nothrow().text()
|
|
364
|
-
).trim();
|
|
365
|
-
if (hasChanges) {
|
|
313
|
+
if ((await git.status(nestedDir)).trim().length > 0) {
|
|
366
314
|
const msg = (await commitMessage?.(combinedDiff)) ?? "changes from isolated task(s)";
|
|
367
|
-
await
|
|
368
|
-
await
|
|
315
|
+
await git.stage.files(nestedDir);
|
|
316
|
+
await git.commit(nestedDir, msg);
|
|
369
317
|
}
|
|
370
318
|
}
|
|
371
319
|
}
|
|
372
320
|
|
|
373
321
|
export async function cleanupWorktree(dir: string): Promise<void> {
|
|
374
322
|
try {
|
|
375
|
-
const
|
|
376
|
-
const commonDir =
|
|
377
|
-
if (commonDir) {
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
await $`git worktree remove -f ${dir}`.cwd(repoRoot).quiet().nothrow();
|
|
323
|
+
const repository = await git.repo.resolve(dir);
|
|
324
|
+
const commonDir = repository?.commonDir ?? "";
|
|
325
|
+
if (commonDir && path.basename(commonDir) === ".git") {
|
|
326
|
+
const repoRoot = path.dirname(commonDir);
|
|
327
|
+
await git.worktree.tryRemove(repoRoot, dir);
|
|
381
328
|
}
|
|
382
329
|
} finally {
|
|
383
330
|
await fs.rm(dir, { recursive: true, force: true });
|
|
@@ -401,7 +348,7 @@ export async function ensureFuseOverlay(baseCwd: string, id: string): Promise<st
|
|
|
401
348
|
const mergedDir = path.join(baseDir, "merged");
|
|
402
349
|
|
|
403
350
|
// Clean up any stale mount at this path (linux only)
|
|
404
|
-
const fusermount =
|
|
351
|
+
const fusermount = $which("fusermount3") ?? $which("fusermount");
|
|
405
352
|
if (fusermount) {
|
|
406
353
|
await $`${fusermount} -u ${mergedDir}`.quiet().nothrow();
|
|
407
354
|
}
|
|
@@ -411,7 +358,7 @@ export async function ensureFuseOverlay(baseCwd: string, id: string): Promise<st
|
|
|
411
358
|
await fs.mkdir(workDir, { recursive: true });
|
|
412
359
|
await fs.mkdir(mergedDir, { recursive: true });
|
|
413
360
|
|
|
414
|
-
const binary =
|
|
361
|
+
const binary = $which("fuse-overlayfs");
|
|
415
362
|
if (!binary) {
|
|
416
363
|
await fs.rm(baseDir, { recursive: true, force: true });
|
|
417
364
|
throw new Error(
|
|
@@ -433,7 +380,7 @@ export async function ensureFuseOverlay(baseCwd: string, id: string): Promise<st
|
|
|
433
380
|
|
|
434
381
|
export async function cleanupFuseOverlay(mergedDir: string): Promise<void> {
|
|
435
382
|
try {
|
|
436
|
-
const fusermount =
|
|
383
|
+
const fusermount = $which("fusermount3") ?? $which("fusermount");
|
|
437
384
|
if (fusermount) {
|
|
438
385
|
await $`${fusermount} -u ${mergedDir}`.quiet().nothrow();
|
|
439
386
|
}
|
|
@@ -518,33 +465,31 @@ export async function commitToBranch(
|
|
|
518
465
|
|
|
519
466
|
// Only create a branch if the root repo has changes
|
|
520
467
|
if (rootPatch.trim()) {
|
|
521
|
-
await
|
|
468
|
+
await git.branch.create(repoRoot, branchName);
|
|
522
469
|
const tmpDir = path.join(os.tmpdir(), `omp-branch-${Snowflake.next()}`);
|
|
523
470
|
try {
|
|
524
|
-
await
|
|
525
|
-
const patchPath = path.join(os.tmpdir(), `omp-branch-patch-${Snowflake.next()}.patch`);
|
|
471
|
+
await git.worktree.add(repoRoot, tmpDir, branchName);
|
|
526
472
|
try {
|
|
527
|
-
await
|
|
528
|
-
|
|
529
|
-
if (
|
|
530
|
-
const stderr =
|
|
473
|
+
await git.patch.applyText(tmpDir, rootPatch);
|
|
474
|
+
} catch (err) {
|
|
475
|
+
if (err instanceof git.GitCommandError) {
|
|
476
|
+
const stderr = err.result.stderr.slice(0, 2000);
|
|
531
477
|
logger.error("commitToBranch: git apply failed", {
|
|
532
478
|
taskId,
|
|
533
|
-
exitCode:
|
|
479
|
+
exitCode: err.result.exitCode,
|
|
534
480
|
stderr,
|
|
535
481
|
patchSize: rootPatch.length,
|
|
536
482
|
patchHead: rootPatch.slice(0, 500),
|
|
537
483
|
});
|
|
538
484
|
throw new Error(`git apply failed for task ${taskId}: ${stderr}`);
|
|
539
485
|
}
|
|
540
|
-
|
|
541
|
-
await fs.rm(patchPath, { force: true });
|
|
486
|
+
throw err;
|
|
542
487
|
}
|
|
543
|
-
await
|
|
488
|
+
await git.stage.files(tmpDir);
|
|
544
489
|
const msg = (commitMessage && (await commitMessage(rootPatch))) || fallbackMessage;
|
|
545
|
-
await
|
|
490
|
+
await git.commit(tmpDir, msg);
|
|
546
491
|
} finally {
|
|
547
|
-
await
|
|
492
|
+
await git.worktree.tryRemove(repoRoot, tmpDir);
|
|
548
493
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
549
494
|
}
|
|
550
495
|
}
|
|
@@ -570,29 +515,66 @@ export async function mergeTaskBranches(
|
|
|
570
515
|
const merged: string[] = [];
|
|
571
516
|
const failed: string[] = [];
|
|
572
517
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const stderr = result.stderr.toString().trim();
|
|
579
|
-
failed.push(branchName);
|
|
580
|
-
return {
|
|
581
|
-
merged,
|
|
582
|
-
failed: [...failed, ...branches.slice(merged.length + failed.length).map(b => b.branchName)],
|
|
583
|
-
conflict: `${branchName}: ${stderr}`,
|
|
584
|
-
};
|
|
585
|
-
}
|
|
518
|
+
// Stash dirty working tree so cherry-pick can operate on a clean HEAD.
|
|
519
|
+
// Without this, cherry-pick refuses to run when uncommitted changes exist.
|
|
520
|
+
const didStash = await git.stash.push(repoRoot, "omp-task-merge");
|
|
521
|
+
|
|
522
|
+
let conflictResult: MergeBranchResult | undefined;
|
|
586
523
|
|
|
587
|
-
|
|
524
|
+
try {
|
|
525
|
+
for (const { branchName } of branches) {
|
|
526
|
+
try {
|
|
527
|
+
await git.cherryPick(repoRoot, branchName);
|
|
528
|
+
} catch (err) {
|
|
529
|
+
try {
|
|
530
|
+
await git.cherryPick.abort(repoRoot);
|
|
531
|
+
} catch {
|
|
532
|
+
/* no state to abort */
|
|
533
|
+
}
|
|
534
|
+
const stderr =
|
|
535
|
+
err instanceof git.GitCommandError
|
|
536
|
+
? err.result.stderr.trim()
|
|
537
|
+
: err instanceof Error
|
|
538
|
+
? err.message
|
|
539
|
+
: String(err);
|
|
540
|
+
failed.push(branchName);
|
|
541
|
+
conflictResult = {
|
|
542
|
+
merged,
|
|
543
|
+
failed: [...failed, ...branches.slice(merged.length + failed.length).map(b => b.branchName)],
|
|
544
|
+
conflict: `${branchName}: ${stderr}`,
|
|
545
|
+
};
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
merged.push(branchName);
|
|
550
|
+
}
|
|
551
|
+
} finally {
|
|
552
|
+
if (didStash) {
|
|
553
|
+
try {
|
|
554
|
+
await git.stash.pop(repoRoot, { index: true });
|
|
555
|
+
} catch {
|
|
556
|
+
// Stash-pop conflicts mean the replayed changes clash with the user's
|
|
557
|
+
// uncommitted edits. Treat this as a merge failure so the caller preserves
|
|
558
|
+
// recovery branches instead of reporting success and deleting them.
|
|
559
|
+
logger.warn("Failed to restore stashed changes after task merge; stash entry preserved");
|
|
560
|
+
if (!conflictResult) {
|
|
561
|
+
conflictResult = {
|
|
562
|
+
merged,
|
|
563
|
+
failed: merged,
|
|
564
|
+
conflict:
|
|
565
|
+
"stash pop: cherry-picked changes conflict with uncommitted edits. Run `git stash pop` and resolve manually.",
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
588
570
|
}
|
|
589
571
|
|
|
590
|
-
return { merged, failed };
|
|
572
|
+
return conflictResult ?? { merged, failed };
|
|
591
573
|
}
|
|
592
574
|
|
|
593
575
|
/** Clean up temporary task branches. */
|
|
594
576
|
export async function cleanupTaskBranches(repoRoot: string, branches: string[]): Promise<void> {
|
|
595
577
|
for (const branch of branches) {
|
|
596
|
-
await
|
|
578
|
+
await git.branch.tryDelete(repoRoot, branch);
|
|
597
579
|
}
|
|
598
580
|
}
|
package/src/tools/ask.ts
CHANGED
|
@@ -17,9 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
19
19
|
import { type Component, Container, Markdown, renderInlineMarkdown, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
20
|
-
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
20
|
+
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
21
21
|
import { type Static, Type } from "@sinclair/typebox";
|
|
22
|
-
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
23
22
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
24
23
|
import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
|
|
25
24
|
import askDescription from "../prompts/tools/ask.md" with { type: "text" };
|
|
@@ -385,7 +384,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
385
384
|
readonly strict = true;
|
|
386
385
|
|
|
387
386
|
constructor(private readonly session: ToolSession) {
|
|
388
|
-
this.description =
|
|
387
|
+
this.description = prompt.render(askDescription);
|
|
389
388
|
}
|
|
390
389
|
|
|
391
390
|
static createIf(session: ToolSession): AskTool | null {
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -3,12 +3,11 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
|
|
|
3
3
|
import { type AstReplaceChange, astEdit } from "@oh-my-pi/pi-natives";
|
|
4
4
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
6
|
-
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
|
-
import {
|
|
8
|
+
import { computeLineHash } from "../edit/line-hash";
|
|
9
9
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
10
|
import type { Theme } from "../modes/theme/theme";
|
|
11
|
-
import { computeLineHash } from "../patch/hashline";
|
|
12
11
|
import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
|
|
13
12
|
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
14
13
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
@@ -31,6 +30,7 @@ import {
|
|
|
31
30
|
PARSE_ERRORS_LIMIT,
|
|
32
31
|
PREVIEW_LIMITS,
|
|
33
32
|
} from "./render-utils";
|
|
33
|
+
import { queueResolveHandler } from "./resolve";
|
|
34
34
|
import { ToolError } from "./tool-errors";
|
|
35
35
|
import { toolResult } from "./tool-result";
|
|
36
36
|
|
|
@@ -71,7 +71,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
71
71
|
readonly strict = true;
|
|
72
72
|
readonly deferrable = true;
|
|
73
73
|
constructor(private readonly session: ToolSession) {
|
|
74
|
-
this.description =
|
|
74
|
+
this.description = prompt.render(astEditDescription);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
async execute(
|
|
@@ -201,7 +201,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
201
201
|
filesSearched: result.filesSearched,
|
|
202
202
|
applied: result.applied,
|
|
203
203
|
limitReached: result.limitReached,
|
|
204
|
-
parseErrors: dedupedParseErrors,
|
|
204
|
+
...(dedupedParseErrors.length > 0 ? { parseErrors: dedupedParseErrors } : {}),
|
|
205
205
|
scopePath,
|
|
206
206
|
files: fileList,
|
|
207
207
|
fileReplacements: [],
|
|
@@ -289,7 +289,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
289
289
|
if (!result.applied && result.totalReplacements > 0) {
|
|
290
290
|
const previewReplacementPlural = result.totalReplacements !== 1 ? "s" : "";
|
|
291
291
|
const previewFilePlural = result.filesTouched !== 1 ? "s" : "";
|
|
292
|
-
this.session
|
|
292
|
+
queueResolveHandler(this.session, {
|
|
293
293
|
label: `AST Edit: ${result.totalReplacements} replacement${previewReplacementPlural} in ${result.filesTouched} file${previewFilePlural}`,
|
|
294
294
|
sourceToolName: this.name,
|
|
295
295
|
apply: async (_reason: string) => {
|
|
@@ -311,7 +311,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
311
311
|
filesSearched: applyResult.filesSearched,
|
|
312
312
|
applied: applyResult.applied,
|
|
313
313
|
limitReached: applyResult.limitReached,
|
|
314
|
-
parseErrors: dedupedApplyParseErrors,
|
|
314
|
+
...(dedupedApplyParseErrors.length > 0 ? { parseErrors: dedupedApplyParseErrors } : {}),
|
|
315
315
|
scopePath,
|
|
316
316
|
files: fileList,
|
|
317
317
|
fileReplacements,
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -3,12 +3,11 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
|
|
|
3
3
|
import { type AstFindMatch, astGrep } from "@oh-my-pi/pi-natives";
|
|
4
4
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
6
|
-
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
|
-
import {
|
|
8
|
+
import { computeLineHash } from "../edit/line-hash";
|
|
9
9
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
10
|
import type { Theme } from "../modes/theme/theme";
|
|
11
|
-
import { computeLineHash } from "../patch/hashline";
|
|
12
11
|
import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text" };
|
|
13
12
|
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
14
13
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
@@ -65,7 +64,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
65
64
|
readonly strict = true;
|
|
66
65
|
|
|
67
66
|
constructor(private readonly session: ToolSession) {
|
|
68
|
-
this.description =
|
|
67
|
+
this.description = prompt.render(astGrepDescription);
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
async execute(
|
|
@@ -188,7 +187,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
188
187
|
fileCount: result.filesWithMatches,
|
|
189
188
|
filesSearched: result.filesSearched,
|
|
190
189
|
limitReached: result.limitReached,
|
|
191
|
-
parseErrors: dedupedParseErrors,
|
|
190
|
+
...(dedupedParseErrors.length > 0 ? { parseErrors: dedupedParseErrors } : {}),
|
|
192
191
|
scopePath,
|
|
193
192
|
files: fileList,
|
|
194
193
|
fileMatches: [],
|
|
@@ -213,12 +212,13 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
213
212
|
const lineNumbers = matchLines.map((_, index) => match.startLine + index);
|
|
214
213
|
const lineWidth = Math.max(...lineNumbers.map(value => value.toString().length));
|
|
215
214
|
const formatLine = (lineNumber: number, line: string, isMatch: boolean): string => {
|
|
215
|
+
const separator = isMatch ? ":" : "-";
|
|
216
216
|
if (useHashLines) {
|
|
217
217
|
const ref = `${lineNumber}#${computeLineHash(lineNumber, line)}`;
|
|
218
|
-
return
|
|
218
|
+
return `${ref}${separator}${line}`;
|
|
219
219
|
}
|
|
220
220
|
const padded = lineNumber.toString().padStart(lineWidth, " ");
|
|
221
|
-
return
|
|
221
|
+
return `${padded}${separator}${line}`;
|
|
222
222
|
};
|
|
223
223
|
for (let index = 0; index < matchLines.length; index++) {
|
|
224
224
|
outputLines.push(formatLine(match.startLine + index, matchLines[index], index === 0));
|