Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ openspec/plan/*
!openspec/plan/migrate-multica-runtime-model/**
!openspec/plan/role-artifact-smoke-main/
!openspec/plan/role-artifact-smoke-main/**
!openspec/plan/agent-claude-submodule-aware-gx-2026-05-07-18-46/
!openspec/plan/agent-claude-submodule-aware-gx-2026-05-07-18-46/**

# multiagent-safety:START
.omx/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Submodule-aware gitguardex: detect, claim, commit, PR, merge nested repos

## Why

`.gitmodules` lists 5 submodules in this repo (`examples/conductor`,
`examples/dmux`, `examples/hive`, `examples/skills_for_claude`,
`vscode-material-icon-theme`). `git submodule status` shows three of
them as uninitialized-but-disk-modified (`-` prefix in `git submodule
status` plus `m` in `git status`). That state is the smoking gun: a
prior agent edited files inside submodule paths, the parent recorded
nothing, and the changes are now stranded on disk with no commit
path.

Today `scripts/agent-branch-start.sh`, `scripts/agent-branch-finish.sh`,
`scripts/agent-file-locks.py`, and `scripts/codex-agent.sh` contain
**zero** `submodule` references — the entire Guardex lifecycle
ignores nested repos. When Codex/Claude works inside a submodule
path, edits never reach the submodule's remote and the parent's
gitlink is never bumped.

This change makes the gx lifecycle submodule-aware: detect at
`branch start`, claim per-(repo,path), and run a per-submodule
commit→push→PR→merge cycle on `branch finish`, then atomically bump
all parent gitlinks in one parent commit.

## What Changes

- **Detection** (`gx branch start` and a new `gx submodules status`):
parse `.gitmodules`, run `git submodule status`, classify each
submodule as `clean | dirty | uninitialized | missing-remote`.
- **Auto-init** (`gx branch start`, default on): run `git submodule
update --init --recursive` inside the new worktree before tier
scaffold so agent edits land in real submodule trees, not stranded
paths. Opt-out via `GUARDEX_SUBMODULE_INIT=0`.
- **Lock keying** (`scripts/agent-file-locks.py`): claim records key
on `(submodule_root, relative_path)` instead of bare absolute path.
Parent-repo and submodule paths share no lock namespace.
- **Per-submodule write flow** (`gx branch finish`): for each dirty
submodule, create `agent/<owner>/<slug>` inside the submodule,
commit, push, open a PR via `gh -R <owner>/<repo>`, and wait for
merge. Configurable via `GUARDEX_SUBMODULE_MODE`:
- `off` — skip entirely (legacy behavior).
- `sync-only` — detect drift, refuse, surface BLOCKED.
- `commit-only` — commit + push to a topic branch, no PR.
- `full-pr` (default) — full PR cycle.
- **Atomic gitlink bump**: parent does NOT bump submodule SHAs
incrementally. Only after every submodule PR reaches `MERGED` does
the parent stage all gitlink updates in a single commit
(`chore(submodules): bump gitlinks for <slug>`). On any submodule
failure, parent stages no bumps and finish exits BLOCKED with
rollback instructions.
- **Multi-org token preflight**: at `branch start`, gx probes
`GET https://api.github.com/repos/<owner>/<repo>` with the active
token for every submodule whose URL host is github.com. Missing
write permission anywhere fails fast with a remediation message.
- **Per-submodule gate-skip**: `openspec validate --specs` is run
only against submodules that own an `openspec/` directory; absent
`openspec/` records a deliberate skip in the finish report.
- **New capability spec** (`openspec/specs/gitguardex-submodules/`)
to make these behaviors testable.

## Impact

- **Affected specs** — adds `gitguardex-submodules` capability;
`gitguardex-branch-lifecycle` (if extracted) gains a §I link to
the new capability.
- **Affected code** — `scripts/agent-branch-start.sh`,
`scripts/agent-branch-finish.sh`, `scripts/agent-file-locks.py`,
`scripts/codex-agent.sh`, plus a new `scripts/agent-submodules.py`
helper. New tests under `test/agent-submodules.test.*`.
- **Affected docs** — `AGENTS.md` Multi-Agent Execution Contract
gains a "Submodules" subsection; `README.md` "How it works" gains
a one-paragraph submodule note.
- **Migration** — first run on a repo with dirty submodules will
refuse `branch finish` until the operator chooses a mode. This is
intentional: silently committing a stranded `m examples/hive`
state into a submodule's `main` would amount to undisclosed
history rewrites for that downstream repo.
- **Risk** — full-PR mode multiplies merge surface (5× repos × 5×
protected-branch dance × 5× wait loops). Mitigations: the mode
switch above; a single shared `--wait-for-merge` poller across
all PRs; fail-fast preflight.
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
## ADDED Requirements

### Requirement: gx auto-recognizes submodules at branch start
`gx branch start` SHALL parse `.gitmodules` (when present), run `git
submodule status` inside the new worktree, classify every submodule
as `clean | dirty | uninitialized | missing-remote`, persist the
classification at
`.omc/agent-worktrees/<slug>/.guardex/submodules.json`, and run `git
submodule update --init --recursive` inside the worktree unless
`GUARDEX_SUBMODULE_INIT=0` is set in the environment.

#### Scenario: Clean repo with five configured submodules
- **WHEN** `gx branch start "<task>" "<agent>"` runs in a worktree
whose `.gitmodules` lists five submodules and `git submodule
status` reports all five as initialized
- **THEN** `submodules.json` contains exactly five entries
- **AND** every entry has `state: "clean"` and a non-null
`parent_gitlink_sha`
- **AND** the worktree's submodule paths each contain a populated
`.git` file or directory (init succeeded).

#### Scenario: Uninitialized submodule with on-disk modifications
- **WHEN** `gx branch start` runs in a repo where `git submodule
status` reports `examples/hive` with a `-` prefix and `git status`
reports `m examples/hive`
- **THEN** the manifest entry for `examples/hive` records
`state: "dirty"` and `was_uninitialized: true`
- **AND** finish-time refusal is set on the entry so a future `gx
branch finish` cannot silently push stranded edits.

#### Scenario: Operator opts out of init
- **WHEN** `GUARDEX_SUBMODULE_INIT=0 gx branch start ...` runs
- **THEN** `git submodule update --init` is NOT invoked
- **AND** the manifest still records `state: "uninitialized"` for
every uninitialized submodule
- **AND** the start log prints `submodule init: skipped
(GUARDEX_SUBMODULE_INIT=0)`.

### Requirement: File locks key on (submodule_root, relative_path)
`scripts/agent-file-locks.py` SHALL key claim records on the tuple
`(submodule_root, relative_path)` rather than the bare absolute or
parent-relative path. The same relative path inside the parent repo
and inside a submodule SHALL NOT collide.

#### Scenario: Same filename in parent and submodule
- **WHEN** branch `agent/claude/foo` claims
`examples/hive/src/index.js` and branch `agent/codex/bar` claims
`src/index.js` in the parent
- **THEN** both claims succeed
- **AND** the locks file records two distinct entries with
`submodule_root` values `"examples/hive"` and `""` respectively.

#### Scenario: Cross-branch collision inside the same submodule
- **WHEN** branch `agent/claude/foo` already holds
`examples/hive/src/index.js` and branch `agent/codex/bar` attempts
to claim the same path
- **THEN** the second claim fails with exit code `2`
- **AND** the failure message identifies the holder branch and the
submodule root.

#### Scenario: Legacy lock entries remain readable
- **WHEN** the locks file already contains an entry written by the
pre-tuple format (bare path string)
- **THEN** `agent-file-locks.py status` lists it without crashing
- **AND** any new claim involving that path is rewritten to the
tuple format on first contact.

### Requirement: gx finish atomically bumps parent gitlinks
`gx branch finish` SHALL stage parent-repo gitlink updates for all
dirty submodules in **exactly one** parent commit, and SHALL stage
that commit only after every submodule PR has reached merge state
`MERGED`. On any submodule failure the parent SHALL NOT receive a
gitlink bump for any submodule, and finish SHALL exit with status
`BLOCKED` and a recovery hint.

#### Scenario: All submodule PRs merge
- **WHEN** `gx branch finish ... --via-pr --wait-for-merge` runs
with three dirty submodules and all three child PRs reach
`MERGED`
- **THEN** the parent's last commit before the parent PR is opened
has subject `chore(submodules): bump gitlinks for <slug>`
- **AND** the commit's diff updates exactly three gitlink entries
- **AND** no parent commit was pushed earlier than the final bump.

#### Scenario: One submodule PR fails to merge
- **WHEN** two child PRs reach `MERGED` and the third is closed
without merge
- **THEN** finish exits non-zero with a `BLOCKED:` line that
includes the failed submodule path and its PR URL
- **AND** `git -C <parent> diff --name-only HEAD` lists no
submodule path bumps
- **AND** the manifest records `parent_bump: "skipped"` with a
`reason` field.

#### Scenario: `commit-only` mode skips PR step
- **WHEN** `GUARDEX_SUBMODULE_MODE=commit-only gx branch finish ...`
runs with one dirty submodule
- **THEN** the submodule's topic branch is pushed but no `gh pr
create` is issued
- **AND** the parent's gitlink bump targets the pushed topic
branch's HEAD SHA
- **AND** the finish report records `mode: "commit-only"`.

### Requirement: gx preflights cross-org token write permission
`gx branch start` SHALL probe write permission on every github.com
submodule remote using the active `GITHUB_TOKEN`, and SHALL fail
fast if any submodule is unreachable or its token-derived
permission level is below `push`. Non-github.com remotes SHALL be
recorded as `permission: "unverified"` without failing start.

#### Scenario: Token has push on every submodule remote
- **WHEN** `gx branch start` runs with five github.com submodules
and `GET /repos/<owner>/<repo>` returns
`"permissions": {"push": true}` for each
- **THEN** start completes
- **AND** the manifest's `preflight` field is `"ok"` for every
submodule.

#### Scenario: Token lacks push on one submodule
- **WHEN** start runs and `repos/<owner>/<repo>` for one submodule
returns `"permissions": {"push": false, "pull": true}`
- **THEN** start exits non-zero with a message naming the
unreachable repo and the remediation
(`gh auth refresh -s repo` or token rotation)
- **AND** no worktree is left dirty (start cleans up the partial
scaffold).

#### Scenario: Self-hosted git remote
- **WHEN** a submodule URL points to `git.internal.example.com`
- **THEN** the manifest entry records `permission: "unverified"`
and `host: "git.internal.example.com"`
- **AND** start does not block on that submodule.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Tasks

## 1. Spec
- [x] 1.1 Capture proposal in `proposal.md` (root cause + 5 invariants).
- [x] 1.2 Capture spec delta in `specs/gitguardex-submodules/spec.md`
(4 ADDED requirements with BDD scenarios).
- [ ] 1.3 Run `openspec validate --specs` and attach output to the
finish handoff.

## 2. Tests
- [ ] 2.1 Add `test/agent-submodules-detect.test.js` covering
`parseGitmodules`, `classifySubmodule` (clean / dirty /
uninitialized / missing-remote), and `manifestForFinish`.
- [ ] 2.2 Add `test/agent-submodules-locks.test.py` covering
`(submodule_root, path)` keying — same relative path inside
parent and submodule must NOT collide.
- [ ] 2.3 Add `test/agent-submodules-finish.test.js` covering the
atomic gitlink bump (one parent commit only after all child
PRs hit `MERGED`; partial-failure leaves parent untouched).
- [ ] 2.4 Add `test/agent-submodules-preflight.test.js` covering the
cross-org token probe (mocks `api.github.com/repos/...`
returning 404/401/403/200 → expected failure modes).

## 3. Implementation
- [ ] 3.1 Add `scripts/agent-submodules.py` with `parse_gitmodules`,
`submodule_status`, `classify`, `manifest_for_branch`,
`preflight_token`, `init_in_worktree`.
- [ ] 3.2 Extend `scripts/agent-branch-start.sh`: after worktree
creation, run `init_in_worktree` (skipped under
`GUARDEX_SUBMODULE_INIT=0`); record manifest in
`.omc/agent-worktrees/<slug>/.guardex/submodules.json`.
- [ ] 3.3 Extend `scripts/agent-file-locks.py`: replace bare-path
keying with `(submodule_root, relative_path)`; back-compat
read of legacy lock entries.
- [ ] 3.4 Extend `scripts/agent-branch-finish.sh`: read manifest,
loop submodules in `GUARDEX_SUBMODULE_MODE`, share one
merge-wait poller, stage atomic gitlink bump only after all
child PRs hit `MERGED`.
- [ ] 3.5 Add `gx submodules status` and `gx submodules preflight`
subcommands.
- [ ] 3.6 Update `AGENTS.md` Multi-Agent Execution Contract with a
"Submodules" subsection; update README "How it works".

## 4. Verification
- [ ] 4.1 `openspec validate --specs` → green.
- [ ] 4.2 `npm test` → green (or recorded skip with reason).
- [ ] 4.3 Live walkthrough on this repo: clean state → edit a file
in `examples/hive` → `gx branch finish --via-pr`
--wait-for-merge` → confirm child PR opens in `NagyVikt/hive`,
merges, and parent commit lists exactly the bumped gitlinks.

## 5. Cleanup
- [ ] 5.1 Commit changes on the agent branch.
- [ ] 5.2 Push branch and open a PR.
- [ ] 5.3 Run `gx branch finish ... --base main --via-pr
--wait-for-merge --cleanup`.
- [ ] 5.4 Record PR URL and `MERGED` evidence in handoff note.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Plan Workspace: agent-claude-submodule-aware-gx-2026-05-07-18-46

Durable pre-implementation planning workspace.

Use this command to update checkpoints:

```bash
/opsx:checkpoint agent-claude-submodule-aware-gx-2026-05-07-18-46 <role> <checkpoint-id> <state> <note...>
```

Roles (each has its own `tasks.md`):

- `planner/` — owns spec + open-questions
- `architect/` — owns manifest schema + failure catalog
- `critic/` — stress-tests design + executor diff
- `executor/` — implements (tests first, then code)
- `writer/` — keeps AGENTS.md, README, and context.md in sync
- `verifier/` — proves the change works against this repo before archive

See `summary.md` for the high-level intent and `open-questions.md`
for unresolved tradeoffs.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# architect tasks — submodule-aware gx

> **Role goal**: turn the spec into an implementable shape. Pick
> data structures, decide where new code lives, choose the
> dependency graph between scripts, and document the failure
> modes the executor must handle.

## Scope boundary

In scope:
- shape of `scripts/agent-submodules.py` (functions, return types,
invariants)
- shape of `.guardex/submodules.json` (manifest schema)
- shape of the new lock-record tuple in `agent-file-locks.py`
- shape of `gx submodules` subcommand routing
- failure mode catalog (what happens on each failure → which
recovery path)

Out of scope:
- writing implementation code (executor)
- writing tests (executor authors, critic reviews)

## 1. Spec
- [ ] 1.1 Enumerate every state transition for a submodule across
its lifecycle: `unknown → uninitialized → init → clean →
dirty → committed → pushed → pr-open → pr-merged →
gitlink-bumped`. Identify which step is allowed to fail
silently.
- [ ] 1.2 Author the manifest schema. Required keys: `name`,
`path`, `url`, `branch`, `state`, `was_uninitialized`,
`parent_gitlink_sha`, `child_branch`, `child_pr_url`,
`child_pr_state`, `permission`, `host`, `mode`,
`parent_bump`, `reason`.

## 2. Tests
- [ ] 2.1 Confirm the failure catalog has a corresponding negative
test in `tasks.md §2`. Add any missing entries.

## 3. Implementation
- [ ] 3.1 Choose: should `agent-submodules.py` shell out to `git`
or use `dulwich`/`pygit2`? Record reasoning. (Default:
shell out to `git` via `subprocess.run`; matches existing
script style.)
- [ ] 3.2 Choose the locking strategy when an agent edits across
parent + submodule in the same task: separate claims, or
one combined claim with a `repo` segment per file? Record
reasoning. (Default: separate claim per `(repo, path)`.)
- [ ] 3.3 Author `failure-modes.md` (sibling of this file) listing
every failure → user-facing message → recovery command.
- [ ] 3.4 Approve or reject the proposed `GUARDEX_SUBMODULE_MODE`
and `GUARDEX_SUBMODULE_INIT` env-var contract.

## 4. Checkpoints
- [ ] 4.1 Publish architect checkpoint after the manifest schema
and failure catalog land.

## Handoff fields

```
branch=agent/claude/submodule-aware-gx-2026-05-07-18-46
task=architect
blocker=<empty when done>
next=critic | executor
evidence=openspec/plan/<slug>/architect/failure-modes.md, manifest schema in proposal.md
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Plan Checkpoints: submodule-aware gitguardex

Chronological checkpoint log for all roles.

- 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.
- 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.
Loading
Loading