Skip to content

Conductor hardening: close six reported defects and twelve found auditing them (GH #6) - #7

Merged
bpmforge merged 8 commits into
mainfrom
fix/conductor-hardening-v3.10.0
Sep 8, 2026
Merged

bpmforge merged 8 commits into
mainfrom
fix/conductor-hardening-v3.10.0

Conversation

@bpmforge

@bpmforge bpmforge commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Closes #6.

All six reported items, plus twelve defects found while auditing them. Eight
commits, each independently landable; the runtime-repair feature is isolated in
its own commit because it is the largest behavioural change here.

The two findings that outrank most of the report

Both affect any deployed version, independently of whether this branch merges.

1. A chatty model self-approved. APPROVED_RE and RUNTIME_PASS_RE were
unanchored .test(body) calls, and the reviewer/runtime prompts contain
"VERDICT: APPROVED" and "RUNTIME: PASS" as instructions. Any model that
restated its instructions was read as approving. Reproduced against the real
regexes: a document ending VERDICT: CHANGES REQUESTED read as APPROVED.
readVerdict() now takes the last line-anchored verdict, treats a missing one
as blocking, and fails closed when a line names both outcomes.

2. Post-approval code landed unreviewed. Round 3 runs as the coder agent
with the worktree writable; "do not edit implementation files" is an
instruction, not a gate. An edit inside write_scope passed the scope re-check
and was folded into the closed commit by an unguarded amend — violating the
report's own stated invariant that modified code always invalidates prior
review. Any non-document change after the last approving review now invalidates
it.

Reported items

# Resolution
Reviewer fix escapes write_scope (comment 2) Scope gate runs on the dirty repair tree before git add
Linked-worktree lock ENOTDIR (comment 1) --git-common-dir, resolved against ROOT (it returns a relative path from the repo root; resolving against cwd passes tests and misplaces the lock in the field). Acquired with 'wx' so two conductors cannot both win the check-then-write race
1 — findings don't cross attempts reviewFailureFeedback(): non-approved only, bounded per-document and overall, (missing) never inferred as approval, fenced in BEGIN/END UNTRUSTED markers
2 — no bounded runtime repair Built, default 1. Re-earns every gate: no-op check → scope → reviewer recompute → fresh review → fresh runtime
3 — --max-tickets bounds successes --max-processed added, charged on the claim; landed/processed/resumed/stopReason in conductor.end
4 — timeout descendants Process-group containment with a liveness proof; fails closed; Windows performs zero same-worktree retries

One deliberate deviation, documented in the commit and README: the report
asks that the failed runtime report be committed separately so a no-op repair
cannot masquerade as a source change. The candidate must land as a single
commit, so the no-op check instead asks which non-document files the repair
touched — documents are excluded by construction. Same guarantee, no extra
commit.

Found while auditing

  • Scope-gate containment bypass. matches_scope() enforced "a bare */**
    is refused outright" with a blacklist of four literal strings. **/*,
    */**, */*, ?*/** and [a-z]*/** each authorised every nested path in
    the repo. Now the first path segment must be a literal name.
  • Renames checked on one side. R src/auth/important.ts -> a/moved.ts
    passed a write_scope of a/** while having deleted a file outside it.
  • close() — the load-bearing gate — was the one unbounded loop. No
    timeout (an unattended run hangs forever on a watch-mode runner), and a 1MB
    maxBuffer that makes execSync throw when a command exiting zero
    simply prints a lot — refusing a green ticket and reporting "did not exit 0",
    which was untrue: there was no exit code at all.
  • Provider-limit exhaustion crashed the run. The throw escaped
    runSession, executeTicket and main(), leaving the ticket claimed and
    released by nobody. And STOP was read only between tickets, so it was
    ignored for the duration of a backoff that doubles to 60m.
  • The two retry budgets shared one counter, so a timeout retry spent a
    rate-limit retry (item 4 asks that they be separate and both bounded).
  • Reviewer selection under-recruited twice, in a module whose header says it
    is "biased toward firing": deletions were invisible (git renders them
    +++ /dev/null, so deleting an auth file recruited nobody), and an
    unroutable declared reviewer vanished silently.
  • "Evidence outranks the claim" ran one way only. A claimed PASS quoting its
    own non-zero exit was trusted while the equivalent FAIL got a deterministic
    re-run. classifyRuntimeVerdict() had implemented the rule since P-A9 and no
    gate path ever called it.
  • A failed merge crashed mid-merge, leaving MERGE_HEAD set — so the next
    run refused to start on a dirty tree the conductor itself created.
  • Scope evidence could dirty the target; runVerifyDirect had no maxBuffer;
    a typo'd numeric flag became NaN and exited reporting landed=0,
    indistinguishable from an empty board.

Test harness

validate-scope.match.test.sh has existed since v3.8.0, is cited in
RELEASE_TRACKER.md as proof the matcher is "fixture-backed", and nothing ran
it
— while carrying its own copy of matches_scope, so it could only fail
when the copy changed. A sweep found six more orphaned suites (45 tests),
including the JIRA board driver's entire unit, parity and integration coverage.
All green as found, so nothing was broken; they were unprotected — the state
conductor.test.mjs was in while it stayed red across three releases, which is
the history that caused Pass 53 to exist.

Pass 53 now discovers every *.test.mjs under scripts/, with two vacuity
guards. A list has to be updated by whoever adds a suite and fails silently when
they don't.

Evidence

  • Pass 53: 9 suites, 93 tests (was 3 suites, 19 tests before this branch).
  • Full suite: 724 passed, 0 failed, run twice to check for flakes under the
    new concurrency; working tree clean after.
  • The review → bounded-fix → runtime path had no end-to-end coverage before
    this branch — every prior conductor fixture runs --rounds 1. The new
    conductor.hardening.test.mjs drives the real 3-round loop with a stateful
    stub.
  • Every new negative control was verified red against its prior commit — the
    one exception being the STOP-during-backoff case, which against unfixed code
    sleeps out the full 5-minute backoff and hits the harness timeout.

Not included: no version bump or CHANGELOG entry. CHANGELOG.md stops at 3.5.4
while package.json says 3.9.0, so recent releases clearly go through
docs/RELEASE_TRACKER.md as a separate deliberate act — that call is the
maintainer's.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JDSdRmtXCgkHtZ9XVgLXoZ

bpmforge and others added 8 commits September 8, 2026 12:38
 + audit)

Wave 1 of the hardening reported in #6. Three defects that all
fail in the same direction — a gate that says green when it has not checked.

1. Reviewer fixes could escape write_scope (#6 comment 2, security boundary).
   runFixLoop() staged and amended the repair, THEN the attempt's scope check
   ran against `git status --porcelain` — empty on a committed tree, so the
   violation was invisible. A reviewer-triggered repair had strictly more write
   authority than the maker session whose identical violation is caught. The
   gate now runs on the dirty repair tree before `git add`; a violation is an
   ordinary red attempt, the diff is preserved, and it never reaches runtime.

2. A chatty model self-approved (found while auditing; not in the report).
   APPROVED_RE and RUNTIME_PASS_RE were unanchored .test(body) calls, and the
   reviewer/runtime prompts contain "VERDICT: APPROVED" and "RUNTIME: PASS" as
   instructions. A model that restated its instructions was read as approving
   regardless of the verdict it reached: reproduced, a document ending
   "VERDICT: CHANGES REQUESTED" read as APPROVED. readVerdict() now takes the
   LAST line-anchored verdict, treats a missing verdict as blocking, and fails
   closed on a line naming both outcomes. classifyRuntimeVerdict() likewise.

3. Post-approval code changes landed unreviewed (found while auditing).
   Round 3 runs as the coder agent with the worktree writable; "do not edit
   implementation files" is an instruction, not a gate. An edit inside
   write_scope passed the scope re-check and was folded into the closed commit
   by an unguarded amend. Any non-document change after the last approving
   review now invalidates that approval and fails the attempt.

Also: the run lock is placed via `git rev-parse --git-common-dir` (#6 comment
1) — `<root>/.git` is a FILE in a linked worktree, so the old path died ENOTDIR
before any gate ran. Resolved against ROOT, not cwd, and acquired with 'wx' so
two conductors starting at once cannot both win the check-then-write race.

Tests: new scripts/conductor/conductor.hardening.test.mjs — negative controls
that pass silently when the defect is present. Verified they fail against
HEAD aaea3c7 (3 fail) and pass on this commit (5 pass). Existing conductor +
resume suites: 19 passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDSdRmtXCgkHtZ9XVgLXoZ
…dings (GH #6)

Wave 2 of #6 — items 1, 3 and 4.

1. --max-processed (item 3). --max-tickets bounds SUCCESSES: `landed` only
   advances on a verified success, so on a board with a low success rate
   `--max-tickets 1` could claim, run and fail an unbounded number of tickets.
   A success target is not a work budget. --max-processed is charged on the
   CLAIM, whatever the outcome. Startup orphans are reported as `resumed` and
   do not consume it — they were claimed by a previous invocation, and
   charging them would make the flag mean two things depending on how the last
   run died. conductor.end now reports landed/processed/resumed/stopReason.

   Every numeric flag now goes through intOpt(). `Number(opt(...))` failed
   silently in two directions: a typo became NaN, so `while (landed < NaN)`
   was false on the first evaluation and the run exited reporting landed=0 —
   indistinguishable from an empty board — and a bare flag became `true`,
   which Number()s to 1.

2. Timeout descendants (item 4). spawnSync's timeout signals the direct child
   only; a session is opencode plus whatever it shells out to. Those
   descendants outlived the timeout and a same-worktree retry raced them.
   Sessions now spawn into their own process group and a timeout kills the
   GROUP — SIGTERM, bounded grace, SIGKILL, then a liveness check. Retry is
   permitted only once the group is observably empty. Containment fails
   closed: no pid, a group surviving SIGKILL, or a platform without group
   semantics all refuse the retry and return 124. Windows retries zero times.

   Timeouts are now classified from both shapes Node reports them in. The
   `res.error` return sat ABOVE the signal check, so ETIMEDOUT was reported as
   a generic session failure — the same timeout, two outcomes, by platform.

   Containment lives in scripts/lib/session-containment.mjs because
   conductor.mjs calls main() at import, so nothing in it is unit-testable —
   the same reason runtime-verdict.mjs and attempt-outcome.mjs exist.

3. Findings cross attempt boundaries (item 1). A red fix loop returned only
   reviewer NAMES, so the next fresh attempt was told who objected and never
   what was wrong. reviewFailureFeedback() carries the terminal findings:
   non-approved verdicts only, bounded per-document and overall with
   truncation announced, `(missing)` for an unreadable review (never inferred
   as approval), and fenced in BEGIN/END UNTRUSTED markers — a review quotes
   the source it reviewed, so it can carry instruction-shaped text into the
   prompt of the agent about to rewrite that source.

Also wires conductor.hardening.test.mjs into Pass 53 and corrects the stale
"not wired into scripts/test.ts" headers (untrue since v3.1.2 — R-15).

Tests: 11 hardening cases, all verified to fail without their fix.
Pass 53 now runs 30 conductor tests (was 19). Full suite: 724 passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDSdRmtXCgkHtZ9XVgLXoZ
None of these are in the report. All four are error paths that fail in the
direction of looking fine.

1. A failed merge crashed the run mid-merge. land()'s `git merge` was a bare
   call and `sh` throws on non-zero git exit, so the throw escaped land() and
   main(); main().catch logged conductor.fatal and exited 1, leaving the
   target's main branch with MERGE_HEAD set and the merge staged. The NEXT run
   then refused to start on "working tree not clean" — blaming a dirty tree the
   conductor itself created — and the ticket was stranded in in_review having
   been accept()ed in memory only. The merge is now an outcome: abort, log,
   comment, push the BRANCH (main never got the work, so the branch is the only
   copy), return false. The in-memory accept() is discarded by re-loading the
   board rather than persisted — "Done" for work that never reached main is a
   lie the board would tell every future reader.

2. Scope-violation evidence could dirty the target. preserveAttemptEvidence()
   wrote a self-ignoring .gitignore into the evidence directory;
   captureScopeEvidence() wrote into the same directory and did not. So whether
   a scope violation poisoned the next run depended on whether some earlier
   ticket had happened to preserve attempt evidence first. Both callers now go
   through ensureEvidenceDir().

3. runVerifyDirect() had no maxBuffer. The default is 1MB; a verbose test suite
   overflows it, spawnSync kills the process, and `status` comes back null —
   which the function reported as a failure. Its one job is to overturn an
   unsubstantiated runtime FAIL by re-running the ticket's own verify, so an
   overflow made it uphold the FAIL it exists to check. Now 256MB (as
   runBaselinePreflight already used), and a run that produces no exit code is
   reported `inconclusive` rather than as a failing verify — a harness problem
   must not be attributed to the candidate's code.

4. pushRemotes() could not push a branch under --merge. Mode-derived ref only;
   the new conflict path needs the branch explicitly. Added `forceRef`.

Tests: the merge-conflict path is reproduced deterministically with a
pre-merge-commit hook that refuses — same shape as a content conflict (non-zero
exit, MERGE_HEAD set, merge staged). Verified it fails against the previous
commit with "a failed merge must be an outcome, not a fatal crash".

Conductor suites: 31 passed, 0 failed. Full suite: 724 passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDSdRmtXCgkHtZ9XVgLXoZ
… item 2)

A deterministic runtime failure used to discard a candidate that had already
passed scope and independent review, restarting the next attempt from main —
throwing away the coding AND the review effort over what is frequently one
mechanical mistake. --runtime-fix-iterations (default 1) allows a bounded
repair instead.

The repair is NOT a shortcut past the gates. It is a new candidate that
re-earns all of them:

  runtime FAIL -> repair -> no-op check -> scope -> reviewer RECOMPUTE
               -> fresh review -> bounded review-fix -> fresh runtime
               -> scope -> close

Three rules make that safe, each because its absence launders unreviewed code
into a closed ticket:

  1. Any code change invalidates ALL prior approval — the re-review is not
     limited to reviewers who objected before.
  2. Reviewers are RECOMPUTED from the post-repair main...branch diff. A repair
     can touch a newly security-sensitive path, and the reviewer set that never
     saw that path is the wrong one.
  3. Freshness is PROVEN. runReviewRound() now archives and removes each review
     document before its session, so an exit-zero reviewer that writes nothing
     reads as "no review" instead of silently re-using its earlier APPROVED.
     (Archived first: the round that TRIGGERED a repair would otherwise be gone
     from the evidence by the time the attempt failed.)

A no-op repair is still a failure; an out-of-scope repair is rejected with its
diff preserved; the repair path never calls close, accept, merge or release.

DELIBERATE DEVIATION from the reported spec: it asks that the failed runtime
report be committed separately so a no-op repair cannot look like a source
change. The candidate is meant to land as a single commit, so the no-op check
instead asks which NON-DOCUMENT files the repair touched — documents are
excluded by construction. Same guarantee, no extra commit.

Also fixes an asymmetry the repair loop exposed: "evidence outranks the claim"
applied to FAIL only. A claimed PASS was taken at face value even when the same
document quoted a non-zero exit and a failing test — the round scrutinised a
pessimistic model and trusted an optimistic one. runtime-verdict.mjs had stated
the rule ("prose never overrides exit codes") and implemented it in
classifyRuntimeVerdict() since P-A9, but no gate path ever called it: a rule
written and left unwired. close() would still have caught the bad candidate,
but only after the repair budget was spent and after the receipts recorded PASS
for work that does not build.

Tests: 7 acceptance cases on a stateful fixture (the same role is invoked
repeatedly and must behave differently each time) — happy path asserted on
event ORDER not final status, stale-approval reuse, no-op repair, out-of-scope
repair, --runtime-fix-iterations 0 compatibility, repair bound respected, and
the self-contradicting PASS. All five new negative controls verified to fail
against the previous commit.

Pass 53 now runs 38 conductor tests. Full suite: 724 passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDSdRmtXCgkHtZ9XVgLXoZ
… failures

close() is documented as "the load-bearing verb" — it runs the ticket's own
verify command from outside the session and refuses to advance on non-zero.
It ran that command with execSync's DEFAULTS, and both are wrong here.

1. NO TIMEOUT. An unattended run hung forever on a verify that hangs — a
   watch-mode test runner, a command that prompts, a wedged container. Every
   other loop in this executor is bounded (sessions, provider retries, fix
   iterations, attempts, and now tickets processed); the one that was not
   bounded was the gate itself. Now 30 minutes by default, overridable per
   project via conductor.config.json `verifyTimeoutMs` and threaded through
   both the conductor and the resume path.

2. 1MB maxBuffer. execSync THROWS ENOBUFS when a command that exits ZERO
   simply prints a lot, and that throw landed in the same catch as a real
   failure — so a fully green ticket was refused. Verified directly: a command
   printing 2MB and exiting 0 throws ENOBUFS. Now 256MB, matching what
   conductor.mjs already uses for the same command shape (and the same defect
   fixed in runVerifyDirect one commit ago — same root cause, two call sites).

3. All three outcomes reported "verify gate did not exit 0", and for the two
   above that sentence is false: there was no exit code at all. An operator
   reading it goes looking for a failing test that does not exist. Timeout and
   buffer-overflow now say what actually happened and that the ticket is NOT
   verified; a real non-zero exit still reports exactly as before.

Checked and NOT a defect, recorded so it is not re-litigated: the JIRA board
driver cannot supply `verify` or `write_scope`. Both come from a checked-in
TICKET_SCOPE_MAP precisely so board content never becomes a command — the
deliberate counterpart to the session env allowlist.

Tests: three library-level cases. The two negative controls fail against the
previous commit — the hang one by running the full 60s test timeout, which is
the defect stated as plainly as it can be.

Conductor suites: 41 passed, 0 failed. Full suite: 724 passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDSdRmtXCgkHtZ9XVgLXoZ
…nd two silent review-gate gaps

Continuing the audit past the reported issue. Four more, all of the same
family: something that should stop, block, or recruit, silently didn't.

1. Exhausting the provider-limit retries CRASHED THE RUN. runSession ended
   with `throw new Error('limit retries exhausted')`, and the throw escaped
   runSession, executeTicket AND main() — main().catch logged conductor.fatal
   and exited 1 with the ticket still claimed and owned, released by nobody. It
   now returns a non-zero code, which routes into the existing
   blockWithoutExhausting() path: released with a reason, evidence preserved,
   and a provider outage does not consume the feature's coding attempts.

2. STOP was ignored for hours. The provider backoff doubles 5m -> 60m and can
   total well over two hours across its retries, and STOP was only ever read
   BETWEEN tickets — so an operator who touched STOP during a rate-limit pause
   was ignored for the rest of the backoff. STOP is the only stop mechanism
   this executor has. The backoff now polls it every 5s and abandons the
   session when it appears.

3. The two retry budgets shared one counter. GH #6 item 4 asks that
   --session-timeout-retries be kept separate from the provider rate-limit
   retries and both bounded; the flag shipped last commit but both still
   consumed the same `attempt` loop, so a timeout retry silently spent a
   rate-limit retry. Now separate counters, with --session-limit-retries
   (default 5, preserving the historical 6-iteration loop) and
   --limit-backoff-minutes (default 5).

4. Reviewer selection under-recruited, twice — in a module whose own header
   says it is "biased toward firing: a false negative ships unreviewed auth".
   - DELETIONS WERE INVISIBLE. It read only `+++ b/<path>`, and git renders a
     deleted file as `+++ /dev/null` with the real path on the `--- a/` line.
     So deleting src/auth/session.ts recruited nobody but code-reviewer, while
     adding one line to it recruited security. Removing an auth check is not
     the lower-risk change. Both header sides are now read, /dev/null filtered.
   - AN UNROUTABLE DECLARED REVIEWER VANISHED. A board declaring
     `reviews: ["securty"]` asked for a security review, got none, and nothing
     anywhere recorded it — the run looked like a normally reviewed ticket.
     triggeredReviewers() now returns `dropped`, and the conductor logs it with
     the routable names. Deliberately NOT fatal: the routable set is small and
     a board may name reviewers a different executor implements. Making it
     refuse the ticket instead is a reasonable stricter call, left open.

Tests: 6 new cases. The review-trigger and provider-limit-exhaustion controls
were verified red against the previous commit. The STOP-during-backoff control
could not be run against the unfixed code to completion — it sleeps out the
full 5-minute backoff and hits the test harness timeout, which is the defect
demonstrated about as plainly as it can be.

Pass 53 now runs 45 conductor tests. Full suite: 724 passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDSdRmtXCgkHtZ9XVgLXoZ
… repo

Three findings in the scope gate — the containment boundary the whole executor
rests on.

1. CONTAINMENT BYPASS. matches_scope() enforced "a bare '*' / '**' is refused
   outright (would authorise the whole repo)" — its own stated intent — with a
   blacklist of four LITERAL strings. Every other spelling walked through.
   Measured against the real function: '**/*', '*/**', '*/*', '?*/**' and
   '[a-z]*/**' each matched "src/auth/session.ts", i.e. authorised every nested
   path in the repository, while only the two exact strings were refused. A
   blacklist of spellings cannot express "names no path".

   Replaced with the intent: the FIRST path segment must be a literal name.
   'a/**' and 'src/features/x/*.test.ts' stay anchored and allowed; '**/*' and
   friends are refused, as is '*.ts' — which spans every tree in the repo,
   the exact thing a write_scope exists to prevent. Verified end to end against
   a real git repo: the old script ALLOWED an out-of-scope write under '**/*';
   the new one refuses it, and every previously-passing pattern still passes.

2. RENAMES WERE CHECKED ON ONE SIDE. The enumerator printed only a rename's
   destination, so `R src/auth/important.ts -> a/moved.ts` passed a write_scope
   of 'a/**' while having DELETED a file outside it. The destination being in
   scope says nothing about the source. Both sides are now checked. Reachable
   only if the session stages the rename itself — which it is told never to do,
   and which is precisely what a gate must not depend on.

   Also: paths are read with core.quotePath=false and surrounding quotes
   stripped, so a correctly-scoped file no longer fails the gate because its
   name is non-ASCII.

3. THE MATCHER'S TEST RAN NOWHERE, AND TESTED A COPY.
   validate-scope.match.test.sh has existed since v3.8.0, is referenced only in
   docs/RELEASE_TRACKER.md as proof the matcher is "fixture-backed", and no
   harness has ever executed it. It also carried its own copy-pasted
   matches_scope, so it could not fail when the product changed — only when the
   copy did; the two had already diverged. The matcher now lives once, in
   _scope-match.sh, sourced by both; the fixture suite gained the six
   unanchored-pattern cases; and Pass 53 runs it, refusing a zero-assertion run
   as a pass exactly as it does for node --test.

   This is scripts/test-conductor-suite.ts's own lesson — "a test that no
   harness executes is not a test; it is a file that resembles one" — recurring
   one directory over, in the file that guards the containment boundary.

Matcher fixtures: 21 passed, 0 failed (was 15, all still passing).
Pass 53 now runs 48 conductor tests. Full suite: 724 passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDSdRmtXCgkHtZ9XVgLXoZ
A sweep for test files no harness executes, prompted by finding
validate-scope.match.test.sh in that state last commit. Six more:

  scripts/lib/jira-tickets.test.mjs              (6 tests)
  scripts/lib/jira-tickets-parity.test.mjs       (4 tests)
  scripts/lib/jira-tickets-integration.test.mjs  (1 test)
  scripts/lib/tests/annotate.test.mjs            (11 tests)
  scripts/lib/tests/img-gate.test.mjs            (16 tests)
  scripts/log-hop.test.mjs                       (7 tests)

45 tests, referenced by nothing but themselves. The JIRA three are the whole
board driver's coverage — a 342-line module that the conductor can be pointed
at with CONDUCTOR_BOARD=jira — with no harness behind it.

All six are GREEN as found, so nothing was broken. They were unprotected, which
is the state conductor.test.mjs was in while it stayed RED across v3.1.0,
v3.1.1 and earlier — the exact history that caused Pass 53 to be written.

The fix is not a longer list. Pass 53 now DISCOVERS every *.test.mjs under
scripts/, because a list has to be updated by whoever adds a suite and fails
silently when they don't, whereas discovery cannot be forgotten. Two vacuity
guards: zero discovered files fails the Pass (discovery broken), and zero
passing tests already failed it (an empty match must never read as green).

Pass 53: 9 suites, 93 tests (was 3 suites, 48 tests). Full suite: 724 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDSdRmtXCgkHtZ9XVgLXoZ
@bpmforge
bpmforge merged commit a8b575f into main Sep 8, 2026
1 check failed
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.

Conductor: bounded self-healing loses retry context and can race timeout descendants

1 participant