diff --git a/packages/cli/src/commands/adf.ts b/packages/cli/src/commands/adf.ts index fadc1a2..69c7973 100644 --- a/packages/cli/src/commands/adf.ts +++ b/packages/cli/src/commands/adf.ts @@ -81,6 +81,34 @@ export const BACKEND_SCAFFOLD = `ADF: 0.1 - Add service/API/database constraints and operational rules `; +export const DECISIONS_SCAFFOLD = `ADF: 0.1 +\u{1F4CB} CONTEXT: + - Decisions module scaffold + - Record architectural decision rationale and outcomes +`; + +export const PLANNING_SCAFFOLD = `ADF: 0.1 +\u{1F4CB} CONTEXT: + - Planning module scaffold + - Track project phases, milestones, and sequencing +`; + +export const MANIFEST_DOCS_SCAFFOLD = `ADF: 0.1 +\u{1F3AF} ROLE: Documentation workspace context router + +\u{1F4E6} DEFAULT_LOAD: + - core.adf + - state.adf + +\u{1F4C2} ON_DEMAND: + - decisions.adf (Triggers on: ADR, decision, rationale) + - planning.adf (Triggers on: plan, milestone, phase, roadmap) + +\u{1F4D0} RULES: + - Prefer smallest relevant module set. + - Never assume unseen modules were loaded. +`; + // ============================================================================ // Dispatcher // ============================================================================ diff --git a/packages/cli/src/commands/bootstrap.ts b/packages/cli/src/commands/bootstrap.ts index 06d5c77..0156ca1 100644 --- a/packages/cli/src/commands/bootstrap.ts +++ b/packages/cli/src/commands/bootstrap.ts @@ -26,10 +26,13 @@ import { } from './setup'; import { MANIFEST_SCAFFOLD, + MANIFEST_DOCS_SCAFFOLD, CORE_SCAFFOLD, STATE_SCAFFOLD, FRONTEND_SCAFFOLD, BACKEND_SCAFFOLD, + DECISIONS_SCAFFOLD, + PLANNING_SCAFFOLD, POINTER_CLAUDE_MD, POINTER_CURSORRULES, POINTER_AGENTS_MD, @@ -73,7 +76,7 @@ export async function bootstrapCommand(options: CLIOptions, args: string[]): Pro } if (presetFlag && !isValidPreset(presetFlag)) { - throw new CLIError(`Invalid --preset value: ${presetFlag}. Use worker|frontend|backend|fullstack.`); + throw new CLIError(`Invalid --preset value: ${presetFlag}. Use worker|frontend|backend|fullstack|docs.`); } const result: BootstrapResult = { @@ -129,7 +132,7 @@ export async function bootstrapCommand(options: CLIOptions, args: string[]): Pro // ======================================================================== // Phase 3: ADF Init // ======================================================================== - const adfResult = runAdfInitPhase(options, force); + const adfResult = runAdfInitPhase(options, force, selectedPreset); result.steps.push(adfResult.step); if (adfResult.step.status === 'fail') warnings++; @@ -406,9 +409,26 @@ function runSetupPhase( // Phase 3: ADF Init // ============================================================================ +function writeAdfScaffolds(aiDir: string, preset?: StackPreset): string[] { + const isDocsPreset = preset === 'docs'; + fs.mkdirSync(aiDir, { recursive: true }); + fs.writeFileSync(path.join(aiDir, 'manifest.adf'), isDocsPreset ? MANIFEST_DOCS_SCAFFOLD : MANIFEST_SCAFFOLD); + fs.writeFileSync(path.join(aiDir, 'core.adf'), CORE_SCAFFOLD); + fs.writeFileSync(path.join(aiDir, 'state.adf'), STATE_SCAFFOLD); + if (isDocsPreset) { + fs.writeFileSync(path.join(aiDir, 'decisions.adf'), DECISIONS_SCAFFOLD); + fs.writeFileSync(path.join(aiDir, 'planning.adf'), PLANNING_SCAFFOLD); + return ['.ai/manifest.adf', '.ai/core.adf', '.ai/state.adf', '.ai/decisions.adf', '.ai/planning.adf']; + } + fs.writeFileSync(path.join(aiDir, 'frontend.adf'), FRONTEND_SCAFFOLD); + fs.writeFileSync(path.join(aiDir, 'backend.adf'), BACKEND_SCAFFOLD); + return ['.ai/manifest.adf', '.ai/core.adf', '.ai/state.adf', '.ai/frontend.adf', '.ai/backend.adf']; +} + function runAdfInitPhase( options: CLIOptions, - force: boolean + force: boolean, + preset?: StackPreset, ): { step: StepResult } { const warnings: string[] = []; const files: string[] = []; @@ -422,14 +442,8 @@ function runAdfInitPhase( const alreadyExists = fs.existsSync(manifestPath); const hasCustomContent = alreadyExists && hasCustomAdfContent(aiDir); if (!alreadyExists) { - // Greenfield: write scaffolds (including on-demand module stubs) - fs.mkdirSync(aiDir, { recursive: true }); - fs.writeFileSync(path.join(aiDir, 'manifest.adf'), MANIFEST_SCAFFOLD); - fs.writeFileSync(path.join(aiDir, 'core.adf'), CORE_SCAFFOLD); - fs.writeFileSync(path.join(aiDir, 'state.adf'), STATE_SCAFFOLD); - fs.writeFileSync(path.join(aiDir, 'frontend.adf'), FRONTEND_SCAFFOLD); - fs.writeFileSync(path.join(aiDir, 'backend.adf'), BACKEND_SCAFFOLD); - files.push('.ai/manifest.adf', '.ai/core.adf', '.ai/state.adf', '.ai/frontend.adf', '.ai/backend.adf'); + // Greenfield: write scaffolds (preset-aware on-demand modules) + files.push(...writeAdfScaffolds(aiDir, preset)); // Write .adf.lock const lockData: Record = {}; @@ -444,14 +458,8 @@ function runAdfInitPhase( warnings.push('.ai/ contains custom ADF content; skipping scaffold overwrite'); warnings.push("Run 'charter adf migrate' to consolidate agent configs into ADF"); } else if (force) { - // Force overwrite (including on-demand module stubs) - fs.mkdirSync(aiDir, { recursive: true }); - fs.writeFileSync(path.join(aiDir, 'manifest.adf'), MANIFEST_SCAFFOLD); - fs.writeFileSync(path.join(aiDir, 'core.adf'), CORE_SCAFFOLD); - fs.writeFileSync(path.join(aiDir, 'state.adf'), STATE_SCAFFOLD); - fs.writeFileSync(path.join(aiDir, 'frontend.adf'), FRONTEND_SCAFFOLD); - fs.writeFileSync(path.join(aiDir, 'backend.adf'), BACKEND_SCAFFOLD); - files.push('.ai/manifest.adf', '.ai/core.adf', '.ai/state.adf', '.ai/frontend.adf', '.ai/backend.adf'); + // Force overwrite (preset-aware on-demand modules) + files.push(...writeAdfScaffolds(aiDir, preset)); const lockData: Record = {}; for (const mod of ['core.adf', 'state.adf']) { @@ -696,7 +704,7 @@ function runDoctorPhase( // ============================================================================ function isValidPreset(value: string | undefined): value is StackPreset { - return value === 'worker' || value === 'frontend' || value === 'backend' || value === 'fullstack'; + return value === 'worker' || value === 'frontend' || value === 'backend' || value === 'fullstack' || value === 'docs'; } function hashContent(content: string): string { diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 6dc2902..67d827c 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -11,7 +11,7 @@ import { EXIT_CODE } from '../index'; import { getFlag } from '../flags'; import { getDefaultConfigJSON } from '../config'; -export type StackPreset = 'worker' | 'frontend' | 'backend' | 'fullstack'; +export type StackPreset = 'worker' | 'frontend' | 'backend' | 'fullstack' | 'docs'; const PATTERN_TEMPLATES: Record = { worker: [ @@ -158,6 +158,32 @@ const PATTERN_TEMPLATES: Record = { status: 'ACTIVE', }, ], + docs: [ + { + name: 'Documentation Standards', + category: 'GOVERNANCE', + blessed_solution: 'Markdown-first authoring with ADR/RFC conventions', + rationale: 'Consistent documentation structure across contributors', + anti_patterns: 'Avoid undocumented decisions or ad-hoc wiki pages', + status: 'ACTIVE', + }, + { + name: 'Decision Records', + category: 'GOVERNANCE', + blessed_solution: 'Lightweight ADR format in docs/ or decisions/', + rationale: 'Preserves architectural rationale over time', + anti_patterns: 'Avoid verbal-only decisions without written records', + status: 'ACTIVE', + }, + { + name: 'Review Process', + category: 'GOVERNANCE', + blessed_solution: 'PR-based review for documentation changes', + rationale: 'Tracks authorship and enables async review', + anti_patterns: 'Avoid direct pushes to main for substantive doc changes', + status: 'ACTIVE', + }, + ], }; const DEFAULT_POLICY_CONTENT = `# Governance Policy @@ -302,7 +328,7 @@ function writeIfChanged(targetPath: string, content: string): boolean { } function isValidPreset(value: string | undefined): value is StackPreset { - return value === 'worker' || value === 'frontend' || value === 'backend' || value === 'fullstack'; + return value === 'worker' || value === 'frontend' || value === 'backend' || value === 'fullstack' || value === 'docs'; } function buildPatternTemplate( diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 47ba984..983f84b 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -186,7 +186,7 @@ export async function setupCommand(options: CLIOptions, args: string[]): Promise } if (presetFlag && !isValidPreset(presetFlag)) { - throw new CLIError(`Invalid --preset value: ${presetFlag}. Use worker|frontend|backend|fullstack.`); + throw new CLIError(`Invalid --preset value: ${presetFlag}. Use worker|frontend|backend|fullstack|docs.`); } const contexts = loadPackageContexts(); @@ -565,6 +565,25 @@ export function detectStack(contexts: PackageContext[]): DetectionResult { warnings, }; } + // Docs/planning workspace detection — no code frameworks, documentation-heavy + const hasDocsDirs = hasAnyPath(['docs', 'ADR', 'adrs', 'decisions', 'papers', 'rfcs']); + const mostlyMarkdown = checkMostlyMarkdown(); + if (!hasFrontend && !hasBackend && !hasWorker && (hasDocsDirs || mostlyMarkdown)) { + return { + runtime: dedupRuntime, + frameworks: dedup(frameworks), + state: dedup(state), + sources: contexts.map((c) => c.source), + agentStandards, + monorepo, + signals, + mixedStack: false, + confidence: hasDocsDirs ? 'HIGH' : 'MEDIUM', + suggestedPreset: 'docs' as StackPreset, + warnings, + }; + } + return { runtime: dedupRuntime, frameworks: dedup(frameworks), @@ -774,6 +793,17 @@ function hasAnyPath(paths: string[]): boolean { return paths.some((p) => fs.existsSync(path.resolve(p))); } +function checkMostlyMarkdown(): boolean { + try { + const entries = fs.readdirSync(process.cwd()); + const visible = entries.filter(e => !e.startsWith('.')); + const mdFiles = visible.filter(e => e.endsWith('.md')); + return visible.length > 0 && mdFiles.length / visible.length >= 0.5; + } catch { + return false; + } +} + function pick(set: Set, candidates: string[]): string[] { return candidates.filter((c) => set.has(c)); } @@ -836,7 +866,7 @@ export function applyManagedFile(targetPath: string, content: string, force: boo } function isValidPreset(value: string | undefined): value is StackPreset { - return value === 'worker' || value === 'frontend' || value === 'backend' || value === 'fullstack'; + return value === 'worker' || value === 'frontend' || value === 'backend' || value === 'fullstack' || value === 'docs'; } export function syncPackageManifest(