Skip to content

feat(workflow): add workspace branches - #1175

Merged
jeff-r2026 merged 25 commits into
Tencent:mainfrom
jeff-r2026:feat/1080-workflow-workspaces
Oct 21, 2026
Merged

jeff-r2026 merged 25 commits into
Tencent:mainfrom
jeff-r2026:feat/1080-workflow-workspaces

Conversation

@jeff-r2026

Copy link
Copy Markdown
Collaborator

Closes #1080

Summary

  • add read-only, new, and continue workspace contracts to workflow leases
  • run local change steps in isolated git worktrees with deterministic branches, dependency merges, conflict handling, commit ranges, pushes, and safe cleanup
  • add for_each branches/results and session continuation across steps and passes
  • pin agent configuration and hooks to the run base before each tool executes
  • document workspace behavior in both workflow guides and the workflow engine notes

Testing

  • npm run build
  • npx tsc --noEmit
  • npm run lint
  • npx vitest run src/__tests__/workflow-workspace.test.ts src/__tests__/workflow-local-driver.test.ts --reporter=dot (38 passed before final review fixes; focused workspace suite rerun below)
  • npx vitest run src/__tests__/workflow-workspace.test.ts --reporter=dot (9 passed)
  • npx vitest run src/__tests__/workflow-records.test.ts src/__tests__/workflow-docs-examples.test.ts --reporter=dot (48 passed)
  • npm run test:e2e -- src/__tests__/e2e/workflow-workspaces.test.ts (real CLI; 1 passed)
    • created and merged independent workspace branches
    • pushed all change branches to a local bare origin
    • verified the active checkout and HEAD stayed untouched
    • verified read-only writes fail with read_only_dirty
    • verified conflicting dependency branches fail with merge_conflict
  • npm run test:e2e -- src/__tests__/e2e/workflow-command.test.ts src/__tests__/e2e/workflow-inputs.test.ts src/__tests__/e2e/workflow-inline.test.ts src/__tests__/e2e/workflow-subworkflow.test.ts src/__tests__/e2e/workflow-loop-fan-out.test.ts src/__tests__/e2e/workflow-signals.test.ts (10 passed)

Co-authored-by: Cursor <cursoragent@cursor.com>
@SaulMoro

Copy link
Copy Markdown
Collaborator

This PR's workspace worktrees start without the team's assets. The details are in my comment on #1080: #1080 (comment)

In short:

  • ensureWorktree creates each worktree under .teamai/workflow-worktrees/, which the post-checkout hook skips, and points core.hooksPath at an empty directory. Nothing delivers the team's skills, rules, agents and MCP when the worktree is created.
  • applyBaseConfig then restores the agent paths from the run base commit. Those assets are git-excluded, so the base commit does not have them.
  • The step runs without them, unless the tool's own session-start pull delivers them in the background. The worktree also has no pull record, so [feat] Record the agent each workflow step ran as #1176 cannot record which team revision the step ran with.

I'd suggest giving the worktree the same delivery the new-worktree hook gives any other new worktree of the workspace (pull with inline and prepareCheckout). Run it after applyBaseConfig restores the base paths and before it records the overlay with fileManifest, so the delivered files do not count as the step's edits. A test would check that a prompt step in a new workspace sees a team skill the base commit lacks, and that the step's branch does not commit the delivered files.

Should this go in this PR or in a follow-up?

A workflow worktree is created where the post-checkout hook does not run, so the step never received the team's skills, rules, agents, or MCP. Deliver them the same way a new worktree does, after the base agent files are restored and before the overlay is recorded, so they are not part of the step's commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Scope limitation: shallow history prevents resolving the requested three-dot diff. I reviewed the endpoint diff read-only; exact merge-base scope remains unverified.

Findings

  • [P1 blocking] Keep hook configuration worktree-local — src/workflow/worktree.ts:77: git config core.hooksPath writes the shared repository configuration. Running any workspace step redirects the active checkout’s hooks to an empty directory, and cleanup never restores them. Subsequent ordinary commits silently bypass existing hooks.
  • [P1 blocking] Resolve the Git directory instead of assuming .git is a directory — src/workflow/worktree.ts:38: In a linked worktree, .git is a file. Starting a workspace: new step there reaches mkdir(<checkout>/.git/info) and throws ENOTDIR, aborting the driver before execution.
  • [P1 blocking] Include fan-out changes when merging dependencies — src/workflow/workspace.ts:105: Dependency collection ignores states without direct changes. A completed for_each stores its branches in item results, so a new workspace with needs: [fan] starts from the run base without merging any item changes. Reading a file produced by those items consequently fails.
  • [P1 blocking] Capture the commit range before execution — src/workflow/worktree.ts:179: from is captured after the command or agent has finished. A step that commits its own changes reports commits: [], so pushIfOrigin skips publishing its branch despite successful execution. Merge-only steps similarly omit the merge commits created during staging.
  • [P1 blocking] Restore the index alongside configuration files — src/workflow/worktree.ts:116: Checking out base configuration changes both files and the index, but restoration only restores files. If one continuation edits an existing AGENTS.md and the next makes no changes, status is dirty until git add -A restores the index; the ensuing empty commit fails the otherwise successful step with push_failed.
  • [P1 blocking] Do not traverse directory symlinks during restoration — src/workflow/worktree.ts:159: With a tracked .claude/skills/shared symlink to an external directory, fileManifest enumerates its target files. Restoration deletes those files through the symlink; the saved symlink points to the same now-deleted source, so copying back fails and is swallowed. A no-op step can therefore delete shared skill data.
  • [P1 blocking] Merge the pinned dependency commit, not its mutable branch — src/workflow/local-driver.ts:292: The lease supplies merge.commit, but staging merges merge.branch. If another continuation has advanced that branch, a join depending only on the earlier step also receives the later step’s changes, potentially introducing an undeclared conflict or changing its result.

Testing
The description supplies sufficient testing evidence: build, type-check, lint, focused tests, and representative real-CLI E2E records. No missing-testing finding. I did not run, build, or install PR code.

jeff-r2026 and others added 4 commits October 19, 2026 11:06
Bring in model selection and agent records, and fix the review: keep
hooks on the worktree, merge a dependency's recorded commit, include
the step's own commits, and restore agent files without a dirty index
or following a directory symlink.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep workspace branches working beside human-in-the-loop steps and the
latest main: a step still merges the commit a dependency reported, and
commands that share a checkout stay read-only.

Co-authored-by: Cursor <cursoragent@cursor.com>
Commands with no workspace fail when they change the repository. These
tests write those files only to show which checkout ran, so git ignores them.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Include workflow children’s changes in dependency merges — src/workflow/workspace.ts:107: A completed workflow runner has no direct changes, and this fallback only collects for_each items. If work opens a workspace and its child writes output.txt, a later workspace: new step with needs: [work] starts without that file. Collect the workflow’s completed change branches too.
  • [P1 blocking] Preserve relative symlink targets when saving configuration — src/workflow/worktree.ts:120: Without verbatimSymlinks: true, fs.cp converts relative symlink targets to absolute paths. With tracked AGENTS.md -> docs/agent-rules.md, even a no-op step restores and commits a link pointing into its temporary worktree. Cleanup then removes that target, leaving the published link broken.
  • [P1 blocking] Retry unpublished commits after a failed continuation push — src/workflow/worktree.ts:220: A continuation’s failed push leaves its commit on the retained enclosing branch. Retrying an idempotent command on that branch produces no additional commits, so this early return skips pushing and the retry succeeds despite the original changes remaining unpublished. Track pending publication or check the remote branch rather than only this attempt’s commit count.
  • [P2 non-blocking] Apply session continuation to interactive prompts — src/workflow/local-tool-adapter.ts:97: promptReport supplies the resolved resume ID for interactive steps, but the interactive invocation paths ignore it. An interactive: true prompt using session.from or session: continue therefore opens a fresh session instead of resuming the selected one.

Earlier Findings
All seven earlier findings are resolved in the current diff: hook scope, linked-checkout Git paths, direct fan-out dependency collection, commit-range capture, index restoration, symlink-target deletion, and pinned dependency merges. The findings above describe distinct remaining issues.

Testing
The description documents sufficient testing, including build and representative real-CLI E2E records. No missing-testing finding. I reviewed the requested three-dot diff read-only; I did not run, build, or install PR code.

jeff-r2026 and others added 2 commits October 20, 2026 16:55
A step that needs a workflow now merges that workflow's child branches.
Relative agent symlinks stay relative, a retry pushes commits an earlier
attempt left unpublished, and an interactive Claude or Codex session resumes.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Retry the first failed push of a continuation branch — src/workflow/worktree.ts:235: When a continuation’s first push fails, no origin/<branch> ref exists. Retrying an idempotent command produces no new commits, so branchNeedsPush returns false and the retry succeeds without publishing the retained commits. The earlier publication finding is only partially resolved.
  • [P1 blocking] Preserve executable permissions when restoring configuration — src/workflow/worktree.ts:159: Restoration deletes each unchanged file and recreates it with copyFile, without restoring its mode. A no-op workspace step therefore changes a tracked executable .githooks/pre-commit from executable to non-executable and commits that change; checkouts using the published hook then silently stop running it.
  • [P1 blocking] Handle directory-to-symlink restoration — src/workflow/worktree.ts:152: If the base contains .claude/skills/shared/ and an earlier continuation replaces it with a symlink, the next no-op continuation restores the base directory temporarily. Restoration removes its files but leaves the directory, then calls non-recursive fs.rm when restoring the saved symlink. This throws EISDIR, aborting the driver instead of completing the step.

Earlier Findings
The other ten earlier findings are resolved, including workflow-child dependency merges, relative symlink preservation, and interactive session continuation. The unpublished-commit retry issue remains in the first-push case above.

Testing
The description documents sufficient testing, including build and representative real-CLI E2E records. No missing-testing finding. I reviewed the requested three-dot diff read-only and did not run, build, or install PR code.

A retry with no new commit still pushes when origin has other refs but not this branch's tip. Restoring agent files keeps the executable bit, and a symlink can replace the directory the base checkout put back.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Retry publication when origin has no tracking refs — src/workflow/worktree.ts:247: If a continuation’s first push to a newly added or empty origin fails, no refs/remotes/origin/* exists. An idempotent retry adds no commits, so this return skips publication and reports success while the retained changes remain unpublished. The earlier retry finding is only partially resolved.

  • [P1 blocking] Preserve edits beneath a saved directory symlink — src/workflow/worktree.ts:155: If an earlier continuation replaced .claude/skills/shared/ with a symlink, the next step temporarily sees the base directory. When that step writes shared/new.md, restoration still treats the parent as unchanged—directories are absent from both manifests—and recursively deletes it to restore the saved symlink. The step succeeds but loses its new file. Preserve edited descendants before replacing their parent.

  • [P1 blocking] Recognize permission-only configuration edits — src/workflow/worktree.ts:189: The manifest records file contents but not executable permissions. A step that runs chmod +x .githooks/pre-commit without changing its contents is therefore treated as leaving the file unchanged; restoration overwrites it with the saved non-executable mode. The step reports success without committing the requested hook activation. Include the executable bit when detecting edits.

Earlier Findings
The other earlier findings are resolved, including preservation of unchanged executable permissions and directory-to-symlink restoration. The two new restoration findings concern step-authored edits, not those resolved no-op cases.

Testing
The description supplies sufficient testing evidence, including build and representative real-CLI E2E records. No missing-testing finding. I reviewed the requested three-dot diff read-only; I did not run, build, or install PR code.

…dits

A retry still pushes when origin has no remote-tracking refs and the commits live only on this branch. Restoring agent files keeps a chmod and a file written under a directory the branch had turned into a symlink.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Preserve configuration conflicts during base overlay — src/workflow/worktree.ts:129: If two dependencies conflict in AGENTS.md, the join prompt’s base checkout clears the unmerged index entries and hides the conflict. When the prompt leaves that pinned file untouched, restoration copies back the saved conflict markers and commits them as resolved. Preserve the conflict separately so unresolved configuration is not silently published.

  • [P1 blocking] Inherit dependencies through nested workflow runners — src/workflow/workspace.ts:142: Dependency inheritance stops at the immediate parent. If work needs a branch producing prepared.txt, but its first executable child is work.inner.check inside another workflow, both inner definitions can omit needs and the file is never merged. The check starts from the run base and fails to read it.

  • [P1 blocking] Include enclosing fan-out items’ after merges — src/workflow/workspace.ts:130: For a for_each running a workflow, the leased child does not collect its enclosing item’s after dependencies. If item 1 waits for item 0 and its continuation reads a file item 0 produced, scheduling waits correctly but the workspace lacks that file. Collect the enclosing item’s branches before executing its children.

  • [P1 blocking] Report branches for workflow-backed fan-out items — src/workflow/fan-out.ts:112: A workflow item stores changes on its children, not its own pass, so this condition omits its branch even when its children use workspace: continue. Consequently, steps.fan.branches reads as null; a downstream for_each: steps.fan.branches fails instead of iterating the completed workspace branches.

Earlier Findings
All previously reported scenarios are resolved in the current diff; none is raised again.

Testing
The description documents sufficient testing, including build and representative real-CLI E2E records. No missing-testing finding. I reviewed only the requested three-dot diff and did not run, build, or install PR code.

…ranches

A prompt that leaves a conflicted agent file untouched no longer publishes the conflict markers. Nested workflows inherit needs, a continued fan child merges the item it waits for, and a workflow fan item reports its branch.

Co-authored-by: Cursor <cursoragent@cursor.com>
Collect each item's child branch in one pass, so settling a skipped fan does not scan every step once per item.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Restore configuration after prompt preflight failures — src/workflow/local-driver.ts:695: Staging overlays base configuration before checking the tool, agent, or model. Those checks can return without restoring the overlay. If an earlier continuation edited AGENTS.md, retrying after an agent_unavailable failure saves the base overlay as the branch configuration and subsequently commits it, erasing the earlier edit. Restore configuration on every exit path.
  • [P1 blocking] Preserve unresolved merges when a prompt fails — src/workflow/local-driver.ts:387: Conflict checking only runs when push is true. A prompt that exits nonzero with unresolved conflicts still reaches commitWork, whose git add -A commits the conflict markers as a resolution. An otherwise successful no-op retry can then publish them. Do not finalize an unresolved merge on the failure path.
  • [P1 blocking] Commit resolutions that leave the tree unchanged — src/workflow/worktree.ts:260: If a prompt resolves every conflict by choosing the current branch’s contents, there is no staged diff, so the merge commit is skipped despite MERGE_HEAD existing. The step succeeds and cleanup discards the pending merge; a later join with the other dependency conflicts again. Check pending merges independently of file differences.
  • [P1 blocking] Do not select fan-out result branches by name length — src/workflow/fan-out.ts:138: For a workflow item with sequential children a and b, both using workspace: new and b needing a, equally long branch names select a. Thus steps.fan.branches advertises a branch missing b’s completed changes, and downstream processing of that branch misses its output. Select the completed result branch rather than the shortest name.

Earlier Findings
The previously reported concrete scenarios are resolved. The fan-out finding here concerns separate child branches, not children continuing one item branch.

Testing
The description documents sufficient testing, including representative real-CLI E2E records. I reviewed the requested three-dot diff read-only; I did not run, build, or install PR code.

A prompt that exits before it runs puts the branch agent files back. An unresolved conflict is not committed, a resolution that matches the current tree still records the merge, and a fan item reports the child that finished last.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Preserve step-created parent symlinks during restoration — src/workflow/worktree.ts:192: Suppose an earlier continuation added .claude/extra.md, which is absent from the run base, and the next step replaces .claude with a symlink to an external directory. extra.md is absent from both current and overlay manifests, so restoration removes it through the new symlink, deleting the external file. removeBlockingSymlink then deletes the step-created link. Check ancestor edits before restoring descendants.

  • [P1 blocking] Do not automatically resolve modify/delete conflicts — src/workflow/local-driver.ts:389: When one dependency modifies note.txt and another deletes it, Git leaves the surviving file unmerged without conflict markers. A no-op prompt therefore reaches this loop, which stages the file merely because it lacks <<<<<<<. The driver commits and publishes the unresolved merge as successful. Require an actual resolution rather than treating marker absence as sufficient.

  • [P1 blocking] Return an empty branch list for an empty fan-out — src/workflow/expression.ts:52: With an empty input list, a workspace for_each succeeds with results: [], but branchNames returns undefined. A downstream for_each: { items: steps.fan.branches } consequently receives null and fails instead of completing with zero items. Preserve [] for a settled empty fan-out.

Earlier Findings
The previously reported concrete scenarios are resolved in the current diff; none is raised again.

Testing
The description documents sufficient testing, including build and representative real-CLI E2E records. No missing-testing finding. I reviewed the requested three-dot diff read-only; I did not run, build, or install PR code.

…anch lists

Restoring agent files does not follow a symlink the step just created. A modify/delete conflict stays unresolved until the step changes the file. An empty fan-out reports an empty branch list.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Check conflict markers before staging configuration — src/workflow/worktree.ts:205: If a join prompt edits a conflicted AGENTS.md but leaves conflict markers, this unconditional git add -A clears its unmerged entries. Because its contents changed, the saved conflict stages are not restored; unresolvedConflicts then sees no conflict, and the driver commits and publishes the markers as successful. Validate edited conflict files before staging them.
  • [P1 blocking] Preserve step-authored directory deletions — src/workflow/worktree.ts:187: If an earlier continuation added .claude/extra.md absent from the run base, and the next step runs rm -rf .claude, restoration recreates extra.md. That file is absent from both current and overlay manifests, and directory deletion is invisible to ancestorEdited. The step succeeds while undoing its requested deletion. Track deleted parent directories before restoring branch-only descendants.

Earlier Findings
The previously reported concrete scenarios are resolved in the current diff; none is raised again.

Testing
The description documents sufficient testing, including build and representative real-CLI E2E records. No missing-testing finding. I reviewed the requested three-dot diff read-only; I did not run, build, or install PR code.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Preserve unmerged configuration stages before execution — src/workflow/worktree.ts:192: With an AGENTS.md modify/delete dependency conflict and a no-op join prompt, the base checkout has already cleared the unmerged index entries. This query therefore treats the untouched surviving file as resolved; restoration stages it, and the driver commits and publishes the merge successfully. Restore the conflict stages before execution so only an actual prompt resolution clears them.

  • [P1 blocking] Retain conflicted paths across continuation retries — src/workflow/local-driver.ts:415: A continuation prompt that leaves markers in note.txt and runs git add -A correctly fails, but leaves that staged file in the retained worktree. On an idempotent retry, conflictContents finds no unmerged paths, so this marker check examines nothing. The driver then commits and publishes the unresolved markers as successful. Persist the original conflict paths until resolution is validated.

Earlier Findings
The latest pass’s first-attempt staged-marker check and explicitly staged configuration resolution are fixed. The findings here concern distinct remaining cases: untouched configuration conflicts and retries of staged conflicts. The other previously reported concrete scenarios are resolved.

Testing
The description documents sufficient testing, including build and representative real-CLI E2E records. No missing-testing finding. I reviewed the requested three-dot diff read-only; I did not run, build, or install PR code.

…them

Restore unmerged agent stages before the prompt runs, and remember conflicted paths so a later attempt cannot publish markers that were already staged.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Retain conflict tracking until validation succeeds — src/workflow/local-driver.ts:397: A continuation prompt can leave conflict markers in note.txt, then run git add -A && git commit. The first attempt correctly fails validation, but the commit clears MERGE_HEAD. On an idempotent retry, this check deletes the remembered conflict paths; dependency merges are already contained, so validation examines nothing and pushes the unresolved markers as successful. Retain those paths until resolution is validated, regardless of MERGE_HEAD.

Earlier Findings
All previously reported concrete scenarios are resolved. This finding concerns a prompt committing unresolved markers, rather than merely staging them.

Testing
The description documents sufficient testing, including build and representative real-CLI E2E records. I reviewed the requested three-dot diff read-only; I did not run, build, or install PR code.

A prompt commit clears MERGE_HEAD before validation. Keep the conflicted paths until the resolution is accepted, so a later attempt cannot publish the markers.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Detect commits made by read-only commands — src/workflow/local-driver.ts:480: In a clean checkout, a command without workspace that edits tracked.txt and commits it leaves both snapshots empty. This check reports success although the command changed the active checkout and advanced HEAD. Compare HEAD as well as working-tree changes to enforce the new read-only contract.

Earlier Findings
The previously reported concrete scenarios are resolved, including retaining conflict tracking after a prompt commits unresolved markers. None is raised again.

Testing
The description documents sufficient testing, including representative real-CLI E2E records. No missing-testing finding. I reviewed only the requested three-dot diff; I did not run, build, or install PR code.

A command with no workspace can edit a file and commit it, leaving the worktree clean. Compare HEAD as well, and leave that commit in place.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Restore configuration when cancelling commands — src/workflow/local-driver.ts:838: Cancelled commands leave dying without restoring their configuration overlay. If an earlier continuation committed an AGENTS.md edit and the next command is cancelled and retried, staging the retry saves the base overlay as the branch configuration. A successful no-op retry then commits away the earlier edit. Restore the saved configuration after the cancelled process stops.
  • [P1 blocking] Detect index changes by read-only commands — src/workflow/local-driver.ts:268: With an existing unstaged edit to tracked.txt, a command without workspace that runs git add tracked.txt leaves the content hashes and HEAD unchanged. This check reports success despite modifying the active checkout’s index. Include index state in the read-only comparison.

Earlier Findings
All previously reported concrete scenarios are resolved in the current diff, including detection of commits made by read-only commands. None is raised again.

Testing
The description documents sufficient testing, including representative real-CLI E2E records. I reviewed the requested three-dot diff read-only; I did not run, build, or install PR code.

A cancelled workspace command left the base agent files in place, so the next attempt committed them over an earlier edit. A read-only command that only stages an existing edit now fails too.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Compare actual index contents for read-only steps — src/workflow/local-driver.ts:258: The snapshot records working-file hashes and porcelain status, not staged contents. With both staged and unstaged hunks in tracked.txt, a command can stage another hunk while leaving one unstaged; status remains MM, working contents and HEAD stay unchanged, and the step incorrectly succeeds. Include staged blob IDs and modes in the comparison.
  • [P1 blocking] Enforce read-only workspaces for interactive prompts — src/workflow/local-driver.ts:556: Setting before to undefined for every interactive prompt disables validation even with explicit workspace: read-only. An interactive prompt that edits or commits tracked.txt therefore succeeds while changing the active checkout. Preserve the snapshot and validate interactive read-only steps too.

Earlier Findings
The previously reported concrete scenarios are resolved, including cancelled-command overlay restoration and detection of staging an otherwise unstaged edit. The index finding above concerns changes that leave porcelain status unchanged.

Testing
The description documents sufficient testing, including representative real-CLI E2E records. No missing-testing finding. I reviewed the requested three-dot diff read-only; I did not run, build, or install PR code.

…rompt

A read-only command can change the index blob while porcelain status stays mixed. An interactive prompt that sets workspace: read-only is checked the same way; other interactive prompts still keep the person's edits.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Detect permission changes on already-dirty files — src/workflow/local-driver.ts:269: With an existing unstaged content edit to a non-executable tracked.txt, a read-only command running chmod +x tracked.txt leaves porcelain status ( M), staged entries, file contents, and HEAD unchanged. Both snapshots therefore match, and the step succeeds despite changing the checkout’s executable bit. Include working-file modes in the snapshot.

Earlier Findings
The previously reported concrete scenarios are resolved, including staged-hunk detection and validation of explicitly read-only interactive prompts. This finding concerns working-tree permissions, not staged modes.

Testing
The description provides sufficient testing evidence, including representative real-CLI E2E records. No missing-testing finding. I reviewed the requested three-dot diff read-only; I did not run, build, or install PR code.

A chmod on a file that is already dirty leaves porcelain status, staged blobs, contents, and HEAD unchanged. Compare the working-tree mode as well.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Decode quoted Git paths before snapshotting — src/workflow/local-driver.ts:275: Git normally quotes non-ASCII filenames using octal escapes, but porcelainPath only strips quotes. With an existing unstaged edit to résumé.txt, a read-only command that overwrites it leaves status unchanged; both snapshots inspect the nonexistent escaped path and match. The step incorrectly succeeds. Decode Git’s quoted paths or use NUL-delimited output.
  • [P1 blocking] Snapshot symlink targets rather than their contents — src/workflow/local-driver.ts:279: For an already-modified tracked symlink pointing to target.txt, a read-only command replacing its target with ./target.txt leaves status, mode, staged entries, HEAD, and dereferenced contents unchanged. The snapshots match despite changing the tracked link. Use readlink when hashing symlinks.

Earlier Findings
All previously reported concrete scenarios are resolved in the current diff; none is raised again.

Testing
The description documents sufficient testing, including build and representative real-CLI E2E records. No missing-testing finding. I reviewed the requested three-dot diff read-only; I did not run, build, or install PR code.

Git quotes non-ASCII porcelain paths, and a symlink can change target without changing the file it names. Read status as raw bytes and hash the link target.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Snapshot the destination of staged renames — src/workflow/local-driver.ts:244: In porcelain -z output, the first path is the destination; the second is the source. With an already-dirty staged rename (RM old.txt → new.txt), a read-only command can overwrite new.txt without changing status or HEAD. Both snapshots inspect the missing old.txt, so the command incorrectly succeeds.
  • [P1 blocking] Decode conflicted paths before validating resolutions — src/workflow/local-driver.ts:451: Git quotes non-ASCII paths in this non-NUL output. If dependencies conflict in résumé.txt and the prompt stages it while leaving conflict markers, conflict tracking reads a nonexistent escaped pathname. The unmerged list is then empty, marker validation checks the wrong path, and the driver commits and publishes the unresolved markers as successful.

Earlier Findings
The previously reported concrete scenarios are resolved. These findings concern rename destinations and conflict-path decoding, not the fixed snapshot quoting and symlink-target cases.

Testing
The description documents sufficient testing, including representative real-CLI E2E records. I reviewed the requested three-dot diff read-only; I did not run, build, or install PR code.

Porcelain -z lists a rename's new path first. Conflict checks use NUL-delimited names so a quoted non-ASCII path is still rejected when markers remain.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] Decode configuration conflict paths before restoring them — src/workflow/worktree.ts:137: If dependencies conflict in .claude/résumé.md, ls-files -u quotes the pathname, but conflictPaths treats it literally. The overlay consequently leaves the base file in place instead of restoring the conflicted contents. A no-op join prompt then has that base file staged during restoration, and validation accepts and publishes it as a resolution. Use NUL-delimited paths throughout configuration conflict preservation.
  • [P1 blocking] Snapshot changes inside dirty submodules — src/workflow/local-driver.ts:254: With an already-dirty submodule, a read-only command can overwrite a tracked file inside it without changing the parent’s porcelain status, gitlink, or HEAD. hashPath attempts to read the submodule directory and returns missing in both snapshots, so the step incorrectly succeeds. Include submodule working-tree state in the comparison.

Earlier Findings
The previously reported concrete scenarios are resolved, including staged-rename destinations and non-configuration conflict-path decoding. Neither is raised again.

Testing
The description supplies sufficient testing evidence, including build and representative real-CLI E2E records. I reviewed the requested three-dot diff read-only; I did not run, build, or install PR code.

Agent conflict paths from ls-files are NUL-delimited, so a non-ASCII name is restored instead of published as the base file. A read-only command that edits a file inside an already dirty submodule fails too.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Findings
No new findings I can confidently substantiate in the current diff.

Earlier Findings
The previously reported scenarios are resolved, including NUL-delimited configuration conflict paths and dirty-submodule snapshots. None is raised again.

Testing
The description documents sufficient testing, including build and representative real-CLI E2E records. No missing-testing finding.

I reviewed the requested three-dot diff read-only against the base version’s review rules. I did not run, build, or install PR code.

@jeff-r2026
jeff-r2026 merged commit 28a1b2e into Tencent:main Oct 21, 2026
14 checks passed
@jeff-r2026
jeff-r2026 deleted the feat/1080-workflow-workspaces branch October 21, 2026 02:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] Add workflow workspaces, branches, and session continuation

3 participants