Skip to content

Commit 1fdff10

Browse files
NagyViktNagyViktclaude
authored
docs(openspec): tick verification boxes (validate passed, scope clean) (#563)
* docs(openspec): seed gx 7-gap roadmap (Q2 2026) Captures the 2026-05-11 conversation about gx gaps into a reviewable OpenSpec change with seven independently-pickable gap analyses: 01 interactive recovery verb (gx recover) [T2, P1] 02 structured observability (events log, --json) [T2, P2] 03 stranded-lane filter (--stranded) [T1, P1] 04 conflict resolution verb (gx resolve) [T2, P3] 05 cross-process lock enforcement [T3, P4 / deferred] 06 per-remote trust policy [T1, P2] 07 src/cli/main.js refactor [T3, P5 / deferred] Docs-only; no src/ or scripts/ changes. Each gap doc is shaped to drop straight into a future change's proposal.md without rewrite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(openspec): tick verification boxes (validate passed, scope clean) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: NagyVikt <nagy.viktordp@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bfaa053 commit 1fdff10

12 files changed

Lines changed: 569 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-05-11
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Gap 01 — Interactive recovery verb (`gx recover`)
2+
3+
## Problem
4+
5+
When an agent lane stalls, the current answer is `scripts/agent-autofinish-watch.sh --auto-merge`, which after 15 minutes of file silence commits whatever is in the worktree, pushes, and tries to merge. That is a daemon for the *common* case (idle worktree, clean intent). It is the wrong tool when the lane is actually broken: dirty submodule, missing lock claim, unmerged PR check failures, raced base-branch push, or a half-applied edit the agent meant to revert.
6+
7+
There is no command that lets a human (or a fresh agent) ask: "this branch is stuck — tell me *why*, and propose the narrow fix."
8+
9+
## Evidence in current code
10+
11+
- `scripts/agent-autofinish-watch.sh` (commit-then-push pipeline, idle-driven).
12+
- `bin/agent-stalled-report.sh` (wired as `SessionStart` hook — emits one-line summary per stalled branch).
13+
- `src/agents/status.js` already builds rich `Session` records via `buildAgentsStatusPayload(repoRoot)` including `worktreeExists`, `lockCount`, `claimedFiles`, `changedFiles`, `prUrl`, `prState`.
14+
- Attention inbox at session start: 3 codex lanes stalled at 9 m, 14 m, 33 m old — recurring real-world signal.
15+
16+
## Proposed CLI surface
17+
18+
```bash
19+
gx recover <branch> # diagnostic mode: print causes, no actions
20+
gx recover <branch> --apply # take the safest single recommended action
21+
gx recover <branch> --dry-run # equivalent to bare invocation (default)
22+
gx recover --all # diagnose every stranded branch
23+
gx recover <branch> --json # machine-readable for cockpit / dashboard
24+
```
25+
26+
Diagnostic output buckets the lane into one of:
27+
28+
- `clean-idle` → recommend `gx branch finish ... --via-pr --wait-for-merge --cleanup`.
29+
- `dirty-uncommitted` → recommend `git -C <wt> commit -am "wip recover"` then finish.
30+
- `unclaimed-files` → list the unclaimed paths, recommend `gx locks claim ...`.
31+
- `submodule-pointer-drift` → recommend `gx submodule advance` inside the worktree.
32+
- `pr-open-checks-red` → link the PR + failing check name.
33+
- `worktree-missing` → recommend `git worktree prune` + branch deletion if no commits exist.
34+
- `unknown` → dump the raw evidence and STOP (do not auto-apply).
35+
36+
## Tier / effort
37+
38+
- **Tier**: T2.
39+
- **Effort**: ~6 files / ~1 day. New `src/recover/index.js` + dispatch entry in `src/cli/main.js` + arg parsing in `src/cli/args.js` + tests + manifest entry. Reuses `agents/status.js`, `git/index.js`, and `submoduleModule` primitives.
40+
41+
## Dependencies
42+
43+
None. Ships independently. Pairs well with Gap 03 (stranded filter) but does not depend on it.
44+
45+
## Open questions
46+
47+
- Should `gx recover --apply` run the finish pipeline directly, or print the exact command and let the user/agent paste? Lean **print**, since the recovery verb is meant to be diagnostic-first.
48+
- Should it touch other agents' lanes (`--all`)? Probably yes, but with `--apply` refusing to act on lanes whose lock owner is not the current user/agent.
49+
- Where do we surface unmerged-PR check failures? Likely via `gh pr checks` shell-out, gated on `gh` presence.
50+
51+
## Acceptance criteria
52+
53+
- [ ] `gx recover <branch>` exits 0 with a human-readable diagnosis for every state in the bucket list.
54+
- [ ] `gx recover <branch> --json` returns `{ branch, state, evidence: {...}, recommended: { command: "...", reason: "..." } }`.
55+
- [ ] `gx recover --all` iterates every `agent/*` branch (current-user-owned) and prints one diagnosis per lane.
56+
- [ ] `--apply` performs the recommended action only for the `clean-idle`, `dirty-uncommitted`, and `unclaimed-files` states; refuses on the rest with an explanation.
57+
- [ ] Regression test covers each bucket against a fixture worktree.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Gap 02 — Structured observability
2+
3+
## Problem
4+
5+
`gx` has no first-class machine-readable surface. `gx agents --json` does not work as a flat flag (`gx agents --json` returns `Unknown agents subcommand: --json` on the current `main` branch). There is no append-only event log of lane lifecycle events (`branch-start`, `lock-claim`, `commit`, `push`, `pr-open`, `merge`, `cleanup`, `stall-detected`). External tools — the Colony planner UI, cockpit, dashboards, scripts that want to watch CI — fall back to parsing human-readable text or scanning `.omc/agent-worktrees/` directly.
6+
7+
The cost shows up two places: planner cards do not refresh on real state changes, and stalled-lane recovery (Gap 01) has to re-derive history every time instead of reading an append-only stream.
8+
9+
## Evidence in current code
10+
11+
- `src/agents/status.js:86 buildAgentsStatusPayload()` already returns a structured `{ schemaVersion: 1, repoRoot, sessions: [...] }` payload — the data exists, just not exposed at the top-level CLI.
12+
- `src/agents/status.js:110 renderAgentsStatus(payload, { json: true })` returns the JSON string; nothing calls it from `gx agents` directly.
13+
- `src/cli/main.js:2692 function agents(rawArgs)` dispatches subcommands only.
14+
- Repeated hand-grepping of `.omc/agent-worktrees/*/manifest.json` in `scripts/agent-autofinish-watch.sh` and `bin/agent-stalled-report.sh`.
15+
16+
## Proposed CLI surface
17+
18+
```bash
19+
gx agents --json # flat surface; same payload as today's subcommand
20+
gx status --json # repo-level health (delegates to scan + agents)
21+
gx events tail [--since=15m] [--branch=...] # stream of NDJSON events
22+
gx events log [--last=100] [--branch=...] # bounded historical query
23+
```
24+
25+
Event log shape (NDJSON, one event per line, append-only file at `.omc/events.ndjson`):
26+
27+
```json
28+
{"ts":"2026-05-11T16:08:00Z","kind":"branch-start","branch":"agent/claude/...","agent":"claude-opus","tier":"T2","worktree":".omc/agent-worktrees/..."}
29+
{"ts":"2026-05-11T16:09:14Z","kind":"lock-claim","branch":"agent/claude/...","files":["proposal.md","tasks.md"]}
30+
{"ts":"2026-05-11T16:30:02Z","kind":"pr-open","branch":"agent/claude/...","prUrl":"https://github.com/recodeee/gitguardex/pull/NNN"}
31+
{"ts":"2026-05-11T16:35:11Z","kind":"merge","branch":"agent/claude/...","sha":"abc1234"}
32+
```
33+
34+
Writer is the `gx branch start/finish`, `gx locks claim`, and the finish pipeline. Events file is gitignored by default and rotated by size in a follow-up.
35+
36+
## Tier / effort
37+
38+
- **Tier**: T2.
39+
- **Effort**: ~10 files / ~1.5 days. New `src/events/index.js` (append/tail/log helpers) + integration calls from `start.js`, `finish/index.js`, `locks` writers + flat `--json` dispatch on `gx agents` and `gx status` + tests.
40+
41+
## Dependencies
42+
43+
None to ship. Unblocks Gap 04 (`gx resolve` reads the event log to detect repeated collisions) and Gap 05 (lock enforcement layer needs an audit trail anyway).
44+
45+
## Open questions
46+
47+
- Append-only file vs. SQLite? Lean **NDJSON**: zero deps, easy to tail in bash, easy to rotate.
48+
- Per-repo only, or also a user-level `~/.gx/events.ndjson` aggregation? Start per-repo; user-level is a follow-up.
49+
- Schema versioning: every event MUST include `schemaVersion: 1`. Bumps require an entry in `roadmap.md`.
50+
51+
## Acceptance criteria
52+
53+
- [ ] `gx agents --json` (flat) returns the same payload as the legacy subcommand, exits 0.
54+
- [ ] `gx status --json` returns `{ schemaVersion, repoRoot, doctor: {...}, agents: {...} }`.
55+
- [ ] `gx events tail` streams new events as they are written; `--since=15m` filters.
56+
- [ ] `gx events log --last=100` returns the most recent N events in newest-first order.
57+
- [ ] All write paths (`branch start`, `branch finish`, `locks claim/release`, `cleanup`) emit events.
58+
- [ ] `.gitignore` updated to exclude `.omc/events.ndjson` and any rotated `.omc/events-*.ndjson`.
59+
- [ ] Regression test: spawn a fixture branch, run the full lifecycle, assert event sequence.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Gap 03 — Stranded-lane inventory (`gx agents --stranded`)
2+
3+
## Problem
4+
5+
The data needed to list "lanes that have not made progress in N minutes" already exists in `src/agents/status.js`. What is missing is a filter and an explicit "stranded" classification. Today, to find them, you either read the attention inbox (which is Colony state, not gx state), or you eyeball `git worktree list` and infer from commit ages.
6+
7+
Concrete present-tense pain: this session's attention inbox shows 3 stranded codex lanes (9 m, 14 m, 33 m old). The Claude session-start hook (`scripts/agent-stalled-report.sh`) surfaces them but does not let you query, sort, or feed them to another tool.
8+
9+
## Evidence in current code
10+
11+
- `src/agents/status.js:86 buildAgentsStatusPayload()` already returns per-session `activity`, `worktreeExists`, `changedFiles`, `lockCount`. None of these are exposed as filter knobs.
12+
- `scripts/agent-autofinish-watch.sh` has its own ad-hoc "stranded" definition based on file-mtime silence — duplicated logic.
13+
- `bin/agent-stalled-report.sh` is a one-shot wrapper that does not accept filters.
14+
- `gx agents` (no args) prints "Agent sessions: none" in the primary checkout; the data is per-worktree but the listing is not.
15+
16+
## Proposed CLI surface
17+
18+
```bash
19+
gx agents # existing behavior, unchanged
20+
gx agents --stranded # only lanes whose last activity > 15m ago
21+
gx agents --stranded --age=30m # custom threshold
22+
gx agents --stranded --json # machine-readable for cockpit / planner
23+
gx agents --owned-by claude-opus # filter by agent (orthogonal to --stranded)
24+
gx agents --tier T2 # filter by tier
25+
gx agents --no-pr # lanes with no PR URL yet
26+
```
27+
28+
The "stranded" definition codifies what `agent-autofinish-watch.sh` already does:
29+
30+
- last mtime of any file inside the worktree > `--age` (default 15 m), AND
31+
- no merged PR, AND
32+
- `worktreeExists` is true.
33+
34+
## Tier / effort
35+
36+
- **Tier**: T1 (≤ 5 files, single capability, no API/schema change).
37+
- **Effort**: ~3 files / ~half day. New filter helpers in `src/agents/status.js` + new `--stranded` / `--age` / `--owned-by` / `--tier` / `--no-pr` flag parsing in `src/cli/args.js` (or wherever `agents` parses its rest) + tests.
38+
39+
## Dependencies
40+
41+
None. Independently shippable. Pairs naturally with Gap 01 (`gx recover --all` would internally call this filter).
42+
43+
## Open questions
44+
45+
- Default `--age` threshold: 15 m (matches `agent-autofinish-watch.sh --idle-seconds=900`) or shorter (5 m) for tighter feedback? Lean **15 m** to match existing semantics.
46+
- Should `--stranded` exit non-zero when one or more lanes are stranded? Lean **yes**, so it composes into CI / shell pipelines (`gx agents --stranded || gx recover --all`).
47+
48+
## Acceptance criteria
49+
50+
- [ ] `gx agents --stranded` lists only lanes meeting the stranded criterion; empty output and exit 0 when none.
51+
- [ ] `gx agents --stranded --json` returns the filtered payload with the same schema as the unfiltered `--json`.
52+
- [ ] `--age=<duration>` accepts `Ns`, `Nm`, `Nh` and rejects invalid input with a clear error.
53+
- [ ] `--owned-by`, `--tier`, `--no-pr` compose with `--stranded` via logical AND.
54+
- [ ] `--stranded` exits non-zero (e.g. exit 2) when at least one stranded lane is found, so it works in shell guards.
55+
- [ ] Regression test fixtures: zero lanes, one stranded lane, one active lane, mixed.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Gap 04 — Conflict resolution verb (`gx resolve`)
2+
3+
## Problem
4+
5+
When two agent lanes both touch `.gitmodules`, a lockfile (`pnpm-lock.yaml`, `package-lock.json`, `Cargo.lock`), or a generated artifact (built CSS, generated OpenAPI client, schema dumps), the lanes collide at merge time. gx has no primitive for this. Agents drop to raw `git merge`, `git rebase --strategy-option=ours`, or `git checkout --theirs` — each of which can quietly destroy work in a sparse-checkout agent worktree.
6+
7+
The pattern is recurring and well-known internally: an existing `submodule-pointer-conflict-resolver` agent worktree has been sitting open with no commits for hours (visible in `git worktree list`), because the conflict-handling story is ambiguous enough that the agent declined to act.
8+
9+
## Evidence in current code
10+
11+
- `git worktree list` includes `agent/codex/...submodule-pointer-conflict-resolver` lanes with zero commits.
12+
- `src/submodule/index.js` has `advance()` for forward-only pointer bumps but no merge-strategy support.
13+
- `src/finish/index.js:241 finish()` calls `branchFinish` asset; no pre-merge conflict-resolution hook.
14+
- Memory 5001: "submodule-pointer-conflict-resolver Worktree Is Freshly Created with No New Commits Yet" — observed pattern.
15+
16+
## Proposed CLI surface
17+
18+
```bash
19+
gx resolve <path...> # inspect, print plan, no actions
20+
gx resolve <path...> --strategy=<name> # apply strategy
21+
gx resolve --auto # scan whole worktree, pick strategy per path
22+
```
23+
24+
Strategies (each path-class has one default):
25+
26+
- `--strategy=submodule-tip` → for `.gitmodules` collisions: take the newer remote tip of every submodule. Refuses if either submodule has uncommitted work.
27+
- `--strategy=lockfile-regen` → for lockfiles: delete, re-run the matching package manager (`pnpm install`, `npm install`, `cargo update`), commit the result.
28+
- `--strategy=generated-rebuild` → for declared generated artifacts: delete, run the registered rebuild command (from `package.json` `scripts.<key>` or `.gx/resolve.json`), commit.
29+
- `--strategy=ours` / `--strategy=theirs` → escape hatches; warn loudly.
30+
31+
`.gx/resolve.json` (new, optional) declares per-path strategies so `--auto` knows what to do without a flag:
32+
33+
```json
34+
{
35+
"rules": [
36+
{ "path": "pnpm-lock.yaml", "strategy": "lockfile-regen", "command": "pnpm install --frozen-lockfile=false" },
37+
{ "path": ".gitmodules", "strategy": "submodule-tip" },
38+
{ "path": "apps/docs/openapi.json", "strategy": "generated-rebuild", "command": "pnpm --filter docs gen:openapi" }
39+
]
40+
}
41+
```
42+
43+
## Tier / effort
44+
45+
- **Tier**: T2.
46+
- **Effort**: ~8 files / ~2 days. New `src/resolve/index.js` + dispatch entry + `.gx/resolve.json` reader + per-strategy implementations + tests + docs entry in the relevant capability context.
47+
48+
## Dependencies
49+
50+
- **Soft on Gap 02** (Structured observability): `gx resolve` should emit `resolve-applied` events so repeat collisions are visible in `gx events log`. Ship without it if Gap 02 is not yet ready; backfill events later.
51+
- Pre-commit hook needs an allowlist so `--strategy=lockfile-regen` commits do not trip "unclaimed files" guard mid-resolve.
52+
53+
## Open questions
54+
55+
- Should `gx resolve` operate only inside the agent worktree, or also on the primary checkout during a finish-time merge conflict? Lean **worktree-only**; primary stays read-only.
56+
- Where does `gx resolve --auto` get its rule set when `.gx/resolve.json` is absent — built-in defaults only, or refuse and require explicit `--strategy`? Lean **built-in defaults for the three named path classes, refuse otherwise**.
57+
58+
## Acceptance criteria
59+
60+
- [ ] `gx resolve <path>` prints the chosen strategy and the exact commands it would run, exits 0, makes no changes.
61+
- [ ] `gx resolve <path> --strategy=submodule-tip` updates `.gitmodules` pointers to the latest remote tip and commits when no submodule is dirty; refuses with a clear error otherwise.
62+
- [ ] `gx resolve <path> --strategy=lockfile-regen` regenerates the lockfile via the registered command and commits the result.
63+
- [ ] `gx resolve --auto` reads `.gx/resolve.json` (or built-in defaults) and resolves every collision in one pass.
64+
- [ ] `--ours` / `--theirs` print a loud warning and still execute.
65+
- [ ] Regression tests cover each strategy against a fixture worktree pair with synthesized collisions.

0 commit comments

Comments
 (0)