scriveno 2.0.8 → 2.0.9

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.
@@ -91,6 +91,116 @@ const REVIEW_KEYWORDS = [
91
91
  'CONTINUITY',
92
92
  ];
93
93
 
94
+ const CORE_PROJECT_FILES = [
95
+ 'WORK.md',
96
+ 'OUTLINE.md',
97
+ 'STYLE-GUIDE.md',
98
+ 'RECORD.md',
99
+ 'config.json',
100
+ ];
101
+
102
+ const AGENT_ROUTE_POLICIES = {
103
+ '/scr:plan': {
104
+ agents: ['plan-checker'],
105
+ reason: 'planning can validate unit plans before drafting',
106
+ },
107
+ '/scr:draft': {
108
+ agents: ['drafter', 'voice-checker'],
109
+ reason: 'drafting uses fresh-context prose generation and voice checks',
110
+ },
111
+ '/scr:editor-review': {
112
+ agents: ['diagnostic worker'],
113
+ reason: 'editor review can isolate flagged issue groups',
114
+ },
115
+ '/scr:voice-check': {
116
+ agents: ['voice-checker'],
117
+ reason: 'voice review compares drafts against STYLE-GUIDE.md',
118
+ },
119
+ '/scr:continuity-check': {
120
+ agents: ['continuity-checker'],
121
+ reason: 'continuity review checks contradictions and timeline drift',
122
+ },
123
+ '/scr:translate': {
124
+ agents: ['translator'],
125
+ reason: 'translation runs one fresh-context translation pass per unit',
126
+ },
127
+ '/scr:back-translate': {
128
+ agents: ['translator'],
129
+ reason: 'back-translation verifies target-language drift',
130
+ },
131
+ '/scr:beta-reader': {
132
+ agents: ['beta-reader worker'],
133
+ reason: 'beta review benefits from isolated reader perspectives',
134
+ },
135
+ '/scr:quick-write': {
136
+ agents: ['drafter', 'voice-checker'],
137
+ reason: 'quick writing still benefits from voice-aware isolation',
138
+ },
139
+ '/scr:map-manuscript': {
140
+ agents: ['voice analyst', 'structure analyst', 'character analyst', 'theme analyst', 'world analyst', 'pacing analyst'],
141
+ reason: 'manuscript import uses parallel analysis workers when available',
142
+ },
143
+ };
144
+
145
+ const LOCAL_ROUTE_POLICIES = {
146
+ '/scr:save': 'refresh CONTEXT.md, HISTORY.log, and project checkpoint state',
147
+ '/scr:scan': 'reconcile STATE.md and disk evidence',
148
+ '/scr:health': 'diagnose project and runtime health',
149
+ '/scr:sync': 'compare and refresh installed runtime surfaces',
150
+ '/scr:validate': 'run project validation checks',
151
+ '/scr:check-notes': 'surface unresolved writer notes',
152
+ '/scr:progress': 'compute read-only project progress',
153
+ '/scr:session-report': 'compute read-only session metrics',
154
+ };
155
+
156
+ const MANUAL_ROUTE_POLICIES = {
157
+ '/scr:publish': 'publication packaging can overwrite deliverables and needs writer choices',
158
+ '/scr:export': 'export writes output artifacts and may overwrite packages',
159
+ '/scr:track merge': 'merging revision tracks is a writer-owned decision',
160
+ '/scr:undo': 'undo changes state and should stay explicit',
161
+ };
162
+
163
+ const CATEGORY_ROUTE_POLICIES = {
164
+ core: { lane: 'mixed', level: 3, reason: 'core lifecycle routes may read, write, or spawn depending on the current stage' },
165
+ navigation: { lane: 'read-only', level: 1, reason: 'navigation routes should inspect and recommend by default' },
166
+ quality: { lane: 'agent-or-local', level: 3, reason: 'quality routes may run bounded diagnostics or text transforms' },
167
+ character_world: { lane: 'local-helper', level: 2, reason: 'character and world routes update project knowledge files' },
168
+ structure: { lane: 'local-helper', level: 2, reason: 'structure routes update maps, outlines, and state evidence' },
169
+ structure_management: { lane: 'manual-gated', level: 4, reason: 'structure management can rename, remove, or reorder manuscript units' },
170
+ review: { lane: 'agent-or-local', level: 3, reason: 'review routes may invoke bounded diagnostic workers' },
171
+ illustration: { lane: 'local-helper', level: 2, reason: 'illustration routes generate prompts and asset briefs' },
172
+ publishing: { lane: 'manual-gated', level: 4, reason: 'publishing routes write deliverables and package outputs' },
173
+ translation: { lane: 'agent-or-local', level: 3, reason: 'translation routes use translator agents or verification helpers' },
174
+ sacred_exclusive: { lane: 'agent-or-local', level: 3, reason: 'sacred routes perform specialized consistency and reference work' },
175
+ utility: { lane: 'local-helper', level: 2, reason: 'utility routes perform deterministic diagnostics or project updates' },
176
+ session: { lane: 'local-helper', level: 2, reason: 'session routes save, compare, resume, or report project state' },
177
+ collaboration: { lane: 'manual-gated', level: 4, reason: 'collaboration routes change revision tracks and require writer control' },
178
+ };
179
+
180
+ function normalizeCommandRef(commandName) {
181
+ if (commandName.startsWith('/scr:')) return commandName;
182
+ return `/scr:${commandName}`;
183
+ }
184
+
185
+ function getCommandAutomationPolicy(commandName, command = {}) {
186
+ const ref = normalizeCommandRef(commandName);
187
+ if (AGENT_ROUTE_POLICIES[ref]) {
188
+ return { ref, lane: 'agent-ready', level: 3, reason: AGENT_ROUTE_POLICIES[ref].reason };
189
+ }
190
+ if (LOCAL_ROUTE_POLICIES[ref]) {
191
+ return { ref, lane: 'local-helper', level: 2, reason: LOCAL_ROUTE_POLICIES[ref] };
192
+ }
193
+ if (MANUAL_ROUTE_POLICIES[ref]) {
194
+ return { ref, lane: 'manual-gated', level: 4, reason: MANUAL_ROUTE_POLICIES[ref] };
195
+ }
196
+ const categoryPolicy = CATEGORY_ROUTE_POLICIES[command.category] || {
197
+ lane: 'read-only',
198
+ level: 1,
199
+ reason: 'unclassified routes should only suggest until a category policy is added',
200
+ };
201
+ return { ref, ...categoryPolicy };
202
+ }
203
+
94
204
  function pathExists(filePath) {
95
205
  try {
96
206
  fs.accessSync(filePath);
@@ -156,11 +266,124 @@ function countMarkdownFiles(dir) {
156
266
  return listFiles(dir, { extensions: ['.md'], recursive: true }).length;
157
267
  }
158
268
 
269
+ function countFiles(dir, extensions = null) {
270
+ return listFiles(dir, { extensions, recursive: true }).length;
271
+ }
272
+
273
+ function anyPathExists(paths) {
274
+ return paths.some(pathExists);
275
+ }
276
+
159
277
  function containsAny(text, keywords) {
160
278
  const haystack = text.toUpperCase();
161
279
  return keywords.some((keyword) => haystack.includes(keyword.toUpperCase()));
162
280
  }
163
281
 
282
+ function detectProjectReadiness(manuscriptDir) {
283
+ const missing = CORE_PROJECT_FILES.filter((file) => !pathExists(path.join(manuscriptDir, file)));
284
+ return {
285
+ state: missing.length ? 'incomplete' : 'ready',
286
+ missing,
287
+ suggest: missing.length ? '/scr:scan' : null,
288
+ };
289
+ }
290
+
291
+ function detectPlanSignal(manuscriptDir, draftFiles) {
292
+ const files = listFiles(path.join(manuscriptDir, 'plans'), { extensions: ['.md'], recursive: true });
293
+ if (files.length === 0) {
294
+ return { state: 'missing', count: 0, suggest: '/scr:plan' };
295
+ }
296
+ if (draftFiles.length === 0) {
297
+ return { state: 'ready-to-draft', count: files.length, suggest: '/scr:draft' };
298
+ }
299
+ if (files.length > draftFiles.length) {
300
+ return { state: 'partially-drafted', count: files.length, suggest: '/scr:draft' };
301
+ }
302
+ return { state: 'covered', count: files.length, suggest: null };
303
+ }
304
+
305
+ function detectReviewCoverage(draftFiles, reviewFiles) {
306
+ if (draftFiles.length === 0) {
307
+ return { state: 'none', suggest: null };
308
+ }
309
+ if (reviewFiles.length === 0) {
310
+ return { state: 'missing', suggest: '/scr:editor-review' };
311
+ }
312
+ if (reviewFiles.length < draftFiles.length) {
313
+ return { state: 'partial', suggest: '/scr:editor-review' };
314
+ }
315
+ return { state: 'covered', suggest: null };
316
+ }
317
+
318
+ function detectNotesSignal(manuscriptDir) {
319
+ const noteFiles = [
320
+ ...listFiles(path.join(manuscriptDir, 'notes'), { extensions: ['.md', '.txt'], recursive: true }),
321
+ path.join(manuscriptDir, 'NOTES.md'),
322
+ path.join(manuscriptDir, 'TODO.md'),
323
+ ].filter(pathExists);
324
+ const pending = noteFiles.filter((file) => containsAny(readText(file), ['TODO', 'FIXME', 'UNRESOLVED', 'QUESTION:', 'NOTE:']));
325
+ return {
326
+ state: pending.length ? 'pending' : 'none',
327
+ count: pending.length,
328
+ files: pending.map((file) => path.relative(manuscriptDir, file)),
329
+ suggest: pending.length ? '/scr:check-notes' : null,
330
+ };
331
+ }
332
+
333
+ function detectTrackSignal(manuscriptDir) {
334
+ const tracks = readJson(path.join(manuscriptDir, 'tracks.json'));
335
+ const proposals = listFiles(path.join(manuscriptDir, 'proposals'), { extensions: ['.md'], recursive: true });
336
+ const activeTracks = Array.isArray(tracks?.tracks)
337
+ ? tracks.tracks.filter((track) => track && track.status !== 'merged')
338
+ : [];
339
+ let state = 'none';
340
+ let suggest = null;
341
+ if (proposals.length > 0) {
342
+ state = 'proposal-ready';
343
+ suggest = '/scr:editor-review --proposal';
344
+ } else if (activeTracks.length > 0) {
345
+ state = 'active';
346
+ suggest = '/scr:track';
347
+ }
348
+ return {
349
+ state,
350
+ activeCount: activeTracks.length,
351
+ proposalCount: proposals.length,
352
+ suggest,
353
+ };
354
+ }
355
+
356
+ function detectPublishingSignal(manuscriptDir, draftFiles) {
357
+ const frontMatter = countMarkdownFiles(path.join(manuscriptDir, 'front-matter'));
358
+ const backMatter = countMarkdownFiles(path.join(manuscriptDir, 'back-matter'));
359
+ const blurb = pathExists(path.join(manuscriptDir, 'output', 'blurb.md'));
360
+ const ebookCover = anyPathExists([
361
+ path.join(manuscriptDir, 'build', 'ebook-cover.jpg'),
362
+ path.join(manuscriptDir, 'build', 'ebook-cover.png'),
363
+ ]);
364
+ const printCover = anyPathExists([
365
+ path.join(manuscriptDir, 'build', 'paperback-cover.pdf'),
366
+ path.join(manuscriptDir, 'build', 'hardcover-cover.pdf'),
367
+ ]);
368
+ const promptFiles = countFiles(path.join(manuscriptDir, 'illustrations', 'cover'), ['.md']);
369
+ const gaps = [];
370
+ if (draftFiles.length > 0 && frontMatter === 0) gaps.push('front-matter');
371
+ if (draftFiles.length > 0 && backMatter === 0) gaps.push('back-matter');
372
+ if (draftFiles.length > 0 && !blurb) gaps.push('blurb');
373
+ if (draftFiles.length > 0 && !ebookCover && promptFiles === 0) gaps.push('cover-art');
374
+ return {
375
+ state: gaps.length ? 'gaps' : draftFiles.length ? 'ready' : 'not-started',
376
+ frontMatter,
377
+ backMatter,
378
+ blurb,
379
+ ebookCover,
380
+ printCover,
381
+ coverPrompts: promptFiles,
382
+ gaps,
383
+ suggest: gaps.length ? `/scr:${gaps[0]}` : null,
384
+ };
385
+ }
386
+
164
387
  function scanReviewSignals(manuscriptDir) {
165
388
  const reviewDirs = [
166
389
  'reviews',
@@ -290,6 +513,13 @@ function chooseRecommendation(signals, counts) {
290
513
  alternatives: ['/scr:progress', '/scr:resume-work'],
291
514
  };
292
515
  }
516
+ if (signals.tracks?.state === 'proposal-ready') {
517
+ return {
518
+ command: signals.tracks.suggest,
519
+ reason: `${signals.tracks.proposalCount} revision proposal(s) are waiting for review.`,
520
+ alternatives: ['/scr:track', '/scr:compare', '/scr:progress'],
521
+ };
522
+ }
293
523
  if (signals.reviews.count > 0) {
294
524
  return {
295
525
  command: '/scr:editor-review',
@@ -297,6 +527,20 @@ function chooseRecommendation(signals, counts) {
297
527
  alternatives: ['/scr:voice-check', '/scr:continuity-check', '/scr:progress'],
298
528
  };
299
529
  }
530
+ if (signals.notes?.count > 0) {
531
+ return {
532
+ command: signals.notes.suggest,
533
+ reason: `${signals.notes.count} note file(s) contain unresolved items.`,
534
+ alternatives: ['/scr:progress', '/scr:scan', '/scr:next'],
535
+ };
536
+ }
537
+ if (signals.plan?.state === 'ready-to-draft' || signals.plan?.state === 'partially-drafted') {
538
+ return {
539
+ command: signals.plan.suggest,
540
+ reason: `${signals.plan.count} plan file(s) exist and drafting is the next connected step.`,
541
+ alternatives: ['/scr:plan', '/scr:voice-test', '/scr:progress'],
542
+ };
543
+ }
300
544
  if (counts.drafts === 0) {
301
545
  return {
302
546
  command: '/scr:plan',
@@ -304,6 +548,13 @@ function chooseRecommendation(signals, counts) {
304
548
  alternatives: ['/scr:discuss', '/scr:draft', '/scr:voice-test'],
305
549
  };
306
550
  }
551
+ if (signals.reviewCoverage?.state === 'missing' || signals.reviewCoverage?.state === 'partial') {
552
+ return {
553
+ command: signals.reviewCoverage.suggest,
554
+ reason: `Drafts exist but review coverage is ${signals.reviewCoverage.state}.`,
555
+ alternatives: ['/scr:voice-check', '/scr:continuity-check', '/scr:progress'],
556
+ };
557
+ }
307
558
  if (signals.translation.state !== 'none') {
308
559
  return {
309
560
  command: '/scr:back-translate',
@@ -311,6 +562,13 @@ function chooseRecommendation(signals, counts) {
311
562
  alternatives: ['/scr:cultural-adaptation', '/scr:multi-publish', '/scr:progress'],
312
563
  };
313
564
  }
565
+ if (signals.publishing?.state === 'gaps' && signals.export.state === 'missing') {
566
+ return {
567
+ command: signals.publishing.suggest || '/scr:publish',
568
+ reason: `Publishing prerequisites have gaps: ${signals.publishing.gaps.join(', ')}.`,
569
+ alternatives: ['/scr:publish', '/scr:export', '/scr:progress'],
570
+ };
571
+ }
314
572
  if (signals.export.state === 'stale' || signals.export.state === 'missing') {
315
573
  return {
316
574
  command: signals.export.suggest || '/scr:export',
@@ -332,6 +590,90 @@ function chooseRecommendation(signals, counts) {
332
590
  };
333
591
  }
334
592
 
593
+ function dedupeByCommand(items) {
594
+ const seen = new Set();
595
+ return items.filter((item) => {
596
+ if (seen.has(item.command)) return false;
597
+ seen.add(item.command);
598
+ return true;
599
+ });
600
+ }
601
+
602
+ function buildAutomationPlan(signals, recommendation) {
603
+ const spawnPolicy = AGENT_ROUTE_POLICIES[recommendation.command];
604
+ const localPolicy = LOCAL_ROUTE_POLICIES[recommendation.command];
605
+ const manualPolicy = MANUAL_ROUTE_POLICIES[recommendation.command];
606
+ const spawnCandidates = [];
607
+ const localCandidates = [];
608
+ const manualGates = [];
609
+
610
+ if (spawnPolicy) {
611
+ spawnCandidates.push({
612
+ command: recommendation.command,
613
+ agents: spawnPolicy.agents,
614
+ reason: spawnPolicy.reason,
615
+ });
616
+ }
617
+ if (signals.plan?.state === 'ready-to-draft' || signals.plan?.state === 'partially-drafted') {
618
+ spawnCandidates.push({
619
+ command: '/scr:draft',
620
+ agents: AGENT_ROUTE_POLICIES['/scr:draft'].agents,
621
+ reason: 'planned units can be drafted by the drafter route',
622
+ });
623
+ }
624
+ if (signals.reviewCoverage?.state === 'missing' || signals.reviewCoverage?.state === 'partial') {
625
+ spawnCandidates.push({
626
+ command: '/scr:editor-review',
627
+ agents: AGENT_ROUTE_POLICIES['/scr:editor-review'].agents,
628
+ reason: 'drafts without review coverage should enter the review route',
629
+ });
630
+ }
631
+ if (signals.translation?.state !== 'none') {
632
+ spawnCandidates.push({
633
+ command: '/scr:back-translate',
634
+ agents: AGENT_ROUTE_POLICIES['/scr:back-translate'].agents,
635
+ reason: 'translation work needs a verification pass',
636
+ });
637
+ }
638
+
639
+ if (localPolicy) {
640
+ localCandidates.push({ command: recommendation.command, reason: localPolicy });
641
+ }
642
+ if (signals.context?.state === 'stale') {
643
+ localCandidates.push({ command: signals.context.suggest || '/scr:scan', reason: 'refresh stale context before chaining work' });
644
+ }
645
+ if (signals.notes?.count > 0) {
646
+ localCandidates.push({ command: '/scr:check-notes', reason: 'surface unresolved notes before the next writing route' });
647
+ }
648
+ if (signals.save?.state !== 'clean') {
649
+ localCandidates.push({ command: signals.save.suggest || '/scr:save', reason: 'save manuscript changes before branching or packaging' });
650
+ }
651
+
652
+ if (manualPolicy) {
653
+ manualGates.push({ command: recommendation.command, reason: manualPolicy });
654
+ }
655
+ if (signals.publishing?.state === 'gaps') {
656
+ manualGates.push({
657
+ command: '/scr:publish',
658
+ reason: `publishing still needs ${signals.publishing.gaps.join(', ')}`,
659
+ });
660
+ }
661
+ if (signals.tracks?.state === 'active' || signals.tracks?.state === 'proposal-ready') {
662
+ manualGates.push({
663
+ command: signals.tracks.suggest || '/scr:track',
664
+ reason: 'revision-track decisions belong to the writer',
665
+ });
666
+ }
667
+
668
+ const recommendationIsManual = manualGates.some((gate) => gate.command === recommendation.command);
669
+ return {
670
+ mode: recommendationIsManual ? 'manual-gated' : spawnCandidates.length ? 'agent-ready' : localCandidates.length ? 'local-helper' : 'read-only',
671
+ spawnCandidates: dedupeByCommand(spawnCandidates),
672
+ localCandidates: dedupeByCommand(localCandidates),
673
+ manualGates: dedupeByCommand(manualGates),
674
+ };
675
+ }
676
+
335
677
  function analyzeProject(projectRoot = process.cwd(), options = {}) {
336
678
  const root = path.resolve(projectRoot);
337
679
  const manuscriptDir = options.manuscriptDir || path.join(root, '.manuscript');
@@ -346,11 +688,18 @@ function analyzeProject(projectRoot = process.cwd(), options = {}) {
346
688
  context: { state: 'none', suggest: null },
347
689
  history: { state: 'none', lastFailed: false },
348
690
  reviews: { state: 'none', count: 0, files: [] },
691
+ reviewCoverage: { state: 'none', suggest: null },
692
+ readiness: { state: 'none', missing: [], suggest: null },
693
+ plan: { state: 'none', count: 0, suggest: null },
694
+ notes: { state: 'none', count: 0, files: [], suggest: null },
695
+ tracks: { state: 'none', activeCount: 0, proposalCount: 0, suggest: null },
349
696
  translation: { state: 'none', count: 0, configuredTargets: [] },
350
697
  export: { state: 'none', suggest: null },
698
+ publishing: { state: 'not-started', gaps: [], suggest: null },
351
699
  save: { state: 'clean', suggest: null },
352
700
  };
353
701
  const recommendation = chooseRecommendation(signals, { drafts: 0 });
702
+ const automation = buildAutomationPlan(signals, recommendation);
354
703
  return {
355
704
  projectRoot: root,
356
705
  manuscriptDir,
@@ -359,12 +708,13 @@ function analyzeProject(projectRoot = process.cwd(), options = {}) {
359
708
  counts: { drafts: 0, plans: 0, reviews: 0 },
360
709
  signals,
361
710
  recommendation,
711
+ automation,
362
712
  };
363
713
  }
364
714
 
365
715
  const draftFiles = listFiles(path.join(manuscriptDir, 'drafts'), { extensions: ['.md'], recursive: true });
366
- const planCount = countMarkdownFiles(path.join(manuscriptDir, 'plans'));
367
716
  const reviewFiles = scanReviewSignals(manuscriptDir);
717
+ const allReviewFiles = listFiles(path.join(manuscriptDir, 'reviews'), { extensions: ['.md', '.txt'], recursive: true });
368
718
  const historySignal = detectHistorySignal(manuscriptDir);
369
719
  const sourceFiles = [
370
720
  statePath,
@@ -379,21 +729,28 @@ function analyzeProject(projectRoot = process.cwd(), options = {}) {
379
729
  hasState: pathExists(statePath),
380
730
  context: detectContextSignal(manuscriptDir, draftFiles),
381
731
  history: historySignal,
732
+ readiness: detectProjectReadiness(manuscriptDir),
733
+ plan: detectPlanSignal(manuscriptDir, draftFiles),
382
734
  reviews: {
383
735
  state: reviewFiles.length ? 'pending' : 'none',
384
736
  count: reviewFiles.length,
385
737
  files: reviewFiles,
386
738
  },
739
+ reviewCoverage: detectReviewCoverage(draftFiles, allReviewFiles),
740
+ notes: detectNotesSignal(manuscriptDir),
741
+ tracks: detectTrackSignal(manuscriptDir),
387
742
  translation: detectTranslationSignal(manuscriptDir, config),
388
743
  export: detectExportSignal(manuscriptDir, sourceFiles),
744
+ publishing: detectPublishingSignal(manuscriptDir, draftFiles),
389
745
  save: detectSaveSignal(historySignal, draftFiles),
390
746
  };
391
747
  const counts = {
392
748
  drafts: draftFiles.length,
393
- plans: planCount,
749
+ plans: signals.plan.count,
394
750
  reviews: reviewFiles.length,
395
751
  };
396
752
  const recommendation = chooseRecommendation(signals, counts);
753
+ const automation = buildAutomationPlan(signals, recommendation);
397
754
  return {
398
755
  projectRoot: root,
399
756
  manuscriptDir,
@@ -402,6 +759,7 @@ function analyzeProject(projectRoot = process.cwd(), options = {}) {
402
759
  counts,
403
760
  signals,
404
761
  recommendation,
762
+ automation,
405
763
  };
406
764
  }
407
765
 
@@ -413,9 +771,15 @@ function formatProactiveChecks(analysis) {
413
771
  return [
414
772
  'Proactive checks:',
415
773
  stateLine,
774
+ ` Readiness: ${signals.readiness?.state || 'none'}${signals.readiness?.missing?.length ? `, missing ${signals.readiness.missing.join(', ')}` : ''}`,
416
775
  ` Session: ${signals.context.state}${signals.context.suggest ? `, suggest ${signals.context.suggest}` : ''}`,
776
+ ` Plans: ${signals.plan?.state || 'none'}${signals.plan?.suggest ? `, suggest ${signals.plan.suggest}` : ''}`,
417
777
  ` Reviews: ${signals.reviews.count ? `${signals.reviews.count} pending, suggest /scr:editor-review` : 'none'}`,
778
+ ` Review coverage: ${signals.reviewCoverage?.state || 'none'}${signals.reviewCoverage?.suggest ? `, suggest ${signals.reviewCoverage.suggest}` : ''}`,
779
+ ` Notes: ${signals.notes?.count ? `${signals.notes.count} pending, suggest ${signals.notes.suggest}` : 'none'}`,
780
+ ` Tracks: ${signals.tracks?.state || 'none'}${signals.tracks?.suggest ? `, suggest ${signals.tracks.suggest}` : ''}`,
418
781
  ` Translation: ${signals.translation.state}`,
782
+ ` Publishing: ${signals.publishing?.state || 'none'}${signals.publishing?.gaps?.length ? `, gaps ${signals.publishing.gaps.join(', ')}` : ''}`,
419
783
  ` Export: ${signals.export.state}${signals.export.suggest ? `, suggest ${signals.export.suggest}` : ''}`,
420
784
  ` Save: ${signals.save.state}${signals.save.suggest ? `, suggest ${signals.save.suggest}` : ''}`,
421
785
  ].join('\n');
@@ -425,14 +789,31 @@ function formatAutomationStatus(analysis, options = {}) {
425
789
  const trigger = options.trigger || '/scr:next';
426
790
  const localOperation = options.localOperation || 'auto-invoke engine: read-only';
427
791
  const autoInvoked = options.autoInvoked || `${analysis.recommendation.command}: no`;
792
+ const automation = analysis.automation || { mode: 'read-only', spawnCandidates: [], localCandidates: [], manualGates: [] };
793
+ const candidateAgentLines = automation.spawnCandidates.length
794
+ ? automation.spawnCandidates.map((candidate) => `- ${candidate.command}: ${candidate.agents.join(', ')} (${candidate.reason})`)
795
+ : ['- none'];
796
+ const localCandidateLines = automation.localCandidates.length
797
+ ? automation.localCandidates.map((candidate) => `- ${candidate.command}: ${candidate.reason}`)
798
+ : ['- none'];
799
+ const manualGateLines = automation.manualGates.length
800
+ ? automation.manualGates.map((gate) => `- ${gate.command}: ${gate.reason}`)
801
+ : ['- none'];
428
802
  return [
429
803
  'Automation status:',
430
804
  `Trigger: ${trigger}`,
805
+ `Mode: ${automation.mode}`,
431
806
  'Spawned agents:',
432
807
  '- none',
808
+ 'Candidate agents:',
809
+ ...candidateAgentLines,
433
810
  'Local operations:',
434
811
  `- ${localOperation}`,
435
812
  `- state route computed: ${analysis.signals.hasProject ? 'yes' : 'no project'}`,
813
+ 'Candidate local helpers:',
814
+ ...localCandidateLines,
815
+ 'Manual gates:',
816
+ ...manualGateLines,
436
817
  'Auto-invoked:',
437
818
  `- ${autoInvoked}`,
438
819
  `Why: ${analysis.recommendation.reason}`,
@@ -508,12 +889,17 @@ if (require.main === module) {
508
889
  }
509
890
 
510
891
  module.exports = {
892
+ AGENT_ROUTE_POLICIES,
893
+ CATEGORY_ROUTE_POLICIES,
511
894
  DEFAULT_RUNTIME_SUPPORT,
895
+ LOCAL_ROUTE_POLICIES,
896
+ MANUAL_ROUTE_POLICIES,
512
897
  analyzeProject,
513
898
  formatProactiveChecks,
514
899
  formatAutomationStatus,
515
900
  formatRecommendation,
516
901
  formatReport,
902
+ getCommandAutomationPolicy,
517
903
  getRuntimeAgentSupport,
518
904
  listRuntimeAgentSupport,
519
905
  parseCliArgs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scriveno",
3
- "version": "2.0.8",
3
+ "version": "2.0.9",
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,30 +1,26 @@
1
1
  {
2
- "scriveno_version": "2.0.8",
2
+ "scriveno_version": "2.0.9",
3
3
  "work_type": "",
4
4
  "group": "",
5
5
  "command_unit": "",
6
6
  "developer_mode": false,
7
7
  "created_at": "",
8
8
  "updated_at": "",
9
-
10
9
  "autopilot": {
11
10
  "enabled": false,
12
11
  "profile": "guided",
13
12
  "custom_checkpoints": []
14
13
  },
15
-
16
14
  "voice": {
17
15
  "calibrated": false,
18
16
  "last_calibration": null,
19
17
  "drift_threshold": 0.3
20
18
  },
21
-
22
19
  "draft": {
23
20
  "rigor": "standard",
24
21
  "context_profile": "standard",
25
22
  "pitfalls_enabled": true
26
23
  },
27
-
28
24
  "export": {
29
25
  "default_format": "docx_manuscript",
30
26
  "include_front_matter": true,
@@ -36,14 +32,12 @@
36
32
  "margins": "1in"
37
33
  }
38
34
  },
39
-
40
35
  "translation": {
41
36
  "source_language": "en",
42
37
  "target_languages": [],
43
38
  "name_handling": "keep_original",
44
39
  "measurement_system": "source"
45
40
  },
46
-
47
41
  "technical": {
48
42
  "audience_level": "mixed",
49
43
  "prerequisite_knowledge": [],
@@ -52,18 +46,15 @@
52
46
  "source_of_truth": [],
53
47
  "review_mode": "accuracy_first"
54
48
  },
55
-
56
49
  "collaboration": {
57
50
  "tracks_enabled": false,
58
51
  "default_track": "canon"
59
52
  },
60
-
61
53
  "illustration": {
62
54
  "style": null,
63
55
  "cover_enabled": false,
64
56
  "interior_enabled": false
65
57
  },
66
-
67
58
  "git": {
68
59
  "auto_commit": true,
69
60
  "commit_message_style": "descriptive",