Attached Git worktrees
Experimental Unix-only local capture, exact native qualification and deliberate Git branch/PR publication. The existing clone is authoritative: attachment operations never write its index, working files or refs. Publication builds a fixed commit in a separately owned bare mirror and never moves the native target or the Git default branch.
Initialize a separate native data directory first. Attach the exact checkout root with an absolute Git executable (2.45 or newer), an actor, remote, remote-tracking target, output branch name and explicit source selection:
agentscm --data-dir /path/to/native attached-worktree attach \ --git /usr/bin/git --repository /path/to/clone --actor alice \ --remote origin --target main --output agents/alice \ --source worktree --path src --path tests --operation-id attach-alicecommitted requires --path . and reads the whole immutable HEAD commit as canonical Git bytes. worktree reads raw selected filesystem bytes, including selected untracked files; its serialized source kind is worktree_raw. It does not perform Git text, EOL, ident or encoding conversions. Consequently raw CRLF content can differ from canonical committed LF content. Configured clean/smudge filters are refused. index is explicitly unsupported; commit the intended index state or select raw worktree paths instead.
Worktree capture uses the shared workspace exclusion selector, including user Git excludes, local info/exclude, per-directory .gitignore, .agentscmignore and private workspace exclusions. HEAD/index inventory preserves tracked ignored files and staged additions. Git and workspace metadata are reserved. Unselected working paths are not read as source; the complete candidate still contains immutable committed branch content. Exclusions are not secret detection: deliberately selecting an untracked credential file can capture it. HTTPS userinfo, query strings and fragments are refused without echoing their contents. HTTPS identities must match the canonical explicit transport parser. Capture and refresh perform no network fetch or mutation. A missing remote-tracking ref never falls back to a local branch or tag.
SSH user@host:path and ssh://user@host/path identities, including common git@ username forms, remain unsupported by attachment identity. The transport must not confuse a username with authenticated ownership. This local interface does not authenticate the actor label or establish who owns a Change.
state --id ID reports identity and the first bounded mapping page. The library’s mappings(id, after_commit, limit) provides further pages (maximum 256). Each exact Git commit and tree maps to an immutable native revision; distinct commits may share a native revision. Worktree identity includes the per-worktree Git directory and index, not just the common Git directory.
capture and refresh require --expected-generation N and --operation-id ID. Retry the same caller inputs and operation ID after an uncertain response: the original complete outcome is returned even after later advancement. Changing inputs with an old operation ID is an error. A new operation must use the current generation; no-op completion uses the same transaction fence as advancing completion.
The recorded baseline is the imported Git target. The candidate separately retains committed branch-ahead work and selected edits. Refresh first incorporates local source changes using the previous observed local source as its verified base, then incorporates an advanced target using the recorded target baseline. If the exact target is already an ancestor of HEAD, refresh verifies that retained candidate-only content came from the prior recorded target and adopts Git’s resolved HEAD plus the selected raw overlay. This permits an ordinary clean merge or explicit conflict resolution without reapplying the target. Ambiguous native-only content still blocks. When the target is not in HEAD, disjoint path changes merge and divergent same-path edits block, including otherwise mergeable text. Refresh never applies the candidate to the checkout. Capture refuses a changed target until refresh imports it. HEAD and target rewrites block both operations. Absent ancestry objects and Git process failures (including timeouts) are reported as unavailable ancestry. A shallow boundary can hide parent relationships and appear as divergence even when the full history would prove ancestry. Restore genuinely rewritten ancestry to continue; supersede and rebase recovery remain unsupported. No merge session or native integration event is fabricated.
All captures and current comparisons are historical observations. Their fingerprints include selected bytes/kinds/absence, Git inventory and observed exclusion inputs. changed_paths is relative to the captured HEAD snapshot, not the preceding native candidate, and worktree source remains raw bytes. Completion revalidates those inputs and Git identity under the journal generation fence, but ordinary filesystem editors cannot be locked atomically by this protocol. validate_current(engine, id, generation) performs a fresh comparison and returns a subject fingerprint; it is not a freshness lease. The publisher repeats comparisons at new effect boundaries and after effects. A historical receipt never grants permission for another write.
bind_context(authority, operation, id, generation, change) explicitly binds an existing Authority Change and its exact candidate context from the same native data directory and repository identity. The context target must match the Git target name and have no native target/base: this is an unlanded check context, with the Git baseline retained separately in the attachment. Native landing is not required to run exact source-path checks. Binding is lifetime-stable for the attachment: a different Change cannot replace it, including after the Change closes or lands. The local actor is an unauthenticated label and is not asserted to own the Change. Every source, exclusion-input or target advance invalidates the attached context pointer. A consumer must recheck the actual Authority current pointer and policy before using evidence. A retained pointer alone does not establish current qualification or publication authority.
Qualify and publish a candidate
Use the existing native context and check commands with the captured candidate_revision. Supply a complete review context that names the actual Change, exact revision, target name, current policy epoch and intended evidence requirements; use base: null for this Git-authoritative profile. start context.json creates the Change; context EXPECTED_VERSION context.json advances its context after capture or refresh. check-source-paths request.json and check-run ATTEMPT GENERATION execute a configured source-path recipe. Command and workflow requirements use the existing workflow interface. Source-path checks establish byte expectations, not model review or execution success. No command in this lifecycle invents passing evidence or a native landing.
Bind that current context, execute its configured checks, and compare the exact subject:
agentscm --data-dir /path/to/native start context.jsonagentscm --data-dir /path/to/native attached-worktree bind-context --id ATTACHMENT --expected-generation 0 --change CHANGE --operation-id bind-changeagentscm --data-dir /path/to/native check-source-paths check-request.jsonagentscm --data-dir /path/to/native check-run ATTEMPT 0agentscm --data-dir /path/to/native attached-worktree compare --id ATTACHMENT --expected-generation 1agentscm --data-dir /path/to/native attached-worktree prepare --id ATTACHMENT --expected-generation 1 --operation-id prepare-1 --mirror /path/to/owned.git --name 'Local operator' --email operator@example.invalid --timestamp 1780000000 --message 'Prepare reviewed candidate'prepare retains a typed draft subject, fixed caller operation inputs and fixed commit metadata. Identity includes the attachment, actual Change/context/policy, native revision, Git source and baseline, selected-source comparison, credential-free endpoint and output branch. Identical preparation retains the commit; a historical operation returns its retained receipt even after later advancement. Changed inputs with the same operation ID fail. A mirror belongs to either the candidate stream or the integrated-event stream; use separate mirrors. Pending preparation reserves its lineage. Replay the same fixed inputs to resume it. If construction never began (no candidate record and the owned ref still equals the reserved parent), attached-worktree abandon-prepare --id ATTACHMENT --mirror /path/to/owned.git --operation-id PREPARE_ID explicitly journals abandonment without deleting the original request or freeing its ID. A refreshed subject can then use a new operation ID. Abandonment also prevents preparing the identical subject under another ID in that mirror; abandon only a subject you intend to replace. Unknown or effect-bearing operations cannot be abandoned based on a missing observation. A candidate left Verified after a failed guard must be explicitly resumed with the original prepare inputs and restored exact source/context; publication cannot complete that transition implicitly. Abandonment of a built candidate is unsupported.
Configured source checks, required review evidence and workflow gates must pass for the exact current subject. Publication does not require or invent a native human approval record unless the configured policy requests that evidence; native landing retains its separate approval requirement.
Output is built only in the owned mirror. A refreshed target is imported with the existing bounded object copier. Candidate commits retain the prior output as first parent and the target as another parent when distinct, preserving both output and target ancestry. This does not merge files again: attachment refresh must already have produced the qualified candidate tree. Regular, executable and symlink storage is preserved; a source kind supported by storage need not be supported by an execution adapter.
publish-branch uses the existing Mirror transport. Its endpoint and branch come from the attachment, not additional destination arguments. An absolute local bare remote and canonical HTTPS are supported; HTTPS requires an explicit mode-0600 username/password JSON file and strict system trust or an explicit CA PEM. Credential bytes are not journaled. No ambient Git push, credential discovery or candidate-source execution is used. Omit --expected-oid only when the output branch must be absent; an update requires the exact old commit and descendant ancestry. The output must differ from both the selected target and the actual remote default branch discovered through HEAD symref and the connected forge. HEAD, fully qualified ref aliases and an unknown default identity are refused. Both remote target and source qualification are compared again around dispatch. This is not an atomic lease over editors, Authority and a forge: after-effect changes produce stale/unknown status while retaining the receipt.
agentscm --data-dir /path/to/native attached-worktree publish-branch --id ATTACHMENT --expected-generation 1 --mirror /path/to/owned.git --candidate CANDIDATE --operation-id push-1 --credential-file /path/to/git-credential.jsonagentscm --data-dir /path/to/native attached-worktree recover-branch --id ATTACHMENT --mirror /path/to/owned.git --operation-id push-1 --credential-file /path/to/git-credential.jsonRecovery only observes. Pending/Unknown fences new operations (a rejected push, a conclusive remote refusal with the expected preimage still in place, does not; publish-branch reports ok:false and publish-pr refuses it) and never causes an authentication or push retry. Reusing an operation ID returns historical state without dispatch. Use preview --id ID --expected-generation N for bounded selection inspection without consuming operation IDs, and compare for current qualification. A successful historical receipt is not a current pass: CLI replay reports ok:false and qualification:not_checked, even when the retained effect succeeded.
publish-pr uses the existing Hub forge client (GitHub PR or GitLab MR, chosen by the connection) and persistent publication records. Configure the Hub URL and repository-scoped agent token using the existing Hub configuration or AGENTSCM_HUB_URL and AGENTSCM_HUB_TOKEN_FILE; the Hub owns the explicitly configured forge connection. The token repository and connected forge host/repository must match the attached HTTPS endpoint. Local bare publication cannot independently establish a forge repository identity. The operation ID below names the successful branch publication receipt:
agentscm --data-dir /path/to/native attached-worktree publish-pr --id ATTACHMENT --expected-generation 1 --mirror /path/to/owned.git --candidate CANDIDATE --operation-id push-1 --credential-file /path/to/git-credential.json --title 'Reviewed candidate'agentscm --data-dir /path/to/native attached-worktree recover-pr --id ATTACHMENT --publication PUBLICATION_IDThe Hub accepts a distinct candidate manifest with no integration event sequence. Legacy event-backed clients retain their manifest and publication identity. Candidate PR creation reserves an attempt before dispatch; an uncertain reply may be adopted from an exact observed head/base, or in explicit read-only recovery from the same branch/base PR carrying the stable publication/commit marker in an HTML comment in the editable PR body, and missing observations never authorize a blind second create. Read-only recover-pr retains uncertainty when it cannot find that association. Later publication uses a new candidate and branch operation with the old commit lease, then publish-pr --previous-publication PUBLICATION_ID to continue the same Change, branch and PR. The Hub reserves one candidate lineage per repository/branch. A successor may name an unattempted stale predecessor with the same owner, attachment, Change, endpoint, output and base; its retained chain carries the association to the last PR. Supersession and the old candidate’s attempt transition share a transaction fence. An uncertain attempted create blocks supersession until observation resolves it. Missing or eventually consistent PR lists never authorize another create. A complete non-rate-limited refusal (400/403/404/422 on GitLab; 403/404/422 on GitHub) is retained separately as a no-create result and permits a newly qualified successor; the same publication ID never retries a write. Transport failures, 5xx, unreadable success and failures to store the result retain uncertainty. After correcting credentials or a refusal, or when read-only recovery confirms no PR after an uncertain transport outcome, an operator may manually open the exact head/base PR and use recover-pr; target movement does not block that read-only historical association. If the head also moved, the original marker is required; removing that comment can prevent historical recovery. A closed or merged PR terminates this lineage; use a new attachment, Change and output branch for new work. An agent-task profile may opt in to a GitHub commit status that the Hub binds to the exact qualified candidate (see workflow status); it is experimental and fake-tested, and GitLab merge requests have the same opt-in status, fake-tested only; verification of provider protection and merge queues remain unimplemented.
GitLab parity: candidate publish-branch, publish-pr and recover-pr also work against a GitLab PAT connection, including nested groups. The branch push is unchanged provider-neutral transport. The Hub creates or adopts one MR in the same project; fork MRs are never adopted, and a truncated MR search or 429 blocks without creating. GitLab’s 409 “already exists” is uncertain, not a refusal, as are 5xx, transport failures and unreadable success; 400/403/404/422 is a no-create refusal. A merge without a merge commit (fast-forward or merged by push) is recorded as merged only when the MR head equals the published commit. Differences and limits: legacy event publication (hub publish --event) stays GitHub-only (409 unsupported_provider); an opt-in publish_forge_status also posts the status on a GitLab candidate MR (fake-tested only; see workflow status), forge_check reports provider gitlab with protection: "unverified", and that SHA status is not a verified MR merged-result pipeline, merge-train or “pipelines must succeed” result; candidate create returns 409 ambiguous_repository_name when the same repository name is connected on another configured forge host; a self-managed GitLab under a relative URL root is refused; a conclusive push refusal is recorded as rejected (the kind mapping is unverified against live GitLab, which may report only hook_declined); and after a source rewrite or target advance the Hub keeps the recorded MR number, creates nothing and does not treat the receipt as current qualification. AgentSCM never alters protections or merges. Qualification used an in-process fake GitLab, real local Git and loopback HTTPS; live GitLab.com and self-managed GitLab are untested. This is neither full-history migration nor native authority cutover.
Preparation inherits the exact seed limits: at most 100,000 reachable Git objects and 128 MiB of raw object bytes; shallow metadata and grafts are refused. Capture/refresh support for locally available shallow/partial objects does not imply prepare support. An already present, verified baseline closure is reused without copying every object again. Candidate metadata scans use bounded structural validation; the selected candidate receives full closure and commit-body verification. Candidate and prepare journals each retain at most 4,096 records, with 64 KiB per record and a 8 MiB cumulative scan budget. Remote operation journals retain at most 1,000 outcomes. These finite journals have no pruning or automatic migration. Hub lineage traversal is bounded to 4,096 predecessors and each token can retain at most 20 unfinished publications. Superseded and refused publications without a PR continue to consume this quota; once exhausted, use a new repository token. Migration 15 preserves integrated publications but is not downgrade-safe for older binaries reading candidate rows.
Qualification uses synthetic attached checkouts, real local Git and loopback authenticated HTTPS, and the existing fake Hub with fake GitHub or GitLab servers. The fake provider’s PR head observations are scripted from commits actually received by Git; this is not live GitHub or provider-backed model review. Live GitHub, real SSH daemon, two-machine behavior, source-index capture, rewritten/squashed ancestry recovery, native authority cutover, full-history migration and automatic merging remain unclaimed. SSH attachment identities are explicitly unsupported; an SSH username is not a password, and the separately qualified standalone Mirror SSH transport does not establish attached SSH lifecycle support.
The attachment journal is bounded and held under a lifetime local-recovery lease. It retains at most 256 attachments, 4,096 operation IDs and 4,096 Git mappings per attachment, 10,000 rows total and 256 MiB of retained state/outcome JSON. Each state or complete operation outcome has a 256 KiB JSON ceiling, including changed paths; exceeding it rolls back the operation without partial journal state. Successful no-ops consume operation rows because same-ID replay is durable; operation IDs are never silently pruned. Polling should use preview, state or compare and reuse an uncertain operation ID rather than issue capture/refresh no-ops as a polling loop. One attachment therefore cannot consume every per-attachment slot, while the separate total bounds still protect the data directory. Exhaustion fails closed. There is no automatic migration or supported recovery into a new journal; preserve the original data and checkout and escalate rather than deleting retained IDs. Unknown, malformed or incompletely initialized foreign journals fail closed, while a validated empty journal does not disable standard backup. Standard backup/restore explicitly refuses a nonempty attached store until attachment recovery has been qualified. Do not copy a live journal as an unmanaged backup. The executable-mode probe must be on the worktree’s filesystem; a separate native data mount is currently refused for raw worktree capture.
Detached HEADs, unmerged indexes, skip-worktree or assume-unchanged entries (and therefore sparse-checkout worktrees using those flags), submodules/gitlinks, configured content filters and index capture are unsupported. Shallow or partial clones work only when every required commit, tree and ancestry object is already local; lazy fetching is disabled. Non-UTF-8 selected relative paths remain unsupported, although Unix-native workspace roots and exclusion-file paths may contain raw non-UTF-8 bytes. A blocked same-path refresh remains attached and unchanged: resolve by merging the exact target in Git and committing the resolution, then retry refresh; if the target is not an ancestor of that HEAD, phase 1 has no supersede/abandon operation for the attachment ID.
Live GitLab qualification checklist (opt-in)
This checklist has not been run. It is for an operator with separate authority, using only the operator’s own GitLab account and token (no AgentSCM-supplied credentials), against a disposable project in a nested group (group/sub/project) created for the purpose. Never use a production project. Record results outside the repository.
- Create the disposable project with a protected default branch. Create a PAT with the
apiscope on a user with at least Developer role; configure it as the Hub GitLab token and confirm the connection and repository appear. - Confirm a direct push to the protected target is rejected, and that AgentSCM reports the push as
rejectedrather than Unknown. Record therejectionkind:protected_branchonly if GitLab’s reason says “protected branch”; a push rule or hook may yield onlyhook_declined. The kind mapping is unverified against live GitLab. publish-brancha candidate to a distinct, unprotected branch; confirm the leased push andrecover-branchagree with the remote ref.publish-pr; confirm exactly one MR in the same project, with the publication marker in its description.- Simulate a lost reply (drop the network after the create POST is sent); confirm the result is uncertain, no second POST occurs, and
recover-pradopts the existing MR with no duplicate. - Repeat create for the same head/base; record GitLab’s duplicate response (expected 409) and confirm it is treated as uncertain.
- Open an MR from a fork with the same branch name; confirm it is never adopted.
- After a push, measure how long the MR
shatakes to match the new head; confirm adoption does not claim a head that has not yet updated. - Merge the MR by fast-forward, by pushing the commits directly to the target, squash and merge commit in separate runs; record
merge_commit_sha,squash_commit_shaandsha, and confirm merged is recorded only when expected (a merge without a merge commit only when MR head equals the published commit). - On a self-managed instance under a relative URL root, confirm the candidate endpoint is refused.
- Exercise rate limiting (429); confirm the Hub blocks without creating.
- Status projection (not yet run). With an agent-task profile that sets
publish_forge_status: true, confirmagentscm/workflow/<target>appears on the exact MR head:pendingbefore qualification andsuccessonce the pinned store qualifies the candidate. Confirm it is posted on the source branch ref and thatforge_check.providerisgitlab. - Push, force-push and advance the target; confirm the old success is replaced by
failedand nosuccessremains on a non-head commit. Close the MR and confirm the old head is retired withfailed. Confirm an MR from a fork gets no status. - Drop the reply to a status POST; confirm the Hub reads the commit’s status list and does not post again. Record the response to a repeated
pendingpost (expected 400 “cannot transition”); a finished status (success or failed) is replaced by a new status, not refused. - Check the pipeline interaction: whether the external status joins or creates a pipeline for the source branch ref, and whether “pipelines must succeed” or required status checks treat it as a gate. Record that it is not a merged-result or merge-train result; AgentSCM does not post the status on those pipelines’ commits (not implemented, by decision, pending live verification).
- Confirm no merge, protection change or bypass was performed by AgentSCM, and that the same name on a second host yields 409
ambiguous_repository_name.