Skip to content

feat(#114): CI grounding lint — plan paths + PR-body refs#121

Merged
Knapp-Kevin merged 8 commits into
devfrom
feat/114-grounding-lint
May 5, 2026
Merged

feat(#114): CI grounding lint — plan paths + PR-body refs#121
Knapp-Kevin merged 8 commits into
devfrom
feat/114-grounding-lint

Conversation

@Knapp-Kevin

Copy link
Copy Markdown
Collaborator

Summary

  • Adds scripts/lint_plan_grounding.py (212 LOC, blocking) — fails CI if a plan-*.md file references a filesystem path that doesn't exist on the working tree, unless the path is marked **new** / (planned) / (future) / (v2) / (nonexistent) / (example). Exemption markers cover the legitimate cases where a plan deliberately proposes a not-yet-existent path.
  • Adds .github/scripts/lint_pr_body_refs.py (162 LOC, advisory) — warns on bare #NUMBER mentions in PR bodies that aren't wrapped with Closes / Fixes / Refs keywords or placed under a ## Linked issues heading. Hardened against OWASP A03 via direct os.environ read (no shell interpolation of user-controlled PR body).
  • Closes the SG-PLAN-GROUNDING-DRIFT loop after 5 instances tracked across recent branches. The lint shipped in this PR is the durable countermeasure that complements the author-time ls -d */ discipline already in use.

Linked issues

Closes #114

Refs #117 (branch is off BicameralAI/dev post-#117 pre-push-hook merge — this PR continues the same drift-defense workstream)

Plan / Audit / Seal

  • Plan: plan-114-grounding-lint.md (v2, content hash sha256:1447b2ad…), committed at 4ea06be.
  • Audit: META_LEDGER Entry v0.4.12.1 — team adapter signature drift hotfix #19, chain hash 850ec57f… — verdict PASS (post-remediation).
    • v1 audit at a5e6a05 was VETO on F-1 (OWASP A03: echo "$PR_BODY" > /tmp/pr-body.md exposed Bash command-substitution injection in CI).
    • v2 at 4ea06be remediated by switching to python ...py --from-env PR_BODY (direct os.environ read, no shell interpreter in the path).
  • Seal: META_LEDGER Entry v0.4.13 — content-addressable dedup (team mode hardening) #20, Merkle hash a19a04de… — Reality matches Promise.

Risk grade: L1 (pure checker scripts + advisory CI workflow; zero production code paths, zero schema migrations, zero MCP tool changes, zero contract changes).

Test plan

  • pytest -q tests/test_lint_plan_grounding.py tests/test_lint_pr_body_refs.py — 14/14 new tests
  • ruff check scripts/lint_plan_grounding.py .github/scripts/lint_pr_body_refs.py tests/test_lint_*.py — clean
  • ruff format --check scripts/lint_plan_grounding.py .github/scripts/lint_pr_body_refs.py tests/test_lint_*.py — clean
  • mypy scripts/lint_plan_grounding.py .github/scripts/lint_pr_body_refs.py — clean
  • Self-test: python scripts/lint_plan_grounding.py plan-114-grounding-lint.md → exit 0 (the plan that builds the lint passes the lint).
  • CI: lint-and-typecheck.yml runs the new Plan-grounding step against this PR's modified plan file.
  • CI: pr-body-refs-lint.yml runs Check B against this very PR body and emits zero warnings (this body uses Closes #114 + Refs #117 + a ## Linked issues section — self-validating).

Phase breakdown

Phase Files New tests Notes
0 — plan-grounding lint (blocking) 1 prod + 1 test 8 Walker + classifier + CLI; pure stdlib; HTML-comment + blockquote skip; multi-word token rejection; glob-char rejection.
1 — PR-body refs lint (advisory) 1 prod + 1 test 6 --body <file> (local/tests) + --from-env <NAME> (CI security path); always returns 0 (advisory).
2 — CI integration 1 modified + 1 new workflow 0 Plan-grounding step only runs on PR-modified plan files (via git diff --name-only) — historical plans don't block unrelated PRs. PR-body workflow uses permissions: pull-requests: read + pull_request (fork-safe) + continue-on-error: true.
3 — Documentation 2 modified 0 DEV_CYCLE.md §2.1 (Check A callout) + §4.3 (Check B PR-body keyword discipline); CHANGELOG.md [Unreleased] entry.

Security note

The --from-env invocation pattern is load-bearing. An earlier draft of this plan used echo "$PR_BODY" > /tmp/pr-body.md in the workflow, which is vulnerable to OWASP A03 command-substitution injection — Bash double-quotes expand $(cmd), and PR body is contributor-editable. The audit caught this at v1 (VETO). The shipped pattern reads PR_BODY directly via os.environ in Python, with no shell interpreter in the path. The historical mistake is documented inline in the workflow YAML and module docstring so the lesson stays load-bearing.

Followups (not in this PR)

  • Post-commit hook bugbicameral-mcp link_commit HEAD is not a registered subcommand; the post-commit hook silently no-ops. Worth its own issue.
  • CLAUDE.md cleanup — the project root CLAUDE.md references pilot/mcp/skills/ which doesn't exist in this repo. Stale doc.
  • DEV_CYCLE.md CI-hardening section — audit suggested capturing the OWASP A03 lesson as a broader CI-hardening principle. The topic deserves more than a one-paragraph callout; track separately if needed.

Knapp-Kevin and others added 5 commits April 29, 2026 17:34
Four phases: Phase 0 — plan-grounding lint (Check A, blocking);
Phase 1 — PR-body refs lint (Check B, advisory); Phase 2 — CI
integration; Phase 3 — DEV_CYCLE.md docs + CHANGELOG.

Six open questions surfaced for audit:

- Q1: CI-only for v1; pre-commit hook deferred (no .pre-commit-config
  infrastructure in repo yet).
- Q2: dynamic discovery of registered packages via ls + __init__.py
  presence (no hardcoded list).
- Q3: Check B advisory (warn-only, never blocks merge).
- Q4: standardised keyword set: Closes/Fixes/Resolves + Refs +
  Related to + See.
- Q5: scripts/ for dev-utility (Check A); .github/scripts/ for
  CI-only (Check B). Mirrors existing precedent.
- Q6: Check A is fast pre-audit catch; doesn't replace audit's
  deeper grounding pass.

Risk grade L1 (pure checker scripts + advisory CI workflow; no
production code paths, no schema, no contracts).

Branched off BicameralAI/dev tip 2e9a842 post-#117. SG-PLAN-GROUNDING-DRIFT
mitigated: ran `ls -d */` and confirmed every package + workflow
path before submission. The plan that builds the lint that prevents
this very pattern.

Self-test exit criterion #5: when the lint lands, this very plan
file must lint clean (zero diagnostics on plan-114-grounding-lint.md).
…rom-env

Audit found:
- F-1 (BLOCKING, OWASP A03): pr-body-refs-lint.yml used `echo
  "$PR_BODY" > /tmp/pr-body.md` which lets Bash double-quote
  interpolation expand $(cmd) substitutions in user-controlled
  PR body text — arbitrary code execution in CI.
- F-2: 6th test needed for the env-var read path.

Remediation:
- Phase 1 main() signature gains `--from-env <NAME>` mutually
  exclusive with `--body <file>`. Direct os.environ read; no
  shell interpreter in the path.
- Phase 2 workflow drops the echo line: `run: python ...py
  --from-env PR_BODY`.
- Phase 1 test count 5 → 6 (added test_main_reads_from_env_var
  verifying the security-critical invocation matches file-mode
  output).
- Razor estimates bumped: lint_pr_body_refs.py 100 → 110 LOC,
  test_lint_pr_body_refs.py 90 → 100 LOC.
- Inline security note in the workflow YAML section explaining
  why we don't use the echo pattern.

The plan that builds the lint that prevents one class of
carelessness (filesystem-grounding drift) had a different class
of carelessness (shell injection) — the audit caught both
classes. Re-audit pending.
GATE TRIBUNAL entry covering both audit iterations:
- v1 (a5e6a05): VETO on F-1 OWASP A03 — `echo "$PR_BODY"` in
  workflow shell exposes command-substitution injection.
- v2 (4ea06be): PASS — workflow command now passes PR_BODY via
  direct os.environ read in Python, eliminating the shell
  intermediate.

Chain hash 850ec57f extends from #18 (#48 SEAL eacc6f89) on dev.

Notable: the plan that builds the lint for one class of
carelessness (filesystem-grounding drift) had a different class
of carelessness (OWASP A03). Audit caught both; QOR
defense-in-depth working as designed.

Plan PASS at 4ea06be; chain to /qor-implement.
Phase 0 — plan-grounding lint (Check A, blocking):
- scripts/lint_plan_grounding.py (212 LOC, 8 tests): walks plan-*.md
  for backtick-wrapped path tokens, verifies each resolves on the
  working tree. Markers (**new**, (planned)/(future)/(v2)/
  (nonexistent)/(example)) exempt the line. Skips HTML comments,
  blockquotes, multi-word tokens, glob patterns.
- .github/workflows/lint-and-typecheck.yml: new step lints only
  plan-*.md files modified in the current PR (git diff against
  base ref). Blocks merge if any modified plan has unresolved paths.

Phase 1 — PR-body refs lint (Check B, advisory):
- .github/scripts/lint_pr_body_refs.py (162 LOC, 6 tests): warns
  on bare #NUMBER mentions outside Closes/Refs/etc keywords or
  Linked-issues section. SECURITY: --from-env PR_BODY reads via
  os.environ directly (no shell interpolation; OWASP A03 mitigation
  per #114 audit v1).
- .github/workflows/pr-body-refs-lint.yml: advisory workflow on
  pull_request: [opened, edited, reopened]. continue-on-error: true.

Phase 2 — CI integration (above).

Phase 3 — docs:
- docs/DEV_CYCLE.md §2.1: plan-grounding lint callout
- docs/DEV_CYCLE.md §4.3: PR-body keyword discipline (Closes/Refs/
  Linked-issues section)
- CHANGELOG.md [Unreleased] entry

Validation:
- 29/29 tests green (8 + 6 + 15 regression on #117/#113)
- Self-test: scripts/lint_plan_grounding.py against
  plan-114-grounding-lint.md exits 0 (clean) — the plan that
  builds the lint passes the lint
- ruff check + format: clean on all 4 new files
- mypy: clean on both new modules
- Razor: scripts/lint_plan_grounding.py 212 LOC; .github/scripts/
  lint_pr_body_refs.py 162 LOC; entry funcs ≤30 LOC; helpers ≤25.

Plan: plan-114-grounding-lint.md (audit v1 VETO at a5e6a05 on
OWASP A03; v2 PASS at 4ea06be after --from-env remediation;
chain bf890347 → 850ec57f). Implementation chains to seal in
/qor-substantiate.

Closes #114
Reality matches Promise. All 7 planned files exist; 14/14 new tests
green; ruff/format/mypy clean; self-test passes (the plan that builds
the lint cleanly passes the lint).

Plan: plan-114-grounding-lint.md (v2 PASS @ 4ea06be)
Audit: META_LEDGER #19 (chain hash 850ec57f)
Merkle seal: a19a04debe5f8f38aab182263e94819d50743849a26cdb8cc4aa3279a81be265

Closes the SG-PLAN-GROUNDING-DRIFT loop after 5 instances tracked.
Defense-in-depth: author-time `ls -d */` mitigation + CI lint
durable countermeasure now both in place.

Capability shortfalls: gate artifacts, reliability sweep, version bump
all skipped (qor/ runtime helpers absent on this branch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Knapp-Kevin Knapp-Kevin added enhancement New feature or request flow:feature Standard feature/fix PR targeting BicameralAI/dev (the default flow) labels Apr 29, 2026
@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 60250754-5297-4cf3-8583-38ac674c11b7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/114-grounding-lint

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

Bicameral drift report — skipped

No bicameral/decisions.yaml found in repo root. Drift report is skipped for this PR.

To enable: add a bicameral/decisions.yaml manifest. See setup guide (link to be added when manifest spec ships).

Knapp-Kevin and others added 2 commits May 5, 2026 18:57
Resolves long-standing conflicts between PR #121 (CI grounding lint
for plan files + PR-body refs, closes #114) and dev's progression
since the PR was opened on 2026-04-29:

- CHANGELOG.md: kept #114 grounding-lint entry alongside dev's #173
  (preflight graph expansion) and #175 (Step 5.6 disambiguation)
  Unreleased entries; collapsed duplicate "### Changed" header.
- docs/META_LEDGER.md: took dev side wholesale. PR #121's branch
  inserted Entry #19 (audit PASS) and #20 (substantiation seal) for
  #114, but dev's chain has progressed to 41 entries since (most
  recent: v0-release-blockers SEAL at 7cc405fc). Re-anchoring the
  PR's mid-chain entries would require recomputing every subsequent
  chain hash — not safely automatable. The audit/seal history for
  #114 is preserved in git history via commits 11685e5 and 218254e.
- docs/SYSTEM_STATE.md: same — took dev side wholesale.
- plan-114-grounding-lint.md: line 61 referenced
  `.github/scripts/post_drift_comment.py` as a precedent example;
  dev deleted that file. Marked as `(nonexistent)` exemption per
  the lint's own exemption vocabulary.

Self-test: `python scripts/lint_plan_grounding.py plan-114-grounding-lint.md`
exits 0 (the plan that builds the lint passes the lint). 14/14 lint
unit tests pass; ruff/mypy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Subsumes the remote's earlier merge with stale dev (8f0253d).
The local merge already includes current dev (fb86ef8 / post-#198)
plus the resolved [Unreleased] section preserving #114, #173, and
#175 entries.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request flow:feature Standard feature/fix PR targeting BicameralAI/dev (the default flow)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant