Skip to content

Fix #71: resync the .agent mirror issue layer; add a parity guard - #74

Open
laird wants to merge 2 commits into
integrationfrom
feature/issue-71
Open

Fix #71: resync the .agent mirror issue layer; add a parity guard#74
laird wants to merge 2 commits into
integrationfrom
feature/issue-71

Conversation

@laird

@laird laird commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Closes #71

Problem

CLAUDE.md declares mirror parity CRITICAL, but nothing enforced it, and the issue layer under .agent/scripts/ drifted badly from plugins/autocoder/scripts/.

The drift was not cosmetic. .agent/workflows/fix.md calls three verbs its own issue-fns.sh never defined:

verb called at
issue_any_claimable fix.md:320
issue_claim fix.md:532, fix.md:648
issue_release fix.md:575, fix.md:681, fix.md:1479

The preflight at fix.md:319-323 branches on issue_any_claimable's exit status. An undefined function exits 127, which falls to the *) arm — "Backend error while checking for claimable issues"; exit 1.

So the Antigravity /fix was not running in a degraded mode. It terminated at its first check on every single invocation.

Two further breaks

  • --state blocked was forwarded straight to gh issue list, which rejects anything outside {open,closed,all}. The mirror's stale issues-file.py rejected it too. Both list-needs-feedback.md and approve-proposal.md depend on that state.
  • The inline _ifns_gh_list applied no blocking-label filter at all, so list --state open did not mean "claimable" as the 9-verb contract requires. (It never had issues-gh.sh list uses invalid no:label search syntax — autonomous loop sees zero issues #57's no:label bug because it had no filter whatsoever.)

Extent

file before
issues-gh.sh absent entirely
issues-file.py 438 differing lines; last touched 2026-05-19; no claim/release/any-claimable; 3 bucket refs vs 30
issue-fns.sh 180 differing lines (inline impl vs dispatcher)
issue-config.sh 4 differing lines

Solution

Sync all four shared files forward from the maintained plugins copy — which already implements the full contract the mirror's own workflows assume. Verified nothing outside issue-fns.sh referenced the removed inline internals (_ifns_gh_*, _ifns_file), so there are no orphaned callers.

Guard against recurrence

New tests/test_agent_mirror_parity.sh asserts:

  1. the mirror ships every shared file,
  2. each is byte-identical across mirrors,
  3. all 9 contract verbs resolve on both sides,
  4. --state blocked returns JSON rather than a gh usage error.

It runs in a throwaway git repo with a poisoned gh on PATH, so it cannot touch the real tracker — same pattern as test_issue_fns.sh after #58.

Verification

$ bash tests/test_agent_mirror_parity.sh
Results: 13 passed, 0 failed      # before the fix: 6 passed, 7 failed

Replaying the fix.md preflight against the mirror:

populated queue: preflight: work exists -> proceeds
empty queue:     preflight: no claimable work -> clean exit 0

Previously both landed on the backend-error branch with rc=127.

Full suite 14/14 files green; bash -n across plugins, .agent, and tests clean; py_compile clean on both issues-file.py copies.

Out of scope, noted

.agent/workflows/fix.md.backup and fix-loop.md.backup are stale editor backups committed to git.

🤖 Generated with Claude Code

CLAUDE.md declares mirror parity CRITICAL, but nothing enforced it and
the issue layer under .agent/scripts/ drifted badly from
plugins/autocoder/scripts/.

The drift was not cosmetic. .agent/workflows/fix.md calls three verbs
its own issue-fns.sh never defined:

  issue_any_claimable   fix.md:320
  issue_claim           fix.md:532, 648
  issue_release         fix.md:575, 681, 1479

The preflight at fix.md:319-323 branches on the exit status of
issue_any_claimable. An undefined function exits 127, which falls to the
`*)` arm — "Backend error while checking for claimable issues"; exit 1.
So the Antigravity /fix did not run in a degraded mode; it terminated at
its first check on every single invocation.

Two further breaks:

- `--state blocked` was forwarded straight to `gh issue list`, which
  rejects anything outside {open,closed,all}. The mirror's stale
  issues-file.py rejected it too. Both list-needs-feedback.md and
  approve-proposal.md depend on that state.
- The mirror's inline _ifns_gh_list applied no blocking-label filter at
  all, so `list --state open` did not mean "claimable" as the 9-verb
  contract requires. (It never had #57's `no:label` bug because it had
  no filter whatsoever.)

Extent of the drift: issues-gh.sh absent entirely; issues-file.py 438
differing lines and last touched 2026-05-19, lacking claim/release/
any-claimable and the bucket layout (3 bucket references vs 30);
issue-fns.sh 180 differing lines; issue-config.sh 4.

Fixed by syncing all four shared files forward from the maintained
plugins copy — which already implements the full contract the mirror's
own workflows assume. Nothing outside issue-fns.sh referenced the
removed inline internals, so there are no orphaned callers.

Added tests/test_agent_mirror_parity.sh so the drift cannot silently
recur: it asserts the mirror ships every shared file, that each is
byte-identical, that all 9 verbs resolve on both sides, and that
`--state blocked` returns JSON rather than a gh usage error. It runs in
a throwaway git repo with a poisoned gh on PATH, so it cannot touch the
real tracker.

Verified: parity test 13/13 (was 6 passed, 7 failed). Replaying the
fix.md preflight against the mirror now yields "work exists -> proceeds"
with a populated queue and "no claimable work -> clean exit 0" when
empty, instead of the backend-error branch. Full suite 14/14 files
green; shell syntax and python compile clean.

Out of scope, still present: .agent/workflows/fix.md.backup and
fix-loop.md.backup are stale editor backups committed to git.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@laird

laird commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

⚠️ Base branch check — this PR targets master, not the integration branch

Flagging before merge; not retargeting it, since master and feat/autocoder-planning-pipeline have diverged (#30 records 11 conflicts), so changing the base here would produce a conflicted diff rather than the intended 5-file change.

Per CLAUDE.md, the integration branch — and the ship branch that defines "shipped" for the #35 close gate — is feat/autocoder-planning-pipeline. This branch is based on master and does not contain it:

$ git merge-base --is-ancestor origin/feat/... origin/feature/issue-71   -> does NOT contain integration

$ bash plugins/autocoder/scripts/verify-shipped.sh $(git rev-parse origin/feature/issue-71)
NOT_SHIPPED: c714eb9 has not reached origin/feat/autocoder-planning-pipeline
PARKED_ON:origin/feature/issue-71

Merging as-is lands the work on master only, so the ship gate keeps returning NOT_SHIPPED and #71 can never be closed by the protocol. The swarm, which works on the integration line, also would not receive the fixed .agent/ mirror scripts.

Root cause and the two resolution options are written up in #76 — it's the stale installed autocoder plugin (3.10.0) still shipping the pre-rename /fix, which bases branches on whatever HEAD the worktree happens to sit on. Not a reflection on this change's contents.

@laird

laird commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

Correction — retracting my previous comment. This PR's base is right.

I was wrong. master is the repo default branch and all 12 recent merged PRs target master — that is the swarm's actual convention, and this PR follows it.

My error: I ran verify-shipped.sh from a worktree sitting on feat/autocoder-planning-pipeline. That script resolves the ship branch from the local worktree's CLAUDE.md (explicit arg > CLAUDE_CODE_SHIP_BRANCH > CLAUDE.md > repo default), and the integration branch's CLAUDE.md carries a **Ship branch**: feat/autocoder-planning-pipeline line. master's CLAUDE.md has no such line, so a master-based worktree resolves to the repo default — master. I reported a worktree-local answer as if it were universal.

Concretely: once this merges to master, it is shipped by the gate as any master-based worker resolves it. Nothing here blocks #71 from closing.

Sorry for the noise on your PR — no action needed from you. I've retitled #76 to the real (and much smaller) underlying issue: the ship-branch designation is worktree-dependent, and the integration branch's designation goes stale the moment #77 lands.

laird added a commit that referenced this pull request Jul 29, 2026
The combined tree (#77 + #74 + #78) exposed a defect neither PR's CI could
see. #77's parser recognises only the Jest "Tests: N passed" form, and #77
alone never exercises it — the unit suite always skipped, so the parser was
dead code. #78 makes the suite actually run, and this repo's own shell tests
print "Results: 13 passed, 0 failed" with no "Tests:" prefix. A fully green
run was therefore reported "no parseable test summary" and exited 1.

That is a false RED — the mirror image of the false GREEN #73 is about, and
just as damaging: a gate that cannot be trusted in either direction.

The fix (in the merge commit) adds a bare "<N> passed" fallback, mirroring
the two-tier approach #77's own E2E block already uses. These assertions pin
both directions so neither can regress:

  - a non-Jest "Results: N passed, M failed" summary is accepted (exit 0,
    not reported as unparseable)
  - output with no counts at all is STILL an error — the fallback widens the
    accepted formats without defeating #77's guard against a runner that
    never produced a summary

Verification on the combined tree:
  - tests/test_regression_test_config.sh: 16/16
  - all tests/test_*.sh: 15/15
  - pytest: 124/124
  - detect_skip_reason present 3x (definition + Unit Tests + E2E guards)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@laird

laird commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

This PR went CONFLICTING when #83 landed — here is a verified resolution

It was MERGEABLE/CLEAN earlier today; #83 (Jira + Azure DevOps backends) landed on master and now it is CONFLICTING/DIRTY. Reproduced locally against current master (a4cc38b).

Exactly one file conflicts: .agent/scripts/issue-fns.sh — one hunk, but a substantive one rather than a textual clash:

side shape
master large inline dispatcher — every verb a bash function, case $ISSUE_SOURCE covering github/file/jira/ado
this PR thin delegating dispatcher — _ifns_BACKEND_BIN + all 9 verbs incl. issue_claim/issue_release/issue_any_claimable

Neither side is correct alone: this PR's version drops the jira/ado backends master just gained, and master's version lacks the three verbs this PR exists to add.

The resolution is simpler than it looks

plugins/autocoder/scripts/issue-fns.sh auto-merged cleanly into exactly the right shape — thin dispatcher plus jira/ado:

  github) _ifns_BACKEND_SCRIPT="issues-gh.sh"   ;;
  file)   _ifns_BACKEND_SCRIPT="issues-file.py" ;;
  jira)   _ifns_BACKEND_SCRIPT="issues-jira.sh" ;;
  ado)    _ifns_BACKEND_SCRIPT="issues-ado.sh"  ;;

So resolve .agent/scripts/issue-fns.sh by taking the plugins-side file verbatim. The mirrors must be byte-identical anyway — your own test_agent_mirror_parity.sh enforces it.

This works because all four backends already speak the same 9-verb CLI. Verified on master:

issues-jira.sh:  any-claimable claim close comment create get list release update
issues-ado.sh:   any-claimable claim close comment create get list release update
issues-gh.sh:    (added by this PR) same 9

Verified, not assumed

I first hand-wrote the two jira)/ado) lines into .agent and got 17 passed, 1 failedtest_agent_mirror_parity.sh: "issue-fns.sh diverged (4 differing lines)". The divergence was whitespace alignment only. Your parity test caught it, which is a good sign for the test. Copying the plugins file verbatim instead:

mirrors identical ✅
shell:  18 passed, 0 failed
pytest: 59 passed

Not applied

I have not pushed this to your branch — it is yours, and I have already learned this session what happens when I push to a PR I do not own. The change is one cp; happy to apply it if you would rather I did.

For reference, master itself is currently healthy: 17/17 shell, 59/59 pytest, BUILD_OK.

laird added a commit that referenced this pull request Jul 29, 2026
Resolves .agent/scripts/issue-fns.sh to the canonical plugins version:
the thin 9-verb dispatcher WITH the jira/ado cases. Recorded as a real
two-parent merge so master's change is in the ancestry and the later
merge to master is clean.
@laird
laird changed the base branch from master to integration July 29, 2026 17:29
@laird

laird commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Retargeted masterintegration per #89's branch model.

Still CONFLICTING, and the base change does not fix it — I checked rather than assuming: feature/issue-71 conflicts with integration on the same single file it conflicted with against master, .agent/scripts/issue-fns.sh. So #87 is still required either way.

Once #87 merges into feature/issue-71, this goes clean — verified the full chain against current integration: #87feature/issue-71integration is 0 conflicts.

🤖 Autonomous fix workflow

@laird

laird commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

This PR is the root of a four-deep stack and is currently blocking two green PRs.

integration
  └── #74  feature/issue-71        ← CONFLICTING (here)
        └── #87  merge/issue-71-master     MERGEABLE
              └── #92  feature/issue-88    MERGEABLE

#87 and #92 both report MERGEABLE, but neither can land while this base is red. Merging #87 into feature/issue-71 clears the conflict — verified: #87feature/issue-71integration is 0 conflicts.

Suggested order: #87#92#74 → integration.

Related: #93 records that neither master nor integration is branch-protected, so nothing structurally prevents bypassing a stuck stack like this one.

🤖 Autonomous fix workflow

…er (jira/ado) (#87)

* Add Jira issue backend parallel with GitHub (#81)

Adds a first-class `jira` issue source alongside `file` and `github`,
implementing the same uniform 9-verb backend contract (list, get, update,
comment, close, create, claim, release, any-claimable) against the Jira
REST API v2.

- issues-jira.sh: self-contained backend. Maps Jira issues (PROJ-N) to the
  gh-compatible JSON shape, exposing the numeric key suffix as `number` so it
  stays compatible with the swarm manifest and the rest of the workflow.
  Uses API v2 for plain-text descriptions. Non-secret connection settings
  (baseUrl, project) come from the `jira` object in .autocoder.json;
  credentials are env-only (JIRA_EMAIL + JIRA_API_TOKEN, or JIRA_AUTH_HEADER
  for Server/DC PATs) and never committed.
- Claimable/open JQL ORs in `labels is EMPTY` so label-less issues are not
  silently dropped — the Jira analogue of the GitHub `no:label` bug (#57).
- Registers `jira` in both dispatchers (plugins + .agent mirror),
  issue-source-lib.sh validation, and the set-issue-source command.
- test_issues_jira.sh: 35 assertions covering JQL, request shaping, output
  schema, and exit codes, with curl fully stubbed (no network).
- Updates CLAUDE.md/GEMINI.md, launcher help strings, and bumps autocoder
  4.5.2 -> 4.6.0 and the marketplace 3.26.0 -> 3.27.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zJKDkbj54vZeGaTWLJEHv

* Add hermetic Jira integration test with a stateful fake server

The unit test (test_issues_jira.sh) stubs curl and asserts request/JQL
shape; this adds real end-to-end coverage without any network:

- tests/fixtures/fake_jira.py: a small stdlib-only STATEFUL fake of the
  Jira REST API v2 covering exactly the endpoints issues-jira.sh calls
  (search, issue CRUD, comment, transitions, assignee, myself). It keeps
  issues in memory and evaluates the JQL the backend emits, so state
  filters and the lifecycle behave like real Jira. Binds an ephemeral
  loopback port (printed as "LISTENING <port>") so CI runs never collide.
- tests/test_issues_jira_integration.sh: starts the fake, exercises the
  full lifecycle (create → get → claim/release → comment → update → close)
  over real HTTP and asserts every state filter — including the label-less
  "labels is EMPTY" guard a stateless mock cannot exercise — then tears the
  server down. Skips cleanly if curl/python3 are absent. 17 assertions.
- plugins/autocoder/scripts/jira-smoke-test.sh: opt-in check against a REAL
  Jira instance (JIRA_* env). Kept out of tests/ so CI never needs egress.
- CLAUDE.md: document the three test layers.

Full shell suite green (15/15); server teardown leaves no stray process.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zJKDkbj54vZeGaTWLJEHv

* Add docs/jira-setup.md — Jira backend setup & testing guide

Walks through creating a free Jira Cloud site, project, and API token;
wiring the non-secret baseUrl/project into .autocoder.json while keeping
credentials in the environment; verifying with jira-smoke-test.sh; and the
two hermetic CI tests. No secrets in the doc. Linked from CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zJKDkbj54vZeGaTWLJEHv

* Add Azure DevOps issue backend parallel with GitHub/Jira (#82)

Adds a first-class `ado` issue source implementing the same uniform 9-verb
backend contract against the Azure DevOps Work Item Tracking REST API.

- issues-ado.sh: self-contained backend. Work item IDs are integers, so
  `number` maps to System.Id directly. Labels ↔ work-item Tags
  (System.Tags), state ↔ System.State (done-state set covers the default
  Agile/Basic/Scrum/CMMI processes). Uses WIQL for list/any-claimable,
  workitemsbatch for details, and json-patch for create/update. Non-secret
  orgUrl/project come from the `ado` object in .autocoder.json; the PAT is
  env-only (ADO_PAT) and never committed. Overridable work-item type and
  open/closed state names via env.
- WIQL `[System.Tags] NOT CONTAINS 'x'` already matches tag-less items, so
  untagged work stays claimable without a special empty clause (the ADO
  analogue of the GitHub no:label / Jira empty-labels traps) — asserted by
  both tests.
- Registers `ado` in both dispatchers (plugins + .agent mirror),
  issue-source-lib validation, the set-issue-source command, and launcher
  help strings.
- Tests (both hermetic, in CI): test_issues_ado.sh (28 assertions, stubs
  curl; WIQL + json-patch shape, schema, exit codes) and
  test_issues_ado_integration.sh (18 assertions; real HTTP against a stateful
  fake, tests/fixtures/fake_ado.py, full lifecycle + WIQL filtering).
- Docs: docs/ado-setup.md; updates CLAUDE.md/GEMINI.md. Bumps autocoder
  4.6.0 -> 4.7.0 and the marketplace 3.27.0 -> 3.28.0.

Full shell suite green (17/17).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zJKDkbj54vZeGaTWLJEHv

* Document all issue backends (file, GitHub, Jira, Azure DevOps)

- docs/issue-backends.md: unified overview of the four built-in backends —
  the 9-verb contract, uniform JSON shape and exit codes, per-backend model
  mapping, secrets handling, configuration precedence, and the hermetic
  test layers. Links to the Jira and ADO setup guides.
- README: refresh the "Issue Backends" section — full 9-verb contract table
  (adds claim/release/any-claimable), point at the new docs, and use a
  genuinely custom name (linear) for the custom-backend example now that
  jira/ado are first-class.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zJKDkbj54vZeGaTWLJEHv

* Complete the .agent issue-layer mirror and widen the parity guard (#71)

Two gaps found by a dry-run of the full merge chain.

1. issue-source-lib.sh existed only under plugins/autocoder/scripts/.
   It is issue-source resolution used by the swarm launch/lifecycle
   scripts — shared issue-layer infrastructure, not platform-specific —
   so the mirror was incomplete. Copied verbatim.

2. The parity guard's SHARED list covered only the two original backends
   (file, gh). issues-jira.sh and issues-ado.sh arrived with #83 and were
   never checked, and neither was issue-source-lib.sh. A backend that
   ships only under plugins/ is invisible to the guard — which is exactly
   how the drift in #71 went unnoticed: the mirror looked healthy because
   nothing checked the files missing from it. SHARED now lists the whole
   issue layer (7 files).

Verification: parity guard 19/19, up from 13/13. Negative-tested both new
assertion classes — removing issue-source-lib.sh from the mirror fails 2
assertions, and perturbing issues-jira.sh fails the divergence check — so
the added coverage is not vacuous. 18/18 suites pass, build clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.agent mirror issue layer is stale: workflows call issue_claim/issue_release/issue_any_claimable which do not exist

1 participant