From 76765bc24d95b6c5b20e615ce92761a89e9f79b1 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 15 Jun 2026 17:44:25 -0400 Subject: [PATCH] =?UTF-8?q?refactor(#1289):=20T5=20=E2=80=94=20migrate=20t?= =?UTF-8?q?he=20final=20idiom-hard=20callers=20off=20the=20core=20spine=20?= =?UTF-8?q?(#1290)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last 4 core importers, migrated off non-destructure idioms: - gsd-tools.cjs: core.{error,ERROR_REASON,setJsonErrorMode,output} -> io.cjs; core.findProjectRoot -> project-root.cjs (lazy wrapper preserved); inline resolveWorktreeRoot require -> worktree-safety.cjs - audit-command-router: DI default `_core ?? core` -> `_core ?? io` (seam preserved) - intel-command-router: DI default -> `{ output: io.output, timeAgo: coreUtils.timeAgo }` (seam preserved) - check-command-router: io destructure -> io.cjs; dynamic core['planningDir'] -> planning-workspace, core['findPhaseInternal'] -> phase-locator (typed imports, dropped the Record-cast bracket hack) Allowlist is now EMPTY — NO file imports the core spine. core.cts re-exports are dead weight; T-final deletes core.cts + remaining shim tests + the lint. No behaviour change. Closes #1289 Co-authored-by: Claude Opus 4.8 --- gsd-core/bin/gsd-tools.cjs | 97 ++++++++++--------- .../lint-core-spine-imports.allowlist.json | 7 +- src/audit-command-router.cts | 8 +- src/check-command-router.cts | 82 +++++++--------- src/intel-command-router.cts | 9 +- 5 files changed, 96 insertions(+), 107 deletions(-) diff --git a/gsd-core/bin/gsd-tools.cjs b/gsd-core/bin/gsd-tools.cjs index c6a02f6db..ccdb13168 100755 --- a/gsd-core/bin/gsd-tools.cjs +++ b/gsd-core/bin/gsd-tools.cjs @@ -194,13 +194,14 @@ const fs = require('fs'); const path = require('path'); const { ExitError, runMain } = require('./lib/cli-exit.cjs'); -const core = require('./lib/core.cjs'); -const { error, ERROR_REASON } = core; +const io = require('./lib/io.cjs'); +const { error, ERROR_REASON, setJsonErrorMode, output } = io; +const projectRoot = require('./lib/project-root.cjs'); // Resolve findProjectRoot lazily at call time rather than binding it at module -// load. It is a re-export from core.cjs (sourced from project-root.cjs); a -// call-time lookup is robust against any require/load-ordering edge where the -// re-export isn't bound yet when this entrypoint is first required (#604). -const findProjectRoot = (...args) => core.findProjectRoot(...args); +// load. It is sourced from project-root.cjs; a call-time lookup is robust +// against any require/load-ordering edge where the export isn't bound yet +// when this entrypoint is first required (#604). +const findProjectRoot = (...args) => projectRoot.findProjectRoot(...args); const { getActiveWorkstream } = require('./lib/planning-workspace.cjs'); const { resolveActiveWorkstream, applyResolvedWorkstreamEnv } = require('./lib/active-workstream-store.cjs'); const state = require('./lib/state.cjs'); @@ -255,7 +256,7 @@ const { getEffectiveAuthority, classifyDriftSeverity } = require('./lib/plan-dri * @param {string} opts.cwd - project dir * @param {boolean} opts.raw - raw output mode * @param {Function} opts.error - error reporter - * @param {Function} opts.output - output emitter (core.output) + * @param {Function} opts.output - output emitter (output) */ function _dispatchNonFamily({ registryCommand, registryArgs, legacyCommand, legacyArgs, cwd, raw, error, output }) { void registryCommand; @@ -294,7 +295,7 @@ function _dispatchNonFamily({ registryCommand, registryArgs, legacyCommand, lega * @param {string[]} opts.args Remaining args passed to the router * @param {string} opts.cwd Project working directory * @param {boolean} opts.raw Raw output mode flag - * @param {Function} opts.error Error reporter (core.error) + * @param {Function} opts.error Error reporter (io.error) * @param {object} [opts.registry] Injectable registry (for tests) * @param {Function} [opts.requireModule] Injectable module loader (for tests) * @returns {boolean} true if the command was dispatched, false otherwise @@ -404,10 +405,10 @@ async function main() { // their plain-text diagnostic. const jsonErrorsIdx = args.indexOf('--json-errors'); if (jsonErrorsIdx !== -1) { - core.setJsonErrorMode(true); + setJsonErrorMode(true); args.splice(jsonErrorsIdx, 1); } else if (process.env.GSD_JSON_ERRORS === '1') { - core.setJsonErrorMode(true); + setJsonErrorMode(true); } // Optional cwd override for sandboxed subagents running outside project root. @@ -433,7 +434,7 @@ async function main() { // Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree. // However, in monorepo worktrees where the subdirectory itself owns .planning/, // skip worktree resolution — the CWD is already the correct project root. - const { resolveWorktreeRoot } = require('./lib/core.cjs'); + const { resolveWorktreeRoot } = require('./lib/worktree-safety.cjs'); if (!fs.existsSync(path.join(cwd, '.planning'))) { const worktreeRoot = resolveWorktreeRoot(cwd); if (worktreeRoot !== cwd) { @@ -596,7 +597,7 @@ async function main() { } // Intercept stdout to transparently resolve @file: references (#1891). - // core.cjs output() writes @file: when JSON > 50KB. The --pick path + // io.cjs output() writes @file: when JSON > 50KB. The --pick path // already resolves this, but the normal path wrote @file: to stdout, forcing // every workflow to have a bash-specific `if [[ "$INIT" == @file:* ]]` check // that breaks on PowerShell and other non-bash shells. @@ -802,7 +803,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand cwd, raw, error, - output: core.output, + output: output, }); if (!handled) phase.cmdFindPhase(cwd, args[1], raw); break; @@ -895,7 +896,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand cwd, raw, error, - output: core.output, + output: output, }); if (handled) break; } @@ -957,7 +958,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand cwd, raw, error, - output: core.output, + output: output, }); if (!handled) commands.cmdGenerateSlug(args[1], raw); break; @@ -997,7 +998,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand cwd, raw, error, - output: core.output, + output: output, }); if (!handled) config.cmdConfigEnsureSection(cwd, raw); break; @@ -1013,7 +1014,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand cwd, raw, error, - output: core.output, + output: output, }); if (!handled) config.cmdConfigSet(cwd, args[1], args[2], raw); break; @@ -1029,7 +1030,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand cwd, raw, error, - output: core.output, + output: output, }); if (!handled) config.cmdConfigSetModelProfile(cwd, args[1], raw); break; @@ -1054,7 +1055,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand cwd, raw, error, - output: core.output, + output: output, }); if (!handled) config.cmdConfigGet(cwd, args[1], raw, defaultValue); break; @@ -1070,7 +1071,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand cwd, raw, error, - output: core.output, + output: output, }); if (!handled) config.cmdConfigNewProject(cwd, args[1], raw); break; @@ -1183,7 +1184,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand args, cwd, raw, - output: core.output, + output: output, error, }); break; @@ -1255,12 +1256,12 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand const configDirIdx = args.indexOf('--config-dir'); if (configDirEqArg) { const value = configDirEqArg.slice('--config-dir='.length).trim(); - if (!value) error('Missing value for --config-dir', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined); + if (!value) error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined); loopConfigDir = value; } else if (configDirIdx !== -1) { const value = args[configDirIdx + 1]; if (!value || value.startsWith('--')) { - error('Missing value for --config-dir', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined); + error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined); } loopConfigDir = value; } @@ -1270,12 +1271,12 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand const activeCapIdx = args.indexOf('--active-cap'); if (activeCapEqArg) { const value = activeCapEqArg.slice('--active-cap='.length).trim(); - if (!value) error('Missing value for --active-cap (e.g. --active-cap tdd)', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined); + if (!value) error('Missing value for --active-cap (e.g. --active-cap tdd)', ERROR_REASON ? ERROR_REASON.USAGE : undefined); loopActiveCap = value; } else if (activeCapIdx !== -1) { const value = args[activeCapIdx + 1]; if (!value || value.startsWith('--')) { - error('Missing value for --active-cap (e.g. --active-cap tdd)', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined); + error('Missing value for --active-cap (e.g. --active-cap tdd)', ERROR_REASON ? ERROR_REASON.USAGE : undefined); } loopActiveCap = value; } @@ -1286,7 +1287,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand } else { error( `Unknown loop subcommand: ${loopSubcommand}. Available: render-hooks`, - core.ERROR_REASON ? core.ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined, + ERROR_REASON ? ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined, ); } break; @@ -1307,7 +1308,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand const configDirVal = args[configDirIdx + 1]; // Validate that --config-dir has a following non-flag value. if (!configDirVal || configDirVal.startsWith('--')) { - error('Missing value for --config-dir', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined); + error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined); } configDir = configDirVal; } @@ -1317,7 +1318,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand // capability set [--on|--off|--enable|--disable] [--gate =]... [--config-dir ] [--runtime ] [--scope ] const capId = args[2]; if (!capId || capId.startsWith('--')) { - error('Missing capability id for: capability set ', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined); + error('Missing capability id for: capability set ', ERROR_REASON ? ERROR_REASON.USAGE : undefined); } // Parse --config-dir const setConfigDirIdx = args.indexOf('--config-dir'); @@ -1325,7 +1326,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand if (setConfigDirIdx !== -1) { const setConfigDirVal = args[setConfigDirIdx + 1]; if (!setConfigDirVal || setConfigDirVal.startsWith('--')) { - error('Missing value for --config-dir', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined); + error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined); } setConfigDir = setConfigDirVal; } @@ -1334,7 +1335,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand const hasOn = args.includes('--on') || args.includes('--enable'); const hasOff = args.includes('--off') || args.includes('--disable'); if (hasOn && hasOff) { - error('Conflicting flags: --on/--enable and --off/--disable cannot both be present', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined); + error('Conflicting flags: --on/--enable and --off/--disable cannot both be present', ERROR_REASON ? ERROR_REASON.USAGE : undefined); } let setEnabled; if (hasOn) { @@ -1348,16 +1349,16 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand if (args[gi] === '--gate') { const gateVal = args[gi + 1]; if (!gateVal || gateVal.startsWith('--')) { - error('Missing value for --gate (expected =)', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined); + error('Missing value for --gate (expected =)', ERROR_REASON ? ERROR_REASON.USAGE : undefined); } const eqIdx = gateVal.indexOf('='); if (eqIdx === -1) { - error(`Malformed --gate value "${gateVal}": expected =`, core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined); + error(`Malformed --gate value "${gateVal}": expected =`, ERROR_REASON ? ERROR_REASON.USAGE : undefined); } const gateKey = gateVal.slice(0, eqIdx); const gateBoolStr = gateVal.slice(eqIdx + 1); if (gateBoolStr !== 'true' && gateBoolStr !== 'false') { - error(`Malformed --gate value "${gateVal}": bool must be true or false`, core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined); + error(`Malformed --gate value "${gateVal}": bool must be true or false`, ERROR_REASON ? ERROR_REASON.USAGE : undefined); } setGates[gateKey] = gateBoolStr === 'true'; gi++; // skip consumed value @@ -1369,7 +1370,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand if (runtimeIdx !== -1) { const runtimeVal = args[runtimeIdx + 1]; if (!runtimeVal || runtimeVal.startsWith('--')) { - error('Missing value for --runtime', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined); + error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined); } setRuntime = runtimeVal; } @@ -1378,7 +1379,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand if (scopeIdx !== -1) { const scopeVal = args[scopeIdx + 1]; if (!scopeVal || scopeVal.startsWith('--')) { - error('Missing value for --scope', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined); + error('Missing value for --scope', ERROR_REASON ? ERROR_REASON.USAGE : undefined); } setScope = scopeVal; } @@ -1392,7 +1393,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand } else { error( `Unknown capability subcommand: ${capSubcommand}. Available: state, set`, - core.ERROR_REASON ? core.ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined, + ERROR_REASON ? ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined, ); } break; @@ -1484,7 +1485,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand cwd, raw, error, - output: core.output, + output: output, }); if (!handled) docs.cmdDocsInit(cwd, raw); break; @@ -1810,7 +1811,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand // ─── Research Store ──────────────────────────────────────────────────── // // research-store get [--kind ] - // -> getResearch(cwd, key, { homeDir }); searches both tiers; core.output(result, raw) + // -> getResearch(cwd, key, { homeDir }); searches both tiers; output(result, raw) // (--kind is accepted for backward compatibility but no longer drives tier selection) // research-store put --content --source --provider

// --confidence --kind @@ -1834,7 +1835,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand } // --kind is accepted but no longer drives tier selection; getResearch searches both tiers const result = researchStore.getResearch(cwd, key, { homeDir }); - core.output(result, raw); + output(result, raw); } else if (subcommand === 'put') { const key = args[2]; if (!key || key.startsWith('--')) { @@ -1866,7 +1867,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand error('Usage: gsd-tools research-store put --content --source --provider

--confidence --kind ', ERROR_REASON.USAGE); } const entry = researchStore.putResearch(cwd, key, { content, source, provider, confidence, kind }, { homeDir }); - core.output(entry, raw); + output(entry, raw); } else { error('Unknown research-store subcommand. Available: get, put', ERROR_REASON.SDK_UNKNOWN_COMMAND); } @@ -1902,14 +1903,14 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand const { ecosystem = '', config: planConfig = {}, questions } = planInput; const homeDir = process.env.HOME || require('os').homedir(); const plan = researchProvider.planResearch({ questions, ecosystem, config: planConfig, cwd, homeDir }); - core.output(plan, raw); + output(plan, raw); break; } // ─── Classify Confidence ────────────────────────────────────────────── // // classify-confidence --provider [--package --ecosystem ] [--verified] - // -> classifyConfidence({ provider, verifiedAgainstOfficial, legitimacyVerdict }); core.output(result, raw) + // -> classifyConfidence({ provider, verifiedAgainstOfficial, legitimacyVerdict }); output(result, raw) // // legitimacyVerdict is CODE-COMPUTED via checkPackages — never caller-supplied — so an agent cannot self-assert OK→HIGH. @@ -1936,7 +1937,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand legitimacyVerdict = results[0] ? results[0].verdict : null; } const confidence = researchProvider.classifyConfidence({ provider, verifiedAgainstOfficial: verified, legitimacyVerdict }); - core.output({ provider, package: pkg || null, ecosystem: ecosystem || null, legitimacyVerdict, verified, confidence }, raw); + output({ provider, package: pkg || null, ecosystem: ecosystem || null, legitimacyVerdict, verified, confidence }, raw); break; } @@ -1980,7 +1981,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand } catch (pkgErr) { error(`package-legitimacy: ${pkgErr && pkgErr.message ? pkgErr.message : String(pkgErr)}`, ERROR_REASON.UNKNOWN); } - core.output(pkgResults, raw); + output(pkgResults, raw); break; } @@ -2101,7 +2102,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand } } - core.output({ valid: errors.length === 0, errors, slots }, raw); + output({ valid: errors.length === 0, errors, slots }, raw); break; } @@ -2114,7 +2115,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand // Read config.json directly for both plan_review.source_grounding_authority // and intel.enabled. Neither key is in the config-loader.cjs whitelist that - // core.loadConfig() returns; plan_review is only in config.cjs's private + // config-loader.cjs's loadConfig() whitelist does not return; plan_review is only in config.cjs's private // buildConfig(), and intel is a federated capability config key. let configuredAuthority = 'grep'; let intelEnabled = false; @@ -2138,7 +2139,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand if (subcommand === 'authority') { // Pass rawValue as 3rd arg so --raw returns unquoted string (not JSON) - core.output(effectiveAuthority, raw, effectiveAuthority); + output(effectiveAuthority, raw, effectiveAuthority); break; } @@ -2155,7 +2156,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand ? authVal : effectiveAuthority; const result = classifyDriftSeverity({ status: statusVal, authority: authorityForClassify }); - core.output(result, raw); + output(result, raw); break; } diff --git a/scripts/lint-core-spine-imports.allowlist.json b/scripts/lint-core-spine-imports.allowlist.json index 14ca24a72..40c9959ee 100644 --- a/scripts/lint-core-spine-imports.allowlist.json +++ b/scripts/lint-core-spine-imports.allowlist.json @@ -1,9 +1,4 @@ { "_comment": "Files allowed to import the core re-export spine during the staged retirement (issue #1268). Entries are REMOVED as each tranche migrates a leaf; the file + this lint are deleted in T-final.", - "allow": [ - "gsd-core/bin/gsd-tools.cjs", - "src/audit-command-router.cts", - "src/check-command-router.cts", - "src/intel-command-router.cts" - ] + "allow": [] } diff --git a/src/audit-command-router.cts b/src/audit-command-router.cts index fa40c3862..7b1bbcb37 100644 --- a/src/audit-command-router.cts +++ b/src/audit-command-router.cts @@ -25,7 +25,7 @@ */ // eslint-disable-next-line @typescript-eslint/no-require-imports -import core = require('./core.cjs'); +import io = require('./io.cjs'); // ─── Types ──────────────────────────────────────────────────────────────────── @@ -82,15 +82,15 @@ function routeAuditOpen({ args, cwd, raw, error, _audit, _core }: RouteAuditOpen void error; // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment const a: AuditModule = _audit ?? require('./audit.cjs'); - const c: CoreModule = _core ?? core; + const c: CoreModule = _core ?? io; const wantJson = args.includes('--json'); const result = a.auditOpenArtifacts(cwd); if (wantJson) { - // core.output JSON-stringifies its first arg; pass the object directly. + // io.output JSON-stringifies its first arg; pass the object directly. c.output(result, raw); } else { // Human-readable report must bypass JSON encoding — use the rawValue - // form (third arg) which core.output emits verbatim. + // form (third arg) which io.output emits verbatim. c.output(null, true, a.formatAuditReport(result)); } } diff --git a/src/check-command-router.cts b/src/check-command-router.cts index b93c59b8a..8dceaecbe 100644 --- a/src/check-command-router.cts +++ b/src/check-command-router.cts @@ -10,8 +10,14 @@ import fs from 'node:fs'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; // eslint-disable-next-line @typescript-eslint/no-require-imports -import core = require('./core.cjs'); -const { output, error, ERROR_REASON } = core; +import io = require('./io.cjs'); +const { output, error, ERROR_REASON } = io; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import planningWorkspaceMod = require('./planning-workspace.cjs'); +const { planningDir } = planningWorkspaceMod; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import phaseLocatorMod = require('./phase-locator.cjs'); +const { findPhaseInternal } = phaseLocatorMod; import { parseDecisions } from './decisions.cjs'; import type { Decision } from './decisions.cjs'; import { checkUiPresence } from './ui-safety-gate.cjs'; @@ -358,7 +364,7 @@ function cmdDecisionCoverageVerify(projectDir: string, args: string[], raw: bool * Invocable as: gsd_run check ui-plan-gate * * Uses checkUiPresence from ui-safety-gate.cjs — does NOT reimplement frontend detection. - * Uses getRoadmapPhaseInternal + findPhaseInternal from core.cjs for phase data. + * Uses getRoadmapPhaseWithFallback + findPhaseInternal from leaf modules for phase data. */ function findUiSpecInDir(phaseDir: string): string { if (!phaseDir || !fs.existsSync(phaseDir)) return ''; @@ -384,7 +390,7 @@ function findUiSpecInDir(phaseDir: string): string { * surface the miss — we do NOT silently degrade to frontend:false if the roadmap * exists but the phase header is absent. * (b) Runs checkUiPresence (frontend detection) — no reimplementation. - * (c) Resolves the phase directory via core.findPhaseInternal; checks for *-UI-SPEC.md. + * (c) Resolves the phase directory via findPhaseInternal (phase-locator.cjs); checks for *-UI-SPEC.md. * * Returns: { frontend, hasUiSpec, block, uiSpecPath, phaseLookupFailed } * block = frontend && !hasUiSpec @@ -407,10 +413,8 @@ function computeUiPlanGate(projectDir: string, phase: string): { const section = getRoadmapPhaseWithFallback(projectDir, phase); if (section === null) { // Distinguish: ROADMAP.md missing (no-roadmap project) vs phase not found in ROADMAP. - // core.planningDir(cwd) resolves the .planning/ root for workstream-aware paths. - const planDir: string = typeof (core as unknown as Record)['planningDir'] === 'function' - ? (core as unknown as Record string>)['planningDir'](projectDir) - : path.join(projectDir, '.planning'); + // planningDir(cwd) resolves the .planning/ root for workstream-aware paths. + const planDir: string = planningDir(projectDir); const roadmapPath = path.join(planDir, 'ROADMAP.md'); if (fs.existsSync(roadmapPath)) { // ROADMAP.md exists but phase was not found → surface the miss @@ -427,22 +431,18 @@ function computeUiPlanGate(projectDir: string, phase: string): { const frontend = presenceResult.hasUI; // (c) Resolve phase directory via findPhaseInternal and check for *-UI-SPEC.md - const coreModule = core as unknown as Record; let phaseDir = ''; try { - const findPhase = coreModule['findPhaseInternal'] as ((cwd: string, phase: string) => Record | string | null) | undefined; - if (typeof findPhase === 'function') { - const result = findPhase(projectDir, phase); - if (result && typeof result === 'object') { - // findPhaseInternal returns { directory: '', ... } - // directory is relative to cwd — resolve it to absolute. - const relDir = typeof result['directory'] === 'string' ? result['directory'] : ''; - if (relDir) { - phaseDir = path.resolve(projectDir, relDir); - } - } else if (typeof result === 'string') { - phaseDir = result; + const result = findPhaseInternal(projectDir, phase); + if (result && typeof result === 'object') { + // findPhaseInternal returns { directory: '', ... } + // directory is relative to cwd — resolve it to absolute. + const relDir = typeof result['directory'] === 'string' ? result['directory'] : ''; + if (relDir) { + phaseDir = path.resolve(projectDir, relDir); } + } else if (typeof result === 'string') { + phaseDir = result; } } catch { /* phase dir lookup failure → hasUiSpec=false */ } @@ -501,7 +501,7 @@ const UI_PATH_PATTERNS_RE = /\/(components|pages|views|screens|layouts|ui|fronte * same lookup as computeUiPlanGate — to determine if this is a frontend phase. * (b) Runs checkUiPresence (frontend detection) — no reimplementation. * (c) Checks git diff HEAD~1..HEAD for UI file changes in the current worktree. - * (d) Resolves the phase directory via core.findPhaseInternal; checks for *-UI-SPEC.md. + * (d) Resolves the phase directory via findPhaseInternal (phase-locator.cjs); checks for *-UI-SPEC.md. * * Returns: { frontend, hasUiFiles, hasUiSpec, block, message?, phaseLookupFailed? } * block = frontend && hasUiFiles && !hasUiSpec @@ -521,9 +521,7 @@ function computeUiSafetyGate(projectDir: string, phase: string): { try { const section = getRoadmapPhaseWithFallback(projectDir, phase); if (section === null) { - const planDir: string = typeof (core as unknown as Record)['planningDir'] === 'function' - ? (core as unknown as Record string>)['planningDir'](projectDir) - : path.join(projectDir, '.planning'); + const planDir: string = planningDir(projectDir); const roadmapPath = path.join(planDir, 'ROADMAP.md'); if (fs.existsSync(roadmapPath)) { phaseLookupFailed = true; @@ -554,20 +552,16 @@ function computeUiSafetyGate(projectDir: string, phase: string): { } catch { /* git unavailable or no prior commit — treat as no UI files changed */ } // (d) Resolve phase directory and check for *-UI-SPEC.md (same as computeUiPlanGate) - const coreModule = core as unknown as Record; let phaseDir = ''; try { - const findPhase = coreModule['findPhaseInternal'] as ((cwd: string, phase: string) => Record | string | null) | undefined; - if (typeof findPhase === 'function') { - const result = findPhase(projectDir, phase); - if (result && typeof result === 'object') { - const relDir = typeof result['directory'] === 'string' ? result['directory'] : ''; - if (relDir) { - phaseDir = path.resolve(projectDir, relDir); - } - } else if (typeof result === 'string') { - phaseDir = result; + const result = findPhaseInternal(projectDir, phase); + if (result && typeof result === 'object') { + const relDir = typeof result['directory'] === 'string' ? result['directory'] : ''; + if (relDir) { + phaseDir = path.resolve(projectDir, relDir); } + } else if (typeof result === 'string') { + phaseDir = result; } } catch { /* phase dir lookup failure → hasUiSpec=false */ } @@ -639,18 +633,14 @@ function cmdTddReviewCheckpoint(projectDir: string, args: string[], raw: boolean } // Resolve phase directory - const coreModule = core as unknown as Record; let phaseDir = ''; try { - const findPhase = coreModule['findPhaseInternal'] as ((cwd: string, phase: string) => Record | string | null) | undefined; - if (typeof findPhase === 'function') { - const result = findPhase(projectDir, phase); - if (result && typeof result === 'object') { - const relDir = typeof result['directory'] === 'string' ? result['directory'] : ''; - if (relDir) phaseDir = path.resolve(projectDir, relDir); - } else if (typeof result === 'string') { - phaseDir = result; - } + const result = findPhaseInternal(projectDir, phase); + if (result && typeof result === 'object') { + const relDir = typeof result['directory'] === 'string' ? result['directory'] : ''; + if (relDir) phaseDir = path.resolve(projectDir, relDir); + } else if (typeof result === 'string') { + phaseDir = result; } } catch { /* phase dir lookup failure */ } diff --git a/src/intel-command-router.cts b/src/intel-command-router.cts index ffbf0aebf..ab00f8d16 100644 --- a/src/intel-command-router.cts +++ b/src/intel-command-router.cts @@ -38,14 +38,17 @@ * the case block). */ -// eslint-disable-next-line @typescript-eslint/no-require-imports -import core = require('./core.cjs'); // eslint-disable-next-line @typescript-eslint/no-require-imports import io = require('./io.cjs'); // eslint-disable-next-line @typescript-eslint/no-require-imports +import coreUtils = require('./core-utils.cjs'); +// eslint-disable-next-line @typescript-eslint/no-require-imports import path = require('path'); const { ERROR_REASON } = io; +// Default CoreModule implementation assembled from leaf modules. +// _core seam overrides this entirely for test injection. +const _defaultCore = { output: io.output, timeAgo: coreUtils.timeAgo }; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -83,7 +86,7 @@ interface RouteIntelCommandOptions { function routeIntelCommand({ args, cwd, raw, error, _intel, _core }: RouteIntelCommandOptions): void { // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment const intel: IntelModule = _intel ?? require('./intel.cjs'); - const c: CoreModule = _core ?? core; + const c: CoreModule = _core ?? _defaultCore; const subcommand = args[1]; if (subcommand === 'query') {