* feat(#2982): extend no-source-grep lint to catch var-binding readFileSync.includes()
The base lint (scripts/lint-no-source-grep.cjs) only catches
readFileSync(...).<text-method>() chained directly. The much more
common var-binding form escapes it:
const src = fs.readFileSync(p, 'utf8');
// 50 lines later
if (src.includes('foo')) {} // ← still grep, lint missed it
Scan of the test suite found ~141 files using this pattern.
Implementation built TDD per #2982 with structured-IR assertions:
scripts/lint-no-source-grep-extras.cjs
- detectVarBindingViolations(src) — pure detector, two passes:
pass 1 collects vars bound from readFileSync, pass 2 finds any
<var>.<includes|startsWith|endsWith|match|search>( on those vars.
- detectWrappedAssertOkMatch(src) — flags
assert.ok(<expr>.match(...)) which escapes the assert.match rule.
- VIOLATION enum exposes stable codes for tests to assert on.
scripts/lint-no-source-grep.cjs
- Wires the new detectors into the existing per-file check; one
additional violation row per file with the first 3 sample tokens.
tests/bug-2982-lint-var-binding.test.cjs
- 13 tests, all assertions on typed VIOLATION enum / structured
records. Covers all 5 text-match methods, multi-var, no-bind,
string literal (must NOT trigger), wrapped assert.ok(.match),
and assert.match (must NOT double-flag).
Migration backlog (#2974 expanded scope):
- 42 files annotated `// allow-test-rule: source-text-is-the-product`
(legitimate — they read .md/.json/.yml files whose deployed text
IS the product)
- 3 files annotated `// allow-test-rule: pending-migration-to-typed-ir [#2974]`
(read .cjs/.js source — clear migration debt)
- 95 files annotated `pending-migration-to-typed-ir [#2974]` with
`Per-file review may reclassify as source-text-is-the-product
during migration` (mixed — manual review under #2974)
After this lands the lint reports 0 violations on main; new
violations in PRs surface immediately.
Closes#2982
Refs #2974
* test(#2982): fix truncated test name per CR
The label ended with a bare '(' from a copy-paste mishap. Now reads
'does NOT flag .matchAll(...) — matchAll is not match, so
assert.ok(.matchAll(...)) is not flagged'.
* chore(#2982): add changeset fragment for PR #2985
* chore(#2982): add changeset fragment for PR #2985
* fix(#2772): only disable worktree isolation when planned paths touch submodules
The previous guard in execute-phase.md and quick.md unconditionally set
USE_WORKTREES=false whenever .gitmodules existed, penalising every plan in
a submodule project even when no plan touched a submodule path.
Replace with submodule-path parsing + per-plan path intersection:
- Parse SUBMODULE_PATHS once from .gitmodules via
`git config --file .gitmodules --get-regexp '^submodule\..*\.path$'`.
- In execute-phase.md, intersect SUBMODULE_PATHS with each plan's
files_modified frontmatter; disable worktree isolation only for plans
with non-empty intersection. Fall back to safe-disable for that plan
when files_modified is missing/unparseable, with a log line explaining
why.
- In quick.md (no pre-declared paths), keep submodule-path parsing and
document a fail-loud commit-time guard so the executor aborts only when
it actually stages a submodule path.
Add tests/bug-2772-gitmodules-path-intersection.test.cjs covering both
files: no unconditional disable, submodule paths are parsed, intersection
logic exists in execute-phase, fallback path is documented.
Full suite: 5680 / 5680 pass.
Closes#2772
* test(#2772): replace source-grep with behavioral test of submodule path intersection
* fix(#2772): wire USE_WORKTREES_FOR_PLAN into dispatch + fix glob matcher + add quick.md commit guard
Address CodeRabbit review on PR #2779 — the original fix computed
USE_WORKTREES_FOR_PLAN but never read it, so the per-plan submodule
intersection was dead code. Dispatch sites still branched on the
project-level USE_WORKTREES.
Changes:
1. execute-phase.md (CRITICAL — dispatch wiring): Move per-plan
computation into execute_waves as sub-step 2.5, run it for each plan
before its dispatch, and gate all four dispatch sites on
USE_WORKTREES_FOR_PLAN: worktree-mode header, sequential-mode header,
"worktrees disabled" sequential rule, and post-wave cleanup. Document
PLAN_FILES extraction via jq from the phase-plan-index JSON. Track
WAVE_WORKTREE_PLANS so post-wave cleanup only runs when at least one
plan in the wave actually used worktrees.
2. Per-plan gate matcher (MAJOR — glob safety): Strip leading "./" and
trailing "/" from both submodule and planned paths. Match
bidirectionally (pf inside sm AND sm inside pf). Handle globby
planned paths like "vendor/**/*.c" by extracting the literal prefix
before the first glob metachar and re-checking. Wrap the iteration
in set -f / set +f so glob expansion does not corrupt patterns.
Extracted the gate (~92 lines) into
workflows/execute-phase/steps/per-plan-worktree-gate.md to keep
execute-phase.md under the 1700-line XL budget.
3. quick.md (CRITICAL — fail-loud guard): Inject SUBMODULE_PATHS into
the executor Task prompt and add a <submodule_commit_guard> bash
block the executor must run before every git commit. The guard
inspects staged paths via `git diff --cached --name-only`, normalizes
paths, and aborts with a clear ABORT message + recovery instruction
("re-run with workflow.use_worktrees=false") when any staged path
falls inside a submodule.
4. tests/bug-2772-gitmodules-path-intersection.test.cjs: 25 tests total.
Updated GATE_SNIPPET to match the new bash matcher. Added
normalization tests (./ prefix, trailing /, glob "vendor/**/*.c",
parent directory, ./ in .gitmodules). Added workflow-markdown
wiring assertions for all 4 dispatch sites + per-plan gate file
extraction. Added quick.md guard tests: prompt injection assertion +
behavioral fixture-repo tests that stage a submodule path and assert
the guard exits non-zero with the ABORT message.
Test count: 5701 pass / 0 fail (was 5698/1 before).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>