* feat(#3586): warn when .planning/ is gitignored but still tracked
git ignore rules have no effect on files git already tracks, so a project
that committed .planning/ before ignoring it keeps staging those files --
while commit_docs correctly resolves to false, which is exactly what makes
the contradiction invisible.
The probe lives in the SNAPSHOT BUILDER, not the rule: Rule.check may perform
no ambient I/O (ADR-3180 8.1 rule 1, enforced by lint-planning-snapshot-bypass).
buildPlanningTrackedField follows buildWorktreeHealthField's precedent --
injected execGit, bounded, degrading to UNREADABLE with a typed reason rather
than throwing. W024 went inline instead only because no snapshot field carried
its fact; that precondition does not apply here.
W029 fires only on COMPLETE scope with ignored and tracked both true, so a
degraded probe yields neither a finding nor a false all-clear, and the default
project (tracked, not ignored) stays silent. The remedy is ADVISE-only --
--repair never untracks anything.
* docs(#3586): document W029 and correct the health rule count
CONFIGURATION.md documented the gitignore auto-detect without the caveat that
ignore rules do not affect already-tracked files -- the very gap W029 exists to
surface. Adds the caveat, the warning, its remedy, and why --repair will not
act on it.
CONTEXT.md's rule count was stale at 31 before this change (actual 32 through
W028); corrected to 33 and pointed at the two other places the count is locked,
so the next editor updates all three together.
* fix(#3586): treat ls-files overflow as tracked, add CLI-level W029 tests
Review findings.
Security (minor, confirmed): execGit sets no maxBuffer, so Node's 1MB default
applies to git ls-files. A .planning/ tree large enough to overflow it failed
into git_list_failed and silenced W029 -- a false negative in exactly the
large-history case most likely to have the real bug. Overflow is now treated
as PROOF of tracking (the output was non-empty by definition) and resolves to
tracked:true, scope COMPLETE, reason ok_truncated.
Spec (major): test-matrix rows C1 and C2 were never implemented -- there was no
CLI-level integration test at all, only rule-level ones. Both now drive the real
validate-health dispatch and confirm W029 is reachable end-to-end.
Known limit documented, not papered over: a deliberate git add -f under an
otherwise-ignored .planning/ raises the same signal as the accidental case.
There is no reliable way to tell them apart, the finding is advisory-only, and
a heuristic that cannot actually distinguish them would be worse than the
honest caveat.
* test(#3586): update frozen health-doc counts and acknowledge health.md growth
The remote matrix caught three gates that lint:ci does not cover.
gen-health-docs.test.cjs froze a 35-row / 32-rule assertion; W029 makes it
36/33. Updated both the assertion and the test NAME, which embeds the counts --
a stale name is a lie even when the assertion passes. The second reported
failure was the same assertion surfacing at describe-rollup granularity, not a
distinct bug.
emitted-attribution's growth arm needed an ack for the generated health.md.
health.md was already named in 3309-health-docs-generated.json, and two ack
sources naming one path is a hard error -- so a new fragment was not an option.
That fragment's own history shows the pattern: #3309 created it, #2873 amended
it in place for W028. Amended again for W029, with a note recording why this
one file is amended rather than joined by a sibling.
* docs(#3586): add the private-planning how-to and fix a wrong link
docs/CONFIGURATION.md pointed 'Configure private planning' at
how-to/configure-model-profiles.md -- an unrelated page -- and no
private-planning how-to existed at all. Found while editing that section.
The how-to test genuinely fires here: going private is four steps and crosses
planning.search_gitignored, a setting owned by another concern, so a reference
table structurally cannot carry it. The new page walks the whole sequence and
leads with the step people miss -- .gitignore does not untrack what git already
tracks -- which is the exact state W029 now detects.
Also corrects 'artefacts' to 'artifacts' (repo house style is American).
* chore(#3586): backfill changeset pr number to 3598
---------
Co-authored-by: sim <sim@local>
gsd-test found two real regressions in the migrated W006/W007:
1. A phase whose directory lives under an archived milestone
(.planning/milestones/v*-phases/<phase>/) instead of the active
phases/ dir read as "in ROADMAP but no directory on disk" — the
original forEachArchivedPhaseToken(planBase, ...) fed archived
tokens into the same existence check (verify.cts:2038); the
migrated rule's allPhaseDirNames never included them.
2. Comparing a ROADMAP-declared phase id against a disk directory name
dropped phaseVariants() normalization the original ran as a second,
independent check (verify.cts:2071-2073/2092-2093) — a ROADMAP
"01A" and a disk "1A-..." read as mismatched instead of the same
phase, since matchPhaseDirs's own token comparison never unifies
that padding/letter-suffix difference.
Adds planning-snapshot.cts's archivedPhaseTokens field (mirrors
forEachArchivedPhaseToken/listMilestoneArchiveDirs exactly, no new
regex derivation) and a phaseVariants()-based fallback in
dirsForPhase when matchPhaseDirs finds nothing.
The migrated checkW027 (stale worktree) dropped the pre-migration
exclusion of the CLI's own current worktree, since a Rule.check(snapshot)
has no cwd access (§8.1 rule 1 forbids ambient I/O) — flagged as a
disclosed regression during this phase's own design work, then
confirmed as a real, fixable gap by the Spec-axis orthogonal review
rather than an inherent limitation.
Fixes it properly instead of accepting the regression: buildPlanningSnapshot(cwd)
already receives cwd as its own input, so exposing it as snapshot.cwd
is not new ambient I/O, just surfacing an existing parameter — fully
consistent with §8.1 rule 2's "parsed value" allowance. checkW027 now
excludes the entry matching snapshot.cwd before flagging, matching the
original verify.cts:2233-2242 behavior exactly.
W007 (orphan disk dir with no ROADMAP entry) cannot be sourced from
phaseDirs, which is windowed to ROADMAP-declared phases only — an
orphan dir can never appear in an already-ROADMAP-filtered set. Adds
an unwindowed allPhaseDirNames field so the rule can actually fire.
Phase 11 of epic #3180 (ADR-3180 §8.1 rule 2). PlanningSnapshot grows from
7 fields to 15: projectSections, statePhaseTokens, stateStatus,
roadmapDeclaredPhases, roadmapPhaseCheckboxes, researchValidationStatus,
milestoneArchiveStatus, planningRootFiles.
Every field is a reused owner (buildRoadmapPhaseVariants/
buildNotStartedPhaseVariants from src/validate.cts, stateFieldValue) or a
small relocation of already-working verify.cts logic (PHASE_NUMBER_TOKEN_SOURCE
scanning, the checkMilestonePrefixMismatches sectionRx walk, W009/W018's
file-existence checks) — never a new algorithm, and never raw document text:
§8.1 rule 2 forbids exposing raw text, not exposing a parsed list or boolean
derived from it once by the snapshot builder.
roadmapPhaseCheckboxes deliberately reads the same ROADMAP checkbox
isPhaseComplete (§7.4, disk-strict) refuses to consult — that owner decides
completion and must not read it; this field only exposes what the checkbox
says, for a diagnostic (W011) whose whole purpose is flagging disagreement.
Not a re-derivation of §7.4, recorded explicitly to prevent that reading.
Adds PROJECT_UNREADABLE to UNUSABLE_REASON (ninth #1879 site), closing a
gap the implementing agent correctly flagged rather than silently leaving
absent-vs-corrupt collapsed for PROJECT.md, matching the STATE_UNREADABLE/
CONFIG_UNREADABLE precedent from this same effort's prior commits.
currentPhaseLabel/statePhaseTokens/stateStatus share one STATE.md read
(buildStateFields) rather than three independent reads.
Additive only — all prior fields and worstScope/buildPhaseSnapshot
unchanged.
Phase 11 of epic #3180 (ADR-3180 §8.2/§8.3/§8.5) foundation. Extends the
already-merged Phase-10 PlanningSnapshot additively with three fields the
upcoming health-diagnostic rule table needs and Phase 10 never required:
- config: {value, scope, exists} — parsed .planning/config.json. `exists`
distinguishes absent (no diagnostic, non-answer) from present-but-invalid
(CONFIG_UNREADABLE diagnostic, corruption) — both collapse to scope
UNREADABLE, so a rule needs the extra bit to tell "not configured yet"
apart from "config.json is broken."
- agentInstall / worktreeHealth — not .planning/-sourced, wrap the existing
checkAgentsInstalled/inspectWorktreeHealth owners with the same arguments
cmdValidateHealth already passes them, so a later migration step reads
these fields instead of calling the owners itself.
Adds CONFIG_UNREADABLE to src/unusable-input.cts's UNUSABLE_REASON (eighth
#1879 site), mirroring STATE_UNREADABLE's exact shape from Phase 10.
Additive only — the four Phase-10 fields and worstScope/buildPhaseSnapshot
are unchanged; existing tests for them are untouched.
Phase 10 of epic #3180. src/planning-snapshot.cts is a new parsed
projection of .planning/, composed exclusively from the already-
consolidated §7 owners (getMilestoneInfo, listMilestonePhaseDirs,
isPhaseComplete, scanPhasePlans, stateFieldValue, planningPaths) plus
the frozen SCOPE enum. No new semantic derivation is introduced beyond
worstScope, a pure combinator folding several independently-scoped
owner answers into one composite signal.
Adds STATE_UNREADABLE to src/unusable-input.cts's UNUSABLE_REASON
(seventh #1879 site) for STATE.md exists-but-unreadable, distinct
from absent.
Adds scripts/lint-planning-snapshot-bypass-drift.cjs, a ratcheted
drift guard (ADR-3180 Decision 4(e)) scoped to DIAGNOSTIC_RULE_FUNCTIONS
(currently cmdValidateHealth in src/verify.cts only) preventing new
raw .planning/ reads from bypassing the snapshot, while acknowledging
cmdValidateHealth's existing 15 raw-read sites as debt owned by
Phase 11 (#3309).
Six-gate .cts ripple: .gitignore, eslint.config.mjs,
docs/INVENTORY.md + manifest regen, CONTEXT.md glossary entry.
Breaking changes: none. This phase adds the subject only; Phase 11
migrates cmdValidateHealth onto it.