Merge branch 'next' into fix/2003-capability-state-runtime-flag
This commit is contained in:
@@ -33,6 +33,7 @@ export const CLUSTERS: ClusterMap = Object.freeze({
|
||||
core_loop: Object.freeze([
|
||||
'next',
|
||||
'new-project',
|
||||
'onboard',
|
||||
'discuss-phase',
|
||||
'plan-phase',
|
||||
'execute-phase',
|
||||
|
||||
@@ -296,6 +296,14 @@ export const INIT_COMMAND_ALIASES: CommandAlias[] = [
|
||||
"subcommand": "new-milestone",
|
||||
"mutation": false
|
||||
},
|
||||
{
|
||||
"canonical": "init.onboard",
|
||||
"aliases": [
|
||||
"init onboard"
|
||||
],
|
||||
"subcommand": "onboard",
|
||||
"mutation": false
|
||||
},
|
||||
{
|
||||
"canonical": "init.quick",
|
||||
"aliases": [
|
||||
|
||||
@@ -28,6 +28,7 @@ interface InitModule {
|
||||
cmdInitPlanPhase(cwd: string, phase: string | undefined, raw: boolean, opts: Record<string, string | boolean | null>): void;
|
||||
cmdInitNewProject(cwd: string, raw: boolean): void;
|
||||
cmdInitNewMilestone(cwd: string, raw: boolean): void;
|
||||
cmdInitOnboard(cwd: string, raw: boolean, opts?: Record<string, string | boolean | null>): void;
|
||||
cmdInitQuick(cwd: string, name: string, raw: boolean): void;
|
||||
cmdInitIngestDocs(cwd: string, raw: boolean): void;
|
||||
cmdInitResume(cwd: string, raw: boolean): void;
|
||||
@@ -71,6 +72,10 @@ function routeInitCommand({ init, args, cwd, raw, error }: RouteInitCommandOptio
|
||||
},
|
||||
'new-project': () => init.cmdInitNewProject(cwd, raw),
|
||||
'new-milestone': () => init.cmdInitNewMilestone(cwd, raw),
|
||||
onboard: () => {
|
||||
const namedArgs = parseNamedArgs(args, [], ['fast', 'text']);
|
||||
init.cmdInitOnboard(cwd, raw, { fast: namedArgs['fast'], text: namedArgs['text'] });
|
||||
},
|
||||
quick: () => init.cmdInitQuick(cwd, args.slice(2).join(' '), raw),
|
||||
'ingest-docs': () => init.cmdInitIngestDocs(cwd, raw),
|
||||
resume: () => init.cmdInitResume(cwd, raw),
|
||||
|
||||
103
src/init.cts
103
src/init.cts
@@ -51,6 +51,15 @@ const { checkAgentsInstalled } = agentInstallCheck;
|
||||
import gitBaseBranch = require('./git-base-branch.cjs');
|
||||
const { gitWorktreeInfoInternal } = gitBaseBranch;
|
||||
import { makeResolution } from './resolution.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- onboard-projection.cjs is an export= CommonJS module
|
||||
import onboardProjection = require('./onboard-projection.cjs');
|
||||
const {
|
||||
REQUIRED_CODEBASE_MAP_FILES,
|
||||
buildOnboardProjection,
|
||||
hasCodeFilesInternal,
|
||||
hasPackageFileInternal,
|
||||
listCodebaseMapFiles,
|
||||
} = onboardProjection;
|
||||
|
||||
const { output, error } = io;
|
||||
const { loadConfig, loadConfigResolved } = configLoader;
|
||||
@@ -625,68 +634,11 @@ function cmdInitNewProject(cwd: string, raw: boolean): void {
|
||||
const exaKeyFile = path.join(homedir, '.gsd', 'exa_api_key');
|
||||
const hasExaSearch = !!(process.env['EXA_API_KEY'] || fs.existsSync(exaKeyFile));
|
||||
|
||||
let hasCode = false;
|
||||
let hasPackageFile = false;
|
||||
try {
|
||||
const codeExtensions = new Set([
|
||||
'.ts', '.js', '.py', '.go', '.rs', '.swift', '.java',
|
||||
'.kt', '.kts',
|
||||
'.c', '.cpp', '.h',
|
||||
'.cs',
|
||||
'.rb',
|
||||
'.php',
|
||||
'.dart',
|
||||
'.m', '.mm',
|
||||
'.scala',
|
||||
'.groovy',
|
||||
'.lua',
|
||||
'.r', '.R',
|
||||
'.zig',
|
||||
'.ex', '.exs',
|
||||
'.clj',
|
||||
]);
|
||||
const skipDirs = new Set([
|
||||
'node_modules', '.git', '.planning', '.claude', '.codex',
|
||||
'__pycache__', 'target', 'dist', 'build',
|
||||
]);
|
||||
function findCodeFiles(dir: string, depth: number): boolean {
|
||||
if (depth > 3) return false;
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile() && codeExtensions.has(path.extname(entry.name))) return true;
|
||||
if (entry.isDirectory() && !skipDirs.has(entry.name)) {
|
||||
if (findCodeFiles(path.join(dir, entry.name), depth + 1)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
hasCode = findCodeFiles(cwd, 0);
|
||||
} catch {
|
||||
/* intentionally empty — best-effort detection */
|
||||
}
|
||||
|
||||
hasPackageFile =
|
||||
pathExistsInternal(cwd, 'package.json') ||
|
||||
pathExistsInternal(cwd, 'requirements.txt') ||
|
||||
pathExistsInternal(cwd, 'Cargo.toml') ||
|
||||
pathExistsInternal(cwd, 'go.mod') ||
|
||||
pathExistsInternal(cwd, 'Package.swift') ||
|
||||
pathExistsInternal(cwd, 'build.gradle') ||
|
||||
pathExistsInternal(cwd, 'build.gradle.kts') ||
|
||||
pathExistsInternal(cwd, 'pom.xml') ||
|
||||
pathExistsInternal(cwd, 'Gemfile') ||
|
||||
pathExistsInternal(cwd, 'composer.json') ||
|
||||
pathExistsInternal(cwd, 'pubspec.yaml') ||
|
||||
pathExistsInternal(cwd, 'CMakeLists.txt') ||
|
||||
pathExistsInternal(cwd, 'Makefile') ||
|
||||
pathExistsInternal(cwd, 'build.zig') ||
|
||||
pathExistsInternal(cwd, 'mix.exs') ||
|
||||
pathExistsInternal(cwd, 'project.clj');
|
||||
const hasCode = hasCodeFilesInternal(cwd);
|
||||
const hasPackageFile = hasPackageFileInternal(cwd);
|
||||
const isBrownfield = hasCode || hasPackageFile;
|
||||
const codebaseMapFiles = listCodebaseMapFiles(cwd);
|
||||
const hasCodebaseMap = codebaseMapFiles.length === REQUIRED_CODEBASE_MAP_FILES.length;
|
||||
|
||||
const result: Record<string, unknown> = {
|
||||
researcher_model: resolveModelInternal(cwd, 'gsd-project-researcher'),
|
||||
@@ -696,14 +648,13 @@ function cmdInitNewProject(cwd: string, raw: boolean): void {
|
||||
commit_docs: config.commit_docs,
|
||||
|
||||
project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
|
||||
has_codebase_map: pathExistsInternal(cwd, '.planning/codebase'),
|
||||
has_codebase_map: hasCodebaseMap,
|
||||
planning_exists: pathExistsInternal(cwd, '.planning'),
|
||||
|
||||
has_existing_code: hasCode,
|
||||
has_package_file: hasPackageFile,
|
||||
is_brownfield: hasCode || hasPackageFile,
|
||||
needs_codebase_map:
|
||||
(hasCode || hasPackageFile) && !pathExistsInternal(cwd, '.planning/codebase'),
|
||||
is_brownfield: isBrownfield,
|
||||
needs_codebase_map: isBrownfield && !hasCodebaseMap,
|
||||
|
||||
...getInitGitState(cwd),
|
||||
|
||||
@@ -840,6 +791,25 @@ function cmdInitIngestDocs(cwd: string, raw: boolean): void {
|
||||
output(withProjectRoot(cwd, result), raw);
|
||||
}
|
||||
|
||||
function cmdInitOnboard(
|
||||
cwd: string,
|
||||
raw: boolean,
|
||||
options: Record<string, unknown> = {},
|
||||
): void {
|
||||
const config = loadConfig(cwd);
|
||||
const workflowConfig = (config.workflow ?? {}) as Record<string, unknown>;
|
||||
const result = {
|
||||
...buildOnboardProjection(cwd, {
|
||||
commitDocs: !!config.commit_docs,
|
||||
fast: options['fast'] === true,
|
||||
textMode: options['text'] === true || !!config.text_mode || !!workflowConfig['text_mode'],
|
||||
}),
|
||||
...getInitGitState(cwd),
|
||||
};
|
||||
|
||||
output(withProjectRoot(cwd, result), raw);
|
||||
}
|
||||
|
||||
function cmdInitResume(cwd: string, raw: boolean): void {
|
||||
const config = loadConfig(cwd);
|
||||
|
||||
@@ -2606,6 +2576,7 @@ export = {
|
||||
cmdInitNewMilestone,
|
||||
cmdInitQuick,
|
||||
cmdInitIngestDocs,
|
||||
cmdInitOnboard,
|
||||
cmdInitResume,
|
||||
cmdInitVerifyWork,
|
||||
cmdInitPhaseOp,
|
||||
|
||||
@@ -51,6 +51,7 @@ const PROFILES = Object.freeze({
|
||||
standard: Object.freeze([
|
||||
// Core loop
|
||||
'new-project',
|
||||
'onboard',
|
||||
'discuss-phase',
|
||||
'plan-phase',
|
||||
'execute-phase',
|
||||
|
||||
437
src/onboard-projection.cts
Normal file
437
src/onboard-projection.cts
Normal file
@@ -0,0 +1,437 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- core-utils.cjs is an export= CommonJS module
|
||||
import coreUtils = require('./core-utils.cjs');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
|
||||
import planningWorkspace = require('./planning-workspace.cjs');
|
||||
import { formatGsdSlash, resolveRuntime } from './runtime-slash.cjs';
|
||||
|
||||
const { pathExistsInternal, toPosixPath } = coreUtils;
|
||||
const { planningDir, planningRoot } = planningWorkspace;
|
||||
|
||||
const CODE_EXTENSIONS = new Set([
|
||||
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.swift', '.java',
|
||||
'.kt', '.kts', '.c', '.cpp', '.cc', '.h', '.hpp', '.cs', '.rb', '.php', '.dart',
|
||||
'.m', '.mm', '.scala', '.groovy', '.lua', '.r', '.R', '.zig', '.ex', '.exs', '.clj',
|
||||
]);
|
||||
|
||||
const CODE_SCAN_SKIP_DIRS = new Set([
|
||||
'node_modules', '.git', '.planning', '.claude', '.codex', '__pycache__', 'target',
|
||||
'dist', 'build', '.next', '.nuxt', '.svelte-kit', 'coverage', 'vendor', '.venv', 'venv',
|
||||
]);
|
||||
|
||||
const PACKAGE_FILES = [
|
||||
'package.json', 'requirements.txt', 'pyproject.toml', 'Cargo.toml', 'go.mod',
|
||||
'Package.swift', 'build.gradle', 'build.gradle.kts', 'pom.xml', 'Gemfile',
|
||||
'composer.json', 'pubspec.yaml', 'CMakeLists.txt', 'Makefile', 'build.zig',
|
||||
'mix.exs', 'project.clj',
|
||||
];
|
||||
|
||||
const REQUIRED_CODEBASE_MAP_FILES = [
|
||||
'STACK.md', 'ARCHITECTURE.md', 'STRUCTURE.md', 'CONVENTIONS.md', 'TESTING.md',
|
||||
'INTEGRATIONS.md', 'CONCERNS.md',
|
||||
];
|
||||
|
||||
const FAST_CODEBASE_MAP_FILES = [
|
||||
'STACK.md', 'INTEGRATIONS.md', 'ARCHITECTURE.md', 'STRUCTURE.md',
|
||||
];
|
||||
|
||||
const PLANNING_DOC_SEGMENTS = new Set([
|
||||
'adr', 'adrs', 'prd', 'prds', 'spec', 'specs', 'rfc', 'rfcs',
|
||||
]);
|
||||
|
||||
type MapReadiness = 'none' | 'fast' | 'complete';
|
||||
|
||||
interface OnboardHandoffCommands {
|
||||
ingest_docs: string;
|
||||
manager: string;
|
||||
map_codebase: string;
|
||||
map_codebase_fast: string;
|
||||
new_project: string;
|
||||
onboard: string;
|
||||
}
|
||||
|
||||
type OnboardNextAction =
|
||||
| { kind: 'map-codebase'; command: string; reason: string }
|
||||
| { kind: 'ingest-docs'; command: string; reason: string }
|
||||
| { kind: 'new-project'; command: string; reason: string }
|
||||
| { kind: 'complete-map-before-new-project'; command: string; reason: string }
|
||||
| { kind: 'partial-planning'; missing: string[]; reason: string }
|
||||
| { kind: 'write-summary'; summary_path: string; reason: string }
|
||||
| { kind: 'ready'; reason: string };
|
||||
|
||||
interface BuildOnboardProjectionOptions {
|
||||
commitDocs: boolean;
|
||||
fast: boolean;
|
||||
textMode: boolean;
|
||||
}
|
||||
|
||||
interface OnboardProjection {
|
||||
commit_docs: boolean;
|
||||
text_mode: boolean;
|
||||
|
||||
project_exists: boolean;
|
||||
planning_exists: boolean;
|
||||
requirements_exists: boolean;
|
||||
roadmap_exists: boolean;
|
||||
state_exists: boolean;
|
||||
config_exists: boolean;
|
||||
|
||||
handoff_commands: OnboardHandoffCommands;
|
||||
|
||||
has_existing_code: boolean;
|
||||
has_package_file: boolean;
|
||||
is_brownfield: boolean;
|
||||
fast_mode: boolean;
|
||||
map_readiness: MapReadiness;
|
||||
next_action: OnboardNextAction;
|
||||
needs_codebase_map: boolean;
|
||||
needs_fast_codebase_map: boolean;
|
||||
has_codebase_map: boolean;
|
||||
has_fast_codebase_map: boolean;
|
||||
codebase_dir_exists: boolean;
|
||||
fast_codebase_map_files_required: string[];
|
||||
codebase_map_files_present: string[];
|
||||
missing_codebase_map_files: string[];
|
||||
missing_fast_codebase_map_files: string[];
|
||||
codebase_map_summary_status: string;
|
||||
codebase_map_final_status: string;
|
||||
|
||||
has_docs_candidates: boolean;
|
||||
doc_candidate_count: number;
|
||||
doc_candidates: string[];
|
||||
|
||||
onboarding_summary_exists: boolean;
|
||||
onboarding_summary_path: string;
|
||||
|
||||
project_path: string;
|
||||
requirements_path: string;
|
||||
roadmap_path: string;
|
||||
state_path: string;
|
||||
codebase_dir: string;
|
||||
onboarding_dir: string;
|
||||
}
|
||||
|
||||
function hasCodeFilesInternal(dir: string, depth = 0): boolean {
|
||||
if (depth > 3) return false;
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile() && CODE_EXTENSIONS.has(path.extname(entry.name))) return true;
|
||||
if (entry.isDirectory() && !CODE_SCAN_SKIP_DIRS.has(entry.name)) {
|
||||
if (hasCodeFilesInternal(path.join(dir, entry.name), depth + 1)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasPackageFileInternal(cwd: string): boolean {
|
||||
return PACKAGE_FILES.some((file) => pathExistsInternal(cwd, file));
|
||||
}
|
||||
|
||||
function listPlanningDocCandidates(cwd: string): string[] {
|
||||
const roots = ['docs', 'adr', 'adrs', 'prd', 'prds', 'spec', 'specs', 'rfc', 'rfcs'];
|
||||
const candidates = new Set<string>();
|
||||
|
||||
function isPlanningDocCandidate(rel: string, name: string): boolean {
|
||||
const upperName = name.toUpperCase();
|
||||
const relLower = rel.toLowerCase();
|
||||
const pathSegments = relLower.split('/');
|
||||
return (
|
||||
/(^|[-_ ])(ADR|PRD|SPEC|RFC)([-_ ]|\.)/i.test(name) ||
|
||||
/^\d{4}[-_].+\.md$/i.test(name) ||
|
||||
pathSegments.some((segment) => PLANNING_DOC_SEGMENTS.has(segment)) ||
|
||||
upperName === 'REQUIREMENTS.MD'
|
||||
);
|
||||
}
|
||||
|
||||
function addCandidate(rel: string, name: string): void {
|
||||
if (name.toLowerCase().endsWith('.md') && isPlanningDocCandidate(rel, name)) {
|
||||
candidates.add(toPosixPath(rel));
|
||||
}
|
||||
}
|
||||
|
||||
function visit(dir: string, relDir: string, depth: number): void {
|
||||
if (depth > 3) return;
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
||||
if (entry.isDirectory()) {
|
||||
if (!CODE_SCAN_SKIP_DIRS.has(entry.name)) {
|
||||
visit(path.join(dir, entry.name), rel, depth + 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isFile()) addCandidate(rel, entry.name);
|
||||
}
|
||||
}
|
||||
|
||||
let rootEntries: fs.Dirent[] = [];
|
||||
try {
|
||||
rootEntries = fs.readdirSync(cwd, { withFileTypes: true });
|
||||
} catch {
|
||||
rootEntries = [];
|
||||
}
|
||||
|
||||
for (const entry of rootEntries) {
|
||||
if (entry.isFile()) addCandidate(entry.name, entry.name);
|
||||
}
|
||||
|
||||
for (const root of roots) {
|
||||
const full = path.join(cwd, root);
|
||||
if (fs.existsSync(full)) visit(full, root, 0);
|
||||
}
|
||||
|
||||
return [...candidates].sort();
|
||||
}
|
||||
|
||||
function listCodebaseMapFiles(cwd: string): string[] {
|
||||
const codebaseDir = path.join(planningRoot(cwd), 'codebase');
|
||||
if (!fs.existsSync(codebaseDir)) return [];
|
||||
return REQUIRED_CODEBASE_MAP_FILES.filter((file) =>
|
||||
fs.existsSync(path.join(codebaseDir, file)),
|
||||
);
|
||||
}
|
||||
|
||||
function getMapReadiness(hasCompleteMap: boolean, hasFastMap: boolean): MapReadiness {
|
||||
if (hasCompleteMap) return 'complete';
|
||||
if (hasFastMap) return 'fast';
|
||||
return 'none';
|
||||
}
|
||||
|
||||
function mapSummaryStatus(mapReadiness: MapReadiness): string {
|
||||
if (mapReadiness === 'complete') return '.planning/codebase/ (complete codebase map)';
|
||||
if (mapReadiness === 'fast') return '.planning/codebase/ (fast/partial codebase map; complete map still required for project setup)';
|
||||
return 'missing';
|
||||
}
|
||||
|
||||
function mapFinalStatus(mapReadiness: MapReadiness): string {
|
||||
if (mapReadiness === 'complete') return 'complete';
|
||||
if (mapReadiness === 'fast') return 'fast/partial; complete map still required for project setup';
|
||||
return 'missing';
|
||||
}
|
||||
|
||||
function planningMissing(
|
||||
projectExists: boolean,
|
||||
requirementsExists: boolean,
|
||||
roadmapExists: boolean,
|
||||
stateExists: boolean,
|
||||
): string[] {
|
||||
const missing: string[] = [];
|
||||
if (!projectExists) missing.push('PROJECT.md');
|
||||
if (!requirementsExists) missing.push('REQUIREMENTS.md');
|
||||
if (!roadmapExists) missing.push('ROADMAP.md');
|
||||
if (!stateExists) missing.push('STATE.md');
|
||||
return missing;
|
||||
}
|
||||
|
||||
function nextAction(params: {
|
||||
fastMode: boolean;
|
||||
isBrownfield: boolean;
|
||||
needsOnboardCodebaseMap: boolean;
|
||||
hasDocsCandidates: boolean;
|
||||
projectExists: boolean;
|
||||
mapReadiness: MapReadiness;
|
||||
onboardingSummaryExists: boolean;
|
||||
onboardingSummaryPath: string;
|
||||
hasPlanningArtifacts: boolean;
|
||||
missingPlanningFiles: string[];
|
||||
handoffCommands: OnboardHandoffCommands;
|
||||
}): OnboardNextAction {
|
||||
if (params.isBrownfield && params.needsOnboardCodebaseMap) {
|
||||
return {
|
||||
kind: 'map-codebase',
|
||||
command: params.fastMode ? params.handoffCommands.map_codebase_fast : params.handoffCommands.map_codebase,
|
||||
reason: 'Existing code was detected, but the required .planning/codebase/ map is missing.',
|
||||
};
|
||||
}
|
||||
|
||||
if (params.hasPlanningArtifacts && params.missingPlanningFiles.length > 0) {
|
||||
return {
|
||||
kind: 'partial-planning',
|
||||
missing: params.missingPlanningFiles,
|
||||
reason: 'Project planning exists but required planning files are missing.',
|
||||
};
|
||||
}
|
||||
|
||||
if (params.fastMode && params.mapReadiness === 'fast' && !params.projectExists) {
|
||||
return {
|
||||
kind: 'complete-map-before-new-project',
|
||||
command: params.handoffCommands.map_codebase,
|
||||
reason: 'The fast map is enough for lightweight onboarding, but project setup still requires the complete codebase map.',
|
||||
};
|
||||
}
|
||||
|
||||
if (params.hasDocsCandidates && !params.projectExists) {
|
||||
return {
|
||||
kind: 'ingest-docs',
|
||||
command: params.handoffCommands.ingest_docs,
|
||||
reason: 'Detected existing ADR/PRD/SPEC/RFC document(s) before project setup.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!params.isBrownfield && !params.projectExists && !params.hasDocsCandidates) {
|
||||
return {
|
||||
kind: 'new-project',
|
||||
command: params.handoffCommands.new_project,
|
||||
reason: 'No existing code or planning docs were detected.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!params.projectExists) {
|
||||
return {
|
||||
kind: 'new-project',
|
||||
command: params.handoffCommands.new_project,
|
||||
reason: 'Codebase context is ready for project initialization.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!params.onboardingSummaryExists) {
|
||||
return {
|
||||
kind: 'write-summary',
|
||||
summary_path: params.onboardingSummaryPath,
|
||||
reason: 'Onboarding summary is missing.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'ready',
|
||||
reason: 'Onboarding summary already exists.',
|
||||
};
|
||||
}
|
||||
|
||||
function buildHandoffCommands(cwd: string): OnboardHandoffCommands {
|
||||
const runtime = resolveRuntime(cwd);
|
||||
return {
|
||||
ingest_docs: formatGsdSlash('ingest-docs', runtime) as string,
|
||||
manager: formatGsdSlash('manager', runtime) as string,
|
||||
map_codebase: formatGsdSlash('map-codebase', runtime) as string,
|
||||
map_codebase_fast: formatGsdSlash('map-codebase --fast', runtime) as string,
|
||||
new_project: formatGsdSlash('new-project', runtime) as string,
|
||||
onboard: formatGsdSlash('onboard', runtime) as string,
|
||||
};
|
||||
}
|
||||
|
||||
function buildOnboardProjection(cwd: string, options: BuildOnboardProjectionOptions): OnboardProjection {
|
||||
const handoffCommands = buildHandoffCommands(cwd);
|
||||
const codebaseMapFiles = listCodebaseMapFiles(cwd);
|
||||
const missingCodebaseMapFiles = REQUIRED_CODEBASE_MAP_FILES.filter(
|
||||
(file) => !codebaseMapFiles.includes(file),
|
||||
);
|
||||
const missingFastCodebaseMapFiles = FAST_CODEBASE_MAP_FILES.filter(
|
||||
(file) => !codebaseMapFiles.includes(file),
|
||||
);
|
||||
const docCandidates = listPlanningDocCandidates(cwd);
|
||||
const hasCode = hasCodeFilesInternal(cwd);
|
||||
const hasPackageFile = hasPackageFileInternal(cwd);
|
||||
const isBrownfield = hasCode || hasPackageFile;
|
||||
const hasCodebaseMap = codebaseMapFiles.length === REQUIRED_CODEBASE_MAP_FILES.length;
|
||||
const hasFastCodebaseMap = missingFastCodebaseMapFiles.length === 0;
|
||||
const mapReadinessValue = getMapReadiness(hasCodebaseMap, hasFastCodebaseMap);
|
||||
const needsCodebaseMap = isBrownfield && !hasCodebaseMap;
|
||||
const needsFastCodebaseMap = isBrownfield && !hasFastCodebaseMap;
|
||||
const needsOnboardCodebaseMap = options.fast ? needsFastCodebaseMap : needsCodebaseMap;
|
||||
const projectRootPath = path.join(planningRoot(cwd), 'PROJECT.md');
|
||||
const projectScopedPath = path.join(planningDir(cwd), 'PROJECT.md');
|
||||
const projectExists =
|
||||
fs.existsSync(projectRootPath) || fs.existsSync(projectScopedPath);
|
||||
const requirementsExists = fs.existsSync(path.join(planningDir(cwd), 'REQUIREMENTS.md'));
|
||||
const roadmapExists = fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md'));
|
||||
const stateExists = fs.existsSync(path.join(planningDir(cwd), 'STATE.md'));
|
||||
const onboardingSummaryPath = path.join(planningRoot(cwd), 'onboarding', 'SUMMARY.md');
|
||||
const onboardingSummaryExists = fs.existsSync(onboardingSummaryPath);
|
||||
const hasPlanningArtifacts = projectExists || requirementsExists || roadmapExists || stateExists;
|
||||
const missingPlanningFiles = planningMissing(
|
||||
projectExists,
|
||||
requirementsExists,
|
||||
roadmapExists,
|
||||
stateExists,
|
||||
);
|
||||
|
||||
return {
|
||||
commit_docs: options.commitDocs,
|
||||
text_mode: options.textMode,
|
||||
|
||||
project_exists: projectExists,
|
||||
planning_exists: fs.existsSync(planningRoot(cwd)),
|
||||
requirements_exists: requirementsExists,
|
||||
roadmap_exists: roadmapExists,
|
||||
state_exists: stateExists,
|
||||
config_exists: fs.existsSync(path.join(planningDir(cwd), 'config.json')),
|
||||
|
||||
handoff_commands: handoffCommands,
|
||||
|
||||
has_existing_code: hasCode,
|
||||
has_package_file: hasPackageFile,
|
||||
is_brownfield: isBrownfield,
|
||||
fast_mode: options.fast,
|
||||
map_readiness: mapReadinessValue,
|
||||
next_action: nextAction({
|
||||
fastMode: options.fast,
|
||||
isBrownfield,
|
||||
needsOnboardCodebaseMap,
|
||||
hasDocsCandidates: docCandidates.length > 0,
|
||||
projectExists,
|
||||
mapReadiness: mapReadinessValue,
|
||||
onboardingSummaryExists,
|
||||
onboardingSummaryPath: toPosixPath(path.relative(cwd, onboardingSummaryPath)),
|
||||
hasPlanningArtifacts,
|
||||
missingPlanningFiles,
|
||||
handoffCommands,
|
||||
}),
|
||||
needs_codebase_map: needsCodebaseMap,
|
||||
needs_fast_codebase_map: needsFastCodebaseMap,
|
||||
has_codebase_map: hasCodebaseMap,
|
||||
has_fast_codebase_map: hasFastCodebaseMap,
|
||||
codebase_dir_exists: fs.existsSync(path.join(planningRoot(cwd), 'codebase')),
|
||||
fast_codebase_map_files_required: FAST_CODEBASE_MAP_FILES,
|
||||
codebase_map_files_present: codebaseMapFiles,
|
||||
missing_codebase_map_files: missingCodebaseMapFiles,
|
||||
missing_fast_codebase_map_files: missingFastCodebaseMapFiles,
|
||||
codebase_map_summary_status: mapSummaryStatus(mapReadinessValue),
|
||||
codebase_map_final_status: mapFinalStatus(mapReadinessValue),
|
||||
|
||||
has_docs_candidates: docCandidates.length > 0,
|
||||
doc_candidate_count: docCandidates.length,
|
||||
doc_candidates: docCandidates,
|
||||
|
||||
onboarding_summary_exists: onboardingSummaryExists,
|
||||
onboarding_summary_path: toPosixPath(path.relative(cwd, onboardingSummaryPath)),
|
||||
|
||||
project_path: toPosixPath(path.relative(
|
||||
cwd,
|
||||
fs.existsSync(projectRootPath) ? projectRootPath : projectScopedPath,
|
||||
)),
|
||||
requirements_path: toPosixPath(
|
||||
path.relative(cwd, path.join(planningDir(cwd), 'REQUIREMENTS.md')),
|
||||
),
|
||||
roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
|
||||
state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
|
||||
codebase_dir: toPosixPath(path.relative(cwd, path.join(planningRoot(cwd), 'codebase'))),
|
||||
onboarding_dir: toPosixPath(path.relative(cwd, path.join(planningRoot(cwd), 'onboarding'))),
|
||||
};
|
||||
}
|
||||
|
||||
export = {
|
||||
REQUIRED_CODEBASE_MAP_FILES,
|
||||
FAST_CODEBASE_MAP_FILES,
|
||||
buildOnboardProjection,
|
||||
hasCodeFilesInternal,
|
||||
hasPackageFileInternal,
|
||||
listCodebaseMapFiles,
|
||||
listPlanningDocCandidates,
|
||||
};
|
||||
Reference in New Issue
Block a user