scriveno 2.0.8 → 2.0.10
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.
- package/README.md +21 -7
- package/bin/install.js +137 -3
- package/commands/scr/health.md +6 -0
- package/commands/scr/new-work.md +1 -1
- package/commands/scr/next.md +6 -0
- package/commands/scr/progress.md +6 -0
- package/commands/scr/save.md +6 -0
- package/commands/scr/scan.md +6 -0
- package/commands/scr/session-report.md +6 -0
- package/commands/scr/sync.md +4 -2
- package/data/CONSTRAINTS.json +3082 -701
- package/docs/architecture.md +23 -5
- package/docs/auto-invoke-policy.md +30 -0
- package/docs/configuration.md +1 -1
- package/docs/getting-started.md +5 -1
- package/docs/release-notes.md +67 -0
- package/docs/route-graph.md +51 -0
- package/docs/runtime-support.md +23 -1
- package/docs/shipped-assets.md +1 -0
- package/docs/testing.md +10 -0
- package/lib/auto-invoke-engine.js +889 -2
- package/package.json +1 -1
- package/templates/config.json +1 -10
|
@@ -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 = {
|
|
@@ -91,6 +92,229 @@ const REVIEW_KEYWORDS = [
|
|
|
91
92
|
'CONTINUITY',
|
|
92
93
|
];
|
|
93
94
|
|
|
95
|
+
const CORE_PROJECT_FILES = [
|
|
96
|
+
'WORK.md',
|
|
97
|
+
'OUTLINE.md',
|
|
98
|
+
'STYLE-GUIDE.md',
|
|
99
|
+
'RECORD.md',
|
|
100
|
+
'config.json',
|
|
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
|
+
|
|
216
|
+
const AGENT_ROUTE_POLICIES = {
|
|
217
|
+
'/scr:plan': {
|
|
218
|
+
agents: ['plan-checker'],
|
|
219
|
+
reason: 'planning can validate unit plans before drafting',
|
|
220
|
+
},
|
|
221
|
+
'/scr:draft': {
|
|
222
|
+
agents: ['drafter', 'voice-checker'],
|
|
223
|
+
reason: 'drafting uses fresh-context prose generation and voice checks',
|
|
224
|
+
},
|
|
225
|
+
'/scr:editor-review': {
|
|
226
|
+
agents: ['diagnostic worker'],
|
|
227
|
+
reason: 'editor review can isolate flagged issue groups',
|
|
228
|
+
},
|
|
229
|
+
'/scr:voice-check': {
|
|
230
|
+
agents: ['voice-checker'],
|
|
231
|
+
reason: 'voice review compares drafts against STYLE-GUIDE.md',
|
|
232
|
+
},
|
|
233
|
+
'/scr:continuity-check': {
|
|
234
|
+
agents: ['continuity-checker'],
|
|
235
|
+
reason: 'continuity review checks contradictions and timeline drift',
|
|
236
|
+
},
|
|
237
|
+
'/scr:translate': {
|
|
238
|
+
agents: ['translator'],
|
|
239
|
+
reason: 'translation runs one fresh-context translation pass per unit',
|
|
240
|
+
},
|
|
241
|
+
'/scr:back-translate': {
|
|
242
|
+
agents: ['translator'],
|
|
243
|
+
reason: 'back-translation verifies target-language drift',
|
|
244
|
+
},
|
|
245
|
+
'/scr:beta-reader': {
|
|
246
|
+
agents: ['beta-reader worker'],
|
|
247
|
+
reason: 'beta review benefits from isolated reader perspectives',
|
|
248
|
+
},
|
|
249
|
+
'/scr:quick-write': {
|
|
250
|
+
agents: ['drafter', 'voice-checker'],
|
|
251
|
+
reason: 'quick writing still benefits from voice-aware isolation',
|
|
252
|
+
},
|
|
253
|
+
'/scr:map-manuscript': {
|
|
254
|
+
agents: ['voice analyst', 'structure analyst', 'character analyst', 'theme analyst', 'world analyst', 'pacing analyst'],
|
|
255
|
+
reason: 'manuscript import uses parallel analysis workers when available',
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const LOCAL_ROUTE_POLICIES = {
|
|
260
|
+
'/scr:save': 'refresh CONTEXT.md, HISTORY.log, and project checkpoint state',
|
|
261
|
+
'/scr:scan': 'reconcile STATE.md and disk evidence',
|
|
262
|
+
'/scr:health': 'diagnose project and runtime health',
|
|
263
|
+
'/scr:sync': 'compare and refresh installed runtime surfaces',
|
|
264
|
+
'/scr:validate': 'run project validation checks',
|
|
265
|
+
'/scr:check-notes': 'surface unresolved writer notes',
|
|
266
|
+
'/scr:progress': 'compute read-only project progress',
|
|
267
|
+
'/scr:session-report': 'compute read-only session metrics',
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const MANUAL_ROUTE_POLICIES = {
|
|
271
|
+
'/scr:publish': 'publication packaging can overwrite deliverables and needs writer choices',
|
|
272
|
+
'/scr:export': 'export writes output artifacts and may overwrite packages',
|
|
273
|
+
'/scr:track merge': 'merging revision tracks is a writer-owned decision',
|
|
274
|
+
'/scr:undo': 'undo changes state and should stay explicit',
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const CATEGORY_ROUTE_POLICIES = {
|
|
278
|
+
core: { lane: 'mixed', level: 3, reason: 'core lifecycle routes may read, write, or spawn depending on the current stage' },
|
|
279
|
+
navigation: { lane: 'read-only', level: 1, reason: 'navigation routes should inspect and recommend by default' },
|
|
280
|
+
quality: { lane: 'agent-or-local', level: 3, reason: 'quality routes may run bounded diagnostics or text transforms' },
|
|
281
|
+
character_world: { lane: 'local-helper', level: 2, reason: 'character and world routes update project knowledge files' },
|
|
282
|
+
structure: { lane: 'local-helper', level: 2, reason: 'structure routes update maps, outlines, and state evidence' },
|
|
283
|
+
structure_management: { lane: 'manual-gated', level: 4, reason: 'structure management can rename, remove, or reorder manuscript units' },
|
|
284
|
+
review: { lane: 'agent-or-local', level: 3, reason: 'review routes may invoke bounded diagnostic workers' },
|
|
285
|
+
illustration: { lane: 'local-helper', level: 2, reason: 'illustration routes generate prompts and asset briefs' },
|
|
286
|
+
publishing: { lane: 'manual-gated', level: 4, reason: 'publishing routes write deliverables and package outputs' },
|
|
287
|
+
translation: { lane: 'agent-or-local', level: 3, reason: 'translation routes use translator agents or verification helpers' },
|
|
288
|
+
sacred_exclusive: { lane: 'agent-or-local', level: 3, reason: 'sacred routes perform specialized consistency and reference work' },
|
|
289
|
+
utility: { lane: 'local-helper', level: 2, reason: 'utility routes perform deterministic diagnostics or project updates' },
|
|
290
|
+
session: { lane: 'local-helper', level: 2, reason: 'session routes save, compare, resume, or report project state' },
|
|
291
|
+
collaboration: { lane: 'manual-gated', level: 4, reason: 'collaboration routes change revision tracks and require writer control' },
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
function normalizeCommandRef(commandName) {
|
|
295
|
+
if (commandName.startsWith('/scr:')) return commandName;
|
|
296
|
+
return `/scr:${commandName}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function getCommandAutomationPolicy(commandName, command = {}) {
|
|
300
|
+
const ref = normalizeCommandRef(commandName);
|
|
301
|
+
if (AGENT_ROUTE_POLICIES[ref]) {
|
|
302
|
+
return { ref, lane: 'agent-ready', level: 3, reason: AGENT_ROUTE_POLICIES[ref].reason };
|
|
303
|
+
}
|
|
304
|
+
if (LOCAL_ROUTE_POLICIES[ref]) {
|
|
305
|
+
return { ref, lane: 'local-helper', level: 2, reason: LOCAL_ROUTE_POLICIES[ref] };
|
|
306
|
+
}
|
|
307
|
+
if (MANUAL_ROUTE_POLICIES[ref]) {
|
|
308
|
+
return { ref, lane: 'manual-gated', level: 4, reason: MANUAL_ROUTE_POLICIES[ref] };
|
|
309
|
+
}
|
|
310
|
+
const categoryPolicy = CATEGORY_ROUTE_POLICIES[command.category] || {
|
|
311
|
+
lane: 'read-only',
|
|
312
|
+
level: 1,
|
|
313
|
+
reason: 'unclassified routes should only suggest until a category policy is added',
|
|
314
|
+
};
|
|
315
|
+
return { ref, ...categoryPolicy };
|
|
316
|
+
}
|
|
317
|
+
|
|
94
318
|
function pathExists(filePath) {
|
|
95
319
|
try {
|
|
96
320
|
fs.accessSync(filePath);
|
|
@@ -156,11 +380,124 @@ function countMarkdownFiles(dir) {
|
|
|
156
380
|
return listFiles(dir, { extensions: ['.md'], recursive: true }).length;
|
|
157
381
|
}
|
|
158
382
|
|
|
383
|
+
function countFiles(dir, extensions = null) {
|
|
384
|
+
return listFiles(dir, { extensions, recursive: true }).length;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function anyPathExists(paths) {
|
|
388
|
+
return paths.some(pathExists);
|
|
389
|
+
}
|
|
390
|
+
|
|
159
391
|
function containsAny(text, keywords) {
|
|
160
392
|
const haystack = text.toUpperCase();
|
|
161
393
|
return keywords.some((keyword) => haystack.includes(keyword.toUpperCase()));
|
|
162
394
|
}
|
|
163
395
|
|
|
396
|
+
function detectProjectReadiness(manuscriptDir) {
|
|
397
|
+
const missing = CORE_PROJECT_FILES.filter((file) => !pathExists(path.join(manuscriptDir, file)));
|
|
398
|
+
return {
|
|
399
|
+
state: missing.length ? 'incomplete' : 'ready',
|
|
400
|
+
missing,
|
|
401
|
+
suggest: missing.length ? '/scr:scan' : null,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function detectPlanSignal(manuscriptDir, draftFiles) {
|
|
406
|
+
const files = listFiles(path.join(manuscriptDir, 'plans'), { extensions: ['.md'], recursive: true });
|
|
407
|
+
if (files.length === 0) {
|
|
408
|
+
return { state: 'missing', count: 0, suggest: '/scr:plan' };
|
|
409
|
+
}
|
|
410
|
+
if (draftFiles.length === 0) {
|
|
411
|
+
return { state: 'ready-to-draft', count: files.length, suggest: '/scr:draft' };
|
|
412
|
+
}
|
|
413
|
+
if (files.length > draftFiles.length) {
|
|
414
|
+
return { state: 'partially-drafted', count: files.length, suggest: '/scr:draft' };
|
|
415
|
+
}
|
|
416
|
+
return { state: 'covered', count: files.length, suggest: null };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function detectReviewCoverage(draftFiles, reviewFiles) {
|
|
420
|
+
if (draftFiles.length === 0) {
|
|
421
|
+
return { state: 'none', suggest: null };
|
|
422
|
+
}
|
|
423
|
+
if (reviewFiles.length === 0) {
|
|
424
|
+
return { state: 'missing', suggest: '/scr:editor-review' };
|
|
425
|
+
}
|
|
426
|
+
if (reviewFiles.length < draftFiles.length) {
|
|
427
|
+
return { state: 'partial', suggest: '/scr:editor-review' };
|
|
428
|
+
}
|
|
429
|
+
return { state: 'covered', suggest: null };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function detectNotesSignal(manuscriptDir) {
|
|
433
|
+
const noteFiles = [
|
|
434
|
+
...listFiles(path.join(manuscriptDir, 'notes'), { extensions: ['.md', '.txt'], recursive: true }),
|
|
435
|
+
path.join(manuscriptDir, 'NOTES.md'),
|
|
436
|
+
path.join(manuscriptDir, 'TODO.md'),
|
|
437
|
+
].filter(pathExists);
|
|
438
|
+
const pending = noteFiles.filter((file) => containsAny(readText(file), ['TODO', 'FIXME', 'UNRESOLVED', 'QUESTION:', 'NOTE:']));
|
|
439
|
+
return {
|
|
440
|
+
state: pending.length ? 'pending' : 'none',
|
|
441
|
+
count: pending.length,
|
|
442
|
+
files: pending.map((file) => path.relative(manuscriptDir, file)),
|
|
443
|
+
suggest: pending.length ? '/scr:check-notes' : null,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function detectTrackSignal(manuscriptDir) {
|
|
448
|
+
const tracks = readJson(path.join(manuscriptDir, 'tracks.json'));
|
|
449
|
+
const proposals = listFiles(path.join(manuscriptDir, 'proposals'), { extensions: ['.md'], recursive: true });
|
|
450
|
+
const activeTracks = Array.isArray(tracks?.tracks)
|
|
451
|
+
? tracks.tracks.filter((track) => track && track.status !== 'merged')
|
|
452
|
+
: [];
|
|
453
|
+
let state = 'none';
|
|
454
|
+
let suggest = null;
|
|
455
|
+
if (proposals.length > 0) {
|
|
456
|
+
state = 'proposal-ready';
|
|
457
|
+
suggest = '/scr:editor-review --proposal';
|
|
458
|
+
} else if (activeTracks.length > 0) {
|
|
459
|
+
state = 'active';
|
|
460
|
+
suggest = '/scr:track';
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
state,
|
|
464
|
+
activeCount: activeTracks.length,
|
|
465
|
+
proposalCount: proposals.length,
|
|
466
|
+
suggest,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function detectPublishingSignal(manuscriptDir, draftFiles) {
|
|
471
|
+
const frontMatter = countMarkdownFiles(path.join(manuscriptDir, 'front-matter'));
|
|
472
|
+
const backMatter = countMarkdownFiles(path.join(manuscriptDir, 'back-matter'));
|
|
473
|
+
const blurb = pathExists(path.join(manuscriptDir, 'output', 'blurb.md'));
|
|
474
|
+
const ebookCover = anyPathExists([
|
|
475
|
+
path.join(manuscriptDir, 'build', 'ebook-cover.jpg'),
|
|
476
|
+
path.join(manuscriptDir, 'build', 'ebook-cover.png'),
|
|
477
|
+
]);
|
|
478
|
+
const printCover = anyPathExists([
|
|
479
|
+
path.join(manuscriptDir, 'build', 'paperback-cover.pdf'),
|
|
480
|
+
path.join(manuscriptDir, 'build', 'hardcover-cover.pdf'),
|
|
481
|
+
]);
|
|
482
|
+
const promptFiles = countFiles(path.join(manuscriptDir, 'illustrations', 'cover'), ['.md']);
|
|
483
|
+
const gaps = [];
|
|
484
|
+
if (draftFiles.length > 0 && frontMatter === 0) gaps.push('front-matter');
|
|
485
|
+
if (draftFiles.length > 0 && backMatter === 0) gaps.push('back-matter');
|
|
486
|
+
if (draftFiles.length > 0 && !blurb) gaps.push('blurb');
|
|
487
|
+
if (draftFiles.length > 0 && !ebookCover && promptFiles === 0) gaps.push('cover-art');
|
|
488
|
+
return {
|
|
489
|
+
state: gaps.length ? 'gaps' : draftFiles.length ? 'ready' : 'not-started',
|
|
490
|
+
frontMatter,
|
|
491
|
+
backMatter,
|
|
492
|
+
blurb,
|
|
493
|
+
ebookCover,
|
|
494
|
+
printCover,
|
|
495
|
+
coverPrompts: promptFiles,
|
|
496
|
+
gaps,
|
|
497
|
+
suggest: gaps.length ? `/scr:${gaps[0]}` : null,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
164
501
|
function scanReviewSignals(manuscriptDir) {
|
|
165
502
|
const reviewDirs = [
|
|
166
503
|
'reviews',
|
|
@@ -290,6 +627,13 @@ function chooseRecommendation(signals, counts) {
|
|
|
290
627
|
alternatives: ['/scr:progress', '/scr:resume-work'],
|
|
291
628
|
};
|
|
292
629
|
}
|
|
630
|
+
if (signals.tracks?.state === 'proposal-ready') {
|
|
631
|
+
return {
|
|
632
|
+
command: signals.tracks.suggest,
|
|
633
|
+
reason: `${signals.tracks.proposalCount} revision proposal(s) are waiting for review.`,
|
|
634
|
+
alternatives: ['/scr:track', '/scr:compare', '/scr:progress'],
|
|
635
|
+
};
|
|
636
|
+
}
|
|
293
637
|
if (signals.reviews.count > 0) {
|
|
294
638
|
return {
|
|
295
639
|
command: '/scr:editor-review',
|
|
@@ -297,6 +641,20 @@ function chooseRecommendation(signals, counts) {
|
|
|
297
641
|
alternatives: ['/scr:voice-check', '/scr:continuity-check', '/scr:progress'],
|
|
298
642
|
};
|
|
299
643
|
}
|
|
644
|
+
if (signals.notes?.count > 0) {
|
|
645
|
+
return {
|
|
646
|
+
command: signals.notes.suggest,
|
|
647
|
+
reason: `${signals.notes.count} note file(s) contain unresolved items.`,
|
|
648
|
+
alternatives: ['/scr:progress', '/scr:scan', '/scr:next'],
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
if (signals.plan?.state === 'ready-to-draft' || signals.plan?.state === 'partially-drafted') {
|
|
652
|
+
return {
|
|
653
|
+
command: signals.plan.suggest,
|
|
654
|
+
reason: `${signals.plan.count} plan file(s) exist and drafting is the next connected step.`,
|
|
655
|
+
alternatives: ['/scr:plan', '/scr:voice-test', '/scr:progress'],
|
|
656
|
+
};
|
|
657
|
+
}
|
|
300
658
|
if (counts.drafts === 0) {
|
|
301
659
|
return {
|
|
302
660
|
command: '/scr:plan',
|
|
@@ -304,6 +662,13 @@ function chooseRecommendation(signals, counts) {
|
|
|
304
662
|
alternatives: ['/scr:discuss', '/scr:draft', '/scr:voice-test'],
|
|
305
663
|
};
|
|
306
664
|
}
|
|
665
|
+
if (signals.reviewCoverage?.state === 'missing' || signals.reviewCoverage?.state === 'partial') {
|
|
666
|
+
return {
|
|
667
|
+
command: signals.reviewCoverage.suggest,
|
|
668
|
+
reason: `Drafts exist but review coverage is ${signals.reviewCoverage.state}.`,
|
|
669
|
+
alternatives: ['/scr:voice-check', '/scr:continuity-check', '/scr:progress'],
|
|
670
|
+
};
|
|
671
|
+
}
|
|
307
672
|
if (signals.translation.state !== 'none') {
|
|
308
673
|
return {
|
|
309
674
|
command: '/scr:back-translate',
|
|
@@ -311,6 +676,13 @@ function chooseRecommendation(signals, counts) {
|
|
|
311
676
|
alternatives: ['/scr:cultural-adaptation', '/scr:multi-publish', '/scr:progress'],
|
|
312
677
|
};
|
|
313
678
|
}
|
|
679
|
+
if (signals.publishing?.state === 'gaps' && signals.export.state === 'missing') {
|
|
680
|
+
return {
|
|
681
|
+
command: signals.publishing.suggest || '/scr:publish',
|
|
682
|
+
reason: `Publishing prerequisites have gaps: ${signals.publishing.gaps.join(', ')}.`,
|
|
683
|
+
alternatives: ['/scr:publish', '/scr:export', '/scr:progress'],
|
|
684
|
+
};
|
|
685
|
+
}
|
|
314
686
|
if (signals.export.state === 'stale' || signals.export.state === 'missing') {
|
|
315
687
|
return {
|
|
316
688
|
command: signals.export.suggest || '/scr:export',
|
|
@@ -332,6 +704,90 @@ function chooseRecommendation(signals, counts) {
|
|
|
332
704
|
};
|
|
333
705
|
}
|
|
334
706
|
|
|
707
|
+
function dedupeByCommand(items) {
|
|
708
|
+
const seen = new Set();
|
|
709
|
+
return items.filter((item) => {
|
|
710
|
+
if (seen.has(item.command)) return false;
|
|
711
|
+
seen.add(item.command);
|
|
712
|
+
return true;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function buildAutomationPlan(signals, recommendation) {
|
|
717
|
+
const spawnPolicy = AGENT_ROUTE_POLICIES[recommendation.command];
|
|
718
|
+
const localPolicy = LOCAL_ROUTE_POLICIES[recommendation.command];
|
|
719
|
+
const manualPolicy = MANUAL_ROUTE_POLICIES[recommendation.command];
|
|
720
|
+
const spawnCandidates = [];
|
|
721
|
+
const localCandidates = [];
|
|
722
|
+
const manualGates = [];
|
|
723
|
+
|
|
724
|
+
if (spawnPolicy) {
|
|
725
|
+
spawnCandidates.push({
|
|
726
|
+
command: recommendation.command,
|
|
727
|
+
agents: spawnPolicy.agents,
|
|
728
|
+
reason: spawnPolicy.reason,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
if (signals.plan?.state === 'ready-to-draft' || signals.plan?.state === 'partially-drafted') {
|
|
732
|
+
spawnCandidates.push({
|
|
733
|
+
command: '/scr:draft',
|
|
734
|
+
agents: AGENT_ROUTE_POLICIES['/scr:draft'].agents,
|
|
735
|
+
reason: 'planned units can be drafted by the drafter route',
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
if (signals.reviewCoverage?.state === 'missing' || signals.reviewCoverage?.state === 'partial') {
|
|
739
|
+
spawnCandidates.push({
|
|
740
|
+
command: '/scr:editor-review',
|
|
741
|
+
agents: AGENT_ROUTE_POLICIES['/scr:editor-review'].agents,
|
|
742
|
+
reason: 'drafts without review coverage should enter the review route',
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
if (signals.translation?.state !== 'none') {
|
|
746
|
+
spawnCandidates.push({
|
|
747
|
+
command: '/scr:back-translate',
|
|
748
|
+
agents: AGENT_ROUTE_POLICIES['/scr:back-translate'].agents,
|
|
749
|
+
reason: 'translation work needs a verification pass',
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (localPolicy) {
|
|
754
|
+
localCandidates.push({ command: recommendation.command, reason: localPolicy });
|
|
755
|
+
}
|
|
756
|
+
if (signals.context?.state === 'stale') {
|
|
757
|
+
localCandidates.push({ command: signals.context.suggest || '/scr:scan', reason: 'refresh stale context before chaining work' });
|
|
758
|
+
}
|
|
759
|
+
if (signals.notes?.count > 0) {
|
|
760
|
+
localCandidates.push({ command: '/scr:check-notes', reason: 'surface unresolved notes before the next writing route' });
|
|
761
|
+
}
|
|
762
|
+
if (signals.save?.state !== 'clean') {
|
|
763
|
+
localCandidates.push({ command: signals.save.suggest || '/scr:save', reason: 'save manuscript changes before branching or packaging' });
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (manualPolicy) {
|
|
767
|
+
manualGates.push({ command: recommendation.command, reason: manualPolicy });
|
|
768
|
+
}
|
|
769
|
+
if (signals.publishing?.state === 'gaps') {
|
|
770
|
+
manualGates.push({
|
|
771
|
+
command: '/scr:publish',
|
|
772
|
+
reason: `publishing still needs ${signals.publishing.gaps.join(', ')}`,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
if (signals.tracks?.state === 'active' || signals.tracks?.state === 'proposal-ready') {
|
|
776
|
+
manualGates.push({
|
|
777
|
+
command: signals.tracks.suggest || '/scr:track',
|
|
778
|
+
reason: 'revision-track decisions belong to the writer',
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const recommendationIsManual = manualGates.some((gate) => gate.command === recommendation.command);
|
|
783
|
+
return {
|
|
784
|
+
mode: recommendationIsManual ? 'manual-gated' : spawnCandidates.length ? 'agent-ready' : localCandidates.length ? 'local-helper' : 'read-only',
|
|
785
|
+
spawnCandidates: dedupeByCommand(spawnCandidates),
|
|
786
|
+
localCandidates: dedupeByCommand(localCandidates),
|
|
787
|
+
manualGates: dedupeByCommand(manualGates),
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
335
791
|
function analyzeProject(projectRoot = process.cwd(), options = {}) {
|
|
336
792
|
const root = path.resolve(projectRoot);
|
|
337
793
|
const manuscriptDir = options.manuscriptDir || path.join(root, '.manuscript');
|
|
@@ -346,11 +802,18 @@ function analyzeProject(projectRoot = process.cwd(), options = {}) {
|
|
|
346
802
|
context: { state: 'none', suggest: null },
|
|
347
803
|
history: { state: 'none', lastFailed: false },
|
|
348
804
|
reviews: { state: 'none', count: 0, files: [] },
|
|
805
|
+
reviewCoverage: { state: 'none', suggest: null },
|
|
806
|
+
readiness: { state: 'none', missing: [], suggest: null },
|
|
807
|
+
plan: { state: 'none', count: 0, suggest: null },
|
|
808
|
+
notes: { state: 'none', count: 0, files: [], suggest: null },
|
|
809
|
+
tracks: { state: 'none', activeCount: 0, proposalCount: 0, suggest: null },
|
|
349
810
|
translation: { state: 'none', count: 0, configuredTargets: [] },
|
|
350
811
|
export: { state: 'none', suggest: null },
|
|
812
|
+
publishing: { state: 'not-started', gaps: [], suggest: null },
|
|
351
813
|
save: { state: 'clean', suggest: null },
|
|
352
814
|
};
|
|
353
815
|
const recommendation = chooseRecommendation(signals, { drafts: 0 });
|
|
816
|
+
const automation = buildAutomationPlan(signals, recommendation);
|
|
354
817
|
return {
|
|
355
818
|
projectRoot: root,
|
|
356
819
|
manuscriptDir,
|
|
@@ -359,12 +822,13 @@ function analyzeProject(projectRoot = process.cwd(), options = {}) {
|
|
|
359
822
|
counts: { drafts: 0, plans: 0, reviews: 0 },
|
|
360
823
|
signals,
|
|
361
824
|
recommendation,
|
|
825
|
+
automation,
|
|
362
826
|
};
|
|
363
827
|
}
|
|
364
828
|
|
|
365
829
|
const draftFiles = listFiles(path.join(manuscriptDir, 'drafts'), { extensions: ['.md'], recursive: true });
|
|
366
|
-
const planCount = countMarkdownFiles(path.join(manuscriptDir, 'plans'));
|
|
367
830
|
const reviewFiles = scanReviewSignals(manuscriptDir);
|
|
831
|
+
const allReviewFiles = listFiles(path.join(manuscriptDir, 'reviews'), { extensions: ['.md', '.txt'], recursive: true });
|
|
368
832
|
const historySignal = detectHistorySignal(manuscriptDir);
|
|
369
833
|
const sourceFiles = [
|
|
370
834
|
statePath,
|
|
@@ -379,21 +843,28 @@ function analyzeProject(projectRoot = process.cwd(), options = {}) {
|
|
|
379
843
|
hasState: pathExists(statePath),
|
|
380
844
|
context: detectContextSignal(manuscriptDir, draftFiles),
|
|
381
845
|
history: historySignal,
|
|
846
|
+
readiness: detectProjectReadiness(manuscriptDir),
|
|
847
|
+
plan: detectPlanSignal(manuscriptDir, draftFiles),
|
|
382
848
|
reviews: {
|
|
383
849
|
state: reviewFiles.length ? 'pending' : 'none',
|
|
384
850
|
count: reviewFiles.length,
|
|
385
851
|
files: reviewFiles,
|
|
386
852
|
},
|
|
853
|
+
reviewCoverage: detectReviewCoverage(draftFiles, allReviewFiles),
|
|
854
|
+
notes: detectNotesSignal(manuscriptDir),
|
|
855
|
+
tracks: detectTrackSignal(manuscriptDir),
|
|
387
856
|
translation: detectTranslationSignal(manuscriptDir, config),
|
|
388
857
|
export: detectExportSignal(manuscriptDir, sourceFiles),
|
|
858
|
+
publishing: detectPublishingSignal(manuscriptDir, draftFiles),
|
|
389
859
|
save: detectSaveSignal(historySignal, draftFiles),
|
|
390
860
|
};
|
|
391
861
|
const counts = {
|
|
392
862
|
drafts: draftFiles.length,
|
|
393
|
-
plans:
|
|
863
|
+
plans: signals.plan.count,
|
|
394
864
|
reviews: reviewFiles.length,
|
|
395
865
|
};
|
|
396
866
|
const recommendation = chooseRecommendation(signals, counts);
|
|
867
|
+
const automation = buildAutomationPlan(signals, recommendation);
|
|
397
868
|
return {
|
|
398
869
|
projectRoot: root,
|
|
399
870
|
manuscriptDir,
|
|
@@ -402,6 +873,7 @@ function analyzeProject(projectRoot = process.cwd(), options = {}) {
|
|
|
402
873
|
counts,
|
|
403
874
|
signals,
|
|
404
875
|
recommendation,
|
|
876
|
+
automation,
|
|
405
877
|
};
|
|
406
878
|
}
|
|
407
879
|
|
|
@@ -413,9 +885,15 @@ function formatProactiveChecks(analysis) {
|
|
|
413
885
|
return [
|
|
414
886
|
'Proactive checks:',
|
|
415
887
|
stateLine,
|
|
888
|
+
` Readiness: ${signals.readiness?.state || 'none'}${signals.readiness?.missing?.length ? `, missing ${signals.readiness.missing.join(', ')}` : ''}`,
|
|
416
889
|
` Session: ${signals.context.state}${signals.context.suggest ? `, suggest ${signals.context.suggest}` : ''}`,
|
|
890
|
+
` Plans: ${signals.plan?.state || 'none'}${signals.plan?.suggest ? `, suggest ${signals.plan.suggest}` : ''}`,
|
|
417
891
|
` Reviews: ${signals.reviews.count ? `${signals.reviews.count} pending, suggest /scr:editor-review` : 'none'}`,
|
|
892
|
+
` Review coverage: ${signals.reviewCoverage?.state || 'none'}${signals.reviewCoverage?.suggest ? `, suggest ${signals.reviewCoverage.suggest}` : ''}`,
|
|
893
|
+
` Notes: ${signals.notes?.count ? `${signals.notes.count} pending, suggest ${signals.notes.suggest}` : 'none'}`,
|
|
894
|
+
` Tracks: ${signals.tracks?.state || 'none'}${signals.tracks?.suggest ? `, suggest ${signals.tracks.suggest}` : ''}`,
|
|
418
895
|
` Translation: ${signals.translation.state}`,
|
|
896
|
+
` Publishing: ${signals.publishing?.state || 'none'}${signals.publishing?.gaps?.length ? `, gaps ${signals.publishing.gaps.join(', ')}` : ''}`,
|
|
419
897
|
` Export: ${signals.export.state}${signals.export.suggest ? `, suggest ${signals.export.suggest}` : ''}`,
|
|
420
898
|
` Save: ${signals.save.state}${signals.save.suggest ? `, suggest ${signals.save.suggest}` : ''}`,
|
|
421
899
|
].join('\n');
|
|
@@ -425,14 +903,31 @@ function formatAutomationStatus(analysis, options = {}) {
|
|
|
425
903
|
const trigger = options.trigger || '/scr:next';
|
|
426
904
|
const localOperation = options.localOperation || 'auto-invoke engine: read-only';
|
|
427
905
|
const autoInvoked = options.autoInvoked || `${analysis.recommendation.command}: no`;
|
|
906
|
+
const automation = analysis.automation || { mode: 'read-only', spawnCandidates: [], localCandidates: [], manualGates: [] };
|
|
907
|
+
const candidateAgentLines = automation.spawnCandidates.length
|
|
908
|
+
? automation.spawnCandidates.map((candidate) => `- ${candidate.command}: ${candidate.agents.join(', ')} (${candidate.reason})`)
|
|
909
|
+
: ['- none'];
|
|
910
|
+
const localCandidateLines = automation.localCandidates.length
|
|
911
|
+
? automation.localCandidates.map((candidate) => `- ${candidate.command}: ${candidate.reason}`)
|
|
912
|
+
: ['- none'];
|
|
913
|
+
const manualGateLines = automation.manualGates.length
|
|
914
|
+
? automation.manualGates.map((gate) => `- ${gate.command}: ${gate.reason}`)
|
|
915
|
+
: ['- none'];
|
|
428
916
|
return [
|
|
429
917
|
'Automation status:',
|
|
430
918
|
`Trigger: ${trigger}`,
|
|
919
|
+
`Mode: ${automation.mode}`,
|
|
431
920
|
'Spawned agents:',
|
|
432
921
|
'- none',
|
|
922
|
+
'Candidate agents:',
|
|
923
|
+
...candidateAgentLines,
|
|
433
924
|
'Local operations:',
|
|
434
925
|
`- ${localOperation}`,
|
|
435
926
|
`- state route computed: ${analysis.signals.hasProject ? 'yes' : 'no project'}`,
|
|
927
|
+
'Candidate local helpers:',
|
|
928
|
+
...localCandidateLines,
|
|
929
|
+
'Manual gates:',
|
|
930
|
+
...manualGateLines,
|
|
436
931
|
'Auto-invoked:',
|
|
437
932
|
`- ${autoInvoked}`,
|
|
438
933
|
`Why: ${analysis.recommendation.reason}`,
|
|
@@ -474,6 +969,381 @@ function listRuntimeAgentSupport() {
|
|
|
474
969
|
}));
|
|
475
970
|
}
|
|
476
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
|
+
|
|
477
1347
|
function parseCliArgs(argv) {
|
|
478
1348
|
const out = {
|
|
479
1349
|
projectRoot: process.cwd(),
|
|
@@ -508,13 +1378,30 @@ if (require.main === module) {
|
|
|
508
1378
|
}
|
|
509
1379
|
|
|
510
1380
|
module.exports = {
|
|
1381
|
+
AGENT_ROUTE_POLICIES,
|
|
1382
|
+
CATEGORY_ROUTE_POLICIES,
|
|
511
1383
|
DEFAULT_RUNTIME_SUPPORT,
|
|
1384
|
+
DEFAULT_AGENT_NAMES,
|
|
1385
|
+
LOCAL_ROUTE_POLICIES,
|
|
1386
|
+
MANUAL_ROUTE_POLICIES,
|
|
1387
|
+
ROUTE_PRIORITY_FIXTURES,
|
|
512
1388
|
analyzeProject,
|
|
1389
|
+
buildRouteGraph,
|
|
1390
|
+
collectSafeApplyActions,
|
|
1391
|
+
expectedCommandCount,
|
|
1392
|
+
formatAgentAvailabilityReport,
|
|
1393
|
+
formatRouteGraphReport,
|
|
513
1394
|
formatProactiveChecks,
|
|
514
1395
|
formatAutomationStatus,
|
|
515
1396
|
formatRecommendation,
|
|
516
1397
|
formatReport,
|
|
1398
|
+
formatRuntimeSmokeReport,
|
|
1399
|
+
formatSafeApplyReport,
|
|
1400
|
+
getCommandAutomationPolicy,
|
|
1401
|
+
getExpectedAgentNames,
|
|
517
1402
|
getRuntimeAgentSupport,
|
|
1403
|
+
inspectAgentAvailability,
|
|
1404
|
+
inspectRuntimeSmoke,
|
|
518
1405
|
listRuntimeAgentSupport,
|
|
519
1406
|
parseCliArgs,
|
|
520
1407
|
};
|