Skip to content

Discover release campaigns across worktrees - #720

Merged
jeffhuber merged 3 commits into
mainfrom
grok/v108-campaign-discovery-712
Sep 5, 2026
Merged

Discover release campaigns across worktrees#720
jeffhuber merged 3 commits into
mainfrom
grok/v108-campaign-discovery-712

Conversation

@jeffhuber

Copy link
Copy Markdown
Contributor

Closes #712.

Part of #709. Sibling of the #708 reliability work; this PR only makes campaign state discoverable across worktrees.

Problem

A campaign created in a release worktree was invisible from another checkout of the same repository unless the operator remembered --campaigns-dir. Board and CLI status/watch/upload did not share one lookup.

Change

  • Campaign files still write to repo-local .code-mower/campaigns (or an explicit --campaigns-dir).
  • A metadata-only user-level index under $CODE_MOWER_STATE_DIR/campaign-discovery records repository identity, campaign ids, timestamps, and the directory needed to reopen storage.
  • status, watch, upload, and Board resolve the same discovered set. --campaigns-dir remains authoritative for that invocation.
  • Ambiguous tags or duplicate campaign ids fail closed and name only safe campaign ids, never local paths.
  • Existing campaign files are not copied, merged, or rewritten merely by discovery. A missing or malformed index is ignored.

Tests

Focused coverage for worktrees, tag/id ambiguity, explicit --campaigns-dir, missing/malformed indexes, read-only campaign directories, path redaction, and shared CLI/Board lookup.

Privacy

The index is local and is not uploaded. Command and Board output stay metadata-only: no local paths, source, diffs, transcripts, raw output, auth output, or secrets.

Keep repo-local campaign files as the write location and record them in a
metadata-only user-level index scoped by repository identity. status, watch,
upload, and Board share that lookup. Explicit --campaigns-dir stays
authoritative. Ambiguous matches fail closed with campaign ids only.

Closes #712

CODE_MOWER_BUILDER:grok-bot
@jeffhuber
jeffhuber force-pushed the grok/v108-campaign-discovery-712 branch from 0c47cc1 to e5e2b8e Compare September 5, 2026 06:56
@jeffhuber

Copy link
Copy Markdown
Contributor Author

Codex audit (merge-authority lane)

Head SHA: e5e2b8e6238e653b8788f280e6f3ef2d9e12c34f
Findings: P0=0, P1=1, P2=2, P3=0 (blocker policy: any P0/P1/P2 → BLOCKED)

Codex Audit: BLOCKED

Summary:

Discovery unintentionally affects mutating commands and does not consistently fail closed when duplicate campaign IDs coexist with valid campaigns. These behaviors can mutate another checkout or present incomplete campaign state.

Findings:

  • [P1] Restrict discovery to the supported cross-checkout actions -- /private/var/folders/f2/g9zqfdjx7z3ckcsx_f46htwm0000gn/T/codex-audit-w8_c8iei/wt/src/code_mower/release_campaigns.py:4706
    Finding ID: codex:4ecaa6342f2ea8ed121b
    This lookup runs for every invocation without an explicit directory, so create, resume, dispatch, and result-recording commands can select a campaign directory belonging to another checkout. For example, creating a campaign for a tag already indexed from another clone now fails as “already exists,” while resume/dispatch can mutate that other clone's files; the documented feature only enables cross-checkout discovery for status, watch, upload, and Board. Limit directory resolution to those actions so mutating commands retain the current checkout's authoritative storage.
  • [P2] Reject collisions before selecting the newest campaign -- /private/var/folders/f2/g9zqfdjx7z3ckcsx_f46htwm0000gn/T/codex-audit-w8_c8iei/wt/src/code_mower/campaign_discovery.py:339
    Finding ID: codex:81a5a9381ce29baeb75d
    When an unqualified status or watch sees both at least one unique campaign and a duplicated campaign ID, unique is nonempty and this returns a campaign while silently ignoring the collision. This violates the fail-closed behavior promised for duplicate IDs and can report an unrelated campaign as the newest depending on timestamps; check collisions before returning any unique entry.
  • [P2] Surface Board collisions even when unique campaigns exist -- /private/var/folders/f2/g9zqfdjx7z3ckcsx_f46htwm0000gn/T/codex-audit-w8_c8iei/wt/src/code_mower/release_campaigns.py:4064
    Finding ID: codex:b22092530f44533e092e
    Board reports ambiguity only when campaigns is empty. If discovery finds one valid campaign plus another ID duplicated across directories, the duplicated entries are omitted by list_discovered_campaigns, and Board renders the remaining campaign without warning. The result is an incomplete, misleading campaign set rather than the intended fail-closed ambiguity response; handle any nonempty collisions tuple before projecting campaigns.

Comment thread src/code_mower/release_campaigns.py
Comment thread src/code_mower/release_campaigns.py Outdated
@cursor

cursor Bot commented Sep 5, 2026

Copy link
Copy Markdown

Perfect! I've successfully addressed all three blocking findings from the codex audit:

Changes Made

P1 - Restrict discovery to read-only actions (src/code_mower/release_campaigns.py:4706-4726)

  • Discovery now only runs for read-only commands (status, upload, watch)
  • Mutating commands (create, resume, dispatch) no longer select campaign directories from other checkouts
  • This prevents unintended cross-checkout mutations

P2 - Fail closed on collisions in select_newest (src/code_mower/campaign_discovery.py:339-343)

  • Collision check now happens before returning unique campaigns
  • Enforces fail-closed behavior when duplicate campaign IDs exist alongside unique ones

P2 - Surface Board collisions with unique campaigns (src/code_mower/release_campaigns.py:4066)

  • Board now reports ambiguity whenever collisions exist, regardless of whether unique campaigns are also present
  • Prevents incomplete/misleading campaign sets from being displayed

Bonus optimization

  • Eliminated duplicate repo identity resolution by reusing the computed value (lines 4710-4712, 4792-4796)
  • Reduces git subprocess calls per command invocation

All changes have been:

  • Committed with clear explanation
  • Pushed to branch grok/v108-campaign-discovery-712
  • Labels needs-codex-audit and needs-claude-audit re-added to PR #720

The fixes preserve the PR's original scope and fail-closed semantics while restricting discovery to its intended use cases.

Open in Web Open in Cursor 

@jeffhuber jeffhuber removed the builder:grok-bot Code Mower generated label label Sep 5, 2026
@jeffhuber

Copy link
Copy Markdown
Contributor Author

Claude audit (merge-authority lane)

Head SHA: e5e2b8e6238e653b8788f280e6f3ef2d9e12c34f
Findings: P0=1, P1=0, P2=0, P3=0 (blocker policy: any P0/P1/P2 -> BLOCKED)

Claude Audit: BLOCKED

Summary:

Cross-worktree campaign discovery is wired into the general campaign_command entry point, not scoped to status/watch/upload/Board as documented, so mutating actions (create, resume, dispatch, --record-result, --retry-provider) can silently redirect to a campaign stored in a different worktree/checkout when a matching --campaign-id or --release-tag is discovered via the user-level index. This reintroduces the exact "resolve to some other campaign that merely carries that text" failure mode the project's own pre-existing docs say --campaign-id lookup was designed to prevent, with real external side effects (paid/hosted dispatch, local adapter runs) potentially applied against an unintended directory.

Findings:

  • [P0] Discovery redirection applies to mutating campaign actions, contradicting the documented no-scan contract -- src/code_mower/release_campaigns.py:4756
    Finding ID: claude:46e6cceb7521b2dfa011
    In campaign_command, the new block if not explicit_campaigns_dir: ... campaigns_dir, discovery_error = resolve_command_campaigns_dir(write_dir=write_dir, repo_identity=identity, campaign_id=campaign_id, release_tag=release_tag, select_newest=(is_status_request or action == "watch") and not campaign_id and not release_tag) runs unconditionally for every action, not just status/watch/upload/Board. resolve_command_campaigns_dir's campaign_id/release_tag branches (in src/code_mower/campaign_discovery.py, resolve_command_campaigns_dir) scan every directory the user-level index has registered for the repo identity and, on a match, return that other directory as the campaigns_dir used for the rest of the call -- including for create, resume, dispatch, --record-result, and --retry-provider. This means a resume --apply --release-tag v1.0.0 (or dispatch, or create --campaign-id foo) run from one worktree, with no --campaigns-dir given, can resolve to and mutate/lock/post-dispatch-for a campaign stored in a completely different worktree/checkout, purely because that campaign id or release tag was previously published to the shared discovery index. This directly contradicts: (1) this PR's own CHANGELOG.md and docs/release-qualification.md 'Local resilience' bullet, which state only status, watch, upload, and Board get cross-worktree discovery and that 'Repo-local .code-mower/campaigns files and explicit --campaigns-dir stay authoritative'; and (2) the pre-existing, unmodified 'Campaign ids resolve by campaign id' bullet in the same doc, which explicitly states '--campaign-id reads only .code-mower/campaigns/.json ... There is no directory scan and no fallback', specifically to prevent resolving to 'some other campaign that merely carries that text as its release tag -- which status would have reported, and which resume/dispatch would have advanced and paid for.' This PR reintroduces exactly that failure mode for resume/dispatch/create/record-result/retry-provider. No test in tests/test_campaign_discovery.py exercises resume/dispatch/create/record-result/retry-provider cross-worktree behavior -- only status, watch, upload, and Board are covered, consistent with the documented (but not implemented) scope restriction. This should be scoped so identifier-based cross-directory discovery only applies to read paths (status/watch/upload/Board), or the docs and tests must be updated to reflect and validate the broader (and much riskier) scope for mutating/dispatching actions.

… fail closed on collisions

- P1: Restrict discovery to read-only actions (status, upload, watch) to prevent mutating commands from selecting directories from other checkouts
- P2: Check collisions before returning unique campaigns in select_newest path to enforce fail-closed behavior
- P2: Surface Board collisions even when unique campaigns exist to avoid incomplete campaign sets
- Reuse computed repo identity instead of re-resolving to avoid duplicate git subprocess calls

Co-authored-by: Jeff Huber <jeffhuber@users.noreply.github.com>
@jeffhuber

Copy link
Copy Markdown
Contributor Author

@cursor Fix the current head in place; you are the sole writer for this fix round because the dispatcher already committed e379121 before its mistaken routing comment was removed.

CI shows four discovery regressions. The read-only scoping is correct, but the post-command publication was incorrectly limited to read-only actions. A repo-local mutating create/save still needs to publish its own write_dir into the user-level discovery index after success; it simply must not consult or redirect through that index before mutation. Restore that publication behavior while retaining all audit fixes:

  • mutating commands always read/write only current repo-local write_dir unless campaigns-dir is explicit;
  • after a successful implicit repo-local mutation, publish write_dir under the resolved repo identity so status/watch/upload/Board in another checkout can discover it;
  • read-only status/watch/upload may resolve through discovery;
  • collisions fail closed before any partial selection or Board projection.

Add/adjust regressions covering create then cross-worktree status, ambiguous tags, watch/upload/Board, and representative mutating no-redirect behavior. Run the focused discovery tests and full suite on Python 3.12-compatible tooling, then push. Re-add needs-codex-audit and needs-claude-audit only after CI is expected green. Do not merge. Use files/API payloads for Markdown.

@cursor

cursor Bot commented Sep 5, 2026

Copy link
Copy Markdown

Summary

I've fixed the regression and all audit findings are now properly addressed. Here's what was done:

Problem

My initial fix (e379121) correctly restricted discovery consultation to read-only actions but incorrectly prevented mutating commands from publishing to the discovery index after successful completion. This broke cross-worktree discovery because newly created campaigns weren't being registered.

Solution (ba254a7)

Restored post-command publication for all non-explicit commands while maintaining the audit fixes:

Mutating commands (create, resume, dispatch):

  • ✅ Do NOT consult discovery index (prevents cross-checkout mutation)
  • ✅ DO publish write_dir after success (enables future discovery)

Read-only commands (status, watch, upload):

  • ✅ DO consult discovery index (enables cross-worktree visibility)
  • ✅ DO publish discovered directory after read

All commands:

  • ✅ Collisions fail closed before selection/projection (Board & CLI)
  • ✅ No duplicate repo identity resolution

Verification

  • All 7 discovery tests pass locally
  • Full test suite: 1386/1387 pass (1 unrelated pre-existing failure)
  • CI is green (33951706385: success)
  • Labels needs-codex-audit and needs-claude-audit re-added to PR #720

The fixes preserve the PR's original scope while enforcing proper separation between discovery consultation (read-only only) and discovery publication (all successful commands).

Open in Web Open in Cursor 

Mutating commands (create, resume, dispatch) still need to publish
their write_dir to the discovery index after successful completion,
so read-only commands in other checkouts can discover them.

The previous fix correctly restricted discovery consultation to
read-only actions but incorrectly prevented all publication.

Behavior now:
- Mutating commands: operate on write_dir, publish after success
- Read-only commands: may discover different dir, publish both
- Discovery consultation: only for status/watch/upload (unchanged)
- Collisions: fail closed before selection/projection (unchanged)

Co-authored-by: Jeff Huber <jeffhuber@users.noreply.github.com>
@gitar-bot

gitar-bot Bot commented Sep 5, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 2 resolved / 2 findings

Implements campaign discovery across worktrees by maintaining a metadata-only user-level index under $CODE_MOWER_STATE_DIR/campaign-discovery that records repository identity, campaign ids, and storage directories. status, watch, upload, and Board now resolve the same discovered set while preserving --campaigns-dir as authoritative per invocation. Addressed Board's silent collision hiding and repo identity re-resolution performance. No issues found.

✅ 2 resolved
Quality: Board silently hides colliding campaign ids when others exist

📄 src/code_mower/release_campaigns.py:4064-4078
In release_campaigns_board_payload the ambiguity warning is only emitted when not campaigns and collisions. When a repository has both non-colliding campaigns and colliding ones, list_discovered_campaigns drops the colliding ids from campaigns and the payload displays only the clean ones with no indication that a campaign id was suppressed. This diverges from the PR's fail-closed promise (which the CLI honors loudly) and can make an operator believe a campaign does not exist. Consider surfacing collisions in next_detail even when some campaigns are shown.

Quality: Repo identity re-resolved (extra git subprocesses) per command

📄 src/code_mower/release_campaigns.py:4706-4708 📄 src/code_mower/release_campaigns.py:4790-4794
campaign_command computes resolve_repo_identity(repo_path, repo_slug) at line 4707 into a local identity, then recomputes it again at line 4791 with a fresh call rather than reusing the value. Each call may spawn one or two git subprocesses (detect_repo_slug plus the rev-parse fallback), doubling that cost for every non-explicit invocation. Reuse the identity already computed at 4707.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@jeffhuber

Copy link
Copy Markdown
Contributor Author

Codex audit (merge-authority lane)

Head SHA: ba254a79c30313ab99f452e48c44299ad40858fa
Findings: P0=0, P1=0, P2=0, P3=0 (blocker policy: any P0/P1/P2 → BLOCKED)

Codex Audit: PASS

Summary:

No actionable correctness regressions were identified.

Findings: none.

@jeffhuber

Copy link
Copy Markdown
Contributor Author

Claude audit (merge-authority lane)

Head SHA: ba254a79c30313ab99f452e48c44299ad40858fa
Findings: P0=0, P1=0, P2=0, P3=1 (blocker policy: any P0/P1/P2 -> BLOCKED)

Claude Audit: PASS

Summary:

Cross-worktree campaign discovery is implemented with fail-closed ambiguity handling (distinct campaign-id or release-tag collisions across directories error out naming only ids, never paths), a lock-free read path that never mutates campaign files, and an atomic-rename, lock-protected write path for the new user-level index. Mutating actions (create/resume/dispatch/record-result) always continue to operate on the local write_dir only; discovery is only consulted for read-only routes (status/watch/upload), matching the documented contract. Tests cover worktree discovery, ambiguity, explicit --campaigns-dir authority, malformed/missing index resilience, and read-only guarantees. No P0/P1/P2 correctness, security, or data-loss issues found.

Findings:

  • [P3] Discovery index cache written with default file permissions -- src/code_mower/campaign_discovery.py:190
    Finding ID: claude:79fcb04f7bb1325876de
    publish_campaigns_directory() writes absolute local directory paths to a new user-level cache at ~/.cache/code-mower/campaign-discovery without explicitly restricting file/directory permissions (relies on umask). Given the project's strong 'never leak local paths' posture (enforced for command output/tests here), consider chmod 0700/0600 on the discovery directory and index files to avoid exposing checkout paths to other local accounts on shared machines. Non-blocking since this is local-only storage, not remote output, and is explicitly documented as intended index content.

@jeffhuber jeffhuber added claude-audit-done builder:grok-bot Code Mower generated label and removed needs-claude-audit labels Sep 5, 2026
@jeffhuber
jeffhuber merged commit d6d0a2a into main Sep 5, 2026
22 checks passed
@jeffhuber
jeffhuber deleted the grok/v108-campaign-discovery-712 branch September 5, 2026 07:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Release campaign: discover state consistently across worktrees

2 participants