diff --git a/AGENTS.md b/AGENTS.md index dc2c071..292bef9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ Scriveno is a spec-driven writing, publishing, and translation pipeline that run - **Architecture**: Must remain a pure skill/command system -- no compiled code, no runtime dependencies beyond Node.js for the installer - **Voice fidelity**: Every feature must preserve the Voice DNA pipeline -- fresh context per atomic unit, STYLE-GUIDE.md loaded first -- **Backward compatibility**: Existing 113 commands and templates must continue working as new features are added +- **Backward compatibility**: Existing 115 commands and templates must continue working as new features are added - **Plan authority**: If a command file contradicts the product plan, fix the command -- plan is canonical (section 15 for command specs) - **Progressive disclosure**: Onboarding asks 3 questions max; depth is optional and additive - **Runtime credibility**: `>=20.0.0` is the installer compatibility floor. For new installs, prefer a currently supported LTS such as Node.js 24. `docs/runtime-support.md` is the canonical runtime matrix, and installer targets are not interchangeable proof of host-runtime parity. diff --git a/CHANGELOG.md b/CHANGELOG.md index d74f3be..aad494f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ All notable package-level changes to `scriveno` are documented here. +## 2.8.0 - 2026-05-30 + +Feature release focused on command-surface control, one-unit proofing, context-health visibility, and safer release gates. + +**Command surface profiles** + +- Added installer profiles (`core`, `writing`, `publishing`, `translation`, `specialist`, `full`) so writers can install a focused command set instead of the whole surface. +- Added `scriveno surface list`, `scriveno surface status`, and `scriveno surface profile ` with `--dry-run` support. +- Added `/scr:surface`, a writer-facing command for inspecting and changing the installed profile without touching manuscript content. +- Hardened command-runtime installs so Scriveno-owned mirrors are cleaned while unrelated user files are preserved. + +**Proof and context health** + +- Added `/scr:proof-unit`, a one-unit vertical proof path through voice, plan, draft, review, context health, and optional export-tool checks. +- Added context-health estimation to the shared auto-invoke engine and `/scr:health --context`, with watch, tight, and critical thresholds. +- The proactive status report now includes a `Context health:` line so long sessions show context pressure before another large operation. + +**Release and publishing gates** + +- Added `/scr:export --check` for format availability and external tool readiness without assembling or writing export files. +- Added `/scr:publish --preflight` so publishing presets can validate manuscript readiness and required tools before generating deliverables. +- Refreshed command inventory counts to 115 and added regression coverage for profiles, dry-run plans, context-health pressure, proof-unit, and preflight docs. + ## 2.7.2 - 2026-05-30 Correctness follow-up to the 2.7.x progress ledger release. @@ -78,7 +101,7 @@ Major release focused on making the first 10 minutes executable and proving the **Runtime parity proof** - Added committed first-run and runtime-parity proof bundles under `data/proof/`. -- Refreshed runtime smoke expectations for the 113-command surface across Claude Code, Codex, Cursor, Gemini CLI, OpenCode, GitHub Copilot, Windsurf, Antigravity, Manus, Perplexity Desktop, and the generic fallback. +- Refreshed runtime smoke expectations for the 115-command surface across Claude Code, Codex, Cursor, Gemini CLI, OpenCode, GitHub Copilot, Windsurf, Antigravity, Manus, Perplexity Desktop, and the generic fallback. - Kept Codex skill metadata, Claude Code command surfaces, guided local-MCP targets, and generic skill bundles aligned through the shared installer path. **Release alignment** diff --git a/CLAUDE.md b/CLAUDE.md index 19cbcce..a6794de 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ Scriveno is a spec-driven writing, publishing, and translation pipeline that run - **Architecture**: Must remain a pure skill/command system -- no compiled code, no runtime dependencies beyond Node.js for the installer - **Voice fidelity**: Every feature must preserve the Voice DNA pipeline -- fresh context per atomic unit, STYLE-GUIDE.md loaded first -- **Backward compatibility**: Existing 113 commands and templates must continue working as new features are added +- **Backward compatibility**: Existing 115 commands and templates must continue working as new features are added - **Plan authority**: If a command file contradicts the product plan, fix the command -- plan is canonical (section 15 for command specs) - **Progressive disclosure**: Onboarding asks 3 questions max; depth is optional and additive - **Runtime credibility**: `>=20.0.0` is the installer compatibility floor. For new installs, prefer a currently supported LTS such as Node.js 24. `docs/runtime-support.md` is the canonical runtime matrix, and installer targets are not interchangeable proof of host-runtime parity. diff --git a/README.md b/README.md index f6b57a5..72a7819 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![CI](https://github.com/aihxp/scriveno/actions/workflows/ci.yml/badge.svg)](https://github.com/aihxp/scriveno/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[![Version](https://img.shields.io/badge/version-2.7.2-blue)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-2.8.0-blue)](CHANGELOG.md) [![npm](https://img.shields.io/npm/v/scriveno.svg)](https://www.npmjs.com/package/scriveno) [![Downloads](https://img.shields.io/npm/dm/scriveno.svg)](https://www.npmjs.com/package/scriveno) [![Status CLI](https://img.shields.io/badge/status%20CLI-scriveno%20status-blue)](docs/runtime-support.md#shared-auto-invoke-engine) @@ -37,7 +37,7 @@ scriveno sync --check Scriveno is a command system that turns your AI coding agent into a voice-preserving writing studio. It supports 50 work types -- novels, screenplays, research papers, technical guides, runbooks, scripture commentaries, comics, memoirs -- each with its own adaptive vocabulary and toolset. -The wedge comes first: Scriveno profiles the writer, loads that voice into every drafting step, and keeps each unit on fresh context so the prose stays specific to the project. From there, it expands into 113 writing commands covering the rest of the pipeline: +The wedge comes first: Scriveno profiles the writer, loads that voice into every drafting step, and keeps each unit on fresh context so the prose stays specific to the project. From there, it expands into 115 writing commands covering the rest of the pipeline: - **Create** -- Set up a project with tailored context files. Progressive onboarding, never overwhelming. - **Write** -- Discuss, plan, draft, and revise one unit at a time. The drafter agent loads your Voice DNA and writes in *your* voice, not generic AI prose. Run `/scr:progress` any time to open a per-unit ledger (`.manuscript/PROGRESS.md`) showing what is done, in progress, and untouched. @@ -201,7 +201,7 @@ Scriveno is built on five principles: - [Quick Proof](docs/quick-proof.md) -- 10-minute proof-first route through install checks, the demo, and the next draft - [Starter Sets](docs/starter-sets.md) -- Small command paths for drafting, polishing, publishing, translation, sacred commentary, and repair - [Getting Started](docs/getting-started.md) -- Install to first draft in 10 minutes -- [Command Reference](docs/command-reference.md) -- All 113 commands with usage, flags, and examples +- [Command Reference](docs/command-reference.md) -- All 115 commands with usage, flags, and examples - [Work Types Guide](docs/work-types.md) -- How 50 work types adapt Scriveno's vocabulary - [Voice DNA Guide](docs/voice-dna.md) -- The 15+ dimension voice profiling system - [Creative Context](docs/creative-context.md) -- Writer-native context routing, craft notes, and core-loop memory @@ -246,11 +246,11 @@ Scriveno currently ships installer targets for these AI tooling environments: ## Status -**Version:** 2.7.2 +**Version:** 2.8.0 -Scriveno's core command surface is stable across 113 commands, 50 work types, and 11 installer targets. The current repo baseline includes shipped planning milestones through `v2.0 Publishing Cover Packaging`, plus the creative-context, record-store, branching-next, runtime-sync, adaptive concierge, human-first writing-safeguard, authenticity-diagnostic, domain-grilling, installer-marker cleanup, cross-runtime agent metadata, visible automation status, the shared `scriveno status --project .` auto-invoke engine, route-intelligence lanes, safe apply reporting, runtime smoke checks, agent availability checks, route graph audits, the full audit repair pass through `2.0.11`, the first-run proof surface in `2.5.0`, and the executable `/scr:first-run` path. See [Quick Proof](docs/quick-proof.md) for the fastest proof path, [Shipped Assets](docs/shipped-assets.md) for the canonical asset inventory, and [Runtime Support](docs/runtime-support.md) for the runtime compatibility matrix. +Scriveno's core command surface is stable across 115 commands, 50 work types, and 11 installer targets. The current repo baseline includes shipped planning milestones through `v2.0 Publishing Cover Packaging`, plus the creative-context, record-store, branching-next, runtime-sync, adaptive concierge, human-first writing-safeguard, authenticity-diagnostic, domain-grilling, installer-marker cleanup, cross-runtime agent metadata, visible automation status, the shared `scriveno status --project .` auto-invoke engine, route-intelligence lanes, safe apply reporting, runtime smoke checks, agent availability checks, route graph audits, the full audit repair pass through `2.0.11`, the first-run proof surface in `2.5.0`, the executable `/scr:first-run` path, command profiles, context-health checks, and `/scr:proof-unit`. See [Quick Proof](docs/quick-proof.md) for the fastest proof path, [Shipped Assets](docs/shipped-assets.md) for the canonical asset inventory, and [Runtime Support](docs/runtime-support.md) for the runtime compatibility matrix. -Version `2.7.2` publishes Scriveno under the package name `scriveno`, so the current install command is `npx scriveno@latest`. The older `scriveno-cli` package name is historical and was unpublished during the rename, so npm cannot attach a deprecation notice to it while it has no active registry record. The older `scriven-cli` package remains on npm only as a deprecated legacy name that points users to `scriveno`. Do not treat either legacy package name as active unless a deliberate compatibility shim is republished. See [CHANGELOG](CHANGELOG.md) for the full list and [docs/release-notes.md](docs/release-notes.md) for the public-facing summary. +Version `2.8.0` publishes Scriveno under the package name `scriveno`, so the current install command is `npx scriveno@latest`. The older `scriveno-cli` package name is historical and was unpublished during the rename, so npm cannot attach a deprecation notice to it while it has no active registry record. The older `scriven-cli` package remains on npm only as a deprecated legacy name that points users to `scriveno`. Do not treat either legacy package name as active unless a deliberate compatibility shim is republished. See [CHANGELOG](CHANGELOG.md) for the full list and [docs/release-notes.md](docs/release-notes.md) for the public-facing summary. Package history is tracked in [CHANGELOG.md](CHANGELOG.md), and the public-facing summary for this release is in [docs/release-notes.md](docs/release-notes.md). diff --git a/bin/install.js b/bin/install.js index 6a882fa..32d26b4 100755 --- a/bin/install.js +++ b/bin/install.js @@ -218,9 +218,167 @@ const RUNTIMES = { }, }; -function generateSkillManifest(constraintsPath) { +const SURFACE_PROFILES = { + core: { + label: 'Core', + description: 'Main writing loop and orientation commands.', + commands: [ + 'new-work', + 'profile-writer', + 'voice-test', + 'discuss', + 'plan', + 'draft', + 'editor-review', + 'submit', + 'progress', + 'save', + 'next', + 'health', + 'help', + 'surface', + 'proof-unit', + ], + }, + writing: { + label: 'Writing', + description: 'Core workflow plus revision, structure, character, and quality commands.', + includeProfiles: ['core'], + categories: ['structure', 'structure_management', 'character_world', 'quality', 'review', 'session', 'navigation'], + }, + publishing: { + label: 'Publishing', + description: 'Core workflow plus export, publishing, metadata, and platform packaging commands.', + includeProfiles: ['core'], + categories: ['publishing'], + }, + translation: { + label: 'Translation', + description: 'Core workflow plus translation, localization, glossary, and multi-publish commands.', + includeProfiles: ['core'], + categories: ['translation'], + }, + specialist: { + label: 'Specialist', + description: 'Core workflow plus sacred, illustration, collaboration, and utility surfaces.', + includeProfiles: ['core'], + categories: ['sacred_exclusive', 'illustration', 'collaboration', 'utility'], + }, + full: { + label: 'Full', + description: 'Every Scriveno command.', + all: true, + }, +}; + +const DEFAULT_SURFACE_PROFILE = 'full'; + +function normalizeSurfaceProfile(profile) { + const value = String(profile || DEFAULT_SURFACE_PROFILE).trim().toLowerCase(); + if (!Object.prototype.hasOwnProperty.call(SURFACE_PROFILES, value)) { + throw new Error(`Unknown profile "${profile}". Expected one of: ${Object.keys(SURFACE_PROFILES).join(', ')}`); + } + return value; +} + +function resolveProfileCommandKeys(profile, constraints = loadConstraintsForInstall()) { + const resolvedProfile = normalizeSurfaceProfile(profile); + const commands = constraints.commands || {}; + const out = new Set(); + const visiting = new Set(); + + function addProfile(name) { + const key = normalizeSurfaceProfile(name); + if (visiting.has(key)) { + throw new Error(`Profile cycle detected at "${key}"`); + } + visiting.add(key); + const spec = SURFACE_PROFILES[key]; + if (spec.all) { + for (const commandKey of Object.keys(commands)) out.add(commandKey); + visiting.delete(key); + return; + } + for (const parent of spec.includeProfiles || []) addProfile(parent); + for (const commandKey of spec.commands || []) { + if (Object.prototype.hasOwnProperty.call(commands, commandKey)) out.add(commandKey); + } + const categorySet = new Set(spec.categories || []); + if (categorySet.size > 0) { + for (const [commandKey, command] of Object.entries(commands)) { + if (categorySet.has(command.category)) out.add(commandKey); + } + } + visiting.delete(key); + } + + addProfile(resolvedProfile); + return out; +} + +function loadConstraintsForInstall(constraintsPath = path.join(PKG_ROOT, 'data', 'CONSTRAINTS.json')) { + return JSON.parse(fs.readFileSync(constraintsPath, 'utf8')); +} + +function commandEntryInProfile(entry, commandKeys) { + return commandKeys.has(commandRefToConstraintKey(entry.commandRef)); +} + +function collectCommandEntriesForProfile(commandsRoot, profile = DEFAULT_SURFACE_PROFILE, constraintsPath = path.join(PKG_ROOT, 'data', 'CONSTRAINTS.json')) { + const entries = collectCommandEntries(commandsRoot); + const commandKeys = resolveProfileCommandKeys(profile, loadConstraintsForInstall(constraintsPath)); + return entries.filter((entry) => commandEntryInProfile(entry, commandKeys)); +} + +function collectInstallCommandEntries(profile = DEFAULT_SURFACE_PROFILE) { + return collectCommandEntriesForProfile( + path.join(PKG_ROOT, 'commands', 'scr'), + profile, + path.join(PKG_ROOT, 'data', 'CONSTRAINTS.json') + ); +} + +function writeProfileCommandFiles(commandsRoot, commandsDir, commandEntries, transform = null) { + let count = 0; + for (const entry of commandEntries) { + const sourcePath = path.join(commandsRoot, entry.relativePath); + const sourceContent = fs.readFileSync(sourcePath, 'utf8'); + const targetPath = path.join(commandsDir, entry.relativePath); + atomicWriteFileSync(targetPath, transform ? transform(entry, sourceContent) : sourceContent); + count++; + } + return count; +} + +function writeSurfaceProfileMarker(targetDir, profile) { + if (!targetDir) return; + fs.mkdirSync(targetDir, { recursive: true }); + atomicWriteFileSync(path.join(targetDir, '.scriveno-profile'), `${normalizeSurfaceProfile(profile)}\n`); +} + +function surfaceProfileSummary(profile = DEFAULT_SURFACE_PROFILE, constraintsPath = path.join(PKG_ROOT, 'data', 'CONSTRAINTS.json')) { + const resolvedProfile = normalizeSurfaceProfile(profile); + const constraints = loadConstraintsForInstall(constraintsPath); + const commandKeys = resolveProfileCommandKeys(resolvedProfile, constraints); + return { + profile: resolvedProfile, + label: SURFACE_PROFILES[resolvedProfile].label, + description: SURFACE_PROFILES[resolvedProfile].description, + commandCount: commandKeys.size, + commands: [...commandKeys].sort(), + }; +} + +function listSurfaceProfiles(constraintsPath = path.join(PKG_ROOT, 'data', 'CONSTRAINTS.json')) { + return Object.keys(SURFACE_PROFILES).map((profile) => surfaceProfileSummary(profile, constraintsPath)); +} + +function generateSkillManifest(constraintsPath, profile = DEFAULT_SURFACE_PROFILE) { const commandsRoot = path.join(PKG_ROOT, 'commands', 'scr'); - const entries = collectCanonicalCommandInventory(commandsRoot, constraintsPath).map((entry) => ({ + const profileKeys = resolveProfileCommandKeys(profile, loadConstraintsForInstall(constraintsPath)); + const entries = collectCanonicalCommandInventory(commandsRoot, constraintsPath) + .filter((entry) => commandEntryInProfile(entry, profileKeys)) + .map((entry) => ({ name: entry.commandRef, category: entry.category, description: entry.description, @@ -239,6 +397,7 @@ function generateSkillManifest(constraintsPath) { return `# Scriveno -- AI Creative Writing Skills Version: ${VERSION} +Profile: ${normalizeSurfaceProfile(profile)} Scriveno is a spec-driven creative writing, publishing, and translation pipeline. @@ -829,12 +988,18 @@ function printHelp() { scriveno smoke --json scriveno agents --json scriveno routes --json + scriveno surface list + scriveno surface status + scriveno surface profile core --runtimes codex --project scriveno --runtimes codex,claude-code --global --writer --silent Options: --runtimes Comma-separated runtime keys to install (for example: codex,claude-code) --runtime Repeatable single-runtime selector --detected Install to every detected runtime + --profile Command profile: ${Object.keys(SURFACE_PROFILES).join(', ')} + --dry-run Show planned writes without changing files + --json Print machine-readable output for supported commands --global Install for all projects (default) --project Install only in the current directory --writer Use writer mode (default) @@ -856,6 +1021,7 @@ Audit commands: smoke Smoke-test installed runtime surfaces agents Inspect installed agent prompts and metadata routes Audit route graph and automation lanes + surface Inspect or change the installed command profile Runtime keys: ${Object.keys(RUNTIMES).join(', ')} @@ -878,6 +1044,11 @@ function parseArgs(argv) { statusApplySafe: false, auditJson: false, syncCheck: false, + installProfile: DEFAULT_SURFACE_PROFILE, + installDryRun: false, + installJson: false, + surfaceAction: 'status', + surfaceProfile: DEFAULT_SURFACE_PROFILE, }; if (argv[0] === 'status' || argv[0] === 'first-run') { @@ -959,6 +1130,72 @@ function parseArgs(argv) { } } + if (argv[0] === 'surface') { + options.command = 'surface'; + let actionSet = false; + for (let i = 1; i < argv.length; i++) { + const arg = argv[i]; + if (arg === '--help' || arg === '-h') { + options.showHelp = true; + } else if (arg === '--version' || arg === '-v') { + options.showVersion = true; + } else if (arg === '--json') { + options.installJson = true; + } else if (arg === '--silent' || arg === '--yes') { + options.silent = true; + } else if (arg === '--dry-run') { + options.installDryRun = true; + } else if (arg === '--detected') { + options.installDetected = true; + } else if (arg === '--global') { + options.isGlobal = true; + } else if (arg === '--project') { + options.isGlobal = false; + } else if (arg === '--writer') { + options.developerMode = false; + } else if (arg === '--developer') { + options.developerMode = true; + } else if (arg === '--runtime') { + const value = argv[i + 1]; + if (!value) throw new Error('--runtime requires a value'); + addRuntimeList(value); + i++; + } else if (arg.startsWith('--runtime=')) { + addRuntimeList(arg.slice('--runtime='.length)); + } else if (arg === '--runtimes') { + const value = argv[i + 1]; + if (!value) throw new Error('--runtimes requires a value'); + addRuntimeList(value); + i++; + } else if (arg.startsWith('--runtimes=')) { + addRuntimeList(arg.slice('--runtimes='.length)); + } else if (arg === '--profile') { + const value = argv[i + 1]; + if (!value) throw new Error('--profile requires a value'); + options.surfaceProfile = normalizeSurfaceProfile(value); + options.installProfile = options.surfaceProfile; + i++; + } else if (arg.startsWith('--profile=')) { + options.surfaceProfile = normalizeSurfaceProfile(arg.slice('--profile='.length)); + options.installProfile = options.surfaceProfile; + } else if (arg.startsWith('-')) { + throw new Error(`Unknown surface argument "${arg}"`); + } else if (!actionSet) { + options.surfaceAction = arg; + actionSet = true; + } else if (options.surfaceAction === 'profile') { + options.surfaceProfile = normalizeSurfaceProfile(arg); + options.installProfile = options.surfaceProfile; + } else { + throw new Error(`Unknown surface argument "${arg}"`); + } + } + if (!['status', 'list', 'profile'].includes(options.surfaceAction)) { + throw new Error(`Unknown surface action "${options.surfaceAction}". Expected status, list, or profile.`); + } + return options; + } + for (let i = 0; i < argv.length; i++) { const arg = argv[i]; if (arg === '--help' || arg === '-h') { @@ -967,6 +1204,10 @@ function parseArgs(argv) { options.showVersion = true; } else if (arg === '--silent' || arg === '--yes') { options.silent = true; + } else if (arg === '--dry-run') { + options.installDryRun = true; + } else if (arg === '--json') { + options.installJson = true; } else if (arg === '--detected') { options.installDetected = true; } else if (arg === '--global') { @@ -991,6 +1232,13 @@ function parseArgs(argv) { i++; } else if (arg.startsWith('--runtimes=')) { addRuntimeList(arg.slice('--runtimes='.length)); + } else if (arg === '--profile') { + const value = argv[i + 1]; + if (!value) throw new Error('--profile requires a value'); + options.installProfile = normalizeSurfaceProfile(value); + i++; + } else if (arg.startsWith('--profile=')) { + options.installProfile = normalizeSurfaceProfile(arg.slice('--profile='.length)); } else { throw new Error(`Unknown argument "${arg}"`); } @@ -1144,6 +1392,98 @@ function runRouteAudit({ json }) { return result; } +function resolveSurfaceDataDir(isGlobal) { + if (isGlobal === false) return path.resolve('.scriveno'); + if (isGlobal === true) return path.join(os.homedir(), '.scriveno'); + const projectDir = path.resolve('.scriveno'); + const projectSettings = path.join(projectDir, 'settings.json'); + return fs.existsSync(projectSettings) ? projectDir : path.join(os.homedir(), '.scriveno'); +} + +function readSurfaceSettings(isGlobal) { + const dataDir = resolveSurfaceDataDir(isGlobal); + const settingsPath = path.join(dataDir, 'settings.json'); + const raw = readJsonIfExists(settingsPath); + if (!raw) { + return { dataDir, settings: null, settingsPath }; + } + try { + const settings = migrateSettings(raw); + const validation = validateSettings(settings); + return { dataDir, settings, settingsPath, validation }; + } catch (err) { + return { dataDir, settings: null, settingsPath, error: err.message }; + } +} + +function formatSurfaceList(profiles) { + return [ + 'Scriveno command profiles', + ...profiles.map((profile) => `- ${profile.profile}: ${profile.commandCount} registered commands, ${profile.description}`), + ].join('\n'); +} + +function formatSurfaceStatus(status) { + const lines = [ + 'Scriveno surface status', + `Settings: ${status.settingsPath}`, + ]; + if (!status.settings) { + lines.push('Installed profile: not found'); + } else { + lines.push(`Installed profile: ${status.settings.profile || DEFAULT_SURFACE_PROFILE}`); + lines.push(`Installed runtimes: ${(status.settings.runtimes || []).join(', ') || 'none recorded'}`); + lines.push(`Scope: ${status.settings.scope || 'unknown'}`); + lines.push(`Mode: ${status.settings.developer_mode ? 'developer' : 'writer'}`); + } + lines.push(''); + lines.push('Available profiles:'); + for (const profile of listSurfaceProfiles()) { + lines.push(`- ${profile.profile}: ${profile.commandCount} registered commands`); + } + lines.push(''); + lines.push('Use `scriveno surface profile --runtimes ` to reinstall a smaller or larger surface.'); + return lines.join('\n'); +} + +function runSurface(parsed, detectedRuntimeKeys) { + if (parsed.surfaceAction === 'list') { + const profiles = listSurfaceProfiles(); + console.log(parsed.installJson ? JSON.stringify(profiles, null, 2) : formatSurfaceList(profiles)); + return profiles; + } + + const surfaceState = readSurfaceSettings(parsed.isGlobal); + + if (parsed.surfaceAction === 'status') { + console.log(parsed.installJson ? JSON.stringify(surfaceState, null, 2) : formatSurfaceStatus(surfaceState)); + return surfaceState; + } + + const runtimeKeys = parsed.runtimeKeys.length > 0 + ? parsed.runtimeKeys + : parsed.installDetected + ? detectedRuntimeKeys + : (surfaceState.settings?.runtimes && surfaceState.settings.runtimes.length > 0) + ? surfaceState.settings.runtimes + : detectedRuntimeKeys; + + if (runtimeKeys.length === 0) { + throw new Error('No runtime target found. Re-run with --runtimes or --detected.'); + } + + return runInstall({ + runtimeKeys, + isGlobal: parsed.isGlobal ?? (surfaceState.settings?.scope !== 'project'), + developerMode: parsed.developerMode ?? Boolean(surfaceState.settings?.developer_mode), + silent: parsed.silent, + installMode: 'non-interactive', + profile: parsed.surfaceProfile, + dryRun: parsed.installDryRun, + json: parsed.installJson, + }); +} + function resolveInstallRequest(parsed, detectedRuntimeKeys, { isTTY }) { const hasRuntimeDirective = parsed.runtimeKeys.length > 0 || parsed.installDetected; const hasModifierOverrides = parsed.isGlobal !== null || parsed.developerMode !== null; @@ -1172,6 +1512,9 @@ function resolveInstallRequest(parsed, detectedRuntimeKeys, { isTTY }) { developerMode: parsed.developerMode ?? false, silent: parsed.silent, installMode: 'non-interactive', + profile: parsed.installProfile, + dryRun: parsed.installDryRun, + json: parsed.installJson, }; } @@ -1180,6 +1523,9 @@ function resolveInstallRequest(parsed, detectedRuntimeKeys, { isTTY }) { isGlobal: parsed.isGlobal, developerMode: parsed.developerMode, hasModifierOverrides, + profile: parsed.installProfile, + dryRun: parsed.installDryRun, + json: parsed.installJson, }; } @@ -1314,6 +1660,7 @@ const SETTINGS_SCHEMA = [ { name: 'developer_mode', type: 'boolean', required: true, owned_by: 'user' }, { name: 'data_dir', type: 'string', required: true, owned_by: 'installer' }, { name: 'install_mode', type: 'string', required: true, enum: ['interactive', 'non-interactive'], owned_by: 'installer' }, + { name: 'profile', type: 'string', required: false, enum: Object.keys(SURFACE_PROFILES), owned_by: 'installer' }, { name: 'installed_at', type: 'string', required: true, owned_by: 'installer' }, ]; @@ -1347,6 +1694,9 @@ function migrateSettings(raw) { if (!Object.prototype.hasOwnProperty.call(out, 'install_mode') || out.install_mode === undefined) { out.install_mode = 'interactive'; } + if (!Object.prototype.hasOwnProperty.call(out, 'profile') || out.profile === undefined) { + out.profile = DEFAULT_SURFACE_PROFILE; + } return out; } @@ -1609,6 +1959,11 @@ async function main() { } const detectedRuntimeKeys = Object.entries(RUNTIMES).filter(([, runtime]) => runtime.detect()).map(([key]) => key); + if (parsed.command === 'surface') { + runSurface(parsed, detectedRuntimeKeys); + return; + } + const installRequest = resolveInstallRequest(parsed, detectedRuntimeKeys, { isTTY: Boolean(process.stdin.isTTY) }); if (installRequest.action === 'usage_error') { @@ -1684,25 +2039,31 @@ async function main() { silent: false, detectedRuntimeKeys, installMode: 'interactive', + profile: installRequest.profile, + dryRun: installRequest.dryRun, + json: installRequest.json, }); } -function installCommandRuntime(runtime, isGlobal, log) { +function installCommandRuntime(runtime, isGlobal, log, profile = DEFAULT_SURFACE_PROFILE) { const commandsDir = isGlobal ? runtime.commands_dir_global : path.resolve(runtime.commands_dir_project); const agentsDir = isGlobal ? runtime.agents_dir_global : path.resolve(runtime.agents_dir_project); - removePathIfExists(commandsDir); + const commandsRoot = path.join(PKG_ROOT, 'commands', 'scr'); + const commandEntries = collectInstallCommandEntries(profile); + const removedCommandFiles = cleanMirroredFiles(commandsRoot, commandsDir); const removedAgentFiles = cleanMirroredFiles(path.join(PKG_ROOT, 'agents'), agentsDir); - const commandCount = copyDir(path.join(PKG_ROOT, 'commands', 'scr'), commandsDir); + const commandCount = writeProfileCommandFiles(commandsRoot, commandsDir, commandEntries); + writeSurfaceProfileMarker(commandsDir, profile); const agentCount = copyDir(path.join(PKG_ROOT, 'agents'), agentsDir); - log(` ${c('green', 'OK')} ${runtime.label}: ${commandCount} command files -> ${c('dim', commandsDir)}`); + log(` ${c('green', 'OK')} ${runtime.label}: ${commandCount} command files (${normalizeSurfaceProfile(profile)} profile) -> ${c('dim', commandsDir)}${removedCommandFiles ? c('dim', ` (cleaned ${removedCommandFiles} stale files)`) : ''}`); log(` ${c('green', 'OK')} ${runtime.label}: ${agentCount} agent prompts -> ${c('dim', agentsDir)}${removedAgentFiles ? c('dim', ` (cleaned ${removedAgentFiles} stale files)`) : ''}`); } -function installClaudeCommandRuntime(runtime, isGlobal, log) { +function installClaudeCommandRuntime(runtime, isGlobal, log, profile = DEFAULT_SURFACE_PROFILE) { const commandsDir = isGlobal ? runtime.commands_dir_global : path.resolve(runtime.commands_dir_project); const agentsDir = isGlobal ? runtime.agents_dir_global : path.resolve(runtime.agents_dir_project); const commandsRoot = path.join(PKG_ROOT, 'commands', 'scr'); - const commandEntries = collectCommandEntries(commandsRoot); + const commandEntries = collectInstallCommandEntries(profile); const fileNames = commandEntries.map((entry) => commandEntryToFlatCommandFileName(entry)); fs.mkdirSync(commandsDir, { recursive: true }); @@ -1718,23 +2079,31 @@ function installClaudeCommandRuntime(runtime, isGlobal, log) { } writeInstalledCommandManifest(commandsDir, 'claude-code', fileNames); + writeSurfaceProfileMarker(commandsDir, profile); const agentCount = copyDir(path.join(PKG_ROOT, 'agents'), agentsDir); - log(` ${c('green', 'OK')} ${runtime.label}: ${commandEntries.length} /scr-* command files -> ${c('dim', commandsDir)}${removedCommandFiles ? c('dim', ` (cleaned ${removedCommandFiles} stale items)`) : ''}`); + log(` ${c('green', 'OK')} ${runtime.label}: ${commandEntries.length} /scr-* command files (${normalizeSurfaceProfile(profile)} profile) -> ${c('dim', commandsDir)}${removedCommandFiles ? c('dim', ` (cleaned ${removedCommandFiles} stale items)`) : ''}`); log(` ${c('green', 'OK')} ${runtime.label}: ${agentCount} agent prompts -> ${c('dim', agentsDir)}${removedAgentFiles ? c('dim', ` (cleaned ${removedAgentFiles} stale files)`) : ''}`); } -function installManifestSkillRuntime(runtime, isGlobal, log) { +function installManifestSkillRuntime(runtime, isGlobal, log, profile = DEFAULT_SURFACE_PROFILE) { const skillsDir = isGlobal ? runtime.skills_dir_global : path.resolve(runtime.skills_dir_project); - removePathIfExists(skillsDir); - const manifest = generateSkillManifest(path.join(PKG_ROOT, 'data', 'CONSTRAINTS.json')); + const commandsRoot = path.join(PKG_ROOT, 'commands', 'scr'); + const commandEntries = collectInstallCommandEntries(profile); + const manifest = generateSkillManifest(path.join(PKG_ROOT, 'data', 'CONSTRAINTS.json'), profile); fs.mkdirSync(skillsDir, { recursive: true }); + removePathIfExists(path.join(skillsDir, 'SKILL.md')); + const commandsTarget = path.join(skillsDir, 'commands', 'scr'); + const agentsTarget = path.join(skillsDir, 'agents'); + cleanMirroredFiles(commandsRoot, commandsTarget); + cleanMirroredFiles(path.join(PKG_ROOT, 'agents'), agentsTarget); atomicWriteFileSync(path.join(skillsDir, 'SKILL.md'), manifest); - const commandCount = copyDir(path.join(PKG_ROOT, 'commands', 'scr'), path.join(skillsDir, 'commands', 'scr')); - const agentCount = copyDir(path.join(PKG_ROOT, 'agents'), path.join(skillsDir, 'agents')); + const commandCount = writeProfileCommandFiles(commandsRoot, commandsTarget, commandEntries); + writeSurfaceProfileMarker(skillsDir, profile); + const agentCount = copyDir(path.join(PKG_ROOT, 'agents'), agentsTarget); log(` ${c('green', 'OK')} ${runtime.label}: SKILL.md manifest -> ${c('dim', path.join(skillsDir, 'SKILL.md'))}`); - log(` ${c('green', 'OK')} ${runtime.label}: ${commandCount} command files -> ${c('dim', path.join(skillsDir, 'commands', 'scr'))}`); - log(` ${c('green', 'OK')} ${runtime.label}: ${agentCount} agent prompts -> ${c('dim', path.join(skillsDir, 'agents'))}`); + log(` ${c('green', 'OK')} ${runtime.label}: ${commandCount} command files (${normalizeSurfaceProfile(profile)} profile) -> ${c('dim', commandsTarget)}`); + log(` ${c('green', 'OK')} ${runtime.label}: ${agentCount} agent prompts -> ${c('dim', agentsTarget)}`); } function installCodexAgentsWithMetadata(agentsDir) { @@ -1757,17 +2126,15 @@ function installCodexAgentsWithMetadata(agentsDir) { }; } -function installCodexRuntime(runtime, isGlobal, log) { +function installCodexRuntime(runtime, isGlobal, log, profile = DEFAULT_SURFACE_PROFILE) { const skillsDir = isGlobal ? runtime.skills_dir_global : path.resolve(runtime.skills_dir_project); const commandsDir = isGlobal ? runtime.commands_dir_global : path.resolve(runtime.commands_dir_project); const agentsDir = isGlobal ? runtime.agents_dir_global : path.resolve(runtime.agents_dir_project); const sourceCommandsRoot = path.join(PKG_ROOT, 'commands', 'scr'); - const commandEntries = collectCommandEntries(sourceCommandsRoot); + const commandEntries = collectInstallCommandEntries(profile); const skillNames = commandEntries.map((entry) => entry.skillName); - // Wipe the installed commands dir so stale files from previous installs - // (removed commands, legacy flat layouts, etc.) do not linger. - removePathIfExists(commandsDir); + const removedCommandFiles = cleanMirroredFiles(sourceCommandsRoot, commandsDir); fs.mkdirSync(skillsDir, { recursive: true }); const removedSkillDirs = cleanCodexSkillDirs(skillsDir, skillNames); @@ -1779,13 +2146,8 @@ function installCodexRuntime(runtime, isGlobal, log) { // clean content -- not on top of a previously-marked installed file -- so // re-runs are idempotent (single marker, current prose rewrite). let commandCount = 0; - for (const entry of commandEntries) { - const sourcePath = path.join(sourceCommandsRoot, entry.relativePath); - const sourceContent = fs.readFileSync(sourcePath, 'utf8'); - const targetPath = path.join(commandsDir, entry.relativePath); - atomicWriteFileSync(targetPath, generateCodexCommandContent(entry, sourceContent)); - commandCount++; - } + commandCount = writeProfileCommandFiles(sourceCommandsRoot, commandsDir, commandEntries, generateCodexCommandContent); + writeSurfaceProfileMarker(commandsDir, profile); const agentInstall = installCodexAgentsWithMetadata(agentsDir); @@ -1796,13 +2158,14 @@ function installCodexRuntime(runtime, isGlobal, log) { atomicWriteFileSync(path.join(skillDir, 'SKILL.md'), generateCodexSkill(entry, commandPath)); } writeCodexSkillManifest(skillsDir, skillNames); + writeSurfaceProfileMarker(skillsDir, profile); - log(` ${c('green', 'OK')} ${runtime.label}: ${commandEntries.length} \$scr-* skills -> ${c('dim', skillsDir)}${removedSkillDirs ? c('dim', ` (cleaned ${removedSkillDirs} stale dirs)`) : ''}`); - log(` ${c('green', 'OK')} ${runtime.label}: ${commandCount} command files -> ${c('dim', commandsDir)}`); + log(` ${c('green', 'OK')} ${runtime.label}: ${commandEntries.length} \$scr-* skills (${normalizeSurfaceProfile(profile)} profile) -> ${c('dim', skillsDir)}${removedSkillDirs ? c('dim', ` (cleaned ${removedSkillDirs} stale dirs)`) : ''}`); + log(` ${c('green', 'OK')} ${runtime.label}: ${commandCount} command files -> ${c('dim', commandsDir)}${removedCommandFiles ? c('dim', ` (cleaned ${removedCommandFiles} stale files)`) : ''}`); log(` ${c('green', 'OK')} ${runtime.label}: ${agentInstall.agentCount} agent prompts + ${agentInstall.metadataCount} metadata files -> ${c('dim', agentsDir)}${agentInstall.removed ? c('dim', ` (cleaned ${agentInstall.removed} stale files)`) : ''}`); } -function installGuidedRuntime(runtime, isGlobal, dataDir, log) { +function installGuidedRuntime(runtime, isGlobal, dataDir, log, profile = DEFAULT_SURFACE_PROFILE) { const guideDir = isGlobal ? runtime.guide_dir_global : path.resolve(runtime.guide_dir_project); const currentProjectDir = path.resolve('.'); const setupGuide = generatePerplexitySetupGuide({ @@ -1821,12 +2184,13 @@ function installGuidedRuntime(runtime, isGlobal, dataDir, log) { atomicWriteFileSync(path.join(guideDir, 'SETUP.md'), setupGuide); atomicWriteFileSync(path.join(guideDir, 'connector-command.txt'), connectorCommand + '\n'); atomicWriteFileSync(path.join(guideDir, 'connector-command.current-project.txt'), currentProjectCommand + '\n'); + writeSurfaceProfileMarker(guideDir, profile); log(` ${c('green', 'OK')} ${runtime.label}: setup guide -> ${c('dim', path.join(guideDir, 'SETUP.md'))}`); log(` ${c('green', 'OK')} ${runtime.label}: connector recipe -> ${c('dim', path.join(guideDir, 'connector-command.txt'))}`); } -function writeSharedAssets(dataDir, runtimeKeys, isGlobal, developerMode, installMode, log) { +function writeSharedAssets(dataDir, runtimeKeys, isGlobal, developerMode, installMode, log, profile = DEFAULT_SURFACE_PROFILE) { fs.mkdirSync(path.join(dataDir, 'templates'), { recursive: true }); fs.mkdirSync(path.join(dataDir, 'data'), { recursive: true }); fs.mkdirSync(path.join(dataDir, 'lib'), { recursive: true }); @@ -1873,6 +2237,7 @@ function writeSharedAssets(dataDir, runtimeKeys, isGlobal, developerMode, instal developer_mode: developerMode, data_dir: dataDir, install_mode: installMode, + profile: normalizeSurfaceProfile(profile), installed_at: new Date().toISOString(), }; const mergedSettings = mergeSettings(existingSettings, incomingSettings); @@ -1929,14 +2294,101 @@ function collectTargetDirsForSweep(runtimeKeys, isGlobal, dataDir) { return Array.from(dirs); } -function runInstall({ runtimeKeys, isGlobal, developerMode, silent, installMode }) { +function runtimeInstallTargets(runtimeKey, runtime, isGlobal) { + const resolve = (globalPath, projectPath) => isGlobal ? globalPath : (projectPath ? path.resolve(projectPath) : null); + return { + runtime: runtimeKey, + label: runtime.label, + type: runtime.type, + commandsDir: resolve(runtime.commands_dir_global, runtime.commands_dir_project), + skillsDir: resolve(runtime.skills_dir_global, runtime.skills_dir_project), + agentsDir: resolve(runtime.agents_dir_global, runtime.agents_dir_project), + guideDir: resolve(runtime.guide_dir_global, runtime.guide_dir_project), + }; +} + +function buildInstallDryRun({ runtimeKeys, isGlobal, developerMode, installMode, profile = DEFAULT_SURFACE_PROFILE }) { + const resolvedProfile = normalizeSurfaceProfile(profile); const dataDir = isGlobal ? path.join(os.homedir(), '.scriveno') : path.resolve('.scriveno'); - const log = silent ? () => {} : (message) => console.log(message); + const commandEntries = collectInstallCommandEntries(resolvedProfile); + const profileSummary = surfaceProfileSummary(resolvedProfile); + const agentCount = collectAgentEntries(path.join(PKG_ROOT, 'agents')).length; + const sharedAssets = { + templates: listRelativeFiles(path.join(PKG_ROOT, 'templates')).length, + data: listRelativeFiles(path.join(PKG_ROOT, 'data')).length, + lib: listRelativeFiles(path.join(PKG_ROOT, 'lib')).length, + }; + + return { + version: VERSION, + profile: resolvedProfile, + profileLabel: profileSummary.label, + profileDescription: profileSummary.description, + registeredCommands: profileSummary.commandCount, + commandFiles: commandEntries.length, + agentPrompts: agentCount, + scope: isGlobal ? 'global' : 'project', + mode: developerMode ? 'developer' : 'writer', + installMode, + dataDir, + sharedAssets, + targets: runtimeKeys.map((runtimeKey) => { + const runtime = RUNTIMES[runtimeKey]; + if (!runtime) throw new Error(`Unknown runtime "${runtimeKey}"`); + return runtimeInstallTargets(runtimeKey, runtime, isGlobal); + }), + writes: false, + }; +} + +function formatInstallDryRunReport(plan) { + const lines = [ + 'Scriveno install dry run', + `Version: ${plan.version}`, + `Profile: ${plan.profile} (${plan.profileLabel})`, + `Scope: ${plan.scope}`, + `Mode: ${plan.mode}`, + `Command files selected: ${plan.commandFiles}`, + `Registered command entries selected: ${plan.registeredCommands}`, + `Agent prompts selected: ${plan.agentPrompts}`, + `Shared assets: ${plan.sharedAssets.templates} templates, ${plan.sharedAssets.data} data files, ${plan.sharedAssets.lib} lib files`, + `Data directory: ${plan.dataDir}`, + '', + 'Runtime targets:', + ]; + for (const target of plan.targets) { + lines.push(`- ${target.label} (${target.runtime})`); + if (target.commandsDir) lines.push(` commands: ${target.commandsDir}`); + if (target.skillsDir) lines.push(` skills: ${target.skillsDir}`); + if (target.agentsDir) lines.push(` agents: ${target.agentsDir}`); + if (target.guideDir) lines.push(` guide: ${target.guideDir}`); + } + lines.push(''); + lines.push('No files were written.'); + return lines.join('\n'); +} + +function runInstall({ runtimeKeys, isGlobal, developerMode, silent, installMode, profile = DEFAULT_SURFACE_PROFILE, dryRun = false, json = false }) { + const resolvedProfile = normalizeSurfaceProfile(profile); + const dataDir = isGlobal ? path.join(os.homedir(), '.scriveno') : path.resolve('.scriveno'); + const log = (silent || json || dryRun) ? () => {} : (message) => console.log(message); if (!runtimeKeys.length) { throw new Error('No runtimes selected for installation'); } + if (dryRun) { + const plan = buildInstallDryRun({ + runtimeKeys, + isGlobal, + developerMode, + installMode, + profile: resolvedProfile, + }); + console.log(json ? JSON.stringify(plan, null, 2) : formatInstallDryRunReport(plan)); + return plan; + } + let totalOrphansRemoved = 0; for (const dir of collectTargetDirsForSweep(runtimeKeys, isGlobal, dataDir)) { totalOrphansRemoved += cleanOrphanedTempFiles(dir); @@ -1945,7 +2397,7 @@ function runInstall({ runtimeKeys, isGlobal, developerMode, silent, installMode log(c('dim', ` Cleaned ${totalOrphansRemoved} orphaned temp file(s) from prior interrupted install`)); } - if (!silent) { + if (!silent && !json) { console.log('\n' + c('bold', 'Installing...')); } @@ -1955,27 +2407,42 @@ function runInstall({ runtimeKeys, isGlobal, developerMode, silent, installMode throw new Error(`Unknown runtime "${runtimeKey}"`); } if (runtimeKey === 'codex') { - installCodexRuntime(runtime, isGlobal, log); + installCodexRuntime(runtime, isGlobal, log, resolvedProfile); } else if (runtime.command_layout === 'flat-prefixed') { - installClaudeCommandRuntime(runtime, isGlobal, log); + installClaudeCommandRuntime(runtime, isGlobal, log, resolvedProfile); } else if (runtime.type === 'skills') { - installManifestSkillRuntime(runtime, isGlobal, log); + installManifestSkillRuntime(runtime, isGlobal, log, resolvedProfile); } else if (runtime.type === 'guided-mcp') { - installGuidedRuntime(runtime, isGlobal, dataDir, log); + installGuidedRuntime(runtime, isGlobal, dataDir, log, resolvedProfile); } else { - installCommandRuntime(runtime, isGlobal, log); + installCommandRuntime(runtime, isGlobal, log, resolvedProfile); } } - writeSharedAssets(dataDir, runtimeKeys, isGlobal, developerMode, installMode, log); + writeSharedAssets(dataDir, runtimeKeys, isGlobal, developerMode, installMode, log, resolvedProfile); + + const summary = { + version: VERSION, + runtimes: runtimeKeys, + scope: isGlobal ? 'global' : 'project', + mode: developerMode ? 'developer' : 'writer', + profile: resolvedProfile, + dataDir, + }; + + if (json) { + console.log(JSON.stringify(summary, null, 2)); + return summary; + } if (silent) { - console.log(`Installed Scriveno ${VERSION} to ${runtimeKeys.join(', ')} (${isGlobal ? 'global' : 'project'}, ${developerMode ? 'developer' : 'writer'} mode).`); - return; + console.log(`Installed Scriveno ${VERSION} to ${runtimeKeys.join(', ')} (${isGlobal ? 'global' : 'project'}, ${developerMode ? 'developer' : 'writer'} mode, ${resolvedProfile} profile).`); + return summary; } console.log('\n' + c('bold', c('green', 'Installation complete!'))); printNextSteps(runtimeKeys); + return summary; } // Only run interactive installer when executed directly @@ -1990,14 +2457,19 @@ if (require.main === module) { module.exports = { copyDir, RUNTIMES, + SURFACE_PROFILES, + DEFAULT_SURFACE_PROFILE, parseArgs, resolveInstallRequest, + runInstall, runStatus, runSyncCheck, runRuntimeSmoke, runAgentAvailability, runRouteAudit, collectCommandEntries, + collectCommandEntriesForProfile, + collectInstallCommandEntries, collectAgentEntries, assertNoSkillNameCollisions, cleanCodexSkillDirs, @@ -2015,9 +2487,16 @@ module.exports = { installManifestSkillRuntime, installCodexRuntime, installCodexAgentsWithMetadata, + runSurface, cleanFlatCommandFiles, generateCodexSkill, generateSkillManifest, + normalizeSurfaceProfile, + resolveProfileCommandKeys, + surfaceProfileSummary, + listSurfaceProfiles, + buildInstallDryRun, + formatInstallDryRunReport, buildFilesystemMcpCommand, generatePerplexitySetupGuide, atomicWriteFileSync, diff --git a/commands/scr/export.md b/commands/scr/export.md index c967df9..43959e4 100644 --- a/commands/scr/export.md +++ b/commands/scr/export.md @@ -1,6 +1,6 @@ --- description: Compile and export manuscript to publication-ready formats. -argument-hint: "[--format ] [--formatted] [--print-ready] [--skip-validate]" +argument-hint: "[--format ] [--formatted] [--print-ready] [--skip-validate] [--check]" --- # /scr:export -- Manuscript Export @@ -11,11 +11,13 @@ Assemble the manuscript from OUTLINE.md and export to the specified format. Hand ``` /scr:export # interactive picker -/scr:export --format [--formatted] [--print-ready] [--skip-validate] +/scr:export --format [--formatted] [--print-ready] [--skip-validate] [--check] ``` If `--format` is omitted, run the interactive picker (see STEP 0). Otherwise jump straight to STEP 1 with the requested format. +If `--check` is passed, check format availability and required external tools, print a readiness report, and stop before assembling or writing export files. + --- ### STEP 0: INTERACTIVE PICKER (only when --format is omitted) @@ -135,6 +137,8 @@ Then **stop**. **Check for scaffold markers in `.manuscript/drafts/`.** +If `--check` was passed, skip the scaffold-marker scan and proceed to STEP 2. Tool check mode verifies readiness of external commands only; it does not certify manuscript content. + Scan all `.md` files in `.manuscript/drafts/` for: - Lines containing `[Fill in` (covers `[Fill in:]`, `[Fill in or delete:]`) - Lines containing `[Delete if not applicable:]` @@ -217,6 +221,27 @@ Proceed to STEP 2. Check for required external tools based on the requested format. +**Tool gate rules:** + +1. Never silently substitute one tool for another. For example, do not use WeasyPrint when Typst is missing, and do not use Calibre when Pandoc is missing. +2. Record the tool command that was checked and, when available, the detected version. +3. If `--check` was passed, do not create `.manuscript/output/`, do not assemble the manuscript, and do not run conversion commands. Print the report and stop after this step. +4. If a required tool is missing, stop and show install instructions. Do not attempt a degraded export. + +Use this readiness report shape in `--check` mode: + +```text +Export Tool Check +================= +Format: pdf --print-ready + +[PASS] pandoc .......... pandoc 3.x +[PASS] typst ........... typst 0.x +[SKIP] ghostscript ..... not required for this format + +Result: ready +``` + **For markdown:** No external tools needed. Skip to Step 3. **For docx, epub, latex, query-package:** Check for Pandoc: @@ -269,6 +294,22 @@ If not found, provide install instructions (`pip install screenplain`) and stop. **For fountain:** No external tools needed. +**For kdp-package:** Check for Pandoc and Typst. If either tool is missing, stop before building the package. + +**For ingram-package:** Check for Pandoc, Typst, and Ghostscript (`gs`). If any required tool is missing, stop before building the package. + +**For query-package and submission-package:** Check for Pandoc. + +**Optional screenplay PDF:** Afterwriting is optional for `fountain`. If it is missing, do not fail the Fountain export; report it as optional. If `--check` is passed, show it as optional with install guidance: + +```bash +command -v afterwriting >/dev/null 2>&1 +``` + +> Optional: To generate a formatted screenplay PDF, install Afterwriting with `npm i -g afterwriting`. + +If `--check` was passed, stop here after printing the readiness report. + --- ### STEP 3: ASSEMBLE MANUSCRIPT diff --git a/commands/scr/health.md b/commands/scr/health.md index 3fe22af..83156e9 100644 --- a/commands/scr/health.md +++ b/commands/scr/health.md @@ -1,6 +1,6 @@ --- description: Diagnose and repair common project state issues. -argument-hint: "[--repair]" +argument-hint: "[--repair] [--context]" --- # Health @@ -87,6 +87,39 @@ With `--repair`, fix what can be auto-fixed: After repair: re-run diagnostics and show the updated health report. +## Context mode (--context) + +With `--context`, focus only on loaded-context health. This is a read-only guard for long projects and large unit sessions. + +1. Estimate the files an agent is likely to load before writing: + - `.manuscript/STYLE-GUIDE.md` + - `.manuscript/CONTEXT.md` + - `.manuscript/STATE.md` + - `.manuscript/OUTLINE.md` + - `.manuscript/RECORD.md` + - The newest plan, draft, and review files for the active unit +2. Estimate tokens as `ceil(bytes / 4)`. This is intentionally simple and conservative. +3. Classify the result: + - `ok`: under 45,000 estimated tokens + - `watch`: 45,000-79,999 estimated tokens + - `tight`: 80,000-119,999 estimated tokens + - `critical`: 120,000 or more estimated tokens +4. Report the five largest loaded files so the writer can see what is making the session heavy. +5. If the state is `watch`, suggest `/scr:health --context` again before the next large operation. +6. If the state is `tight`, suggest `/scr:save` before drafting, review, translation, or export. +7. If the state is `critical`, suggest `/scr:thread` or a fresh unit session before writing more prose. + +If the shared CLI engine is available, prefer it: + +```bash +scriveno status --project "$PWD" --trigger "/scr:health --context" +node lib/auto-invoke-engine.js --project "$PWD" --trigger "/scr:health --context" +node "$HOME/.scriveno/lib/auto-invoke-engine.js" --project "$PWD" --trigger "/scr:health --context" +node .scriveno/lib/auto-invoke-engine.js --project "$PWD" --trigger "/scr:health --context" +``` + +Return only the context-health portion of the report unless another health issue blocks the command. + ## Automation Status Every response must include a compact status block: @@ -100,6 +133,7 @@ Candidate agents: - none Local operations: - health checks run: {count} +- context estimate run: {yes|no} - repairs applied: {count} Candidate local helpers: - /scr:scan or /scr:save when health detects repairable drift diff --git a/commands/scr/help.md b/commands/scr/help.md index fe05fff..7de140d 100644 --- a/commands/scr/help.md +++ b/commands/scr/help.md @@ -34,7 +34,7 @@ You are helping the user navigate Scriveno commands. Load Scriveno's installed/s ## The "getting started" view (no project yet) -Ask the user what they want to do. Don't list all 113 commands -- show them this: +Ask the user what they want to do. Don't list all 115 commands -- show them this: ``` Scriveno -- ready to start. diff --git a/commands/scr/new-work.md b/commands/scr/new-work.md index b8e3ef5..2b2413e 100644 --- a/commands/scr/new-work.md +++ b/commands/scr/new-work.md @@ -69,7 +69,7 @@ Always create `RECORD.md` from `templates/RECORD.md` without renaming it. It is Write `.manuscript/config.json` by starting from `templates/config.json` and filling the project-specific values. The generated config must include the shared settings blocks that later commands read: ```json { - "scriveno_version": "2.7.2", + "scriveno_version": "2.8.0", "work_type": "", "group": "", "command_unit": "", diff --git a/commands/scr/proof-unit.md b/commands/scr/proof-unit.md new file mode 100644 index 0000000..2a3ee2a --- /dev/null +++ b/commands/scr/proof-unit.md @@ -0,0 +1,138 @@ +--- +description: Run one manuscript unit through a proof path from voice profile to export readiness. +argument-hint: "[unit] [--demo] [--export-check]" +--- + +# Proof Unit + +You are proving that Scriveno can carry one unit through the real writing loop without losing the writer's voice. + +Follow the auto-invoke policy. In the source repository it is documented at `docs/auto-invoke-policy.md`. `/scr:proof-unit` coordinates existing commands. It should keep every step small and stop before any destructive or publishing action unless the writer explicitly confirms. + +## Goal + +Run one chosen unit through this vertical slice: + +1. Confirm project and voice context. +2. Discuss or refine the unit shape. +3. Plan the unit. +4. Draft the unit with fresh context. +5. Review the draft for voice, continuity, and revision issues. +6. Check context health before another large operation. +7. Check export prerequisites without writing final packages unless requested. + +The proof is successful only if the unit still sounds like the writer and the next production step is obvious. + +## Inputs + +- `unit`: The unit number or label. If omitted, infer it from `.manuscript/STATE.md`. +- `--demo`: Prefer the demo project path and use its planned proof unit. +- `--export-check`: Run `/scr:export --check` after review to verify external tools. + +If there is no `.manuscript/` directory, recommend `/scr:demo` or `/scr:new-work` before doing anything else. + +## Required Checks + +1. Read `.manuscript/STYLE-GUIDE.md` first. If it is missing or thin, suggest `/scr:profile-writer` or `/scr:voice-test` before drafting. +2. Read `.manuscript/STATE.md`, `.manuscript/OUTLINE.md`, and `.manuscript/config.json`. +3. Run context health with the shared engine when available: + +```bash +scriveno status --project "$PWD" --trigger /scr:proof-unit +node lib/auto-invoke-engine.js --project "$PWD" --trigger /scr:proof-unit +node "$HOME/.scriveno/lib/auto-invoke-engine.js" --project "$PWD" --trigger /scr:proof-unit +node .scriveno/lib/auto-invoke-engine.js --project "$PWD" --trigger /scr:proof-unit +``` + +4. If context health is `tight` or `critical`, stop before drafting and suggest `/scr:save`, `/scr:thread`, or a fresh unit session. +5. Verify that the plan, draft, and review files are all unit-scoped. Do not load the whole manuscript into a drafting pass. + +## Execution Path + +Use the first missing step in order: + +1. Missing voice profile: `/scr:profile-writer` or `/scr:voice-test`. +2. Missing unit discussion: `/scr:discuss `. +3. Missing unit plan: `/scr:plan `. +4. Missing unit draft: `/scr:draft `. +5. Missing review: `/scr:editor-review `. +6. Review flags voice drift: `/scr:voice-check `. +7. Review flags continuity drift: `/scr:continuity-check `. +8. Review passes and `--export-check` was provided: `/scr:export --check`. +9. Review passes without export check: `/scr:submit ` or `/scr:progress`. + +Do not run more than one prose-generating step without reporting back. The writer should see the proof path as a sequence of verified stops, not a silent batch. + +## Proof Report + +End with a compact proof report: + +```markdown +Proof unit: Chapter 3 +Voice profile: present +Plan: present +Draft: present +Review: present +Context health: ok +Export prerequisites: not checked + +Result: ready for submit, with no blocking voice drift found. +``` + +When something fails, name the missing file or blocking step and offer the direct command that fixes it. + +## Automation Status + +Every response must include a compact status block: + +```text +Automation status: +Trigger: /scr:proof-unit {unit} +Spawned agents: +- drafter when /scr:draft is the selected step +- voice-checker when draft or voice review is selected +- diagnostic worker when /scr:editor-review is selected +Candidate agents: +- drafter +- voice-checker +- continuity-checker +Local operations: +- project state inspected: yes +- context health estimated: yes +- export prerequisite check: {yes|no} +Candidate local helpers: +- /scr:health --context +- /scr:export --check +Manual gates: +- publish or final package creation +Auto-invoked: +- {selected command}: {yes|no} +Why: proof-unit coordinates the smallest real writing loop while keeping publishing actions explicit +``` + +## Response Contract + +Every writer-facing response must end with one to four next-command suggestions. Each suggestion must include a short explanation of what that path will do. + +Use this format: + +```markdown +Next commands: +- `/scr:...`: One short sentence explaining what this path will do. +- `/scr:...`: One short sentence explaining what this alternate path will do. +``` + +If exactly one path is clearly best, provide one suggestion. If two, three, or four useful paths exist, show them as alternatives. Do not force a linear path when the writer has a real choice. + +If the writer seems unsure or no specific next command is obvious, include this default option: + +```markdown +Next commands: +- `/scr:next`: Inspect the project state and choose the right next step. +``` + +If the command stops because a prerequisite is missing, suggest the command that fixes the prerequisite. Keep every explanation practical and writer-facing. + +## Tone + +Focused and evidence-first. The point is to build trust with one real unit, not to rush the whole manuscript. diff --git a/commands/scr/publish.md b/commands/scr/publish.md index 648ca2e..641cfb3 100644 --- a/commands/scr/publish.md +++ b/commands/scr/publish.md @@ -1,6 +1,6 @@ --- description: "Publishing wizard or preset-driven pipeline. Chains export commands based on destination." -argument-hint: "[--preset ] [--all] [--skip-validate]" +argument-hint: "[--preset ] [--all] [--skip-validate] [--preflight]" --- # /scr:publish -- Publishing Wizard @@ -10,12 +10,13 @@ You are the publishing wizard. Your job is to turn a completed manuscript into p ## Usage ``` -/scr:publish [--preset ] [--all] [--skip-validate] +/scr:publish [--preset ] [--all] [--skip-validate] [--preflight] ``` - `--preset ` -- Run a named preset pipeline without questions - `--all` -- Run every preset available for the current work type - `--skip-validate` -- Skip the scaffold-marker gate (not recommended) +- `--preflight` -- Check readiness and required tools, then stop before writing deliverables - No arguments -- Run the interactive wizard --- @@ -124,6 +125,48 @@ Proceed to STEP 2. --- +### STEP 1.7: PREFLIGHT MODE + +If `--preflight` was passed, run a readiness check only and stop before generating front matter, back matter, exports, packages, or history entries. + +Preflight must include: + +1. The validation gate from STEP 1.5 unless `--skip-validate` was also passed. +2. The front-matter scaffold exclusion check from STEP 1.6a. +3. The publishing prerequisite checklist from STEP 3a. +4. Preset availability against `CONSTRAINTS.json`. +5. External tool checks by calling the matching export checks: + - `share-pdf`: `/scr:export --format pdf --check` + - `share-docx`: `/scr:export --format docx --check` + - `share-epub`: `/scr:export --format epub --check` + - `kdp-ebook`: `/scr:export --format epub --check` + - `kdp-paperback`: `/scr:export --format pdf --print-ready --check` and `/scr:export --format kdp-package --check` + - `ingram-paperback`: `/scr:export --format pdf --print-ready --check` and `/scr:export --format ingram-package --check` + - `query-submission`: `/scr:export --format query-package --check` + - `submission-package`: `/scr:export --format submission-package --check` + - `screenplay-query`: `/scr:export --format fountain --check`, `/scr:export --format fdx --check`, and `/scr:export --format query-package --check` + - `all-formats`: check each available base format + +Use this report shape: + +```text +Publishing Preflight +==================== +Preset: kdp-paperback + +[PASS] manuscript validation +[WARN] front matter ........ not generated +[PASS] pandoc +[PASS] typst +[SKIP] ghostscript ......... not required until ingram-package + +Result: ready after 1 optional item +``` + +If preflight finds a blocking issue, stop and suggest the command that fixes it. Do not continue into STEP 2. + +--- + ### STEP 2: ROUTE **If `--preset` is given:** Validate the preset name against the known presets below. If valid, jump to STEP 4 (preset pipeline). If invalid, show the list of available presets and stop. diff --git a/commands/scr/surface.md b/commands/scr/surface.md new file mode 100644 index 0000000..171eabc --- /dev/null +++ b/commands/scr/surface.md @@ -0,0 +1,149 @@ +--- +description: Inspect or change the installed Scriveno command profile. +argument-hint: "[list|status|profile ] [--runtime ] [--dry-run]" +--- + +# Surface + +You are helping the writer keep Scriveno's installed command surface clear, small, and appropriate for the work they are doing. + +Follow the auto-invoke policy. In the source repository it is documented at `docs/auto-invoke-policy.md`. `/scr:surface` is a local installer helper. It does not spawn agents and should not edit a manuscript. + +## What This Command Does + +Use this command when the writer wants fewer commands, a publishing-only install, a translation-focused install, or a check of what profile is currently installed. + +Available profiles: + +- `core`: Main writing loop and orientation commands. +- `writing`: Core workflow plus revision, structure, character, and quality commands. +- `publishing`: Core workflow plus export, publishing, metadata, and platform packaging commands. +- `translation`: Core workflow plus translation, localization, glossary, and multi-publish commands. +- `specialist`: Core workflow plus sacred, illustration, collaboration, and utility surfaces. +- `full`: Every Scriveno command. + +## Actions + +### `list` + +Run: + +```bash +scriveno surface list +``` + +Report each profile, command count, and intended use. + +### `status` + +Run: + +```bash +scriveno surface status +``` + +Report: + +- Installed profile from `.scriveno/settings.json` or `~/.scriveno/settings.json` +- Installed runtimes +- Scope (`project` or `global`) +- Writer or developer mode +- Suggested profile if the current one is much larger than the active workflow needs + +### `profile ` + +Before changing anything, run a dry run: + +```bash +scriveno surface profile --dry-run +``` + +If the writer confirms, reinstall that profile for the same runtime surface: + +```bash +scriveno surface profile +``` + +If no prior runtime is recorded, ask for one runtime target, then run: + +```bash +scriveno surface profile --runtimes codex --project +``` + +Use the active runtime name instead of `codex` when the writer is in Claude Code, Cursor, Gemini CLI, OpenCode, GitHub Copilot, Windsurf, Antigravity, Manus, Perplexity Desktop, or the generic skill fallback. + +## Safety Rules + +1. Prefer `--dry-run` before changing the installed surface. +2. Do not delete custom user files. The installer cleans only Scriveno-owned command mirrors. +3. Keep agents installed even when a smaller command profile is selected. Drafting, review, and translation still need their agent prompts when a profile includes those workflows. +4. Do not imply that a smaller profile disables project data. It only changes the installed command surface. +5. If the writer is unsure, recommend `writing` for active drafting and `full` for exploration. + +## Output Shape + +Use this shape: + +```markdown +Surface profile: writing +Installed runtimes: codex +Scope: project + +Selected commands: 52 command files +Best use: active drafting with revision and structure tools + +No files changed because this was a dry run. +``` + +## Automation Status + +Every response must include a compact status block: + +```text +Automation status: +Trigger: /scr:surface {args} +Spawned agents: +- none +Candidate agents: +- none +Local operations: +- surface profile inspected: {yes|no} +- installer dry run: {yes|no} +- installer profile change: {yes|no} +Candidate local helpers: +- scriveno surface list +- scriveno surface status +- scriveno surface profile --dry-run +Manual gates: +- writer confirmation before changing installed runtimes +Auto-invoked: +- none +Why: surface management changes installed command files, not manuscript prose +``` + +## Response Contract + +Every writer-facing response must end with one to four next-command suggestions. Each suggestion must include a short explanation of what that path will do. + +Use this format: + +```markdown +Next commands: +- `/scr:...`: One short sentence explaining what this path will do. +- `/scr:...`: One short sentence explaining what this alternate path will do. +``` + +If exactly one path is clearly best, provide one suggestion. If two, three, or four useful paths exist, show them as alternatives. Do not force a linear path when the writer has a real choice. + +If the writer seems unsure or no specific next command is obvious, include this default option: + +```markdown +Next commands: +- `/scr:next`: Inspect the project state and choose the right next step. +``` + +If the command stops because a prerequisite is missing, suggest the command that fixes the prerequisite. Keep every explanation practical and writer-facing. + +## Tone + +Clear and practical. Treat command-surface changes like workspace organization, not like a feature upsell. diff --git a/data/CONSTRAINTS.json b/data/CONSTRAINTS.json index 3ad148e..2cc9c72 100644 --- a/data/CONSTRAINTS.json +++ b/data/CONSTRAINTS.json @@ -1,6 +1,6 @@ { "$schema": "./constraints.schema.json", - "version": "2.7.2", + "version": "2.8.0", "description": "Scriveno constraint system: work types, command availability, exports, and dependencies. Every command checks this file at runtime.", "_notes": { "sacred_keys": "Sacred subcommands live at commands/scr/sacred/.md and run as /scr:sacred:. Their CONSTRAINTS keys use the sacred: form so /scr:help can render the runnable slash-command path directly. The sacred-numbering-format entry is a separate flat command (commands/scr/sacred-numbering-format.md) that surfaces the active tradition's numbering format. It used to be named sacred-verse-numbering, which collided with sacred:verse-numbering at install time -- both flattened to scr-sacred-verse-numbering. Renamed in v1.6.x; the installer now refuses to install when two sources share a flat skill name." @@ -3015,6 +3015,23 @@ ], "description": "Diagnose and repair common project state issues" }, + "surface": { + "category": "utility", + "available": [ + "all" + ], + "description": "Inspect or change the installed Scriveno command profile" + }, + "proof-unit": { + "category": "core", + "available": [ + "all" + ], + "description": "Run one manuscript unit through a proof path from voice profile to export readiness", + "requires": [ + "profile-writer" + ] + }, "scan": { "category": "utility", "available": [ diff --git a/data/proof/first-run/README.md b/data/proof/first-run/README.md index 0c5bfbd..e96bd56 100644 --- a/data/proof/first-run/README.md +++ b/data/proof/first-run/README.md @@ -43,7 +43,7 @@ Proof artifacts: > scriveno smoke --json { "ok": true, - "expectedCommands": 113, + "expectedCommands": 115, "expectedAgents": [ "continuity-checker", "drafter", diff --git a/docs/architecture.md b/docs/architecture.md index 332ea57..66f9ae3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -439,7 +439,7 @@ Scriveno's `package.json` has no runtime dependencies. The installer is pure Nod ### Plan is canonical -The product plan is the source of truth. If a command file contradicts the plan, the command file is wrong. This ensures consistency across 113 commands and prevents drift as multiple contributors work on the system. +The product plan is the source of truth. If a command file contradicts the plan, the command file is wrong. This ensures consistency across 115 commands and prevents drift as multiple contributors work on the system. ### Backward compatibility diff --git a/docs/command-reference.md b/docs/command-reference.md index 4af6788..9acf606 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1,6 +1,6 @@ # Command Reference -Scriveno has **113 commands** organized into **14 categories**. Commands adapt automatically to your work type -- for example, `/scr:draft` talks about drafting a surah for Quranic commentary, an act for screenplays, and a section for research papers. +Scriveno has **115 commands** organized into **14 categories**. Commands adapt automatically to your work type -- for example, `/scr:draft` talks about drafting a surah for Quranic commentary, an act for screenplays, and a section for research papers. Commands marked with **adaptive terminology** change how Scriveno talks about your work type's `command_unit` in `.manuscript/config.json`, while keeping the runnable command id stable. Commands marked with **group adaptation** have different labels for specific work type groups (academic, sacred, etc.). @@ -146,6 +146,26 @@ Finalize Chapter 3 after editor review. Marks it as complete in the workflow. --- +### `/scr:proof-unit` + +**Description:** Run one manuscript unit through a proof path from voice profile to export readiness. + +**Usage:** `/scr:proof-unit [unit] [--demo] [--export-check]` + +**Prerequisites:** STYLE-GUIDE.md should exist. If it is missing, run `/scr:profile-writer` or `/scr:voice-test` first. + +**Flags:** +- `--demo` -- Use the demo proof path when available +- `--export-check` -- Run `/scr:export --check` after review to verify external tools + +**Example:** +``` +/scr:proof-unit 3 --export-check +``` +Prove one chapter through discuss, plan, draft, review, context health, and export-tool readiness without silently running a full publishing pipeline. + +--- + ### `/scr:complete-draft` **Description:** Mark the entire manuscript draft as complete. @@ -1336,7 +1356,7 @@ Generate 10-15 book club questions that spark real conversation about your theme **Description:** Publishing wizard or preset-driven pipeline. Chains export commands based on destination. -**Usage:** `/scr:publish [--preset ] [--all] [--skip-validate]` +**Usage:** `/scr:publish [--preset ] [--all] [--skip-validate] [--preflight]` **Prerequisites:** None (wraps export commands) @@ -1348,6 +1368,7 @@ Generate 10-15 book club questions that spark real conversation about your theme - **Academic / archival:** `academic-submission`, `thesis-defense`, `all-formats` - `--all` -- Run all applicable presets - `--skip-validate` -- Skip the scaffold-marker validation gate (not recommended) +- `--preflight` -- Check publishing readiness and external tools, then stop before writing deliverables - No flags -- Run the interactive wizard, which asks the writer-facing question "What are you doing?" (Share / Publish / Submit / Academic / Screenplay / Everything / Custom) and drills into the matching branch. **Available for:** All work types @@ -1364,7 +1385,7 @@ Run the full KDP paperback publishing pipeline: prepare the interior package, ge **Description:** Compile and export manuscript to publication-ready formats. -**Usage:** `/scr:export [--format ] [--formatted] [--print-ready] [--skip-validate]` +**Usage:** `/scr:export [--format ] [--formatted] [--print-ready] [--skip-validate] [--check]` **Prerequisites:** Complete draft must exist @@ -1373,6 +1394,7 @@ Run the full KDP paperback publishing pipeline: prepare the interior package, ge - `--formatted` -- Use designed/formatted template (vs. manuscript format) - `--print-ready` -- Generate the interior print PDF surface used by print-package flows - `--skip-validate` -- Skip the scaffold-marker validation gate (not recommended) +- `--check` -- Verify format availability and required external tools without assembling or writing export files **Available for:** All work types (format availability varies by work type) @@ -1932,12 +1954,13 @@ See all your writing projects with status, word count, and last activity. **Description:** Diagnose and repair common project state issues. -**Usage:** `/scr:health [--repair]` +**Usage:** `/scr:health [--repair] [--context]` **Prerequisites:** None **Flags:** - `--repair` -- Attempt to fix detected issues +- `--context` -- Estimate loaded-context size and flag watch, tight, or critical sessions **Available for:** All work types @@ -1949,6 +1972,30 @@ Check for missing files, broken references, and state inconsistencies, then fix --- +### `/scr:surface` + +**Description:** Inspect or change the installed Scriveno command profile. + +**Usage:** `/scr:surface [list|status|profile ] [--runtime ] [--dry-run]` + +**Prerequisites:** Node.js >=20.0.0 and a Scriveno package or repo checkout with `bin/install.js` + +**Flags:** +- `list` -- Show available profiles and command counts +- `status` -- Show the currently installed profile and runtime surface +- `profile ` -- Reinstall a named profile: core, writing, publishing, translation, specialist, or full +- `--dry-run` -- Show the planned install changes without writing files + +**Available for:** All work types + +**Example:** +``` +/scr:surface profile writing --dry-run +``` +Preview a smaller drafting-focused command surface before changing installed runtime files. + +--- + ### `/scr:scan` **Description:** Detect drift between recorded state (STATE.md, OUTLINE.md, config.json) and what is actually on disk. diff --git a/docs/configuration.md b/docs/configuration.md index 82a57e6..8103102 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -57,7 +57,7 @@ When a writer runs `/scr:new-work`, Scriveno creates `.manuscript/config.json`. ```json { - "scriveno_version": "2.7.2", + "scriveno_version": "2.8.0", "work_type": "", "group": "", "command_unit": "", diff --git a/docs/development.md b/docs/development.md index 46fdd75..c9cee3e 100644 --- a/docs/development.md +++ b/docs/development.md @@ -159,7 +159,7 @@ For release-oriented documentation surfaces, the main files are: - `docs/runtime-support.md` - `docs/route-graph.md` - `templates/*/README.md` when shipped profiles or templates change -- `.planning/` milestone summaries when you are still using the GSD planning layer +- `.planning/` milestone summaries when you are still using an external planning layer ## Before shipping diff --git a/docs/release-notes.md b/docs/release-notes.md index 3a9d470..168f699 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,20 @@ This document is the public-facing summary of what changed between package releases. For package history, see the root [CHANGELOG](../CHANGELOG.md). +## 2.8.0 - 2026-05-30 + +### What changed + +- Scriveno can now install focused command profiles: `core`, `writing`, `publishing`, `translation`, `specialist`, or `full`. The new `/scr:surface` command and `scriveno surface` CLI helpers let writers inspect, dry-run, and switch profiles. +- Added `/scr:proof-unit`, a one-unit proof path that checks voice context, plans, drafts, reviews, context health, and optional export-tool readiness. +- Added context-health estimation to `scriveno status`, `/scr:next`, and `/scr:health --context`, with watch, tight, and critical thresholds. +- Added `/scr:export --check` and `/scr:publish --preflight` so tool readiness and publishing prerequisites can be verified before writing deliverables. +- Updated the public command inventory to 115 commands and added regression tests for profiles, dry-run installs, context health, proof-unit, and preflight surfaces. + +### Why it matters + +Writers can keep Scriveno small when they only need the active workflow, prove the product on one real unit before trusting a larger run, and check export or publishing readiness before generating packages. + ## 2.7.2 - 2026-05-30 ### What changed @@ -76,7 +90,7 @@ This release closes the gap between what Scriveno documented and what it shipped - The public CLI now supports `scriveno first-run --project .`, giving terminal users the same proof path without relying on host-specific slash-command behavior. - First-run guidance is connected to `/scr:help`, `/scr:demo`, Quick Proof, Starter Sets, Runtime Support, Shipped Assets, Command Reference, README launch copy, and architecture docs. - Scriveno now includes committed first-run and runtime-parity proof bundles under `data/proof/`. -- Runtime smoke now validates the 113-command installed surface across Claude Code, Codex, Cursor, Gemini CLI, OpenCode, GitHub Copilot, Windsurf, Antigravity, Manus, Perplexity Desktop, and the generic fallback. +- Runtime smoke now validates the 115-command installed surface across Claude Code, Codex, Cursor, Gemini CLI, OpenCode, GitHub Copilot, Windsurf, Antigravity, Manus, Perplexity Desktop, and the generic fallback. - README badges, package metadata, constraints metadata, generated config metadata, changelog, release notes, configuration docs, route graph docs, architecture docs, proof docs, and release tests are aligned on `2.5.0`. ### Why it matters diff --git a/docs/route-graph.md b/docs/route-graph.md index 3acebb7..4bab5a0 100644 --- a/docs/route-graph.md +++ b/docs/route-graph.md @@ -13,9 +13,9 @@ The text report summarizes command count, graph edges, agent-capable routes, loc ## Current Shape -As of `2.7.2`, the route graph contains: +As of `2.8.0`, the route graph contains: -- 113 commands +- 115 commands - intent-order edges from `command_intents` - dependency-chain edges from `dependencies.core_chain` - automation lanes from `getCommandAutomationPolicy()` diff --git a/docs/sacred-texts.md b/docs/sacred-texts.md index ebc7fd0..d510455 100644 --- a/docs/sacred-texts.md +++ b/docs/sacred-texts.md @@ -292,5 +292,5 @@ Here is a quick walkthrough for starting a sacred writing project: ## See Also - [Getting Started](getting-started.md) -- Install Scriveno and create your first project -- [Command Reference](command-reference.md) -- Full reference for all 113 commands, including the [Sacred Exclusive](command-reference.md#sacred-exclusive) section +- [Command Reference](command-reference.md) -- Full reference for all 115 commands, including the [Sacred Exclusive](command-reference.md#sacred-exclusive) section - [README](../README.md) -- Project overview and feature list diff --git a/docs/voice-dna.md b/docs/voice-dna.md index fcea6d5..5cbba41 100644 --- a/docs/voice-dna.md +++ b/docs/voice-dna.md @@ -293,5 +293,5 @@ See [docs/drafter-quality.md](drafter-quality.md) for the full system, including - [Proof Artifacts](proof-artifacts.md) -- inspect the Voice DNA before/after bundle first if you want the fastest concrete evidence - [Getting Started](getting-started.md) -- Install Scriveno and write your first draft - [Drafter Quality](drafter-quality.md) -- the three rule layers, the `draft` config block, and model-tier recommendations -- [Command Reference](command-reference.md) -- Full list of all 113 commands with usage and examples +- [Command Reference](command-reference.md) -- Full list of all 115 commands with usage and examples - [Work Types Guide](work-types.md) -- How work types adapt Scriveno's vocabulary and commands diff --git a/docs/work-types.md b/docs/work-types.md index 5c613fe..5cf2e3f 100644 --- a/docs/work-types.md +++ b/docs/work-types.md @@ -335,5 +335,5 @@ Your work type is stored in `.manuscript/config.json` and can be changed later b ## See Also - [Getting Started](getting-started.md) -- Install Scriveno and write your first draft -- [Command Reference](command-reference.md) -- Full list of all 113 commands with usage and examples +- [Command Reference](command-reference.md) -- Full list of all 115 commands with usage and examples - [Voice DNA Guide](voice-dna.md) -- How Scriveno profiles and preserves your writing voice diff --git a/lib/auto-invoke-engine.js b/lib/auto-invoke-engine.js index 5a9d1e0..4c47c75 100644 --- a/lib/auto-invoke-engine.js +++ b/lib/auto-invoke-engine.js @@ -581,6 +581,72 @@ function detectContextSignal(manuscriptDir, draftFiles) { return { state: 'fresh', suggest: null }; } +const CONTEXT_HEALTH_LIMITS = { + watch: 45000, + tight: 80000, + critical: 120000, +}; + +function estimateTextTokens(byteCount) { + return Math.ceil(byteCount / 4); +} + +function fileByteSize(filePath) { + const stat = safeStat(filePath); + return stat && stat.isFile() ? stat.size : 0; +} + +function newestFiles(files, limit) { + return files + .map((file) => ({ file, mtime: safeStat(file)?.mtimeMs || 0 })) + .sort((a, b) => b.mtime - a.mtime) + .slice(0, limit) + .map((entry) => entry.file); +} + +function detectContextHealth(manuscriptDir, draftFiles) { + const coreFiles = [ + 'STYLE-GUIDE.md', + 'CONTEXT.md', + 'STATE.md', + 'OUTLINE.md', + 'RECORD.md', + 'WORK.md', + 'config.json', + ].map((name) => path.join(manuscriptDir, name)); + const reviewFiles = newestFiles(listFiles(path.join(manuscriptDir, 'reviews'), { extensions: ['.md', '.txt'], recursive: true }), 3); + const planFiles = newestFiles(listFiles(path.join(manuscriptDir, 'plans'), { extensions: ['.md'], recursive: true }), 3); + const recentDrafts = newestFiles(draftFiles, 5); + const uniqueFiles = [...new Set([...coreFiles, ...planFiles, ...recentDrafts, ...reviewFiles])].filter(pathExists); + const files = uniqueFiles.map((file) => ({ + file: path.relative(manuscriptDir, file), + bytes: fileByteSize(file), + })); + const estimatedBytes = files.reduce((sum, file) => sum + file.bytes, 0); + const estimatedTokens = estimateTextTokens(estimatedBytes); + let state = 'ok'; + let suggest = null; + if (estimatedTokens >= CONTEXT_HEALTH_LIMITS.critical) { + state = 'critical'; + suggest = '/scr:thread'; + } else if (estimatedTokens >= CONTEXT_HEALTH_LIMITS.tight) { + state = 'tight'; + suggest = '/scr:save'; + } else if (estimatedTokens >= CONTEXT_HEALTH_LIMITS.watch) { + state = 'watch'; + suggest = '/scr:health --context'; + } + return { + state, + estimatedTokens, + estimatedBytes, + fileCount: files.length, + limits: { ...CONTEXT_HEALTH_LIMITS }, + largestFiles: files.sort((a, b) => b.bytes - a.bytes).slice(0, 5), + suggest, + }; +} + function detectExportSignal(manuscriptDir, sourceFiles) { const newestSource = newestMtime(sourceFiles); const newestOutput = findNewestOutput(manuscriptDir); @@ -757,6 +823,12 @@ function buildAutomationPlan(signals, recommendation) { if (signals.context?.state === 'stale') { localCandidates.push({ command: signals.context.suggest || '/scr:scan', reason: 'refresh stale context before chaining work' }); } + if (signals.contextHealth?.state === 'tight' || signals.contextHealth?.state === 'critical') { + localCandidates.push({ + command: signals.contextHealth.suggest || '/scr:health --context', + reason: `loaded context estimate is ${signals.contextHealth.state}`, + }); + } if (signals.notes?.count > 0) { localCandidates.push({ command: '/scr:check-notes', reason: 'surface unresolved notes before the next writing route' }); } @@ -801,6 +873,7 @@ function analyzeProject(projectRoot = process.cwd(), options = {}) { hasProject: false, hasState: false, context: { state: 'none', suggest: null }, + contextHealth: { state: 'none', estimatedTokens: 0, estimatedBytes: 0, fileCount: 0, largestFiles: [], suggest: null }, history: { state: 'none', lastFailed: false }, reviews: { state: 'none', count: 0, files: [] }, reviewCoverage: { state: 'none', suggest: null }, @@ -844,6 +917,7 @@ function analyzeProject(projectRoot = process.cwd(), options = {}) { hasProject: true, hasState: pathExists(statePath), context: detectContextSignal(manuscriptDir, draftFiles), + contextHealth: detectContextHealth(manuscriptDir, draftFiles), history: historySignal, readiness: detectProjectReadiness(manuscriptDir), plan: detectPlanSignal(manuscriptDir, draftFiles), @@ -895,6 +969,7 @@ function formatProactiveChecks(analysis) { progressLine, ` Readiness: ${signals.readiness?.state || 'none'}${signals.readiness?.missing?.length ? `, missing ${signals.readiness.missing.join(', ')}` : ''}`, ` Session: ${signals.context.state}${signals.context.suggest ? `, suggest ${signals.context.suggest}` : ''}`, + ` Context health: ${signals.contextHealth?.state || 'none'}${signals.contextHealth?.estimatedTokens ? `, about ${signals.contextHealth.estimatedTokens} tokens` : ''}${signals.contextHealth?.suggest ? `, suggest ${signals.contextHealth.suggest}` : ''}`, ` Plans: ${signals.plan?.state || 'none'}${signals.plan?.suggest ? `, suggest ${signals.plan.suggest}` : ''}`, ` Reviews: ${signals.reviews.count ? `${signals.reviews.count} pending, suggest /scr:editor-review` : 'none'}`, ` Review coverage: ${signals.reviewCoverage?.state || 'none'}${signals.reviewCoverage?.suggest ? `, suggest ${signals.reviewCoverage.suggest}` : ''}`, @@ -1574,4 +1649,6 @@ module.exports = { listRuntimeAgentSupport, parseCliArgs, computeProgressLedger, + detectContextHealth, + estimateTextTokens, }; diff --git a/package-lock.json b/package-lock.json index 8017a53..8cf9dc5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "scriveno", - "version": "2.7.2", + "version": "2.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "scriveno", - "version": "2.7.2", + "version": "2.8.0", "license": "MIT", "bin": { "scriveno": "bin/install.js" diff --git a/package.json b/package.json index 4a0db75..a4488e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scriveno", - "version": "2.7.2", + "version": "2.8.0", "description": "Spec-driven creative writing, publishing, and translation pipeline for AI coding agents. From blank page to published book.", "bin": { "scriveno": "bin/install.js" diff --git a/templates/config.json b/templates/config.json index 40773e4..4bdac37 100644 --- a/templates/config.json +++ b/templates/config.json @@ -1,5 +1,5 @@ { - "scriveno_version": "2.7.2", + "scriveno_version": "2.8.0", "work_type": "", "group": "", "command_unit": "", diff --git a/test/first-run-proof-surface.test.js b/test/first-run-proof-surface.test.js index df03ef6..b34e5ab 100644 --- a/test/first-run-proof-surface.test.js +++ b/test/first-run-proof-surface.test.js @@ -71,7 +71,7 @@ describe('first-run proof surface', () => { assert.match(firstRunProof, /scriveno first-run --project \./); assert.match(firstRunProof, /scriveno smoke --json/); assert.match(firstRunProof, /\/scr:draft 5/); - assert.match(firstRunProof, /"expectedCommands": 113/); + assert.match(firstRunProof, /"expectedCommands": 115/); assert.match(runtimeParity, /Claude Code/); assert.match(runtimeParity, /Codex/); assert.match(runtimeParity, /Remaining Host-In-The-Loop Gap/); diff --git a/test/human-first-principles.test.js b/test/human-first-principles.test.js index 81f7417..e79e36c 100644 --- a/test/human-first-principles.test.js +++ b/test/human-first-principles.test.js @@ -167,15 +167,15 @@ describe('authenticity-check diagnostic discipline', () => { assert.match(drafter, /This self-check is a write-to-the-voice judgement, not a score-then-rewrite optimization loop/); }); - it('release metadata is aligned on 2.7.2', () => { + it('release metadata is aligned on 2.8.0', () => { const pkg = JSON.parse(read('package.json')); const cfg = JSON.parse(read('templates/config.json')); const constraints = JSON.parse(read('data/CONSTRAINTS.json')); - assert.equal(pkg.version, '2.7.2'); - assert.equal(cfg.scriveno_version, '2.7.2'); - assert.equal(constraints.version, '2.7.2'); - assert.match(read('CHANGELOG.md'), /## 2\.7\.2 - /); - assert.match(read('docs/release-notes.md'), /## 2\.7\.2 - /); + assert.equal(pkg.version, '2.8.0'); + assert.equal(cfg.scriveno_version, '2.8.0'); + assert.equal(constraints.version, '2.8.0'); + assert.match(read('CHANGELOG.md'), /## 2\.8\.0 - /); + assert.match(read('docs/release-notes.md'), /## 2\.8\.0 - /); }); }); diff --git a/test/phase49-surface-proof-preflight.test.js b/test/phase49-surface-proof-preflight.test.js new file mode 100644 index 0000000..27ea69a --- /dev/null +++ b/test/phase49-surface-proof-preflight.test.js @@ -0,0 +1,111 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +const ROOT = path.join(__dirname, '..'); +const install = require('../bin/install.js'); +const autoInvokeEngine = require('../lib/auto-invoke-engine.js'); + +function read(relativePath) { + return fs.readFileSync(path.join(ROOT, relativePath), 'utf8'); +} + +function mkdirp(dir) { + fs.mkdirSync(dir, { recursive: true }); +} + +describe('phase 49 command profiles and proof path', () => { + it('defines install profiles that can shrink the command surface', () => { + const core = install.surfaceProfileSummary('core'); + const full = install.surfaceProfileSummary('full'); + const coreEntries = install.collectInstallCommandEntries('core').map((entry) => entry.commandRef); + + assert.ok(core.commandCount < full.commandCount, 'core profile should be smaller than full'); + assert.ok(core.commands.includes('surface'), 'core profile should include surface management'); + assert.ok(core.commands.includes('proof-unit'), 'core profile should include the one-unit proof path'); + assert.ok(coreEntries.includes('/scr:proof-unit')); + assert.ok(!coreEntries.includes('/scr:export'), 'core profile should keep publishing commands out'); + }); + + it('parses profile and dry-run installer flags', () => { + const parsed = install.parseArgs(['--runtime', 'codex', '--profile', 'core', '--dry-run', '--json', '--project']); + const resolved = install.resolveInstallRequest(parsed, ['codex'], { isTTY: true }); + + assert.equal(parsed.installProfile, 'core'); + assert.equal(parsed.installDryRun, true); + assert.equal(parsed.installJson, true); + assert.equal(resolved.profile, 'core'); + assert.equal(resolved.dryRun, true); + assert.equal(resolved.json, true); + }); + + it('parses surface profile actions with runtime targets', () => { + const parsed = install.parseArgs(['surface', 'profile', 'writing', '--runtime', 'codex', '--project', '--dry-run']); + + assert.equal(parsed.command, 'surface'); + assert.equal(parsed.surfaceAction, 'profile'); + assert.equal(parsed.surfaceProfile, 'writing'); + assert.deepEqual(parsed.runtimeKeys, ['codex']); + assert.equal(parsed.isGlobal, false); + assert.equal(parsed.installDryRun, true); + }); + + it('builds a dry-run plan without writes', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scriveno-phase49-plan-')); + const originalCwd = process.cwd(); + try { + process.chdir(tmpDir); + const plan = install.buildInstallDryRun({ + runtimeKeys: ['codex'], + isGlobal: false, + developerMode: false, + installMode: 'non-interactive', + profile: 'core', + }); + + assert.equal(plan.writes, false); + assert.equal(plan.profile, 'core'); + assert.equal(plan.commandFiles, install.collectInstallCommandEntries('core').length); + assert.equal(plan.targets[0].commandsDir, path.join(process.cwd(), '.codex/commands/scr')); + assert.ok(!fs.existsSync(path.join(tmpDir, '.codex')), 'dry run should not create runtime directories'); + } finally { + process.chdir(originalCwd); + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('surfaces context-health pressure in project analysis', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scriveno-phase49-context-')); + try { + const manuscript = path.join(tmpDir, '.manuscript'); + mkdirp(path.join(manuscript, 'drafts', 'body')); + mkdirp(path.join(manuscript, 'plans')); + fs.writeFileSync(path.join(manuscript, 'config.json'), JSON.stringify({ work_type: 'novel', command_unit: 'chapter' })); + fs.writeFileSync(path.join(manuscript, 'STATE.md'), '# State\n'); + fs.writeFileSync(path.join(manuscript, 'OUTLINE.md'), '| 1 | Chapter |\n'); + fs.writeFileSync(path.join(manuscript, 'STYLE-GUIDE.md'), 'voice '.repeat(26000)); + fs.writeFileSync(path.join(manuscript, 'CONTEXT.md'), 'context '.repeat(26000)); + fs.writeFileSync(path.join(manuscript, 'drafts', 'body', '1-DRAFT.md'), 'draft '.repeat(26000)); + + const analysis = autoInvokeEngine.analyzeProject(tmpDir); + const report = autoInvokeEngine.formatProactiveChecks(analysis); + + assert.ok(['tight', 'critical'].includes(analysis.signals.contextHealth.state)); + assert.match(report, /Context health:/); + assert.match(report, /suggest \/scr:/); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('documents proof, surface, export check, and publish preflight commands', () => { + assert.match(read('commands/scr/proof-unit.md'), /\/scr:export --check/); + assert.match(read('commands/scr/surface.md'), /scriveno surface profile --dry-run/); + assert.match(read('commands/scr/export.md'), /If `--check` is passed/); + assert.match(read('commands/scr/publish.md'), /If `--preflight` was passed/); + assert.match(read('docs/command-reference.md'), /\/scr:proof-unit/); + assert.match(read('docs/command-reference.md'), /\/scr:surface/); + }); +});