Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions packages/cli/src/commands/adf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ============================================================================
Expand Down
48 changes: 28 additions & 20 deletions packages/cli/src/commands/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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++;

Expand Down Expand Up @@ -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[] = [];
Expand All @@ -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<string, string> = {};
Expand All @@ -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<string, string> = {};
for (const mod of ['core.adf', 'state.adf']) {
Expand Down Expand Up @@ -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 {
Expand Down
30 changes: 28 additions & 2 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<StackPreset, unknown[]> = {
worker: [
Expand Down Expand Up @@ -158,6 +158,32 @@ const PATTERN_TEMPLATES: Record<StackPreset, unknown[]> = {
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
Expand Down Expand Up @@ -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(
Expand Down
34 changes: 32 additions & 2 deletions packages/cli/src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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<string>, candidates: string[]): string[] {
return candidates.filter((c) => set.has(c));
}
Expand Down Expand Up @@ -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(
Expand Down