scriveno 2.0.9 → 2.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ # Route Graph Audit
2
+
3
+ Scriveno's route graph is generated from `data/CONSTRAINTS.json` and `lib/auto-invoke-engine.js`. It is not a separate hand-maintained registry.
4
+
5
+ Run the audit with:
6
+
7
+ ```bash
8
+ scriveno routes
9
+ scriveno routes --json
10
+ ```
11
+
12
+ The text report summarizes command count, graph edges, agent-capable routes, local-helper routes, manual-gated routes, read-only routes, and lane counts. The JSON report includes nodes and edges for host adapters, CI checks, and future visualizations.
13
+
14
+ ## Current Shape
15
+
16
+ As of `2.0.11`, the route graph contains:
17
+
18
+ - 112 commands
19
+ - intent-order edges from `command_intents`
20
+ - dependency-chain edges from `dependencies.core_chain`
21
+ - automation lanes from `getCommandAutomationPolicy()`
22
+ - route priority fixtures for high-value state transitions
23
+
24
+ ## Automation Lanes
25
+
26
+ Each command receives one lane:
27
+
28
+ - `read-only`: inspect and recommend
29
+ - `local-helper`: deterministic local helper, visible in status
30
+ - `agent-ready`: bounded specialist route with known agent prompts
31
+ - `agent-or-local`: route may use a specialist or local diagnostic pass
32
+ - `mixed`: lifecycle route whose behavior depends on project state
33
+ - `manual-gated`: writer-owned action that must stay explicit
34
+
35
+ ## Priority Fixtures
36
+
37
+ The engine exports priority fixtures for the flows most likely to regress:
38
+
39
+ - empty workspace routes to `/scr:new-work`
40
+ - scanned project without drafts routes to `/scr:plan`
41
+ - planned work without drafts routes to `/scr:draft`
42
+ - drafts without review coverage route to `/scr:editor-review`
43
+ - revision proposals route to `/scr:editor-review --proposal`
44
+ - translation work routes to `/scr:back-translate`
45
+ - publishing prerequisite gaps route to the first missing packaging prerequisite
46
+
47
+ These fixtures are covered by `test/auto-invoke-engine.test.js`.
48
+
49
+ ## Cross-Runtime Use
50
+
51
+ All runtimes use the same route graph audit. Claude Code, Cursor, Gemini CLI, OpenCode, GitHub Copilot, Windsurf, and Antigravity read command files and prompt agents. Codex reads generated `$scr-*` skills plus `.toml` agent metadata. Manus and the generic fallback read bundled skill files. Perplexity Desktop receives guided local-MCP setup. The graph stays shared even when the host's native spawning behavior differs.
@@ -11,6 +11,8 @@ Node is required for:
11
11
  - running `npx scriveno@latest`
12
12
  - executing `bin/install.js`
13
13
  - running `scriveno status --project .`
14
+ - running `scriveno status --project . --apply-safe`
15
+ - running `scriveno sync --check`, `scriveno smoke`, `scriveno agents`, and `scriveno routes`
14
16
  - executing the shared auto-invoke status engine at `lib/auto-invoke-engine.js`
15
17
  - running the repo's JavaScript test suite
16
18
 
@@ -78,12 +80,30 @@ The public CLI entrypoint is:
78
80
  ```bash
79
81
  scriveno status --project .
80
82
  scriveno status . --json
83
+ scriveno status --project . --apply-safe
81
84
  ```
82
85
 
83
86
  The JSON form is intended for CI, host adapters, and future runtime smoke tests.
84
87
 
85
88
  Status output uses the same route-intelligence shape across runtimes: `Candidate agents`, `Candidate local helpers`, and `Manual gates`. That keeps Claude Code, Codex, Cursor, Gemini CLI, OpenCode, GitHub Copilot, Windsurf, Antigravity, Manus, Perplexity Desktop, and generic skill installs aligned even when their native spawning mechanisms differ.
86
89
 
90
+ ## Runtime Smoke and Agent Checks
91
+
92
+ The package now exposes cross-runtime checks through the public CLI:
93
+
94
+ ```bash
95
+ scriveno sync --check
96
+ scriveno smoke --json
97
+ scriveno agents --json
98
+ scriveno routes --json
99
+ ```
100
+
101
+ `scriveno smoke` checks installed command surfaces, Codex skill directories, bundled skill manifests, agent prompt counts, Codex `.toml` metadata, guided Perplexity setup assets, and the shared engine under `~/.scriveno/lib/auto-invoke-engine.js`.
102
+
103
+ `scriveno agents` checks whether each runtime has the expected Scriveno agent prompts and reports the correct fallback: prompt-run fallback for Claude Code and standard command runtimes, metadata-ready for Codex when `.toml` files are present, guided setup for Perplexity Desktop, and bundled skill prompts for Manus or generic skill installs.
104
+
105
+ `scriveno routes` audits the command graph and automation lanes from `data/CONSTRAINTS.json`. It is useful when adding commands because it surfaces whether a route is read-only, local-helper, agent-ready, agent-or-local, mixed, or manual-gated.
106
+
87
107
  ## What Scriveno Proves Today
88
108
 
89
109
  Scriveno currently proves all of the following in-repo:
@@ -182,7 +182,7 @@ Apply or adjust verse numbering systems to your drafted text. Supports tradition
182
182
 
183
183
  **Requires:** At least one drafted unit.
184
184
 
185
- The top-level `/scr:sacred-verse-numbering` compatibility command is read-only: it shows the active tradition's numbering format and can render example citations. The nested `/scr:sacred:verse-numbering` command changes or verifies the project's numbering system.
185
+ The top-level `/scr:sacred-numbering-format` compatibility command is read-only: it shows the active tradition's numbering format and can render example citations. The nested `/scr:sacred:verse-numbering` command changes or verifies the project's numbering system.
186
186
 
187
187
  ### `/scr:sacred:source-tracking`
188
188
 
@@ -39,6 +39,7 @@ These files ship in `templates/` and `docs/` and provide the trust trio for sess
39
39
  ## Runtime Sync Asset Shipped Today
40
40
 
41
41
  - `commands/scr/sync.md` -- local runtime-surface synchronization command. It compares and refreshes installed Scriveno commands, Codex skills, command mirrors, and agent prompts from the current source tree by delegating to `bin/install.js`.
42
+ - `lib/auto-invoke-engine.js` -- shared project status, safe apply, runtime smoke, agent availability, and route graph audit engine used by `scriveno status`, `scriveno sync --check`, `scriveno smoke`, `scriveno agents`, and `scriveno routes`.
42
43
 
43
44
  ## Export Templates Shipped Today
44
45
 
package/docs/testing.md CHANGED
@@ -136,6 +136,16 @@ For the standard release gate, prefer:
136
136
  npm run release:check
137
137
  ```
138
138
 
139
+ When changing proactive routing, runtime install paths, or agent surfaces, also run:
140
+
141
+ ```bash
142
+ scriveno status --project . --apply-safe
143
+ scriveno sync --check
144
+ scriveno smoke --json
145
+ scriveno agents --json
146
+ scriveno routes --json
147
+ ```
148
+
139
149
  Use those for release prep so you can inspect what would ship without mutating the registry.
140
150
 
141
151
  ## Practical workflow
@@ -1,4 +1,5 @@
1
1
  const fs = require('fs');
2
+ const os = require('os');
2
3
  const path = require('path');
3
4
 
4
5
  const DEFAULT_RUNTIME_SUPPORT = {
@@ -99,6 +100,119 @@ const CORE_PROJECT_FILES = [
99
100
  'config.json',
100
101
  ];
101
102
 
103
+ const DEFAULT_AGENT_NAMES = [
104
+ 'continuity-checker',
105
+ 'drafter',
106
+ 'plan-checker',
107
+ 'researcher',
108
+ 'translator',
109
+ 'voice-checker',
110
+ ];
111
+
112
+ const ROUTE_PRIORITY_FIXTURES = [
113
+ {
114
+ name: 'empty workspace',
115
+ setup: 'no .manuscript directory',
116
+ expectedCommand: '/scr:new-work',
117
+ reason: 'start or import before lifecycle routing',
118
+ },
119
+ {
120
+ name: 'scanned project without drafts',
121
+ setup: 'STATE.md and CONTEXT.md exist, drafts are absent',
122
+ expectedCommand: '/scr:plan',
123
+ reason: 'planning comes before drafting when no plan is ready',
124
+ },
125
+ {
126
+ name: 'planned work without draft',
127
+ setup: 'plan files exist and drafts are absent',
128
+ expectedCommand: '/scr:draft',
129
+ reason: 'connected plan evidence should route to drafting',
130
+ },
131
+ {
132
+ name: 'draft without review coverage',
133
+ setup: 'draft files exist and reviews are absent',
134
+ expectedCommand: '/scr:editor-review',
135
+ reason: 'review should precede export and packaging',
136
+ },
137
+ {
138
+ name: 'revision proposal waiting',
139
+ setup: 'proposal files exist in .manuscript/proposals',
140
+ expectedCommand: '/scr:editor-review --proposal',
141
+ reason: 'proposal review is more urgent than general notes',
142
+ },
143
+ {
144
+ name: 'translation follow-up',
145
+ setup: 'translation folders or target languages exist after review coverage',
146
+ expectedCommand: '/scr:back-translate',
147
+ reason: 'translation needs verification before multi-publish',
148
+ },
149
+ {
150
+ name: 'publishing prerequisite gap',
151
+ setup: 'reviewed drafts exist without front matter, back matter, blurb, or cover handoff',
152
+ expectedCommand: '/scr:front-matter',
153
+ reason: 'specific packaging prerequisites come before final publish',
154
+ },
155
+ ];
156
+
157
+ const RUNTIME_INSTALL_SURFACES = {
158
+ 'claude-code': {
159
+ commands: (homeDir) => path.join(homeDir, '.claude', 'commands'),
160
+ agents: (homeDir) => path.join(homeDir, '.claude', 'agents'),
161
+ commandLayout: 'flat',
162
+ },
163
+ cursor: {
164
+ commands: (homeDir) => path.join(homeDir, '.cursor', 'commands', 'scr'),
165
+ agents: (homeDir) => path.join(homeDir, '.cursor', 'agents'),
166
+ commandLayout: 'nested',
167
+ },
168
+ 'gemini-cli': {
169
+ commands: (homeDir) => path.join(homeDir, '.gemini', 'commands', 'scr'),
170
+ agents: (homeDir) => path.join(homeDir, '.gemini', 'agents'),
171
+ commandLayout: 'nested',
172
+ },
173
+ codex: {
174
+ commands: (homeDir) => path.join(homeDir, '.codex', 'commands', 'scr'),
175
+ skills: (homeDir) => path.join(homeDir, '.codex', 'skills'),
176
+ agents: (homeDir) => path.join(homeDir, '.codex', 'agents'),
177
+ commandLayout: 'nested',
178
+ metadata: 'toml',
179
+ },
180
+ opencode: {
181
+ commands: (homeDir) => path.join(homeDir, '.config', 'opencode', 'commands', 'scr'),
182
+ agents: (homeDir) => path.join(homeDir, '.config', 'opencode', 'agents'),
183
+ commandLayout: 'nested',
184
+ },
185
+ copilot: {
186
+ commands: (homeDir) => path.join(homeDir, '.github', 'commands', 'scr'),
187
+ agents: (homeDir) => path.join(homeDir, '.github', 'agents'),
188
+ commandLayout: 'nested',
189
+ },
190
+ windsurf: {
191
+ commands: (homeDir) => path.join(homeDir, '.windsurf', 'commands', 'scr'),
192
+ agents: (homeDir) => path.join(homeDir, '.windsurf', 'agents'),
193
+ commandLayout: 'nested',
194
+ },
195
+ antigravity: {
196
+ commands: (homeDir) => path.join(homeDir, '.gemini', 'antigravity', 'commands', 'scr'),
197
+ agents: (homeDir) => path.join(homeDir, '.gemini', 'antigravity', 'agents'),
198
+ commandLayout: 'nested',
199
+ },
200
+ manus: {
201
+ skills: (homeDir) => path.join(homeDir, '.manus', 'skills', 'scriveno'),
202
+ agents: (homeDir) => path.join(homeDir, '.manus', 'skills', 'scriveno', 'agents'),
203
+ commandLayout: 'skill-bundle',
204
+ },
205
+ 'perplexity-desktop': {
206
+ guide: (homeDir) => path.join(homeDir, '.scriveno', 'perplexity'),
207
+ commandLayout: 'guided-mcp',
208
+ },
209
+ generic: {
210
+ skills: (homeDir) => path.join(homeDir, '.scriveno', 'skills'),
211
+ agents: (homeDir) => path.join(homeDir, '.scriveno', 'skills', 'agents'),
212
+ commandLayout: 'skill-bundle',
213
+ },
214
+ };
215
+
102
216
  const AGENT_ROUTE_POLICIES = {
103
217
  '/scr:plan': {
104
218
  agents: ['plan-checker'],
@@ -855,6 +969,381 @@ function listRuntimeAgentSupport() {
855
969
  }));
856
970
  }
857
971
 
972
+ function getPackageRoot() {
973
+ return path.resolve(__dirname, '..');
974
+ }
975
+
976
+ function loadConstraints(options = {}) {
977
+ const constraintsPath = options.constraintsPath || path.join(getPackageRoot(), 'data', 'CONSTRAINTS.json');
978
+ const constraints = readJson(constraintsPath);
979
+ return constraints && constraints.commands ? constraints : { commands: {}, command_intents: {}, dependencies: {} };
980
+ }
981
+
982
+ function expectedCommandCount(options = {}) {
983
+ return Object.keys(loadConstraints(options).commands || {}).length;
984
+ }
985
+
986
+ function getExpectedAgentNames(options = {}) {
987
+ if (Array.isArray(options.agentNames) && options.agentNames.length > 0) {
988
+ return options.agentNames.slice().sort();
989
+ }
990
+ const agentsRoot = options.agentsRoot || path.join(getPackageRoot(), 'agents');
991
+ const files = listFiles(agentsRoot, { extensions: ['.md'], recursive: false })
992
+ .map((file) => path.basename(file, '.md'))
993
+ .sort();
994
+ return files.length ? files : DEFAULT_AGENT_NAMES.slice().sort();
995
+ }
996
+
997
+ function collectSafeApplyActions(projectRoot = process.cwd(), options = {}) {
998
+ const analysis = options.analysis || analyzeProject(projectRoot);
999
+ const actions = [
1000
+ {
1001
+ name: 'status sweep',
1002
+ command: 'scriveno status',
1003
+ status: 'ran',
1004
+ mutation: false,
1005
+ reason: 'computed the current route, local-helper, agent, and manual-gate state',
1006
+ },
1007
+ ];
1008
+
1009
+ const readOnlyHelpers = new Set(['/scr:progress', '/scr:session-report', '/scr:check-notes', '/scr:health', '/scr:validate']);
1010
+ const writeOrInstallHelpers = new Set(['/scr:save', '/scr:scan', '/scr:sync']);
1011
+
1012
+ for (const candidate of analysis.automation.localCandidates || []) {
1013
+ const command = candidate.command;
1014
+ if (readOnlyHelpers.has(command)) {
1015
+ actions.push({
1016
+ name: command.replace('/scr:', ''),
1017
+ command,
1018
+ status: 'ready',
1019
+ mutation: false,
1020
+ reason: candidate.reason,
1021
+ });
1022
+ } else if (writeOrInstallHelpers.has(command)) {
1023
+ actions.push({
1024
+ name: command.replace('/scr:', ''),
1025
+ command,
1026
+ status: 'skipped',
1027
+ mutation: true,
1028
+ reason: `${candidate.reason}; safe apply reports this instead of writing files`,
1029
+ });
1030
+ } else {
1031
+ actions.push({
1032
+ name: command.replace('/scr:', ''),
1033
+ command,
1034
+ status: 'suggested',
1035
+ mutation: null,
1036
+ reason: candidate.reason,
1037
+ });
1038
+ }
1039
+ }
1040
+
1041
+ for (const candidate of analysis.automation.spawnCandidates || []) {
1042
+ actions.push({
1043
+ name: candidate.command.replace('/scr:', ''),
1044
+ command: candidate.command,
1045
+ status: 'agent-candidate',
1046
+ mutation: null,
1047
+ reason: `${candidate.agents.join(', ')}: ${candidate.reason}`,
1048
+ });
1049
+ }
1050
+
1051
+ for (const gate of analysis.automation.manualGates || []) {
1052
+ actions.push({
1053
+ name: gate.command.replace('/scr:', ''),
1054
+ command: gate.command,
1055
+ status: 'manual-gate',
1056
+ mutation: true,
1057
+ reason: gate.reason,
1058
+ });
1059
+ }
1060
+
1061
+ return {
1062
+ projectRoot: analysis.projectRoot,
1063
+ trigger: options.trigger || 'scriveno status --apply-safe',
1064
+ appliedCount: actions.filter((action) => action.status === 'ran').length,
1065
+ skippedCount: actions.filter((action) => action.status === 'skipped' || action.status === 'manual-gate').length,
1066
+ safeToRunCount: actions.filter((action) => action.status === 'ready').length,
1067
+ agentCandidateCount: actions.filter((action) => action.status === 'agent-candidate').length,
1068
+ actions,
1069
+ };
1070
+ }
1071
+
1072
+ function formatSafeApplyReport(result) {
1073
+ const actionLines = result.actions.length
1074
+ ? result.actions.map((action) => {
1075
+ const mutation = action.mutation === false ? 'read-only' : action.mutation === true ? 'writes or external action' : 'host-dependent';
1076
+ return `- ${action.command}: ${action.status} (${mutation}) - ${action.reason}`;
1077
+ })
1078
+ : ['- none'];
1079
+ return [
1080
+ 'Safe apply status:',
1081
+ `Trigger: ${result.trigger}`,
1082
+ `Project: ${result.projectRoot}`,
1083
+ `Read-only checks run: ${result.appliedCount}`,
1084
+ `Safe helpers ready: ${result.safeToRunCount}`,
1085
+ `Agent candidates: ${result.agentCandidateCount}`,
1086
+ `Manual or write-gated actions: ${result.skippedCount}`,
1087
+ 'Actions:',
1088
+ ...actionLines,
1089
+ ].join('\n');
1090
+ }
1091
+
1092
+ function runtimeSurfacePaths(runtimeKey, options = {}) {
1093
+ const homeDir = options.homeDir || os.homedir();
1094
+ const surface = RUNTIME_INSTALL_SURFACES[runtimeKey];
1095
+ if (!surface) return null;
1096
+ const out = { runtimeKey };
1097
+ for (const [key, value] of Object.entries(surface)) {
1098
+ if (typeof value === 'function') out[key] = value(homeDir);
1099
+ }
1100
+ out.commandLayout = surface.commandLayout;
1101
+ out.metadata = surface.metadata || 'none';
1102
+ return out;
1103
+ }
1104
+
1105
+ function inspectAgentAvailability(options = {}) {
1106
+ const runtimeKeys = options.runtimeKeys || Object.keys(DEFAULT_RUNTIME_SUPPORT);
1107
+ const agentNames = getExpectedAgentNames(options);
1108
+ const runtimes = [];
1109
+
1110
+ for (const runtimeKey of runtimeKeys) {
1111
+ const support = getRuntimeAgentSupport(runtimeKey);
1112
+ const paths = runtimeSurfacePaths(runtimeKey, options);
1113
+ if (!support || !paths) continue;
1114
+
1115
+ if (runtimeKey === 'perplexity-desktop') {
1116
+ const guideReady = pathExists(path.join(paths.guide || '', 'SETUP.md'));
1117
+ runtimes.push({
1118
+ runtime: runtimeKey,
1119
+ label: support.label,
1120
+ status: guideReady ? 'guided-ready' : 'guided-missing',
1121
+ nativeSpawn: support.nativeSpawn,
1122
+ fallback: support.fallback,
1123
+ agentsDir: null,
1124
+ promptCount: 0,
1125
+ missingPrompts: agentNames,
1126
+ metadataCount: 0,
1127
+ missingMetadata: [],
1128
+ });
1129
+ continue;
1130
+ }
1131
+
1132
+ const agentsDir = paths.agents || path.join(paths.skills || '', 'agents');
1133
+ const promptFiles = agentNames.map((name) => `${name}.md`);
1134
+ const missingPrompts = promptFiles
1135
+ .filter((fileName) => !pathExists(path.join(agentsDir, fileName)))
1136
+ .map((fileName) => path.basename(fileName, '.md'));
1137
+ const metadataFiles = support.metadata === 'toml'
1138
+ ? agentNames.map((name) => `${name}.toml`)
1139
+ : [];
1140
+ const missingMetadata = metadataFiles
1141
+ .filter((fileName) => !pathExists(path.join(agentsDir, fileName)))
1142
+ .map((fileName) => path.basename(fileName, '.toml'));
1143
+ const promptCount = promptFiles.length - missingPrompts.length;
1144
+ const metadataCount = metadataFiles.length - missingMetadata.length;
1145
+ let status = 'missing';
1146
+ if (missingPrompts.length === 0 && missingMetadata.length === 0 && support.metadata === 'toml') {
1147
+ status = 'metadata-ready';
1148
+ } else if (missingPrompts.length === 0) {
1149
+ status = 'prompt-fallback-ready';
1150
+ }
1151
+
1152
+ runtimes.push({
1153
+ runtime: runtimeKey,
1154
+ label: support.label,
1155
+ status,
1156
+ nativeSpawn: support.nativeSpawn,
1157
+ fallback: support.fallback,
1158
+ agentsDir,
1159
+ promptCount,
1160
+ missingPrompts,
1161
+ metadataCount,
1162
+ missingMetadata,
1163
+ });
1164
+ }
1165
+
1166
+ return {
1167
+ checkedAt: new Date().toISOString(),
1168
+ expectedAgents: agentNames,
1169
+ runtimes,
1170
+ };
1171
+ }
1172
+
1173
+ function formatAgentAvailabilityReport(report) {
1174
+ const lines = [
1175
+ 'Agent availability:',
1176
+ `Expected agents: ${report.expectedAgents.join(', ')}`,
1177
+ ];
1178
+ for (const runtime of report.runtimes) {
1179
+ lines.push(`- ${runtime.label}: ${runtime.status}`);
1180
+ if (runtime.agentsDir) lines.push(` Agents: ${runtime.agentsDir}`);
1181
+ lines.push(` Prompts: ${runtime.promptCount}/${report.expectedAgents.length}`);
1182
+ if (runtime.missingPrompts.length) lines.push(` Missing prompts: ${runtime.missingPrompts.join(', ')}`);
1183
+ if (runtime.missingMetadata.length) lines.push(` Missing metadata: ${runtime.missingMetadata.join(', ')}`);
1184
+ lines.push(` Fallback: ${runtime.fallback}`);
1185
+ }
1186
+ return lines.join('\n');
1187
+ }
1188
+
1189
+ function countInstalledCommands(paths) {
1190
+ if (!paths) return 0;
1191
+ if (paths.commandLayout === 'flat') {
1192
+ return listFiles(paths.commands, { extensions: ['.md'], recursive: false })
1193
+ .filter((file) => /^scr-/.test(path.basename(file)))
1194
+ .length;
1195
+ }
1196
+ if (paths.commandLayout === 'skill-bundle') {
1197
+ return countMarkdownFiles(path.join(paths.skills || '', 'commands', 'scr'));
1198
+ }
1199
+ if (paths.commandLayout === 'guided-mcp') {
1200
+ return pathExists(path.join(paths.guide || '', 'SETUP.md')) ? 1 : 0;
1201
+ }
1202
+ return countMarkdownFiles(paths.commands || '');
1203
+ }
1204
+
1205
+ function inspectRuntimeSmoke(options = {}) {
1206
+ const runtimeKeys = options.runtimeKeys || Object.keys(DEFAULT_RUNTIME_SUPPORT);
1207
+ const expectedCommands = options.expectedCommands || expectedCommandCount(options);
1208
+ const expectedAgents = getExpectedAgentNames(options);
1209
+ const dataDir = options.dataDir || path.join(options.homeDir || os.homedir(), '.scriveno');
1210
+ const enginePath = path.join(dataDir, 'lib', 'auto-invoke-engine.js');
1211
+ const results = [];
1212
+
1213
+ for (const runtimeKey of runtimeKeys) {
1214
+ const support = getRuntimeAgentSupport(runtimeKey);
1215
+ const paths = runtimeSurfacePaths(runtimeKey, options);
1216
+ if (!support || !paths) continue;
1217
+ const commandCount = countInstalledCommands(paths);
1218
+ const skillCount = runtimeKey === 'codex' && pathExists(paths.skills || '')
1219
+ ? fs.readdirSync(paths.skills, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith('scr-')).length
1220
+ : paths.skills && pathExists(path.join(paths.skills, 'SKILL.md')) ? 1 : 0;
1221
+ const agentsDir = paths.agents || (paths.skills ? path.join(paths.skills, 'agents') : null);
1222
+ const promptCount = countFiles(agentsDir, ['.md']);
1223
+ const metadataCount = runtimeKey === 'codex' ? countFiles(agentsDir, ['.toml']) : 0;
1224
+ const commandReady = runtimeKey === 'perplexity-desktop' ? commandCount === 1 : commandCount >= expectedCommands;
1225
+ const skillReady = runtimeKey === 'codex' ? skillCount >= expectedCommands : !paths.skills || skillCount >= 1;
1226
+ const agentReady = runtimeKey === 'perplexity-desktop' ? true : promptCount >= expectedAgents.length;
1227
+ const metadataReady = runtimeKey === 'codex' ? metadataCount >= expectedAgents.length : true;
1228
+ const engineReady = pathExists(enginePath);
1229
+ const ok = commandReady && skillReady && agentReady && metadataReady && engineReady;
1230
+
1231
+ results.push({
1232
+ runtime: runtimeKey,
1233
+ label: support.label,
1234
+ ok,
1235
+ commands: commandCount,
1236
+ expectedCommands: runtimeKey === 'perplexity-desktop' ? 1 : expectedCommands,
1237
+ skills: skillCount,
1238
+ agents: promptCount,
1239
+ expectedAgents: runtimeKey === 'perplexity-desktop' ? 0 : expectedAgents.length,
1240
+ metadata: metadataCount,
1241
+ expectedMetadata: runtimeKey === 'codex' ? expectedAgents.length : 0,
1242
+ engineReady,
1243
+ paths,
1244
+ });
1245
+ }
1246
+
1247
+ return {
1248
+ checkedAt: new Date().toISOString(),
1249
+ dataDir,
1250
+ enginePath,
1251
+ expectedCommands,
1252
+ expectedAgents,
1253
+ ok: results.every((result) => result.ok),
1254
+ runtimes: results,
1255
+ };
1256
+ }
1257
+
1258
+ function formatRuntimeSmokeReport(report) {
1259
+ const lines = [
1260
+ 'Runtime smoke status:',
1261
+ `Shared engine: ${report.enginePath} (${report.runtimes.some((runtime) => runtime.engineReady) ? 'present' : 'missing'})`,
1262
+ `Overall: ${report.ok ? 'pass' : 'needs attention'}`,
1263
+ ];
1264
+ for (const runtime of report.runtimes) {
1265
+ lines.push(`- ${runtime.label}: ${runtime.ok ? 'pass' : 'needs attention'}`);
1266
+ lines.push(` Commands: ${runtime.commands}/${runtime.expectedCommands}`);
1267
+ if (runtime.skills) lines.push(` Skills: ${runtime.skills}`);
1268
+ if (runtime.expectedAgents) lines.push(` Agent prompts: ${runtime.agents}/${runtime.expectedAgents}`);
1269
+ if (runtime.expectedMetadata) lines.push(` Agent metadata: ${runtime.metadata}/${runtime.expectedMetadata}`);
1270
+ lines.push(` Shared engine: ${runtime.engineReady ? 'present' : 'missing'}`);
1271
+ }
1272
+ return lines.join('\n');
1273
+ }
1274
+
1275
+ function buildRouteGraph(options = {}) {
1276
+ const constraints = options.commands ? options : loadConstraints(options);
1277
+ const commands = constraints.commands || {};
1278
+ const categories = {};
1279
+ const lanes = {};
1280
+ const nodes = Object.entries(commands).map(([name, command]) => {
1281
+ const policy = getCommandAutomationPolicy(name, command);
1282
+ categories[command.category || 'uncategorized'] = (categories[command.category || 'uncategorized'] || 0) + 1;
1283
+ lanes[policy.lane] = (lanes[policy.lane] || 0) + 1;
1284
+ return {
1285
+ id: `/scr:${name}`,
1286
+ name,
1287
+ category: command.category || 'uncategorized',
1288
+ lane: policy.lane,
1289
+ level: policy.level,
1290
+ available: command.available || [],
1291
+ reason: policy.reason,
1292
+ };
1293
+ });
1294
+
1295
+ const edges = [];
1296
+ for (const [intent, names] of Object.entries(constraints.command_intents || {})) {
1297
+ for (let i = 0; i < names.length - 1; i++) {
1298
+ if (commands[names[i]] && commands[names[i + 1]]) {
1299
+ edges.push({ from: `/scr:${names[i]}`, to: `/scr:${names[i + 1]}`, type: 'intent-order', label: intent });
1300
+ }
1301
+ }
1302
+ }
1303
+ for (const [chainName, entries] of Object.entries(constraints.dependencies || {})) {
1304
+ if (!Array.isArray(entries)) continue;
1305
+ const commandEntries = entries.filter((entry) => entry && typeof entry === 'object' && entry.command);
1306
+ for (let i = 0; i < commandEntries.length - 1; i++) {
1307
+ const from = commandEntries[i].command;
1308
+ const to = commandEntries[i + 1].command;
1309
+ if (commands[from] && commands[to]) {
1310
+ edges.push({ from: `/scr:${from}`, to: `/scr:${to}`, type: 'dependency-chain', label: chainName });
1311
+ }
1312
+ }
1313
+ }
1314
+
1315
+ return {
1316
+ generatedAt: new Date().toISOString(),
1317
+ commandCount: nodes.length,
1318
+ edgeCount: edges.length,
1319
+ categories,
1320
+ lanes,
1321
+ agentRoutes: nodes.filter((node) => node.lane === 'agent-ready' || node.lane === 'agent-or-local').length,
1322
+ localRoutes: nodes.filter((node) => node.lane === 'local-helper').length,
1323
+ manualRoutes: nodes.filter((node) => node.lane === 'manual-gated').length,
1324
+ readOnlyRoutes: nodes.filter((node) => node.lane === 'read-only').length,
1325
+ nodes,
1326
+ edges,
1327
+ };
1328
+ }
1329
+
1330
+ function formatRouteGraphReport(graph) {
1331
+ const laneLines = Object.entries(graph.lanes)
1332
+ .sort(([a], [b]) => a.localeCompare(b))
1333
+ .map(([lane, count]) => `- ${lane}: ${count}`);
1334
+ return [
1335
+ 'Route graph audit:',
1336
+ `Commands: ${graph.commandCount}`,
1337
+ `Edges: ${graph.edgeCount}`,
1338
+ `Agent-capable routes: ${graph.agentRoutes}`,
1339
+ `Local-helper routes: ${graph.localRoutes}`,
1340
+ `Manual-gated routes: ${graph.manualRoutes}`,
1341
+ `Read-only routes: ${graph.readOnlyRoutes}`,
1342
+ 'Automation lanes:',
1343
+ ...laneLines,
1344
+ ].join('\n');
1345
+ }
1346
+
858
1347
  function parseCliArgs(argv) {
859
1348
  const out = {
860
1349
  projectRoot: process.cwd(),
@@ -892,15 +1381,27 @@ module.exports = {
892
1381
  AGENT_ROUTE_POLICIES,
893
1382
  CATEGORY_ROUTE_POLICIES,
894
1383
  DEFAULT_RUNTIME_SUPPORT,
1384
+ DEFAULT_AGENT_NAMES,
895
1385
  LOCAL_ROUTE_POLICIES,
896
1386
  MANUAL_ROUTE_POLICIES,
1387
+ ROUTE_PRIORITY_FIXTURES,
897
1388
  analyzeProject,
1389
+ buildRouteGraph,
1390
+ collectSafeApplyActions,
1391
+ expectedCommandCount,
1392
+ formatAgentAvailabilityReport,
1393
+ formatRouteGraphReport,
898
1394
  formatProactiveChecks,
899
1395
  formatAutomationStatus,
900
1396
  formatRecommendation,
901
1397
  formatReport,
1398
+ formatRuntimeSmokeReport,
1399
+ formatSafeApplyReport,
902
1400
  getCommandAutomationPolicy,
1401
+ getExpectedAgentNames,
903
1402
  getRuntimeAgentSupport,
1403
+ inspectAgentAvailability,
1404
+ inspectRuntimeSmoke,
904
1405
  listRuntimeAgentSupport,
905
1406
  parseCliArgs,
906
1407
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scriveno",
3
- "version": "2.0.9",
3
+ "version": "2.0.11",
4
4
  "description": "Spec-driven creative writing, publishing, and translation pipeline for AI coding agents. From blank page to published book.",
5
5
  "bin": {
6
6
  "scriveno": "bin/install.js"
@@ -1,5 +1,5 @@
1
1
  {
2
- "scriveno_version": "2.0.9",
2
+ "scriveno_version": "2.0.11",
3
3
  "work_type": "",
4
4
  "group": "",
5
5
  "command_unit": "",
@@ -42,4 +42,4 @@
42
42
 
43
43
  ---
44
44
 
45
- *Drafter agents check this file to avoid contradicting established doctrines when drafting passages. `/scr:doctrinal-check` flags any unit that contradicts or undermines an affirmed doctrine without explicit intent.*
45
+ *Drafter agents check this file to avoid contradicting established doctrines when drafting passages. `/scr:sacred:doctrinal-check` flags any unit that contradicts or undermines an affirmed doctrine without explicit intent.*