@hanna84/mcp-writing 2.18.0 → 3.0.0

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.
@@ -70,7 +70,7 @@ export function registerSearchTools(s, {
70
70
  // ---- find_scenes ---------------------------------------------------------
71
71
  s.tool(
72
72
  "find_scenes",
73
- "Find scenes by filtering on character, Save the Cat beat, tags, part, chapter, or POV. Returns ordered scene metadata only — no prose. All filters are optional and combinable. Supports pagination via page/page_size and auto-paginates large result sets with total_count. Warns if any matching scenes have stale metadata.",
73
+ "Find scenes by filtering on character, Save the Cat beat, tags, part, chapter, or POV. Returns ordered scene metadata only — no prose. All filters are optional and combinable. Supports pagination via page/page_size and auto-paginates large result sets with total_count. Warns if any matching scenes have stale metadata. Response shape note: always returns a structured envelope (`results`, `total_count`, with pagination fields when paging is active).",
74
74
  {
75
75
  project_id: z.string().optional().describe("Project ID (e.g. 'the-lamb'). Use to scope results to one project."),
76
76
  character: z.string().optional().describe("A character_id (e.g. 'char-mira-nystrom'). Returns only scenes that character appears in. Use list_characters first to find valid IDs."),
@@ -95,11 +95,11 @@ export function registerSearchTools(s, {
95
95
  const params = [];
96
96
 
97
97
  if (character) {
98
- joins.push(`JOIN scene_characters sc ON sc.scene_id = s.scene_id AND sc.character_id = ?`);
98
+ joins.push(`JOIN scene_characters sc ON sc.scene_id = s.scene_id AND sc.project_id = s.project_id AND sc.character_id = ?`);
99
99
  params.push(character);
100
100
  }
101
101
  if (tag) {
102
- joins.push(`JOIN scene_tags st ON st.scene_id = s.scene_id AND st.tag = ?`);
102
+ joins.push(`JOIN scene_tags st ON st.scene_id = s.scene_id AND st.project_id = s.project_id AND st.tag = ?`);
103
103
  params.push(tag);
104
104
  }
105
105
  if (project_id) { conditions.push(`s.project_id = ?`); params.push(project_id); }
@@ -133,8 +133,18 @@ export function registerSearchTools(s, {
133
133
  results: paged.rows,
134
134
  ...paged.meta,
135
135
  warning,
136
+ next_step: staleCount > 0
137
+ ? "Touch stale scenes as you work and run enrich_scene(scene_id, project_id) to recover metadata parity incrementally."
138
+ : undefined,
136
139
  }
137
- : rows;
140
+ : {
141
+ results: rows,
142
+ total_count: rows.length,
143
+ warning,
144
+ next_step: staleCount > 0
145
+ ? "Touch stale scenes as you work and run enrich_scene(scene_id, project_id) to recover metadata parity incrementally."
146
+ : undefined,
147
+ };
138
148
 
139
149
  return {
140
150
  content: [{
@@ -148,15 +158,36 @@ export function registerSearchTools(s, {
148
158
  // ---- get_scene_prose -----------------------------------------------------
149
159
  s.tool(
150
160
  "get_scene_prose",
151
- "Load the full prose text of a single scene. Use this for close reading, continuity checks, or when you need the actual writing. For overview or filtering, use find_scenes instead — it is much cheaper. Optionally retrieve a past version from git history.",
161
+ "Load the full prose text of a single scene. Use this for close reading, continuity checks, or when you need the actual writing. For overview or filtering, use find_scenes instead — it is much cheaper. Optionally retrieve a past version from git history. If scene IDs are reused across projects, omitting project_id returns CONFLICT with candidate project_ids.",
152
162
  {
153
163
  scene_id: z.string().describe("The scene_id to retrieve (e.g. 'sc-001-prologue'). Get this from find_scenes or get_arc."),
164
+ project_id: z.string().optional().describe("Optional project ID to disambiguate duplicate scene IDs across projects."),
154
165
  commit: z.string().optional().describe("Optional git commit hash to retrieve a past version. Use list_snapshots to find valid hashes. If omitted, returns the current prose."),
155
166
  },
156
- async ({ scene_id, commit }) => {
157
- const scene = db.prepare(`SELECT file_path, metadata_stale FROM scenes WHERE scene_id = ?`).get(scene_id);
158
- if (!scene) {
159
- return errorResponse("NOT_FOUND", `Scene '${scene_id}' not found. Run sync() if you just added it.`);
167
+ async ({ scene_id, project_id, commit }) => {
168
+ let scene;
169
+ if (project_id) {
170
+ scene = db.prepare(`SELECT file_path, metadata_stale, project_id FROM scenes WHERE scene_id = ? AND project_id = ?`).get(scene_id, project_id);
171
+ if (!scene) {
172
+ return errorResponse("NOT_FOUND", `Scene '${scene_id}' not found in project '${project_id}'. Run sync() if you just added it.`, {
173
+ next_step: "Run sync() to refresh the index, then call find_scenes with project_id to locate the scene by current metadata.",
174
+ });
175
+ }
176
+ } else {
177
+ const scenes = db.prepare(`SELECT file_path, metadata_stale, project_id FROM scenes WHERE scene_id = ? ORDER BY project_id`).all(scene_id);
178
+ if (scenes.length === 0) {
179
+ return errorResponse("NOT_FOUND", `Scene '${scene_id}' not found. Run sync() if you just added it.`, {
180
+ next_step: "Run sync() to refresh the index, then call find_scenes to locate the scene by current metadata.",
181
+ });
182
+ }
183
+ if (scenes.length > 1) {
184
+ return errorResponse(
185
+ "CONFLICT",
186
+ `Scene ID '${scene_id}' exists in multiple projects. Provide project_id to disambiguate.`,
187
+ { scene_id, project_ids: scenes.map(s => s.project_id) }
188
+ );
189
+ }
190
+ scene = scenes[0];
160
191
  }
161
192
  try {
162
193
  let rawContent;
@@ -170,10 +201,18 @@ export function registerSearchTools(s, {
170
201
 
171
202
  const { content: prose } = matter(rawContent);
172
203
  const versionNote = commit ? `\n\n(Retrieved from commit: ${commit})` : "";
173
- const warning = scene.metadata_stale && !commit
174
- ? `\n\n⚠️ Metadata for this scene may be stale — prose has changed since last enrichment.`
175
- : "";
176
- return { content: [{ type: "text", text: prose.trim() + versionNote + warning }] };
204
+ return {
205
+ content: [{
206
+ type: "text",
207
+ text: prose.trim() + versionNote,
208
+ }],
209
+ structuredContent: scene.metadata_stale && !commit
210
+ ? {
211
+ warning: "Metadata for this scene may be stale — prose has changed since last enrichment.",
212
+ next_step: `Run enrich_scene with scene_id ${scene_id} and project_id ${scene.project_id} after this read to recover metadata parity for this scene.`,
213
+ }
214
+ : undefined,
215
+ };
177
216
  } catch (err) {
178
217
  if (err.code === "ENOENT") {
179
218
  return errorResponse(
@@ -221,17 +260,24 @@ export function registerSearchTools(s, {
221
260
  }
222
261
  }
223
262
 
224
- const warning = truncated
225
- ? `\n\n⚠️ Chapter has ${allScenes.length} scenes only the first ${MAX_CHAPTER_SCENES} were loaded. Set MAX_CHAPTER_SCENES to increase this limit.`
226
- : "";
227
- return { content: [{ type: "text", text: parts.join("\n\n---\n\n") + warning }] };
263
+ return {
264
+ content: [{ type: "text", text: parts.join("\n\n---\n\n") }],
265
+ structuredContent: {
266
+ ...(truncated
267
+ ? { warning: `Chapter has ${allScenes.length} scenes — only the first ${MAX_CHAPTER_SCENES} were loaded. Set MAX_CHAPTER_SCENES to increase this limit.` }
268
+ : {}),
269
+ next_step: truncated
270
+ ? "Narrow with find_scenes and inspect key scenes individually with get_scene_prose before expanding chapter scope."
271
+ : "If you only need a subset, switch to find_scenes + get_scene_prose for tighter context control.",
272
+ },
273
+ };
228
274
  }
229
275
  );
230
276
 
231
277
  // ---- get_arc -------------------------------------------------------------
232
278
  s.tool(
233
279
  "get_arc",
234
- "Get every scene a character appears in, ordered by part/chapter/position. Returns scene metadata only — no prose. Use this to trace a character's arc through the story. Supports pagination via page/page_size and auto-paginates large result sets with total_count. Call list_characters first to get the character_id.",
280
+ "Get every scene a character appears in, ordered by part/chapter/position. Returns scene metadata only — no prose. Use this as the primary structural entry point when the question is about a character's progression through the manuscript. Supports pagination via page/page_size and auto-paginates large result sets with total_count. Use list_characters only when you need help finding a character_id. Response shape note: always returns a structured envelope (`results`, `total_count`, with pagination fields when paging is active).",
235
281
  {
236
282
  character_id: z.string().describe("The character_id to trace (e.g. 'char-mira-nystrom'). Use list_characters to find valid IDs."),
237
283
  project_id: z.string().optional().describe("Limit to a specific project (e.g. 'the-lamb')."),
@@ -244,7 +290,7 @@ export function registerSearchTools(s, {
244
290
  s.scene_change, s.causality, s.stakes, s.scene_functions,
245
291
  s.save_the_cat_beat, s.timeline_position, s.story_time, s.pov, s.metadata_stale
246
292
  FROM scenes s
247
- JOIN scene_characters sc ON sc.scene_id = s.scene_id
293
+ JOIN scene_characters sc ON sc.scene_id = s.scene_id AND sc.project_id = s.project_id
248
294
  WHERE sc.character_id = ?
249
295
  `;
250
296
  const params = [character_id];
@@ -272,8 +318,18 @@ export function registerSearchTools(s, {
272
318
  results: paged.rows,
273
319
  ...paged.meta,
274
320
  warning,
321
+ next_step: staleCount > 0
322
+ ? "Touch stale scenes as you work and run enrich_scene(scene_id, project_id) to recover metadata parity incrementally."
323
+ : undefined,
275
324
  }
276
- : rows;
325
+ : {
326
+ results: rows,
327
+ total_count: rows.length,
328
+ warning,
329
+ next_step: staleCount > 0
330
+ ? "Touch stale scenes as you work and run enrich_scene(scene_id, project_id) to recover metadata parity incrementally."
331
+ : undefined,
332
+ };
277
333
 
278
334
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
279
335
  }
@@ -282,7 +338,7 @@ export function registerSearchTools(s, {
282
338
  // ---- list_characters -----------------------------------------------------
283
339
  s.tool(
284
340
  "list_characters",
285
- "List all indexed characters with their character_id, name, role, and arc_summary. Call this first whenever you need to filter scenes by character or look up a character sheet it gives you the character_id values required by other tools.",
341
+ "List indexed characters with their character_id, name, role, and arc_summary. Use this mainly as a lookup and disambiguation helper when you need to find a character_id for a broader reasoning task. Response shape note: returns a structured envelope (`results`, `total_count`).",
286
342
  {
287
343
  project_id: z.string().optional().describe("Limit to a specific project (e.g. 'the-lamb')."),
288
344
  universe_id: z.string().optional().describe("Limit to a specific universe (if using cross-project world-building)."),
@@ -300,21 +356,31 @@ export function registerSearchTools(s, {
300
356
  if (rows.length === 0) {
301
357
  return errorResponse("NO_RESULTS", "No characters found.");
302
358
  }
303
- return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
359
+ return {
360
+ content: [{
361
+ type: "text",
362
+ text: JSON.stringify({
363
+ results: rows,
364
+ total_count: rows.length,
365
+ }, null, 2),
366
+ }],
367
+ };
304
368
  }
305
369
  );
306
370
 
307
371
  // ---- get_character_sheet -------------------------------------------------
308
372
  s.tool(
309
373
  "get_character_sheet",
310
- "Get full character details: role, arc_summary, traits, the canonical sheet content, and any adjacent support notes when the character uses a folder-based layout. Use list_characters first to get the character_id.",
374
+ "Get full character details: role, arc_summary, traits, the canonical sheet content, and any adjacent support notes when the character uses a folder-based layout. Use this when the reasoning task needs the character's canonical profile rather than only their scene progression.",
311
375
  {
312
376
  character_id: z.string().describe("The character_id to look up (e.g. 'char-sebastian'). Use list_characters to find valid IDs."),
313
377
  },
314
378
  async ({ character_id }) => {
315
379
  const character = db.prepare(`SELECT * FROM characters WHERE character_id = ?`).get(character_id);
316
380
  if (!character) {
317
- return errorResponse("NOT_FOUND", `Character '${character_id}' not found.`);
381
+ return errorResponse("NOT_FOUND", `Character '${character_id}' not found.`, {
382
+ next_step: "Call list_characters to find valid character_id values, then retry get_character_sheet or get_arc.",
383
+ });
318
384
  }
319
385
 
320
386
  const traits = db.prepare(`SELECT trait FROM character_traits WHERE character_id = ?`)
@@ -336,6 +402,7 @@ export function registerSearchTools(s, {
336
402
  traits,
337
403
  notes: notes || undefined,
338
404
  supporting_notes: supportingNotes.length ? supportingNotes : undefined,
405
+ next_step: "Use get_arc with this character_id to trace scene-level progression, then open specific scenes with get_scene_prose when prose evidence is needed.",
339
406
  };
340
407
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
341
408
  }
@@ -344,7 +411,7 @@ export function registerSearchTools(s, {
344
411
  // ---- list_places ---------------------------------------------------------
345
412
  s.tool(
346
413
  "list_places",
347
- "List all indexed places with their place_id and name. Use this to find place_id values for scene filtering or to get an overview of the story's locations.",
414
+ "List indexed places with their place_id and name. Use this mainly as a lookup and disambiguation helper when place context becomes relevant to the current reasoning task. Response shape note: returns a structured envelope (`results`, `total_count`).",
348
415
  {
349
416
  project_id: z.string().optional().describe("Limit to a specific project (e.g. 'the-lamb')."),
350
417
  universe_id: z.string().optional().describe("Limit to a specific universe."),
@@ -362,21 +429,31 @@ export function registerSearchTools(s, {
362
429
  if (rows.length === 0) {
363
430
  return errorResponse("NO_RESULTS", "No places found.");
364
431
  }
365
- return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
432
+ return {
433
+ content: [{
434
+ type: "text",
435
+ text: JSON.stringify({
436
+ results: rows,
437
+ total_count: rows.length,
438
+ }, null, 2),
439
+ }],
440
+ };
366
441
  }
367
442
  );
368
443
 
369
444
  // ---- get_place_sheet -----------------------------------------------------
370
445
  s.tool(
371
446
  "get_place_sheet",
372
- "Get full place details: associated_characters, tags, the canonical sheet content, and any adjacent support notes when the place uses a folder-based layout. Use list_places first to get the place_id.",
447
+ "Get full place details: associated_characters, tags, the canonical sheet content, and any adjacent support notes when the place uses a folder-based layout. Use this when the current scene or question makes the place itself materially relevant.",
373
448
  {
374
449
  place_id: z.string().describe("The place_id to look up (e.g. 'place-harbor-district'). Use list_places to find valid IDs."),
375
450
  },
376
451
  async ({ place_id }) => {
377
452
  const place = db.prepare(`SELECT * FROM places WHERE place_id = ?`).get(place_id);
378
453
  if (!place) {
379
- return errorResponse("NOT_FOUND", `Place '${place_id}' not found.`);
454
+ return errorResponse("NOT_FOUND", `Place '${place_id}' not found.`, {
455
+ next_step: "Call list_places to find valid place_id values, then retry get_place_sheet.",
456
+ });
380
457
  }
381
458
 
382
459
  let notes = "";
@@ -403,6 +480,7 @@ export function registerSearchTools(s, {
403
480
  tags: tags.length ? tags : undefined,
404
481
  notes: notes || undefined,
405
482
  supporting_notes: supportingNotes.length ? supportingNotes : undefined,
483
+ next_step: "Use find_scenes with related filters to locate scenes where this place matters, then open targeted scenes with get_scene_prose when prose context is needed.",
406
484
  };
407
485
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
408
486
  }
@@ -445,7 +523,15 @@ export function registerSearchTools(s, {
445
523
  ORDER BY rank
446
524
  `).all(query);
447
525
 
448
- return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
526
+ return {
527
+ content: [{
528
+ type: "text",
529
+ text: JSON.stringify({
530
+ results: rows,
531
+ total_count: rows.length,
532
+ }, null, 2),
533
+ }],
534
+ };
449
535
  }
450
536
 
451
537
  const safePageSize = Math.max(1, page_size ?? DEFAULT_METADATA_PAGE_SIZE);
@@ -480,7 +566,7 @@ export function registerSearchTools(s, {
480
566
  // ---- search_reference ----------------------------------------------------
481
567
  s.tool(
482
568
  "search_reference",
483
- "Full-text search across indexed reference document titles, summaries, and tags. Use this to discover world-building notes, continuity references, research docs, and other reference material without loading full file contents.",
569
+ "Full-text search across indexed reference document titles, summaries, and tags. Use this to discover world-building notes, continuity references, research docs, and other reference material without loading full file contents. Response shape note: returns a structured envelope (`results`, `total_count`).",
484
570
  {
485
571
  query: z.string().describe("Search terms (e.g. 'vampirism' or 'blood replacement'). FTS5 syntax supported."),
486
572
  type: z.string().optional().describe("Optional reference type filter (for example: 'world', 'continuity', 'research', 'style')."),
@@ -534,7 +620,15 @@ export function registerSearchTools(s, {
534
620
  return errorResponse("NO_RESULTS", "No reference documents matched the provided filters.");
535
621
  }
536
622
 
537
- return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
623
+ return {
624
+ content: [{
625
+ type: "text",
626
+ text: JSON.stringify({
627
+ results: rows,
628
+ total_count: rows.length,
629
+ }, null, 2),
630
+ }],
631
+ };
538
632
  }
539
633
  );
540
634
 
@@ -695,7 +789,7 @@ export function registerSearchTools(s, {
695
789
  // ---- list_threads --------------------------------------------------------
696
790
  s.tool(
697
791
  "list_threads",
698
- "List all subplot/storyline threads for a project. Returns a structured JSON envelope with results and total_count. Use this to discover valid thread_id values before calling get_thread_arc or upsert_thread_link. Supports pagination via page/page_size.",
792
+ "List subplot/storyline threads for a project. Returns a structured JSON envelope with results and total_count. Use this mainly as a lookup and disambiguation helper before deeper thread reasoning with get_thread_arc. Supports pagination via page/page_size.",
699
793
  {
700
794
  project_id: z.string().describe("Project ID."),
701
795
  page: z.number().int().min(1).optional().describe("Optional page number for paginated responses (1-based)."),
@@ -709,11 +803,13 @@ export function registerSearchTools(s, {
709
803
  project_id,
710
804
  results: paged.rows,
711
805
  ...paged.meta,
806
+ next_step: "Use a thread_id from results with get_thread_arc to inspect storyline progression across scenes.",
712
807
  }
713
808
  : {
714
809
  project_id,
715
810
  results: rows,
716
811
  total_count: rows.length,
812
+ next_step: "Use a thread_id from results with get_thread_arc to inspect storyline progression across scenes.",
717
813
  };
718
814
 
719
815
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
@@ -723,7 +819,7 @@ export function registerSearchTools(s, {
723
819
  // ---- get_thread_arc ------------------------------------------------------
724
820
  s.tool(
725
821
  "get_thread_arc",
726
- "Get ordered scene metadata for all scenes belonging to a thread, including the per-thread beat. Returns a structured JSON envelope with thread metadata, results, and total_count. Use list_threads first to find a valid thread_id, then call get_scene_prose for close reading of specific scenes. Supports pagination via page/page_size.",
822
+ "Get ordered scene metadata for all scenes belonging to a thread, including the per-thread beat. Returns a structured JSON envelope with thread metadata, results, and total_count. Use this when the question is about subplot movement, continuity, or recurring storyline structure across scenes. Supports pagination via page/page_size.",
727
823
  {
728
824
  thread_id: z.string().describe("Thread ID."),
729
825
  page: z.number().int().min(1).optional().describe("Optional page number for paginated responses (1-based)."),
@@ -739,7 +835,7 @@ export function registerSearchTools(s, {
739
835
  SELECT s.scene_id, s.project_id, s.part, s.chapter, s.chapter_title, s.title, s.logline,
740
836
  st.beat AS thread_beat, s.timeline_position, s.story_time, s.metadata_stale
741
837
  FROM scenes s
742
- JOIN scene_threads st ON st.scene_id = s.scene_id AND st.thread_id = ?
838
+ JOIN scene_threads st ON st.scene_id = s.scene_id AND st.project_id = s.project_id AND st.thread_id = ?
743
839
  ORDER BY s.part, s.chapter, s.timeline_position
744
840
  `).all(thread_id);
745
841
  const staleCount = rows.filter(r => r.metadata_stale).length;
@@ -752,12 +848,18 @@ export function registerSearchTools(s, {
752
848
  results: paged.rows,
753
849
  ...paged.meta,
754
850
  warning,
851
+ next_step: staleCount > 0
852
+ ? "Touch stale scenes as you work and run enrich_scene(scene_id, project_id) to recover metadata parity incrementally."
853
+ : undefined,
755
854
  }
756
855
  : {
757
856
  thread,
758
857
  results: rows,
759
858
  total_count: rows.length,
760
859
  warning,
860
+ next_step: staleCount > 0
861
+ ? "Touch stale scenes as you work and run enrich_scene(scene_id, project_id) to recover metadata parity incrementally."
862
+ : undefined,
761
863
  };
762
864
 
763
865
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
@@ -855,12 +957,12 @@ export function registerSearchTools(s, {
855
957
  if (!loadedSceneEntitiesFromMetadata) {
856
958
  // Fallback for scenes without readable indexed file paths.
857
959
  characterIds = db.prepare(`
858
- SELECT character_id FROM scene_characters WHERE scene_id = ?
859
- `).all(scene_id).map((row) => row.character_id);
960
+ SELECT character_id FROM scene_characters WHERE scene_id = ? AND project_id = ?
961
+ `).all(scene_id, resolvedProjectId).map((row) => row.character_id);
860
962
 
861
963
  placeIds = db.prepare(`
862
- SELECT place_id FROM scene_places WHERE scene_id = ?
863
- `).all(scene_id).map((row) => row.place_id);
964
+ SELECT place_id FROM scene_places WHERE scene_id = ? AND project_id = ?
965
+ `).all(scene_id, resolvedProjectId).map((row) => row.place_id);
864
966
  }
865
967
 
866
968
  // Get explicit scene → reference links already present
package/src/tools/sync.js CHANGED
@@ -28,6 +28,9 @@ export function registerSyncTools(s, {
28
28
  s.tool("sync", "Re-scan the sync folder and update the scene/character/place index from disk. Call this after making edits in Scrivener or updating sidecar files outside the MCP.", {}, async () => {
29
29
  const result = syncAll(db, SYNC_DIR, { writable: SYNC_DIR_WRITABLE });
30
30
  const parts = [`Sync complete. ${result.indexed} scenes indexed. ${result.staleMarked} scenes marked stale.`];
31
+ if (result.staleMarked > 0) {
32
+ parts.push(`Next step: when you touch one of those scenes, run enrich_scene(scene_id, project_id) to recover metadata parity incrementally.`);
33
+ }
31
34
  if (result.sidecarsMigrated) parts.push(`${result.sidecarsMigrated} sidecar(s) auto-generated from frontmatter.`);
32
35
  if (result.skipped) {
33
36
  parts.push(`${result.skipped} file(s) skipped (no scene_id).`);
@@ -1,86 +1,116 @@
1
1
  export const WORKFLOW_CATALOGUE = [
2
2
  {
3
- id: "first_time_setup",
4
- label: "First-time setup",
5
- use_when: "Connecting to a project for the first time or verifying the runtime is correctly configured.",
3
+ id: "question_driven_discovery",
4
+ label: "Find scenes for a manuscript question",
5
+ use_when: "Start here for most sessions: when the user has a manuscript question, you need to narrow scope, or you are not yet sure which scene matters.",
6
6
  steps: [
7
- { tool: "get_runtime_config", note: "Verify sync dir, writability, and git availability." },
8
- { tool: "sync", note: "Index scenes from disk." },
7
+ { tool: "find_scenes", note: "Use structured metadata filters first when the question already suggests characters, beats, tags, parts, chapters, or POV." },
8
+ { tool: "search_metadata", note: "Use this when the question is thematic, fuzzy, or keyword-driven rather than cleanly filterable." },
9
+ { tool: "get_scene_prose", note: "Escalate to prose only after likely scenes have been identified and metadata is no longer enough." },
10
+ { tool: "flag_scene", note: "Use only when the current task naturally leads to recording a follow-up note for later editorial attention." },
9
11
  ],
10
12
  },
11
13
  {
12
- id: "styleguide_setup_new",
13
- label: "Styleguide setup (new project)",
14
- use_when: "No prose styleguide config exists and you want to create one based on the manuscript's existing conventions.",
14
+ id: "targeted_scene_reading",
15
+ label: "Inspect prose for a likely scene",
16
+ use_when: "Use this after metadata has narrowed the space and the user needs details, nuance, continuity, tone, pacing, or other evidence only prose can confirm.",
15
17
  steps: [
16
- { tool: "describe_workflows", note: "Check context.scene_count; use that value as max_scenes in the next call." },
17
- { tool: "bootstrap_prose_styleguide_config", note: "Detect dominant conventions. Confirm suggestions with the user before applying." },
18
- { tool: "setup_prose_styleguide_config", note: "Only if ALL context.styleguide_exists fields are false a config at any scope is sufficient. Create at project_root scope (requires project_id and language e.g. 'english_us'), or sync_root if no project_id is known." },
19
- { tool: "update_prose_styleguide_config", note: "Apply the fields accepted from bootstrap suggestions." },
18
+ { tool: "find_scenes", note: "Use metadata discovery first if the target scene is not already known." },
19
+ { tool: "get_scene_prose", note: "Load the specific scene that matters once you have a likely target." },
20
+ { tool: "get_chapter_prose", note: "Escalate only when the question cannot be answered scene-by-scene and chapter-wide prose context is truly required." },
21
+ { tool: "list_scene_references", note: "Use when linked references are relevant to understanding the scene in context." },
20
22
  ],
21
23
  },
22
24
  {
23
- id: "styleguide_drift_check",
24
- label: "Styleguide drift check",
25
- use_when: "A styleguide config exists and you want to check whether recent scenes conform to it.",
25
+ id: "safe_scene_revision",
26
+ label: "Revise a scene safely",
27
+ use_when: "Use when the next meaningful step is changing prose rather than continuing discovery or inspection.",
26
28
  steps: [
27
- { tool: "get_prose_styleguide_config", note: "Confirm the currently resolved config." },
28
- { tool: "check_prose_styleguide_drift", note: "Detect non-conforming scenes. Pass project_id from context.project_id and set max_scenes from context.scene_count." },
29
- { tool: "update_prose_styleguide_config", note: "If drift found and user approves, update config or note the outliers." },
29
+ { tool: "find_scenes", note: "Identify the target scene if the user has not already narrowed to a specific scene_id." },
30
+ { tool: "get_scene_prose", note: "Read the current prose before proposing a revision." },
31
+ { tool: "propose_edit", note: "Stage a revision and review the diff preview with the user before writing anything." },
32
+ { tool: "commit_edit", note: "Apply the revision only after explicit user approval." },
33
+ { tool: "discard_edit", note: "Use when the proposed change should not be applied." },
34
+ ],
35
+ },
36
+ {
37
+ id: "character_understanding",
38
+ label: "Understand a character in context",
39
+ use_when: "Use when the user wants to understand a character's path through the manuscript or needs the character's canonical profile in support of a story question.",
40
+ steps: [
41
+ { tool: "get_arc", note: "Use as the primary structural entry point when the question is about the character's progression across scenes." },
42
+ { tool: "get_character_sheet", note: "Use when you need the canonical character profile, traits, notes, or supporting material." },
43
+ { tool: "list_characters", note: "Use only as a helper to find or disambiguate character_id values." },
44
+ ],
45
+ },
46
+ {
47
+ id: "place_understanding",
48
+ label: "Understand a place in context",
49
+ use_when: "Use when the current scene involves a place that matters, or when the user is asking directly about a location's role in the manuscript.",
50
+ steps: [
51
+ { tool: "get_place_sheet", note: "Use when the place itself is part of the reasoning task and you need its canonical profile or notes." },
52
+ { tool: "find_scenes", note: "Use to locate scenes where the place is likely to matter through tags, chapter context, or related story structure." },
53
+ { tool: "list_places", note: "Use only as a helper to find or disambiguate place_id values." },
30
54
  ],
31
55
  },
32
56
  {
33
- id: "manuscript_exploration",
34
- label: "Manuscript exploration",
35
- use_when: "Answering questions about the manuscript, finding scenes, or getting an overview.",
57
+ id: "thread_understanding",
58
+ label: "Understand a thread or arc in context",
59
+ use_when: "Use when the question is about progression, continuity, subplot movement, or recurring storyline structure across scenes.",
36
60
  steps: [
37
- { tool: "find_scenes", note: "Filter by character, beat, tag, part, chapter, or POV. No filters returns all scenes." },
38
- { tool: "get_scene_prose", note: "Load prose for specific scenes identified by find_scenes." },
39
- { tool: "get_chapter_prose", note: "Load all prose for a chapter. Use sparingly large chapters can overflow context." },
40
- { tool: "search_metadata", note: "Full-text search across scene metadata fields." },
61
+ { tool: "get_thread_arc", note: "Use when the storyline or subplot is already identified and you need its ordered scene progression." },
62
+ { tool: "list_threads", note: "Use only as a helper to find or disambiguate thread_id values." },
63
+ { tool: "get_arc", note: "Use when the thread question is really a character-progression question and character context is the better structural entry point." },
41
64
  ],
42
65
  },
43
66
  {
44
- id: "prose_editing",
45
- label: "Prose editing",
46
- use_when: "Revising scene prose. All edits require explicit user confirmation before writing.",
67
+ id: "parity_recovery",
68
+ label: "Recover metadata parity",
69
+ use_when: "Use when new material has been added, sync/import reveals metadata gaps, or normal work touches scenes or documents with weak, stale, or missing metadata support.",
47
70
  steps: [
48
- { tool: "find_scenes", note: "Identify the target scene." },
49
- { tool: "get_scene_prose", note: "Load the current prose." },
50
- { tool: "propose_edit", note: "Stage a revision; returns a diff preview and a proposal_id." },
51
- { tool: "commit_edit", note: "Write the revision after the user confirms. Runs preflight checks before writing." },
52
- { tool: "discard_edit", note: "Reject the revision if the user does not approve." },
71
+ { tool: "sync", note: "Refresh the index and use the result as the main signal that material has changed or parity may need attention." },
72
+ { tool: "enrich_scene", note: "Use for lightweight opportunistic recovery when the current task is already touching a specific low-parity scene." },
73
+ { tool: "enrich_scene_characters_batch", note: "Use when recovery scope is broad enough to justify focused catch-up work; prefer dry_run first." },
74
+ { tool: "suggest_scene_references", note: "Use when low parity is specifically about missing scene-to-reference relationships." },
53
75
  ],
54
76
  },
55
77
  {
56
- id: "character_management",
57
- label: "Character management",
58
- use_when: "Finding characters, reading their sheets, or updating character details.",
78
+ id: "review_preparation",
79
+ label: "Prepare material for human review",
80
+ use_when: "Use when the task has shifted from reasoning or revising into packaging material for editors, collaborators, or beta readers.",
59
81
  steps: [
60
- { tool: "list_characters", note: "Find character_id values." },
61
- { tool: "get_character_sheet", note: "Read full character details." },
62
- { tool: "create_character_sheet", note: "Create a new character. Requires exactly one of project_id or universe_id." },
63
- { tool: "update_character_sheet", note: "Edit character metadata." },
82
+ { tool: "preview_review_bundle", note: "Check scope, warnings, and planned outputs before generating anything." },
83
+ { tool: "create_review_bundle", note: "Generate the review artifact once scope and warnings have been reviewed." },
64
84
  ],
65
85
  },
66
86
  {
67
- id: "place_management",
68
- label: "Place management",
69
- use_when: "Finding locations, reading place sheets, or updating place details.",
87
+ id: "first_time_setup",
88
+ label: "Connect and verify a project",
89
+ use_when: "Use when connecting to a project for the first time or when runtime configuration needs to be verified before normal workflows begin.",
70
90
  steps: [
71
- { tool: "list_places", note: "Find place_id values." },
72
- { tool: "get_place_sheet", note: "Read full place details." },
73
- { tool: "create_place_sheet", note: "Create a new place. Requires exactly one of project_id or universe_id." },
74
- { tool: "update_place_sheet", note: "Edit place metadata." },
91
+ { tool: "get_runtime_config", note: "Verify sync dir, writability, and git availability." },
92
+ { tool: "sync", note: "Index scenes from disk so the main manuscript workflows can operate." },
75
93
  ],
76
94
  },
77
95
  {
78
- id: "review_bundle",
79
- label: "Review bundle",
80
- use_when: "Preparing a formatted bundle for human review (outline, editorial, or beta read profile).",
96
+ id: "styleguide_setup_new",
97
+ label: "Set up a prose styleguide",
98
+ use_when: "Use only when styleguide configuration is intentionally part of the task and no suitable config exists yet.",
81
99
  steps: [
82
- { tool: "preview_review_bundle", note: "Check which scenes would be included and the estimated size. Requires project_id and profile." },
83
- { tool: "create_review_bundle", note: "Generate the bundle. Requires project_id." },
100
+ { tool: "describe_workflows", note: "Check context.scene_count; use that value as max_scenes in the next call." },
101
+ { tool: "bootstrap_prose_styleguide_config", note: "Detect dominant conventions. Confirm suggestions with the user before applying." },
102
+ { tool: "setup_prose_styleguide_config", note: "Only if ALL context.styleguide_exists fields are false — a config at any scope is sufficient. Create at project_root scope (requires project_id and language e.g. 'english_us'), or sync_root if no project_id is known." },
103
+ { tool: "update_prose_styleguide_config", note: "Apply the fields accepted from bootstrap suggestions." },
104
+ ],
105
+ },
106
+ {
107
+ id: "styleguide_drift_check",
108
+ label: "Check styleguide drift",
109
+ use_when: "Use only when styleguide conformance is intentionally the task and a styleguide config already exists.",
110
+ steps: [
111
+ { tool: "get_prose_styleguide_config", note: "Confirm the currently resolved config." },
112
+ { tool: "check_prose_styleguide_drift", note: "Detect non-conforming scenes. Pass project_id from context.project_id and set max_scenes from context.scene_count." },
113
+ { tool: "update_prose_styleguide_config", note: "If drift found and user approves, update config or note the outliers." },
84
114
  ],
85
115
  },
86
116
  {