Skip to content

Initialize config-managed submodules at their branch tip - #802

Merged
alexander-yevsyukov merged 4 commits into
masterfrom
init-submodules-float-to-branch-tip
Oct 19, 2026
Merged

alexander-yevsyukov merged 4 commits into
masterfrom
init-submodules-float-to-branch-tip

Conversation

@alexander-yevsyukov

Copy link
Copy Markdown
Contributor

Problem

init-submodules is the bootstrap that gives a fresh working tree its agent
assets: a git worktree add, a shallow CI checkout, or a cloud session starts
with no submodules checked out, the .agents/skills → .agents/shared/skills
symlinks dangle, and no skills, scripts, or guidelines resolve. A SessionStart
hook runs the script automatically to fix that.

Until now it initialized a missing config-managed submodule and stopped at the
commit the superproject pins. That pin is not a version anyone maintains:

  • .gitmodules declares ignore = all for .agents/shared, which suppresses the
    submodule from git status and git diff;
  • ./config/pull floats the on-disk copy to the tracked branch tip without
    committing the moved gitlink
    .

So the drift is invisible by design, and the pin keeps the value it had on the day
the submodule was adopted — git log --all -- .agents/shared returns exactly one
commit, the adoption commit, in 17 of 19 consumer repositories.

An audit on 2026-10-16 found all 19 Spine consumer repos pinned at a
.agents/shared commit predating d0abf64 ("Make the shared hook scripts
worktree-aware", 2026-10-05) — pins dating from June to 1 October. Every fresh
worktree, CI checkout, and cloud session therefore materialized hook scripts,
skills, and guidelines nobody had run for months. In compiler, pinned at
7bd2d9ee, .agents/scripts/session-work-tree.sh did not exist at all.

Bumping the committed pins is a treadmill, and not even an available one:
Version Guard and ensure-reports-updated.yml make a submodule-pointer-only PR
structurally unmergeable in 15 of the 19 repos. The durable fix is to stop fresh
checkouts from materializing the stale pin in the first place.

Change

A config-managed submodule that is not yet checked out is now initialized and
then advanced to the tip of its tracked branch, with ./config/pull's own three
steps (fetch, checkout, merge --ff-only), leaving it on the branch rather
than in a detached HEAD.

Everything else is deliberately unchanged:

  • The "config-managed" rule is not widened. It stays config itself plus every
    submodule declaring a tracked branch in .gitmodules — exactly what
    ./config/pull floats, so the two scripts cannot disagree. Consumer-owned
    submodules (a Hugo theme, a vendored library, doc examples) declare no tracked
    branch and are still skipped, so the automatic per-session run never tries to
    clone — or fail on credentials for — a submodule the project does not manage.
  • A submodule already present is left exactly as it is, neither rewound to the
    pin nor advanced. The script still acts only on the - lines of
    git submodule status, and floating makes a submodule + (ahead of the pin), so
    a second run does nothing and touches the network not at all. A tree someone
    floated with ./config/pull, or deliberately parked on a commit, is never
    disturbed.
  • No failure can fail the session start; exit 0 semantics are preserved. With
    no network the clone is what fails, and the submodule is left uninitialized just
    as before. A float that fails after a successful clone warns and leaves the
    submodule wherever the steps that did succeed put it — at worst the pin, a
    usable, if older, tree. Because the submodule is checked out by then, it is no
    longer a - line and no later run retries the float, so that warning points at
    ./config/pull.

The path<TAB>branch table replaces the old path-only set, so one scan of
.gitmodules answers both "is this config-managed?" and "which branch?". Declared
entries come first, so a .gitmodules that does give config an explicit tracked
branch wins over the master default pull applies in step 1.

The header comment block and the AGENTS.md paragraph that migrate distributes
to consumers both documented the pin behaviour; both are updated.

Verification

In a throwaway git worktree of compiler (pinned at 7bd2d9ee):

  • .agents/shared landed on the agents origin/master tip afabda57, on
    heads/master, and config on its own origin/master tip;
  • .agents/scripts/session-work-tree.sh is present afterwards — it does not exist
    at 7bd2d9ee at all (git cat-file on the pin fails);
  • a second run initialized nothing, floated nothing, and exited 0.

In a synthetic consumer repo with one branch-tracking and one consumer-owned
submodule:

  • the branch-tracking submodule moved from the stale pin to the branch tip, and a
    file that exists only at the tip appeared;
  • the consumer-owned submodule stayed uninitialized across both runs, skipped with
    a note on stderr;
  • a float that could not fetch its branch warned, left a usable checkout, and still
    exited 0.

Reviewed by the review-docs and spine-code-review agents; the latter
reproduced all five invariants in its own throwaway repos. Both verdicts were
"approve with changes", and the changes they asked for are applied: the offline
wording now distinguishes a failed clone from a failed float, and the two git
calls inside the loop read from /dev/null so they cannot consume the loop's
path list.

A second review round, run against this exact diff before the PR was opened,
caught the documentation still overclaiming where the first round had only fixed
the inline comment: the header, the AGENTS.md paragraph, and the task file all
said a failed float leaves the submodule "as the clone left it", which is untrue
for one of the failure cases the header itself lists — when checkout succeeds
and merge --ff-only does not, HEAD has already moved off the pin. All three
now match the inline comment, and both reviewers' companion point is documented
too: a float that fails after a successful clone is never retried, because the
submodule is no longer a - line, so the warning now names ./config/pull
instead of claiming the submodule was "left as-is". No executable line changed
beyond that one warning string; bash -n and shellcheck are clean.

Notes

  • init-submodules lives in this repository, not in agents: migrate copies it
    to the consumer root (cp init-submodules ..), deliberately outside any
    submodule so a fresh worktree always checks it out. Consumers pick the change up
    on their next ./config/pull.
  • The gate scripts are untouched; Stop the version gate falling back to another checkout's branch agents#48 covers the gate's
    no-cwd fallback hardening independently.
  • Follow-up for the agents repo: the StalePinTest docstring in
    scripts/tests/test_hooks.py says a worktree's .agents/shared sits "at the
    branch's old pin, as init-submodules leaves it". The test itself builds that
    fixture with git submodule update --init and does not call init-submodules,
    so nothing breaks — only the aside is now inaccurate.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T15:16:49.793485Z 896480d New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e3f3ed3a4f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread init-submodules
`init-submodules` initialized a missing config-managed submodule and left it
at the commit the superproject pins. That pin is not a version anyone
maintains: `.gitmodules` declares `ignore = all` for `.agents/shared`, hiding
the submodule from `git status` and `git diff`, and `./config/pull` floats the
on-disk copy without ever committing the moved gitlink. An audit on
2026-10-16 found the pin months stale in all 19 Spine consumer repositories,
so every fresh `git worktree add`, CI checkout, and cloud session materialized
hook scripts, skills, and guidelines nobody had run for months.

A freshly initialized submodule is now advanced to the tip of its tracked
branch with `./config/pull`'s own three steps, leaving it on the branch rather
than in a detached `HEAD`. The "config-managed" rule is unchanged — `config`
itself plus every submodule declaring a tracked `branch` — so consumer-owned
submodules are still skipped, and the script still acts only on the `-` lines
of `git submodule status`, so a submodule already present is neither rewound
nor advanced. A float that fails warns and leaves the submodule wherever the
steps that did succeed put it, at worst the pin, which is still a usable
checkout; because the submodule is checked out by then, no later run retries
the float, so the warning points at `./config/pull`. The script keeps its
`exit 0` semantics throughout, so the `SessionStart` hook can never fail a
session.

A `branch = .` declaration, which gitmodules(5) defines as the branch the
superproject is on, is resolved before it reaches a refspec: `git fetch origin
.` is a `fatal: invalid refspec`, which would leave the pin with no retry. With
the superproject itself detached there is no name to inherit, so the float is
skipped and the pin kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approved

No blocking issues were identified in the scoped initialization, branch selection, or failure handling.

No open issues 🎉

What changed in this PR

Updates bootstrap initialization so fresh working trees receive current config-managed submodules rather than stale pinned commits.

Changes:

  • Initializes missing managed submodules and advances them to their tracked branch tips.
  • Preserves existing checkouts, consumer-owned exclusions, and non-fatal failure handling.
  • Updates bootstrap documentation and records verification.
File Description
init-submodules Adds tracked-branch resolution and floating after initialization.
AGENTS.md Documents branch-tip initialization and failure behavior.
.agents/​tasks/​init-submodules-float-to-branch-tip.md Records rationale, implementation decisions, and verification.

Review effort: Balanced

alexander-yevsyukov added a commit that referenced this pull request Oct 19, 2026
`git rev-parse --abbrev-ref HEAD` shortens a ref to a *non-ambiguous* name, so
a superproject that carries both a branch and a tag called `foo` resolves `.`
to `heads/foo`. That is not a no-op: the fetch of `heads/foo` succeeds, the
following `git checkout heads/foo` then fails in the submodule, and the float
leaves the submodule stale and detached at the superproject's pin while
warning about `origin/heads/foo`.

`git branch --show-current` prints the branch name exactly and prints nothing
when `HEAD` is detached, so it both fixes the ambiguity and keeps signalling
detachment. `git symbolic-ref --short` is no alternative — it applies the same
shortening and returns `heads/foo` too, which the throwaway repo confirmed.
The `[ "$branch" = "HEAD" ]` half of the detachment guard is dropped along
with the old command, since `--show-current` never prints that.

`init-submodules` resolves `.` the same way on the unmerged branch of #802 and
needs the identical change; the two scripts are kept in step deliberately.

Reported by the Codex review of #804.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 896480d222

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread init-submodules
Comment thread init-submodules
@alexander-yevsyukov
alexander-yevsyukov merged commit c85ce53 into master Oct 19, 2026
2 checks passed
@alexander-yevsyukov
alexander-yevsyukov deleted the init-submodules-float-to-branch-tip branch October 19, 2026 15:18
@github-project-automation github-project-automation Bot moved this from In Review to ✅ Done in v2.0 Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

2 participants