Repository navigation
Conversation
Share custom phase directory construction and directory-side matching in phase-id. Match longest known IDs without relying on roadmap titles, and derive expected paths independently. Keep config loading in callers and cover active and archived phases. Concurrent files left unstaged: none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Please use the PR template formatThis PR is allowed to stay open because the author is a contributor or higher, but the body does not follow one of the required PR templates. Please update the PR description to use the fix, enhancement, or feature template format from Detected problem: PR body does not match the fix, enhancement, or feature template. |
trek-e
left a comment
There was a problem hiding this comment.
Status note (posted against head
2dc5a411e171dac3e2c8d6156470c15acc5ccf30):nextis green again as ofad90644e23(Tests run 39099590460). Any finding below that is attributed to base-branch health (Base branch health,Required tests) or to the branch being behindnextis base drift, not a defect in this PR; it clears on a refresh of the branch and is not one of the open findings. The open findings are the ones listed under Findings that name a file and line in this PR.
Review — fix(#5186): resolve custom phase directories consistently
Reviewed head 2dc5a411e171dac3e2c8d6156470c15acc5ccf30 (fetched live this turn: mergeStateStatus BEHIND, mergeable MERGEABLE, reviewDecision REVIEW_REQUIRED). No prior maintainer review or inline comments exist on the PR; the only conversation item is the gsd-pr-template-policy bot comment (see Classification).
Summary
The fix introduces phaseDirectoryName in src/phase-id.cts as the single writer-side shape, adds a directory-side matcher (customPhaseQuery / customPhaseDirectoryParts / matchPhaseDirs(..., custom)), plumbs it through findPhaseInternal, cmdFindPhase, cmdPhasePlanIndex, guardedFindPhase (init.*), cmdInitPlanPhase (expected_phase_dir), cmdScaffold and cmdRoadmapUpdatePlanProgress. The 8-file diff matches the diagnosis in the triage comment. The five issue symptoms that have tests (find-phase, init.plan-phase, roadmap.update-plan-progress, SETUP vs SETUP-2, legacy resolution) are addressed. Gaps: the new matcher is only wired into a subset of readers; readers outside that subset stay on the old path or on an unconfigured heuristic mode (Findings 1 and 2). Remaining items are minor.
Classification & gate compliance
- Linked issue #5186 labels (read live):
bug,confirmed-bug-> Bug-Fix track. Pass. - PR has no labels (
labels: []). Fact, not a blocker for the bug track, but nono-changelogis needed because a changeset exists. - Branch
fix/5186-custom-code-dirs:<prefix>/NNN-slugOK. Titlefix(#5186): ...OK. Not draft. Body hasFixes #5186. - PR template: body uses the Fix template. The bot comment "PR body does not match the fix, enhancement, or feature template" (posted 2026-10-18T00:14:06Z) is still on the PR, but the later
Pull request template formatcheck run (39082252621) is SUCCESS at this head. Author should confirm the comment is stale. - Changeset
.changeset/lucky-deer-travel.md: typeFixed,pr: 5509backfilled (OK). Body is a plain sentence and does not use the required**<Bold user-visible change>** — <symptom-led explanation>.form (CONTRIBUTING.md:194-195). See Findings (Minor). - Docs gate: type Fixed is exempt from docs/. No
references/orworkflows/files added, so no INVENTORY change. - One concern per PR: yes. Unrelated churn:
src/phase-locator.ctsheader comment edit (em dash replaced by;, plus a new dependency line). Nit.
Must-Have Acceptance Checklist (from the Agent Brief on #5186)
- With custom naming and a project code, add then
find-phase Xreturns the created directory. PASS by reasoning:custom ... find-phase resolves a newly added custom ID with project code CCMassertsfound.directory === dir;cmdFindPhasethreads config intocustomPhaseQueryFor. - Same without a project code. PASS by reasoning (same test,
nullcode;customDirectoryBodiesleavesdiras the body). init.plan-phase Xreturnsphase_direqual toexpected_phase_dir. PASS by reasoning: asserted ininit.plan-phase agrees with expected_phase_dir ...;expected_phase_diris now always computed for custom ids viaphaseDirectoryName.roadmap.update-plan-progress X <plan> completeticks thePhase Xentry. PASS by reasoning forPRODUCT-BOUNDARYandSETUP-2(literal heading viaphaseMarkdownRegexSourceExact). Asserts the persisted ROADMAP.md.- Prefix-of-another-id (
SETUP,SETUP-2) resolves each to its own directory. PASS by reasoning: two tests (collisions and headings-removed variant). - Numeric, letter-suffixed and bracket-convention results unchanged. PASS by reasoning for
find-phaseon3,3A,3.2dirs andfindPhaseInternal(..., 'bracket')(bracket short-circuitscustomto null atphase-locator.ctsfindPhaseInternal). Not executed here.cmdScaffoldchanged branch (custom id literal) has no test (Finding 4). - Regression test fails before the fix and passes after, run via gsd-test. NOT VERIFIABLE by me (no local execution allowed). Reasoned: every add-then-resolve test fails on base because
find-phasereturnsfound:false. CI:Testsshards and conformance are SUCCESS at this head, but the aggregateRequired testsis FAILURE (base-health gate, see CI). I do not decide that is benign.
Root-cause / functional verdict
Root cause (one sentence): the custom-id writer (cmdPhaseAdd) emitted [CODE-]<ID>-<slug> while every reader matched via digit-anchored extractPhaseToken, so no reader had an inverse of the writer; the fix gives readers that inverse via the same phaseDirectoryName shape. This is a root-cause fix for the readers it reaches, not a special case of the symptom.
Not a complete root-cause fix at the "everywhere" level required by the issue (resolves by X everywhere). The inverse lives in matchPhaseDirs's optional 4th parameter, so every caller that does not pass custom still runs the old digit-anchored matching. See Findings 1 and 2.
Blast radius
CodeGraph (worktree index at head; Memtrace unavailable):
src/phase-id.ctsis imported by 16 files;matchPhaseDirssignature change is additive (optional 4th param), and the export surface gainsphaseDirectoryName,customPhaseQuery,customPhaseDirectoryParts,isCustomPhaseId.phase-locator.ctsexportscustomPhaseQueryFor.findPhaseInternalgains an optional 4th param. Direct callers (grep ofsrc): 20+ sites. Onlyinit.cts(guardedFindPhase),roadmap.cts:1132, and the find-phase/plan-index paths pass config. Callers left on the default:gate-phase-context.cts:82,roadmap.cts:1800,init.cts:3065,refactor-trigger-command-router.cts:217,gate-api-coverage-verify-pre.cts:190,state.cts:5665,commands.cts:2051/3546/3766,verify.cts:1450,template.cts:110,phase.cts:3733/4961/5213/5240.- Direct
matchPhaseDirscallers that cannot see custom ids:roadmap.cts:583/665/871,milestone.cts:778,state.cts:1004/1008,phase.cts:269/357/2589,smart-entry.cts:360,gate-phase-context.cts:115,health-diagnostic-rules/state-consistency.cts:377,health-diagnostic-rules/roadmap-disk-consistency.cts:140. phase-id.ctsnow importsmarkdown-sectionizer.cjs(new edge into a pure module);check:phase-id-driftpassed in CI per PR body and the lint jobs are green.
Co-change completeness
tests/phase-resolution-parity.test.cjsunchanged; PR states it stays green. The parity requirement (writer shape vs reader shape) is covered by the add-then-resolve and divergence tests; no dedicated unit test pinsphaseDirectoryNameoutput againstcmdPhaseAdd/cmdScaffold/expected_phase_dircall sites.- Readers listed in Blast radius that were not updated (Findings 1 and 2).
CONTEXT.md: no entry for the new shared directory-shape owner. Fixed changesets are docs-exempt; vocabulary change is Nit-level.
Test quality
- Behavioral CLI tests, assert persisted artifacts (directory name, ROADMAP.md checkbox text). No source-grep, no wall-clock.
homeSandboxEnvis used for HOME isolation. - Control tests: legacy numeric/letter/decimal/bracket resolution assert specific values.
- The fast-check property (
custom phase add-then-resolve round trip) generates only uppercase letters^[A-Z]{2,8}-[A-Z]{2,8}$ids and a single added phase besides INITIAL. It cannot generate ids that prefix another id, ids ending in digits (SETUP-2), or lowercase/mixed ids, so it does not exercise the longest-id-wins logic it exists to guard. Each of 20 runs spawns several CLI processes; no per-test timeout is set. Minor (Finding 5). - No boundary test at
limit-1/limit/limit+1for the 60-char slug truncation inexpected_phase_dir(generateSlugInternal(...).substring(0, 60)), which the PR touches. Minor (Finding 5).
Memtrace Evidence
get_impact: UNAVAILABLE — Memtrace MCP failed to connect (ENOENT).get_symbol_context: UNAVAILABLE — Memtrace MCP failed to connect (ENOENT).recall_decision: UNAVAILABLE — Memtrace MCP failed to connect (ENOENT).find_code_review_issues: UNAVAILABLE — Memtrace MCP failed to connect (ENOENT)._graph_state: not obtainable (Memtrace down).
Substitute evidence: a CodeGraph index built in a detached worktree at head (codegraph init -y), codegraph_explore/codegraph node/codegraph query for phaseMarkdownRegexSourceExact (src/phase-id.cts:786, exported at :1872), hasProjectCodePrefix (:401, regex /^[A-Z][A-Z0-9_]*-(?=\d)/i), tokenizeHeadings, searchPhaseInDir (src/phase-locator.cts:208-361), and roadmap.cts:19,41 (loadConfigForScope is loadConfig). Caller enumeration used a grep of src (CodeGraph does not index destructured-import call sites). No ADR decision lookup was possible; no governing ADR is cited by the PR.
Findings
Major
- (judgment, verified by static reading; not executed) The directory-side inverse is applied only where a caller passes
custom/options, so "resolves by X everywhere" (issue Desired behavior) is not met. Callers ofmatchPhaseDirslisted under Blast radius (roadmap analyzeatsrc/roadmap.cts:583/665/871,milestone completehasDirectoryatsrc/milestone.cts:778,src/state.cts:1004/1008,src/smart-entry.cts:360, health rulesstate-consistency.cts:377androadmap-disk-consistency.cts:140,phase.cts:269/357/2589,gate-phase-context.cts:115) still use digit-anchored matching and will report a phase created byphase add --id Xas having no directory. Fix: make the custom match part of the canonicalmatchPhaseDirscontract (derive the custom query inside the owner from(dirs, normalized, namingConfig)or have one sharedresolvePhaseDirshelper) so every reader goes through the single owner, and add a test for at leastroadmap analyzedisk status andmilestone completedirectory check with a custom id. - (judgment, verified by static reading) Unconfigured heuristic mode can mis-resolve.
findPhaseInternalcallers that omit options (list under Blast radius, e.g.src/verify.cts:1450,src/phase.cts:3733/5213/5240,src/roadmap.cts:1800) getphaseNaming === undefinedandprojectCode === undefined, socustomDirectoryBodies(src/phase-id.cts) strips any leading^[A-Z][A-Z0-9_]*-as a project code. For a project without a project code and a directoryPRODUCT-BOUNDARY-product-boundary, queryBOUNDARYproduces bodyBOUNDARY-product-boundary, whichcustomPhaseDirectoryPartsaccepts (owner === query.id), soBOUNDARYresolves toPRODUCT-BOUNDARY's directory. It also activates custom matching in non-custom (numeric) projects. Fix: resolve naming config insidefindPhaseInternal(or require callers to pass it) and never guess the project code from the directory; add a negative test (find-phase BOUNDARYis not found when onlyPRODUCT-BOUNDARYexists, with and without project code). - (deterministic fact) The issue's symptom table lists
roadmap.annotate-dependencies PRODUCT-BOUNDARYand the plan-phase probes (check verify-command-paths,check verify-failure-directions,verify context-drift) as broken; the PR neither tests nor mentions any of them (no occurrence of these verbs in the diff or tests).roadmap.cts:1800andverify.cts:1450are on the unconfigured path of Finding 2. Fix: add add-then-resolve tests for each verb the issue names.
Minor
- (deterministic fact)
src/commands.ctscmdScaffoldchanged behavior forphase_naming: customwith a custom id (now writes<ID>-<slug>literal instead ofnormalizePhaseName(id)); the only scaffold test (custom naming preserves legacy directory resolution) covers numeric ids. Fix: add a scaffold test for a custom id with and without project code, asserting the directory equals the onephase add --idwrites. - (judgment) Test strength: the fast-check property cannot generate prefix-colliding, digit-suffixed or lowercase ids (see Test quality) so it does not discriminate the longest-id-wins rule; no slug-length boundary test (
limit-1/limit/limit+1, 60 chars) forexpected_phase_dir. Fix: generate ids from a small alphabet so one id prefixes another and include digit and lowercase segments; add 59/60/61 char title cases. - (deterministic fact) Changeset body
Phases created with custom IDs resolve to their directories in find-phase, planning initialization, and roadmap progress updates.lacks the required**<Bold user-visible change>** —lead (CONTRIBUTING.md:194-195). Fix:**Custom phase IDs resolve to their directories** — phases created with phase add --id now resolve in find-phase, init.plan-phase and roadmap.update-plan-progress. (#5186). - (deterministic fact) The
gsd-pr-template-policybot comment remains on the PR though the later template check passed; author should confirm the body or resolve the comment.
Nit
src/phase-locator.ctsheader comment: unrelated edit (—to;) and an added dependency line; revert the punctuation churn.isCustomPhaseId(src/phase-id.cts) compiles aRegExpon every call and is on the hot path of everyfindPhaseInternal; hoist to a module constant.customPhaseQueryFor(src/phase-locator.cts) readsROADMAP.mdwith an unboundedreadFileSyncon every custom-candidate lookup; the roadmap is only used to extract ids. Consider a bounded read or reuse the parsed roadmap.
CI
Head 2dc5a411e171dac3e2c8d6156470c15acc5ccf30, latest-per-name non-success/non-skipped runs (live):
- failure Base branch health (Tests workflow, run 39082013651)
- failure Required tests (Tests workflow, run 39082013651)
Verbatim failing log line from Required tests: ##[error]the base branch's own last Tests run is red — see the 'Base branch health' job above for the failing run. Wait for a fix-forward merge, or if this PR IS the fix, ask a maintainer to apply the 'fix-next' label to override. The same step's env shows NEXT_HEALTH_RESULT: failure and TEST_RESULT: success, TEST_CONFORMANCE_RESULT: success, COVERAGE_GATE_RESULT: success, QA_LOOP_WALK_RESULT: success, LINT_RESULT: success. Attribution: the red is the base-branch-health gate, not a test file touched by this PR (no failing test in the PR's own matrix); I report it verbatim and do not rule on whether the maintainer's fix-next override applies.
action_required runs for this head sha: none (0).
Mergeability: MERGEABLE but BEHIND next (merge-base aeabede8e9, origin/next at review time ad90644e23); rebase is needed before merge.
Verdict
Changes requested (zero tolerance: 3 Major, 4 Minor, 3 Nit). Substance is close, but the custom-id inverse is not owned by the single shared matcher, and the unconfigured heuristic path can mis-resolve. CI is not clean (Required tests and Base branch health red) and the branch is behind next.
Require naming context at the canonical directory matcher and resolve it silently through the scoped config owner. Preserve raw custom IDs, remove guessed project prefixes, and bound roadmap identity hints without config migration or warnings. Cover all reviewed readers, numeric compatibility, and prefix collisions with digit-ending custom segments.
|
Thanks for the review. Every finding is addressed on the new head
Tests: 33 new cases. The 18 regressions fail on Gates on Ready for your re-review of |
trek-e
left a comment
There was a problem hiding this comment.
Review — fix(#5186): resolve custom phase directories consistently (re-review at 5c12b4fcb8)
I verified the author's claim that the ten earlier findings are fixed: every matchPhaseDirs and findPhaseInternal caller in src/ now passes a naming context, naming is resolved inside the locator, and the missing reader tests are added. The issue's expected behavior ("a phase created by phase add --id X resolves by X everywhere") is still not met. CI is green on this head. Three Major, three Minor and three Nit remain.
Findings
Major
src/phase-id.cts:1468-1487(customIdsFromDirectories,customPhaseDirectoryParts): a directory written byphase addbecomes unreadable when the title's slug begins with a token that has no lowercase letter. Reproduced on a build of the head with{"phase_naming":"custom"}and no project code:query phase.add "2024 roadmap" --id SETUPcreates.planning/phases/SETUP-2024-roadmap, andquery find-phase SETUPreturns"found": false. The directory yields the inferred idSETUP-2024, the longer id sorts first, and it becomes the owner instead of the query id. Resolve against the known id set only (query id plus roadmap and config ids), preferring the roadmap-listed id when two known ids both prefix a directory, or encode an unambiguous id/slug separator inphaseDirectoryName. Add tests with digit-leading titles (2024 roadmap,3 steps), with and without a project code.- Phase-key readers still do not understand custom ids. On the head build
phaseKeyFromDir('PRODUCT-BOUNDARY-boundary')returnsPRODUCT-BOUNDARY-BOUNDARYwhilephaseKeyFromToken('PRODUCT-BOUNDARY')returnsPRODUCT-BOUNDARY, so a ROADMAP entry and its directory never compare equal insrc/planning-inspect.cts:1254,1336-1337,src/workstream-inventory.cts:628,686andsrc/state.cts:2862,2870,6413,6680.phaseKeyFromToken('SETUP-2')returns02, soSETUP-2collides with numeric phase 2. Route these through the same custom-aware owner and test at least planning-inspect and workstream-inventory with a custom id. - Unvalidated custom id reaches a path join.
query phase.add x --id '../evil'creates.planning/evil-xoutside.planning/phases, andscaffold phase-dir --phase '../zz' --name Foocreates.planning/zz-foo. Both reproduced. The write shape existed before this PR, butphaseDirectoryNameis now the single owner of it and the reader already rejects/and\(customPhaseQuery,src/phase-id.cts:1437), so writer and reader disagree. Reject ids containing/,\,..or NUL inphaseDirectoryName(or incmdPhaseAddandcmdScaffold) with a usage error, assert containment under the phases directory, and add a test.
Minor
4. matchPhaseDirs custom branch is quadratic (src/phase-id.cts:1566 recomputes and sorts the id set per directory). Measured: 100 dirs 5 ms, 500 dirs 141 ms, 1000 dirs 778 ms per lookup, and roadmap analyze calls it once per phase. Compute the sorted id list once per matchPhaseDirs call.
5. phaseDirectoryOptions(cwd) is the default argument of findPhaseInternal and is re-evaluated at about 30 call sites, each doing two scoped config reads and, for custom projects, a 1 MiB buffer and a roadmap read; src/state.cts:1040,1044 call it twice in one function. Resolve once per command and thread it, as cmdRoadmapAnalyze does.
6. Both fast-check properties (tests/custom-phase-readers.test.cjs, tests/phase.test.cjs) use a constant title and a fixed seed with 20 runs, so they replay one sample and cannot fail on finding 1. Generate the title, including digit-leading words, and keep the seed only as a recorded example.
Nit
7. src/phase-id.cts:1561: const normalized = context ? normalizePhaseName(query) : query; makes behavior depend on whether a context was passed, while the signature and CONTEXT.md say it is required. Enforce it or normalize unconditionally.
8. tests/custom-phase-readers.test.cjs bounded-read test: locator.phaseDirectoryOptions ? ... : options(null) is a dead fallback that would mask a missing export. Call it unconditionally.
9. src/phase-locator.cts header lists phase-id imports as (normalizePhaseName, matchPhaseDirs, phaseNumberForMatch); add the custom-id symbols it now consumes.
Process
10. The branch is behind next by 4 commits and merges cleanly. Rebase and push.
Memtrace Evidence
- get_impact: UNAVAILABLE — Memtrace MCP failed to connect (ENOENT)
- get_symbol_context: UNAVAILABLE — Memtrace MCP failed to connect (ENOENT)
- recall_decision: UNAVAILABLE — Memtrace MCP failed to connect (ENOENT)
- find_code_review_issues: UNAVAILABLE — Memtrace MCP failed to connect (ENOENT)
- Substitute evidence: CodeGraph index of a detached worktree at the head (
matchPhaseDirs -> customPhaseDirectoryParts -> customIdsFromDirectories),git grepofsrc/at the head andnextto enumerate callers, and CLI probes against atscbuild of the head (commands as stated in findings 1 to 4).
Verdict
Changes requested (zero tolerance: Major 1 to 3, Minor 4 to 6, Nit 7 to 9; branch behind).
Resolve custom directories from explicit identities rather than slug text. Share custom phase keys across planning readers, reuse command naming contexts, and reject path-like IDs before creating directories. Add regressions for digit-leading titles, custom key collisions, and unsafe IDs, plus generated-title round trips. Preserve literal continuation IDs after normalization.
|
Round 4 for review 5565188388, pushed as a fast-forward Major
Minor Nit Evidence
|
Merge conflict with
|
Retain complete custom-ID round-4 regressions and the upstream phase-completion and milestone-fallback regression blocks.
Keep custom phase parsing context with upstream status phrase handling.
|
@trek-e Thanks for flagging the conflict. The branch now merges cleanly into I merged The full Node 24 suite passes with 0 failures, along with |
|
Heads-up on a change to how fixes land in 1. No need to keep merging 2. Add a Self-test report (fix PRs). Fix PRs now carry a short 3. Review. The advisory 4. CI cost. Windows/macOS lanes now run on the queue, not on every push. If your change is platform-specific, add the Questions are welcome here. Details: docs/branch-protection.md. Posted at the maintainer's request. |
trek-e
left a comment
There was a problem hiding this comment.
Re-review of 9adebfba29 (round 4 + merge of next)
Prior findings 1 to 9 (SETUP-2024 slug, phase-key readers, path-like ids, per-directory sort, header, dead fallback) are verified fixed on this head by CLI probes; the rest of the original list is superseded by the findings below. CI for this exact head is complete and green (latest-per-name, no pending, no action_required runs). The issue's expected behavior ("a phase created by phase add --id X resolves by X everywhere") is still not met. Branch merges cleanly into next (13 behind, 8 ahead; git merge-tree clean); this is not a finding.
Findings
Major
src/phase-id.ctscustomPhaseQuery(theLEADING_DIGIT_RUN_PREFIX_REearlyreturn null): custom ids that begin with a digit run followed by letters are written byphase addbut never resolve. Reproduced with{"phase_naming":"custom"}:query phase.add "T 2FA" --id 2FAwrites2FA-t-2fa,find-phase 2FAreturnsfound:false; same for7Band12AB.3and2024-Q1resolve. Source: deterministic (CLI probe). The fast-check properties (tests/custom-phase-readers.test.cjs,tests/phase.test.cjs) only generate letter-leading ids, so they cannot fail on this.- Writer and readers read naming from different sources.
cmdPhaseAdd/cmdScaffolduseloadConfig(merges~/.gsd/defaults.json);phaseDirectoryOptions(src/phase-locator.cts) usesreadScopedConfigValue, which reads only project config files. Reproduced:HOMEwith~/.gsd/defaults.json = {"phase_naming":"custom"}and projectconfig.json = {}:phase.add Boundary --id PRODUCT-BOUNDARYwritesPRODUCT-BOUNDARY-boundary,find-phase PRODUCT-BOUNDARYreturnsfound:false. This is the #5186 symptom for a supported config source. Same forproject_code. Source: deterministic. src/smart-entry.ctsdetectVerifyFailed: in a custom-naming project the STATE token iscurrentPhaseRaw.trim().split(/\s+/)[0]and every directory is accepted as a phase token. Reproduced: dir03-foowithSTATUS: failed,current_phase: "Phase 3":verify_failedistruewithphase_namingunset andfalsewithphase_naming: custom("3"and"3 of 5 (Foo)"staytrue). A silent routing regression for numeric phases in custom projects. Source: deterministic.tests/custom-phase-readers.test.cjscovers onlycurrent_phase: PRODUCT-BOUNDARY.- Adversarial pass (isolated reviewer, reproduced, details in its file): prefix ownership depends on ROADMAP headings, so
phase.remove A --forceremovedA-B-secondwhenA-Bwas not listed in the roadmap (destructive,src/phase-id.ctscustomPhaseQuery/customDirectoryOwner);phase.completenext-phase scan insrc/phase.cts(~4840-4910) is not custom-aware (AUTHcomplete reportsis_last_phase:true, STATE not advanced);progressshows numberAUTH-swith empty name andvalidate.healthW005 flags every custom directory;phase-plan-indexforA-2prints phase02. Source: judgment plus reproduction by the isolated reviewer; verify against/private/tmp/claude-501/-Users-trekkie-projects-gsd-core--claude-worktrees-nostalgic-turing-176e72/d3f576f1-5f2a-4712-8469-1ed12dbb6380/scratchpad/pr-5509-adversarial.md(local).
Minor
5. customPhaseQuery re-runs tokenizeHeadings(roadmap) on every matchPhaseDirs/phaseKeyFromToken/phaseKeyFromDir call, so readers are O(phases x roadmap bytes). Measured on a custom project, one directory per roadmap heading: 600 phases / 183 KB roadmap.analyze 1.63 s; 1200 phases / 366 KB 8.32 s. Compute the known-id list once per context. Source: deterministic (timing).
6. phaseDirectoryOptions allocates Buffer.alloc(1 MiB) per call (about 30 call sites); size the buffer from fstat.
7. cmdPhaseAdd with --id but without phase_naming: custom writes a custom-shaped directory that no reader resolves (adversarial pass); either require custom naming or resolve it.
Nit
8. tests/custom-phase-round4.test.cjs and its titles (round 4 Major 1) name a review round, not behavior; rename by behavior.
9. tests/phase.test.cjs test "directory IDs disambiguate SETUP from SETUP-2 without headings" injects knownIds that no production path supplies; the name overstates the covered behavior.
Process
- PR body has no
## Self-test reportJSON block bound to the final head sha (advisory check; fix template). Add it after the last push. - The "Reviewed or maintainer" check is advisory and clears with an approval on a clean head.
Memtrace Evidence
get_impact/get_symbol_context(targetmatchPhaseDirs, repogsd-core): both returnedambiguouswith only stale, branch-mixed constant declarations (graph indexed on another branch, no head episodes). No dependency traversal ran; not a clean result. Blast radius taken fromgit grepofsrc/callers at the head instead: everymatchPhaseDirscaller passes a context; about 25 files importphase-id.recall_decision(query: custom phase_naming directory resolution): returned ADR-612 and ADR-2121 (phase-identifier parsing consolidation) as ranked candidates; no ban violated by the diff that I can prove. Not proof that no decision applies.find_code_review_issues(graph_mode off, review_mode online, max_candidates 100): 0 issues oversrc/phase-id.cts(full diff),src/phase-locator.cts,src/phase.cts,src/commands.cts,src/init.cts(diff hunks reconstructed for the tool call, not byte-identical to the PR diff), and a hunk subset ofroadmap.cts,state.cts,workstream-inventory.cts,planning-workspace.cts,planning-snapshot.cts.find_yaml_rule_matches: 0 issues oversrc/phase-id.ctsand a hunk subset ofphase-locator,phase,commands,smart-entry. Not run on the test files,milestone,planning-inspect, health rules or gates. Zero findings from these tools do not cover findings 1 to 7, which are semantic. Cross-module check not run (graph stale).- Substitute evidence: tsc build of the head and CLI probes in throwaway projects; CodeGraph caller listing.
Verdict
Changes requested (Major 1 to 4, Minor 5 to 7, Nit 8 to 9). CI green on this head does not retire any finding above.
Preserve complete custom tokens before numeric normalization. Cover mixed case and digit-leading IDs in both generated round-trip properties. Concurrent files left unstaged: none.
…rojects Parse STATE values through the shared prose parser and exclude non-phase directories from the latest-phase fallback. Concurrent files left unstaged: none.
Record custom IDs beside phase artifacts, including scaffold and batch creation. Refuse destructive ownership guesses for legacy directories. Concurrent files left unstaged: none.
Remove injected identity hints from the heading-free SETUP regression. Exercise directory ownership through the real read path. Concurrent files left unstaged: none.
Reuse heading IDs, their membership set and sorted directory prefixes. Remove the redundant roadmap argument at every query caller. Concurrent files left unstaged: none.
Allocate only the file size while retaining the one MiB read bound. Cover empty, small and oversized roadmaps. Concurrent files left unstaged: none.
Keep configured numeric, suffix and decimal tokens on their legacy path unless the writer recorded that exact custom ID. Pin CLI and bracket parity. Concurrent files left unstaged: none.
Reject equivalent IDs and prefer exact records over aliases. Permit proven prefix owners and refuse only ambiguous markerless mutations. Concurrent files left unstaged: none.
Match explicit basenames before identity filtering and report the owner ID. Use that owner when removing the roadmap entry. Concurrent files left unstaged: none.
Record inserted identities only when their parent has a custom identity. Legacy numeric children retain their parent's renumbering behavior. Concurrent files left unstaged: none.
Resolve directory identities before ordering and shaping init records. Keep custom phase IDs and names consistent across progress and manager readers. Concurrent files left unstaged: none.
Accept custom naming case-insensitively without changing the config loader. Report corrupt project config separately from valid config shadowing defaults. Concurrent files left unstaged: none.
Use one preflight and writer tail for add, batch, insert and scaffold. Export the context type, preserve optional locator calls and document dependencies. Concurrent files left unstaged: none.
Share fixture helpers and assert on typed errors and parsed roadmap data. Thread custom identity into checkbox metadata and remove global test patches. Concurrent files left unstaged: none.
Describe custom naming, identity markers, grammar and refusal behavior. Classify the change and disclose the dangling-symlink verification fix. Concurrent files left unstaged: none.
Regenerate the Windows tier after routing the probes through the process seam. Remove the fixture import made obsolete by the shared helpers. Concurrent files left unstaged: none.
Retain the existing reason keys and add the three typed phase refusals. Keep the exit-contract matrix exhaustive across all thirty reasons. Concurrent files left unstaged: none.
Resolve recorded identities before interpreting a query as a full basename. Cover both creation orders across find, init and destructive removal. Concurrent files left unstaged: none.
Preflight existing paths and duplicate batch destinations before any write. Preserve existing identity markers across add, batch, insert and scaffold. Concurrent files left unstaged: none.
Allow a unique markerless target without claiming its longer sibling. Retain ambiguous-owner refusal and document the identity safety fixes. Concurrent files left unstaged: none.
Align two older refusal assertions with the unique-owner ruling. Keep ambiguous-target refusal and prove the untouched sibling survives. Concurrent files left unstaged: none.
|
Thanks for the round-5 review. All 19 findings are addressed on
Two further cases the re-run found:
We have no Windows host, so finding 1 is fixed by reading the assertions rather than by a Windows run; the Windows shard on this push is the real check. Full suite in a CI-like environment (Node 24, empty HOME, no system gitconfig): 46,781 passed, 0 failed, 103 skipped. The Self-test report in the body is bound to this head. The branch is not merged with |
trek-e
left a comment
There was a problem hiding this comment.
Re-review of 857ec0a1f5 (round 6: the 19 findings of the 2026-10-23 review plus the four follow-up commits)
Verdict: changes requested. 0 Blocker, 2 Major, 1 Minor. All 19 prior findings are verified fixed on this head; the two Majors below are new and sit in the same area (a custom id treated as a prefix of another).
Classification and gate compliance
- Linked issue #5186: OPEN, labels
bug,confirmed-bug(Bug-Fix Track).RULESET.CONTRIB.CLASSIFY.fix=requires confirmed/confirmed-bug before implementation: satisfied. - Author follow-up (2026-10-23 22:51, "all 19 findings addressed") is the subject of this review; no question is left open for me. The branch is 10 commits behind
nextand merges cleanly (git merge-tree).
Verification of the prior round (CLI probes in throwaway projects, head built)
Fixed and re-probed: fold-equal ids (1/01, Foo/FOO, CCM-FOO, batch same same) refused before any write with typed reasons; SETUP then SETUP-2 creatable and each resolves to its own directory; API / API-v2 resolve and phase remove API-v2 deletes only its own directory; the A+title b x / A-b shared-target case is refused; full directory-name lookup works again; progress and roadmap.analyze report roadmap order; Windows conformance shards 1 to 3 are now green; docs/CONFIGURATION.md documents the contract. node --test on the three PR test files: 918 pass, 0 fail.
Blast radius and risk tier
matchPhaseDirs and findPhaseInternal (signature changed) are the shared locators: 14 and 17 callers, all in files this PR changes. The graph head is 0ce32128b3 on next, so none of this PR's new symbols exist in it; caller counts are pre-PR and partial evidence. No changed symbol is in the PageRank or bridge top lists (same as the previous round).
Co-change completeness
Same result as the previous round: no missing co-change partner of substance; docs, CONTEXT.md, changeset and conformance tier lists are all in the diff.
Memtrace Evidence
get_symbol_context(matchPhaseDirs, branchnext): ambiguous (eight declarations, seven of them import constants); the resolved function issrc/phase-id.cts:1474, pre-PR shape. The graph predates this PR, so it cannot see the new resolvers. Partial evidence only.get_impact(matchPhaseDirs, upstream): returned the same ambiguity (risk: UNKNOWN, no traversal performed). I rely on the previous round's node-id-pinned impact (HIGH, 48 symbols) and on the diff.recall_decision(custom phase_naming,.phase-id, project_code): returned ADR-612 and ADR-2121 (phase-identifier parsing consolidation) among unrelated rows.governing_rulesonsrc/phase-id.ctsreturned only ADR-443 (effort routing), unrelated. No recorded decision is proven to govern this change; that is not proof none applies.check:phase-id-driftpasses in CI.find_code_review_issues/review_github_pr(post:false, online, minSeverity low, maxComments 30, whole PR): 0 comments, sources ast 0 / cross_module 0 / online 0 / yaml 0,graphState: ready. Deterministic tools found nothing; every finding below comes from CLI probes (source tag [probe]) or the isolated adversarial pass (reproduced by me).
Findings
Major
- [probe]
phase completeskips the next prefix-related custom phase. Custom project with phasesSETUP,SETUP-2,APIin roadmap order:phase complete SETUPreturnsnext_phase: "API", skippingSETUP-2(reproduced on this head). The cause is incomparePhaseNuminsrc/phase-id.cts: its identity step runs the id throughcustomDirectoryOwner, soSETUP-2collapses toSETUPand compares equal. The same aliasing inisCompletePhaseNummeans a[x] A-bcheckbox marksAcomplete (A,A-b,B:phase complete XyieldsB, skippingA). This is the exact id family the PR now allows to coexist, so the writer accepts what the lifecycle then mishandles. State is persisted with the wrongcurrent_phase/next_phase. Fix: compare custom ids by exact recorded id first and fall back to directory ownership only for markerless input; add a regression forSETUP/SETUP-2/APIandA/A-b/B. - [probe, adversarial pass] Same root cause on the completion side:
isCompletePhaseNumaliasing marks a phase complete from another phase's checkbox (see 1). Needs its own assertion in the regression.
Minor
3. [adversarial pass] validateCustomPhaseIds in src/phase-locator.cts accepts ids that phases list and roadmap.analyze then hide (999, 0, 0.5, 00-00: all numeric-shaped). Refuse numeric-shaped custom ids at the writer, the same way fold-equal ids are refused.
CI state for 857ec0a1f5 (latest run per name)
Failing: test (ubuntu-latest, 24, shard 3/3), test (ubuntu-latest, 24) and conformance test (macos-latest, 24), all the single test differential attribution over the real tree (tests/emitted-attribution.test.cjs): plan-phase.md grew 773 bytes plus Emitted-Drift-Ack-Growth trailer key "plan-phase.md" is declared twice with ambiguous, conflicting reasons. Attribution: this PR touches no gsd-core/workflows/* file. The merge-base is d8c3947; since then bc5dfc2887 (#5651) and 39bb1b367a (#5644) landed on next and each grew plan-phase.md with its own ack, and the gate (run on the merge ref against the older base sha) counts both. That is base drift, not this diff; a merge of current next clears the red check. Required tests is the aggregate and Reviewed or maintainer is expected until approval. Windows shards, Stryker, lint, docs-lint, changeset-lint are green. Nothing queued or action_required.
This CI attribution says nothing about the diff: the 2 Major findings and 1 Minor finding above stand independently and still require a push. CI release is moot (nothing pending). I did not push a rebase.
Verdict
Changes requested (Major 1 to 2, Minor 3). Please fix the comparator/complete aliasing with the regression, then merge next (or rely on the merge queue) to clear the base-drift red.
trek-e
left a comment
There was a problem hiding this comment.
Re-review of 857ec0a1f5 (round 6: the 19 findings of the 2026-10-23 review plus the four follow-up commits)
Verdict: changes requested. 0 Blocker, 2 Major, 1 Minor. All 19 prior findings are verified fixed on this head; the two Majors below are new and sit in the same area (a custom id treated as a prefix of another).
Classification and gate compliance
- Linked issue #5186: OPEN, labels
bug,confirmed-bug(Bug-Fix Track).RULESET.CONTRIB.CLASSIFY.fix=requires confirmed/confirmed-bug before implementation: satisfied. - Author follow-up (2026-10-23 22:51, "all 19 findings addressed") is the subject of this review; no question is left open for me. The branch is 10 commits behind
nextand merges cleanly (git merge-tree).
Verification of the prior round (CLI probes in throwaway projects, head built)
Fixed and re-probed: fold-equal ids (1/01, Foo/FOO, CCM-FOO, batch same same) refused before any write with typed reasons; SETUP then SETUP-2 creatable and each resolves to its own directory; API / API-v2 resolve and phase remove API-v2 deletes only its own directory; the A+title b x / A-b shared-target case is refused; full directory-name lookup works again; progress and roadmap.analyze report roadmap order; Windows conformance shards 1 to 3 are now green; docs/CONFIGURATION.md documents the contract. node --test on the three PR test files: 918 pass, 0 fail.
Blast radius and risk tier
matchPhaseDirs and findPhaseInternal (signature changed) are the shared locators: 14 and 17 callers, all in files this PR changes. The graph head is 0ce32128b3 on next, so none of this PR's new symbols exist in it; caller counts are pre-PR and partial evidence. No changed symbol is in the PageRank or bridge top lists (same as the previous round).
Co-change completeness
Same result as the previous round: no missing co-change partner of substance; docs, CONTEXT.md, changeset and conformance tier lists are all in the diff.
Memtrace Evidence
get_symbol_context(matchPhaseDirs, branchnext): ambiguous (eight declarations, seven of them import constants); the resolved function issrc/phase-id.cts:1474, pre-PR shape. The graph predates this PR, so it cannot see the new resolvers. Partial evidence only.get_impact(matchPhaseDirs, upstream): returned the same ambiguity (risk: UNKNOWN, no traversal performed). I rely on the previous round's node-id-pinned impact (HIGH, 48 symbols) and on the diff.recall_decision(custom phase_naming,.phase-id, project_code): returned ADR-612 and ADR-2121 (phase-identifier parsing consolidation) among unrelated rows.governing_rulesonsrc/phase-id.ctsreturned only ADR-443 (effort routing), unrelated. No recorded decision is proven to govern this change; that is not proof none applies.check:phase-id-driftpasses in CI.find_code_review_issues/review_github_pr(post:false, online, minSeverity low, maxComments 30, whole PR): 0 comments, sources ast 0 / cross_module 0 / online 0 / yaml 0,graphState: ready. Deterministic tools found nothing; every finding below comes from CLI probes (source tag [probe]) or the isolated adversarial pass (reproduced by me).
Findings
Major
- [probe]
phase completeskips the next prefix-related custom phase. Custom project with phasesSETUP,SETUP-2,APIin roadmap order:phase complete SETUPreturnsnext_phase: "API", skippingSETUP-2(reproduced on this head). The cause is incomparePhaseNuminsrc/phase-id.cts: its identity step runs the id throughcustomDirectoryOwner, soSETUP-2collapses toSETUPand compares equal. The same aliasing inisCompletePhaseNummeans a[x] A-bcheckbox marksAcomplete (A,A-b,B:phase complete XyieldsB, skippingA). This is the exact id family the PR now allows to coexist, so the writer accepts what the lifecycle then mishandles. State is persisted with the wrongcurrent_phase/next_phase. Fix: compare custom ids by exact recorded id first and fall back to directory ownership only for markerless input; add a regression forSETUP/SETUP-2/APIandA/A-b/B. - [probe, adversarial pass] Same root cause on the completion side:
isCompletePhaseNumaliasing marks a phase complete from another phase's checkbox (see 1). Needs its own assertion in the regression.
Minor
3. [adversarial pass] validateCustomPhaseIds in src/phase-locator.cts accepts ids that phases list and roadmap.analyze then hide (999, 0, 0.5, 00-00: all numeric-shaped). Refuse numeric-shaped custom ids at the writer, the same way fold-equal ids are refused.
CI state for 857ec0a1f5 (latest run per name)
Failing: test (ubuntu-latest, 24, shard 3/3), test (ubuntu-latest, 24) and conformance test (macos-latest, 24), all the single test differential attribution over the real tree (tests/emitted-attribution.test.cjs): plan-phase.md grew 773 bytes plus Emitted-Drift-Ack-Growth trailer key "plan-phase.md" is declared twice with ambiguous, conflicting reasons. Attribution: this PR touches no gsd-core/workflows/* file. The merge-base is d8c3947; since then bc5dfc2887 (#5651) and 39bb1b367a (#5644) landed on next and each grew plan-phase.md with its own ack, and the gate (run on the merge ref against the older base sha) counts both. That is base drift, not this diff; a merge of current next clears the red check. Required tests is the aggregate and Reviewed or maintainer is expected until approval. Windows shards, Stryker, lint, docs-lint, changeset-lint are green. Nothing queued or action_required.
This CI attribution says nothing about the diff: the 2 Major findings and 1 Minor finding above stand independently and still require a push. CI release is moot (nothing pending). I did not push a rebase.
Skills and passes run
/skills-from-the-artificer (preset bugfix-review, block below) and one isolated adversarial reviewer (pr-5509-adversarial.md). I did not run /diagnose, /qa-test-architect, /code-review or /security-review as separate skill calls this round; reproduction was by CLI probes against the head.
Verdict
Changes requested (Major 1 to 2, Minor 3). Please fix the comparator/complete aliasing with the regression, then merge next (or rely on the merge queue) to clear the base-drift red.
Laws that apply
Subject: PR #5509 gives custom phase ids a reader-side inverse (.phase-id markers, phaseDirectoryName, custom-aware readers) and re-threads the lifecycle commands; phase.add --id outside custom naming becomes a usage error.
- postels-law — fired: writers are now strict (grammar, fold-equal and prefix refusals) but the lifecycle comparators are not:
comparePhaseNum/isCompletePhaseNuminsrc/phase-id.ctsnormalise a custom id to its directory owner, soSETUP-2equalsSETUPandphase complete SETUPskips it (Major 1 and 2);validateCustomPhaseIdsinsrc/phase-locator.ctsaccepts numeric-shaped ids the readers hide (Minor 3). Action: normalise once at the edge and compare the exact recorded id downstream. - hyrums-law — fired:
phase.add --idoutside custom naming, markerless-directory refusal inphase remove, and the typed refusal reasons are new observable behaviour; the PR discloses them under Breaking changes and restored directory-name lookup. Action: none beyond the findings; disclosure is adequate. - leaky-abstractions — fired:
phaseDirectoryOptionsinsrc/phase-locator.ctsre-implements the config loader's file selection instead of asking the loader for a quiet read (CONTEXT.md now records it). Action: none required this round; candidate follow-up. - kernighans-law — fired: the comparator/ownership resolution layers (recorded id, fold match, markerless owner, directory name) are what produced the wrong-target defect in Major 1. Action: a decision table in the
src/phase-id.ctsheader plus the regression forSETUP/SETUP-2/API.
Considered and cleared: kerckhoffs-principle (marker reads and path containment hold, no secret involved), knuths-optimization-principle (perf measured by the author and previous round).
Action items: 1. Fix comparator/complete aliasing (Major 1 to 2). 2. Refuse numeric-shaped custom ids (Minor 3).
trek-e
left a comment
There was a problem hiding this comment.
Re-review of 857ec0a1f5 (round 6: the 19 findings of the 2026-10-23 review plus the four follow-up commits)
Verdict: changes requested. 0 Blocker, 2 Major, 3 Minor. All 19 prior findings are verified fixed on this head; the two Majors below are new and sit in the same area (a custom id treated as a prefix of another).
Classification and gate compliance
- Linked issue #5186: OPEN, labels
bug,confirmed-bug(Bug-Fix Track).RULESET.CONTRIB.CLASSIFY.fix=requires confirmed/confirmed-bug before implementation: satisfied. - Author follow-up (2026-10-23 22:51, "all 19 findings addressed") is the subject of this review; no question is left open for me. The branch is 10 commits behind
nextand merges cleanly (git merge-tree).
Verification of the prior round (CLI probes in throwaway projects, head built)
Fixed and re-probed: fold-equal ids (1/01, Foo/FOO, CCM-FOO, batch same same) refused before any write with typed reasons; SETUP then SETUP-2 creatable and each resolves to its own directory; API / API-v2 resolve and phase remove API-v2 deletes only its own directory; the A+title b x / A-b shared-target case is refused; full directory-name lookup works again; progress and roadmap.analyze report roadmap order; Windows conformance shards 1 to 3 are now green; docs/CONFIGURATION.md documents the contract. node --test on the three PR test files: 918 pass, 0 fail.
Blast radius and risk tier
matchPhaseDirs and findPhaseInternal (signature changed) are the shared locators: 14 and 17 callers, all in files this PR changes. The graph head is 0ce32128b3 on next, so none of this PR's new symbols exist in it; caller counts are pre-PR and partial evidence. No changed symbol is in the PageRank or bridge top lists (same as the previous round).
Co-change completeness
Same result as the previous round: no missing co-change partner of substance; docs, CONTEXT.md, changeset and conformance tier lists are all in the diff.
Memtrace Evidence
Graph caveat for everything below: the index is at 0ce32128b3 on next, so none of this PR's new symbols (customPhaseQuery, phaseDirectoryOptions, validateCustomPhaseIds) exist in it. Graph results describe the pre-PR code only.
get_symbol_context(matchPhaseDirs, branchnext): returnedambiguous(eight same-named declarations, seven of them imported constants), no traversal; the function issrc/phase-id.cts:1474. Reissuing with a node id was not done; caller facts come fromanalyze_relationshipsandpreflight_checkbelow.get_impact(matchPhaseDirs, upstream, depth 3): returnedambiguous,risk: UNKNOWN, no traversal. No blast-radius number from this call.analyze_relationships(comparePhaseNum, find_callers, depth 1): 19 callers, allglobal-fuzzyconfidence 0.6, spanningphase.cts,smart-entry.cts,init.cts,commands.cts,state-transition.cts,phase-locator.cts,verify-command-grounding.cts,core-utils.cts,health-diagnostic-rules/phase-structure.cts.preflight_check(comparePhaseNum) listed 12 target files and its impact block was likewise ambiguous.get_function_quality_metrics(comparePhaseNum, pre-PR): cyclomatic 32, cognitive 47, 16 direct callers, risk "high" (this tool also weighs callers); the PR adds a custom branch to it.recall_decision(custom phase_naming,.phase-id, project_code): returned ADR-612 (bracket convention) and ADR-2121 (phase-identifier parsing consolidation) among unrelated rows;governing_rulesonsrc/phase-id.ctsreturned only ADR-443 (effort routing), unrelated. I did not runverify_intent/get_arcon those ids. No recorded decision is proven to govern this change; that is not proof none applies.find_code_review_issues(standalone,review_mode: online,max_candidates: 200,graph_mode: strict,repo_idset): 0 issues,_graph_state: ready, sources ast 0 / cross_module 0 / online 0 / yaml 0. Scope limit: I passed a condensed diff (the signature-changing and security-relevant hunks ofsrc/phase-id.ctsandsrc/phase-locator.cts:comparePhaseNum,matchPhaseDirs,phaseMarkdownRegexSource,createPhaseDirectories, the marker read), not the full 4867-line diff and notphase.cts,state.cts,init.cts,roadmap.ctsor the rest ofsrc/.find_cross_module_issues(signature hunks ofcomparePhaseNum,matchPhaseDirs,searchPhaseInDir,findPhaseInternal, nobase_sha_files): 0 issues,_graph_state: ready; without base files the removed-symbol and signature-drift patterns could not fire.find_yaml_rule_matches(marker-read and containment hunks only): 0.find_ast_review_issues: skipped, no.pyin the diff.review_github_pr(post:false, online, minSeverity low, maxComments 30, whole PR fetched by the tool): 0 comments, same source counts. Earlier in this run,detect_changeswith a bare file list returned emptyaffected_symbols(the graph predates the PR), so it carries no information.- Risk tier:
find_central_symbolsandfind_bridge_symbols(bounded projection, 100000 of 100001 nodes,next): top 25 of each are test fixtures and scripts (runGsdTools,worktree-safety.test.cjshelpers); no changed symbol appears, so there is no PageRank or chokepoint signal.find_dead_codereturned only unrelatedscripts/entries (limit 5) and cannot see the PR's new symbols.find_hotspots(14 d): one hotspot,cellResultinsrc/phase.cts:2432, score 14, low.get_style_fingerprint(src/phase-id.cts): the only delta is early-return share 0.72 versus codebase 0.88 (a Nit by this tool's own calibration, not a finding).get_cochange_context(src/phase-id.cts, 30 d): single-commit coupling only (docs/INVENTORY*,eslint.config.mjs,package.json, one commit), nothing absent from the diff that matters. - Deterministic tools found nothing; every finding below is tagged [probe] (reproduced by command on this head), [adversarial] or [std]/[spec] (sub-agent passes, with files), and the cause of Major 1 is read from the
comparePhaseNumhunk (identity()passes the raw text throughcustomDirectoryOwner).
Findings
Major
- [probe]
phase completeskips the next prefix-related custom phase. Custom project with phasesSETUP,SETUP-2,APIin roadmap order:phase complete SETUPreturnsnext_phase: "API", skippingSETUP-2(reproduced on this head). The cause is incomparePhaseNuminsrc/phase-id.cts: its identity step runs the id throughcustomDirectoryOwner, soSETUP-2collapses toSETUPand compares equal. The same aliasing inisCompletePhaseNummeans a[x] A-bcheckbox marksAcomplete (A,A-b,B:phase complete XyieldsB, skippingA). This is the exact id family the PR now allows to coexist, so the writer accepts what the lifecycle then mishandles. State is persisted with the wrongcurrent_phase/next_phase. Fix: compare custom ids by exact recorded id first and fall back to directory ownership only for markerless input; add a regression forSETUP/SETUP-2/APIandA/A-b/B. - [probe, adversarial pass] Same root cause on the completion side:
isCompletePhaseNumaliasing marks a phase complete from another phase's checkbox (see 1). Needs its own assertion in the regression.
Minor
3. [adversarial pass] validateCustomPhaseIds in src/phase-locator.cts accepts ids that phases list and roadmap.analyze then hide (999, 0, 0.5, 00-00: all numeric-shaped). Refuse numeric-shaped custom ids at the writer, the same way fold-equal ids are refused.
- [spec pass, judgment] A markerless legacy directory
SETUPbeside aSETUP-2-bdirectory makesfind-phase SETUPreturnfound:false(ambiguity refusal, by design per the author). Acceptable if documented indocs/CONFIGURATION.mdwith the.phase-idremedy; please confirm it is. - [std pass] No regression test pins Major 1: the existing completion tests cover
AUTH->BILLINGandA-2, not an id that is a segment-prefix of a later phase (SETUP/SETUP-2/API,A/A-b/B). Add one intests/custom-phase-readers.test.cjsthat fails on this head.
CI state for 857ec0a1f5 (latest run per name)
Failing: test (ubuntu-latest, 24, shard 3/3), test (ubuntu-latest, 24) and conformance test (macos-latest, 24), all the single test differential attribution over the real tree (tests/emitted-attribution.test.cjs): plan-phase.md grew 773 bytes plus Emitted-Drift-Ack-Growth trailer key "plan-phase.md" is declared twice with ambiguous, conflicting reasons. Attribution: this PR touches no gsd-core/workflows/* file. The merge-base is d8c3947; since then bc5dfc2887 (#5651) and 39bb1b367a (#5644) landed on next and each grew plan-phase.md with its own ack, and the gate (run on the merge ref against the older base sha) counts both. That is base drift, not this diff; a merge of current next clears the red check. Required tests is the aggregate and Reviewed or maintainer is expected until approval. Windows shards, Stryker, lint, docs-lint, changeset-lint are green. Nothing queued or action_required.
This CI attribution says nothing about the diff: the 2 Major findings and 3 Minor findings above stand independently and still require a push. CI release is moot (nothing pending). I did not push a rebase.
Skills and passes run
Skill-tool calls made: skills-from-the-artificer (preset bugfix-review), diagnosing-bugs, qa-test-architect, code-review, security-review. Notes on how each was actually applied: diagnosing-bugs and qa-test-architect were applied by me through CLI probes of the head (the red loop is phase complete SETUP returning next_phase: "API"); code-review ran as a Standards sub-agent and a Spec sub-agent (pr-5509-standards2.md, pr-5509-spec2.md); the security-review skill's embedded diff was for a different branch, so I ran its brief as a sub-agent against this PR (pr-5509-security2.md: clean, no finding at confidence 8 or higher). The isolated adversarial reviewer is pr-5509-adversarial.md. /root-cause-analysis is not an installed skill and rubber-duck is not part of the bug-fix track; neither was run. Deterministic tools were limited as stated under Memtrace Evidence.
Verdict
Changes requested (Major 1 to 2, Minor 3 to 5). Please fix the comparator/complete aliasing with the regression, then merge next (or rely on the merge queue) to clear the base-drift red.
Laws that apply
Subject: PR #5509 gives custom phase ids a reader-side inverse (.phase-id markers, phaseDirectoryName, custom-aware readers) and re-threads the lifecycle commands; phase.add --id outside custom naming becomes a usage error.
- postels-law — fired: writers are now strict (grammar, fold-equal and prefix refusals) but the lifecycle comparators are not:
comparePhaseNum/isCompletePhaseNuminsrc/phase-id.ctsnormalise a custom id to its directory owner, soSETUP-2equalsSETUPandphase complete SETUPskips it (Major 1 and 2);validateCustomPhaseIdsinsrc/phase-locator.ctsaccepts numeric-shaped ids the readers hide (Minor 3). Action: normalise once at the edge and compare the exact recorded id downstream. - hyrums-law — fired:
phase.add --idoutside custom naming, markerless-directory refusal inphase remove, and the typed refusal reasons are new observable behaviour; the PR discloses them under Breaking changes and restored directory-name lookup. Action: none beyond the findings; disclosure is adequate. - leaky-abstractions — fired:
phaseDirectoryOptionsinsrc/phase-locator.ctsre-implements the config loader's file selection instead of asking the loader for a quiet read (CONTEXT.md now records it). Action: none required this round; candidate follow-up. - kernighans-law — fired: the comparator/ownership resolution layers (recorded id, fold match, markerless owner, directory name) are what produced the wrong-target defect in Major 1. Action: a decision table in the
src/phase-id.ctsheader plus the regression forSETUP/SETUP-2/API.
Considered and cleared: kerckhoffs-principle (marker reads and path containment hold, no secret involved), knuths-optimization-principle (perf measured by the author and previous round).
Action items: 1. Fix comparator/complete aliasing (Major 1 to 2). 2. Refuse numeric-shaped custom ids (Minor 3).
trek-e
left a comment
There was a problem hiding this comment.
Re-review of 857ec0a1f5 (round 6: the 19 findings of the 2026-10-23 review plus the four follow-up commits)
Verdict: changes requested. 0 Blocker, 2 Major, 4 Minor. All 19 prior findings are verified fixed on this head; the two Majors below are new and sit in the same area (a custom id treated as a prefix of another).
Classification and gate compliance
- Linked issue #5186: OPEN, labels
bug,confirmed-bug(Bug-Fix Track).RULESET.CONTRIB.CLASSIFY.fix=requires confirmed/confirmed-bug before implementation: satisfied. - Author follow-up (2026-10-23 22:51, "all 19 findings addressed") is the subject of this review; no question is left open for me. The branch is 10 commits behind
nextand merges cleanly (git merge-tree).
Verification of the prior round (CLI probes in throwaway projects, head built)
Fixed and re-probed: fold-equal ids (1/01, Foo/FOO, CCM-FOO, batch same same) refused before any write with typed reasons; SETUP then SETUP-2 creatable and each resolves to its own directory; API / API-v2 resolve and phase remove API-v2 deletes only its own directory; the A+title b x / A-b shared-target case is refused; full directory-name lookup works again; progress and roadmap.analyze report roadmap order; Windows conformance shards 1 to 3 are now green; docs/CONFIGURATION.md documents the contract. node --test on the three PR test files: 918 pass, 0 fail.
Blast radius and risk tier
matchPhaseDirs and findPhaseInternal (signature changed) are the shared locators: 14 and 17 callers, all in files this PR changes. The graph head is 0ce32128b3 on next, so none of this PR's new symbols exist in it; caller counts are pre-PR and partial evidence. No changed symbol is in the PageRank or bridge top lists (same as the previous round).
Co-change completeness
Same result as the previous round: no missing co-change partner of substance; docs, CONTEXT.md, changeset and conformance tier lists are all in the diff.
Memtrace Evidence
Graph caveat for everything below: the index is at 0ce32128b3 on next, so none of this PR's new symbols (customPhaseQuery, phaseDirectoryOptions, validateCustomPhaseIds) exist in it. Graph results describe the pre-PR code only.
get_symbol_context(matchPhaseDirs, branchnext): returnedambiguous(eight same-named declarations, seven of them imported constants), no traversal; the function issrc/phase-id.cts:1474. Reissuing with a node id was not done; caller facts come fromanalyze_relationshipsandpreflight_checkbelow.get_impact(matchPhaseDirs, upstream, depth 3): returnedambiguous,risk: UNKNOWN, no traversal. No blast-radius number from this call.analyze_relationships(comparePhaseNum, find_callers, depth 1): 19 callers, allglobal-fuzzyconfidence 0.6, spanningphase.cts,smart-entry.cts,init.cts,commands.cts,state-transition.cts,phase-locator.cts,verify-command-grounding.cts,core-utils.cts,health-diagnostic-rules/phase-structure.cts.preflight_check(comparePhaseNum) listed 12 target files and its impact block was likewise ambiguous.get_function_quality_metrics(comparePhaseNum, pre-PR): cyclomatic 32, cognitive 47, 16 direct callers, risk "high" (this tool also weighs callers); the PR adds a custom branch to it.recall_decision(custom phase_naming,.phase-id, project_code): returned ADR-612 (bracket convention) and ADR-2121 (phase-identifier parsing consolidation) among unrelated rows;governing_rulesonsrc/phase-id.ctsreturned only ADR-443 (effort routing), unrelated. I did not runverify_intent/get_arcon those ids. No recorded decision is proven to govern this change; that is not proof none applies.find_code_review_issues(standalone,review_mode: online,max_candidates: 200,graph_mode: strict,repo_idset): 0 issues,_graph_state: ready, sources ast 0 / cross_module 0 / online 0 / yaml 0. Scope limit: I passed a condensed diff (the signature-changing and security-relevant hunks ofsrc/phase-id.ctsandsrc/phase-locator.cts:comparePhaseNum,matchPhaseDirs,phaseMarkdownRegexSource,createPhaseDirectories, the marker read), not the full 4867-line diff and notphase.cts,state.cts,init.cts,roadmap.ctsor the rest ofsrc/.find_cross_module_issues(signature hunks ofcomparePhaseNum,matchPhaseDirs,searchPhaseInDir,findPhaseInternal, nobase_sha_files): 0 issues,_graph_state: ready; without base files the removed-symbol and signature-drift patterns could not fire.find_yaml_rule_matches(marker-read and containment hunks only): 0.find_ast_review_issues: skipped, no.pyin the diff.review_github_pr(post:false, online, minSeverity low, maxComments 30, whole PR fetched by the tool): 0 comments, same source counts. Earlier in this run,detect_changeswith a bare file list returned emptyaffected_symbols(the graph predates the PR), so it carries no information.- Diff-tool coverage, exactly (a follow-up pass pasted per-file U0 diffs; several delegated runs were cut off, so coverage is partial and I am not claiming more):
src/phase-id.ctsfull file:detect_changes,find_cross_module_issues(nobase_sha_files, base is over 100 KB),find_code_review_issues(online, graph strict; the server reported_max_candidates100 although 500 was requested) andfind_yaml_rule_matches, all 0 issues,_graph_state: ready.src/phase-locator.ctsfull file:detect_changes,find_code_review_issues,find_cross_module_issues(one hunk header was altered, so treat as partial), 0 issues;find_yaml_rule_matchesNOT covered beyond my earlier marker-read and containment hunks. Tests:tests/phase.test.cjs,custom-phase-identity-boundaries.test.cjs,helpers.cjs,cli-exit.test.cjs,io.test.cjs,smart-entry.unit.test.cjseach randetect_changes,find_code_review_issuesandfind_yaml_rule_matches(18 calls, 0 issues). NOT covered by any deterministic diff tool:src/phase.cts,src/state.cts,src/init.cts,src/roadmap.cts,src/commands.cts(beyond onedetect_changesthat returned no affected symbols),src/smart-entry.cts, the other 14 smallsrc/files,tests/custom-phase-readers.test.cjs(a single 1204-line hunk), docs, CONTEXT.md, changeset and the two generated tier lists.review_github_prdid fetch and scan the whole PR with 0 comments, which is the only whole-diff deterministic result. get_cochange_contextran for 33 changed files (allsrc/files, docs/CONFIGURATION.md, CONTEXT.md and five test files; 17 returned empty). The only partner with 2 or more co-changes absent from the PR istests/roadmap.test.cjs(pairs withsrc/roadmap.cts); low signal.analyze_relationshipsran onmatchPhaseDirs,findPhaseInternal,searchPhaseInDir,phaseMarkdownRegexSource,phaseKeyFromToken,phaseKeyFromDir,parsePhaseFromProse,buildPhaseHeadingScanRegex,getMilestonePhaseFilter,listMilestonePhaseDirs(find_callers; pre-PR counts 14, 17, 1, 13, 8, 7, 5, 7, 4, 18) andcomparePhaseNum(19); theexportersqueries were inconclusive. Call sites were then checked on the head checkout:matchPhaseDirs,findPhaseInternalandsearchPhaseInDirare fully re-threaded.- Risk tier:
find_central_symbolsandfind_bridge_symbols(bounded projection, 100000 of 100001 nodes,next): top 25 of each are test fixtures and scripts (runGsdTools,worktree-safety.test.cjshelpers); no changed symbol appears, so there is no PageRank or chokepoint signal.find_dead_codereturned only unrelatedscripts/entries (limit 5) and cannot see the PR's new symbols.find_hotspots(14 d): one hotspot,cellResultinsrc/phase.cts:2432, score 14, low.get_style_fingerprint(src/phase-id.cts): the only delta is early-return share 0.72 versus codebase 0.88 (a Nit by this tool's own calibration, not a finding).get_cochange_context(src/phase-id.cts, 30 d): single-commit coupling only (docs/INVENTORY*,eslint.config.mjs,package.json, one commit), nothing absent from the diff that matters. - Deterministic tools found nothing; every finding below is tagged [probe] (reproduced by command on this head), [adversarial] or [std]/[spec] (sub-agent passes, with files), and the cause of Major 1 is read from the
comparePhaseNumhunk (identity()passes the raw text throughcustomDirectoryOwner).
Findings
Major
- [probe]
phase completeskips the next prefix-related custom phase. Custom project with phasesSETUP,SETUP-2,APIin roadmap order:phase complete SETUPreturnsnext_phase: "API", skippingSETUP-2(reproduced on this head). The cause is incomparePhaseNuminsrc/phase-id.cts: its identity step runs the id throughcustomDirectoryOwner, soSETUP-2collapses toSETUPand compares equal. The same aliasing inisCompletePhaseNummeans a[x] A-bcheckbox marksAcomplete (A,A-b,B:phase complete XyieldsB, skippingA). This is the exact id family the PR now allows to coexist, so the writer accepts what the lifecycle then mishandles. State is persisted with the wrongcurrent_phase/next_phase. Fix: compare custom ids by exact recorded id first and fall back to directory ownership only for markerless input; add a regression forSETUP/SETUP-2/APIandA/A-b/B. - [probe, adversarial pass] Same root cause on the completion side:
isCompletePhaseNumaliasing marks a phase complete from another phase's checkbox (see 1). Needs its own assertion in the regression.
Minor
3. [adversarial pass] validateCustomPhaseIds in src/phase-locator.cts accepts ids that phases list and roadmap.analyze then hide (999, 0, 0.5, 00-00: all numeric-shaped). Refuse numeric-shaped custom ids at the writer, the same way fold-equal ids are refused.
-
[spec pass, judgment] A markerless legacy directory
SETUPbeside aSETUP-2-bdirectory makesfind-phase SETUPreturnfound:false(ambiguity refusal, by design per the author). Acceptable if documented indocs/CONFIGURATION.mdwith the.phase-idremedy; please confirm it is. -
[std pass] No regression test pins Major 1: the existing completion tests cover
AUTH->BILLINGandA-2, not an id that is a segment-prefix of a later phase (SETUP/SETUP-2/API,A/A-b/B). Add one intests/custom-phase-readers.test.cjsthat fails on this head. -
[graph + checkout read] New-context argument is not passed at call sites where a context is in scope, so those sites keep the context-free (numeric) behaviour in custom projects:
src/phase.cts:4109and:4429(phaseMarkdownRegexSource(phaseNum)inphase complete,directoryContextin scope; the roadmap checkbox lookup is the place Major 2's aliasing should be pinned),src/phase.cts:596and:880(comparePhaseNumas a directory sort comparator),src/smart-entry.cts:351,src/health-diagnostic-rules/phase-structure.cts:121;src/state.cts:7182/:7255(parsePhaseFromProse) andsrc/planning-inspect.cts:1099receive no context from their callers. Lower risk:listMilestonePhaseDirs(dir)without options insrc/estimate-cli.cts:243andsrc/learnings.cts:219(unchanged by the PR). Please thread the context or state why each is deliberately numeric-only.
CI state for 857ec0a1f5 (latest run per name)
Failing: test (ubuntu-latest, 24, shard 3/3), test (ubuntu-latest, 24) and conformance test (macos-latest, 24), all the single test differential attribution over the real tree (tests/emitted-attribution.test.cjs): plan-phase.md grew 773 bytes plus Emitted-Drift-Ack-Growth trailer key "plan-phase.md" is declared twice with ambiguous, conflicting reasons. Attribution: this PR touches no gsd-core/workflows/* file. The merge-base is d8c3947; since then bc5dfc2887 (#5651) and 39bb1b367a (#5644) landed on next and each grew plan-phase.md with its own ack, and the gate (run on the merge ref against the older base sha) counts both. That is base drift, not this diff; a merge of current next clears the red check. Required tests is the aggregate and Reviewed or maintainer is expected until approval. Windows shards, Stryker, lint, docs-lint, changeset-lint are green. Nothing queued or action_required.
This CI attribution says nothing about the diff: the 2 Major findings and 4 Minor findings above stand independently and still require a push. CI release is moot (nothing pending). I did not push a rebase.
Skills and passes run
Skill-tool calls made: skills-from-the-artificer (preset bugfix-review), diagnosing-bugs, qa-test-architect, code-review, security-review. Notes on how each was actually applied: diagnosing-bugs and qa-test-architect were applied by me through CLI probes of the head (the red loop is phase complete SETUP returning next_phase: "API"); code-review ran as a Standards sub-agent and a Spec sub-agent (pr-5509-standards2.md, pr-5509-spec2.md); the security-review skill's embedded diff was for a different branch, so I ran its brief as a sub-agent against this PR (pr-5509-security2.md: clean, no finding at confidence 8 or higher). The isolated adversarial reviewer is pr-5509-adversarial.md. /root-cause-analysis is not an installed skill and rubber-duck is not part of the bug-fix track; neither was run. Deterministic tools were limited as stated under Memtrace Evidence.
Verdict
Changes requested (Major 1 to 2, Minor 3 to 6). Please fix the comparator/complete aliasing with the regression, then merge next (or rely on the merge queue) to clear the base-drift red.
Laws that apply
Subject: PR #5509 gives custom phase ids a reader-side inverse (.phase-id markers, phaseDirectoryName, custom-aware readers) and re-threads the lifecycle commands; phase.add --id outside custom naming becomes a usage error.
- postels-law — fired: writers are now strict (grammar, fold-equal and prefix refusals) but the lifecycle comparators are not:
comparePhaseNum/isCompletePhaseNuminsrc/phase-id.ctsnormalise a custom id to its directory owner, soSETUP-2equalsSETUPandphase complete SETUPskips it (Major 1 and 2);validateCustomPhaseIdsinsrc/phase-locator.ctsaccepts numeric-shaped ids the readers hide (Minor 3). Action: normalise once at the edge and compare the exact recorded id downstream. - hyrums-law — fired:
phase.add --idoutside custom naming, markerless-directory refusal inphase remove, and the typed refusal reasons are new observable behaviour; the PR discloses them under Breaking changes and restored directory-name lookup. Action: none beyond the findings; disclosure is adequate. - leaky-abstractions — fired:
phaseDirectoryOptionsinsrc/phase-locator.ctsre-implements the config loader's file selection instead of asking the loader for a quiet read (CONTEXT.md now records it). Action: none required this round; candidate follow-up. - kernighans-law — fired: the comparator/ownership resolution layers (recorded id, fold match, markerless owner, directory name) are what produced the wrong-target defect in Major 1. Action: a decision table in the
src/phase-id.ctsheader plus the regression forSETUP/SETUP-2/API.
Considered and cleared: kerckhoffs-principle (marker reads and path containment hold, no secret involved), knuths-optimization-principle (perf measured by the author and previous round).
Action items: 1. Fix comparator/complete aliasing (Major 1 to 2). 2. Refuse numeric-shaped custom ids (Minor 3). 3. Thread the context at the Minor 6 sites.
Compare recorded ids before directory ownership so prefix phases stay distinct. Pin next-phase selection, checkbox membership and persisted STATE position. Concurrent files left unstaged: none.
Reuse the readers' sentinel predicate before any custom directory write. Cover add, scaffold and whole-batch refusals without rejecting 2FA or 7B. Concurrent files left unstaged: none.
Thread naming context into completion, state, ordering and goal lookups. Keep health collision keys tied to recorded owners instead of token prefixes. Concurrent files left unstaged: none.
Explain markerless lookup refusals and the exact .phase-id repair. Describe reserved writer ids and distinct completion identities. Concurrent files left unstaged: none.
…e-dirs Bring current next into the custom phase fixes and clear attribution base drift. Concurrent files left unstaged: none.
Exclude reserved zero-leading tokens from the successful round-trip domain. Keep lowercase and digit-leading ids while explicit refusal tests cover sentinels. Concurrent files left unstaged: none.
Use the recorded owner in custom roadmap anchors instead of the query alias. Preserve padded numeric completion while threading custom naming context. Concurrent files left unstaged: none.
|
Thanks for the round-6 review. Everything is addressed on
The six threaded sites are covered by new Per your CI note, |
Use the shared home environment for both HOME and USERPROFILE so health checks reach the phase diagnostics instead of the home-directory guard. Assert the Windows home override on every platform. Concurrent files left unstaged: none.
…e-dirs Preserve milestone-prefixed prose parsing alongside custom directory identity context. Regenerate the platform conformance lists. Concurrent files left unstaged: none.
Fix PR
Linked Issue
Fixes #5186
What was broken
With
phase_naming: custom,phase add --id PRODUCT-BOUNDARYwrote a directory named[CODE-]PRODUCT-BOUNDARY-<slug>that nothing could resolve by its id:find-phase PRODUCT-BOUNDARYreported not found,init.plan-phasereturned a nullphase_dirwhileexpected_phase_dirnamed the existing directory, androadmap.update-plan-progressreported the phase missing. It happened with or without aproject_code.What this fix does
The directory shape custom writers produce and the shape readers match come from one definition,
phaseDirectoryNameinsrc/phase-id.cts. Every custom writer (phase add,add-batch,insert, customscaffold) records the exact id in a.phase-idfile inside the directory, so ownership is the directory's own record, not an inference from roadmap headings or prefix length. Resolution order is: an exact recorded id, then a markerless directory that exactly one known id can own, then the full directory name. Writers share one preflight that refuses, with a typed reason and before any write, an id equal to or case- or padding-equivalent to a known id (Foo/FOO,1/01), a repeated id in one batch, an id that already carries the project code, and a target directory that already exists or that another entry in the batch claims. Segment-prefix pairs (SETUPandSETUP-2) are allowed and each resolves to its own directory; only a markerless legacy directory that would make the pair ambiguous is refused. Destructive commands (phase remove, renumbering) act on a markerless directory when its owner is unique and refuse only a genuinely ambiguous one, naming the candidates and the.phase-idfix. Removing a recorded or custom id deletes only its own heading, row and directory; unmarked numeric phases, including inserted decimals, keep their padding-tolerant removal and renumbering. Writers and readers readphase_namingandproject_codethrough the same resolution; a corrupt project config is reported as such. Lifecycle readers are custom-aware and report ids in roadmap order:phase complete,progress,roadmap.analyze,init.progress,init.manager,validate.health,phase-plan-index, STATE and smart entry. Marker reads are bounded (regular file, no symlink or FIFO, 1 KiB). Numeric, letter-suffixed, decimal, bracket and full-directory-name resolution is unchanged.docs/CONFIGURATION.mddocuments custom naming, the marker and the id grammar.Root cause
As the triage found: the custom-id write path arrived with the TS migration (df04aae, #602), but the token grammar and project-code stripping (#1455) were digit-anchored and never extended to custom ids.
extractPhaseTokenfound no token in a custom directory name, sophaseTokenMatchesandmatchPhaseDirsnever matched it, and the writer had no reader-side inverse.Testing
How I verified the fix
Twenty regressions in
tests/phase.test.cjs, parameterized with project codeCCMand with none, fail onupstream/nextand pass with the fix:phase add --id Xthenfind-phase X,init.plan-phase Xwithphase_direqual toexpected_phase_dir,roadmap.update-plan-progress XforPRODUCT-BOUNDARYandSETUP-2, prefix and slug collisions, a renamed roadmap heading, an archived custom phase, and a fast-check add-then-resolve round trip. Copying the resolved path into the expected path fails both divergence tests. The numeric scaffold control passes on both; 21 focused checks and 304 affected tests pass, andtests/phase-resolution-parity.test.cjsis unchanged and green.lint:ci(includingcheck:phase-id-drift) andlint:changesetpass. Round-5 regressions intests/custom-phase-readers.test.cjs(identity precedence, occupied-directory and fold-equal collisions, markerless ownership, reader order, typed refusals) fail on6efa98166and pass on the head. Round-6 regressions (custom lifecycle identity,custom hidden identity,custom reader context) fail on857ec0a1fand pass on the head. Clean Node 24 full suite: 46,861 passed, 0 failed, 104 skipped.Regression test added?
Platforms tested
Runtimes tested
Self-test report
{ "schema": 1, "head_sha": "e139b996474fbe6c466326cdb5b2033bea63d471", "tool": "npm-test", "failing_first": { "test": "custom lifecycle identity: phase complete SETUP advances to SETUP-2", "fails_on_base": true, "passes_on_head": true }, "commands": [ "npm run build:lib", "npx eslint src tests scripts --max-warnings 0", "npm run lint:ci", "node --test tests/custom-phase-readers.test.cjs tests/phase.test.cjs", "npm test" ], "full_suite": { "command": "npm test", "result": "passed", "passed": 46861, "failed": 0 }, "environment": { "os": "macOS 26.6.2", "node": "v24.22.0" }, "output_excerpt": "npm test exit 0 at head; summed over all chunks: 46861 passed, 0 failed, 104 skipped. Run as on CI: Node 24, an empty HOME, GIT_CONFIG_NOSYSTEM=1 (Apple's system gitconfig sets init.defaultBranch=main) and TMPDIR as its real path. Last chunk:\nrun-tests: chunk 28/29 completed in 56690ms\n# tests 2029\n# pass 2027\n# fail 0\n# skipped 2\n# duration_ms 47487.287\nrun-tests: chunk 29/29 completed in 47605ms" }Checklist
Fixes #NNN— PR will be auto-closed if missingconfirmed-buglabelnpm test).changeset/fragment added if this is a user-facing fix (npm run changeset -- --type Fixed --pr <NNN> --body "...") — orno-changeloglabel appliedBreaking changes
phase.add --id(andadd-batch) now returns a usage error unless the project's effectivephase_namingiscustom. Before, it wrote a custom-shaped directory that no reader could resolve. When~/.gsd/defaults.jsonsetscustombut the project config shadows it, the error says so.999,0.5,00-00), or contain whitespace, a colon or a leading[; such ids could not be resolved unambiguously. The refusals carry typed reasons (phase_id_collision,ambiguous_phase_owner,phase_naming_not_custom).scaffold phase-dirwith milestone-qualified and bracket arguments.