refactor(#1289): T5 — migrate the final idiom-hard callers off the core spine (#1290)

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 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-15 17:44:25 -04:00
committed by GitHub
parent b108f101b0
commit 76765bc24d
5 changed files with 96 additions and 107 deletions

View File

@@ -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:<path> when JSON > 50KB. The --pick path
// io.cjs output() writes @file:<path> 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 <id> [--on|--off|--enable|--disable] [--gate <key>=<bool>]... [--config-dir <dir>] [--runtime <r>] [--scope <s>]
const capId = args[2];
if (!capId || capId.startsWith('--')) {
error('Missing capability id for: capability set <id>', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
error('Missing capability id for: capability set <id>', 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 <key>=<true|false>)', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
error('Missing value for --gate (expected <key>=<true|false>)', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
}
const eqIdx = gateVal.indexOf('=');
if (eqIdx === -1) {
error(`Malformed --gate value "${gateVal}": expected <key>=<true|false>`, core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
error(`Malformed --gate value "${gateVal}": expected <key>=<true|false>`, 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 <key> [--kind <k>]
// -> 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 <key> --content <str> --source <s> --provider <p>
// --confidence <c> --kind <k>
@@ -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 <key> --content <str> --source <s> --provider <p> --confidence <c> --kind <k>', 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 <id> [--package <name> --ecosystem <npm|pypi|crates>] [--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;
}

View File

@@ -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": []
}

View File

@@ -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));
}
}

View File

@@ -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 <phase>
*
* 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<string, unknown>)['planningDir'] === 'function'
? (core as unknown as Record<string, (cwd: string) => 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<string, unknown>;
let phaseDir = '';
try {
const findPhase = coreModule['findPhaseInternal'] as ((cwd: string, phase: string) => Record<string, unknown> | string | null) | undefined;
if (typeof findPhase === 'function') {
const result = findPhase(projectDir, phase);
if (result && typeof result === 'object') {
// findPhaseInternal returns { directory: '<relative-posix-path>', ... }
// 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: '<relative-posix-path>', ... }
// 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<string, unknown>)['planningDir'] === 'function'
? (core as unknown as Record<string, (cwd: string) => 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<string, unknown>;
let phaseDir = '';
try {
const findPhase = coreModule['findPhaseInternal'] as ((cwd: string, phase: string) => Record<string, unknown> | 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<string, unknown>;
let phaseDir = '';
try {
const findPhase = coreModule['findPhaseInternal'] as ((cwd: string, phase: string) => Record<string, unknown> | 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 */ }

View File

@@ -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') {