@hanna84/mcp-writing 2.10.4 → 2.10.6

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/CHANGELOG.md CHANGED
@@ -4,11 +4,31 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ #### [v2.10.6](https://github.com/hannasdev/mcp-writing.git
8
+ /compare/v2.10.5...v2.10.6)
9
+
10
+ - refactor(index): extract getRuntimeDiagnostics into runtime-diagnostics.js [`#110`](https://github.com/hannasdev/mcp-writing.git
11
+ /pull/110)
12
+
13
+ #### [v2.10.5](https://github.com/hannasdev/mcp-writing.git
14
+ /compare/v2.10.4...v2.10.5)
15
+
16
+ > 27 April 2026
17
+
18
+ - refactor(index): extract workflow catalogue module [`#109`](https://github.com/hannasdev/mcp-writing.git
19
+ /pull/109)
20
+ - Release 2.10.5 [`bee9b2b`](https://github.com/hannasdev/mcp-writing.git
21
+ /commit/bee9b2bee5992cfc69571d95ab1dc281d89d8812)
22
+
7
23
  #### [v2.10.4](https://github.com/hannasdev/mcp-writing.git
8
24
  /compare/v2.10.3...v2.10.4)
9
25
 
26
+ > 27 April 2026
27
+
10
28
  - fix(review-bundles): guard oversized scene_ids in planner [`#108`](https://github.com/hannasdev/mcp-writing.git
11
29
  /pull/108)
30
+ - Release 2.10.4 [`6afb4e5`](https://github.com/hannasdev/mcp-writing.git
31
+ /commit/6afb4e51408ce9a42ade76087f4a2f86a6b13c19)
12
32
 
13
33
  #### [v2.10.3](https://github.com/hannasdev/mcp-writing.git
14
34
  /compare/v2.10.2...v2.10.3)
package/git.js CHANGED
@@ -35,7 +35,7 @@ export function initGitRepository(dirPath) {
35
35
  }
36
36
  return true;
37
37
  } catch (err) {
38
- throw new Error(`Failed to initialize git repository: ${err.message}`);
38
+ throw new Error(`Failed to initialize git repository: ${err.message}`, { cause: err });
39
39
  }
40
40
  }
41
41
 
@@ -98,7 +98,7 @@ export function createSnapshot(dirPath, filePath, sceneId, instruction, options
98
98
  reason: "no changes to commit",
99
99
  };
100
100
  }
101
- throw new Error(`Failed to create snapshot: ${err.message}`);
101
+ throw new Error(`Failed to create snapshot: ${err.message}`, { cause: err });
102
102
  }
103
103
  }
104
104
 
@@ -137,7 +137,7 @@ export function listSnapshots(dirPath, filePath) {
137
137
  if (err.message.includes("your current branch") || err.status === 128) {
138
138
  return [];
139
139
  }
140
- throw new Error(`Failed to list snapshots: ${err.message}`);
140
+ throw new Error(`Failed to list snapshots: ${err.message}`, { cause: err });
141
141
  }
142
142
  }
143
143
 
@@ -163,10 +163,16 @@ export function getSceneProseAtCommit(dirPath, filePath, commitHash) {
163
163
 
164
164
  return content;
165
165
  } catch (err) {
166
+ // ENOENT from execFileSync means the git binary was not found on PATH — not a missing file in the commit.
166
167
  if (err.code === "ENOENT") {
167
- throw new Error(`File not found in commit ${commitHash}`);
168
+ throw new Error("git executable not found; ensure git is installed and on PATH", { cause: err });
168
169
  }
169
- throw new Error(`Failed to retrieve scene prose: ${err.message}`);
170
+ // git exit 128 with "path ... does not exist in" or "bad object" indicates missing path/commit.
171
+ const stderr = err?.stderr ? String(err.stderr) : "";
172
+ if (err.status === 128 && (stderr.includes("does not exist in") || stderr.includes("bad object"))) {
173
+ throw new Error(`File not found in commit ${commitHash}`, { cause: err });
174
+ }
175
+ throw new Error(`Failed to retrieve scene prose: ${err.message}`, { cause: err });
170
176
  }
171
177
  }
172
178
 
package/index.js CHANGED
@@ -25,6 +25,8 @@ import { registerMetadataTools } from "./tools/metadata.js";
25
25
  import { registerReviewBundleTools } from "./tools/review-bundles.js";
26
26
  import { registerStyleguideTools } from "./tools/styleguide.js";
27
27
  import { registerEditingTools } from "./tools/editing.js";
28
+ import { WORKFLOW_CATALOGUE } from "./workflow-catalogue.js";
29
+ import { getRuntimeDiagnostics } from "./runtime-diagnostics.js";
28
30
 
29
31
  const SYNC_DIR = process.env.WRITING_SYNC_DIR ?? "./sync";
30
32
  const DB_PATH = process.env.DB_PATH ?? "./writing.db";
@@ -199,78 +201,16 @@ function generateProposalId() {
199
201
  return `proposal-${randomUUID()}`;
200
202
  }
201
203
 
202
- function getRuntimeDiagnostics() {
203
- const warnings = [];
204
- const recommendations = [];
205
-
206
- if (OWNERSHIP_GUARD_MODE_RAW !== OWNERSHIP_GUARD_MODE) {
207
- warnings.push(
208
- `OWNERSHIP_GUARD_MODE_INVALID: Unsupported OWNERSHIP_GUARD_MODE=${OWNERSHIP_GUARD_MODE_RAW_DISPLAY}. Falling back to 'warn'.`
209
- );
210
- recommendations.push("Set OWNERSHIP_GUARD_MODE to either 'warn' or 'fail'.");
211
- }
212
-
213
- if (SYNC_OWNERSHIP_DIAGNOSTICS.runtime_uid_override_ignored) {
214
- warnings.push("RUNTIME_UID_OVERRIDE_IGNORED: RUNTIME_UID_OVERRIDE is ignored unless NODE_ENV=test or ALLOW_RUNTIME_UID_OVERRIDE=1.");
215
- recommendations.push("Avoid RUNTIME_UID_OVERRIDE in production runtime environments.");
216
- }
217
-
218
- if (SYNC_OWNERSHIP_DIAGNOSTICS.runtime_uid_override_invalid) {
219
- warnings.push("RUNTIME_UID_OVERRIDE_INVALID: RUNTIME_UID_OVERRIDE must be a non-negative integer when enabled.");
220
- recommendations.push("Set RUNTIME_UID_OVERRIDE to a non-negative integer, or unset it.");
221
- }
222
-
223
- if (!SYNC_DIR_WRITABLE) {
224
- warnings.push("SYNC_DIR_READ_ONLY: sync dir is read-only; metadata write-back and prose editing tools are unavailable.");
225
- recommendations.push("Mount WRITING_SYNC_DIR with write access (avoid read-only mounts like ':ro').");
226
- recommendations.push("If running in Docker/OpenClaw, verify volume ownership and permissions for the container user.");
227
- }
228
-
229
- if (SYNC_OWNERSHIP_DIAGNOSTICS.supported && SYNC_OWNERSHIP_DIAGNOSTICS.non_runtime_owned_paths > 0) {
230
- warnings.push(
231
- `OWNERSHIP_MISMATCH: ${SYNC_OWNERSHIP_DIAGNOSTICS.non_runtime_owned_paths} sampled path(s) are not owned by runtime UID ${SYNC_OWNERSHIP_DIAGNOSTICS.runtime_uid}.`
232
- );
233
- recommendations.push(
234
- `Repair ownership once on host: sudo chown -R "$(id -u):$(id -g)" "${SYNC_DIR_ABS}"`
235
- );
236
- recommendations.push(
237
- "For Docker/OpenClaw, run container as host user (compose: user: \"${OPENCLAW_UID:-1000}:${OPENCLAW_GID:-1000}\")."
238
- );
239
- }
240
-
241
- if (OWNERSHIP_GUARD_MODE === "fail" && SYNC_OWNERSHIP_DIAGNOSTICS.runtime_uid === 0) {
242
- warnings.push(
243
- "OWNERSHIP_GUARD_SKIPPED_FOR_ROOT: OWNERSHIP_GUARD_MODE=fail is skipped because runtime UID is 0 (root)."
244
- );
245
- recommendations.push("Prefer running as a non-root host-mapped UID/GID to make ownership guard checks meaningful.");
246
- }
247
-
248
- if (SYNC_OWNERSHIP_DIAGNOSTICS.supported && SYNC_OWNERSHIP_DIAGNOSTICS.root_owned_paths > 0) {
249
- warnings.push(
250
- `ROOT_OWNED_PATHS: ${SYNC_OWNERSHIP_DIAGNOSTICS.root_owned_paths} sampled path(s) are owned by UID 0 (root).`
251
- );
252
- }
253
-
254
- if (!GIT_AVAILABLE) {
255
- warnings.push("GIT_NOT_FOUND: git is not available on PATH; snapshot/edit tools are unavailable.");
256
- recommendations.push("Install git in the runtime image/environment.");
257
- }
258
-
259
- if (GIT_AVAILABLE && SYNC_DIR_WRITABLE && !GIT_ENABLED) {
260
- warnings.push("GIT_DISABLED: git is available but repository snapshot tools are not active.");
261
- recommendations.push("Ensure WRITING_SYNC_DIR points to a writable git repository root, or allow mcp-writing to initialize one.");
262
- }
263
-
264
- if (GIT_AVAILABLE && !SYNC_DIR_WRITABLE) {
265
- recommendations.push("If git reports 'dubious ownership' for mounted repos, add: git config --system --add safe.directory /sync");
266
- }
267
-
268
- recommendations.push("If indexing finds many files without scene_id, run scripts/import.js first for Scrivener Draft exports, then run sync.");
269
-
270
- return { warnings, recommendations };
271
- }
272
-
273
- const RUNTIME_DIAGNOSTICS = getRuntimeDiagnostics();
204
+ const RUNTIME_DIAGNOSTICS = getRuntimeDiagnostics({
205
+ ownershipGuardModeRaw: OWNERSHIP_GUARD_MODE_RAW,
206
+ ownershipGuardMode: OWNERSHIP_GUARD_MODE,
207
+ ownershipGuardModeRawDisplay: OWNERSHIP_GUARD_MODE_RAW_DISPLAY,
208
+ syncDirWritable: SYNC_DIR_WRITABLE,
209
+ syncDirAbs: SYNC_DIR_ABS,
210
+ syncOwnershipDiagnostics: SYNC_OWNERSHIP_DIAGNOSTICS,
211
+ gitAvailable: GIT_AVAILABLE,
212
+ gitEnabled: GIT_ENABLED,
213
+ });
274
214
  if (RUNTIME_DIAGNOSTICS.warnings.length) {
275
215
  process.stderr.write(`[mcp-writing] Runtime diagnostics:\n`);
276
216
  for (const line of RUNTIME_DIAGNOSTICS.warnings) {
@@ -357,102 +297,6 @@ function maxScenesNextStep(matchedCount) {
357
297
  return `Re-run with max_scenes set to at least ${matchedCount}.`;
358
298
  }
359
299
 
360
- const WORKFLOW_CATALOGUE = [
361
- {
362
- id: "first_time_setup",
363
- label: "First-time setup",
364
- use_when: "Connecting to a project for the first time or verifying the runtime is correctly configured.",
365
- steps: [
366
- { tool: "get_runtime_config", note: "Verify sync dir, writability, and git availability." },
367
- { tool: "sync", note: "Index scenes from disk." },
368
- ],
369
- },
370
- {
371
- id: "styleguide_setup_new",
372
- label: "Styleguide setup (new project)",
373
- use_when: "No prose styleguide config exists and you want to create one based on the manuscript's existing conventions.",
374
- steps: [
375
- { tool: "describe_workflows", note: "Check context.scene_count; use that value as max_scenes in the next call." },
376
- { tool: "bootstrap_prose_styleguide_config", note: "Detect dominant conventions. Confirm suggestions with the user before applying." },
377
- { 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." },
378
- { tool: "update_prose_styleguide_config", note: "Apply the fields accepted from bootstrap suggestions." },
379
- ],
380
- },
381
- {
382
- id: "styleguide_drift_check",
383
- label: "Styleguide drift check",
384
- use_when: "A styleguide config exists and you want to check whether recent scenes conform to it.",
385
- steps: [
386
- { tool: "get_prose_styleguide_config", note: "Confirm the currently resolved config." },
387
- { 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." },
388
- { tool: "update_prose_styleguide_config", note: "If drift found and user approves, update config or note the outliers." },
389
- ],
390
- },
391
- {
392
- id: "manuscript_exploration",
393
- label: "Manuscript exploration",
394
- use_when: "Answering questions about the manuscript, finding scenes, or getting an overview.",
395
- steps: [
396
- { tool: "find_scenes", note: "Filter by character, beat, tag, part, chapter, or POV. No filters returns all scenes." },
397
- { tool: "get_scene_prose", note: "Load prose for specific scenes identified by find_scenes." },
398
- { tool: "get_chapter_prose", note: "Load all prose for a chapter. Use sparingly — large chapters can overflow context." },
399
- { tool: "search_metadata", note: "Full-text search across scene metadata fields." },
400
- ],
401
- },
402
- {
403
- id: "prose_editing",
404
- label: "Prose editing",
405
- use_when: "Revising scene prose. All edits require explicit user confirmation before writing.",
406
- steps: [
407
- { tool: "find_scenes", note: "Identify the target scene." },
408
- { tool: "get_scene_prose", note: "Load the current prose." },
409
- { tool: "propose_edit", note: "Stage a revision; returns a diff preview and a proposal_id." },
410
- { tool: "commit_edit", note: "Write the revision after the user confirms. Runs preflight checks before writing." },
411
- { tool: "discard_edit", note: "Reject the revision if the user does not approve." },
412
- ],
413
- },
414
- {
415
- id: "character_management",
416
- label: "Character management",
417
- use_when: "Finding characters, reading their sheets, or updating character details.",
418
- steps: [
419
- { tool: "list_characters", note: "Find character_id values." },
420
- { tool: "get_character_sheet", note: "Read full character details." },
421
- { tool: "create_character_sheet", note: "Create a new character. Requires exactly one of project_id or universe_id." },
422
- { tool: "update_character_sheet", note: "Edit character metadata." },
423
- ],
424
- },
425
- {
426
- id: "place_management",
427
- label: "Place management",
428
- use_when: "Finding locations, reading place sheets, or updating place details.",
429
- steps: [
430
- { tool: "list_places", note: "Find place_id values." },
431
- { tool: "get_place_sheet", note: "Read full place details." },
432
- { tool: "create_place_sheet", note: "Create a new place. Requires exactly one of project_id or universe_id." },
433
- { tool: "update_place_sheet", note: "Edit place metadata." },
434
- ],
435
- },
436
- {
437
- id: "review_bundle",
438
- label: "Review bundle",
439
- use_when: "Preparing a formatted bundle for human review (outline, editorial, or beta read profile).",
440
- steps: [
441
- { tool: "preview_review_bundle", note: "Check which scenes would be included and the estimated size. Requires project_id and profile." },
442
- { tool: "create_review_bundle", note: "Generate the bundle. Requires project_id." },
443
- ],
444
- },
445
- {
446
- id: "async_job_tracking",
447
- label: "Async job tracking",
448
- use_when: "A tool returned a job_id instead of an immediate result (e.g. import_scrivener_sync_async).",
449
- steps: [
450
- { tool: "get_async_job_status", note: "Poll with the job_id until status is 'completed' or 'failed'." },
451
- { tool: "sync", note: "Call after a completed job that modified files on disk." },
452
- ],
453
- },
454
- ];
455
-
456
300
  // ---------------------------------------------------------------------------
457
301
  // MCP server factory
458
302
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "2.10.4",
3
+ "version": "2.10.6",
4
4
  "description": "MCP service for AI-assisted reasoning and editing on long-form fiction projects",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -16,6 +16,8 @@
16
16
  "sync.js",
17
17
  "git.js",
18
18
  "world-entity-templates.js",
19
+ "workflow-catalogue.js",
20
+ "runtime-diagnostics.js",
19
21
  "metadata-lint.js",
20
22
  "scene-character-normalization.js",
21
23
  "review-bundles.js",
@@ -44,7 +46,7 @@
44
46
  "normalize:scene-characters": "node --experimental-sqlite scripts/normalize-scene-characters.mjs",
45
47
  "setup:openclaw-env": "sh scripts/setup-openclaw-env.sh",
46
48
  "release": "release-it",
47
- "lint": "eslint index.js importer.js db.js sync.js metadata-lint.js scripts/ tools/",
49
+ "lint": "eslint *.js scripts/ tools/",
48
50
  "docs": "node scripts/generate-tool-docs.mjs",
49
51
  "lint:metadata": "node scripts/lint-metadata.mjs",
50
52
  "test:unit": "node --experimental-sqlite --test test/unit/*.test.mjs",
@@ -16,7 +16,7 @@ export class ReviewBundlePlanError extends Error {
16
16
 
17
17
  export function normalizeRecipientDisplayName(recipientName) {
18
18
  const normalized = String(recipientName ?? "")
19
- .replace(/[\x00-\x1f\x7f]+/g, " ")
19
+ .replace(/[\x00-\x1f\x7f]+/g, " ") // eslint-disable-line no-control-regex
20
20
  .replace(/\s+/g, " ")
21
21
  .trim()
22
22
  .slice(0, 100);
@@ -7,7 +7,7 @@ import { ReviewBundlePlanError, normalizeRecipientDisplayName } from "./review-b
7
7
  function escapeMarkdown(text) {
8
8
  return String(text ?? "")
9
9
  .replace(/\\/g, "\\\\")
10
- .replace(/([*_`\[\]#])/g, "\\$1");
10
+ .replace(/([*_`[\]#])/g, "\\$1");
11
11
  }
12
12
 
13
13
  function renderBetaNoticeMarkdown({ projectId, recipientName }) {
@@ -0,0 +1,97 @@
1
+ /**
2
+ * getRuntimeDiagnostics
3
+ *
4
+ * Inspects the startup environment and returns { warnings, recommendations }.
5
+ * All inputs are passed explicitly so this module has no side effects and
6
+ * is straightforward to test.
7
+ *
8
+ * @param {object} opts
9
+ * @param {string} opts.ownershipGuardModeRaw Raw env value before normalisation
10
+ * @param {string} opts.ownershipGuardMode Normalised value ("warn" | "fail")
11
+ * @param {string} opts.ownershipGuardModeRawDisplay JSON.stringify of the raw value
12
+ * @param {boolean} opts.syncDirWritable
13
+ * @param {string} opts.syncDirAbs Resolved absolute path shown in messages
14
+ * @param {object} opts.syncOwnershipDiagnostics Result of getSyncOwnershipDiagnostics()
15
+ * @param {boolean} opts.gitAvailable
16
+ * @param {boolean} opts.gitEnabled
17
+ * @returns {{ warnings: string[], recommendations: string[] }}
18
+ */
19
+ export function getRuntimeDiagnostics({
20
+ ownershipGuardModeRaw,
21
+ ownershipGuardMode,
22
+ ownershipGuardModeRawDisplay,
23
+ syncDirWritable,
24
+ syncDirAbs,
25
+ syncOwnershipDiagnostics,
26
+ gitAvailable,
27
+ gitEnabled,
28
+ }) {
29
+ const warnings = [];
30
+ const recommendations = [];
31
+
32
+ if (ownershipGuardModeRaw !== ownershipGuardMode) {
33
+ warnings.push(
34
+ `OWNERSHIP_GUARD_MODE_INVALID: Unsupported OWNERSHIP_GUARD_MODE=${ownershipGuardModeRawDisplay}. Falling back to 'warn'.`
35
+ );
36
+ recommendations.push("Set OWNERSHIP_GUARD_MODE to either 'warn' or 'fail'.");
37
+ }
38
+
39
+ if (syncOwnershipDiagnostics.runtime_uid_override_ignored) {
40
+ warnings.push("RUNTIME_UID_OVERRIDE_IGNORED: RUNTIME_UID_OVERRIDE is ignored unless NODE_ENV=test or ALLOW_RUNTIME_UID_OVERRIDE=1.");
41
+ recommendations.push("Avoid RUNTIME_UID_OVERRIDE in production runtime environments.");
42
+ }
43
+
44
+ if (syncOwnershipDiagnostics.runtime_uid_override_invalid) {
45
+ warnings.push("RUNTIME_UID_OVERRIDE_INVALID: RUNTIME_UID_OVERRIDE must be a non-negative integer when enabled.");
46
+ recommendations.push("Set RUNTIME_UID_OVERRIDE to a non-negative integer, or unset it.");
47
+ }
48
+
49
+ if (!syncDirWritable) {
50
+ warnings.push("SYNC_DIR_READ_ONLY: sync dir is read-only; metadata write-back and prose editing tools are unavailable.");
51
+ recommendations.push("Mount WRITING_SYNC_DIR with write access (avoid read-only mounts like ':ro').");
52
+ recommendations.push("If running in Docker/OpenClaw, verify volume ownership and permissions for the container user.");
53
+ }
54
+
55
+ if (syncOwnershipDiagnostics.supported && syncOwnershipDiagnostics.non_runtime_owned_paths > 0) {
56
+ warnings.push(
57
+ `OWNERSHIP_MISMATCH: ${syncOwnershipDiagnostics.non_runtime_owned_paths} sampled path(s) are not owned by runtime UID ${syncOwnershipDiagnostics.runtime_uid}.`
58
+ );
59
+ recommendations.push(
60
+ `Repair ownership once on host: sudo chown -R "$(id -u):$(id -g)" "${syncDirAbs}"`
61
+ );
62
+ recommendations.push(
63
+ "For Docker/OpenClaw, run container as host user (compose: user: \"${OPENCLAW_UID:-1000}:${OPENCLAW_GID:-1000}\")."
64
+ );
65
+ }
66
+
67
+ if (ownershipGuardMode === "fail" && syncOwnershipDiagnostics.runtime_uid === 0) {
68
+ warnings.push(
69
+ "OWNERSHIP_GUARD_SKIPPED_FOR_ROOT: OWNERSHIP_GUARD_MODE=fail is skipped because runtime UID is 0 (root)."
70
+ );
71
+ recommendations.push("Prefer running as a non-root host-mapped UID/GID to make ownership guard checks meaningful.");
72
+ }
73
+
74
+ if (syncOwnershipDiagnostics.supported && syncOwnershipDiagnostics.root_owned_paths > 0) {
75
+ warnings.push(
76
+ `ROOT_OWNED_PATHS: ${syncOwnershipDiagnostics.root_owned_paths} sampled path(s) are owned by UID 0 (root).`
77
+ );
78
+ }
79
+
80
+ if (!gitAvailable) {
81
+ warnings.push("GIT_NOT_FOUND: git is not available on PATH; snapshot/edit tools are unavailable.");
82
+ recommendations.push("Install git in the runtime image/environment.");
83
+ }
84
+
85
+ if (gitAvailable && syncDirWritable && !gitEnabled) {
86
+ warnings.push("GIT_DISABLED: git is available but repository snapshot tools are not active.");
87
+ recommendations.push("Ensure WRITING_SYNC_DIR points to a writable git repository root, or allow mcp-writing to initialize one.");
88
+ }
89
+
90
+ if (gitAvailable && !syncDirWritable) {
91
+ recommendations.push("If git reports 'dubious ownership' for mounted repos, add: git config --system --add safe.directory /sync");
92
+ }
93
+
94
+ recommendations.push("If indexing finds many files without scene_id, run scripts/import.js first for Scrivener Draft exports, then run sync.");
95
+
96
+ return { warnings, recommendations };
97
+ }
@@ -705,7 +705,7 @@ export function mergeScrivenerProjectMetadata({
705
705
  });
706
706
  }
707
707
 
708
- let didRelocate = false;
708
+ let didRelocate;
709
709
 
710
710
  if (dryRun) {
711
711
  logger(` DRY ${filename}`);
@@ -0,0 +1,95 @@
1
+ export const WORKFLOW_CATALOGUE = [
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.",
6
+ steps: [
7
+ { tool: "get_runtime_config", note: "Verify sync dir, writability, and git availability." },
8
+ { tool: "sync", note: "Index scenes from disk." },
9
+ ],
10
+ },
11
+ {
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.",
15
+ 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." },
20
+ ],
21
+ },
22
+ {
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.",
26
+ 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." },
30
+ ],
31
+ },
32
+ {
33
+ id: "manuscript_exploration",
34
+ label: "Manuscript exploration",
35
+ use_when: "Answering questions about the manuscript, finding scenes, or getting an overview.",
36
+ 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." },
41
+ ],
42
+ },
43
+ {
44
+ id: "prose_editing",
45
+ label: "Prose editing",
46
+ use_when: "Revising scene prose. All edits require explicit user confirmation before writing.",
47
+ 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." },
53
+ ],
54
+ },
55
+ {
56
+ id: "character_management",
57
+ label: "Character management",
58
+ use_when: "Finding characters, reading their sheets, or updating character details.",
59
+ 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." },
64
+ ],
65
+ },
66
+ {
67
+ id: "place_management",
68
+ label: "Place management",
69
+ use_when: "Finding locations, reading place sheets, or updating place details.",
70
+ 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." },
75
+ ],
76
+ },
77
+ {
78
+ id: "review_bundle",
79
+ label: "Review bundle",
80
+ use_when: "Preparing a formatted bundle for human review (outline, editorial, or beta read profile).",
81
+ 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." },
84
+ ],
85
+ },
86
+ {
87
+ id: "async_job_tracking",
88
+ label: "Async job tracking",
89
+ use_when: "A tool returned a job_id instead of an immediate result (e.g. import_scrivener_sync_async).",
90
+ steps: [
91
+ { tool: "get_async_job_status", note: "Poll with the job_id until status is 'completed' or 'failed'." },
92
+ { tool: "sync", note: "Call after a completed job that modified files on disk." },
93
+ ],
94
+ },
95
+ ];