next
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a9a7a328e6 |
refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted. |
||
|
|
740ba0d8a3 |
fix(#4628): expose DAG-ready plans and restrict dispatch to them (#4781)
Emitted-Drift-Ack-Growth: execute-phase.md — #4628 consumer wiring: ready_plans parse pointer, not-ready named skip, and waiting condition 2b reference to the ready-wave-gate step file Co-authored-by: sim <sim@local> |
||
|
|
9ee6d54cc3 |
fix(#4306): extend fault-injection fd-swallow fix across the whole suite (#4308)
* fix(#4306): forward real bytes through io.test.cjs's fault-injection mocks The bug #1008 fault-injection tests mock fs.writeSync scoped only by file descriptor. On their "success" arms (the retry-after-EAGAIN/EINTR call, and the short-write simulation) they fabricated a return byte count without ever calling the real writeSync -- the bytes went into a local array and nowhere else. node:test's process-isolation runner (default on Node >= 22) reads each test file's own stdout to parse its child-to-parent result protocol. If the runner's own reporter write for an adjacent test lands on fd 1 while one of these mocks is installed, that write was silently swallowed instead of reaching the real pipe -- observed in CI as "Unable to deserialize cloned data" (a corrupted/truncated byte stream on the parent's read side), not a thrown exception. Every "success" arm now forwards the real bytes to orig()/restore() instead of fabricating a return value, so anything else sharing the fd during the mocked window still gets its bytes delivered for real. writeAllSync (the only production caller reaching this mock) always passes a Buffer, so the forwarded calls use the buffer-form fs.writeSync overload unambiguously. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#4306): extend fault-injection fd-swallow fix across the whole suite The originally-fixed instance (tests/io.test.cjs) was one occurrence of a copy-pasted defect: mocked fs.writeSync arms fabricated a return byte count without ever forwarding the call to the real fs.writeSync, silently discarding bytes. Under node:test's process-isolated runner, the parent reads the child's real stdout to parse v8-serialized report frames interleaved with plain output (confirmed against node's own lib/internal/test_runner/runner.js and a matching upstream issue, nodejs/node#64061) — a swallowed write on that fd corrupts the parent's parse ("Unable to deserialize cloned data"). Adds a shared, safe capture helper to tests/helpers.cjs, captureFdSync(fd, fn): it always forwards every write to the real fs.writeSync first, then records only the observed fd's bytes, sliced by the real return count (not the requested length), decoded once via Buffer.concat so a short write can't split a multi-byte codepoint across two decodes. 17 test files migrate their local copy of the unsafe mock to this shared helper. tests/worktree-base-ref.test.cjs keeps a narrower in-place fix instead (it needs to record every fd a write touched, which the shared helper doesn't expose). tests/io.test.cjs gets two follow-up correctness fixes on top of the already-committed forwarding fix: the EAGAIN/EINTR/short-write arms now derive their recorded chunk from the real return count everywhere (including the string-form overload), and the short-write test no longer forces a Buffer-shaped truncation call onto a string-form write that could land on the same fd. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
4dfc46bbe7 |
enhance(#3348): add a context-drift pre-check gate to plan-phase (#4147)
* test(#3348): add failing-first coverage for the context-drift gate * feat(#3348): add context-drift pre-check gate for plan-phase Compares each phase's *-RESEARCH.md/*-PATTERNS.md/*-VALIDATION.md/*-SPEC.md effective last-changed time (git commit time, falling back to mtime for uncommitted edits) against *-CONTEXT.md's, so plan-phase no longer silently reuses an upstream artifact that predates a decision added to CONTEXT.md after that artifact was derived from it. Deterministic, no model call. New `gsd_run verify context-drift <phase>` command, sibling to the existing verify.codebase-drift/verify.schema-drift gates in the drift capability. Warn-only by default (workflow.context_drift_precheck), with an opt-in workflow.context_drift_action: block escape hatch. Wired at plan:pre in plan-phase.md, before both the RESEARCH.md and PATTERNS.md reuse decisions. * fix(#3348): address code-review findings — raw-text-match, stale comment, import placement, duplicated phase resolution * fix(#3859): pin the real commit's diff.ignoreSubmodules to match the empty-diff probe The #3859 empty-diff guard decides whether a submodule bump would land using `--ignore-submodules=dirty`, overriding the caller's `diff.ignoreSubmodules` config. The real `git commit -- <paths>` that follows was never given the same override, so under a bare `diff.ignoreSubmodules=all` repo config the two calculations disagree: driven on git 2.39.5 (Debian bookworm, the linux-node24 test-matrix image), the guard correctly stands aside but the scoped commit itself then silently fails (exit 1, no error text) for a gitlink bump it had just confirmed would be recorded, surfacing as commit_failed instead of committed:true. Pin `-c diff.ignoreSubmodules=dirty` onto the scoped commit call too, so the probe and the commit it protects can never diverge. Harmless when no submodule path is involved (driven: identical outcome on an ordinary scoped file, with and without the flag). * fix(#3348): guard resolvePhaseDirByToken's exact-match fallback against path traversal * fix(#3348): retarget phase-enumeration-drift exemption to the consolidated resolvePhaseDirByToken helper cmdVerifySchemaDrift's inline readdirSync was already function-scoped-exempt in lint-phase-enumeration-drift.cjs as a single-phase LOOKUP (not a current-milestone enumeration). This PR's refactor pass lifted that block into a shared helper, resolvePhaseDirByToken, also used by the new cmdVerifyContextDrift — the guard tracks exemptions by enclosing function name, so the readdirSync now lives in an unexempted function and started firing. Move the exemption to resolvePhaseDirByToken (same written reason, now covering both callers) instead of migrating to listAllPhaseDirs, which would introduce two real behavior deltas here: it catches readdirSync failures internally (old code let them throw) and sorts results by phase number before matchPhaseDirs picks matches[0] (old code used raw, OS-dependent readdirSync order). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3348): satisfy lint:ci — slash form, capability registry regen - docs/features/context-drift-gate.md used the deprecated /gsd: colon form; docs are never passed through the install-time slash-form converters, so lint-docs-command-form requires the hyphen form. Regenerated docs/FEATURES.md from the corrected fragment. - Regenerated gsd-core/bin/lib/capability-registry.cjs after editing capabilities/drift/capability.json (lint:generated-sync). * fix(#3859): pin the real commit's diff.ignoreSubmodules via env, not argv -c The prior fix pinned `-c diff.ignoreSubmodules=dirty` onto the scoped commit's argv via `commitArgs.unshift(...)`. `-c key=val` must precede the `commit` subcommand, so this shifted `commitArgs[0]` from `'commit'` to `'-c'` for every scoped commit call, breaking 17 position-based assertions in the commit-files pathspec regression suite that read `a[0] === 'commit'` to find the commit invocation among recorded git calls. `execGit` already accepts an `env` option merged onto `process.env` before spawning. Git honors `GIT_CONFIG_COUNT`/`GIT_CONFIG_KEY_0`/`GIT_CONFIG_VALUE_0` as a per-invocation config override functionally identical to `-c key=val`, expressed via env instead of argv. Passing that env alongside the existing commitArgs (still `['commit', ..., '--', ...stagedPaths]`, argv unchanged) fixes the real commit's effective diff.ignoreSubmodules to match the empty-diff guard's probe without moving anything in argv position 0. Scoped to exactly the canScope branch, matching the probe's own preconditions and leaving no behavior change for commits the probe never evaluated. No test file changes needed — the 17 previously-failing assertions test argv[0] against the array passed into execGit, which never changes. * fix(#3348): register verify-context-drift in the check subcommand router The drift capability's new plan:pre gate declares check.query "verify.context-drift", which normalizes to `check verify-context-drift`, but no such subcommand was routed — phase6-capstone-conformance's uniform-block-field test failed with "Unknown check subcommand" for every declared gate query. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3348): extend #1592's exact-key-list snapshot for the new context-drift config keys tests/capability-registry.test.cjs asserted an exact, hardcoded snapshot of the drift capability's config keys. #3348 legitimately adds two new keys (workflow.context_drift_precheck, workflow.context_drift_action) for its own plan:pre context-drift gate — extend the expected set (and clarify the assertion message) without weakening the test's exactness. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3348): reconcile E2's exemption-migration pin with the resolvePhaseDirByToken extraction #3348 (an earlier commit on this branch, e4b80ad81) extracted cmdVerifySchemaDrift's inline phasesDir readdirSync/matchPhaseDirs block into the shared resolvePhaseDirByToken helper (also used by the new cmdVerifyContextDrift), and retargeted lint-phase-enumeration-drift.cjs's function-scoped exemption from cmdVerifySchemaDrift to resolvePhaseDirByToken accordingly — cmdVerifySchemaDrift no longer contains a line the guard's detectors match, so it needs no exemption. tests/phase-locator.test.cjs's E2 test still pinned the exemption to the old name (cmdVerifySchemaDrift), unaware of the migration. Update E2 to match the same "migrated call site's exemption must move, not duplicate" pattern the test already applies to cmdRoadmapAnalyze and cmdInitMilestoneOp just below it: drop cmdVerifySchemaDrift from the still-exempt list and add symmetric assertions that it no longer carries the exemption while resolvePhaseDirByToken now does. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3348): fix two self-contradicting/nondeterministic tests in context-drift.test.cjs 'always exits 0 (query command contract)' included the no-phase-arg case, which contradicts the file's own earlier 'errors with usage message on missing phase arg' test (that case legitimately exits 1 via the Usage error) — drop it from the always-exits-0 cases. 'degrades to mtime comparison outside a git repo' and '...in a repo with no commits' relied on real wall-clock ordering between two back-to-back writeFileSync calls to prove CONTEXT.md is newer than RESEARCH.md; on a fast filesystem both can land in the same mtime tick, producing a tie that computeContextDrift's strict `<` correctly treats as not-stale, so stale_artifacts comes back empty. Make both tests deterministic via explicit fs.utimesSync instead of relying on timing (CONTRIBUTING.md: never assert elapsed wall-clock time). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3348): add context_drift_precheck:false to the plan:pre all-off fixture The "all plan:pre when-keys false" fixture explicitly disables every known workflow.* plan:pre toggle, but didn't yet know about the new workflow.context_drift_precheck key (defaults to true), so the new drift context-drift gate stayed active and broke the empty-activeHooks assertion. Emitted-Drift-Ack-Growth: plan-phase.md — adds the #3348 context-drift plan:pre pre-check section (new ## 4.6); this PR's own diff, not incidental drift. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs(#3348): backfill changeset PR number (pr:0 -> 4147) --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
a638ca4332 |
enhance(#3882): stop sentinel phases skewing estimation calibration (#3893)
* test(#3882): failing-first rows for sentinel phases skewing calibration Adds A1a/A1b/A2/A3 to tests/estimate-calibrate.test.cjs, the module's existing test file, rather than a new bug-NNNN file. collectCalibrationSamples (src/estimate-cli.cts:206) does a raw readdirSync over .planning/phases and never applies isSentinelPhaseId, so a sentinel phase (milestone 0 or 999) carrying a PLAN estimate / SUMMARY actuals pair contributes a phantom calibration sample. computeCalibration is median-based, so a single 50x outlier among three samples leaves the factor unmoved — asserting "the factor is unchanged" against one sentinel would pass on the broken code for the wrong reason. Each row instead asserts the WHOLE computed CalibrationResult object (factor, applied, confidence, sampleCount, clamped) for a sentinel-free project against its sentinel-injected twin: - A1a: one sentinel flips applied false->true and confidence low->med on phantom evidence (calibration switches on with zero real signal). - A1b: two sentinels corrupt the factor itself (1 -> 3, clamped false->true). - A2: the sentinel's own sample is verified absent from the returned list. - A3: the two genuine phases still contribute their own unchanged samples (regression pin — stops A1/A2 passing by filtering everything). Verified RED on today's code (node tests/estimate-calibrate.test.cjs): A1a/A1b/A2 fail with the exact differing objects; A3 and all pre-existing rows in the file remain green (no collateral). Refs #3882 * feat(#3882): route phase enumeration through its owner and name the sentinel axis Task 1: collectCalibrationSamples (src/estimate-cli.cts) hand-rolled a raw readdirSync over .planning/phases, treating every directory (including sentinel phases, milestone 0/999) as a completed phase and feeding phantom PLAN/SUMMARY samples into the estimation calibration factor. Routed through the existing owner, listMilestonePhaseDirs(phasesRoot) with no cwd -- already 'all milestones, sentinels excluded', exactly the combination this caller needs; no new API was required for this half. It now also surfaces the scope discriminator: an unreadable phases directory throws PhasesUnreadableError instead of silently returning zero samples, and cmdEstimateCalibrate reports it via a new ERROR_REASON.ESTIMATE_PHASES_UNREADABLE instead of persisting a phantom empty calibration document. Task 2: added listAllPhaseDirs(phasesDir, { includeSentinels }) to src/phase-locator.cts -- the one genuinely missing axis: 'physical set, sentinels INCLUDED'. includeSentinels has no default and is required, so a call site cannot obtain sentinel-inclusion by omission (compile-time refusal, not just documentation). Mirrors listMilestonePhaseDirs's absent/unreadable scope handling. Task 3: migrated the two exemptions whose written reason maps cleanly onto 'physical set, sentinels included' -- cmdRoadmapAnalyze's _phaseDirNames (src/roadmap.cts) and cmdInitMilestoneOp's diskPhaseDirs (src/init.cts), both heading->directory lookup indexes. Left the rest: archivePhaseDirectories's own body has no readdirSync to migrate (its callers already resolve dirs before calling it, and both current callers deliberately EXCLUDE sentinels -- migrating it would be an unauthorized behavior change, not an API swap); cmdValidateHealth's exemption is vestigial (its actual physical-set sweep already lives in planning-snapshot.cts's buildAllPhaseDirNamesField, a pre-existing near-duplicate of the new axis, flagged as a finding, not restructured); cmdPhasesClear/cmdMilestoneComplete/cmdVerifySchemaDrift/detectHasPriorPhases/detectUiPhaseActive want a different combination (sentinels excluded, or a single-phase lookup) and are unaffected. Task 4: detector 2 (sentinel literal) is untouched and retained. Removed exemption entries only for the two migrated call sites; every other function-scoped exemption is preserved. Guard exits 0. Refs #3882 * refactor(#3882): delegate the snapshot phase-dir scan to its owner buildAllPhaseDirNamesField duplicated listAllPhaseDirs's own readdirSync + directory-filter + absent/unreadable handling — the 'one implementation per rule' defect ADR-3473 SS8.3 names, introduced by this branch's own #3882 work. Delegate to listAllPhaseDirs and re-apply the field's existing lexicographic sort on top, since W007's observable order must not change. Refs #3882 * docs(#3882): document the sentinel axis and the enumeration consolidation Records listAllPhaseDirs in the Phase Locator glossary entry, and the fact that the owner already answers the all-milestones sentinel-free question when called without a cwd -- the call collectCalibrationSamples was missing. Also notes that buildAllPhaseDirNamesField now delegates rather than carrying a second readdir, and that exactly one readdirSync over the phases directory remains across the two modules. Refs #3882 * test(#3882): close review findings — real order proof, unreadable coverage, collision fixtures Refs #3882 * chore(#3882): backfill changeset PR number Refs #3882 --------- Co-authored-by: sim <sim@local> |
||
|
|
a875372f18 |
test(#3335): fold the workflow-content & phase-lifecycle fix-* cluster — Wave 3 (#3373)
* test(#3335): fold the workflow-content & phase-lifecycle fix-* cluster — Wave 3 Folds 13 legacy fix-*.test.cjs regression files (131 test() blocks) into their module's main suite, per H3 (#3315) of the test-hygiene epic (#3053): - 6 files with no prior target coverage: renamed (git mv) into new suites (spike-manifest-scoping, ship-note, add-todo, workflow-jq-dependency, resolve-execution-dynamic-routing, clock) - 7 files merged into 5 pre-existing suites (worktree-base-ref x2, model-resolver, phase-locator x2, frontmatter, verification-status), deduplicated against existing coverage Zero net test-coverage loss: every source assertion preserved or verified as a genuine pre-existing duplicate. No production code changed. Last fix-* wave (Wave 1 #3341, Wave 2 #3342 already merged); 4 issue-* waves remain in #3315. * test(#3335): fix orthogonal-review findings — Wave 3 fold Standards-axis review + Memtrace graph pass found real defects in the just-folded suites, all fixed here: - phase-locator.test.cjs: pinned an unseeded fast-check property test (CONTRIBUTING.md determinism requirement), matching the sibling test's seed:7 convention. - phase-locator.test.cjs: added assert.ok() presence guards after 9 data.plans.find() calls that were dereferenced unguarded, inconsistent with 5 sibling tests in the same file that already guard correctly. Latent robustness gap — an omitted plan would throw an opaque TypeError instead of a clear assertion failure. - Standardized the fold-wrapper convention (block-scoped __foldDescribe) across worktree-base-ref.test.cjs, verification-status.test.cjs, and phase-locator.test.cjs to match the pattern already established in frontmatter.test.cjs and model-resolver.test.cjs from earlier folds. - worktree-base-ref.test.cjs: moved a mid-file require to the top-of-file require block. - Eliminated duplicated env-isolation helpers: model-resolver.test.cjs and phase-locator.test.cjs each reimplemented GSD_WORKSTREAM/GSD_PROJECT save-restore independently; factored a shared isolateWorkstreamEnv()/ restoreWorkstreamEnv() into tests/helpers.cjs and pointed both call sites at it. No test() count changed in any file. No production code touched. --------- Co-authored-by: sim <sim@local> |
||
|
|
67e2ff7b25 |
fix(#2855): scope the phase-locator archived-milestone fallback to the active workstream (#3008)
* test(#2855): add failing-first regression test for cross-workstream archive leak Covers findPhaseInternal/getArchivedPhaseDirs in src/phase-locator.cts resolving a pending workstream phase to an unrelated workstream's (or flat-mode's) archived phase because the archive fallback hardcodes the project-root .planning/milestones/ tree. Fails against the current implementation; the fix lands in a follow-up commit. * fix(#2855): scope phase-locator archived-milestone fallback to the active workstream findPhaseInternal and getArchivedPhaseDirs in src/phase-locator.cts hardcoded the project-root .planning/milestones/ tree when falling back to search archived phases, ignoring GSD_WORKSTREAM. A pending phase in one workstream whose own phases/ directory didn't exist yet would silently resolve to a same-numbered phase archived under an unrelated workstream's (or flat-mode's) history, complete with stale plan/summary counts and an archived status. Route the archive fallback through planningDir(cwd) instead — the same workstream-aware helper the active-phase search (three lines above) and the archive-write path (archivePhaseDirectories in milestone.cts) already use. Flat/non-workstream projects are unaffected: planningDir(cwd) with no GSD_WORKSTREAM resolves to the same root .planning path as before. Also switch the reported relBase/basePath from a hardcoded '.planning/milestones/...' literal to path.relative(cwd, archivePath), so the paths returned to callers stay consistent with wherever the archive actually resolved to (root or workstream-scoped). * chore(#2855): add changeset for phase-locator workstream archive fix * fix(#2855): normalize getArchivedPhaseDirs basePath to posix separators Orthogonal code-review finding: findPhaseInternal's relBase/directory field was explicitly toPosixPath-normalized, but getArchivedPhaseDirs's basePath used a bare path.relative() call, leaving it native-separator on Windows — an inconsistency between two sibling "relative path from cwd" report fields introduced by the same #2855 fix. Wrap basePath in toPosixPath to match, and update the two existing assertions that compared basePath against path.join output (which would break on Windows now that the field is guaranteed posix) to compare against forward-slash literals instead, matching how the sibling `directory` field is already asserted elsewhere in this suite. * refactor(#2855): share archive-directory resolution between findPhaseInternal and getArchivedPhaseDirs Orthogonal code-review finding: the two functions carried independent copies of the same resolve-milestonesDir-then-enumerate-archive-dirs logic — the exact shape that let the original #2855 bug (hardcoded root path) exist in one copy while the workstream-aware active-phase search sat three lines above it. Extract listArchiveVersionDirs(cwd) as the single seam both functions now consume, so a future change to how the archive tree is located only needs to happen once. Byte-for-behaviour preserved: readSubdirectories and searchPhaseInDir already self-contain their own try/catch and never throw, so moving the iteration outside the old inline try block changes nothing observable (verified via manual repro scripts covering leak prevention, positive resolution, flat-mode parity, and multi-milestone reverse-sort ordering). * test(#2855): demonstrate ROADMAP.md presence does not affect the archive-leak guard Orthogonal code-review (spec axis) finding: issue #2855's AC1 states the guard must hold "regardless of whether workstream A's roadmap already lists the phase and when it doesn't yet" — an explicit two-value dimension that had no direct test coverage; it was only inferable by reading findPhaseInternal's source and confirming it never touches ROADMAP.md. Add a parametrized test creating the workstream's ROADMAP.md with and without a matching Phase heading, asserting the archive-leak guard resolves identically (null) either way. * chore(#2855): backfill changeset PR number to 3008 --------- Co-authored-by: sim <sim@local> |
||
|
|
5f609762ed |
fix(#2237): fail loud on ambiguous bare-number phase directory collision (#2262)
* fix(#2237): fail loud on ambiguous bare-number phase directory collision When two unrelated projects share a .planning/phases/ tree, a bare phase number silently resolved to the first 0N-* directory found — risking cross-project file writes. The fix detects multiple matches for the same phase number and surfaces an ambiguous_matches result instead of silently taking the first. Changes: - src/phase-locator.cts: searchPhaseInDir uses filter() + ambiguity check - src/phase.cts: cmdFindPhase same pattern - src/init.cts: cmdInitPhaseOp surfaces ambiguous_matches in the result - tests/phase-locator.test.cjs: 3 regression tests * docs: backfill changeset PR number (#2262) * merge: keep up to date with next * fix: regenerate stale capability-registry after next merge |
||
|
|
c76827afbc |
refactor(#1291): T6 — migrate test files off the core spine ahead of deletion (#1293)
The convergence lint only scanned src/ + gsd-core/bin, so ~35 test files still imported core.cjs. Repoint all 33 behaviour importers to the leaf modules directly (same symbol->leaf map as the src migration; leaves are the objects core re-exported by reference), delete the now-meaningless shim-identity describe blocks in the 8 leaf tests, and delete tests/core.test.cjs (forwarded-behaviour coverage now lives at the leaves; resolveWorktreeRoot test relocated to worktree-safety in T0) and tests/lint-core-spine-imports.test.cjs (the lint is removed in T-final). Dropped the stale core.test.cjs entries from the allow-test-rule-refs allowlist; eslint-rules RuleTester fixture path pointed at io.cjs. After T6: ZERO test imports core.cjs. core.cts still builds (now fully unused); T-final deletes it. No behaviour change. Closes #1291 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
dd81e3d120 |
refactor(#881): extract phase-locator fs-search into phase-locator.cts (#882)
ADR-857 rollout phase 2d. Move the phase-directory search/location functions (searchPhaseInDir, findPhaseInternal, getArchivedPhaseDirs) + their interfaces (PhaseSearchResult, ArchivedPhaseDir) out of core.cts into a new module src/phase-locator.cts. core.cts re-exports all three (callers unchanged). Cycle-free: phase-locator depends only on leaves (phase-id for token/name matching, core-utils for fs-scan/path helpers, planning-workspace for planningDir) — unblocked by the core-utils leaf (2c). This completes the phase-search split: parsing in phase-id (2a), fs-search in phase-locator. New-CLI-module checklist done (.gitignore, eslint, INVENTORY 94->95 + row, manifest, ARCHITECTURE, CONTEXT.md "Phase Locator Module"). Adds tests/phase-locator.test.cjs (37 tests: behavioral + shim-identity + adversarial phase-dir fixtures). Gates: lint, code-review, security-review, codex adversarial-review (0 findings; verbatim move checksum-verified). Mac 4078 pass; clean-build docker 12972 pass, 0 fail. Closes #881 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |