Skip to content

Commit 7aa1089

Browse files
NagyViktNagyViktclaude
authored
Plan: submodule-aware gitguardex (T3 OpenSpec change + 6 role prompts) (#537)
Adds the OpenSpec change `agent-claude-submodule-aware-gx-2026-05-07-18-46` plus a full T3 plan workspace under `openspec/plan/<slug>/`: - proposal.md, tasks.md, and `specs/gitguardex-submodules/spec.md` with 4 ADDED requirements covering detection, (repo,path) lock keying, atomic gitlink bump after all child PRs merge, and cross-org GITHUB_TOKEN preflight. - summary.md, README.md, checkpoints.md, open-questions.md. - planner / architect / critic / executor / writer / verifier tasks.md files — each with role goal, scope boundary, the standard 4-phase shape, and paste-ready handoff fields. Why: `.gitmodules` lists 5 submodules but `scripts/agent-branch-{start,finish}.sh`, `scripts/agent-file-locks.py`, and `scripts/codex-agent.sh` contain zero submodule references. `git submodule status` currently reports three submodules as `-` prefixed (uninitialized) while `git status` reports `m` (modified) for the same paths — agent edits are stranded with no commit path. This commit is plan-only. Implementation lands in subsequent commits per the executor role's task list. `.gitignore` adds an allowlist entry so the plan workspace ships to the PR (matches existing `migrate-multica-runtime-model` and `role-artifact-smoke-main` exemptions). Co-authored-by: NagyVikt <nagy.viktordp@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c6abe19 commit 7aa1089

14 files changed

Lines changed: 798 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ openspec/plan/*
7676
!openspec/plan/migrate-multica-runtime-model/**
7777
!openspec/plan/role-artifact-smoke-main/
7878
!openspec/plan/role-artifact-smoke-main/**
79+
!openspec/plan/agent-claude-submodule-aware-gx-2026-05-07-18-46/
80+
!openspec/plan/agent-claude-submodule-aware-gx-2026-05-07-18-46/**
7981

8082
# multiagent-safety:START
8183
.omx/
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Submodule-aware gitguardex: detect, claim, commit, PR, merge nested repos
2+
3+
## Why
4+
5+
`.gitmodules` lists 5 submodules in this repo (`examples/conductor`,
6+
`examples/dmux`, `examples/hive`, `examples/skills_for_claude`,
7+
`vscode-material-icon-theme`). `git submodule status` shows three of
8+
them as uninitialized-but-disk-modified (`-` prefix in `git submodule
9+
status` plus `m` in `git status`). That state is the smoking gun: a
10+
prior agent edited files inside submodule paths, the parent recorded
11+
nothing, and the changes are now stranded on disk with no commit
12+
path.
13+
14+
Today `scripts/agent-branch-start.sh`, `scripts/agent-branch-finish.sh`,
15+
`scripts/agent-file-locks.py`, and `scripts/codex-agent.sh` contain
16+
**zero** `submodule` references — the entire Guardex lifecycle
17+
ignores nested repos. When Codex/Claude works inside a submodule
18+
path, edits never reach the submodule's remote and the parent's
19+
gitlink is never bumped.
20+
21+
This change makes the gx lifecycle submodule-aware: detect at
22+
`branch start`, claim per-(repo,path), and run a per-submodule
23+
commit→push→PR→merge cycle on `branch finish`, then atomically bump
24+
all parent gitlinks in one parent commit.
25+
26+
## What Changes
27+
28+
- **Detection** (`gx branch start` and a new `gx submodules status`):
29+
parse `.gitmodules`, run `git submodule status`, classify each
30+
submodule as `clean | dirty | uninitialized | missing-remote`.
31+
- **Auto-init** (`gx branch start`, default on): run `git submodule
32+
update --init --recursive` inside the new worktree before tier
33+
scaffold so agent edits land in real submodule trees, not stranded
34+
paths. Opt-out via `GUARDEX_SUBMODULE_INIT=0`.
35+
- **Lock keying** (`scripts/agent-file-locks.py`): claim records key
36+
on `(submodule_root, relative_path)` instead of bare absolute path.
37+
Parent-repo and submodule paths share no lock namespace.
38+
- **Per-submodule write flow** (`gx branch finish`): for each dirty
39+
submodule, create `agent/<owner>/<slug>` inside the submodule,
40+
commit, push, open a PR via `gh -R <owner>/<repo>`, and wait for
41+
merge. Configurable via `GUARDEX_SUBMODULE_MODE`:
42+
- `off` — skip entirely (legacy behavior).
43+
- `sync-only` — detect drift, refuse, surface BLOCKED.
44+
- `commit-only` — commit + push to a topic branch, no PR.
45+
- `full-pr` (default) — full PR cycle.
46+
- **Atomic gitlink bump**: parent does NOT bump submodule SHAs
47+
incrementally. Only after every submodule PR reaches `MERGED` does
48+
the parent stage all gitlink updates in a single commit
49+
(`chore(submodules): bump gitlinks for <slug>`). On any submodule
50+
failure, parent stages no bumps and finish exits BLOCKED with
51+
rollback instructions.
52+
- **Multi-org token preflight**: at `branch start`, gx probes
53+
`GET https://api.github.com/repos/<owner>/<repo>` with the active
54+
token for every submodule whose URL host is github.com. Missing
55+
write permission anywhere fails fast with a remediation message.
56+
- **Per-submodule gate-skip**: `openspec validate --specs` is run
57+
only against submodules that own an `openspec/` directory; absent
58+
`openspec/` records a deliberate skip in the finish report.
59+
- **New capability spec** (`openspec/specs/gitguardex-submodules/`)
60+
to make these behaviors testable.
61+
62+
## Impact
63+
64+
- **Affected specs** — adds `gitguardex-submodules` capability;
65+
`gitguardex-branch-lifecycle` (if extracted) gains a §I link to
66+
the new capability.
67+
- **Affected code**`scripts/agent-branch-start.sh`,
68+
`scripts/agent-branch-finish.sh`, `scripts/agent-file-locks.py`,
69+
`scripts/codex-agent.sh`, plus a new `scripts/agent-submodules.py`
70+
helper. New tests under `test/agent-submodules.test.*`.
71+
- **Affected docs**`AGENTS.md` Multi-Agent Execution Contract
72+
gains a "Submodules" subsection; `README.md` "How it works" gains
73+
a one-paragraph submodule note.
74+
- **Migration** — first run on a repo with dirty submodules will
75+
refuse `branch finish` until the operator chooses a mode. This is
76+
intentional: silently committing a stranded `m examples/hive`
77+
state into a submodule's `main` would amount to undisclosed
78+
history rewrites for that downstream repo.
79+
- **Risk** — full-PR mode multiplies merge surface (5× repos × 5×
80+
protected-branch dance × 5× wait loops). Mitigations: the mode
81+
switch above; a single shared `--wait-for-merge` poller across
82+
all PRs; fail-fast preflight.
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
## ADDED Requirements
2+
3+
### Requirement: gx auto-recognizes submodules at branch start
4+
`gx branch start` SHALL parse `.gitmodules` (when present), run `git
5+
submodule status` inside the new worktree, classify every submodule
6+
as `clean | dirty | uninitialized | missing-remote`, persist the
7+
classification at
8+
`.omc/agent-worktrees/<slug>/.guardex/submodules.json`, and run `git
9+
submodule update --init --recursive` inside the worktree unless
10+
`GUARDEX_SUBMODULE_INIT=0` is set in the environment.
11+
12+
#### Scenario: Clean repo with five configured submodules
13+
- **WHEN** `gx branch start "<task>" "<agent>"` runs in a worktree
14+
whose `.gitmodules` lists five submodules and `git submodule
15+
status` reports all five as initialized
16+
- **THEN** `submodules.json` contains exactly five entries
17+
- **AND** every entry has `state: "clean"` and a non-null
18+
`parent_gitlink_sha`
19+
- **AND** the worktree's submodule paths each contain a populated
20+
`.git` file or directory (init succeeded).
21+
22+
#### Scenario: Uninitialized submodule with on-disk modifications
23+
- **WHEN** `gx branch start` runs in a repo where `git submodule
24+
status` reports `examples/hive` with a `-` prefix and `git status`
25+
reports `m examples/hive`
26+
- **THEN** the manifest entry for `examples/hive` records
27+
`state: "dirty"` and `was_uninitialized: true`
28+
- **AND** finish-time refusal is set on the entry so a future `gx
29+
branch finish` cannot silently push stranded edits.
30+
31+
#### Scenario: Operator opts out of init
32+
- **WHEN** `GUARDEX_SUBMODULE_INIT=0 gx branch start ...` runs
33+
- **THEN** `git submodule update --init` is NOT invoked
34+
- **AND** the manifest still records `state: "uninitialized"` for
35+
every uninitialized submodule
36+
- **AND** the start log prints `submodule init: skipped
37+
(GUARDEX_SUBMODULE_INIT=0)`.
38+
39+
### Requirement: File locks key on (submodule_root, relative_path)
40+
`scripts/agent-file-locks.py` SHALL key claim records on the tuple
41+
`(submodule_root, relative_path)` rather than the bare absolute or
42+
parent-relative path. The same relative path inside the parent repo
43+
and inside a submodule SHALL NOT collide.
44+
45+
#### Scenario: Same filename in parent and submodule
46+
- **WHEN** branch `agent/claude/foo` claims
47+
`examples/hive/src/index.js` and branch `agent/codex/bar` claims
48+
`src/index.js` in the parent
49+
- **THEN** both claims succeed
50+
- **AND** the locks file records two distinct entries with
51+
`submodule_root` values `"examples/hive"` and `""` respectively.
52+
53+
#### Scenario: Cross-branch collision inside the same submodule
54+
- **WHEN** branch `agent/claude/foo` already holds
55+
`examples/hive/src/index.js` and branch `agent/codex/bar` attempts
56+
to claim the same path
57+
- **THEN** the second claim fails with exit code `2`
58+
- **AND** the failure message identifies the holder branch and the
59+
submodule root.
60+
61+
#### Scenario: Legacy lock entries remain readable
62+
- **WHEN** the locks file already contains an entry written by the
63+
pre-tuple format (bare path string)
64+
- **THEN** `agent-file-locks.py status` lists it without crashing
65+
- **AND** any new claim involving that path is rewritten to the
66+
tuple format on first contact.
67+
68+
### Requirement: gx finish atomically bumps parent gitlinks
69+
`gx branch finish` SHALL stage parent-repo gitlink updates for all
70+
dirty submodules in **exactly one** parent commit, and SHALL stage
71+
that commit only after every submodule PR has reached merge state
72+
`MERGED`. On any submodule failure the parent SHALL NOT receive a
73+
gitlink bump for any submodule, and finish SHALL exit with status
74+
`BLOCKED` and a recovery hint.
75+
76+
#### Scenario: All submodule PRs merge
77+
- **WHEN** `gx branch finish ... --via-pr --wait-for-merge` runs
78+
with three dirty submodules and all three child PRs reach
79+
`MERGED`
80+
- **THEN** the parent's last commit before the parent PR is opened
81+
has subject `chore(submodules): bump gitlinks for <slug>`
82+
- **AND** the commit's diff updates exactly three gitlink entries
83+
- **AND** no parent commit was pushed earlier than the final bump.
84+
85+
#### Scenario: One submodule PR fails to merge
86+
- **WHEN** two child PRs reach `MERGED` and the third is closed
87+
without merge
88+
- **THEN** finish exits non-zero with a `BLOCKED:` line that
89+
includes the failed submodule path and its PR URL
90+
- **AND** `git -C <parent> diff --name-only HEAD` lists no
91+
submodule path bumps
92+
- **AND** the manifest records `parent_bump: "skipped"` with a
93+
`reason` field.
94+
95+
#### Scenario: `commit-only` mode skips PR step
96+
- **WHEN** `GUARDEX_SUBMODULE_MODE=commit-only gx branch finish ...`
97+
runs with one dirty submodule
98+
- **THEN** the submodule's topic branch is pushed but no `gh pr
99+
create` is issued
100+
- **AND** the parent's gitlink bump targets the pushed topic
101+
branch's HEAD SHA
102+
- **AND** the finish report records `mode: "commit-only"`.
103+
104+
### Requirement: gx preflights cross-org token write permission
105+
`gx branch start` SHALL probe write permission on every github.com
106+
submodule remote using the active `GITHUB_TOKEN`, and SHALL fail
107+
fast if any submodule is unreachable or its token-derived
108+
permission level is below `push`. Non-github.com remotes SHALL be
109+
recorded as `permission: "unverified"` without failing start.
110+
111+
#### Scenario: Token has push on every submodule remote
112+
- **WHEN** `gx branch start` runs with five github.com submodules
113+
and `GET /repos/<owner>/<repo>` returns
114+
`"permissions": {"push": true}` for each
115+
- **THEN** start completes
116+
- **AND** the manifest's `preflight` field is `"ok"` for every
117+
submodule.
118+
119+
#### Scenario: Token lacks push on one submodule
120+
- **WHEN** start runs and `repos/<owner>/<repo>` for one submodule
121+
returns `"permissions": {"push": false, "pull": true}`
122+
- **THEN** start exits non-zero with a message naming the
123+
unreachable repo and the remediation
124+
(`gh auth refresh -s repo` or token rotation)
125+
- **AND** no worktree is left dirty (start cleans up the partial
126+
scaffold).
127+
128+
#### Scenario: Self-hosted git remote
129+
- **WHEN** a submodule URL points to `git.internal.example.com`
130+
- **THEN** the manifest entry records `permission: "unverified"`
131+
and `host: "git.internal.example.com"`
132+
- **AND** start does not block on that submodule.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Tasks
2+
3+
## 1. Spec
4+
- [x] 1.1 Capture proposal in `proposal.md` (root cause + 5 invariants).
5+
- [x] 1.2 Capture spec delta in `specs/gitguardex-submodules/spec.md`
6+
(4 ADDED requirements with BDD scenarios).
7+
- [ ] 1.3 Run `openspec validate --specs` and attach output to the
8+
finish handoff.
9+
10+
## 2. Tests
11+
- [ ] 2.1 Add `test/agent-submodules-detect.test.js` covering
12+
`parseGitmodules`, `classifySubmodule` (clean / dirty /
13+
uninitialized / missing-remote), and `manifestForFinish`.
14+
- [ ] 2.2 Add `test/agent-submodules-locks.test.py` covering
15+
`(submodule_root, path)` keying — same relative path inside
16+
parent and submodule must NOT collide.
17+
- [ ] 2.3 Add `test/agent-submodules-finish.test.js` covering the
18+
atomic gitlink bump (one parent commit only after all child
19+
PRs hit `MERGED`; partial-failure leaves parent untouched).
20+
- [ ] 2.4 Add `test/agent-submodules-preflight.test.js` covering the
21+
cross-org token probe (mocks `api.github.com/repos/...`
22+
returning 404/401/403/200 → expected failure modes).
23+
24+
## 3. Implementation
25+
- [ ] 3.1 Add `scripts/agent-submodules.py` with `parse_gitmodules`,
26+
`submodule_status`, `classify`, `manifest_for_branch`,
27+
`preflight_token`, `init_in_worktree`.
28+
- [ ] 3.2 Extend `scripts/agent-branch-start.sh`: after worktree
29+
creation, run `init_in_worktree` (skipped under
30+
`GUARDEX_SUBMODULE_INIT=0`); record manifest in
31+
`.omc/agent-worktrees/<slug>/.guardex/submodules.json`.
32+
- [ ] 3.3 Extend `scripts/agent-file-locks.py`: replace bare-path
33+
keying with `(submodule_root, relative_path)`; back-compat
34+
read of legacy lock entries.
35+
- [ ] 3.4 Extend `scripts/agent-branch-finish.sh`: read manifest,
36+
loop submodules in `GUARDEX_SUBMODULE_MODE`, share one
37+
merge-wait poller, stage atomic gitlink bump only after all
38+
child PRs hit `MERGED`.
39+
- [ ] 3.5 Add `gx submodules status` and `gx submodules preflight`
40+
subcommands.
41+
- [ ] 3.6 Update `AGENTS.md` Multi-Agent Execution Contract with a
42+
"Submodules" subsection; update README "How it works".
43+
44+
## 4. Verification
45+
- [ ] 4.1 `openspec validate --specs` → green.
46+
- [ ] 4.2 `npm test` → green (or recorded skip with reason).
47+
- [ ] 4.3 Live walkthrough on this repo: clean state → edit a file
48+
in `examples/hive``gx branch finish --via-pr`
49+
--wait-for-merge` → confirm child PR opens in `NagyVikt/hive`,
50+
merges, and parent commit lists exactly the bumped gitlinks.
51+
52+
## 5. Cleanup
53+
- [ ] 5.1 Commit changes on the agent branch.
54+
- [ ] 5.2 Push branch and open a PR.
55+
- [ ] 5.3 Run `gx branch finish ... --base main --via-pr
56+
--wait-for-merge --cleanup`.
57+
- [ ] 5.4 Record PR URL and `MERGED` evidence in handoff note.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Plan Workspace: agent-claude-submodule-aware-gx-2026-05-07-18-46
2+
3+
Durable pre-implementation planning workspace.
4+
5+
Use this command to update checkpoints:
6+
7+
```bash
8+
/opsx:checkpoint agent-claude-submodule-aware-gx-2026-05-07-18-46 <role> <checkpoint-id> <state> <note...>
9+
```
10+
11+
Roles (each has its own `tasks.md`):
12+
13+
- `planner/` — owns spec + open-questions
14+
- `architect/` — owns manifest schema + failure catalog
15+
- `critic/` — stress-tests design + executor diff
16+
- `executor/` — implements (tests first, then code)
17+
- `writer/` — keeps AGENTS.md, README, and context.md in sync
18+
- `verifier/` — proves the change works against this repo before archive
19+
20+
See `summary.md` for the high-level intent and `open-questions.md`
21+
for unresolved tradeoffs.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# architect tasks — submodule-aware gx
2+
3+
> **Role goal**: turn the spec into an implementable shape. Pick
4+
> data structures, decide where new code lives, choose the
5+
> dependency graph between scripts, and document the failure
6+
> modes the executor must handle.
7+
8+
## Scope boundary
9+
10+
In scope:
11+
- shape of `scripts/agent-submodules.py` (functions, return types,
12+
invariants)
13+
- shape of `.guardex/submodules.json` (manifest schema)
14+
- shape of the new lock-record tuple in `agent-file-locks.py`
15+
- shape of `gx submodules` subcommand routing
16+
- failure mode catalog (what happens on each failure → which
17+
recovery path)
18+
19+
Out of scope:
20+
- writing implementation code (executor)
21+
- writing tests (executor authors, critic reviews)
22+
23+
## 1. Spec
24+
- [ ] 1.1 Enumerate every state transition for a submodule across
25+
its lifecycle: `unknown → uninitialized → init → clean →
26+
dirty → committed → pushed → pr-open → pr-merged →
27+
gitlink-bumped`. Identify which step is allowed to fail
28+
silently.
29+
- [ ] 1.2 Author the manifest schema. Required keys: `name`,
30+
`path`, `url`, `branch`, `state`, `was_uninitialized`,
31+
`parent_gitlink_sha`, `child_branch`, `child_pr_url`,
32+
`child_pr_state`, `permission`, `host`, `mode`,
33+
`parent_bump`, `reason`.
34+
35+
## 2. Tests
36+
- [ ] 2.1 Confirm the failure catalog has a corresponding negative
37+
test in `tasks.md §2`. Add any missing entries.
38+
39+
## 3. Implementation
40+
- [ ] 3.1 Choose: should `agent-submodules.py` shell out to `git`
41+
or use `dulwich`/`pygit2`? Record reasoning. (Default:
42+
shell out to `git` via `subprocess.run`; matches existing
43+
script style.)
44+
- [ ] 3.2 Choose the locking strategy when an agent edits across
45+
parent + submodule in the same task: separate claims, or
46+
one combined claim with a `repo` segment per file? Record
47+
reasoning. (Default: separate claim per `(repo, path)`.)
48+
- [ ] 3.3 Author `failure-modes.md` (sibling of this file) listing
49+
every failure → user-facing message → recovery command.
50+
- [ ] 3.4 Approve or reject the proposed `GUARDEX_SUBMODULE_MODE`
51+
and `GUARDEX_SUBMODULE_INIT` env-var contract.
52+
53+
## 4. Checkpoints
54+
- [ ] 4.1 Publish architect checkpoint after the manifest schema
55+
and failure catalog land.
56+
57+
## Handoff fields
58+
59+
```
60+
branch=agent/claude/submodule-aware-gx-2026-05-07-18-46
61+
task=architect
62+
blocker=<empty when done>
63+
next=critic | executor
64+
evidence=openspec/plan/<slug>/architect/failure-modes.md, manifest schema in proposal.md
65+
```
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Plan Checkpoints: submodule-aware gitguardex
2+
3+
Chronological checkpoint log for all roles.
4+
5+
- 2026-05-07T18:46:00+02:00 | role=planner | scope=openspec/changes/<slug>/proposal.md,specs/gitguardex-submodules/spec.md | action=Drafted proposal with five §V invariants (auto-init, tuple lock keying, atomic gitlink bump, cross-org preflight, per-submodule openspec gate-skip) and 4 ADDED requirements with negative scenarios.
6+
- 2026-05-07T18:46:00+02:00 | role=planner | scope=openspec/plan/<slug>/{summary,checkpoints,open-questions,README,planner,architect,critic,executor,writer,verifier} | action=Scaffolded role tasks files; default GUARDEX_SUBMODULE_MODE=full-pr; default GUARDEX_SUBMODULE_INIT=on; gx submodules subcommand limited to status+preflight in this PR.

0 commit comments

Comments
 (0)