refactor(#189): retire generated cjs artifacts from bin/lib

This commit is contained in:
Tom Boucher
2026-05-25 09:54:50 -04:00
parent 5a6ff5f81f
commit 30c572e537
40 changed files with 635 additions and 984 deletions

View File

@@ -17,7 +17,7 @@ const {
VALID_CONFIG_KEYS,
RUNTIME_STATE_KEYS,
DYNAMIC_KEY_PATTERNS,
} = require('./configuration.generated.cjs');
} = require('./configuration.cjs');
/**
* Returns true if keyPath is a valid config key (exact, dynamic pattern, or runtime state).

View File

@@ -671,7 +671,7 @@ function cmdConfigPath(cwd) {
* summary when --raw is set. Exits 0 in all cases (including no-op).
*/
async function cmdMigrateConfig(cwd, raw) {
const { migrateOnDisk } = require('./configuration.generated.cjs');
const { migrateOnDisk } = require('./configuration.cjs');
const ws = process.env.GSD_WORKSTREAM || null;
const report = await migrateOnDisk(cwd, ws || undefined);

View File

@@ -25,7 +25,7 @@ const {
setActiveWorkstream,
findContextMdIn,
} = require('./planning-workspace.cjs');
const { findProjectRoot } = require('./project-root.generated.cjs');
const { findProjectRoot } = require('./project-root.cjs');
// ─── Configuration Module (generated CJS mirror) ────────────────────────────
// Cycle 4: import canonical defaults + normalization primitives from the
@@ -36,7 +36,7 @@ const { findProjectRoot } = require('./project-root.generated.cjs');
const {
CONFIG_DEFAULTS: CANONICAL_CONFIG_DEFAULTS,
normalizeLegacyKeys,
} = require('./configuration.generated.cjs');
} = require('./configuration.cjs');
// ─── Path helpers ────────────────────────────────────────────────────────────

View File

@@ -1,19 +1,121 @@
'use strict';
/**
* Decisions Module — CJS adapter.
*
* The implementation is generated from sdk/src/query/decisions.ts and
* lives in decisions.generated.cjs. This file is a thin re-export so
* that existing call sites (gap-checker.cjs, tests) can continue to
* require('./decisions') unchanged.
*
* Exports (from generated file):
* - parseDecisions(content) — parse <decisions> blocks, returns {id, text, category, tags, trackable}[]
* CJS callers using only {id, text} safely ignore the extra fields.
* Accepts both numeric (D-42) and alphanumeric (D-INFRA-01) IDs.
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/decisions.ts
* Regenerate: cd sdk && npm run gen:decisions
*
* Shared parser for CONTEXT.md <decisions> blocks.
* Accepts both numeric (D-42) and alphanumeric (D-INFRA-01) IDs.
* Returns {id, text, category, tags, trackable} per decision.
* CJS callers that only use {id, text} safely ignore the extra fields.
*/
module.exports = require('./decisions.generated.cjs');
const DISCRETION_HEADINGS = new Set([
"claude's discretion",
'claudes discretion',
'claude discretion',
]);
const NON_TRACKABLE_TAGS = new Set(['informational', 'folded', 'deferred']);
/**
* Strip fenced code blocks from `content` so example `<decisions>` snippets
* inside ```` ``` ```` do not pollute the parser (review F11).
*/
function stripFencedCode(content) {
return content.replace(/```[\s\S]*?```/g, ' ').replace(/~~~[\s\S]*?~~~/g, ' ');
}
/**
* Extract the inner text of EVERY `<decisions>...</decisions>` block in
* order, concatenated by `\n\n`. Returns null when no block is present.
*
* CONTEXT.md may legitimately contain more than one block (for example, a
* "current decisions" block plus a "carry-over from prior phase" block);
* dropping all-but-the-first silently lost the second batch (review F13).
*/
function extractDecisionsBlock(content) {
const cleaned = stripFencedCode(content);
const matches = [...cleaned.matchAll(/<decisions>([\s\S]*?)<\/decisions>/g)];
if (matches.length === 0)
return null;
return matches.map((m) => m[1]).join('\n\n');
}
/**
* Parse trackable decisions from CONTEXT.md content.
*
* Returns ALL D-NN decisions found inside `<decisions>` (including
* non-trackable ones, with `trackable: false`). Callers that only want the
* gate-enforced decisions should filter `.filter(d => d.trackable)`.
*/
function parseDecisions(content) {
if (!content || typeof content !== 'string')
return [];
const block = extractDecisionsBlock(content);
if (block === null)
return [];
const lines = block.split(/\r?\n/);
const out = [];
let category = '';
let inDiscretion = false;
// Bullet line: `- **D-NN[ [tags]]:** text`
// Phase 6 (#3575): aligned to CJS regex — accepts alphanumeric IDs (D-01, D-INFRA-01, D-FOO_BAR)
// in addition to numeric-only IDs (D-42). The first character after `D-` must
// be alphanumeric, so malformed shapes like `D--foo` or `D-_bar` are rejected.
// CJS callers consume {id, text} and ignore the optional extras.
const bulletRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?\s*:\*\*\s*(.*)$/;
let current = null;
const flush = () => {
if (current) {
current.text = current.text.trim();
out.push(current);
current = null;
}
};
for (const line of lines) {
const trimmed = line.trim();
// Track category headings (`### Heading`)
const headingMatch = trimmed.match(/^###\s+(.+?)\s*$/);
if (headingMatch) {
flush();
category = headingMatch[1];
// Strip the full unicode-quote family so any rendering of "Claude's
// Discretion" (ASCII apostrophe, curly U+2019, U+2018, U+201A, U+201B,
// double-quote variants U+201C/D/E/F, etc.) collapses to the same key
// (review F20).
const normalized = category
.toLowerCase()
.replace(/[\u2018\u2019\u201A\u201B\u201C\u201D\u201E\u201F'"`]/g, '')
.trim();
inDiscretion = DISCRETION_HEADINGS.has(normalized);
continue;
}
const bulletMatch = line.match(bulletRe);
if (bulletMatch) {
flush();
const id = `D-${bulletMatch[1]}`;
const tags = bulletMatch[2]
? bulletMatch[2]
.split(',')
.map((t) => t.trim().toLowerCase())
.filter(Boolean)
: [];
const trackable = !inDiscretion && !tags.some((t) => NON_TRACKABLE_TAGS.has(t));
current = { id, text: bulletMatch[3], category, tags, trackable };
continue;
}
// Continuation line for current decision (indented with space OR tab,
// non-bullet, non-empty) — tab indentation must work too (review F12).
if (current && trimmed !== '' && !trimmed.startsWith('-') && /^[ \t]/.test(line)) {
current.text += ' ' + trimmed;
continue;
}
// Blank line or unrelated content terminates the current decision
if (trimmed === '') {
flush();
}
}
flush();
return out;
}
module.exports = { parseDecisions };

View File

@@ -1,121 +0,0 @@
'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/decisions.ts
* Regenerate: cd sdk && npm run gen:decisions
*
* Shared parser for CONTEXT.md <decisions> blocks.
* Accepts both numeric (D-42) and alphanumeric (D-INFRA-01) IDs.
* Returns {id, text, category, tags, trackable} per decision.
* CJS callers that only use {id, text} safely ignore the extra fields.
*/
const DISCRETION_HEADINGS = new Set([
"claude's discretion",
'claudes discretion',
'claude discretion',
]);
const NON_TRACKABLE_TAGS = new Set(['informational', 'folded', 'deferred']);
/**
* Strip fenced code blocks from `content` so example `<decisions>` snippets
* inside ```` ``` ```` do not pollute the parser (review F11).
*/
function stripFencedCode(content) {
return content.replace(/```[\s\S]*?```/g, ' ').replace(/~~~[\s\S]*?~~~/g, ' ');
}
/**
* Extract the inner text of EVERY `<decisions>...</decisions>` block in
* order, concatenated by `\n\n`. Returns null when no block is present.
*
* CONTEXT.md may legitimately contain more than one block (for example, a
* "current decisions" block plus a "carry-over from prior phase" block);
* dropping all-but-the-first silently lost the second batch (review F13).
*/
function extractDecisionsBlock(content) {
const cleaned = stripFencedCode(content);
const matches = [...cleaned.matchAll(/<decisions>([\s\S]*?)<\/decisions>/g)];
if (matches.length === 0)
return null;
return matches.map((m) => m[1]).join('\n\n');
}
/**
* Parse trackable decisions from CONTEXT.md content.
*
* Returns ALL D-NN decisions found inside `<decisions>` (including
* non-trackable ones, with `trackable: false`). Callers that only want the
* gate-enforced decisions should filter `.filter(d => d.trackable)`.
*/
function parseDecisions(content) {
if (!content || typeof content !== 'string')
return [];
const block = extractDecisionsBlock(content);
if (block === null)
return [];
const lines = block.split(/\r?\n/);
const out = [];
let category = '';
let inDiscretion = false;
// Bullet line: `- **D-NN[ [tags]]:** text`
// Phase 6 (#3575): aligned to CJS regex — accepts alphanumeric IDs (D-01, D-INFRA-01, D-FOO_BAR)
// in addition to numeric-only IDs (D-42). The first character after `D-` must
// be alphanumeric, so malformed shapes like `D--foo` or `D-_bar` are rejected.
// CJS callers consume {id, text} and ignore the optional extras.
const bulletRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?\s*:\*\*\s*(.*)$/;
let current = null;
const flush = () => {
if (current) {
current.text = current.text.trim();
out.push(current);
current = null;
}
};
for (const line of lines) {
const trimmed = line.trim();
// Track category headings (`### Heading`)
const headingMatch = trimmed.match(/^###\s+(.+?)\s*$/);
if (headingMatch) {
flush();
category = headingMatch[1];
// Strip the full unicode-quote family so any rendering of "Claude's
// Discretion" (ASCII apostrophe, curly U+2019, U+2018, U+201A, U+201B,
// double-quote variants U+201C/D/E/F, etc.) collapses to the same key
// (review F20).
const normalized = category
.toLowerCase()
.replace(/[\u2018\u2019\u201A\u201B\u201C\u201D\u201E\u201F'"`]/g, '')
.trim();
inDiscretion = DISCRETION_HEADINGS.has(normalized);
continue;
}
const bulletMatch = line.match(bulletRe);
if (bulletMatch) {
flush();
const id = `D-${bulletMatch[1]}`;
const tags = bulletMatch[2]
? bulletMatch[2]
.split(',')
.map((t) => t.trim().toLowerCase())
.filter(Boolean)
: [];
const trackable = !inDiscretion && !tags.some((t) => NON_TRACKABLE_TAGS.has(t));
current = { id, text: bulletMatch[3], category, tags, trackable };
continue;
}
// Continuation line for current decision (indented with space OR tab,
// non-bullet, non-empty) — tab indentation must work too (review F12).
if (current && trimmed !== '' && !trimmed.startsWith('-') && /^[ \t]/.test(line)) {
current.text += ' ' + trimmed;
continue;
}
// Blank line or unrelated content terminates the current decision
if (trimmed === '') {
flush();
}
}
flush();
return out;
}
module.exports = { parseDecisions };

View File

@@ -1,6 +1,6 @@
'use strict';
const { INIT_SUBCOMMANDS } = require('./command-aliases.generated.cjs');
const { INIT_SUBCOMMANDS } = require('./command-aliases.cjs');
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
const { output } = require('./core.cjs');

View File

@@ -1,6 +1,6 @@
'use strict';
const { PHASE_SUBCOMMANDS } = require('./command-aliases.generated.cjs');
const { PHASE_SUBCOMMANDS } = require('./command-aliases.cjs');
// ─── CommandRoutingHub (issue #3788, simplified in #175, typed in #176) ───────
const { createHub, ERROR_KINDS, makeInvalidArgs } = require('./command-routing-hub.cjs');

View File

@@ -1,202 +0,0 @@
'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/phase-lifecycle-policy.ts
* Regenerate: cd sdk && npm run gen:phase-lifecycle-policy
*
* Phase Lifecycle Policy — pure computation helpers for phase directory naming,
* roadmap entry generation, decimal-phase management, and ID computation.
* No I/O. No async. No filesystem operations.
*
* I/O adapter pattern (ADR-3524 §4): pure transforms extracted from the SDK;
* GSDError is replaced with plain throws that CJS callers can catch.
*
* References:
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
* - Issue #4 (open-gsd/get-shit-done-redux)
*/
// Lightweight stub replacing sdk/src/errors.js GSDError.
// CJS callers that need to translate to process.exit(1) should catch these.
class GSDError extends Error {
constructor(message, classification) {
super(message);
this.name = 'GSDError';
this.classification = classification;
}
}
// ErrorClassification values used by policy functions
const ErrorClassification = { Validation: 'Validation', Internal: 'Internal' };
// escapeRegex — inlined from sdk/dist/query/helpers.js
function escapeRegex(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function assertNoNullBytes(value, label) {
if (value.includes('\0')) {
throw new GSDError(`${label} contains null byte`, ErrorClassification.Validation);
}
}
function assertSafePhaseDirName(dirName, label = 'phase directory') {
if (/[/\\]|\.\./.test(dirName)) {
throw new GSDError(`${label} contains invalid path segments`, ErrorClassification.Validation);
}
}
function assertSafeProjectCode(code) {
if (code && /[/\\]|\.\./.test(code)) {
throw new GSDError('project_code contains invalid characters', ErrorClassification.Validation);
}
}
function generatePhaseSlug(text) {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.substring(0, 60);
}
function parseMultiwordArg(args, flag) {
const idx = args.indexOf(`--${flag}`);
if (idx === -1)
return null;
const tokens = [];
for (let i = idx + 1; i < args.length; i++) {
if (args[i].startsWith('--'))
break;
tokens.push(args[i]);
}
return tokens.length > 0 ? tokens.join(' ') : null;
}
function extractOneLinerFromBody(content) {
if (!content)
return null;
const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n*/, '');
// Find the first heading of any level (GFM section ATX headings)
const headingMatch = body.match(/^(#{1,6}\s[^\n]+\n)/m);
if (!headingMatch || headingMatch.index === undefined)
return null;
const afterHeading = body.slice(headingMatch.index + headingMatch[0].length);
// Bound to the first section: truncate at the next heading of any level
const nextHeadingMatch = afterHeading.match(/^#{1,6}\s/m);
const sectionScope = nextHeadingMatch && nextHeadingMatch.index !== undefined
? afterHeading.slice(0, nextHeadingMatch.index)
: afterHeading;
const boldMatch = sectionScope.match(/\*\*([^*]+)\*\*/);
return boldMatch ? boldMatch[1].trim() : null;
}
function scanSequentialMaxPhaseFromMilestone(milestoneContent) {
const phasePattern = /(?:^|\n)\s*(?:[-*]\s*(?:\[[x ]\]\s*)?|#{2,4}\s*|\*{1,2}\s*)Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
let maxPhase = 0;
let m;
while ((m = phasePattern.exec(milestoneContent)) !== null) {
const num = parseInt(m[1], 10);
if (num === 999)
continue;
if (num > maxPhase)
maxPhase = num;
}
return maxPhase;
}
function scanSequentialMaxPhaseFromDirs(dirNames) {
let maxPhase = 0;
const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)[A-Z]?(?:\.\d+)*-/i;
for (const dirName of dirNames) {
const match = dirNumPattern.exec(dirName);
if (!match)
continue;
const num = parseInt(match[1], 10);
if (num === 999)
continue;
if (num > maxPhase)
maxPhase = num;
}
return maxPhase;
}
function computeNextSequentialPhaseId(milestoneContent, dirNames) {
return Math.max(scanSequentialMaxPhaseFromMilestone(milestoneContent), scanSequentialMaxPhaseFromDirs(dirNames)) + 1;
}
function computePhaseDirectory(namingMode, descriptionSlug, prefix, nextSequentialPhaseId, customId) {
if (customId || namingMode === 'custom') {
const phaseId = customId || descriptionSlug.toUpperCase().replace(/-/g, '_');
if (!phaseId) {
throw new GSDError('--id required when phase_naming is "custom"', ErrorClassification.Validation);
}
assertSafePhaseDirName(String(phaseId), 'custom phase id');
const dirName = `${prefix}${phaseId}-${descriptionSlug}`;
assertSafePhaseDirName(dirName);
return { phaseId, dirName };
}
const phaseId = nextSequentialPhaseId;
const paddedNum = String(phaseId).padStart(2, '0');
const dirName = `${prefix}${paddedNum}-${descriptionSlug}`;
assertSafePhaseDirName(dirName);
return { phaseId, dirName };
}
function buildPhaseRoadmapEntry(phaseId, description, namingMode) {
const prevPhase = typeof phaseId === 'number' ? phaseId - 1 : null;
const dependsOn = namingMode === 'custom' || prevPhase === null || prevPhase < 1
? ''
: `\n**Depends on:** Phase ${prevPhase}`;
return `\n### Phase ${phaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${phaseId} to break down)\n`;
}
function collectDecimalSuffixesFromDirNames(basePhase, dirNames) {
const decimalSet = new Set();
const decimalPattern = new RegExp(`^(?:[A-Z][A-Z0-9]*-)?${escapeRegex(basePhase)}\\.(\\d+)`, 'i');
for (const dir of dirNames) {
const match = dir.match(decimalPattern);
if (match)
decimalSet.add(parseInt(match[1], 10));
}
return decimalSet;
}
function collectDecimalSuffixesFromRoadmap(basePhase, roadmapContent) {
const decimalSet = new Set();
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${escapeRegex(basePhase)}\\.(\\d+)\\s*:`, 'gi');
let match;
while ((match = phasePattern.exec(roadmapContent)) !== null) {
decimalSet.add(parseInt(match[1], 10));
}
return decimalSet;
}
function computeNextDecimalPhase(basePhase, decimalSet) {
const existing = Array.from(decimalSet)
.sort((a, b) => a - b)
.map((n) => `${basePhase}.${n}`);
const next = decimalSet.size === 0
? `${basePhase}.1`
: `${basePhase}.${Math.max(...decimalSet) + 1}`;
return { next, existing };
}
module.exports = {
GSDError,
assertNoNullBytes,
assertSafePhaseDirName,
assertSafeProjectCode,
generatePhaseSlug,
parseMultiwordArg,
extractOneLinerFromBody,
scanSequentialMaxPhaseFromMilestone,
scanSequentialMaxPhaseFromDirs,
computeNextSequentialPhaseId,
computePhaseDirectory,
buildPhaseRoadmapEntry,
collectDecimalSuffixesFromDirNames,
collectDecimalSuffixesFromRoadmap,
computeNextDecimalPhase,
};

View File

@@ -22,7 +22,7 @@ const { writeStateMd, readModifyWriteStateMd, stateExtractField, stateReplaceFie
const { formatGsdSlash, resolveRuntime } = require('./runtime-slash.cjs');
// Generated pure-computation helpers for cmdPhaseComplete (issue #4 fix).
// Source: sdk/src/query/phase-lifecycle.ts. Regenerate: node sdk/scripts/gen-phase-lifecycle.mjs
const { deriveProgressFromRoadmap, clampPercent } = require('./phase-lifecycle.generated.cjs');
const { deriveProgressFromRoadmap, clampPercent } = require('./phase-lifecycle.cjs');
// #2893 — strict canonical filter: `{padded_phase}-{NN}-PLAN.md` or `PLAN.md`.
// Documented in agents/gsd-planner.md (write_phase_prompt step). The wider

View File

@@ -1,47 +0,0 @@
'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/phase.ts
* Regenerate: cd sdk && npm run gen:phase
*
* Phase Query Module — pure helper functions shared between the CJS CLI and SDK.
* No I/O. No async. No filesystem operations.
*
* Scope: isCanonicalPlanFile, describeNonCanonicalPlans.
* Async query handlers (findPhase, phasePlanIndex) are I/O-bound and remain
* per-side per ADR-3524 §4.
*
* References:
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
* - Issue #4 (open-gsd/get-shit-done-redux)
*/
const isCanonicalPlanFile = (f) => f.endsWith('-PLAN.md') || f === 'PLAN.md';
// Regex constants closed over by describeNonCanonicalPlans (from phase.js module scope)
const PLAN_OUTLINE_RE = /-PLAN-OUTLINE\.md$/i;
const PLAN_PRE_BOUNCE_RE = /-PLAN.*\.pre-bounce\.md$/i;
const looksLikePlanFile = (f) =>
/\.md$/i.test(f)
&& /PLAN/i.test(f)
&& !PLAN_OUTLINE_RE.test(f)
&& !PLAN_PRE_BOUNCE_RE.test(f);
function describeNonCanonicalPlans(dirFiles, matchedFiles) {
const matched = new Set(matchedFiles);
const offenders = dirFiles.filter((f) => looksLikePlanFile(f) && !matched.has(f));
if (offenders.length === 0)
return null;
return (`Found ${offenders.length} plan-shaped file(s) in this phase that don't match the canonical `
+ `naming convention "{padded_phase}-{NN}-PLAN.md" (or bare "PLAN.md") and were skipped: `
+ offenders.map((f) => `"${f}"`).join(', ')
+ `. Rename to the canonical form (e.g. "01-01-PLAN.md") so the executor can detect them. `
+ `See agents/gsd-planner.md write_phase_prompt step for the full contract.`);
}
module.exports = {
isCanonicalPlanFile,
describeNonCanonicalPlans,
};

View File

@@ -1,6 +1,6 @@
'use strict';
const { PHASES_SUBCOMMANDS } = require('./command-aliases.generated.cjs');
const { PHASES_SUBCOMMANDS } = require('./command-aliases.cjs');
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
const { output } = require('./core.cjs');

View File

@@ -1,26 +1,97 @@
'use strict';
/**
* Plan Scan Module — CJS adapter.
*
* The implementation is generated from sdk/src/query/plan-scan.ts and
* lives in plan-scan.generated.cjs. This file is a thin re-export so
* that existing call sites (state.cjs, roadmap.cjs, init.cjs,
* workstream-inventory.cjs, and tests) can continue to require('./plan-scan')
* unchanged.
*
* Exports (from generated file):
* - scanPhasePlans(phaseDir) — canonical phase-plan scanner
* - isRootPlanFile(fileName) — extended filter including /PLAN/i slug layouts
* - isNestedPlanFile(fileName) — nested plans/ subdir filter
* - isRootSummaryFile(fileName) — flat summary file filter
* - isNestedSummaryFile(fileName) — nested summary file filter
*
* The isRootPlanFile helper uses /PLAN/i to match the extended slug layout
* (e.g. 5-PLAN-01-setup-database.md) in addition to bare and canonical forms.
* This was the fix for bug #3128 (roadmap.cjs plan-count regression).
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/plan-scan.ts
* Regenerate: cd sdk && npm run gen:plan-scan
*
* Plan Scan Module — detects plan and summary files in a phase directory.
* Supports both flat (pre-#3139) and nested (post-#3139) layouts.
*/
module.exports = require('./plan-scan.generated.cjs');
const { existsSync, readdirSync } = require('node:fs');
const { join } = require('node:path');
// Excluded derivative files
const PLAN_OUTLINE_RE = /-OUTLINE\.md$/i;
const PLAN_PRE_BOUNCE_RE = /\.pre-bounce\.md$/i;
function isRootPlanFile(fileName) {
if (PLAN_OUTLINE_RE.test(fileName))
return false;
if (PLAN_PRE_BOUNCE_RE.test(fileName))
return false;
if (fileName.endsWith('-PLAN.md') || fileName === 'PLAN.md')
return true;
return /\.md$/i.test(fileName) && /PLAN/i.test(fileName);
}
function isNestedPlanFile(fileName) {
if (PLAN_OUTLINE_RE.test(fileName))
return false;
if (PLAN_PRE_BOUNCE_RE.test(fileName))
return false;
return /^PLAN-\d+.*\.md$/i.test(fileName) || /-PLAN-\d+.*\.md$/i.test(fileName);
}
function isRootSummaryFile(fileName) {
return fileName.endsWith('-SUMMARY.md') || fileName === 'SUMMARY.md';
}
function isNestedSummaryFile(fileName) {
return /^SUMMARY-\d+.*\.md$/i.test(fileName) || /-SUMMARY-\d+.*\.md$/i.test(fileName);
}
function scanPhasePlans(phaseDir) {
let rootFiles;
try {
rootFiles = readdirSync(phaseDir);
}
catch {
return {
planCount: 0,
summaryCount: 0,
completed: false,
hasNestedPlans: false,
planFiles: [],
summaryFiles: [],
};
}
const rootPlanFiles = rootFiles.filter(isRootPlanFile);
const rootSummaryFiles = rootFiles.filter(isRootSummaryFile);
let nestedPlanFiles = [];
let nestedSummaryFiles = [];
let hasNestedPlans = false;
const nestedDir = join(phaseDir, 'plans');
if (existsSync(nestedDir)) {
try {
const nestedFiles = readdirSync(nestedDir);
nestedPlanFiles = nestedFiles.filter(isNestedPlanFile);
nestedSummaryFiles = nestedFiles.filter(isNestedSummaryFile);
hasNestedPlans = nestedPlanFiles.length > 0;
}
catch { /* ignore unreadable nested layout */ }
}
const planFiles = rootPlanFiles.concat(nestedPlanFiles);
const summaryFiles = rootSummaryFiles.concat(nestedSummaryFiles);
const planCount = planFiles.length;
const summaryCount = summaryFiles.length;
return {
planCount,
summaryCount,
completed: planCount > 0 && summaryCount >= planCount,
hasNestedPlans,
planFiles,
summaryFiles,
};
}
// CJS callers do: const scanPhasePlans = require('./plan-scan.cjs')
// and also destructure named exports — support both call styles.
module.exports = scanPhasePlans;
module.exports.scanPhasePlans = scanPhasePlans;
module.exports.isRootPlanFile = isRootPlanFile;
module.exports.isNestedPlanFile = isNestedPlanFile;
module.exports.isRootSummaryFile = isRootSummaryFile;
module.exports.isNestedSummaryFile = isNestedSummaryFile;

View File

@@ -1,97 +0,0 @@
'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/plan-scan.ts
* Regenerate: cd sdk && npm run gen:plan-scan
*
* Plan Scan Module — detects plan and summary files in a phase directory.
* Supports both flat (pre-#3139) and nested (post-#3139) layouts.
*/
const { existsSync, readdirSync } = require('node:fs');
const { join } = require('node:path');
// Excluded derivative files
const PLAN_OUTLINE_RE = /-OUTLINE\.md$/i;
const PLAN_PRE_BOUNCE_RE = /\.pre-bounce\.md$/i;
function isRootPlanFile(fileName) {
if (PLAN_OUTLINE_RE.test(fileName))
return false;
if (PLAN_PRE_BOUNCE_RE.test(fileName))
return false;
if (fileName.endsWith('-PLAN.md') || fileName === 'PLAN.md')
return true;
return /\.md$/i.test(fileName) && /PLAN/i.test(fileName);
}
function isNestedPlanFile(fileName) {
if (PLAN_OUTLINE_RE.test(fileName))
return false;
if (PLAN_PRE_BOUNCE_RE.test(fileName))
return false;
return /^PLAN-\d+.*\.md$/i.test(fileName) || /-PLAN-\d+.*\.md$/i.test(fileName);
}
function isRootSummaryFile(fileName) {
return fileName.endsWith('-SUMMARY.md') || fileName === 'SUMMARY.md';
}
function isNestedSummaryFile(fileName) {
return /^SUMMARY-\d+.*\.md$/i.test(fileName) || /-SUMMARY-\d+.*\.md$/i.test(fileName);
}
function scanPhasePlans(phaseDir) {
let rootFiles;
try {
rootFiles = readdirSync(phaseDir);
}
catch {
return {
planCount: 0,
summaryCount: 0,
completed: false,
hasNestedPlans: false,
planFiles: [],
summaryFiles: [],
};
}
const rootPlanFiles = rootFiles.filter(isRootPlanFile);
const rootSummaryFiles = rootFiles.filter(isRootSummaryFile);
let nestedPlanFiles = [];
let nestedSummaryFiles = [];
let hasNestedPlans = false;
const nestedDir = join(phaseDir, 'plans');
if (existsSync(nestedDir)) {
try {
const nestedFiles = readdirSync(nestedDir);
nestedPlanFiles = nestedFiles.filter(isNestedPlanFile);
nestedSummaryFiles = nestedFiles.filter(isNestedSummaryFile);
hasNestedPlans = nestedPlanFiles.length > 0;
}
catch { /* ignore unreadable nested layout */ }
}
const planFiles = rootPlanFiles.concat(nestedPlanFiles);
const summaryFiles = rootSummaryFiles.concat(nestedSummaryFiles);
const planCount = planFiles.length;
const summaryCount = summaryFiles.length;
return {
planCount,
summaryCount,
completed: planCount > 0 && summaryCount >= planCount,
hasNestedPlans,
planFiles,
summaryFiles,
};
}
// CJS callers do: const scanPhasePlans = require('./plan-scan.cjs')
// and also destructure named exports — support both call styles.
module.exports = scanPhasePlans;
module.exports.scanPhasePlans = scanPhasePlans;
module.exports.isRootPlanFile = isRootPlanFile;
module.exports.isNestedPlanFile = isNestedPlanFile;
module.exports.isRootSummaryFile = isRootSummaryFile;
module.exports.isNestedSummaryFile = isNestedSummaryFile;

View File

@@ -1,6 +1,6 @@
'use strict';
const { ROADMAP_SUBCOMMANDS } = require('./command-aliases.generated.cjs');
const { ROADMAP_SUBCOMMANDS } = require('./command-aliases.cjs');
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
const { output } = require('./core.cjs');

View File

@@ -1,21 +1,170 @@
'use strict';
/**
* Schema Detect Module — CJS adapter.
*
* The implementation is generated from sdk/src/query/schema-detect.ts and
* lives in schema-detect.generated.cjs. This file is a thin re-export so
* that existing call sites (verify.cjs and tests) can continue to
* require('./schema-detect') unchanged.
*
* Exports (from generated file):
* - SCHEMA_PATTERNS — ORM file pattern list
* - ORM_INFO — ORM push commands and evidence patterns
* - detectSchemaFiles(files) — detect schema-relevant files
* - detectSchemaOrm(ormName) — get ORM-specific push command info
* - checkSchemaDrift(changedFiles, executionLog, options) — check for drift
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/schema-detect.ts
* Regenerate: cd sdk && npm run gen:schema-detect
*
* Schema Drift Detection — detects schema-relevant file changes and verifies
* that the appropriate database push command was executed during a phase.
* This module does not read the filesystem directly.
*/
module.exports = require('./schema-detect.generated.cjs');
// ─── ORM Patterns ───────────────────────────────────────────────────────────
const SCHEMA_PATTERNS = [
{ pattern: /^src\/collections\/.*\.ts$/, orm: 'payload' },
{ pattern: /^src\/globals\/.*\.ts$/, orm: 'payload' },
{ pattern: /^prisma\/schema\.prisma$/, orm: 'prisma' },
{ pattern: /^prisma\/schema\/.*\.prisma$/, orm: 'prisma' },
{ pattern: /^drizzle\/schema\.ts$/, orm: 'drizzle' },
{ pattern: /^src\/db\/schema\.ts$/, orm: 'drizzle' },
{ pattern: /^drizzle\/.*\.ts$/, orm: 'drizzle' },
{ pattern: /^supabase\/migrations\/.*\.sql$/, orm: 'supabase' },
{ pattern: /^src\/entities\/.*\.ts$/, orm: 'typeorm' },
{ pattern: /^src\/migrations\/.*\.ts$/, orm: 'typeorm' },
];
// ─── Push Commands & Evidence Patterns ──────────────────────────────────────
const ORM_INFO = {
payload: {
pushCommand: 'npx payload migrate',
envHint: 'CI=true PAYLOAD_MIGRATING=true npx payload migrate',
interactiveWarning: 'Payload migrate may require interactive prompts — use CI=true PAYLOAD_MIGRATING=true to suppress',
evidencePatterns: [/payload\s+migrate/i, /PAYLOAD_MIGRATING/],
},
prisma: {
pushCommand: 'npx prisma db push',
envHint: 'npx prisma db push --accept-data-loss (if destructive changes are intended)',
interactiveWarning: 'Prisma db push may prompt for confirmation on destructive changes — use --accept-data-loss to bypass',
evidencePatterns: [/prisma\s+db\s+push/i, /prisma\s+migrate\s+deploy/i, /prisma\s+migrate\s+dev/i],
},
drizzle: {
pushCommand: 'npx drizzle-kit push',
envHint: 'npx drizzle-kit push',
interactiveWarning: null,
evidencePatterns: [/drizzle-kit\s+push/i, /drizzle-kit\s+migrate/i],
},
supabase: {
pushCommand: 'supabase db push',
envHint: 'supabase db push',
interactiveWarning: 'Supabase db push may require authentication — ensure SUPABASE_ACCESS_TOKEN is set',
evidencePatterns: [/supabase\s+db\s+push/i, /supabase\s+migration\s+up/i],
},
typeorm: {
pushCommand: 'npx typeorm migration:run',
envHint: 'npx typeorm migration:run -d src/data-source.ts',
interactiveWarning: null,
evidencePatterns: [/typeorm\s+migration:run/i, /typeorm\s+schema:sync/i],
},
};
// ─── Public API ──────────────────────────────────────────────────────────────
function detectSchemaFiles(files) {
const matches = [];
const orms = new Set();
for (const rawFile of files) {
const file = rawFile.replace(/\\/g, '/');
for (const { pattern, orm } of SCHEMA_PATTERNS) {
if (pattern.test(file)) {
matches.push(rawFile);
orms.add(orm);
break;
}
}
}
return {
detected: matches.length > 0,
matches,
orms: [...orms],
};
}
function detectSchemaOrm(ormName) {
return ORM_INFO[ormName] || null;
}
function checkSchemaDrift(changedFiles, executionLog, options = {}) {
const { skipCheck = false } = options;
const detection = detectSchemaFiles(changedFiles);
if (!detection.detected) {
return {
driftDetected: false,
blocking: false,
schemaFiles: [],
orms: [],
unpushedOrms: [],
message: '',
};
}
const pushedOrms = new Set();
const unpushedOrms = [];
for (const orm of detection.orms) {
const info = ORM_INFO[orm];
if (!info)
continue;
const hasPushEvidence = info.evidencePatterns.some(p => p.test(executionLog));
if (hasPushEvidence) {
pushedOrms.add(orm);
}
else {
unpushedOrms.push(orm);
}
}
const driftDetected = unpushedOrms.length > 0;
if (!driftDetected) {
return {
driftDetected: false,
blocking: false,
schemaFiles: detection.matches,
orms: detection.orms,
unpushedOrms: [],
message: '',
};
}
const pushCommands = unpushedOrms
.map(orm => {
const info = ORM_INFO[orm];
return info ? ` ${orm}: ${info.envHint || info.pushCommand}` : null;
})
.filter(Boolean)
.join('\n');
const message = [
'Schema drift detected: schema-relevant files changed but no database push was executed.',
'',
`Schema files changed: ${detection.matches.join(', ')}`,
`ORMs requiring push: ${unpushedOrms.join(', ')}`,
'',
'Required push commands:',
pushCommands,
'',
'Run the appropriate push command, or set GSD_SKIP_SCHEMA_CHECK=true to bypass this gate.',
].join('\n');
if (skipCheck) {
return {
driftDetected: true,
blocking: false,
skipped: true,
schemaFiles: detection.matches,
orms: detection.orms,
unpushedOrms,
message: 'Schema drift detected but check was skipped (GSD_SKIP_SCHEMA_CHECK=true).',
};
}
return {
driftDetected: true,
blocking: true,
schemaFiles: detection.matches,
orms: detection.orms,
unpushedOrms,
message,
};
}
module.exports = {
SCHEMA_PATTERNS,
ORM_INFO,
detectSchemaFiles,
detectSchemaOrm,
checkSchemaDrift,
};

View File

@@ -1,170 +0,0 @@
'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/schema-detect.ts
* Regenerate: cd sdk && npm run gen:schema-detect
*
* Schema Drift Detection — detects schema-relevant file changes and verifies
* that the appropriate database push command was executed during a phase.
* This module does not read the filesystem directly.
*/
// ─── ORM Patterns ───────────────────────────────────────────────────────────
const SCHEMA_PATTERNS = [
{ pattern: /^src\/collections\/.*\.ts$/, orm: 'payload' },
{ pattern: /^src\/globals\/.*\.ts$/, orm: 'payload' },
{ pattern: /^prisma\/schema\.prisma$/, orm: 'prisma' },
{ pattern: /^prisma\/schema\/.*\.prisma$/, orm: 'prisma' },
{ pattern: /^drizzle\/schema\.ts$/, orm: 'drizzle' },
{ pattern: /^src\/db\/schema\.ts$/, orm: 'drizzle' },
{ pattern: /^drizzle\/.*\.ts$/, orm: 'drizzle' },
{ pattern: /^supabase\/migrations\/.*\.sql$/, orm: 'supabase' },
{ pattern: /^src\/entities\/.*\.ts$/, orm: 'typeorm' },
{ pattern: /^src\/migrations\/.*\.ts$/, orm: 'typeorm' },
];
// ─── Push Commands & Evidence Patterns ──────────────────────────────────────
const ORM_INFO = {
payload: {
pushCommand: 'npx payload migrate',
envHint: 'CI=true PAYLOAD_MIGRATING=true npx payload migrate',
interactiveWarning: 'Payload migrate may require interactive prompts — use CI=true PAYLOAD_MIGRATING=true to suppress',
evidencePatterns: [/payload\s+migrate/i, /PAYLOAD_MIGRATING/],
},
prisma: {
pushCommand: 'npx prisma db push',
envHint: 'npx prisma db push --accept-data-loss (if destructive changes are intended)',
interactiveWarning: 'Prisma db push may prompt for confirmation on destructive changes — use --accept-data-loss to bypass',
evidencePatterns: [/prisma\s+db\s+push/i, /prisma\s+migrate\s+deploy/i, /prisma\s+migrate\s+dev/i],
},
drizzle: {
pushCommand: 'npx drizzle-kit push',
envHint: 'npx drizzle-kit push',
interactiveWarning: null,
evidencePatterns: [/drizzle-kit\s+push/i, /drizzle-kit\s+migrate/i],
},
supabase: {
pushCommand: 'supabase db push',
envHint: 'supabase db push',
interactiveWarning: 'Supabase db push may require authentication — ensure SUPABASE_ACCESS_TOKEN is set',
evidencePatterns: [/supabase\s+db\s+push/i, /supabase\s+migration\s+up/i],
},
typeorm: {
pushCommand: 'npx typeorm migration:run',
envHint: 'npx typeorm migration:run -d src/data-source.ts',
interactiveWarning: null,
evidencePatterns: [/typeorm\s+migration:run/i, /typeorm\s+schema:sync/i],
},
};
// ─── Public API ──────────────────────────────────────────────────────────────
function detectSchemaFiles(files) {
const matches = [];
const orms = new Set();
for (const rawFile of files) {
const file = rawFile.replace(/\\/g, '/');
for (const { pattern, orm } of SCHEMA_PATTERNS) {
if (pattern.test(file)) {
matches.push(rawFile);
orms.add(orm);
break;
}
}
}
return {
detected: matches.length > 0,
matches,
orms: [...orms],
};
}
function detectSchemaOrm(ormName) {
return ORM_INFO[ormName] || null;
}
function checkSchemaDrift(changedFiles, executionLog, options = {}) {
const { skipCheck = false } = options;
const detection = detectSchemaFiles(changedFiles);
if (!detection.detected) {
return {
driftDetected: false,
blocking: false,
schemaFiles: [],
orms: [],
unpushedOrms: [],
message: '',
};
}
const pushedOrms = new Set();
const unpushedOrms = [];
for (const orm of detection.orms) {
const info = ORM_INFO[orm];
if (!info)
continue;
const hasPushEvidence = info.evidencePatterns.some(p => p.test(executionLog));
if (hasPushEvidence) {
pushedOrms.add(orm);
}
else {
unpushedOrms.push(orm);
}
}
const driftDetected = unpushedOrms.length > 0;
if (!driftDetected) {
return {
driftDetected: false,
blocking: false,
schemaFiles: detection.matches,
orms: detection.orms,
unpushedOrms: [],
message: '',
};
}
const pushCommands = unpushedOrms
.map(orm => {
const info = ORM_INFO[orm];
return info ? ` ${orm}: ${info.envHint || info.pushCommand}` : null;
})
.filter(Boolean)
.join('\n');
const message = [
'Schema drift detected: schema-relevant files changed but no database push was executed.',
'',
`Schema files changed: ${detection.matches.join(', ')}`,
`ORMs requiring push: ${unpushedOrms.join(', ')}`,
'',
'Required push commands:',
pushCommands,
'',
'Run the appropriate push command, or set GSD_SKIP_SCHEMA_CHECK=true to bypass this gate.',
].join('\n');
if (skipCheck) {
return {
driftDetected: true,
blocking: false,
skipped: true,
schemaFiles: detection.matches,
orms: detection.orms,
unpushedOrms,
message: 'Schema drift detected but check was skipped (GSD_SKIP_SCHEMA_CHECK=true).',
};
}
return {
driftDetected: true,
blocking: true,
schemaFiles: detection.matches,
orms: detection.orms,
unpushedOrms,
message,
};
}
module.exports = {
SCHEMA_PATTERNS,
ORM_INFO,
detectSchemaFiles,
detectSchemaOrm,
checkSchemaDrift,
};

View File

@@ -1,20 +1,37 @@
'use strict';
/**
* Secrets Module — CJS adapter.
*
* The implementation is generated from sdk/src/query/secrets.ts and
* lives in secrets.generated.cjs. This file is a thin re-export so
* that existing call sites (config.cjs, init.cjs, and tests) can
* continue to require('./secrets') unchanged.
*
* Exports (from generated file):
* - SECRET_CONFIG_KEYS — Set of secret key paths
* - isSecretKey(keyPath) — returns true if keyPath is a secret
* - maskSecret(value) — masks a secret value
* - maskIfSecret(keyPath, value) — masks value only if keyPath is secret
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/secrets.ts
* Regenerate: cd sdk && npm run gen:secrets
*
* Secrets handling — masking convention for API keys and other
* credentials managed via /gsd-settings-integrations.
* This module does not read the filesystem.
*/
module.exports = require('./secrets.generated.cjs');
const SECRET_CONFIG_KEYS = new Set([
'brave_search',
'firecrawl',
'exa_search',
]);
function isSecretKey(keyPath) {
return SECRET_CONFIG_KEYS.has(keyPath);
}
function maskSecret(value) {
if (value === null || value === undefined || value === '')
return '(unset)';
const s = String(value);
if (s.length < 8)
return '****';
return '****' + s.slice(-4);
}
function maskIfSecret(keyPath, value) {
return isSecretKey(keyPath) ? maskSecret(value) : value;
}
module.exports = { SECRET_CONFIG_KEYS, isSecretKey, maskSecret, maskIfSecret };

View File

@@ -1,37 +0,0 @@
'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/secrets.ts
* Regenerate: cd sdk && npm run gen:secrets
*
* Secrets handling — masking convention for API keys and other
* credentials managed via /gsd-settings-integrations.
* This module does not read the filesystem.
*/
const SECRET_CONFIG_KEYS = new Set([
'brave_search',
'firecrawl',
'exa_search',
]);
function isSecretKey(keyPath) {
return SECRET_CONFIG_KEYS.has(keyPath);
}
function maskSecret(value) {
if (value === null || value === undefined || value === '')
return '(unset)';
const s = String(value);
if (s.length < 8)
return '****';
return '****' + s.slice(-4);
}
function maskIfSecret(keyPath, value) {
return isSecretKey(keyPath) ? maskSecret(value) : value;
}
module.exports = { SECRET_CONFIG_KEYS, isSecretKey, maskSecret, maskIfSecret };

View File

@@ -1,6 +1,6 @@
'use strict';
const { STATE_SUBCOMMANDS } = require('./command-aliases.generated.cjs');
const { STATE_SUBCOMMANDS } = require('./command-aliases.cjs');
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
const { output } = require('./core.cjs');
const {

View File

@@ -1,12 +1,127 @@
'use strict';
/**
* STATE.md Document Module — CJS adapter.
* GENERATED FILE — DO NOT EDIT.
*
* The implementation is generated from sdk/src/state/index.ts and
* lives in state-document.generated.cjs. This file is a thin re-export so
* that existing call sites (state.cjs, workstream-inventory.cjs, init.cjs,
* and tests) can continue to require('./state-document') unchanged.
* Source: sdk/src/state/index.ts
* Regenerate: cd sdk && npm run gen:state-document
*
* STATE.md Document Module — pure transforms for STATE.md text.
* This module does not read the filesystem and does not own persistence or locking.
*/
module.exports = require('./state-document.generated.cjs');
// Internal helpers
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function toFiniteNumber(value) {
const number = Number(value);
return Number.isFinite(number) ? number : null;
}
function existingProgressExceedsDerived(existingProgress, derivedProgress, key) {
const existing = toFiniteNumber(existingProgress[key]);
const derived = toFiniteNumber(derivedProgress[key]);
return existing !== null && derived !== null && existing > derived;
}
function stateExtractField(content, fieldName) {
const escaped = escapeRegex(fieldName);
const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*[ \\t]*(.+)`, 'i');
const boldMatch = content.match(boldPattern);
if (boldMatch)
return boldMatch[1].trim();
const plainPattern = new RegExp(`^${escaped}:[ \\t]*(.+)`, 'im');
const plainMatch = content.match(plainPattern);
return plainMatch ? plainMatch[1].trim() : null;
}
function stateReplaceField(content, fieldName, newValue) {
const escaped = escapeRegex(fieldName);
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
if (boldPattern.test(content)) {
return content.replace(boldPattern, (_match, prefix) => `${prefix}${newValue}`);
}
const plainPattern = new RegExp(`(^${escaped}:\\s*)(.*)`, 'im');
if (plainPattern.test(content)) {
return content.replace(plainPattern, (_match, prefix) => `${prefix}${newValue}`);
}
return null;
}
function stateReplaceFieldWithFallback(content, primary, fallback, value) {
let result = stateReplaceField(content, primary, value);
if (result)
return result;
if (fallback) {
result = stateReplaceField(content, fallback, value);
if (result)
return result;
}
return content;
}
function normalizeStateStatus(status, pausedAt) {
let normalizedStatus = status || 'unknown';
const statusLower = (status || '').toLowerCase();
if (statusLower.includes('paused') || statusLower.includes('stopped') || pausedAt) {
normalizedStatus = 'paused';
}
else if (statusLower.includes('executing') || statusLower.includes('in progress')) {
normalizedStatus = 'executing';
}
else if (statusLower.includes('planning') || statusLower.includes('ready to plan')) {
normalizedStatus = 'planning';
}
else if (statusLower.includes('discussing')) {
normalizedStatus = 'discussing';
}
else if (statusLower.includes('verif')) {
normalizedStatus = 'verifying';
}
else if (statusLower.includes('complete') || statusLower.includes('done')) {
normalizedStatus = 'completed';
}
else if (statusLower.includes('ready to execute')) {
normalizedStatus = 'executing';
}
return normalizedStatus;
}
function computeProgressPercent(completedPlans, totalPlans, completedPhases, totalPhases) {
const hasPlanData = totalPlans !== null && totalPlans > 0 && completedPlans !== null;
const hasPhaseData = totalPhases !== null && totalPhases > 0 && completedPhases !== null;
if (!hasPlanData && !hasPhaseData)
return null;
const planFraction = hasPlanData ? completedPlans / totalPlans : 1;
const phaseFraction = hasPhaseData ? completedPhases / totalPhases : 1;
return Math.min(100, Math.round(Math.min(planFraction, phaseFraction) * 100));
}
function shouldPreserveExistingProgress(existingProgress, derivedProgress) {
if (!existingProgress || typeof existingProgress !== 'object')
return false;
if (!derivedProgress || typeof derivedProgress !== 'object')
return false;
const existing = existingProgress;
const derived = derivedProgress;
return (existingProgressExceedsDerived(existing, derived, 'total_phases') ||
existingProgressExceedsDerived(existing, derived, 'completed_phases') ||
existingProgressExceedsDerived(existing, derived, 'total_plans') ||
existingProgressExceedsDerived(existing, derived, 'completed_plans'));
}
function normalizeProgressNumbers(progress) {
if (!progress || typeof progress !== 'object')
return progress;
const normalized = { ...progress };
for (const key of ['total_phases', 'completed_phases', 'total_plans', 'completed_plans', 'percent']) {
const number = toFiniteNumber(normalized[key]);
if (number !== null)
normalized[key] = number;
}
return normalized;
}
module.exports = { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, normalizeStateStatus, computeProgressPercent, shouldPreserveExistingProgress, normalizeProgressNumbers };

View File

@@ -1,127 +0,0 @@
'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/state/index.ts
* Regenerate: cd sdk && npm run gen:state-document
*
* STATE.md Document Module — pure transforms for STATE.md text.
* This module does not read the filesystem and does not own persistence or locking.
*/
// Internal helpers
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function toFiniteNumber(value) {
const number = Number(value);
return Number.isFinite(number) ? number : null;
}
function existingProgressExceedsDerived(existingProgress, derivedProgress, key) {
const existing = toFiniteNumber(existingProgress[key]);
const derived = toFiniteNumber(derivedProgress[key]);
return existing !== null && derived !== null && existing > derived;
}
function stateExtractField(content, fieldName) {
const escaped = escapeRegex(fieldName);
const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*[ \\t]*(.+)`, 'i');
const boldMatch = content.match(boldPattern);
if (boldMatch)
return boldMatch[1].trim();
const plainPattern = new RegExp(`^${escaped}:[ \\t]*(.+)`, 'im');
const plainMatch = content.match(plainPattern);
return plainMatch ? plainMatch[1].trim() : null;
}
function stateReplaceField(content, fieldName, newValue) {
const escaped = escapeRegex(fieldName);
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
if (boldPattern.test(content)) {
return content.replace(boldPattern, (_match, prefix) => `${prefix}${newValue}`);
}
const plainPattern = new RegExp(`(^${escaped}:\\s*)(.*)`, 'im');
if (plainPattern.test(content)) {
return content.replace(plainPattern, (_match, prefix) => `${prefix}${newValue}`);
}
return null;
}
function stateReplaceFieldWithFallback(content, primary, fallback, value) {
let result = stateReplaceField(content, primary, value);
if (result)
return result;
if (fallback) {
result = stateReplaceField(content, fallback, value);
if (result)
return result;
}
return content;
}
function normalizeStateStatus(status, pausedAt) {
let normalizedStatus = status || 'unknown';
const statusLower = (status || '').toLowerCase();
if (statusLower.includes('paused') || statusLower.includes('stopped') || pausedAt) {
normalizedStatus = 'paused';
}
else if (statusLower.includes('executing') || statusLower.includes('in progress')) {
normalizedStatus = 'executing';
}
else if (statusLower.includes('planning') || statusLower.includes('ready to plan')) {
normalizedStatus = 'planning';
}
else if (statusLower.includes('discussing')) {
normalizedStatus = 'discussing';
}
else if (statusLower.includes('verif')) {
normalizedStatus = 'verifying';
}
else if (statusLower.includes('complete') || statusLower.includes('done')) {
normalizedStatus = 'completed';
}
else if (statusLower.includes('ready to execute')) {
normalizedStatus = 'executing';
}
return normalizedStatus;
}
function computeProgressPercent(completedPlans, totalPlans, completedPhases, totalPhases) {
const hasPlanData = totalPlans !== null && totalPlans > 0 && completedPlans !== null;
const hasPhaseData = totalPhases !== null && totalPhases > 0 && completedPhases !== null;
if (!hasPlanData && !hasPhaseData)
return null;
const planFraction = hasPlanData ? completedPlans / totalPlans : 1;
const phaseFraction = hasPhaseData ? completedPhases / totalPhases : 1;
return Math.min(100, Math.round(Math.min(planFraction, phaseFraction) * 100));
}
function shouldPreserveExistingProgress(existingProgress, derivedProgress) {
if (!existingProgress || typeof existingProgress !== 'object')
return false;
if (!derivedProgress || typeof derivedProgress !== 'object')
return false;
const existing = existingProgress;
const derived = derivedProgress;
return (existingProgressExceedsDerived(existing, derived, 'total_phases') ||
existingProgressExceedsDerived(existing, derived, 'completed_phases') ||
existingProgressExceedsDerived(existing, derived, 'total_plans') ||
existingProgressExceedsDerived(existing, derived, 'completed_plans'));
}
function normalizeProgressNumbers(progress) {
if (!progress || typeof progress !== 'object')
return progress;
const normalized = { ...progress };
for (const key of ['total_phases', 'completed_phases', 'total_plans', 'completed_plans', 'percent']) {
const number = toFiniteNumber(normalized[key]);
if (number !== null)
normalized[key] = number;
}
return normalized;
}
module.exports = { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, normalizeStateStatus, computeProgressPercent, shouldPreserveExistingProgress, normalizeProgressNumbers };

View File

@@ -1,6 +1,6 @@
'use strict';
const { VALIDATE_SUBCOMMANDS } = require('./command-aliases.generated.cjs');
const { VALIDATE_SUBCOMMANDS } = require('./command-aliases.cjs');
const { formatGsdSlash, resolveRuntime } = require('./runtime-slash.cjs');
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
const { output } = require('./core.cjs');

View File

@@ -1,6 +1,6 @@
'use strict';
const { VERIFY_SUBCOMMANDS } = require('./command-aliases.generated.cjs');
const { VERIFY_SUBCOMMANDS } = require('./command-aliases.cjs');
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
const { output } = require('./core.cjs');

View File

@@ -7,7 +7,7 @@ const {
phaseVariants, buildRoadmapPhaseVariants, buildNotStartedPhaseVariants,
// Issue #26 exports (W005 regex, W006-archived regex constants, I001 helper)
phaseDirNameRe, PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem,
} = require('./validate.generated.cjs');
} = require('./validate.cjs');
const fs = require('fs');
const path = require('path');

View File

@@ -17,7 +17,7 @@ const { toPosixPath, readSubdirectories } = require('./core.cjs');
const scanPhasePlans = require('./plan-scan.cjs');
const { planningPaths, planningRoot, getActiveWorkstream } = require('./planning-workspace.cjs');
const { stateExtractField } = require('./state-document.cjs');
const { buildWorkstreamInventory, isCompletedInventory } = require('./workstream-inventory-builder.generated.cjs');
const { buildWorkstreamInventory, isCompletedInventory } = require('./workstream-inventory-builder.cjs');
function workstreamsRoot(cwd) {
return path.join(planningRoot(cwd), 'workstreams');

View File

@@ -1,19 +1,61 @@
'use strict';
/**
* Workstream Name Policy Module — CJS adapter.
*
* The implementation is generated from sdk/src/workstream-name-policy.ts and
* lives in workstream-name-policy.generated.cjs. This file is a thin re-export
* so that existing call sites (active-workstream-store.cjs,
* planning-workspace.cjs, workstream.cjs, and tests) can continue to
* require('./workstream-name-policy') unchanged.
*
* Exports (from generated file):
* - toWorkstreamSlug(name) — normalize to URL/filesystem slug
* - hasInvalidPathSegment(name) — true if name has slashes or dot-dot
* - isValidActiveWorkstreamName(name) — true if name passes all policy rules
* - validateWorkstreamName(name) — SDK alias for isValidActiveWorkstreamName
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/workstream-name-policy.ts
* Regenerate: cd sdk && npm run gen:workstream-name-policy
*
* Canonical workstream name validation and slug normalization.
* Used by active-workstream-store.cjs, planning-workspace.cjs, workstream.cjs.
*/
module.exports = require('./workstream-name-policy.generated.cjs');
const ACTIVE_WORKSTREAM_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
/**
* Validate a workstream name.
* Allowed: alphanumeric, hyphens, underscores, dots.
* Disallowed: empty, spaces, slashes, special chars, path traversal.
*
* Alias for isValidActiveWorkstreamName; provided for SDK-layer callers.
*/
function validateWorkstreamName(name) {
return isValidActiveWorkstreamName(name);
}
/**
* Convert a display name to a URL/filesystem-safe workstream slug.
* Lowercases, collapses non-alphanumeric runs to hyphens, strips leading/trailing hyphens.
*/
function toWorkstreamSlug(name) {
return String(name || '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
/**
* Returns true when `name` contains a path separator, a bare dot, or a
* dot-dot sequence — any of which would make the name unsafe for use as a
* filesystem path segment.
*/
function hasInvalidPathSegment(name) {
const value = String(name || '');
return /[/\\]/.test(value) || value === '.' || value === '..' || value.includes('..');
}
/**
* Returns true when `name` is a valid active workstream name:
* - Must start with alphanumeric
* - May contain alphanumeric, dots, underscores, hyphens
* - Must not contain path traversal sequences (..)
*/
function isValidActiveWorkstreamName(name) {
const value = String(name || '');
if (value === '..' || value.startsWith('../') || value.includes('..'))
return false;
return ACTIVE_WORKSTREAM_RE.test(value);
}
module.exports = {
validateWorkstreamName,
toWorkstreamSlug,
hasInvalidPathSegment,
isValidActiveWorkstreamName,
};

View File

@@ -1,61 +0,0 @@
'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/workstream-name-policy.ts
* Regenerate: cd sdk && npm run gen:workstream-name-policy
*
* Canonical workstream name validation and slug normalization.
* Used by active-workstream-store.cjs, planning-workspace.cjs, workstream.cjs.
*/
const ACTIVE_WORKSTREAM_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
/**
* Validate a workstream name.
* Allowed: alphanumeric, hyphens, underscores, dots.
* Disallowed: empty, spaces, slashes, special chars, path traversal.
*
* Alias for isValidActiveWorkstreamName; provided for SDK-layer callers.
*/
function validateWorkstreamName(name) {
return isValidActiveWorkstreamName(name);
}
/**
* Convert a display name to a URL/filesystem-safe workstream slug.
* Lowercases, collapses non-alphanumeric runs to hyphens, strips leading/trailing hyphens.
*/
function toWorkstreamSlug(name) {
return String(name || '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
/**
* Returns true when `name` contains a path separator, a bare dot, or a
* dot-dot sequence — any of which would make the name unsafe for use as a
* filesystem path segment.
*/
function hasInvalidPathSegment(name) {
const value = String(name || '');
return /[/\\]/.test(value) || value === '.' || value === '..' || value.includes('..');
}
/**
* Returns true when `name` is a valid active workstream name:
* - Must start with alphanumeric
* - May contain alphanumeric, dots, underscores, hyphens
* - Must not contain path traversal sequences (..)
*/
function isValidActiveWorkstreamName(name) {
const value = String(name || '');
if (value === '..' || value.startsWith('../') || value.includes('..'))
return false;
return ACTIVE_WORKSTREAM_RE.test(value);
}
module.exports = {
validateWorkstreamName,
toWorkstreamSlug,
hasInvalidPathSegment,
isValidActiveWorkstreamName,
};

20
package-lock.json generated
View File

@@ -18,10 +18,12 @@
"gsd-tools": "bin/gsd-sdk.js"
},
"devDependencies": {
"c8": "^11.0.0"
"c8": "^11.0.0",
"typescript": "^5.9.3"
},
"engines": {
"node": ">=22.0.0"
"node": ">=22.0.0",
"npm": ">=10.0.0"
},
"optionalDependencies": {
"fallow": "^2.70.0"
@@ -1970,6 +1972,20 @@
"node": ">= 0.6"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",

View File

@@ -56,7 +56,8 @@
"ws": "8.20.1"
},
"devDependencies": {
"c8": "^11.0.0"
"c8": "^11.0.0",
"typescript": "^5.9.3"
},
"optionalDependencies": {
"fallow": "^2.70.0"

View File

@@ -7,7 +7,7 @@
* I001 canonicalPlanStem.
*
* After the generator migration, all three helpers are sourced from
* validate.generated.cjs. If they diverge from validate.ts, these
* validate.cjs. If they diverge from validate.ts, these
* tests go RED.
*
* References:
@@ -62,7 +62,7 @@ function writeConfigJson(planningDir) {
// node .claude/get-shit-done/bin/gsd-tools.cjs validate health
// # Bug: emits W005 about 999.1-foo not following NN-name format
//
// verify.cjs must consume phaseDirNameRe from validate.generated.cjs so
// verify.cjs must consume phaseDirNameRe from validate.cjs so
// the regex /^\d{2,}(?:\.\d+)*-[\w-]+$/ is the single source of truth.
describe('Drift item W005 — phaseDirNameRe: 999.X-name dirs must not trigger W005', () => {
@@ -95,10 +95,10 @@ describe('Drift item W005 — phaseDirNameRe: 999.X-name dirs must not trigger W
`Expected zero W005 for 999.1-foo, got: ${JSON.stringify(w005)}`);
});
test('phaseDirNameRe is exported from validate.generated.cjs', () => {
const gen = require('../get-shit-done/bin/lib/validate.generated.cjs');
test('phaseDirNameRe is exported from validate.cjs', () => {
const gen = require('../get-shit-done/bin/lib/validate.cjs');
assert.ok(gen.phaseDirNameRe instanceof RegExp,
'validate.generated.cjs must export phaseDirNameRe as a RegExp');
'validate.cjs must export phaseDirNameRe as a RegExp');
const re = gen.phaseDirNameRe;
assert.ok(re.test('01-setup'), 'should accept 01-setup');
assert.ok(re.test('999-longphase'), 'should accept 999-longphase (3-digit prefix)');
@@ -110,7 +110,7 @@ describe('Drift item W005 — phaseDirNameRe: 999.X-name dirs must not trigger W
// ── Drift Item W006-archived: PHASE_TOKEN_FROM_DIR_RE / MILESTONE_ARCHIVE_DIR_RE ─
//
// forEachArchivedPhaseToken() in verify.cjs uses two inline regex constants.
// After migration both are sourced from validate.generated.cjs:
// After migration both are sourced from validate.cjs:
// PHASE_TOKEN_FROM_DIR_RE — extracts token from dir name like "64-auth-service"
// MILESTONE_ARCHIVE_DIR_RE — matches archive dirs like "v1.0-phases"
//
@@ -173,9 +173,9 @@ describe('Drift item W006-archived — MILESTONE_ARCHIVE_DIR_RE and PHASE_TOKEN_
});
test('MILESTONE_ARCHIVE_DIR_RE is exported and matches vN.N-phases dirs', () => {
const gen = require('../get-shit-done/bin/lib/validate.generated.cjs');
const gen = require('../get-shit-done/bin/lib/validate.cjs');
assert.ok(gen.MILESTONE_ARCHIVE_DIR_RE instanceof RegExp,
'validate.generated.cjs must export MILESTONE_ARCHIVE_DIR_RE');
'validate.cjs must export MILESTONE_ARCHIVE_DIR_RE');
const re = gen.MILESTONE_ARCHIVE_DIR_RE;
assert.ok(re.test('v1.0-phases'), 'should match v1.0-phases');
assert.ok(re.test('v1.10-phases'), 'should match v1.10-phases');
@@ -184,9 +184,9 @@ describe('Drift item W006-archived — MILESTONE_ARCHIVE_DIR_RE and PHASE_TOKEN_
});
test('PHASE_TOKEN_FROM_DIR_RE is exported and extracts phase tokens correctly', () => {
const gen = require('../get-shit-done/bin/lib/validate.generated.cjs');
const gen = require('../get-shit-done/bin/lib/validate.cjs');
assert.ok(gen.PHASE_TOKEN_FROM_DIR_RE instanceof RegExp,
'validate.generated.cjs must export PHASE_TOKEN_FROM_DIR_RE');
'validate.cjs must export PHASE_TOKEN_FROM_DIR_RE');
const re = gen.PHASE_TOKEN_FROM_DIR_RE;
assert.strictEqual(re.exec('64-auth-service')?.[1], '64');
assert.strictEqual(re.exec('03B-feature')?.[1], '03B');
@@ -199,7 +199,7 @@ describe('Drift item W006-archived — MILESTONE_ARCHIVE_DIR_RE and PHASE_TOKEN_
//
// validate.ts Check 7: canonicalPlanStem('68-01-scaffolding') → '68-01'
// verify.cjs had an inline copy. After migration, canonicalPlanStem is
// sourced from validate.generated.cjs.
// sourced from validate.cjs.
//
// Test: "68-01-scaffolding-PLAN.md" + "68-01-SUMMARY.md" → no I001
// Both stems canonicalize to "68-01" → match found → I001 suppressed.
@@ -238,10 +238,10 @@ describe('Drift item I001 — canonicalPlanStem: long PLAN stem matches short SU
`Expected zero I001, got: ${JSON.stringify(i001)}`);
});
test('canonicalPlanStem is exported from validate.generated.cjs', () => {
const gen = require('../get-shit-done/bin/lib/validate.generated.cjs');
test('canonicalPlanStem is exported from validate.cjs', () => {
const gen = require('../get-shit-done/bin/lib/validate.cjs');
assert.strictEqual(typeof gen.canonicalPlanStem, 'function',
'validate.generated.cjs must export canonicalPlanStem as a function');
'validate.cjs must export canonicalPlanStem as a function');
assert.strictEqual(gen.canonicalPlanStem('68-01-scaffolding'), '68-01');
assert.strictEqual(gen.canonicalPlanStem('68-01'), '68-01');
assert.strictEqual(gen.canonicalPlanStem('3A-01-feature'), '3A-01');

View File

@@ -1,5 +1,5 @@
/**
* Regression test for #3571: configuration.generated.cjs used the source
* Regression test for #3571: configuration.cjs used the source
* checkout sdk/shared path only, which breaks installed gsd-tools.cjs because
* runtime installs copy get-shit-done/ but not sdk/.
*/
@@ -15,7 +15,7 @@ const os = require('node:os');
const path = require('node:path');
const REPO_ROOT = path.join(__dirname, '..');
const CONFIGURATION_CJS = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'configuration.generated.cjs');
const CONFIGURATION_CJS = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'configuration.cjs');
const SDK_SHARED_DIR = path.join(REPO_ROOT, 'sdk', 'shared');
const { install } = require('../bin/install.js');
@@ -68,14 +68,14 @@ describe('bug #3571: configuration generated manifests resolve in install layout
fs.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
});
test('co-located bin/shared manifests let configuration.generated.cjs load without sdk/shared', () => {
test('co-located bin/shared manifests let configuration.cjs load without sdk/shared', () => {
const gsdBinDir = path.join(tmpRoot, '.codex', 'get-shit-done', 'bin');
const gsdLibDir = path.join(gsdBinDir, 'lib');
const gsdSharedDir = path.join(gsdBinDir, 'shared');
fs.mkdirSync(gsdLibDir, { recursive: true });
fs.mkdirSync(gsdSharedDir, { recursive: true });
const installedCjs = path.join(gsdLibDir, 'configuration.generated.cjs');
const installedCjs = path.join(gsdLibDir, 'configuration.cjs');
fs.copyFileSync(CONFIGURATION_CJS, installedCjs);
fs.copyFileSync(
path.join(SDK_SHARED_DIR, 'config-defaults.manifest.json'),
@@ -90,7 +90,7 @@ describe('bug #3571: configuration generated manifests resolve in install layout
let mod;
assert.doesNotThrow(() => {
mod = require(installedCjs);
}, 'installed configuration.generated.cjs must not require ~/.codex/sdk/shared');
}, 'installed configuration.cjs must not require ~/.codex/sdk/shared');
assert.ok(mod.VALID_CONFIG_KEYS.has('workflow.plan_review_convergence'));
});
@@ -118,12 +118,12 @@ describe('bug #3571: configuration generated manifests resolve in install layout
'get-shit-done',
'bin',
'lib',
'configuration.generated.cjs'
'configuration.cjs'
);
delete require.cache[installedCjs];
assert.doesNotThrow(() => {
require(installedCjs);
}, 'post-install configuration.generated.cjs must load from co-located manifests');
}, 'post-install configuration.cjs must load from co-located manifests');
});
});

View File

@@ -1,7 +1,7 @@
'use strict';
/**
* Regression guard for issue #3251:
* 14 commands used in workflows must be present in command-aliases.generated.cjs.
* 14 commands used in workflows must be present in command-aliases.cjs.
*
* Asserts structurally by requiring the manifest and checking each canonical
* command appears in either the family arrays or the non-family array.
@@ -22,7 +22,7 @@ const COMMAND_ALIASES_FILE = path.join(
'get-shit-done',
'bin',
'lib',
'command-aliases.generated.cjs',
'command-aliases.cjs',
);
const GSD_TOOLS = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'gsd-tools.cjs');
@@ -43,7 +43,7 @@ const MISSING_14 = [
'workstream.list',
];
describe('feat-3251: command-aliases.generated.cjs manifest coverage', () => {
describe('feat-3251: command-aliases.cjs manifest coverage', () => {
let manifest;
test('manifest file can be required without error', () => {
@@ -59,7 +59,7 @@ describe('feat-3251: command-aliases.generated.cjs manifest coverage', () => {
manifest = manifest ?? require(COMMAND_ALIASES_FILE);
assert.ok(
Array.isArray(manifest.NON_FAMILY_COMMAND_ALIASES),
'NON_FAMILY_COMMAND_ALIASES must be an exported array in command-aliases.generated.cjs',
'NON_FAMILY_COMMAND_ALIASES must be an exported array in command-aliases.cjs',
);
});

View File

@@ -26,7 +26,7 @@ const {
const {
CONFIG_DEFAULTS: CANONICAL_CONFIG_DEFAULTS,
} = require('../get-shit-done/bin/lib/configuration.generated.cjs');
} = require('../get-shit-done/bin/lib/configuration.cjs');
const {
makeStatusProject,

View File

@@ -14,7 +14,7 @@ const path = require('path');
const REPO_ROOT = path.join(__dirname, '..');
const REGISTRY_FILE = path.join(REPO_ROOT, 'sdk', 'src', 'query', 'registry-assembly.ts');
const COMMAND_ALIASES_FILE = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'command-aliases.generated.cjs');
const COMMAND_ALIASES_FILE = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'command-aliases.cjs');
// Prose tokens that repeatedly appear after `gsd-sdk query` in English
// documentation but aren't real command names.