## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Execution workspaces hold the files that agents work in, and a
person can close a workspace when the work ends
> - Before close, Paperclip checks "close readiness" so that it does not
destroy unsaved git work
> - The check assumed that every existing workspace path is a git
repository
> - A shared or `local_fs` workspace can be a plain directory. For that
directory, `git rev-parse` fails, and the check reported "Paperclip
could not verify the workspace git status" as a blocking reason
> - So a person could never close these workspaces
> - This pull request treats a path with no `.git` marker as a workspace
with no git state
> - The benefit is that plain-directory workspaces can close, and real
git errors still block close
## Linked Issues or Issue Description
**What happened?**
`GET /api/execution-workspaces/:id/close-readiness` returned `state:
"blocked"` for a `shared_workspace` with provider `local_fs`. The
workspace directory existed but was not a git repository. The only
blocking reason was "Paperclip could not verify the workspace git
status. Retry before destructive cleanup."
**Steps to reproduce**
1. Create a project workspace and an execution workspace
(`shared_workspace`, `local_fs`) whose `cwd` is a plain directory that
exists.
2. Request close readiness for the execution workspace.
3. See `state: "blocked"` and `isDestructiveCloseAllowed: false`.
**Expected behavior**
The workspace is ready to close. It has no git state to lose.
## What Changed
- `inspectGitCloseReadiness` in
`server/src/services/execution-workspaces.ts`: when `git rev-parse
--show-toplevel` fails, the provider is not `git_worktree`, and no
`.git` entry exists on the path or on an ancestor, return `git: null`
with `statusInspectionSucceeded: true`.
- Added `hasGitMarkerInPathOrAncestors`. The check uses the filesystem,
not git error text, so it does not depend on the git locale. It uses
`lstat`, so a dangling `.git` symlink still counts as a marker. Any
error other than `ENOENT` counts as a possible marker, so the check
fails closed.
- For a plain directory, the result is a git readiness object with
`repoRoot: null`, the same shape as the missing-path case.
`createdByRuntime` stays on it, so close readiness still plans
`remove_local_directory` for a runtime-created directory.
- Added two regression tests in
`server/src/__tests__/execution-workspaces-service.test.ts`:
- A plain directory is `ready_with_warnings` with `git: null` and no
blocking reasons. This test failed before the fix (`state: "blocked"`).
- A directory with a broken `.git` pointer is still `blocked` with the
git-status reason.
- A directory with a dangling `.git` symlink is still `blocked`. This
test failed before the second commit.
- A runtime-created plain directory plans `remove_local_directory`. This
test failed before the second commit.
- A plain directory nested under an ancestor with an unreadable `.git`
marker is still `blocked`. This test covers the ancestor walk.
## Verification
- Red/green: the new non-git test failed on `master` (`state:
"blocked"`) and passes with the fix. The broken-git test passes on both.
- `cd server && npx vitest run
src/__tests__/execution-workspaces-service.test.ts
src/__tests__/execution-workspaces-routes.test.ts`: 2 files, 101 tests
pass. The five close-readiness tests also pass after the third commit.
- `pnpm verify` selected 2,223 of 2,991 test files because this service
has many importers. I did not run that near-full suite locally. Required
CI supplies that coverage.
- `tsc -p server/tsconfig.json` shows no errors in the changed files.
## Risks
- Low risk. The change only affects paths where git inspection already
failed.
- With `git: null` removed, the result uses the same `repoRoot: null`
shape as a missing path. Existing code already handles that shape.
- `git_worktree` workspaces still fail closed. A real repository that
git cannot read (corrupt, missing git dir, ownership refusal) still has
a `.git` marker, so it still blocks.
- The terminal-workspace sweep also uses this inspection. With no repo
root there is no HEAD SHA, so delivery cannot be "merged", and the sweep
still skips these workspaces. Only an explicit close by a person
proceeds.
- A `.git` on an ancestor across a filesystem boundary makes git report
"not a repository" while the marker check finds it. That case stays
blocked, which is the safe result.
## Model Used
- Anthropic Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with
tool use (shell, file edit, test runs).
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>