fix: complete planningDir migration for config CRUD, template fill, and verify (#1986)
* fix(config): route CRUD through planningDir to honor GSD_PROJECT PR #1484 added planningDir(cwd) and the GSD_PROJECT env var so a workspace can host multiple projects under .planning/{project}/. loadConfig() in core.cjs (line 256) was migrated at the time, but the four CRUD entry points in config.cjs and the planningPaths() helper in core.cjs were left resolving against planningRoot(cwd). The result was a silent split-brain in any multi-project workspace: - cmdConfigGet, setConfigValue, ensureConfigFile, cmdConfigNewProject all wrote to and read from .planning/config.json - loadConfig read from .planning/{GSD_PROJECT}/config.json So `gsd-tools config-get workflow.discuss_mode` returned "unset" even when the value was correctly stored in the project-routed file, because the reader and writer pointed at different paths. planningPaths() carried a comment that "Shared paths (project, config) always resolve to the root .planning/" which described the original intent, but loadConfig() already contradicted that intent for config.json. project and config now both resolve through planningDir() so the contract matches the only function that successfully read config.json in the multi-project case. Single-project users (no GSD_PROJECT set) are unaffected: planningRoot() and planningDir() return the same path when no project is configured. Verification: in a workspace with .planning/projectA/config.json and GSD_PROJECT=projectA, `gsd-tools config-get workflow.discuss_mode` now returns the value instead of "Error: Key not found". Backward compat verified by running the same command without GSD_PROJECT in a single-project layout. Affected sites: - get-shit-done/bin/lib/config.cjs cmdConfigNewProject (line 199) - get-shit-done/bin/lib/config.cjs ensureConfigFile (line 244) - get-shit-done/bin/lib/config.cjs setConfigValue (line 294) - get-shit-done/bin/lib/config.cjs cmdConfigGet (line 367) - get-shit-done/bin/lib/core.cjs planningPaths.config (line 706) - get-shit-done/bin/lib/core.cjs planningPaths.project (line 705) * fix(template): emit project-aware references in template fill plan The template fill plan body hardcoded `@.planning/PROJECT.md`, `@.planning/ROADMAP.md`, and `@.planning/STATE.md` references. In a multi-project workspace these resolve to nothing because the actual project, roadmap, and state files live under .planning/{GSD_PROJECT}/. `gsd-tools verify references` reports them as missing on every PLAN.md generated by template fill in any GSD_PROJECT-routed workspace. Fix: route the references through planningDir(cwd), normalize via the existing toPosixPath helper for cross-platform path consistency, and embed them as `@<relative-path>` matching the phase-relative reference pattern used elsewhere in the file. Single-project users (no GSD_PROJECT set) get exactly the same output as before because planningDir() falls back to .planning/ when no project is active. Affected site: get-shit-done/bin/lib/template.cjs cmdTemplateFill plan branch (lines 142-145, the @.planning/ refs in the Context section). * fix(verify): planningDir for cmdValidateHealth and regenerateState cmdValidateHealth resolved projectPath and configPath via planningRoot(cwd) while ROADMAP/STATE/phases/requirements went through planningDir(cwd). The inconsistency reported "missing PROJECT.md" and "missing config.json" in multi-project layouts even when the project-routed copies existed and the config CRUD writers (now also routed by the previous commit in this PR) were writing to them. regenerateState (the /gsd:health --repair STATE.md regeneration path) hardcoded `See: .planning/PROJECT.md` in the generated body, which fails the same reference check it just regenerated for in any GSD_PROJECT-routed workspace. Fix: route both sites through planningDir(cwd). For regenerateState, derive a POSIX-style relative reference from the resolved path so the reference matches verify references' resolution rules. Also dropped the planningRoot import from verify.cjs since it is no longer used after this change. Single-project users (no GSD_PROJECT set) get the same paths as before: planningDir() falls back to .planning/ when no project is configured. Affected sites: - get-shit-done/bin/lib/verify.cjs cmdValidateHealth (lines 536-541) - get-shit-done/bin/lib/verify.cjs regenerateState repair (line 865) - get-shit-done/bin/lib/verify.cjs core.cjs import (line 8, dropped unused planningRoot)
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { output, error, planningRoot, withPlanningLock, CONFIG_DEFAULTS, atomicWriteFileSync } = require('./core.cjs');
|
||||
const { output, error, planningDir, withPlanningLock, CONFIG_DEFAULTS, atomicWriteFileSync } = require('./core.cjs');
|
||||
const {
|
||||
VALID_PROFILES,
|
||||
getAgentToModelMapForProfile,
|
||||
@@ -200,7 +200,7 @@ function buildNewProjectConfig(userChoices) {
|
||||
* Idempotent: if config.json already exists, returns { created: false }.
|
||||
*/
|
||||
function cmdConfigNewProject(cwd, choicesJson, raw) {
|
||||
const planningBase = planningRoot(cwd);
|
||||
const planningBase = planningDir(cwd);
|
||||
const configPath = path.join(planningBase, 'config.json');
|
||||
|
||||
// Idempotent: don't overwrite existing config
|
||||
@@ -245,7 +245,7 @@ function cmdConfigNewProject(cwd, choicesJson, raw) {
|
||||
* the happy path. But note that `error()` will still `exit(1)` out of the process.
|
||||
*/
|
||||
function ensureConfigFile(cwd) {
|
||||
const planningBase = planningRoot(cwd);
|
||||
const planningBase = planningDir(cwd);
|
||||
const configPath = path.join(planningBase, 'config.json');
|
||||
|
||||
// Ensure .planning directory exists
|
||||
@@ -295,7 +295,7 @@ function cmdConfigEnsureSection(cwd, raw) {
|
||||
* the happy path. But note that `error()` will still `exit(1)` out of the process.
|
||||
*/
|
||||
function setConfigValue(cwd, keyPath, parsedValue) {
|
||||
const configPath = path.join(planningRoot(cwd), 'config.json');
|
||||
const configPath = path.join(planningDir(cwd), 'config.json');
|
||||
|
||||
return withPlanningLock(cwd, () => {
|
||||
// Load existing config or start with empty object
|
||||
@@ -368,7 +368,7 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
||||
}
|
||||
|
||||
function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
||||
const configPath = path.join(planningRoot(cwd), 'config.json');
|
||||
const configPath = path.join(planningDir(cwd), 'config.json');
|
||||
const hasDefault = defaultValue !== undefined;
|
||||
|
||||
if (!keyPath) {
|
||||
|
||||
@@ -692,19 +692,23 @@ function planningRoot(cwd) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get common .planning file paths, workstream-aware.
|
||||
* Scoped paths (state, roadmap, phases, requirements) resolve to the active workstream.
|
||||
* Shared paths (project, config) always resolve to the root .planning/.
|
||||
* Get common .planning file paths, project-and-workstream-aware.
|
||||
*
|
||||
* All paths route through planningDir(cwd, ws), which honors the GSD_PROJECT
|
||||
* env var and active workstream. This matches loadConfig() above (line 256),
|
||||
* which has always read config.json via planningDir(cwd). Previously project
|
||||
* and config were resolved against the unrouted .planning/ root, which broke
|
||||
* `gsd-tools config-get` in multi-project layouts (the CRUD writers and the
|
||||
* reader pointed at different files).
|
||||
*/
|
||||
function planningPaths(cwd, ws) {
|
||||
const base = planningDir(cwd, ws);
|
||||
const root = path.join(cwd, '.planning');
|
||||
return {
|
||||
planning: base,
|
||||
state: path.join(base, 'STATE.md'),
|
||||
roadmap: path.join(base, 'ROADMAP.md'),
|
||||
project: path.join(root, 'PROJECT.md'),
|
||||
config: path.join(root, 'config.json'),
|
||||
project: path.join(base, 'PROJECT.md'),
|
||||
config: path.join(base, 'config.json'),
|
||||
phases: path.join(base, 'phases'),
|
||||
requirements: path.join(base, 'REQUIREMENTS.md'),
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { normalizePhaseName, findPhaseInternal, generateSlugInternal, normalizeMd, toPosixPath, output, error } = require('./core.cjs');
|
||||
const { normalizePhaseName, findPhaseInternal, generateSlugInternal, normalizeMd, toPosixPath, planningDir, output, error } = require('./core.cjs');
|
||||
const { reconstructFrontmatter } = require('./frontmatter.cjs');
|
||||
|
||||
function cmdTemplateSelect(cwd, planPath, raw) {
|
||||
@@ -131,6 +131,10 @@ function cmdTemplateFill(cwd, templateType, options, raw) {
|
||||
must_haves: { truths: [], artifacts: [], key_links: [] },
|
||||
...fields,
|
||||
};
|
||||
const planBase = planningDir(cwd);
|
||||
const projectRef = toPosixPath(path.relative(cwd, path.join(planBase, 'PROJECT.md')));
|
||||
const roadmapRef = toPosixPath(path.relative(cwd, path.join(planBase, 'ROADMAP.md')));
|
||||
const stateRef = toPosixPath(path.relative(cwd, path.join(planBase, 'STATE.md')));
|
||||
body = [
|
||||
`# Phase ${options.phase} Plan ${planNum}: [Title]`,
|
||||
'',
|
||||
@@ -140,9 +144,9 @@ function cmdTemplateFill(cwd, templateType, options, raw) {
|
||||
'- **Output:** [Concrete deliverable]',
|
||||
'',
|
||||
'## Context',
|
||||
'@.planning/PROJECT.md',
|
||||
'@.planning/ROADMAP.md',
|
||||
'@.planning/STATE.md',
|
||||
`@${projectRef}`,
|
||||
`@${roadmapRef}`,
|
||||
`@${stateRef}`,
|
||||
'',
|
||||
'## Tasks',
|
||||
'',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { safeReadFile, loadConfig, normalizePhaseName, escapeRegex, execGit, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, planningDir, planningRoot, output, error, checkAgentsInstalled, CONFIG_DEFAULTS } = require('./core.cjs');
|
||||
const { safeReadFile, loadConfig, normalizePhaseName, escapeRegex, execGit, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, planningDir, output, error, checkAgentsInstalled, CONFIG_DEFAULTS } = require('./core.cjs');
|
||||
const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
|
||||
const { writeStateMd } = require('./state.cjs');
|
||||
|
||||
@@ -534,11 +534,10 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
}
|
||||
|
||||
const planBase = planningDir(cwd);
|
||||
const planRoot = planningRoot(cwd);
|
||||
const projectPath = path.join(planRoot, 'PROJECT.md');
|
||||
const projectPath = path.join(planBase, 'PROJECT.md');
|
||||
const roadmapPath = path.join(planBase, 'ROADMAP.md');
|
||||
const statePath = path.join(planBase, 'STATE.md');
|
||||
const configPath = path.join(planRoot, 'config.json');
|
||||
const configPath = path.join(planBase, 'config.json');
|
||||
const phasesDir = path.join(planBase, 'phases');
|
||||
|
||||
const errors = [];
|
||||
@@ -865,9 +864,12 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
}
|
||||
// Generate minimal STATE.md from ROADMAP.md structure
|
||||
const milestone = getMilestoneInfo(cwd);
|
||||
const projectRef = path
|
||||
.relative(cwd, path.join(planningDir(cwd), 'PROJECT.md'))
|
||||
.split(path.sep).join('/');
|
||||
let stateContent = `# Session State\n\n`;
|
||||
stateContent += `## Project Reference\n\n`;
|
||||
stateContent += `See: .planning/PROJECT.md\n\n`;
|
||||
stateContent += `See: ${projectRef}\n\n`;
|
||||
stateContent += `## Position\n\n`;
|
||||
stateContent += `**Milestone:** ${milestone.version} ${milestone.name}\n`;
|
||||
stateContent += `**Current phase:** (determining...)\n`;
|
||||
|
||||
Reference in New Issue
Block a user