Skip to content

[US-400] fix: coverage-gate.sh was committed 644 (unrunnable) — smoke suite now runs pre-merge in CI - #424

Open
rucka wants to merge 16 commits into
mainfrom
feature/US-400-smoke-in-ci
Open

[US-400] fix: coverage-gate.sh was committed 644 (unrunnable) — smoke suite now runs pre-merge in CI#424
rucka wants to merge 16 commits into
mainfrom
feature/US-400-smoke-in-ci

Conversation

@rucka

@rucka rucka commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

PR Information

PR Title: [US-400] fix: coverage-gate.sh was committed 644 (unrunnable) — smoke suite now runs pre-merge in CI
Story/Epic: #400 (scope extended 2026-08-12: #431 absorbed and closed)
Type: Bug Fix (defect + the systemic gap that hid it)
Priority: High
Labels: risk:yellow
Head at time of writing: 7d30b9a7 · 16 commits · 28 files changed (+1603 / −72)

Summary

What Changed

scripts/smoke-tests/run-all.sh --ci was invoked by no workflow. This PR makes it a real status check, gives the runner a vocabulary that can say "this scenario could not run", and stops the repo masking committed file modes anywhere.

  1. smoke job in ci.yml, parallel to build, running run-all.sh --ci --cleanup on every PR — after a dependency-aware pnpm turbo build --filter=@pair/pair-cli....
  2. Four outcomes instead of two: PASS / FAIL / NOT EXECUTABLE / MISSING, decided by scenario_state before a scenario is executed. The NOT EXECUTABLE line names the file, its mode, and one repo-relative command that fixes the tracked mode.
  3. A tracked-mode guardpackages/dev-tools/src/quality-gates/smoke-scenario-modes.ts reads git ls-files -s -z (the git index, never the filesystem). It runs as its own root gate step, pnpm smoke-modes:check, in the quality-gate chain and in the CI build job — not only as a unit test, because turbo test is cacheable with package-scoped inputs and would replay a cached PASS for a mode change outside @pair/dev-tools.
  4. The guard covers scripts/workflows/release/ too, and the workflows stop chmodding — the absorbed Git-mode guard covers smoke scenarios only — release.yml chmods its scripts before running them, masking their committed modes #431, see the scope-extension section at the end.
  5. The CI list moves to scripts/smoke-tests/lib/ci-tests.sh, one copy, with a recorded reason for every exclusion; scenarios/runner-outcomes.sh audits the invariant in both directions by executing the runner against fixtures.
  6. The defect itself: scenarios/coverage-gate.sh was committed 100644 in PR [#282] feat: coverage baseline + CI guardrail (tier-aware pipeline gate) #368 and had been unrunnable since. Running it for the first time found two stale assertions inside it, also fixed here.
  7. The ADL that authorizes the scope extension now binds the review skill it names/pair-process-review's tech-debt step no longer offers to file the card it defers to (round 3, below).

Why This Change

CI_TESTS read like a pipeline consumed it. Nothing did. coverage-gate.sh sat dead for weeks because the two failures were indistinguishable: nobody ran the list, and when someone did, Permission denied rendered as a plain FAIL. Every real-CLI guarantee in the suite — including assert_pinned_bug, whose whole value is failing loudly when a pinned upstream bug is fixed — depended on a human remembering.

Story Context

User Story: #400 — the smoke suite's CI-safe list runs pre-merge, and a scenario that cannot run says so.

Acceptance Criteria coverage (8 original + 3 added with the scope extension):

AC Covered by Evidence
AC1 — a smoke job runs run-all.sh --ci in parallel with build, own status check .github/workflows/ci.yml (smoke job) green runs below; build and smoke are separate contexts
AC2 — a pinned-bug flip is surfaced by the pipeline scaffold-kb.sh (pins #395) is in CI_TESTS executed in CI — | scaffold-kb.sh | ✅ PASS |
AC3 — NOT EXECUTABLE distinct from FAIL and MISSING, naming file + mode run-all.sh run_scenario + scenario_state; asserted by runner-outcomes.sh driving the runner against fixtures deliberate-red run below: | scaffold-kb.sh | 🚫 NOT EXECUTABLE (mode 644) |
AC4 — executability asserted by git mode, not test -x smoke-scenario-modes.ts (+ 35 unit tests) + the smoke-modes:check root step pnpm smoke-modes:check✓ 31 tracked scripts are staged 100755; red reproduction below
AC5 — coverage-gate.sh is 100755 and run-all.sh --ci exits 0 on a clean checkout mode fixed; two stale assertions inside it repaired green run: 18/18 PASS, exit 0, on a CI runner
AC6 — measured duration recorded next to the job + revisit rule ci.yml MEASURED COST / REVISIT THRESHOLD block; ADL measured in CI on the current head's parent (cf25c981): smoke 1m59s vs build 3m37s, same run
AC7 — every exclusion documented with its reason lib/ci-tests.sh CI_EXCLUDED; enforced by runner-outcomes.sh (a scenario in neither array fails the suite) runner-outcomes.sh PASS in the green runs
AC8 — nothing is auto-corrected in CI (check-only) no chmod in runner/workflow; runner-outcomes.sh asserts the 644 fixture is untouched after a run green runs; gate:composition also holds the whole gate check-mode
AC9 — the guard covers scripts/workflows/release/ as well as scripts/smoke-tests/ MUST_BE_EXECUTABLE.releaseDir, read by requiresExecutableBit and by readSmokeTestsIndex (the index read used to name only the smoke tree, so release paths passed by never being looked at) cf25c981; tests "the real index read includes the release scripts", "every tracked release script … is executable by its staged git mode"
AC10 — neither workflow chmod +x-es a repo-committed script (9 lines removed) release.yml (8), website-preview-deploy.yml (1) cf25c981; test "no workflow chmods a repo-committed release script any more" greps both workflows
AC11 — a release script committed 644 fails the guard, named with its mode as read from git checkSmokeScenarioModes test "a release script staged 644 is reported as an offender" + the local red reproduction below

Changes Made

Implementation Details

  • smoke jobpermissions: {contents: read}, timeout-minutes: 15, dependency-aware build, then run-all.sh --ci --cleanup. --ci selects the list, sets PAIR_DIAG=1, and appends the report to the job summary.
  • Runner outcome vocabularyscenario_state / file_mode in lib/utils.sh; run_scenario renders one row per outcome and never chmods.
  • Tracked-mode guardsmoke-scenario-modes.ts + .test.ts, plus the thin CLI pnpm smoke-modes:check wired into the root quality-gate chain and its own CI build step (a guard reachable only from a cacheable turbo task is not enforced — now recorded in way-of-working.md § Quality Gates). Covers scripts/smoke-tests/{run-all.sh,scenarios/*.sh} and scripts/workflows/release/*.sh; fails loudly when the index carries no runnable file at all, naming both trees, so a renamed folder cannot silently disable it.
  • Single CI listlib/ci-tests.sh (CI_TESTS + CI_EXCLUDED with reasons), sourced by the runner, audited by runner-outcomes.sh.
  • Test seamsrun-all.sh accepts SCENARIOS_DIR and CI_TESTS_FILE overrides (both absolute by default) so its own report can be tested against fixtures rather than grepped.
  • Manual-artifact honestypackage-manual.sh advertised a types entry it did not ship; the packager now emits only what it contains and 00-create-install-package.sh asserts it (ADL 2026-08-12-manual-cli-artifact-types-are-optional.md).
  • The bugcoverage-gate.sh mode 100755; it also demanded git config --add remote.origin.fetch … that Coverage baseline auto commit-back — CI persists/ratchets the baseline (opt-in, on top of #282) #372/PR [US-372] feat: coverage baseline auto commit-back — monotonic ratchet as a bot PR, opt-in and off by default #405 had already replaced with the transient git -c … form, and refuted a bare git checkout substring that also matches the restore step.

Files Changed

Complete git diff --name-status origin/main...HEAD (28 paths):

  • Added (8): packages/dev-tools/src/quality-gates/smoke-scenario-modes.ts, …/smoke-scenario-modes.test.ts, packages/knowledge-hub/src/conformance/implementation-never-files-a-card.test.ts, scripts/smoke-tests/lib/ci-tests.sh, scripts/smoke-tests/scenarios/runner-outcomes.sh, .pair/adoption/decision-log/2026-08-11-smoke-suite-runs-in-ci-pre-merge.md, .pair/adoption/decision-log/2026-08-12-manual-cli-artifact-types-are-optional.md, .pair/adoption/decision-log/2026-08-12-implementation-never-files-a-card-it-extends-the-story.md
  • Modified — CI/workflows (3): .github/workflows/ci.yml, .github/workflows/release.yml, .github/workflows/website-preview-deploy.yml
  • Modified — build/tooling wiring (2): package.json (smoke-modes:check + the quality-gate chain), packages/dev-tools/package.json
  • Modified — smoke suite (8): scripts/smoke-tests/run-all.sh, scripts/smoke-tests/lib/utils.sh, scripts/smoke-tests/README.md, scripts/smoke-tests/scenarios/{00-create-install-package,coverage-gate,format-ignore-delegation,pr-state-flow,tier-aware-gate}.sh
  • Modified — release scripts (1): scripts/workflows/release/package-manual.sh
  • Modified — docs/adoption (4): DEVELOPMENT.md, .pair/adoption/tech/way-of-working.md, .pair/adoption/tech/infrastructure.md, .pair/adoption/decision-log/2026-07-28-marketplace-plugin-packaging.md
  • Modified — skill definition, both copies (2): packages/knowledge-hub/dataset/.skills/process/review/SKILL.md (dataset source), .claude/skills/pair-process-review/SKILL.md (installed mirror) — round 3

Testing

Test Coverage

  • Unit: 35 tests in smoke-scenario-modes.test.ts — parser edges (malformed line dropped, mode taken from the pre-tab segment, trailing-space path preserved verbatim, -z NUL parsing), the exemption boundary (lib/utils.sh sourced ⇒ no bit; nested scenarios/** and release fixtures/** ⇒ no bit; *.sh directly in either guarded folder ⇒ bit required), the failure-message contents for both trees, and live assertions against this repo's own index (smoke tree and release tree).
  • Smoke: runner-outcomes.sh executes run-all.sh against a fixture SCENARIOS_DIR + fixture CI list (755 pass, 755 fail, 644, listed-absent) and asserts the four report rows verbatim, exit 1, that NOT EXECUTABLE never renders as FAIL, that the 644 fixture is still 644 after the run, and that both halves of the printed remedy name one identical path.
  • Conformance: 8 cases in packages/knowledge-hub/src/conformance/implementation-never-files-a-card.test.ts (round 3) — asserted on both copies of /pair-process-review's SKILL.md: no write-issue reference anywhere in the skill, the tech-debt step states the ban and allows citing an existing card, the merge gate is named as the alternative, the ADL slug is cited, and the ADL's own review-side bullet no longer stands open. 7 of the 8 were red before the fix. The knowledge-hub mirror guard (skill-md-mirror.test.ts, 83 tests) is what proves the two copies did not drift apart.
  • Manual: none needed — the evidence below is the pipeline's, plus one local red reproduction of AC11.

Test Results — CI evidence, re-pointed at the head

These supersede the d66abf36 / 0dc43057 runs cited in earlier revisions of this description.

GREEN on cf25c981 (the parent of the current head; last commit that changed shipped code paths) — run 31616635648:

smoke:        ✅ 1m59s  — 18/18 scenarios PASS
build:        ✅ 3m37s  (same run) ⇒ smoke is ~54% of the critical-path job
secret-scan:  ✅ 8s

GREEN on the current head 25b1cc21 — run 31619416353: build ✅, smoke ✅, secret-scan ✅ — all three green. The two commits on top of cf25c981 are a new ADL + a way-of-working.md bullet (41fdf075) and a guard failure-message rewording with two new unit tests (25b1cc21) — no behavioral path in the runner or the workflows changed.

GREEN on the current head 7d30b9a7 — run 31621079558: build ✅, smoke ✅, secret-scan ✅. The single commit on top of 25b1cc21 changes no shipped code path: two markdown skill files, one new conformance test, one ADL bullet. The full local gate was run stage by stage and the pre-push gate re-ran the whole chain — see below.

DELIBERATE RED — run 31538595395 on scratch branch smoke-deliberate-red-us400 (= d66abf36 + git update-index --chmod=-x scripts/smoke-tests/scenarios/scaffold-kb.sh). Both jobs went red for the two different intended reasons:

smoke (job 93935557999):  | scaffold-kb.sh | 🚫 NOT EXECUTABLE (mode 644) |   -> exit 1
                          remedy: chmod +x scripts/smoke-tests/scenarios/scaffold-kb.sh
                                  && git update-index --chmod=+x scripts/smoke-tests/scenarios/scaffold-kb.sh
build (job 93935558012):  AssertionError: 1 smoke-test file(s) are staged NON-EXECUTABLE (git index):
                            scripts/smoke-tests/scenarios/scaffold-kb.sh (mode 100644, expected 100755)

This red run predates two later commits and is quoted as history, not as current output: the guard's enforcement point moved from the cached test task to its own smoke-modes:check step (fa3c147e), and its wording was generalized once it covered release scripts (25b1cc21). No fresh red CI run was made for a wording change; the current text is pinned by unit tests and reproduced locally below.

LOCAL RED on the current head — AC11, the absorbed #431 criterion:

$ git update-index --chmod=-x scripts/workflows/release/package-manual.sh
$ pnpm smoke-modes:check
❌ smoke-scenario modes
1 tracked script(s) are staged NON-EXECUTABLE (git index):
  scripts/workflows/release/package-manual.sh (mode 100644, expected 100755)
… a release script in that state runs in CI only because some workflow chmods it first,
  and fails for everyone else (#431).
This guard reports the mode and never fixes it; run:
  chmod +x … && git update-index --chmod=+x …
Exit status 1
$ git update-index --chmod=+x scripts/workflows/release/package-manual.sh   # restored, 100755

Local gate on the head (tiering is disabled in way-of-working.md, so the full suite ran): ts:check, turbo test (144 tests), lint, format:check (prettier + mdlint), gate:composition, hygiene:check, smoke-modes:check, docs:staleness, skills:conformance, dup:check (1.07%), turbo buildall PASS.

Reviewer Guide

What round 3 changed (1 finding, Major, resolved — none escalated)

The Active ADL this PR commits banned filing cards, while the skill it names still offered to file one. The ADL's § Adoption Impact item 3 said /pair-process-review's deferral instruction "must stop offering to file the card it defers to", and — unlike item 2, which is explicitly carved out to the orchestrator's own change — carried no deferral qualifier. The skill was untouched: Phase 4.2 Step 5 still read "note it as a recommendation for deliberate promotion after review via /write-issue (with the tech-debt label)" — the exact instruction that produced #426#431. The repo would have shipped an Active policy next to the instruction violating it.

Both remedies the finding offered were taken:

  1. The instruction is rewritten (7d30b9a7), identically in the dataset source and the installed mirror — the review records the debt and stops; it may cite an already-existing card by number; otherwise the debt is fixed in the PR (extending the story that surfaced it) or left as an actionable finding the maintainer judges at the merge gate. There is now no write-issue reference anywhere in the review skill. Test-first: the conformance file above was written and run red (7/8) before the edit.
  2. The ADL bullet now states where it landed — naming the enforcement point (Phase 4.2 Step 5, both copies), the test that pins it, and closing with "Done in the same change as this record", the same qualifier item 2 already carried.

Deliberately not changed, stated rather than deferred to a card: /pair-capability-analyze-debt still documents deliberate promotion via /write-issue by the developer — the ADL's own carve-out ("new items enter deliberately, from refinement or from the maintainer"), and the review composition path is now governed by the review skill's own step, which overrides it. Rewriting a human-facing capability's promotion path is a different decision from banning the agent from taking it. ADL item 2 (the implement-batch prompts) still lands with the orchestrator's change, as it has said since round 2.

What round 2 changed (2 findings, both Major, both resolved — none escalated)

Both findings were about the record, not the code, and both are fixed here rather than deferred:

  1. This description declared the absorbed Git-mode guard covers smoke scenarios only — release.yml chmods its scripts before running them, masking their committed modes #431 work "not yet implemented" while cf25c981 had fully implemented it, omitted 7 changed files from Files Changed, and cited CI evidence four commits behind the head. The body is republished from the current head: the scope-extension section below states what shipped, Files Changed is the complete 25-path list, and the CI evidence above supersedes the old SHAs. The four post-round-1 commits (fa3c147e, 9d1d2b02, 8a7a248d, cf25c981), previously undescribed, now appear in Summary and AC coverage.
  2. The ADL this PR cites to justify its own scope extension existed in no commit (untracked in the author's checkout). It is now committed in this PR.pair/adoption/decision-log/2026-08-12-implementation-never-files-a-card-it-extends-the-story.md (41fdf075) — with the adoption update its template requires (a policy bullet in way-of-working.md next to "one PR per story"). Its References say plainly that the enforcement point (the implement-batch reviewer/fixer prompts and the tests pinning them) lands with the orchestrator's own change, not with this PR: this commit is the decision, not its mechanism.

Found while fixing and fixed here (not a review finding): the guard's failure text still called a scripts/workflows/release/ offender a "smoke-test file" whose "runner cannot execute" it — a mechanism that does not exist for release scripts — and its saw-nothing failure named only the scenarios tree, so a renamed release folder would have disabled half the guard silently. Test-first, 2 cases red → 35/35 green (25b1cc21).

What round 1 changed (15 findings, all resolved)

The Critical finding was real: the job failed on its own head because run-all.sh's build fallback (pnpm --filter @pair/pair-cli build) bypasses turbo's dependsOn, leaving @pair/content-ops unbuilt (~45 TS2307). The job now builds --filter=@pair/pair-cli..., and the runner's own fallback was fixed too. Other notable changes: runner-outcomes.sh no longer greps the runner's source (a token in a comment used to pass) — it drives the runner and reads the report; parseGitIndexEntries no longer invents a mode from a malformed line nor rewrites paths with .trim(); the chmod tripwire on ci.yml is scoped to smoke-test paths; 00-create-install-package.sh is fatal in CI on a packaging preflight failure instead of a silently-green no-op, and reuses the preflight's artifact instead of packaging twice.

Two claims were corrected rather than defended:

  • "the mode is a property of the commit"git ls-files -s reads the index. Docs say staged/tracked, and say why the index (not ls-tree HEAD) is the right target: it catches a chmod +x that was never git add-ed, one push earlier.
  • "CI runs every scenario that declares OFFLINE_SAFE=true" — the enforced rule is opt-out (default true). Restated as enforced; the five decorative # OFFLINE_SAFE=true comments are now real declarations. The five scenarios that declare nothing were left alone deliberately: install-basic.sh reads $OFFLINE_SAFE as a control variable.
  • the default-resolution.sh exclusion reason claimed release.yml covered the download path. Checked: smoke-test-npm-artifact.sh "Test B" does run pair install with no --source against the real artifact, but it warns on failure — evidence, not a gate. Both the ADL and CI_EXCLUDED now say the fallback stays a manual guarantee.

Review focus areas

  1. ci.yml smoke job — the build step's filter syntax and the recorded cost/threshold arithmetic (restated against the cf25c981 run: 1m59s vs 3m37s).
  2. smoke-scenario-modes.ts — the exemption boundary (requiresExecutableBit, now two trees), the "saw nothing ⇒ fail" guard, and that readSmokeTestsIndex reads both trees (the defect AC9 fixes was an index read that never looked at release paths).
  3. runner-outcomes.sh section 3 — the fixture-driven run and the seams it needs in run-all.sh (SCENARIOS_DIR, CI_TESTS_FILE) as production code rather than test-only escape hatches.

Testing the changes

pnpm install
pnpm smoke-modes:check                              # the guard, as the gate runs it
pnpm --filter @pair/dev-tools test src/quality-gates/smoke-scenario-modes.test.ts
pnpm turbo build --filter=@pair/pair-cli...
./scripts/smoke-tests/run-all.sh --ci --cleanup     # the exact CI invocation

Dependencies & Related Work

Documentation

  • ADL 2026-08-11-smoke-suite-runs-in-ci-pre-merge.md — pre-merge not nightly (on measured data), four outcomes, index-not-filesystem, nothing auto-corrected, CI_TESTS as a rule with recorded exceptions.
  • ADL 2026-08-12-manual-cli-artifact-types-are-optional.md — the manual artifact stops advertising a types file it does not ship.
  • ADL 2026-08-12-implementation-never-files-a-card-it-extends-the-story.md — the decision this PR's scope extension rests on (committed here, round-2 finding 2).
  • /pair-process-review SKILL.md (dataset + mirror) — Phase 4.2 Step 5 rewritten so the review never creates a work item (round 3; the ADL above is its decision record).
  • way-of-working.md — the Custom Gate Registry row for smoke tests; the "a guard whose only caller is a turbo task is not enforced" rule; the "implementation and review never file a new card" policy bullet.
  • DEVELOPMENT.md / infrastructure.md — the Turbo-caching gotcha now names this job as its live example; the manual-artifact contract.
  • scripts/smoke-tests/README.md — the outcomes table and the enforced CI-membership rule.

Scope extension — absorbed #431 (2026-08-12)

The story's scope was widened after this PR was opened, per ADL 2026-08-12-implementation-never-files-a-card-it-extends-the-story.mdcommitted in this PR (41fdf075), so the rule that authorizes the extension is readable at the merge gate. #431 is closed as absorbed and its work belongs to this PR.

This story exists to stop committed file modes being masked, and the guard it adds covered scripts/smoke-tests/ only — while release.yml masked the modes of its own scripts: it ran chmod +x scripts/workflows/release/<script>.sh before executing each of them (8 occurrences), and website-preview-deploy.yml a 9th. A release script committed 644 would run green in CI forever and fail for anyone executing it directly — the exact condition coverage-gate.sh was in for weeks.

All 12 files under scripts/workflows/release/ are committed 100755, so widening the guard is green on arrival: this closes the hole before it is used.

Status in this PR: implemented in cf25c981 ("[US-400] feat: the git-mode guard covers the release scripts, and the workflows stop masking their modes"), refined in 25b1cc21:

Added AC What shipped
AC9 — the guard covers scripts/workflows/release/ too MUST_BE_EXECUTABLE.releaseDir added; requiresExecutableBit accepts *.sh directly in it (not fixtures/**), and readSmokeTestsIndex now passes that pathspec to git ls-files — without it the release scripts would have "passed" by never being read
AC10 — no workflow chmods a repo-committed script 9 chmod +x lines removed: release.yml (8), website-preview-deploy.yml (1). A test greps both workflows so a new one cannot creep back
AC11 — a 644 release script fails the guard, named with its git mode 5 tests added over the release tree in cf25c981 (+2 in 25b1cc21 for the message wording); reproduced locally on the head — see LOCAL RED above

rucka added 5 commits August 11, 2026 08:19
…runnable since #368)

- Guard reads `git ls-files -s`, not the filesystem: the mode is a property of
  the commit, so a locally +x file with a 644 commit still fails
- Scope: scenarios/** and run-all.sh must be 100755; lib/utils.sh (sourced) and
  fixtures/README exempt; empty match fails loudly (a renamed folder must not
  silently disable the guard)
- Check-only (ADL 2026-07-31): names file, mode and the chmod command, never fixes
- RED first: the repo assertion failed on coverage-gate.sh (100644) — 19/20 green,
  then 20/20 after `git update-index --chmod=+x`
- Task: T-1 — git-mode guard + the live defect it catches

Refs: #400
…list carries its exclusions

- scenario_state/file_mode (lib/utils.sh): the state is decided BEFORE execution,
  so "cannot run" is never reported as "ran and failed" again
- NOT EXECUTABLE names the file, its mode and the COMMIT-level remedy; check-only,
  the runner never chmods (ADL 2026-07-31)
- CI_TESTS moves to lib/ci-tests.sh with CI_EXCLUDED reasons — one copy, audited:
  every scenario is in the CI list or excluded WITH a reason (AC7 executable)
- Rule: CI runs every offline-safe scenario. Added registry-exclude.sh (0.8s) and
  no-dataset-in-artifacts.sh (12s), both measured passing offline; removed
  default-resolution.sh, which declares OFFLINE_SAFE=false yet sat in the CI list
  (same unchecked-claim shape as the 644 mode) — it still runs in the local suite
- scenarios/runner-outcomes.sh asserts the three states, the mode in the message,
  check-only, and the list invariants (shell surface -> smoke test, ADL 2026-07-13)
- Demonstrated end-to-end on fixtures: PASS / NOT EXECUTABLE (mode 644) / FAIL /
  MISSING, four distinct rows, runner exit 1
- Task: T-2 — runner outcome vocabulary + documented CI list

Refs: #400
…lready replaced

- First execution ever of this scenario (born mode 644 in #368) found two stale
  assertions, both about the ratchet's command plan:
  1. it demanded `config --add remote.origin.fetch`, while the module deliberately
     uses the TRANSIENT `git -c remote.origin.fetch=<refspec>` and its unit tests
     assert `config --add` is NOT used — the scenario asserted the opposite of the
     decided behavior
  2. it refuted the bare substring `git checkout`, which also matches the restore
     step's `git checkout -- <configPath>` (reverts one file, switches nothing)
- Realigned to the decided behavior, with sharper refutations: no `git checkout
  chore/`, no `git switch`, no `git branch`, and the refspec must not be persisted
- Evidence for the story: an assertion nobody executes is not an assertion
- Task: T-3 — the scenario the mode fix brought back to life

Refs: #400
…build

- New `smoke` job (checkout -> pnpm -> install -> run-all.sh --ci --cleanup), its
  own status check: a red here means a real-CLI scenario broke, a different
  diagnosis from a red unit/lint gate
- Measured cost recorded NEXT TO the job with its revisit threshold (AC6):
  1m58s for 18 scenarios vs ~4 min for `build` => runner time, zero wall-clock;
  if it approaches `build` it gets scoped or scheduled, deliberately
- Check-only (AC8): no chmod, no commit from CI; runner-outcomes.sh asserts the
  workflow runs the suite, records the cost/threshold, and never chmods
- No secrets used => a fork PR is a full-strength run
- Task: T-4 — the CI job

Refs: #400
…orkflow runs it" claims

- ADL 2026-08-11-smoke-suite-runs-in-ci-pre-merge: nightly rejected on measured
  data, four outcomes, git-mode guard, check-only, CI_TESTS as a rule with
  recorded exceptions (+ the alternatives that were rejected and why)
- assert_pinned_bug's reach note: a pinned assertion in the CI list is now a CI
  guarantee, manual only outside it
- marketplace ADL (#277): dated update — registry-exclude.sh IS re-run per change
- way-of-working gate registry: the CI-safe list runs on every PR as `smoke`
- smoke README: outcomes table + the "which scenarios run in CI" rule
- ci.yml: measured cost refined to 1m40s-1m58s over two full local runs
- Verification: run-all.sh --ci exits 0, 18/18 PASS (AC5)
- Task: T-5 — documentation + decision record

Refs: #400
@rucka rucka added risk:yellow Classification: medium risk tier pr-state:to-be-reviewed labels Aug 11, 2026
@rucka

rucka commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict

risk:yellow · cost:greenCHANGES-REQUESTED — the new smoke job is red on this PR's own head commit (it never reaches a single scenario), so AC1/AC2/AC5 are unmet in the only environment that matters.

Open findings: 11. (12 raised, 1 accepted by design.)

PR: #424 · Author: gianlucacarucci5 · Reviewer: independent review agent · Date: 2026-08-11 · Story: US-400 · Type: bug fix + feature

Tier requirements (🟡, quality-model §4): 1 reviewer · SLA 1 working day · standard checklist · reviewer approval; no explicit-approval rule (that is 🔴 only). Review enforcement: disabled in way-of-working.md, so this verdict is advisory — but the merge is independently blocked by the failing smoke CI check (mergeable_state: unstable).

Classification matrix — per dimension
Dimension Tier Source Note
Service/domain criticality yellow KB default tech/risk-matrix.md declares no ## Criticality Table → Medium default (D21)
Change/diff risk yellow diff footprint Shared CI config (ci.yml) + the smoke runner every scenario sources; no production code path touched
Business impact yellow subdomain class Integration & Process Standardization (Supporting) — repo tooling
Security relevance green review read of the diff No secrets in the job; pull_request trigger (not pull_request_target); fork-safe
Coupling balance green integrations touched No new cross-context integration; the CI list moves to one sourced file (fewer copies, not more)

Tier = max(assessed) = yellow, confirming the story's refinement-time tier (no raise). Review value is a floor (D17): confirmed, not lowered.

Assessments

Security — Input validation

Verdict: green — the only new externally-influenced input is the git index text parsed by parseGitIndexEntries, and it is parsed positionally (tab separator), never interpolated into a command.

Details
  • readSmokeTestsIndex() uses execFileSync with an argv array (no shell), so a path cannot break out.
  • file_mode / scenario_state quote every expansion.
  • smoke job runs on pull_request (never pull_request_target), so a fork's code cannot reach repository secrets.

Security — Output handling

Verdict: green — output is console text and a markdown report; no HTML/SQL sink.

Details
  • The report row | $name | 🚫 NOT EXECUTABLE (mode $mode) | interpolates a filename into markdown appended to $GITHUB_STEP_SUMMARY; the values come from basename of a repo path, so the blast radius is a malformed table cell, not injection.

Security — Authentication

Verdict: green — not applicable; the job authenticates to nothing and consumes no token.

Security — Authorization

Verdict: yellow — the smoke job executes shell code from the PR diff with the workflow's default GITHUB_TOKEN permissions (no permissions: block anywhere in ci.yml).

Details

Fork PRs get a read-only token by GitHub's own rule, so the exposure is limited to same-repo branch PRs. It is nonetheless a new job that runs arbitrary shell from the branch under review; permissions: contents: read on the job is a one-line least-privilege fix (finding M5). Pre-existing pattern — build does the same — so this is surfaced, not blocking.

Security — Introduced vulnerabilities

Verdict: green — 0 introduced, 1 pre-existing (the missing permissions: block, inherited from the workflow).

Details
Severity Category File:location Introduced / pre-existing Recommendation
P3 A01 Broken Access Control (least privilege) .github/workflows/ci.yml:200 pre-existing pattern, extended to a new job Add permissions: contents: read to the smoke job

No secret is read, written or logged by the new job; gitleaks (secret-scan) is green on this head.

Cost

Verdict: cost:green — CI runner minutes only; ~2 min of parallel runner time per PR, no new paid service, no infra.

Details
Signal Class Provider Note
New always-on CI job green GitHub Actions Parallel to build (~4 min), so no added wall-clock — but see M1: the recorded figure excludes the CLI build a clean checkout must do, so the real number is not yet measured
Storage / egress green --cleanup removes the temp dir; no artifact upload

Architecture (Coupling)

Verdict: green — the change reduces coupling: the CI list stops being a second copy inside run-all.sh and becomes one sourced file with an executable invariant.

Details
  • Integration strength: low (file-level source, same repo, same team).
  • The one new coupling is run-all.shlib/utils.sh (the runner now shares a namespace with the library every scenario sources). Acceptable — it is what makes scenario_state a single decision point — but a future function-name collision in utils.sh would silently override a runner function.
  • The mode guard couples a @pair/dev-tools unit test to a scripts/ path (MUST_BE_EXECUTABLE.tree); the "index carries no runnable file ⇒ fail loudly" branch is the right mitigation for a rename.

Details

Findings by severity

Critical (must fix before merge)

  • .github/workflows/ci.yml:200-223The smoke job fails on this PR's own head commit (f25abc32, run 31466479347, job 93700317815, 25s, conclusion failure), and it fails before any scenario runs. The job does no build; run-all.sh then falls back to pnpm --filter @pair/pair-cli build (run-all.sh:71), which does not build workspace dependencies, so tsc -b tsconfig.build.json dies with ~40× TS2307: Cannot find module '@pair/content-ops'. The packaging preflight then reports expected build outputs at apps/pair-cli/dist not found, and the runner exits 1 at Error: No usable CLI binary found — before $REPORT_FILE is even written, so nothing is appended to the job summary either. Impact: AC1 (a smoke job executes run-all.sh --ci), AC2 (a pinned-bug flip surfaced by CI) and AC5 (run-all.sh --ci exits 0 on a clean checkout) are unmet; the suite is still not running in CI. Note the job comment at ci.yml:219-221"The runner builds/packages the CLI itself … so no build step is needed here" — is exactly the kind of unverified claim this story exists to delete, and it is false on a fresh checkout (the build job only survives because root pnpm build = turbo build builds dependencies first). Recommendation: add a dependency-inclusive build to the job, e.g. - name: Build the CLI / run: pnpm --filter=@pair/pair-cli... build (note the trailing ...), and/or fix the runner's own fallback at run-all.sh:71 to pnpm --filter @pair/pair-cli... build so a clean local clone self-heals too; then re-run and confirm the job is green, reaches 18/18, and writes the report to the step summary.

Major (should fix before merge)

  • .github/workflows/ci.yml:180-187AC6's recorded cost is not the CI cost. 1m40s–1m58s was measured locally against a workspace that already had apps/pair-cli/dist/cli.js; a clean CI checkout must build the CLI chain first (that is the Critical above). Once the build step exists, the job's duration — and therefore the "adds runner time and ZERO wall-clock" conclusion and the revisit threshold tied to build — must be re-derived. Recommendation: re-measure from the first green smoke run on this PR and update both the MEASURED COST block and the ADL's "≈2 minutes against a 4-minute build" sentence with the real number.
  • scripts/smoke-tests/scenarios/runner-outcomes.sh:180The "CI never chmods" assertion cannot detect the case it targets. grep -Eq '^[[:space:]]*[^#[:space:]].*\bchmod\b' consumes the first non-space character with [^#[:space:]], so on a line whose first token is chmod the remainder is hmod +x … and \bchmod\b never matches. Verified: a run: | block containing chmod +x scripts/foo.sh is NOT detected, and the scenario happily logs runner never chmods (check-only). That is precisely the release.yml shape the comment three lines above calls out — a guard that passes because of how it looks, on a PR whose whole thesis is guards that do not guard. Recommendation: if grep -vE '^[[:space:]]*#' "$CI_WORKFLOW" | grep -q 'chmod'; then … — verified against this branch: 0 matches on the real ci.yml (still green, the doc comments start with #) and DETECTED on the fixture above.

Minor (consider)

  • scripts/smoke-tests/scenarios/runner-outcomes.sh:2 — the new scenario declares # OFFLINE_SAFE=true as a comment, but every reader of that flag greps ^OFFLINE_SAFE= (run-all.sh:235 is_offline_safe, and runner-outcomes.sh:126,135 itself). It passes today only via the implicit default-true. Concretely: if someone later flips it to # OFFLINE_SAFE=false, the CI-list invariant at line 135 will not see it and the scenario stays in CI_TESTS — the identical "unchecked declaration" defect the story removes from default-resolution.sh. Recommendation: use the bare form the other scenarios use (registry-exclude.sh:4): OFFLINE_SAFE=true on its own line.
  • packages/dev-tools/src/quality-gates/smoke-scenario-modes.ts:150git ls-files -s quotes paths containing non-ASCII bytes: a scenario named café.sh is emitted as "scripts/smoke-tests/scenarios/caf\303\251.sh" (verified locally). The leading " makes requiresExecutableBit() return false, so such a file committed 100644 is silently exempt — the exact "passes because it saw nothing" hole the module's own docblock is written against. Recommendation: execFileSync('git', ['-c', 'core.quotePath=false', 'ls-files', '-s', '--', tree]), or -z with a NUL split.
  • scripts/smoke-tests/scenarios/runner-outcomes.sh:86 — the sibling check-only guard over the runner uses a different regex (^[[:space:]]*chmod\b) for the same invariant, so … && chmod +x "$script" or then chmod … evades it. Two spellings of one rule will drift. Recommendation: extract one helper (e.g. assert_no_executed_chmod <file…>) and use it for both the shell files and ci.yml.
  • .github/workflows/ci.yml:206-218 — the pnpm bootstrap (action-setup + corepack fallback + pnpm install) is copy-pasted from build, so the pinned 10.15.0 now appears in four places in one file; a pnpm bump silently half-applies. Recommendation: extract a composite action (.github/actions/setup-pnpm) or accept as tracked debt.
  • .github/workflows/ci.yml:200 — the new job executes shell taken from the diff under review with the workflow's default token permissions (no permissions: block in this file). Recommendation: add permissions: / contents: read to the smoke job (least privilege, one line); the same for build can follow separately.
  • .github/workflows/release.yml:93,98,121,138,145,153,188,225chmod +x before executing each release script masks the committed mode, the exact habit that hid this story's bug. Reviewer note 4 of the PR calls it out and files nothing, so it dies with the PR. Verified: all of scripts/workflows/release/*.sh are already committed 100755, so removing the eight chmod lines is mode-neutral, and extending MUST_BE_EXECUTABLE to cover scripts/workflows/release/ is a two-line change to the guard plus a test. Recommendation: do it here (it is small and provably safe) — or file the follow-up story explicitly instead of leaving it as prose in a PR description.
  • packages/dev-tools/src/quality-gates/smoke-scenario-modes.ts:100.map(line => line.trim()) trims the whole index line, so a path ending in a space is reported (and matched) without it; the printed git update-index --chmod=+x <path> remedy would then not name the real file. Nit — the CR-stripping benefit probably outweighs it; slicing the path after the tab before trimming would give both.

Questions

  • scripts/smoke-tests/scenarios/coverage-gate.sh:305-330 — existing-test modification, correctly flagged for scrutiny by the PR. Independently verified: the module builds the transient form (coverage-baseline-ratchet.ts:649['-c', 'remote.origin.fetch=' + ratchetTrackingRefspec(remote)], :492), and coverage-baseline-ratchet.test.ts:547 asserts config --add remote.origin.fetch is never used — so the old smoke assertion demanded the opposite of the decided, unit-tested behaviour, and the new one matches it. Same for the git checkout refutation: the module's restore step legitimately runs ['git','checkout','--',configPath] (:675), which the bare substring forbade; the three replacement refutations (git checkout chore/, git switch, git branch) are narrower and I found no string in the module's command plan that would trip them. My read: the change is right. Per the project's TDD rule this still wants an explicit human ack — that is a call for the maintainer, not for me.
  • .pair/adoption/decision-log/2026-08-11-smoke-suite-runs-in-ci-pre-merge.md (accepted by design — enforcement is an admin-scope human step)smoke is a declared, not required, status context, so it blocks nothing until branch protection is written. Recorded in the story's Team Concerns and in the ADL's Consequences as a deliberate gap, consistent with way-of-working.md Review enforcement: disabled. Reported for transparency; not treated as blocking.
Positive feedback
  • The RED-first evidence is real, not narrated: the guard's own assertion failed on coverage-gate.sh (mode 100644) before the fix, and the defect it found was a live one that had survived weeks.
  • Reading git ls-files -s instead of test -x is the correct call and is argued from first principles (the mode is a property of the commit) in both the module docblock and the ADL.
  • checkSmokeScenarioModes failing loudly when the index carries no runnable file is the detail that separates a guard from decoration — most implementations would have returned ok: true on an empty match.
  • Turning AC7 into an executable invariant (runner-outcomes.sh §4: every scenario is in CI_TESTS or in CI_EXCLUDED with a reason, both directions) is strictly better than the prose the AC asked for.
  • The coverage-gate.sh assertion realignment is flagged in the PR description with the exact TDD rule it touches, rather than slipped in — that is how an existing-test change should arrive.
  • CI_EXCLUDED carrying a per-entry reason, and the membership deltas (default-resolution.sh out because it declares itself offline-unsafe) being derived from a stated rule rather than taste.
Functionality & requirements (AC coverage)
AC Verdict Evidence
AC1 — smoke job runs run-all.sh --ci parallel to build, own status check not met Job exists and is parallel, but it fails before running any scenario on head f25abc32 (Critical)
AC2 — a pinned-bug flip is surfaced by CI unverified Cannot be demonstrated while the job dies at the CLI build; the story's own "one deliberate red" run has not happened
AC3 — NOT EXECUTABLE distinct from FAIL and MISSING, naming file + mode run-all.sh:255-271 + lib/utils.sh:36-51, asserted by runner-outcomes.sh:55-97
AC4 — executability asserted by git mode, not test -x smoke-scenario-modes.ts (git ls-files -s), 20 tests; caveat M4 (non-ASCII quoting)
AC5 — coverage-gate.sh is 100755 and --ci exits 0 on a clean checkout ⚠️ half Mode fixed (100644 → 100755, verified in the index); "exits 0 on a clean checkout" is false today — see Critical
AC6 — measured duration recorded next to the job + revisit threshold ⚠️ Recorded and well argued, but measured without the build a clean checkout needs (Major M1)
AC7 — exclusions documented with reasons lib/ci-tests.sh CI_EXCLUDED + the executable invariant in runner-outcomes.sh:116-154
AC8 — nothing auto-corrected in CI ✅ (weakly guarded) No chmod in runner/workflow/guard; the assertion protecting it has a hole (Major M2)

Edge cases: MISSING, NOT_EXECUTABLE, all-755, symlink mode 120000, empty index, nested scenario folder and a path with an interior space are all covered by fixtures. Invariant coverage of the list is complete: 21 scenarios = 18 CI_TESTS + 3 CI_EXCLUDED, all committed 100755 (verified against the index).

Testing & quality gates
  • Gate (mechanical, the first filter): RED. On head f25abc32: secret-scan ✅ success · build ✅ success · smokefailure. A red gate caps this review — APPROVED/TECH-DEBT are unreachable and the state can never synthesize to ready-to-merge, whatever the judgment says.
  • 🟡 tier gate set = lint + type + build + unit: all inside the green build job. The new smoke check is additional, and it is the one that is red.
  • Unit coverage of the new module is genuinely good (20 tests: parser, predicate, offender list, message content, empty-index failure, and a real-repo assertion). The parser tests pin behaviour rather than implementation.
  • Shell surface is verified by a smoke scenario rather than a unit test, per ADL 2026-07-13 — the right split.
  • Not verified by me: the "18/18 PASS locally" claim (the only CI run never reached a scenario). That is the point of the story, so it should be re-stated from a green CI run, not from a laptop.
Adoption compliance
  • Degradation level 2 (/verify-adoption reasoning applied inline; /assess-stack not invoked — the diff adds no dependency, so there is nothing to resolve against tech-stack.md).
  • Gate-tooling ADL (2026-07-13): respected — the guard is a tested module in @pair/dev-tools, its enforcement point is its own unit test inside pnpm test, no new CLI entrypoint or pipeline step.
  • Check-only ADL (2026-07-31): respected in the implementation (no chmod in runner, workflow or guard; the message names the remedy for a human to run).
  • Decision record present and complete: .pair/adoption/decision-log/2026-08-11-smoke-suite-runs-in-ci-pre-merge.md (Date / Status / Category / Context / Decision / Consequences / Alternatives / References) — matches the sibling ADL shape; no missing-ADR HALT.
  • way-of-working.md gate registry row updated; the superseded claim in the marketplace ADL (Skill marketplace: marketplace.json + plugin packaging for native Claude Code install #277) is corrected by a dated > Update rather than an edit-in-place — the right treatment for a historical record.
Tech debt
  • CI bootstrap duplication in ci.yml (pnpm setup × 2 jobs, version pinned 4×) — low effort, medium recurrence.
  • Two different regex spellings of the same "no executed chmod" invariant (runner-outcomes.sh:86 vs :180) — the second is already broken; unify before a third appears.
  • release.yml's eight chmod +x lines and the guard's scope stopping at scripts/smoke-tests/ — the story's own lesson, unapplied one directory over.
  • run-all.sh continuing past a failed CLI build with only Warning: turns a build failure into a confusing "No usable CLI binary found" 100 lines later; fail-fast would have made the Critical above obvious in one line.
  • None of these block; surfaced for deliberate promotion.
Documentation
  • scripts/smoke-tests/README.md — outcomes table + the CI-membership rule; accurate.
  • ADL recorded and cross-linked; way-of-working.md registry row updated.
  • assert_pinned_bug's reach note updated — correctly scoped ("CI guarantee inside the list, manual gate outside").
  • ci.yml:219-221 ("no build step is needed here") is wrong and must change with the Critical fix; the MEASURED COST block and the ADL's "≈2 minutes" both need the post-fix number.
Performance & deployment

Process notes (independent review): delivered as a PR comment rather than a native review event — a different actor from the author, with self-approve blocked; the artifact is the same code-review template body. pair-review status not published by this run (advisory — Review enforcement: disabled; the merge is in any case blocked by the red smoke check). pr-state label not applied: the repository provisions only pr-state:to-be-reviewed, so pr-state:not-approved does not exist to set — non-blocking, reported per the degradation rule. No merge, no branch mutation: the human merge gate is unaffected.

rucka and others added 11 commits August 11, 2026 23:25
… .trim() rewrote paths

parseGitIndexEntries took the mode as slice(0, indexOf(' ')): on a line with a
tab but no space that is slice(0, -1) — the line minus its last char — reported
as a non-executable file with a nonsense mode. Now the mode comes from the
pre-tab segment and an unreadable line is dropped, not diagnosed.

The blanket .trim() also rewrote a path with a trailing space, so the chmod
command in the failure named a file that does not exist.

Doc claim corrected too: git ls-files -s reads the INDEX, so the guard asserts
the STAGED mode, not 'the commit' (identical in CI; not identical locally).
…as only grepped

The job ran pnpm install then run-all.sh, whose fallback is
'pnpm --filter @pair/pair-cli build' — that bypasses turbo's dependsOn graph, so
@pair/content-ops (exports -> dist/) is never built and the CLI compile dies with
~45 TS2307. Nothing packaged, no scenario ran, exit 1. Now the job builds
'--filter=@pair/pair-cli...' explicitly, and the runner's own fallback does too.

runner-outcomes.sh now EXECUTES run-all.sh against a fixture scenarios/ dir + CI
list (pass, fail, 644, listed-absent) and asserts the four report rows, exit 1,
that the 644 fixture is untouched, and that both halves of the remedy name ONE
path. Previously it grepped the runner's source, so a token in a comment passed.

Also: file_mode chain terminated (it could abort the suite under set -e);
00-create-install-package.sh is fatal on a CI dry-run failure instead of a green
no-op, and reuses the preflight's artifact instead of packaging twice; the ci.yml
chmod guard is scoped to smoke-test paths; smoke job gets contents:read + 15m.
… observed in CI

The stated rule ('CI runs every scenario that declares OFFLINE_SAFE=true') was
stricter than the code: is_offline_safe defaults to true and only
'^OFFLINE_SAFE=false' opts out — while 5 scenarios declared it in a comment and 5
declared nothing, yet all ran. Now stated as enforced (opt-OUT), and the 5
decorative '# OFFLINE_SAFE=true' comments are real declarations.

AC6 figure replaced with the measured CI number: smoke 1m53s vs build 3m59s in
run 31538360697 — threshold arithmetic restated against it (~75% of build = ~3m).

'as committed' -> 'staged/index' wherever git ls-files -s is described, and the
default-resolution exclusion no longer claims release.yml covers the no---source
fallback: smoke-test-npm-artifact.sh Test B WARNS on failure, so it is evidence,
not a gate — the fallback stays a manual guarantee.
…o task

`test` is cacheable with package-scoped inputs, so a scenario added mode 644
without touching @pair/dev-tools replays a cached PASS and the guard never
runs — the pre-push guarantee the module doc claimed did not hold. Give it the
thin CLI the sibling gates already have (`smoke-modes:check`), wire it into the
root `quality-gate` chain and into its own CI `build` step; both run
unconditionally.

Also narrows the predicate to what the runner actually globs (`*.sh` directly
in `scenarios/`) so a data fixture next to the scenarios is not told to
`chmod +x` a JSON file, and reads the index with `-z` so a non-ASCII path is
not C-quoted into a remedy that matches no file.

Refs: #400
…e they held

`repo_relative_path`'s `${REPO_ROOT:-}` guard was decorative: with the root
empty the pattern degenerates to `/*`, matches every absolute path and strips
the leading slash, so the NOT EXECUTABLE remedy printed
`chmod +x Users/x/repo/...`. Now the unknown-root case falls through to the
verbatim branch, asserted both ways.

The check-only tripwire matched `chmod` only as a line's FIRST word, so
`... && chmod +x`, `... || chmod +x` and `$(chmod ...)` all passed it; it now
matches any command position while still exempting the runner's PRINTED
remedy. Section 5 greps the workflows DIRECTORY instead of pinning
`ci.yml`, so relocating the smoke job (as #413 does for `format`) is a
refactor rather than a red claiming the suite is not wired into CI. It also
now asserts — and EXECUTES — the guard's uncacheable entrypoints.

Refs: #400
…contain

Answers the review's open question. The artifact's contract is `bin/pair-cli`
— executed, never imported — so bundled declarations are optional; recorded as
ADL 2026-08-12-manual-cli-artifact-types-are-optional.md.

But `package.json` was written with `types: bundle-cli/index.d.ts`
UNCONDITIONALLY, before the generator ran, so every dts-bundle-generator
failure shipped a dangling pointer. Test-first: the packaging scenario now
asserts `types` and the file agree in BOTH directions — red on this tree
("declares types=... but bundle-cli/index.d.ts is not in the artifact"), green
after pruning the field when the file is absent.

The generator stays best-effort, and its message now states the intent instead
of reading like a swallowed failure — since #400 it prints inside a green CI
row on every PR.

Refs: #400
… workflows stop masking their modes

Absorbs #431 into this story per ADL
`2026-08-12-implementation-never-files-a-card-it-extends-the-story.md`.

This story exists to stop committed file modes being masked, and its guard covered
`scripts/smoke-tests/` only — while `release.yml` ran
`chmod +x scripts/workflows/release/<script>.sh` before executing each of eight
scripts, and `website-preview-deploy.yml` a ninth. A release script committed 644
would therefore run green in CI forever and fail for anyone executing it directly:
exactly the condition `coverage-gate.sh` sat in for weeks.

- `MUST_BE_EXECUTABLE.releaseDir` added; `requiresExecutableBit` applies ONE rule to
  both folders — a `*.sh` directly inside is executed, anything deeper or of another
  extension is read
- the nine `chmod +x` lines are gone from both workflows
- `readSmokeTestsIndex` now reads BOTH trees. Extending the predicate alone was not
  enough and is worth recording: the repo-level check reads that index, so a release
  script staged 644 stayed invisible and the guard passed — verified by injection,
  which was GREEN until this read was widened. A guard that passes by not looking is
  the failure this module's own empty-index branch exists against.
- tests cover the load-bearing negatives too: `README.md` and
  `fixtures/sample-project/{index.js,package.json}` are committed 644 ON PURPOSE and
  must not require the bit — demanding it would be a real regression
- one test asserts no workflow chmods a committed release script again

All nine release `.sh` are already 100755, so the widened guard is green on arrival:
this closes the hole before it is used.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The PR body and story #400 justified absorbing #431 by citing
`2026-08-12-implementation-never-files-a-card-it-extends-the-story.md`, a
file that existed in no commit — untracked in the author's checkout, so the
sole authority for the scope extension was unreadable at the merge gate.

Records it here (template order + Adoption Impact), plus the way-of-working
policy bullet next to "one PR per story". The References section says
plainly that the enforcement point — the `implement-batch` reviewer/fixer
prompts — lands with the orchestrator's own change, not with this PR.

Refs: #400
Since the guard grew to cover `scripts/workflows/release/` (#431), its failure
still said "N smoke-test file(s)" and "the runner cannot execute them: the
scenario is listed" — a mechanism that does not exist for release scripts, and
which hides the real consequence (it runs in CI only because a workflow chmods
it, and fails for everyone else). The saw-nothing failure named only the
scenarios tree, so a renamed release folder would have disabled half the guard
silently.

Test-first: two cases red (a release offender must not be called a smoke-test
file; the saw-nothing message must name every guarded tree), then the wording.
35/35 green.

Refs: #400
… bans

The ADL committed in round 2 is Active and bans an implementing/reviewing
agent from creating work items; its own Adoption Impact item 3 named
/pair-process-review's deferral instruction as the enforcement point and
left it unmet. Phase 4.2 Step 5 still said "deliberate promotion after
review via /write-issue (with the tech-debt label)" — the exact line that
produced #426-#431 — so the policy would have shipped next to the
instruction violating it.

Test-first: implementation-never-files-a-card.test.ts (cross-cutting, per
2026-07-18-conformance-test-per-file-not-per-story) pins BOTH copies —
dataset source + installed mirror — on: no write-issue reference anywhere,
the ban stated in the tech-debt step, an already-existing card citable, the
merge gate named as the alternative, and the ADL bullet no longer left open.
7/8 red before the edit.

Step 5 now: record the debt in the report and stop; cite an existing card by
number, otherwise fix it in this PR or leave it as an actionable finding for
the maintainer at the merge gate. ADL item 3 records that it landed here.
@rucka

rucka commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

In response to #issuecomment-5250058462 (the first review, CHANGES-REQUESTED, 11 open findings).

Remediation — final synthesis (review converged, head 7d30b9a7)

This is the durable audit of the full review↔fix cycle for US-400 / PR #424 across all rounds (log: .pair/working/reviews/400.md, now removed per the closing instruction — this comment is its replacement record).

Round 1 (head d66abf36) — 15 findings from the first review, all resolved:

  1. [Critical] smoke job failed on its own head; no scenario ran (ci.yml:200-223) → added dependency-aware pnpm turbo build --filter=@pair/pair-cli... before the suite; fixed the same gap in run-all.sh's fallback and its "No usable CLI binary" hint. Verified green: run 31538360697, 18/18 PASS. — c8bd245c
  2. [Major] AC6 figure was measured on a job that never completed (ci.yml:180-192, ADL) → replaced with the observed CI number (1m53s vs 3m59s build, ~47%), threshold restated against it. — 0dc43057
  3. [Major] no empirical CI run; no deliberate red → both produced (green 31538360697; deliberate red 31538595395 on scratch branch smoke-deliberate-red-us400) — evidence only, no code change beyond Setup & Project Management Integration #1.
  4. [Minor] 00-create-install-package.sh WARN + exit 0 → silent green no-op → made FATAL under IS_CI=true/CI; graceful skip kept only for a developer missing local tooling. — c8bd245c
  5. [Minor] runner-outcomes.sh grepped the runner's source instead of executing it → section 3 now executes run-all.sh against a fixture scenarios dir + CI list and asserts the report rows verbatim. — c8bd245c
  6. [Minor] repo-wide chmod tripwire on shared ci.yml → scoped to non-comment chmod lines mentioning scripts/smoke-tests. — c8bd245c
  7. [Minor] parseGitIndexEntries invented a mode; .trim() rewrote paths → test-first (3 new cases), fixed with line.slice(0, tab).split(' ')[0] + .filter(line => line !== ''). 23/23 green. — 12211d04
  8. [Minor] "as committed" overreached — reads the INDEX, not HEAD → reworded to "staged / tracked" everywhere (module doc, interface docs, failure message, README, ADL); index chosen deliberately, documented why. — 12211d04, 0dc43057
  9. [Minor] stated OFFLINE_SAFE rule stricter than enforced → restated as opt-out in README, lib/ci-tests.sh, runner-outcomes.sh, ADL; 5 decorative comments turned into real declarations. — 0dc43057
  10. [Minor] NOT EXECUTABLE remedy printed two disagreeing paths → single repo_relative_path helper, byte-identical in both halves (asserted by fixture). — c8bd245c
  11. [Minor] file_mode's unterminated stat chain could kill the reporter under set -e → terminated with || echo unknown. — c8bd245c
  12. [Minor] 00-create-install-package.sh packaged the CLI twice per run → short-circuits when $TMP_DIR/packaged-cli already exists. — c8bd245c
  13. [Minor] no permissions:/timeout-minutes: on a job running PR-authored code → added permissions: {contents: read} + timeout-minutes: 15, scoped to this job. — c8bd245c
  14. [Minor] stray mid-sentence line break in the MEASURED COST comment → block rewritten around the CI figures. — 0dc43057
  15. [Questions] default-resolution.sh exclusion reason overstated release.yml's coverage → ADL + CI_EXCLUDED reason now name the script/Test B and state the no---source fallback is manual until Test B is fatal. — 0dc43057

Not changed (round 1): none escalated.

Round 2 (head cf25c981) — 2 Major findings, both about the record:

  1. [Major] PR body called absorbed Git-mode guard covers smoke scenarios only — release.yml chmods its scripts before running them, masking their committed modes #431 work "not yet implemented", omitted 5 changed files, cited CI evidence 4 commits behind head → PR body republished from current head (mechanism per AC, complete file list, CI re-pointed to run 31616635648). — no code change; .pair/working/checkpoints/400.md updated.
  2. [Major] cited ADL 2026-08-12-implementation-never-files-a-card-it-extends-the-story.md existed in no commit → ADL committed in template order, paired with a way-of-working.md policy bullet. — 41fdf075

Not changed (round 2): none escalated. Found and fixed while fixing (not a review finding): the mode-guard failure text called a release-tree offender a "smoke-test file" and named a runner mechanism that doesn't exist for release scripts — test-first, 2 cases red→green, 35/35. — 25b1cc21

Round 3 (head 25b1cc21) — 1 Major finding:

  1. [Major] Active ADL bans filing cards while /pair-process-review SKILL.md:218 still routed debt to /pair-capability-write-issue → instruction rewritten identically on both copies (dataset source + installed mirror); new cross-cutting conformance test implementation-never-files-a-card.test.ts (8 cases, 7 red before the edit, 8 green after); ADL § Adoption Impact item 3 now names the enforcement point. — 7d30b9a7

Not changed (round 3): none escalated.

Final independent re-review (head 7d30b9a7) — 10 findings, all below the severity floor (Major) for this tier, so none block; carried to the merge gate unfixed rather than fixed in a further round:

  1. [Minor] emptiness check is a union of both guarded trees, not per-tree (smoke-scenario-modes.ts:181) → disposition: below severity floor — carried to the merge gate unfixed.
  2. [Minor] chmod-tripwire comment still says release.yml chmods its own scripts, contradicting this PR's own removal of those chmods (runner-outcomes.sh:313) → disposition: below severity floor — carried to the merge gate unfixed.
  3. [Minor] packaging preflight bypasses the new scenario_state vocabulary, misdiagnosing a 644 packaging script without a prebuilt dist (run-all.sh:199-209) → disposition: below severity floor — carried to the merge gate unfixed.
  4. [Minor] new conformance guard's only caller is a cacheable turbo task with no matching inputs, so a re-added /pair-capability-write-issue reference could replay a cached PASS (implementation-never-files-a-card.test.ts:24) → disposition: below severity floor — carried to the merge gate unfixed.
  5. [Minor] ADL's stated guard scope omits the release tree it also covers (2026-08-11-smoke-suite-runs-in-ci-pre-merge.md:33) → disposition: below severity floor — carried to the merge gate unfixed.
  6. [Minor] pre-existing malformed markup escaped rather than repaired in an unrelated formatter pass (way-of-working.md:57) → disposition: below severity floor — carried to the merge gate unfixed.
  7. [Minor] Step 6 / verdict table still say "track separately" with the actor unnamed, readable as contradicting the Step 5 ban (pair-process-review/SKILL.md:219,241) → disposition: below severity floor — carried to the merge gate unfixed.
  8. [Minor] comment claims --binary skips the packaging preflight; it doesn't — the fixture scenarios dir just has no packaging script (runner-outcomes.sh:105) → disposition: below severity floor — carried to the merge gate unfixed.
  9. [Minor] AC6 headroom figure in ci.yml is already ~15-20% optimistic versus the current head's run, three numbers in circulation (ci.yml:187-200) → disposition: below severity floor — carried to the merge gate unfixed.

Final verdict: review clean. Zero actionable findings on the latest independent re-review (head 7d30b9a7); the 10 items above are Minor/below-tier-floor and are recorded here as the merge-gate carry list, not as blocking work. All 18 prior findings across rounds 1-3 are resolved with the commits cited above; nothing was silently dropped.

Quality gates: PASS at every round (full suite — ts:check, all package tests, lint, format:check, mdlint, gate:composition, hygiene:check, smoke-modes:check, docs:staleness, skills:conformance, dup:check, turbo build) plus CI green on each round's head (31538360697, 31616635648, 31621079558).

→ No further remediation round required; ready for the merge gate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-state:to-be-reviewed risk:yellow Classification: medium risk tier

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant