Skip to content

Complete the reqdrive roadmap: L0 → L3, all Tier 2 items - #1

Merged
adbarc92 merged 47 commits into
mainfrom
feat/roadmap-completion
Jul 25, 2026
Merged

Complete the reqdrive roadmap: L0 → L3, all Tier 2 items#1
adbarc92 merged 47 commits into
mainfrom
feat/roadmap-completion

Conversation

@adbarc92

Copy link
Copy Markdown
Owner

Takes reqdrive from readiness rung L0 to L3 and completes every remaining CLAUDE.md Tier 2 item. 35 tasks across 9 phases, each landing behind a frozen, tamper-evident test oracle; Tier 3 is explicitly deferred with recorded reasons.

Why this was L0, not the "L2, docs-only gap" the survey assumed

Three findings measured at the start, each verified in-tree:

  • The test suite structurally could not report a failure. set -e plus the bare-subshell + test_result "..." $? shape meant a failing assertion killed the script before test_result ran — so "0 failed" was true by construction and red-first TDD was impossible. Fixed in P0, mutation-proven.
  • The draft-PR gate fail-opened three ways (null verification, missing prd.json, passes-omitted stories) — a PR could present as ready-to-merge with no evidence. Inverted to fail-closed in P4.
  • The prompt heredoc leaked stray backslashes into the commit message the agent is instructed to use. Fixed in P6a.

Phases

Phase What Rung
P0 Repair the harness so failures are reportable (mutation-proven)
P1 Behavior-spec stories for all 158 original assertions L0
P2 Freeze the suite: whole-file-hash tamper gate, all rules proven to fire L1
P3 Pipeline test harness (fake claude/gh) + honest bats (zero-skip)
P4 Fail-closed draft gate L2
P5 Three doc-coverage gates + launch-lifecycle automation L3
P6 Safe heredoc rewrite (byte-identical → forgery guard → escaping fix) + reqdrive verify
P7 Policy cluster: risk tiers (prefix, not glob) + warn/block scope check

Verification

  • tests/oracle-gate.sh202/202, suite exit 0. The freeze hashes simple-test.sh, oracle-gate.sh, pipeline-harness.sh, and spec-map.sh; any change to the test infrastructure is NEEDS_HUMAN.
  • bats e2e green with zero skips (CI-enforced).
  • shellcheck + bash -n clean across bin/, lib/, and the test scripts.
  • Every behavior change was red-first (observed failing) or characterization-locked (refactors), then re-verified against the frozen gate.

The gate caught a real regression mid-effort (a concurrency-guard test that had been passing vacuously), and the final adversarial review closed a genuine freeze-surface hole — both are documented in tests/FINDINGS.md.

Deferred (recorded in CLAUDE.md Decision Log)

Tier 3 (each a separate effort): vision QA (Node/Python + Playwright), worktree orchestrate, PR-rejection feedback, CI polling, cost tracking, adaptive retries. Also: config-load-time schema validation, and making the review agent a genuine writer≠grader.

Design trail

docs/superpowers/specs/2026-07-23-reqdrive-roadmap-completion-design.md (spec, 3 adversarial critique rounds) and docs/superpowers/plans/2026-07-23-reqdrive-roadmap-completion.md (the 35-task plan). Readiness correction owed to the external WORKFLOW.md survey is recorded in docs/STATUS.md.

🤖 Generated with Claude Code

adbarc92 added 30 commits July 23, 2026 09:15
Covers the L2->L3 readiness gap and all remaining Tier 2 items,
with Tier 3 deferred and reasons recorded.

Three key findings from measuring the current state:

- tests/simple-test.sh sets `set -e` and every assertion is a bare
  subshell, so test_result's FAIL branch is unreachable. "0 failed"
  is guaranteed by construction and red-first TDD is impossible
  today. Fixing this is P0, and the obvious fix is wrong: bash
  suppresses errexit inside a subshell used as an `if` condition,
  even when the body sets `set -e` itself.

- The draft-PR gate fail-opens three ways, not one: null
  verification, missing prd.json, and stories with `passes` omitted.
  The gate is inverted to fail-closed.

- The prompt builder's unquoted heredoc leaves stray backslashes in
  the commit message the agent is instructed to use.

Passed three rounds of adversarial critique (38 findings); the log
is in the document.
35 bite-sized tasks across 9 phases, each ending in an independently
testable deliverable with a commit.

Ordering is forced by the foundation: the test harness cannot report a
failure today, so P0 repairs it (and proves the repair by mutation,
because assertion inversion cannot tell the correct fix from the
broken one). Only then does the spec retrofit, the freeze, and any
behavior change land.

Phases: P0 harness truthfulness, P1 spec retrofit, P2 freeze gate,
P3 pipeline harness, P4 fail-closed draft gate, P5 documentation
gates, P6 heredoc fix + verify command, P7 policy cluster, P8 close
out. Estimated 9-12 working days.
shellcheck is not installed natively here; the wrapper runs
koalaman/shellcheck:stable so the plan's lint commands work locally
and verify before CI rather than after.

Also records the two pre-flight conflict resolutions: red-first tasks
squash red into green so every commit is green, and Task 34's
word-splitting is to be resolved with a read loop rather than a
blanket SC2086 disable.
cd "" returns 0 and stays in the invocation directory, so an empty
TEST_TEMP would make the check_git_repo assertion run rm -rf .git in
the repo root. set -e was masking a failed mktemp; the next commit
removes that protection, so make the guard explicit first.
set -e at the top of the suite aborted the script on a failing
subshell before test_result could run, so the FAIL branch was
unreachable and '0 failed' was guaranteed by construction.

The obvious fix does not work: bash suppresses errexit inside a
subshell used as an if condition, and the suppression propagates
into the body even with an explicit set -e. The only form that
preserves the semantics is set +e at top plus set -e as the first
statement of each body, invoked as a simple command.
Assertion inversion cannot validate the errexit fix — inverting a
body's last line flips the status under both the correct and the
broken form. Mutation discriminates: impl-prompt-return1 yields 3
FAILs under the correct harness and 0 under the broken one.
Two of the three implementation-prompt assertions were pure negatives
that an empty file satisfies, so a silent mutant (empty output,
success status) was caught by only 1 of 3. Positive content checks
raise that to 2 of 3. Remaining weak assertions are recorded in
tests/FINDINGS.md rather than frozen silently.
Task 4 (2521737) appended positive grep checks after the two `! grep`
negations in the implementation-prompt sanitization assertions. That
made the negations non-terminal in their subshells; under `set -e`,
bash exempts `!`-prefixed commands from errexit, so a violated
negative (pattern present when it must be absent) would report PASS
instead of failing the assertion. shellcheck SC2251 flagged both.

Convert both to `if grep ...; then exit 1; fi` guards, which
participate in errexit regardless of position and can't be re-masked
by future appended lines. Update FINDINGS.md F1 to reflect the fix.
The names claimed coverage of 'all codes'. Codes 9 and 10 arrive in
P6/P7, and after the freeze lands a rename is a NEEDS_HUMAN event —
so rename now, while the rename surface is declared zero. Bodies are
unchanged; they enumerate 0-8 and remain correct as a subset check.
Every story now names the runtime test that proves it. Names come
from an actual run because source and runtime text differ for 4 of
158 assertions. Stories covering several tests were split so the
mapping is one story, one test, one criterion.
30 assertions covering run state, checkpoints, story selection,
prompt builders, the completion hook, iteration-summary extraction
and implementation-prompt sanitization had tests but no written
criterion. Also fills 8 straggler assertions into the existing
config.sh (US-CFG-14..19) and schema.sh (US-SCH-33..34) sections.

spec-map.sh mapped count: 86 -> 124, zero PHANTOM, zero AMBIGUOUS.
Marks the two claude-gated stories explicitly - they run as
test_result where claude is installed and as test_skip under the
same name where it is not.
Modules 7-11 (preflight, pr-create, init, review phase, validate +
harness) close the remaining gap. spec-map.sh now exits 0: every
runtime test name maps to exactly one story, with no phantom or
ambiguous entries. Also fixes a stray leading digit and stale
module count in the file header.
Plan Task 9 Step 5 — the P1 exit criterion to reproduce F4's count.
Measured 18 pure-negative assertions (plan estimated ~21; Task 4's
conversion of two negations to if/exit form accounts for part of the
gap).
Runtime names come from a real suite run; the parser strips ANSI and
splits on the first ': ' only, because all 158 names contain ': '
themselves. Integrity comes from whole-file hashes of the suite and
the gate.
Strict precedence: after P0 any FAIL makes the suite exit non-zero,
so a truncation rule keyed on the exit code would re-label every
weakening as truncation. R0 therefore fires only when the result
count is short AND no FAIL was parsed.

conditional is a closed enum of one member (claude); an unrecognized
value hard-fails rather than exempting, so it cannot be used as a
one-word kill switch.

Also strips \r from the locked-name extraction: this machine's jq is
a native Windows build that writes multi-line -r output in CRT text
mode, appending \r before every \n. ran.txt (built by pure bash
string parsing) never carries \r, so without stripping, comm would
see zero overlap between the two files and misfire R1+R6 on every
locked test. Re-accepted the lock so gateSha256 matches this file.
The R7 reporter case is the one that matters: all lib/*.sh emptied
and test_result patched to print PASS unconditionally leaves every
assertion body byte-identical, which is why a per-body hash was not
enough and the freeze is a whole-file hash.
The gate needs no fetch-depth and no base-ref checkout — it compares
file hashes against the lock in the same tree, so it behaves
identically on a laptop with no remote and in CI.
…F asymmetry

Two review findings on the freeze gate, neither a correctness bug:

- gate-selftest.sh mut_r2_fail re-ran `oracle-gate.sh --accept` with a
  comment claiming it stopped R7 masking R2. False: the mutation targets
  lib/, which is outside the hash surface (only simple-test.sh and
  oracle-gate.sh are hashed), so R7 never fires here anyway. Removed the
  dead step (also cuts a ~90s suite run from the self-test) and corrected
  the comment. mut_r6/mut_r0 keep their re-locks — they DO edit the hashed
  suite file, so theirs are necessary.

- oracle-gate.sh: documented why the multi-line jq pipeline needs
  `tr -d '\r'` (native Windows jq emits CRLF; a pipe does no newline
  stripping) while single-value `$(jq ...)` substitutions do not (command
  substitution strips the trailing CRLF wholesale). The asymmetry read as
  a latent bug; it isn't. Comment-only; lock re-accepted for the new hash.

R2 path re-verified: breaking a lib fn yields GATE FAIL [R2], not [R7].
Nothing invoked run_pipeline before this. Three fidelity traps are
handled explicitly: ph_run sets pipefail (lib/run.sh sets bare set -e,
but agent failure is detected through a claude|tee pipeline), it
captures run_pipeline's exit rather than its return, and README now
documents the undocumented timeout dependency.

ph_setup also wires a real local bare "origin" remote, since
create_pr's unconditional `git push -u origin` would otherwise fail
before gh pr create is ever invoked, short-circuiting the very path
this test exists to exercise.
The bare-repo origin was $PH_ROOT/../ph-origin.git — a shared sibling
across every ph_setup call under the same TEST_TEMP. Because ph_setup
hard-codes REQ-01 (branch reqdrive/req-01), a second invocation in one
suite run pushed the same branch to the same shared repo and was
rejected, so the pipeline never reached gh pr create and the fake-gh
log came back empty. Task 17 adds four ph_setup/ph_run pairs to one
file and would have hit this immediately, making every --draft grep a
false negative unrelated to the draft logic under test.

Namespaced to ${PH_ROOT}-origin.git so each case gets its own remote.
Verified: two ph_setup/ph_run pairs in one process both reach pr create.
Gutting build_implementation_prompt used to produce 'ok ... # skip'
and a green bats run, so the three e2e tests named as the safety net
for the P6 heredoc rewrite could not fail. The deterministic fake
agent removes the reason the hatches existed.
'bats green' meant nothing while six tests could skip themselves.
The gate cleared --draft on three separate no-evidence paths: null
verification (no testCommand configured, only the literal string
"false" was checked), a missing prd.json left holding the "?"
sentinel, and stories omitting the optional 'passes' field, which
select(.passes == false) never matched (null != false). Enumerating
those negatives was a losing game, so the PR is now a draft unless
the PRD exists, zero stories remain, and verification positively
passed (verification_passed == "true").

final_remaining is now an integer (0 by default) and prd_present
(0|1) replaces the "?" sentinel as the source of truth for whether a
PRD was ever produced. Story counting switched from
select(.passes == false) to select(.passes != true) so a story
missing the field counts as incomplete rather than complete.
verification-summary.json keeps emitting remaining: null when no PRD
exists, and gains prd_present so the two cases stay distinguishable.

Also removed the Phase 1 hard-abort when the agent never produces a
PRD after its planning retries. That path previously called exit
EXIT_AGENT_ERROR before Phase 2/3 ever ran, so a run with no PRD
produced no PR at all rather than the draft PR the new gate is meant
to guarantee for human review. Phase 2 and Phase 3 already tolerate a
missing prd.json gracefully (select_next_story returns empty,
prd_present stays 0), so this now falls through to a draft PR instead
of a silent, evidence-free failure.

Adds four red-first tests under "Draft Gate" proving all three
fail-opens plus a positive control that a run with full evidence
(PRD complete, testCommand passing) still produces a non-draft PR —
confirming the fix doesn't just force --draft unconditionally. New
BEHAVIOR-SPEC stories US-DRAFT-01..04 keep spec-map total at
163/163; oracle.lock.json re-accepted at 163 tests.
2362488 inverted the draft-PR gate to fail-closed (correct, kept) but
also removed Phase 1's abort when planning never produces a valid
prd.json, letting the pipeline fall through to an empty draft PR
instead. Restore the original abort: write_run_status "failed",
run_completion_hook, exit EXIT_AGENT_ERROR.

The prd_present=0 gate branch is now reachable only if an agent
deletes prd.json mid-implementation (after planning succeeded) —
recorded as F6 in tests/FINDINGS.md.

Retarget "draft gate: missing prd.json forces draft" (US-DRAFT-02) to
assert the restored abort directly: noprd mode now exits 5 with no
"pr create" in the gh log, renamed to "draft gate: planning failure
aborts with no PR". Re-locked via oracle-gate.sh --accept.
select_next_story used select(.passes == false and ...) while Phase 3's
story counting used select(.passes != true). A story omitting the
optional passes field entirely was never selected for implementation
(== false doesn't match a missing/null field) yet Phase 3 counted it as
incomplete, so the PR would draft forever with no way to make progress
on that story. Change the predicate to .passes != true to agree with
Phase 3.

Adds a red-first regression test (US-RUN-31) and closes F7 in
tests/FINDINGS.md.
testCommand defaults to empty, so after the fail-closed inversion a
default-configured project gets a draft on every run. Preflight now
says so at run start and the PR body distinguishes 'no test command
configured' from 'tests failed' — which is what makes the tri-state
worth carrying rather than collapsing to a boolean.
The dispatch block accepts nine commands; README documented seven.
The test parses the live case block, so adding a command in a later
phase reddens the suite until README catches up.
Second doc-coverage rule: every REQDRIVE_* config field in lib/config.sh
must appear in README, minus a DOC_EXEMPT list of the three derived
runtime paths (REQDRIVE_MANIFEST, REQDRIVE_PROJECT_ROOT, REQDRIVE_ROOT).
Failed on maxStoryRetries and reviewCommand until documented. US-DOC-02.

(Subagent completed the edits; controller ran the final --accept and
gate to avoid the park-on-background-job issue with the slow suite.)
Third doc-coverage rule: every accepted CLI flag (parsed from bin/reqdrive's
option case-labels, so the --help inside the usage-error string is not a
false positive) must appear in README's Run Options. US-DOC-03.

The flag was already mentioned parenthetically, so this rule's own red-first
was trivial; its value is as a standing gate — when a later task adds --ref,
the suite reddens until README documents it.

Lock regenerated deliberately by the controller (--accept) and gate-verified
170/170 after a background-job race left the on-disk lock ambiguous.
adbarc92 added 17 commits July 23, 2026 20:26
The audit says reqdrive 'never verifies outputs'. lib/run.sh:1106-1117
re-runs testCommand and reads the real exit code, so that is false.
The reasoning that produced the roadmap is kept; only the claim is
corrected, in place and dated.
Cases 2/5/7/8 (status of a finished run, exit-code reporting, completion
hook env, re-launch) assert on run.json state transitions and join the
main suite as US-LAUNCH-01..04. Cases 1/4/6 (detached launch, duplicate
block, crash detection) need real background processes and PID liveness —
unreliable under MSYS2 per CLAUDE.md — so they run in a new Linux-only
tests/launch-lifecycle.sh CI job rather than a lock exemption. Case 3
(logs) asserts process behavior. LAUNCH-TEST-PLAN.md becomes a pointer to
the automated coverage.
P5 review findings:

- The flag doc-coverage test extracted flags from hardcoded line ranges
  (sed -n '90,130p;395,425p'), so when bin/reqdrive's option blocks shift
  — exactly what Task 30 does by adding cmd_verify — a new --ref case label
  falls outside the window and is silently never checked, letting --ref
  ship undocumented while the test reads green. Replaced with a whole-file
  scan for flag case-labels (the )$ anchor still excludes the --help inside
  the usage string). Proven: an undocumented --ref now reddens the test.

- tests/launch-lifecycle.sh killed only the top-level nohup PID, leaking
  the timeout/claude/tee children (reparented to PID 1). Added kill_tree()
  (recursive pgrep -P walk); pgid-kill was unsafe because cmd_launch uses
  plain nohup and shares the test's process group.
Characterization, not red-green: locks build_implementation_prompt's
current output so Task 26's quoted-heredoc rewrite can be proven
unchanged and Task 28 can change it deliberately with an enumerated diff.

The fixture carries every hazard the rewrite could break: &, backslash,
backtick, $, and a literal @@STORY_ID@@ forgery attempt. The golden
captures the current stray-$ escaping defect as-is (Task 28 fixes it).

The golden is canonical LF and the assertion CR-normalizes both sides:
native Windows jq emits the criteria join("\n") as \r\n in text mode, so
the output carries a CR on Windows and none on Linux — a line-ending
artifact, not a semantic difference. US-RUN-32.
Byte-identical output (modulo CR), proven by the golden file. Swaps the
unquoted heredoc — which expanded ${vars} and was a shell-injection
surface — for a quoted heredoc plus explicit ${tpl//@@token@@/"$val"}
injection. Three mechanism traps handled: all 24 backslash-escaped
backticks de-escaped (a quoted heredoc does no escape processing);
replacements quoted so bash >= 5.2 does not expand & in a value to the
matched text; shopt -u patsub_replacement suffixed with || true because
the option does not exist before 5.2 and set -e would abort on its exit 1.

Substitution order puts @@STORY_ID@@ first so a value containing that
literal token (the fixture's forgery attempt) is injected afterward and
never re-matched — reproducing the old single-pass heredoc behavior. The
robust @@-stripping forgery guard is added in the next commit (Task 27),
which deliberately changes the golden; this commit is the pure
mechanism swap. The stray-$ escaping defect is preserved here and fixed
in Task 28.
The quoted-heredoc rewrite injects via ${tpl//@@token@@/"$val"}, so a
PRD value containing a literal @@token@@ could — in a mutual-reference
case that substitution ordering alone cannot fully cover — forge a
placeholder a later pass expands. Stripping @@ from every injected value
before substitution makes that impossible, order-independently.

This is the one deliberate change to the golden the Task 26 refactor
preserved: the fixture's forgery attempt in the description,
'@@STORY_ID@@', is now defanged to 'STORY_ID'. Enumerated golden diff:
  - **Description:** Covers @@STORY_ID@@ forgery ...
  + **Description:** Covers STORY_ID forgery ...
(exactly one line; no other output changed.)

Three assertions pin the guards: the shopt || true survives a bash
without patsub_replacement (US-RUN-33), PRD content cannot forge a token
(US-RUN-34), and an ampersand in a title is not expanded to the match
(US-RUN-35).
sanitize_for_prompt escapes $ to \$ for the OLD unquoted heredoc. The
heredoc is quoted now, so the backslash was pure noise reaching the agent
— including inside the commit message it is instructed to use. Un-escape
at injection time (the correct bash form is ${var//\$/\$}; the
naive ${var//\$/$} does not un-escape). lib/sanitize.sh is unchanged:
its backtick neutralization is load-bearing and it has other callers.

Enumerated golden change (4 lines, all \$ -> $):
  - **Title:**       ...\/c/Users/barclay...   -> ...$HOME...
  - **Description:** ...\...  -> ...${VAR}...
  - criterion:       Check \/c/Users/barclay -> Check ${HOME}
  - commit message:  feat: [...] ...\/c/Users/barclay -> ...$HOME

Updated Task 4's escaped-form assertion to the un-escaped form, added
US-RUN-36 (a $ title reaches the agent verbatim, commit line clean), and
corrected US-RUN-30's now-stale prose.
run_pipeline's inline Phase 3 becomes three shared functions so the new
reqdrive verify command (next commit) reuses one implementation:
  verify_collect     -> VERIFY_STORIES_* + VERIFY_PRD_PRESENT globals
  verify_run_tests   -> tri-state 0 pass / 1 fail / 2 not-configured
  verify_write_summary <agent_dir> <req_id> <max_iterations> <mode>

Named verification.sh, not verify.sh, to stay distinct from the archived
archive/v1-complex/lib/verify.sh. max_iterations is an explicit parameter
(a run_pipeline local interpolated into the JSON; omitting it emits a
malformed "max": ). The summary is written temp-file + mv (atomic).
verify_run_tests's return is captured with || verify_rc=$? because a bare
non-zero return would trip set -e before the case ran.

Characterization: verification-summary.json keeps its full shape
(US-PIPE-02), draft gate behavior unchanged.
Re-runs verification for an existing run and updates its
verification-summary.json in merge mode, so re-verifying preserves the
iterations/tests/commits evidence trail the PR body renders. Refuses when
the run's PID is still alive (EXIT_CONCURRENT_RUN=10), when the checkout
does not match the run's recorded branch and no --ref is given
(EXIT_GIT_ERROR=4), and when the run or its summary is missing
(EXIT_CONFIG_ERROR=3). Exits 0 on pass, EXIT_VERIFICATION_FAILED=9 on fail.

Documented verify + --ref in README — the P5 doc-coverage gates reddened
on both until documented, which is the gate working.

Found a pre-existing latent bug (logged as F8): write_run_status writes
pr_url into run.json without JSON-escaping, so an embedded newline makes
the file invalid JSON and crashes jq consumers under set -e. verify's
pid-read is fail-open to survive it; the root fix in write_run_status is
deferred (it touches frozen run_status tests).
Three review findings, each with a red-first test:

- cmd_verify silently no-op'd its branch guard when checkpoint.json was
  absent (a run with empty userStories / maxIterations=0 writes no
  checkpoint), so verify would record an unrelated branch's evidence.
  Now refuses with EXIT_CONFIG_ERROR.
- --ref checkout failure aborted with git's raw exit 1, contradicting the
  documented 'exit 4 on branch mismatch'. Now guarded to EXIT_GIT_ERROR.
- F8 root cause: write_run_status wrote pr_url into run.json without
  JSON-escaping, so an embedded newline made the file invalid JSON,
  crashing every jq consumer (verify's pid guard, the status command).
  Now escaped via jq -Rn. verify keeps a defensive fail-open too.

Fixing F8 (making run.json valid) surfaced a latent test flaw the freeze
gate caught (R2): two verify tests passed at 185 only because the invalid
JSON made verify's fail-open bypass the concurrency guard. With valid
JSON the guard correctly fires on run.json's pid — which in the harness
is $$ (the live test runner). Both tests now set a dead pid to reflect a
completed run's dead process; 'merge preserves evidence' was passing
vacuously (verify refused) and is now a real merge test.
Aligns reqdrive validate's failure exits to EXIT_CONFIG_ERROR (3) so the
policy validation Task 32 adds isn't the only field whose malformation
exits 3 while every other field exits 1. Closes F5 (the existing assertion
checked only -ne 0, never pinning the code) with two exit-code assertions
(US-VAL-03/04).

Also fixed a latent set -euo pipefail bug: the schema-error-display
'validate_config_schema ... | while read' pipeline failed under pipefail
and aborted cmd_validate before the exit code was set, so a type
violation exited 1 regardless. Terminated the loop with || true.
policy lives inside reqdrive.json (one file, one loader, one validator),
not a separate policy.json. Schema validates it: policy must be an object,
policy.scopeCheck (if present) must be warn|block, policy.riskTiers values
must all be arrays. reqdrive_load_config exports REQDRIVE_POLICY_JSON and
REQDRIVE_POLICY_SCOPE_CHECK (defaults {} / warn). Tasks 33-34 consume these.

The config doc-coverage test would derive false field names from the two
new REQDRIVE_POLICY_* vars, so they're added to its DOC_EXEMPT list as
derived from the single documented policy field. US-POL-01..04.

reqdrive_load_config still does NOT schema-validate on load (deferred, per
the design) — reqdrive validate remains the validation entry point.
lib/policy.sh classifies a path into high/medium/low/none. Patterns are
bare path prefixes, NOT globs: in bash [[ ]] matching globstar does not
apply, so ** and * are indistinguishable and both cross /, and src/auth/**
fails to match src/auth itself. A path matches when it equals the pattern
or begins with pattern/ — so src/auth.sh does NOT match src/auth (the trap
a glob would have hidden), while src/auth/login.ts does. Tiers are probed
high->medium->low so the highest wins.

The jq-per-tier read strips a trailing CR (native Windows jq emits CRLF in
multi-line -r output; a no-op on Linux). US-POL-05..08.
After each implementation iteration, git diff --name-only HEAD~1 HEAD is
classified by risk tier; a high-risk path changed in an iteration whose
tests did not pass is a finding. warn (default) logs it to
scope-findings.txt and the PR body and continues; block aborts the
iteration with EXIT_PREFLIGHT_FAILED. The touched paths are iterated with
a while-read loop, not $changed word-splitting, so filenames with spaces
are safe and there is no SC2086.

Ships warn-only: the roadmap wanted a hard gate, the architecture
principle says warn before enforce. The knob makes the gate one config
edit away, and warn-mode data is what would justify flipping the default.
US-SCOPE-01..03.
P7 review: a risk-tier pattern written as a directory with a trailing
slash ('src/auth/' — a natural way to name a directory) matched NOTHING,
silently defeating the scope check for that path. policy_tier_for_path now
strips a trailing slash so 'src/auth/' behaves as 'src/auth'. For a
security-relevant gate a silently-inert high-risk pattern is a real
footgun. US-POL-09.

Also documented the HEAD~1 invariant in policy_scope_check: it always
resolves because preflight's check_base_branch_exists guarantees the base
branch (and thus >= 1 commit) exists before the work branch is cut; if
that is ever weakened the check fails open rather than erroring.
…eCheck

Final whole-branch review findings, both closed:

- The freeze gate hashed only simple-test.sh + oracle-gate.sh, but the
  suite sources tests/lib/pipeline-harness.sh at enforce time, so its
  content decides ~10 tests' outcomes. Gutting the harness toward
  fake-success (ph_run(){ echo 0; } + a canned pr-create log) made those
  tests pass for the wrong reason with the hash unchanged and the gate
  green — the exact hole R7 closed for test_result. Added harnessSha256
  and specmapSha256 to R7. Proven: the fake-success exploit now fires
  GATE FAIL [R7]. (F9)

- REQDRIVE_POLICY_SCOPE_CHECK came from a single-value jq without a
  CR strip; a surviving CR (native Windows jq) would make [ mode = block ]
  false and silently downgrade the hard scope gate to warn. Strip it.

FINDINGS: F9 closed; F3 re-triaged to essentially-zero exposure per the
review (story_json is schema-validated and aborts under set -euo pipefail
rather than emitting blanks).
@adbarc92
adbarc92 merged commit 0aa2b54 into main Jul 25, 2026
5 of 6 checks passed
@adbarc92
adbarc92 deleted the feat/roadmap-completion branch July 25, 2026 02:01
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.

1 participant