Skip to content

feat: submodule support #517

@nicco

Description

@nicco

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

  1. Cloning fails — anyone who clones the main repo gets fatal: remote error: upload-pack: not our ref <sha>
  2. Commits are lost — when the worktree is cleaned up, the submodule commits become unreachable and are garbage collected
  3. 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

  • I searched existing issues and discussions
  • This request is scoped to Taskplane's goals (task execution/orchestration/docs/tooling)

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions