Problem Statement
Problem Statement
When a task modifies files inside a git submodule (e.g., libs/my_lib/), the agent commits changes on the worktree's local clone of that submodule. These commits exist only in the worktree's isolated checkout and are never pushed to the submodule's upstream remote.
During merge, the orchestrator brings the lane branch (which references the local-only commit) into the orch branch and ultimately into main. The main repo's submodule pointer now references a commit that does not exist on origin.
Consequences
- Cloning fails — anyone who clones the main repo gets
fatal: remote error: upload-pack: not our ref <sha>
- Commits are lost — when the worktree is cleaned up, the submodule commits become unreachable and are garbage collected
- False "succeeded" — the batch reports success and the merge completes, but the actual code changes are missing from main. Only the top-level config changes (YAML, Python scripts, generated board files) survive.
Example
# Agent commits on worktree's clone
$ cd .worktrees/XXXX/lane-1/libs/my_lib
$ git commit -m "Task: Update submodule"
→ creates commit 009dd1e (local only, never pushed to origin)
# Orchestrator merges lane into orch branch
$ git merge -m "wave 1 lane 1 — Task"
→ main now references 009dd1e in libs/my_lib
# Submodule update fails
$ git submodule update
→ fatal: remote error: upload-pack: not our ref 009dd1e
Root Cause
The orchestrator has no awareness that files inside a submodule path point to a separate git repository. It treats the submodule as regular files and lets the agent commit freely. When the lane branch is merged, the submodule pointer update is valid git plumbing but references an unreachable commit.
The orchestrator does not:
- Detect that changes are inside a submodule
- Push submodule commits to origin before merging
- Stage submodule changes on the main repo's submodule working tree
- Validate that merged submodule commits exist on origin
Proposed Solution
Proposed Fix
Add a post-execution submodule sync step in execution.ts:
/**
* After agent execution, sync any submodule changes to the main repo.
*
* When the agent modifies files in a submodule, those changes live in
* the worktree's clone of the submodule. Commits there are local-only.
* This function detects submodule changes and applies them on the main
* repo's submodule working tree instead, ensuring the resulting commit
* exists on origin (or at least doesn't point to unreachable refs).
*/
function syncSubmoduleChanges(
worktreePath: string,
repoRoot: string,
): void {
const submodulesResult = runGit(["submodule", "status"], repoRoot);
if (!submodulesResult.ok || !submodulesResult.stdout.trim()) return;
const submodulePaths = submodulesResult.stdout
.split('\n')
.map(line => line.trim().split(' ')[1])
.filter(Boolean);
for (const subPath of submodulePaths) {
const subWorktreePath = join(worktreePath, subPath);
if (!existsSync(subWorktreePath)) continue;
// Check for uncommitted changes
const status = runGit(['status', '--porcelain'], subWorktreePath);
if (!status.ok || !status.stdout.trim()) continue;
// Stage and commit on the submodule (detached HEAD state)
runGit(['add', '.'], subWorktreePath);
runGit(['commit', '-m', `taskplane: stage workspace changes`], subWorktreePath);
// Get the new commit SHA
const head = runGit(['rev-parse', 'HEAD'], subWorktreePath);
if (!head.ok) continue;
// Update the submodule pointer in the main repo's index
runGit(
['update-index', '--cacheinfo',
`160000,${head.stdout.trim()},${subPath}`
],
repoRoot
);
}
}
Call syncSubmoduleChanges(lane.worktreePath, repoRoot) at the end of executeLane() in execution.ts.
Alternatives Considered
Alternative Approaches Considered
| Approach |
Pros |
Cons |
| Post-execution sync (above) |
Minimal orchestrator changes, works now |
Agent still commits locally first; sync happens after |
| Git worktree for submodules |
Each submodule gets its own worktree with proper isolation |
Significant architecture change; complex to manage |
| Prevent submodule commits |
Simple — agent can't commit on submodule clones |
Agent needs to know which paths are submodules; error handling needed |
| Push submodule commits to origin |
Commits survive by design |
Requires write access to submodule remote; race conditions with other contributors |
Example Workflow
No response
Impact
Impact
Every project that uses git submodules and tasks that modify files inside them (library changes, dependency updates, tooling updates).
Checklist
Problem Statement
Problem Statement
When a task modifies files inside a git submodule (e.g.,
libs/my_lib/), the agent commits changes on the worktree's local clone of that submodule. These commits exist only in the worktree's isolated checkout and are never pushed to the submodule's upstream remote.During merge, the orchestrator brings the lane branch (which references the local-only commit) into the orch branch and ultimately into main. The main repo's submodule pointer now references a commit that does not exist on origin.
Consequences
fatal: remote error: upload-pack: not our ref <sha>Example
Root Cause
The orchestrator has no awareness that files inside a submodule path point to a separate git repository. It treats the submodule as regular files and lets the agent commit freely. When the lane branch is merged, the submodule pointer update is valid git plumbing but references an unreachable commit.
The orchestrator does not:
Proposed Solution
Proposed Fix
Add a post-execution submodule sync step in
execution.ts:Call
syncSubmoduleChanges(lane.worktreePath, repoRoot)at the end ofexecuteLane()inexecution.ts.Alternatives Considered
Alternative Approaches Considered
Example Workflow
No response
Impact
Impact
Every project that uses git submodules and tasks that modify files inside them (library changes, dependency updates, tooling updates).
Checklist