Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/agent-memory/atomic-executor/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
- [pwsh -Command quoting + backtick stripping](project_pwsh_command_quoting_from_bash.md) — backticks vanish; `-split "\r\n"` gives 1 element
- [pwsh -File binds a list as ONE string](project_pwsh_file_array_param_from_bash.md) — `-Tokens a,b,c` is a 1-element array
- [Compile-time red needs body-level refs](project_compile_red_needs_body_level_references.md) — a missing type in a SIGNATURE hides body errors
- [Cross-task shell-variable splat gates](project_cross_task_shell_variable_splat_gate.md) — a bare `@assemblies` in a fresh session runs zero assemblies and still reports zero failures; require same-payload enumeration + a `>= 1` count floor
- [Shared evidence artifact + floating <ts>](project_shared_evidence_artifact_floating_ts.md) — N tasks told to append to "the same" artifact whose
## Test execution & isolation
- [Long runs need a detached process](project_long_runs_need_detached_process.md) — background runners die at ~1h
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
name: cross-task-shell-variable-splat-gate
description: A plan task that consumes a shell variable produced by an earlier task is vacuous unless the enumeration body is re-executed in the SAME -Command payload and a count-identity gate plus a >= 1 floor is asserted.
metadata:
type: project
---

When a plan enumerates test assemblies in task A (`$assemblies = @(...)`) and splats them in task B
(`& $vstest @assemblies ...`), no shell state persists between tool invocations, so a bare
`@assemblies` in task B expands to nothing. vstest then runs zero assemblies and still reports a
zero failure count, so the gate passes while measuring nothing.

**Why:** #491 preflight round 2 blocked on exactly this. The fix that cleared round 3 was two-part:
(1) task B must re-execute task A's enumeration BODY (not the whole `pwsh -NoProfile -Command '...'`
wrapper — nesting pwsh puts the variable in a child session) inside the same payload; (2) acceptance
must assert the assembly count on the command line equals the count recorded in task A's artifact
AND is at least 1. The `>= 1` conjunct is what makes it falsifiable; the equality alone normally
always holds because nothing between the two tasks adds a test project.

**How to apply:** Whenever a plan passes a shell variable across task boundaries, demand both the
same-payload clause and the `>= 1` floor. Also check the composed payload's quoting: a vswhere
re-resolution embeds single quotes (`& 'C:\Program Files (x86)\...'`) and needs them doubled inside
a single-quoted `-Command` payload. See [[project_pwsh_command_quoting_from_bash]] and
[[project_preflight_selfderived_gate_thresholds_are_blind]].
9 changes: 8 additions & 1 deletion .claude/agent-memory/atomic-planner/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- [#501 R3](project_501_r3_preflight_seams.md) — repo-wide 0-skipped gates unsatisfiable; BASELINE_FAILURE_SET subset; `Task.CompletedTask` singleton
- [#511 R1](project_511_r1_preflight_delta_seams.md) — mid-cycle evidence deletion; git-log scans post-commit; Start-Process for 20-min runs
- [#484](project_484_qfc_revision_seams.md) — ownership change sweeps plan→issue.md→spec.md (spec is the AC source); old-cardinal grep sweep
- [QfcItemController test-capacity squeeze](project_qfcitemcontroller_test_capacity_squeeze.md) — four owned test files hold only ~471 aggregate spare lines and `.csproj` edits are barred; budget in Phase 0, permit relocation, mandate DataRow/shared-arrange compaction
- [#494](project_494_threshold_reconciliation_plan_seams.md) — coverage runner throws before post-processing; reported-floor must not become hook-Blocking
- [#498](conditional-ladder-and-unowned-class-gates.md) — gate EVERY rung of a recorded-selector ladder incl. rung 1; scope failing-identifier clauses to owned test classes; 0/0 changed-line figure → NOT APPLICABLE
- [#503](project_503_ribbon_readiness_plan_seams.md) — RibbonViewer 487/500 forces a region move; 6+4 Compile entries; compile-time red + dossier
Expand Down Expand Up @@ -40,6 +41,7 @@
- [Wiring gates must be wiring-sensitive](feedback_wiring_gates_must_be_wiring_sensitive.md) — count floors deflate with the defect they guard
- [Research claims as acceptance clauses](research-claims-as-acceptance-clauses.md) — never encode an unmeasured third-party claim as a literal AC clause
- [Literal-call clauses block file-size tightening](literal-call-clauses-block-file-size-tightening.md) — pinning a call in 2+ places near a 500-line file is unsatisfiable
- [Enumeration variable must match its consumer](enumeration-variable-must-match-consumer.md) — `$kept` produced vs `@assemblies` splatted = zero-assembly vstest run reporting zero failures; same-payload re-execution + a count-parity floor
- [Diff gates need a commit task](diff-gates-need-a-commit-task.md) — `git diff <BASE>..HEAD` passes vacuously with no commit task
- [Never pin a HEAD SHA as a plan expectation](never-pin-head-sha-as-plan-expectation.md) — gate on tree invariants instead
- [Absolute counts in shared files go stale](absolute-counts-in-shared-files-go-stale.md) — lower-bound/baseline-relative for co-owned files; keep exact the count the task changes
Expand All @@ -48,7 +50,7 @@

## C# toolchain and test mechanics

- [Phase 0 toolchain bootstrap](project_csharp_phase0_toolchain_bootstrap.md) — global csharpier/dotnet-coverage exe + mandatory NuGet restore
- [Phase 0 toolchain bootstrap](project_csharp_phase0_toolchain_bootstrap.md) — `dotnet tool run csharpier` works once the SDK is bootstrapped (global.json's missing .dotnet-sdk was the real blocker, not the manifest); mandatory NuGet restore
- [Agent worktrees need SDK + NuGet + analyzer backfill](agent-worktrees-need-sdk-and-nuget-bootstrap.md) — four Phase 0 steps; CS0006 is an error, not a warning
- [vstest scoped-run + csharpier 1.2.6 commands](reference_vstest_scoped_run_command.md) — vswhere + `/InIsolation` + `/TestCaseFilter`; csharpier needs a subcommand
- [CSharpier gate: format not pipe-files](csharpier-format-not-pipe-files-gate.md) — `pipe-files` is stdout-only and non-enforcing
Expand All @@ -64,6 +66,8 @@
- [Pester exits 0 on failing It blocks](pester-invoke-does-not-exit-nonzero.md) — scope every exit-code clause to a named channel
- [PowerShell gate observables](powershell-gate-observables.md) — no Invoke-Pester exit code; explicit `scan_folders`; aggregate-only `CoveragePercent`
- [Legacy csproj wiring](project_legacy_csproj_explicit_compile_include.md) — `Compile Include` + own `Reference`; ProjectReference gives no compile-time flow
- [Invoke-VSBuild rewrites csproj HintPaths](invoke-vsbuild-rewrites-csproj-hintpaths.md) — the wrapper runs Sync-PackageReferences over EVERY csproj; with a forbidden .csproj the build itself commits the scope violation — use vswhere-resolved MSBuild
- [Declaration-only seam task for fail-before](declaration-only-seam-task-for-fail-before.md) — tests citing not-yet-existing internals redden the whole assembly; order compile-clean tests first, open the fix phase with a no-behaviour seam task + whole-set assertion-time red run
- [net48 / nullable context mismatch](project_nullable_context_mismatch_prod_vs_test.md) — check `#nullable enable` in prod AND missing `<LangVersion>` in the test csproj
- [Worktree root breaks the `\.claude\` exclusion](worktree-root-breaks-dotclaude-exclusion.md) — assert a workspace-root prefix instead

Expand All @@ -80,7 +84,10 @@
- [#511 R1 preflight delta seams](project_511_r1_preflight_delta_seams.md) — mid-cycle raw-evidence deletion breaks resolves gates; git-log scans post-commit only; absolute MSBuild path; Start-Process mechanic for 20-min runs; per-class coverage noise -0.50pp
- [CSharpier "Formatted N files" is processed count](csharpier-formatted-n-is-processed-count.md) — restart-on-rewrite loops keyed on it never terminate; define rewritten-count via before/after SHA-256
- [Terminal-phase planner traps](terminal-phase-planner-traps.md) — sweep the last phase for an unowned "a follow-up issue should carry it", artifacts written after the clean-tree commit task, and a false "clarification against the spec's wording"
- [#493 UiThread dispatcher plan seams](project_493_uithread_dispatcher_plan_seams.md) — signature-change fail-before gets a REAL red build by staging the two `<Compile Include>` lines; the coverage script IS the parallelized run; scope `SwapUiThreadDispatcher`/`UiThreadDispatcherGate` greps to `QuickFiler.Test/`
- [#553 CI parallel-split plan seams](project_553_ci_parallel_split_plan_seams.md) — workflow-only scope: no C# toolchain; Phase 0 snapshot for byte-identity; ruleset PUT + gh pr create orchestrator-gated; no jq (ConvertTo-Json -Depth 20); pathspec anchoring; BRANCH/SCRATCH conventions
- [#442 QuickFiler metrics plan seams](project_442_quickfiler_metrics_plan_seams.md) — commented-out code defeats zero-hit grep gates; an AC conjunct already green pre-fix; declare the seam BEFORE the red tests or they don't compile; Invoke-MSTestWithCoverage throws below 80%
- [#468 QfcCollectionController plan seams](project_468_qfc_collection_controller_plan_seams.md) — ToggleUnGroupConv is not COM-free drivable; MakeSpaceForItems never touches Size; a sign-defect seam must land carrying the defect; `LoadItemGroup(` needs the paren
- [Threshold conflict: CLAUDE.md vs general-unit-test.md](project_coverage_threshold_conflict_claude_md_vs_general_unit_test.md) — 80/90 vs 85/75; repo-wide figure non-blocking, change-scoped gates blocking
- [JaCoCo hook, Cobertura also accepted](project_csharp_coverage_gate_jacoco_format.md) — follow the format the delta names
- [Async state machines split the denominator](async-state-machine-coverage-aggregation.md) — aggregate by `filename` or a >=90% gate fails for measurement reasons
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ before the first `dotnet tool restore` and before the first `msbuild`, in this o
`pwsh -NoProfile -File .\scripts\vscode\Install-RepoDotNetSdk.ps1`, or mirror the populated
`.dotnet-sdk` tree from the main checkout. Falsifiable acceptance: `dotnet --version` prints
`8.0.205` AND `dotnet --list-sdks` includes a path ending `.dotnet-sdk\sdk`.
2. **`nuget restore TaskMaster.sln`.** A fresh worktree has no `packages/`. Every project declares
2. **`nuget restore TaskMaster.sln`.** Prefer `pwsh -NoProfile -File .\scripts\vscode\Invoke-Restore.ps1`,
which resolves MSBuild through `vswhere` and runs `/t:Restore /p:RestorePackagesConfig=true /m` — the
`packages.config`-aware form. `nuget.exe` is not guaranteed to be on `PATH` in an agent worktree, so a
plan task whose only stated command is `nuget restore` can be unrunnable.
A fresh worktree has no `packages/`. Every project declares
`<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">` whose `<Error>`
fires before compilation when the tree is missing (e.g. `QuickFiler.Test.csproj:452-466`), and every
`Reference` `HintPath` under `..\packages\` is unresolvable. CI does not hit this because
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
name: declaration-only-seam-task-for-fail-before
description: When regression tests reference internal seams that do not exist yet, the whole test ASSEMBLY goes red and no test in it can run — order compile-clean tests first, then open Phase 2 with a declaration-only seam task so every test fails at assertion time
metadata:
type: feedback
---

A C# `[expect-fail]` phase whose tests reference not-yet-existing `internal` members produces a compile error, not a failing test. Because the failure is per-ASSEMBLY, it also destroys the assertion-time evidence of every other test in that phase, including ones that would have compiled fine.

Two ordering rules fix it:

1. **Order the fail-first phase so every compile-clean test is authored and RUN before the first compile-breaking one.** Reflection/structural tests and argument-guard tests against existing members compile today; marshalling tests through a new internal constructor and state assertions through a new internal observation member do not. Run and record the former group first.
2. **Open the fix phase with a declaration-only seam task.** It declares exactly the new members (constructor overload, `internal bool` observation properties, nullable collaborator field) with no behaviour change, and its acceptance is `EXIT_CODE: 0` on a `/t:Build`. The next task then runs the WHOLE test set and records "N discovered, N failed, 0 passed, 0 build errors" — that artifact, not the compile errors, is the authoritative fail-before record, and no behavioural fix precedes it.

Give each compile-time-red task its own falsifiable acceptance anyway: "a `[TestMethod]` named `<X>` exists in `<file>` AND the build is red with the error list recorded verbatim". The method name is a distinct literal per task, so two tasks that share the same broken build are still separately checkable. Quote every such method name in a plan prose block so rule G5 exonerates it.

**Why:** on the #476 plan, six of eight Phase 1 tasks referenced `internal WebView2BreadcrumbHost(` / `IsAttached` / `HasUiDispatcher`. Without this shape the plan would have claimed a fail-before it could not show, and a preflight reviewer cannot tell a genuine assertion failure from a typo that broke the build.

**How to apply:** whenever a bug plan's tests need a NEW seam, sort the phase by "compiles against HEAD" first, then add the declaration-only task and the whole-set fail-before run as the first two tasks of the fix phase. Related: [[acceptance-edits-must-be-false-before-true-after]], [[one-ac-per-checkoff-task]].
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ Any plan gate expressed as `git diff --numstat <MERGE_BASE>..HEAD` or `git diff
2. **Phase 0 porcelain is non-empty by construction.** Never write "empty `git status --porcelain`" as a Phase 0 acceptance. Record the verbatim output and make the binary outcome a *type* assertion instead: no `.cs`, `.csproj`, `.xml`, or `.sln` path appears. Docs/evidence/agent-memory paths are expected.
3. **Scope-lock diff gates must whitelist docs.** Once commits exist, the diff legitimately contains `docs/features/` and `.claude/agent-memory/` paths. Phrase the gate as "every path is a scope-lock member OR under docs/evidence" and keep the hard clause on source extensions only. In a companion file-size audit, state that Markdown docs are exempt from the 500-line cap per `.claude/rules/general-code-change.md`.

4. **Branch history can make a `base..HEAD` gate unsatisfiable no matter how it is whitelisted.** An epic child branch often already carries commits the child does not own — #491's branch head sat on a merge of `main` that restored 58 `docs/features/potential/` records, so a P5 scope-lock gate enumerating five `QuickFiler.Test/**` paths failed before the executor touched anything. When the gate's intent is "this child changed only its own paths", gate the child's OWN commit with `git show --name-only --format= HEAD` run right after the plan's commit task, and demote the branch-wide `base..HEAD` list to a recorded observation rather than a pass/fail condition.

Related: [[never-pin-head-sha-as-plan-expectation]] — the fix is commit tasks plus tree-invariant gates, never a pinned SHA equality check.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
name: enumeration-variable-must-match-consumer
description: A plan task that enumerates test assemblies must assign the exact variable name later vstest tasks splat, and enumeration plus invocation must sit in one pwsh -Command payload — otherwise vstest runs on zero assemblies and still reports zero failures
metadata:
type: feedback
---

When one task computes a list and a later task splats it (`& $vstest @assemblies ...`), the producing command must assign that exact name, and the producer's body must be re-executed inside the SAME `pwsh -NoProfile -Command` payload that invokes the consumer.

**Why:** #491 preflight revision 3. P0-T17 assigned `$all`, `$claude`, and `$kept`; P0-T18 and P3-T6 both splatted `@assemblies`. An undefined `@assemblies` expands to nothing, so vstest would have run against ZERO assemblies and still reported a zero failure count — a silently green full-suite gate. The plan's own conventions paragraph already stated the no-shell-state-persists rule and still shipped the mismatch, because the rule was written about session lifetime and nobody re-read the producing command for the name it actually emits. P0-T17 is itself a `pwsh -Command` child process, so no variable it sets can survive into a later tool invocation regardless.

**How to apply:** Two checks before a plan leaves preflight.

1. **Name check.** Grep the plan for every `@<name>` splat and every `$<name>` consumption, then confirm the producing command literally assigns that identifier. Renaming the producer's variable is the cheaper fix than renaming every consumer; a display label such as `KEPT={2}` in a format string may stay as-is and should be called out as deliberate so a reviewer does not flag the mismatch.
2. **Payload check.** Every consuming task's text must say explicitly that it re-executes the enumeration body verbatim in the same payload, and that a bare `@<name>` in a fresh session is forbidden. Add an acceptance clause pinning the count actually passed on the command line to the count recorded in the enumerating task's artifact, with a `>= 1` floor — otherwise the empty-splat failure mode has no gate that can catch it.

Related: [[wiring-gates-must-be-wiring-sensitive]] — a count floor is the observable that makes an empty run fail; [[powershell-gate-observables]] for the quoting rules of the enclosing payload.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
name: invoke-vsbuild-rewrites-csproj-hintpaths
description: Invoke-VSBuild.ps1 runs Sync-PackageReferences.ps1, which rewrites <HintPath> in EVERY csproj — so when a feature has a forbidden .csproj, the build wrapper itself can commit the scope violation; use direct MSBuild via vswhere instead
metadata:
type: feedback
---

When a plan carries a forbidden-file list that includes a `.csproj`, do NOT plan `scripts/vscode/Invoke-VSBuild.ps1`. Resolve `MSBuild.exe` through `vswhere` and pass the `CLAUDE.md` argument list directly.

**Why:** `Invoke-VSBuild.ps1:152-155` unconditionally invokes `scripts/vscode/Sync-PackageReferences.ps1 -SolutionRoot $repoRoot` before building. That script scans every `*.csproj` for `<HintPath>..\packages\...\lib\...</HintPath>` values that do not resolve on disk and rewrites them in place (`Sync-PackageReferences.ps1:55-154`). In a fresh agent worktree — where `packages/` is absent or holds versions skewed from `packages.config` — that is a live rewrite path, not a theoretical one. On #476 the forbidden list included `QuickFiler/QuickFiler.csproj`, so a single wrapper-driven build would have produced a scope violation that the feature's own scope-containment gate then reports against the executor.

Two related facts:

- The wrapper otherwise supports everything the mandated commands need (`-Target Rebuild`, `-EnableNETAnalyzers`, `-EnforceCodeStyleInBuild`, `-TreatWarningsAsErrors`), so the reason to skip it is exclusively the csproj rewrite. The general "prefer repo-defined tasks" guidance in `policy-compliance-order` still holds everywhere else.
- The wrapper `throw`s on a non-zero MSBuild exit (`:165-167`), so a `pwsh -File` run of it exits 1 rather than surfacing MSBuild's own code. `scripts/vscode/Invoke-MSTestWithCoverage.ps1:236` does the same. Baseline tasks must take `EXIT_CODE:` from the pwsh process and must not gate on `EXIT_CODE: 0`.

**How to apply:** grep a draft plan for `Invoke-VSBuild`; if the feature forbids any `.csproj`, swap to the vswhere-resolved `& $msbuild TaskMaster.sln /t:Rebuild ...` form and say in the plan why. Also add a Phase 5 gate that no `.csproj` outside the writable set appears in the change inventory. Related: [[poshqc-mcp-and-msbuild-invocation-facts]], [[project-csharp-phase0-toolchain-bootstrap]].
Loading
Loading