38 Commits

Author SHA1 Message Date
can1357 b279db1790 test: refactored test suites to eliminate time-based sleeps and polling loops
- Replaced time-based sleeps and polling loops with event-driven promise resolvers and fake timers across agent and tool tests.
- Migrated test suites to share in-memory auth storage and fixtures using lifecycle hooks.
- Updated catalog model definitions, metadata, and configurations.
2026-08-13 19:32:22 +02:00
roboomp 48a7287c68 fix(coding-agent): replayed commits over dirty parent
- Seeded dirty-baseline blobs into the parent object database before reconstructing filtered agent commits.
- Used three-way synthetic-tree application for committed and trailing task state while preserving parent WIP.
- Added a focused merge regression covering unrelated edits in the same tracked file.

Fixes #6135
2026-07-21 21:34:27 +00:00
can1357 df9b04a0eb fix(task): canonicalize the shared-common-dir gate in detachGitDir
rev-parse --git-common-dir resolves symlinks while ensureIsolation derives
sourceCommonDir lexically from the session cwd (resolveRepository walks
path.resolve'd components). On any symlinked repo path (macOS /tmp,
symlinked project dirs) the lexical comparison missed, detachGitDir
returned "independent", and the parent-mutation leak silently survived.
Realpath both sides before comparing; regression test drives the gate
through a symlink alias.
2026-07-18 19:42:36 +02:00
can1357 53437aff3d fix(task): harden detachGitDir edge handling
- Match the rcopy worktree-add registration via realpath: git canonicalizes
  the admin gitdir back-reference (macOS /var -> /private/var), so the
  lexical comparison missed it and left a stale registration in the source
  repo's worktree list.
- Carry core.fileMode so an explicit filemode=false source does not read as
  mode-changed files in the detached isolation.
- Carry core.splitIndex and the sharedindex.* files referenced by a split
  source index; restoring the raw index without them broke every git read.
- Carry the source shallow boundary file so history traversal over the
  borrowed object DB stops at the boundary instead of failing.
- Regression test covering all three carries.
2026-07-18 19:42:36 +02:00
roboomp 970043bd8a fix(task): preserve sparse-checkout state when detaching isolation
A fresh `git init` in detachGitDir dropped core.sparseCheckout and the
sparse-checkout patterns, and rebuilding the index via write-tree/
read-tree discarded skip-worktree bits. Files intentionally absent from
a sparse working tree then read as deletions, which delta capture could
apply back to the parent.

detachGitDir now restores the index verbatim (preserving skip-worktree,
assume-unchanged, and exact stage entries) and carries
core.sparseCheckout, core.sparseCheckoutCone, and info/sparse-checkout
into the detached .git before restoring the index. Falls back to
read-tree HEAD only when the source had no index.

Fixes #6003
2026-07-18 17:04:40 +00:00
roboomp 37304a12a3 fix(task): detach unborn linked worktrees from parent checkout
A linked git worktree with an unborn HEAD (a fresh/orphan branch with no
commits) still shares the parent's common dir, so an isolated task's
first branch and commit would write into the parent repo. The previous
early return on a missing HEAD SHA left that shared metadata intact.

detachGitDir now severs unborn worktrees too: `git init -b <branch>`
preserves the checked-out branch name, ref freezing is gated on a born
HEAD, and the rcopy worktree registration is still removed.

Fixes #6003
2026-07-18 16:57:41 +00:00
roboomp afea682b7f fix(task): detach isolated worktree git dir from parent checkout
Copy isolation backends (reflink/apfs/btrfs/zfs/block-clone/rcopy)
materialise the worktree by duplicating its `.git` verbatim. When the
parent is a linked git worktree its `.git` is a pointer file, so the
isolation shared the parent's HEAD/index/ref namespace: a task's
`git checkout`/`commit` moved the parent's branch, and the rcopy
`git worktree add` path stacked task branches in the shared namespace.

`ensureIsolation` now runs `git.detachGitDir` after `isoStart`, turning
each isolation into a standalone repo with a frozen HEAD/refs/index
snapshot that borrows the source object database via
`objects/info/alternates`. Isolated git ops stay private, every task
branch is parented on the requested base, and patch/branch capture
(`git fetch <merged>`) still resolves objects.

Fixes #6003
2026-07-18 16:48:33 +00:00
roboomp 12acf7e645 fix(task): auto-skipped empty commits during task-branch cherry-pick
An intermediate commit whose net effect is already on HEAD (redundant
change, or 3-way merged to HEAD by "theirs == ours") stopped the
sequencer with "The previous cherry-pick is now empty" and was
treated as a hard conflict. mergeTaskBranches aborted the whole range,
marked the branch failed, and dropped every remaining non-overlapping
commit.

Add cherryPick.skip and cherryPick.isEmptyError to the git namespace,
then in mergeTaskBranches' catch classify the failure before aborting:
loop --skip while the error stderr matches the "now empty" phrase so
consecutive empties advance the sequencer; fall through to abort/fail
on the first non-empty error (genuine conflict with unmerged files).

Fixes #4438
2026-07-03 14:19:16 +00:00
can1357 a23d1d6554 merge PR #4140: fix(coding-agent): stopped isolated task merges failing when working tree carries WIP for files the agent also modifies (@roboomp) 2026-07-02 10:30:06 +02:00
can1357 e8a8c2402c fix(coding-agent): prevented asynchronous write races in plan reviews
- Ensures in-memory overlay edits are durably written to the plan file before proceeding with approval.
- Avoids asynchronous write races by awaiting the final plan file serialization.
- Aligns synthetic approved-plan prompts with reference-only expectations.
2026-07-02 02:19:26 +02:00
roboomp e109883ee2 fix(coding-agent/task): treated stash cleanup paths literally
Failed stash-pop cleanup now invokes git clean with literal pathspecs for
stash-derived untracked paths. Filenames such as `:(glob)*` are valid POSIX
filenames and valid Git pathspec magic; passing them as ordinary pathspecs with
`-x` could delete unrelated ignored artifacts that were never stashed and are
not recoverable from the preserved stash.

Extend the fallback regression with a literal `:(glob)*` stash file and an
ignored `build.log` that must survive cleanup.

Fixes #4175
2026-07-01 22:03:23 +00:00
roboomp 4d471b1aa4 fix(coding-agent/task): removed ignored restored stash files after pop failure
When a task branch adds ignore rules for a path that was untracked in the
user's stashed WIP, a failed stash pop can restore the file and then leave it
hidden from normal status after reset. Default `git clean -fd -- <path>` does
not remove ignored files, so the partial restore could still leak into later
isolated task baselines.

Add an includeIgnored clean mode and use `git clean -fdx -- <stash path>` for
failed stash-pop cleanup. Extend the fallback regression so the task branch adds
.gitignore for the restored untracked path and verify both normal and ignored
status return clean.

Fixes #4175
2026-07-01 21:52:29 +00:00
roboomp abd0e2bdcc fix(coding-agent/task): cleaned untracked files after failed stash pop
A failed `git stash pop --index` can restore unrelated untracked files before
exiting on a tracked conflict while still preserving the stash entry. The
previous fallback only reset tracked/index state, leaving those untracked files
in the working tree for subsequent task baselines.

Record the top stash entry's untracked paths before popping and clean exactly
those paths if the pop fails after preflight. Add a regression that forces the
fallback branch and verifies the worktree returns clean with the stash preserved.

Fixes #4175
2026-07-01 21:44:31 +00:00
roboomp ffd6a57d2f fix(coding-agent/task): kept .git/index clean when stash pop conflicts after task merge
mergeTaskBranches and applyNestedPatches both stashed dirty WIP, cherry-picked
task branches, then called `git stash pop` in a finally block. On conflict git
left stage 1/2/3 unmerged entries in .git/index with no MERGE_HEAD to abort;
neither call cleaned up. The corrupted index persisted indefinitely, and every
subsequent overlay-isolated task inherited it through the lower layer —
captureRepoDeltaPatch then emitted `diff --cc` (combined merge format) that
git apply rejects with "No valid patches in input", failing every downstream
task merge with 'Branch merge failed before a task branch could be created'.

Fix at the git API level: git.stash.tryPop now runs `git apply --3way --check`
on `git stash show -p --binary stash@{0}` before popping (`--3way` matches
what git stash pop does internally, so context that drifted after cherry-pick
is still accepted). Preflight failure short-circuits — stash entry preserved,
index untouched. Preflight pass falls through to pop; if pop still leaves
unmerged entries (mode-only or delete/modify conflicts the preflight can miss),
a `reset --hard HEAD" fallback restores the merged HEAD without losing the
cherry-picked commits (stash is preserved by git on failed pop, so the user's
WIP stays recoverable).

Both call sites now share this contract via git.stash.tryPop.

Fixes #4175
2026-07-01 21:36:17 +00:00
roboomp ecbf0760cf fix(coding-agent): guarded leftover WIP seed by filtered-commit landings
Tracking raw agent-commit count meant an agent that committed only its
inherited baseline WIP (via `git add -A`) then left the real edit
uncommitted collapsed every filtered patch to empty, so tmpDir never
advanced past baselineSha yet the leftover path assumed it had.

replayFilteredAgentCommits now counts filtered commits actually applied
and, when zero landed, bypasses the finalFilteredTree/leftoverPatch
synthesis entirely — writeSyntheticTree(HEAD, [rootPatch]) fails hard
whenever rootPatch has HEAD+WIP context for a file missing from HEAD's
index (untracked / staged-new WIP). Instead, commit rootPatch directly
with WIP seed, matching the no-agent-commit path.

Added regression test covering the reviewer's scenario.
2026-07-01 12:49:28 +00:00
roboomp 286e971bfe fix(coding-agent): stopped isolated task merges failing when working tree carries WIP for files the agent also modifies
captureRepoDeltaPatch records the delta against `HEAD + WIP`, so the
patch's context lines and blob SHAs reference the WIP-modified files.
commitPatchToBranchWorktree then tried to apply that patch to a fresh
worktree pinned at HEAD, which failed hard whenever the WIP-side file
was missing from HEAD's index (untracked WIP files, staged-new WIP
files) or when --3way could not resolve an overlap.

commitPatchToBranchWorktree now tries plain apply first, then `--3way`
(which cleanly subtracts WIP via the shared ODB blob for tracked files),
and only when both fail replays baseline WIP into the temp worktree so
the delta's context lines up, rewinding WIP-only files afterward so
they never leak into the branch commit.

Added git.ls.tree helper for the WIP-only-file filter and a set of
regression tests covering the untracked, staged-new, and overlap
scenarios.

Fixes #4136
2026-07-01 12:24:32 +00:00
can1357 8cd23ebd15 merge #3844: 3-way dirty-context fallback for isolated branch merges
# Conflicts:
#	packages/coding-agent/src/task/worktree.ts
#	packages/coding-agent/test/task/worktree.test.ts
2026-06-30 03:03:43 +02:00
roboomp d120ba6b7d fix(coding-agent): filter baseline wip from preserved agent commits
Dirty isolated baselines can be accidentally committed by subagents that run git add -A. Fetching the raw isolation HEAD then cherry-picking the range would replay that baseline WIP into parent history.

Add a dirty-baseline replay path that rewrites each agent commit against the captured baseline tree, preserving the agent commit message and author while excluding staged, unstaged, and untracked changes that existed before isolation started. Clean baselines still use the raw git fetch path, and nested-only changes keep returning patches without creating an empty root branch.

Add a regression for baseline staged + untracked WIP committed by the agent, asserting the task branch contains only the agent file and parent WIP remains staged/untracked after merge.

Fixes #3842
2026-06-30 00:28:27 +00:00
roboomp da715aae7c fix(coding-agent): preserve agent commits across isolated branch merges
When an isolated task agent commits its own changes before yielding, the
harness used to collapse the captured delta into one AI-summarized commit
and discard the agent's commit messages and authorship entirely. This
violated commit discipline for agentic swarms — multiple logical commits
("fix bug" + "add test") became a single opaque commit, and the
agent's commit object (which lived in isolation/.git/objects under
overlayfs/rcopy) was lost when cleanupIsolation tore down the overlay.

commitToBranch now detects when isolation HEAD moved past baseline.root
.headCommit. When it has, the function git-fetches the agent's HEAD into
the parent repo as omp/task/${taskId} so the commit objects survive
cleanupIsolation, and stamps the captured baselineSha onto the returned
CommitToBranchResult. mergeTaskBranches cherry-picks the inclusive range
baseSha..branchName when baseSha is provided, replaying each agent
commit verbatim with its original message and author. Any uncommitted
leftover (staged, unstaged, untracked) on top of the agent's last commit
becomes one trailing AI-summarized commit on the same branch.

Falls back to the legacy single-commit path when the agent never moved
HEAD (purely dirty working tree); existing patch-mode flow is untouched.

Fixes #3842
2026-06-30 00:13:07 +00:00
roboomp 4b98211c64 fix(coding-agent): fixed dirty isolated branch merges
Applied isolated branch patches with three-way fallback when unrelated parent dirt appears in patch context.

Surfaced branch preparation failures instead of reporting no changes.

Fixes #3841
2026-06-30 00:09:34 +00:00
roboomp 58c0a305d6 fix(agent): shortened isolated task paths
Used compact hashed isolation directory segments and the short m mount dir so long task ids are not copied into subagent working paths.

Kept worktree cleanup compatible with legacy merged task-isolation directories.

Fixes #3756
2026-06-28 21:25:20 +00:00
oldschoola a2854ba768 fix: migrate coding-agent tests from fs.rm to removeWithRetries
Migrate 203 test files (356 call sites) from fs.rm/fs.rmSync to
removeWithRetries/removeSyncWithRetries to reduce EBUSY test failures
on Windows. removeWithRetries is now exported from @oh-my-pi/pi-utils.

The migration uses a regex-based approach that:
- Replaces fs.rm(path, { recursive, force }) → removeWithRetries(path)
- Replaces fs.rmSync(path, { recursive, force }) → removeSyncWithRetries(path)
- Replaces fs.rm(path) → removeWithRetries(path) (no options)
- Skips fs.rm/fs.rmSync inside template literals (bun --eval scripts)
- Adds imports to existing @oh-my-pi/pi-utils import or creates new one
- Removes unused fs imports where fs.rm was the only fs usage (4 files)
2026-06-23 15:28:05 -07:00
roboomp 5b6e9f904d fix(eval): paused timeout over baseline capture and surfaced nested stash-restore failures
Two Codex P2 findings landed against 978d2a76d0 that were not in the previously delivered review event:

1) prepareIsolationContext() (which runs captureBaseline → walks nested repos and untracked diffs) was running OUTSIDE withBridgeTimeoutPause; on dirty/large repos the baseline walk can exceed the eval idle timeout while the runtime is blocked. Moved the prep call into the pause closure so the watchdog is suspended for the whole bridge call from prep through cleanup.

2) applyNestedPatches() swallowed git stash pop failures with only a logger.warn, so a stash-pop conflict after a successful agent commit was invisible to the workflow. Changed the helper to return Promise<string[]> of warnings; applyEligibleNestedPatches now wraps them in a <system-notification> appended to the merge summary so the caller actually sees the partial-success case.

Added regression tests:
- bridge: prepare fires after timeout-pause and before timeout-resume.
- runner: applyEligibleNestedPatches surfaces stash-restore warnings as a system-notification.
- worktree (real git): a pre-existing dirty edit on the same file the agent patches causes stash pop to conflict; the helper returns a warning naming the nested repo and the stash entry is preserved for manual recovery.

Fixes #3196
2026-06-22 21:57:35 +02:00
can1357 2f2acaf928 Merge pull request #3205: feat(eval): isolated/apply/merge options for agent() helper
Resolves conflict in test/task/worktree.test.ts by keeping both the
getRepoRoot (main) and applyNestedPatches (PR) describe blocks.

Extends the PR's Python/JS work to the remaining workflow runtimes:
- eval/rb/prelude.rb, eval/jl/prelude.jl: agent() now accepts and
  forwards isolated/apply/merge (as booleans) plus returnHandle, and the
  return_handle node carries isolated/patch_path/branch_name/
  nested_patches/changes_applied/isolation_summary.

Post-merge fixups:
- task/index.ts: drop dead commitStyle var (the dedup refactor reads
  task.isolation.commits inside makeIsolationCommitMessage).
- CHANGELOG: move the misplaced Added entry under [Unreleased], correct
  the stale "defaults track task.isolation.mode" wording to the final
  strict opt-in behavior, and note all four runtimes.

Fixes #3196
2026-06-22 21:56:45 +02:00
roboomp abc9a8f292 fix(task): restored nested stash with index state
git stash pop without --index restores stashed staged changes as unstaged. When a nested repo had staged WIP before the isolated agent ran, the pop in applyNestedPatches() brought the content back but lost the user's index state.

Pass { index: true } so pop uses --index, matching the root merge path that already does the same thing.

Added a regression test that stages a pre-existing edit in the nested repo, runs applyNestedPatches, and asserts the file is still in the index (porcelain "M  " with the trailing space) and the cached diff still shows the staged WIP.

Fixes #3196
2026-06-22 19:41:59 +00:00
roboomp 978d2a76d0 fix(task): preserved nested-repo dirty state across nested patch apply
applyNestedPatches() applied the captured patch then ran git.stage.files(nestedDir), which stages every working-tree change in the nested repo. A nested repo that was already dirty before the agent ran ended up with the user's unrelated work-in-progress committed alongside the agent delta.

Stash any pre-existing dirty state (tracked + untracked) before applying the patch and pop it back in the finally block after the commit, so the agent commit contains only the captured patch and the user's in-flight work is restored on top of it. A failing stash pop logs a warning and leaves the stash entry intact for manual recovery; the broader nested-apply failure path is already non-fatal.

Added a worktree integration test that confirms a pre-existing untracked file in the nested repo is not staged into the agent commit and is still present in the working tree afterwards.

Fixes #3196
2026-06-22 19:34:42 +00:00
can1357 f353ae0067 chore: biome format after PR integration 2026-06-21 17:20:22 +02:00
can1357 e8b60408b2 Merge PR #1939: fix(coding-agent): guard Git-mutating automation in pure jj workspaces (@roboomp)
# Conflicts:
#	packages/coding-agent/src/task/worktree.ts
#	packages/coding-agent/test/task/worktree.test.ts
2026-06-21 17:05:15 +02:00
can1357 6385afdfb7 test(coding-agent): replaced Bun.sleep and wall-clock timing
- Replaced Bun.sleep and wall-clock timing with fake timers (vi.useFakeTimers), release gates, and deterministic polling across 15+ test files to eliminate flakiness and improve speed.
- Consolidated per-test fixture setup into beforeAll/afterAll lifecycle hooks across 20+ test files, reducing redundant initialization and improving test performance by reusing shared immutable fixtures.
- Stubbed network calls in ModelRegistry and test discovery to prevent unintended outbound requests during test execution.
- Replaced subprocess-based test coordination (file markers, Bun.sleep polling) with in-memory fakes (FakeWebSocket, FakeLspServer, VirtualClock) for deterministic, fast test execution.
2026-06-15 11:48:55 +02:00
can1357 9d457f73d9 test: migrated test imports to package subpath exports
- Replaced relative `../src` imports with `@oh-my-pi/pi-ai` and `@oh-my-pi/pi-agent-core` subpaths.
2026-06-08 19:03:55 +02:00
roboomp b6aafab3c1 fix(coding-agent): made isPureJjRepo depth-aware so nested git checkouts win
Previously `isPureJjRepo` returned true whenever the resolved jj and git roots merely differed. That punishes the legitimate inverse-nesting case: a real git checkout (vendored repo, fixture, independent nested checkout) living UNDER an outer pure jj workspace. Both `jj.repo.root` and `git.repo.root` walk upward from cwd and return the closest ancestor — so the deeper root is the one the user is actually working inside, and Git automation against the inner checkout never touches the surrounding jj tree.

`isPureJjRepo` now returns true iff jj is the *deeper* ancestor (or no git is present at all). The new `isStrictDescendant` helper does the depth check via `path.relative`. Added unit + integration tests covering both nesting directions on all three surfaces (`utils/jj`, `task/worktree`, `autoresearch/git`).

Refs #1935
2026-06-05 14:49:44 +00:00
roboomp d7c7d2b0e1 fix(coding-agent): guarded nested pure jj before outer-git fallback
Previously `getRepoRoot` and `ensureAutoresearchBranch` only consulted `jj.isPureJjRepo` after `git.repo.root` returned null. For a jj workspace nested under an unrelated outer Git checkout, `git.repo.root` walks up and finds the outer .git, so the pure-jj branch never fired and isolation/autoresearch silently mutated the surrounding Git tree behind jj's back.

Both call sites now run the pure-jj guard first, rejecting at the jj workspace's own root regardless of any surrounding Git checkout. Added integration tests for the nested case on both surfaces.

Refs #1935
2026-06-05 14:44:05 +00:00
roboomp 2dcb0c8aba style: bun run fix 2026-06-05 14:39:44 +00:00
roboomp 7ba2227c3c fix(coding-agent): guarded Git-mutating automation in pure jj workspaces
Worktree setup and autoresearch Git prep silently misbehaved in pure Jujutsu workspaces (`.jj/repo/` present, no colocated `.git/`):

- `task/worktree.ts#getRepoRoot` threw a generic "Git repository not found for isolated task execution.", giving a jj user no hint about what was wrong.

- `autoresearch/git.ts#ensureAutoresearchBranch` returned a soft "Not in a git repository" warning, letting `/autoresearch` proceed with no branch isolation, baseline reset, or auto-commits.

Both paths now detect a pure jj workspace via a new `jj.isPureJjRepo` helper and surface an actionable Jujutsu-specific error pointing at `jj git init --colocate`. Colocated jj-git workspaces (both `.jj/` and `.git/` at the same root) and plain Git checkouts behave exactly as before.

Fixes #1935
2026-06-05 14:39:39 +00:00
can1357 1cffb3473a fix: corrected worktree baseline capture with synthetic tree diff
- Added untracked worktree baseline capture via `untrackedPatch` and synthetic tree diffing.
- Added fallback-aware backend ordering by collecting host candidates and retrying alternates on unavailable PAL.
- Hardened overlay mount lifecycle by removing stale overlays and deleting upper/work dirs after unmount.
- Refined ZFS clone deletion to validate ownership and remove dataset+origin only when checks pass.
- Updated ProjFS integration to use extended-info callbacks and symlink metadata reads.
- Updated rcopy path handling to absolutize paths, and added `writeTree` plus combined shell timeout checks.
2026-05-12 14:36:31 +02:00
can1357 d37d48d11d feat(pi-iso): added unified pi-iso backend resolver with auto probe
- Added unified isolation primitives (BackendKind, ProbeResult, IsoError) and resolve fallback selection logic.
- Added Diff, FileChange, and ChangeKind with default_diff choosing git mode or filesystem walk based on repository state.
- Added APFS/btrfs/zfs/reflink/overlayfs/projfs/rcopy/block-clone backends with platform-aware start, stop, and probe.
- Mapped backend operations to canonicalized paths, recursive clone helpers, rollback cleanup, and unavailable error mapping.
- Added unified native exports isoBackend/isoProbe/isoResolve/isoStart/isoStop/isoDiff and removed projfsOverlay APIs.
- Replaced task isolation resolver flow with ensureIsolation/cleanupIsolation and migrated mode handling to auto plus legacy-mode aliases.
2026-05-12 13:39:45 +02:00
can1357 3d29a48e7a fix(coding-agent): fixed memory leak by cancelling idle compaction
- Fixed memory leak by cancelling idle compaction timer on event controller disposal.
- Fixed session resumption to preserve last non-empty session when starting fresh.
- Fixed stash detection to use git ref resolution instead of output parsing for reliability.
- Fixed secret obfuscation to deobfuscate restored session messages locally while keeping LLM messages obfuscated.
- Fixed stash pop operation to preserve staged changes with --index flag after task branch merges.
- Changed idle compaction settings from enum to numeric type for flexible configuration.
2026-04-05 03:50:52 +02:00
haram 801553b118 Implement fuse-projfs as an alternative to fuse-overlay for Windows. (#244)
* Implement fuse-projfs to use ProjFS on WIndows.

* fix(pi-natives): prevent ProjFS session key mismatch and start race

---------

Co-authored-by: can1357 <me@can.ac>
2026-03-03 04:12:30 +01:00