dsh-openwrite 0.2.18 → 0.2.20

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.
Files changed (45) hide show
  1. package/README.md +22 -5
  2. package/docs/ISSUE_76_REPAIRS.md +61 -0
  3. package/docs/RELEASE_ACCEPTANCE.md +10 -1
  4. package/package.json +1 -1
  5. package/packages/openwrite-bridge/lib/client.d.ts +1 -0
  6. package/packages/openwrite-bridge/lib/client.js +26 -4
  7. package/packages/openwrite-bridge/lib/contracts-generated.d.ts +1 -1
  8. package/packages/openwrite-bridge/lib/contracts-generated.js +1 -2
  9. package/packages/openwrite-bridge/lib/domain.js +2 -0
  10. package/packages/openwrite-bridge/lib/index.js +6 -6
  11. package/packages/openwrite-bridge/lib/managed-runtime.js +37 -37
  12. package/packages/openwrite-bridge/lib/tools.js +198 -32
  13. package/packages/studio-panel/lib/client.js +488 -488
  14. package/presets/openwrite/agent.cordis.yml +10 -2
  15. package/presets/openwrite/skills/canon/SKILL.md +2 -0
  16. package/presets/openwrite/skills/dialoguequality/SKILL.md +2 -0
  17. package/presets/openwrite/skills/dog-delivery-query/SKILL.md +2 -0
  18. package/presets/openwrite/skills/dog-import-query/SKILL.md +2 -0
  19. package/presets/openwrite/skills/dog-review-query/SKILL.md +2 -0
  20. package/presets/openwrite/skills/export-book/SKILL.md +2 -0
  21. package/presets/openwrite/skills/foreshadow/SKILL.md +2 -0
  22. package/presets/openwrite/skills/foreshadowing-system/SKILL.md +2 -0
  23. package/presets/openwrite/skills/learn/SKILL.md +2 -0
  24. package/presets/openwrite/skills/novel-creator/SKILL.md +2 -0
  25. package/presets/openwrite/skills/novel-reviewer/SKILL.md +2 -0
  26. package/presets/openwrite/skills/oh-story-deslop/SKILL.md +2 -0
  27. package/presets/openwrite/skills/oh-story-long-analyze/SKILL.md +2 -0
  28. package/presets/openwrite/skills/oh-story-long-scan/SKILL.md +2 -0
  29. package/presets/openwrite/skills/oh-story-long-write/SKILL.md +2 -0
  30. package/presets/openwrite/skills/oh-story-review/SKILL.md +2 -0
  31. package/presets/openwrite/skills/progress/SKILL.md +2 -0
  32. package/presets/openwrite/skills/review-chapter/SKILL.md +2 -0
  33. package/presets/openwrite/skills/revise-span/SKILL.md +2 -0
  34. package/presets/openwrite/skills/style-system/SKILL.md +2 -0
  35. package/presets/openwrite/skills/truth-validation/SKILL.md +2 -0
  36. package/presets/openwrite/skills/workflow-manager/SKILL.md +2 -0
  37. package/presets/openwrite/skills/world-query/SKILL.md +2 -0
  38. package/presets/openwrite/skills/write-next/SKILL.md +2 -0
  39. package/release/{openwrite-5.8.9-py3-none-any.whl → openwrite-5.8.10-py3-none-any.whl} +0 -0
  40. package/release/runtime-manifest.json +9 -6
  41. package/release/source.json +3 -3
  42. package/vendor/dsh-dog/lib/core.js +1 -1
  43. package/vendor/dsh-dog/lib/index.d.ts +1 -1
  44. package/vendor/dsh-dog/lib/index.js +202 -4
  45. package/vendor/dsh-dog/lib/{core-DdfMcrB4.js → core-BAlgZpLq.js} +2 -2
@@ -8,7 +8,7 @@ import { createHash } from 'node:crypto';
8
8
  import { realpathSync } from 'node:fs';
9
9
  import { mkdir, writeFile } from 'node:fs/promises';
10
10
  import { isAbsolute, join } from 'node:path';
11
- import { defineTool } from '@deepseek-ai/dsh-tools';
11
+ import { ToolArgsError, parameterSchemaSpecToJsonSchema, validateJsonSchemaValue, valueSchemaSpecToJsonSchema, } from '@deepseek-ai/dsh-tools';
12
12
  import { StudioError } from './client.js';
13
13
  import { materializeChapterDelivery } from './dog-delivery.js';
14
14
  import { materializeCompletedDogTaskReview, materializeDogReview, reviewChapterId } from './dog-review.js';
@@ -23,6 +23,146 @@ const JSON_OUTPUT_SCHEMA = {
23
23
  };
24
24
  /** Manuscript chapter ids accepted by OpenWrite (`ch_<digits>`). */
25
25
  const CHAPTER_ID_PATTERN = /^ch_\d+$/;
26
+ /** Short, model-facing description of a received argument value's actual shape. */
27
+ function describeValue(value) {
28
+ if (value === undefined)
29
+ return 'undefined';
30
+ if (value === null)
31
+ return 'null';
32
+ if (Array.isArray(value))
33
+ return `an array of ${value.length} item(s)`;
34
+ if (typeof value === 'string')
35
+ return 'a string';
36
+ return `a value of type ${typeof value}`;
37
+ }
38
+ /**
39
+ * Parse a JSON string argument once, naming the parameter and the received
40
+ * type when the text is not valid JSON.
41
+ */
42
+ function parseJsonArgument(name, value) {
43
+ try {
44
+ return JSON.parse(value);
45
+ }
46
+ catch (error) {
47
+ const detail = error instanceof Error ? error.message : String(error);
48
+ throw new Error(`parameter "${name}" expects a JSON value; received a string that is not valid JSON (${detail})`);
49
+ }
50
+ }
51
+ /**
52
+ * Normalize arguments for parameters declared `type: 'json'` (and
53
+ * `type: 'array'` with json items). DSH keeps `json` an annotation-only
54
+ * schema, so the value reaches the plugin in whatever shape the model
55
+ * emitted; models that serialize the object/array as a JSON *string* would
56
+ * otherwise fail the plugin-side object/array checks (GitHub issue #75).
57
+ */
58
+ function normalizeJsonArguments(parameters, args) {
59
+ let normalized;
60
+ for (const [name, spec] of Object.entries(parameters)) {
61
+ const value = args[name];
62
+ if (value === undefined || spec === null || typeof spec !== 'object')
63
+ continue;
64
+ const type = spec.type;
65
+ if (type === 'json' && typeof value === 'string') {
66
+ normalized ??= { ...args };
67
+ normalized[name] = parseJsonArgument(name, value);
68
+ }
69
+ else if (type === 'array') {
70
+ const items = spec.items;
71
+ const itemType = items !== null && typeof items === 'object' ? items.type : undefined;
72
+ if (itemType !== 'json')
73
+ continue;
74
+ if (typeof value === 'string') {
75
+ normalized ??= { ...args };
76
+ normalized[name] = parseJsonArgument(name, value);
77
+ }
78
+ else if (Array.isArray(value) && value.some((item) => typeof item === 'string')) {
79
+ normalized ??= { ...args };
80
+ normalized[name] = value.map((item) => (typeof item === 'string' ? parseJsonArgument(`${name}[]`, item) : item));
81
+ }
82
+ }
83
+ }
84
+ return normalized ?? args;
85
+ }
86
+ /**
87
+ * Local `defineTool`: mirrors the host helper (`@deepseek-ai/dsh-tools`
88
+ * `defineTool`) using the host's own schema compiler and validator, except
89
+ * every argument flow (execute and the replay presenters) first normalizes
90
+ * string-typed JSON arguments — see {@link normalizeJsonArguments}.
91
+ * Normalization must run *before* validation, otherwise an `array` parameter
92
+ * receiving a JSON string would be rejected by the schema check before the
93
+ * plugin ever sees it (GitHub issue #75). Centralizing here keeps all
94
+ * `novel_*` call sites free of per-parameter parsing.
95
+ */
96
+ function defineTool(options) {
97
+ const userExecute = options.execute;
98
+ const userFinalizeContent = options.finalizeContent;
99
+ const userRender = options.output.render;
100
+ const userPresentationMeta = options.output.presentationMeta;
101
+ const userPresentCall = options.presentCall;
102
+ const userPresentResult = options.presentResult;
103
+ const userIsConcurrencySafe = options.isConcurrencySafe;
104
+ if (options.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0))
105
+ throw new Error(`defineTool(${options.name}): timeoutMs must be a positive finite number`);
106
+ const parameters = parameterSchemaSpecToJsonSchema(options.parameters);
107
+ const outputSchema = valueSchemaSpecToJsonSchema(options.output.schema);
108
+ const normalize = (args) => normalizeJsonArguments(options.parameters, objectValue(args));
109
+ const validate = (args) => validateJsonSchemaValue(parameters, args, '');
110
+ const tool = {
111
+ name: options.name,
112
+ description: options.description,
113
+ parameters,
114
+ output: {
115
+ schema: outputSchema,
116
+ render: (args, value) => userRender(args, value),
117
+ ...(userPresentationMeta !== undefined
118
+ ? { presentationMeta: (args, value) => userPresentationMeta(args, value) }
119
+ : {}),
120
+ },
121
+ ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
122
+ async execute(args, exec) {
123
+ const normalized = normalize(args);
124
+ const violations = validate(normalized);
125
+ if (violations.length > 0)
126
+ throw new ToolArgsError(violations);
127
+ try {
128
+ return await userExecute(normalized, exec);
129
+ }
130
+ catch (error) {
131
+ if (!(error instanceof StudioError) || error.code.startsWith('WORKSPACE_'))
132
+ throw error;
133
+ error.message = JSON.stringify({ code: error.code, message: error.message,
134
+ status: error.status, details: error.details, request_id: error.requestId,
135
+ recovery: error.code === 'NETWORK_ERROR' ? 'List recent tasks and chapter runs before resubmitting; the server may have accepted the request.' : null,
136
+ });
137
+ throw error;
138
+ }
139
+ },
140
+ };
141
+ if (userFinalizeContent)
142
+ tool.finalizeContent = (exec, result) => userFinalizeContent(exec, result);
143
+ if (userPresentCall)
144
+ tool.presentCall = (args) => {
145
+ const normalized = normalize(args);
146
+ if (validate(normalized).length > 0)
147
+ return undefined;
148
+ return userPresentCall(normalized);
149
+ };
150
+ if (userPresentResult)
151
+ tool.presentResult = (args, result) => {
152
+ const normalized = normalize(args);
153
+ if (validate(normalized).length > 0)
154
+ return undefined;
155
+ return userPresentResult(normalized, result);
156
+ };
157
+ if (userIsConcurrencySafe)
158
+ tool.isConcurrencySafe = (args) => {
159
+ const normalized = normalize(args);
160
+ if (validate(normalized).length > 0)
161
+ return false;
162
+ return userIsConcurrencySafe(normalized);
163
+ };
164
+ return tool;
165
+ }
26
166
  /** Resolve a review target: an explicit manuscript path, or a chapter id mapped to `data/manuscript/<id>.md`. */
27
167
  function resolveReviewPath(args) {
28
168
  const path = args.path?.trim();
@@ -389,9 +529,14 @@ export function registerNovelTools(ctx, clientFactory, options) {
389
529
  'rejects the edit with a conflict if the outline changed in between. ' +
390
530
  'Operations: "rename" (node_id + title), "update_summary" (node_id + summary), ' +
391
531
  '"add_child" (node_id = parent, + title/summary/kind), "add_after" (node_id = sibling, + title/summary/kind), ' +
392
- '"delete" (node_id).',
393
- parameters: {
394
- operation: { type: 'string', required: true, enum: ['rename', 'update_summary', 'add_child', 'add_after', 'delete'], description: 'The structural operation to apply.' },
532
+ '"delete" (node_id, cascade=false keeps children), "promote" (remove a container), and ' +
533
+ '"get_meta"/"set_meta" (chapter metadata, optionally updates[] for a batch).',
534
+ parameters: {
535
+ operation: { type: 'string', required: true, enum: ['rename', 'update_summary', 'add_child', 'add_after', 'delete', 'promote', 'get_meta', 'set_meta'], description: 'The structural operation to apply.' },
536
+ cascade: { type: 'boolean', description: 'For delete, false removes only the container and keeps children and chapter IDs.' },
537
+ metadata: { type: 'json', description: 'Field object for set_meta; use target_words, beats, hooks or characters.' },
538
+ chapter_id: { type: 'string', description: 'Chapter ID alias for node_id in get_meta/set_meta.' },
539
+ updates: { type: 'json', description: 'Batch of 1-50 {chapter_id, metadata} objects for set_meta, or {chapter_id} for get_meta; one revision guards the whole batch.' },
395
540
  revision: { type: 'string', required: true, description: 'The outline revision fingerprint from a fresh novel_outline_read call.' },
396
541
  node_id: { type: 'string', description: 'Target node id (parent for add_child, sibling for add_after); not needed for add_child at root.' },
397
542
  title: { type: 'string', description: 'Node title (rename / add_child / add_after).' },
@@ -409,6 +554,14 @@ export function registerNovelTools(ctx, clientFactory, options) {
409
554
  body['title'] = args.title;
410
555
  if (args.summary !== undefined)
411
556
  body['summary'] = args.summary;
557
+ if (args.cascade !== undefined)
558
+ body['cascade'] = args.cascade;
559
+ if (args.metadata !== undefined)
560
+ body['metadata'] = args.metadata;
561
+ if (args.chapter_id !== undefined)
562
+ body['chapter_id'] = args.chapter_id;
563
+ if (args.updates !== undefined)
564
+ body['updates'] = args.updates;
412
565
  if (args.kind !== undefined)
413
566
  body['kind'] = args.kind;
414
567
  return await client.postJson('/api/outline/edit', body, exec.signal);
@@ -542,11 +695,13 @@ export function registerNovelTools(ctx, clientFactory, options) {
542
695
  }));
543
696
  ctx.tools.register(defineTool({
544
697
  name: 'novel_write_chapter',
545
- description: 'Write a chapter with OpenWrite (LONG-RUNNING, may take several minutes). Omit chapter_id to write ' +
698
+ description: 'Queue a chapter write and immediately return a durable task_id. Poll novel_task_get for progress/result. Omit chapter_id to write ' +
546
699
  'the outline-recommended next chapter; a given chapter_id must match the current recommendation. ' +
547
700
  'Preview context first with novel_context_preview and consult novel_outline_read for the recommendation.',
548
701
  parameters: {
549
- target_words: { type: 'integer', description: 'Target word count, 200-12000 (default 3000).' },
702
+ run_id_v2: { type: 'string', description: 'Resume this completed draft without generating it again.' },
703
+ resume_from_draft: { type: 'boolean', description: 'Resume the latest uncommitted draft for this chapter.' },
704
+ target_words: { type: 'integer', description: 'Target word count, 200-50000. When omitted use the outline target, then default 3000.' },
550
705
  guidance: { type: 'string', description: 'Writing guidance for this chapter (plot beats, tone, POV notes).' },
551
706
  chapter_id: { type: 'string', description: 'Chapter id to write; must equal the current outline recommendation. Omit for the next chapter.' },
552
707
  temperature: { type: 'number', description: 'Sampling temperature (default 0.7).' },
@@ -556,11 +711,17 @@ export function registerNovelTools(ctx, clientFactory, options) {
556
711
  timeoutMs,
557
712
  async execute(args, exec) {
558
713
  const client = clientFor(exec);
559
- const targetWords = args.target_words ?? 3000;
560
- if (!Number.isInteger(targetWords) || targetWords < 200 || targetWords > 12000) {
561
- throw new Error(`target_words must be an integer between 200 and 12000, got ${String(args.target_words)}`);
714
+ const targetWords = args.target_words;
715
+ if (targetWords !== undefined && (!Number.isInteger(targetWords) || targetWords < 200 || targetWords > 50000)) {
716
+ throw new Error(`target_words must be an integer between 200 and 50000, got ${String(args.target_words)}`);
562
717
  }
563
- const body = { target_words: targetWords, guidance: args.guidance ?? '' };
718
+ const body = { guidance: args.guidance ?? '' };
719
+ if (targetWords !== undefined)
720
+ body['target_words'] = targetWords;
721
+ if (args.run_id_v2 !== undefined)
722
+ body['run_id_v2'] = args.run_id_v2;
723
+ if (args.resume_from_draft !== undefined)
724
+ body['resume_from_draft'] = args.resume_from_draft;
564
725
  const chapterId = args.chapter_id?.trim();
565
726
  if (chapterId)
566
727
  body['chapter_id'] = chapterId;
@@ -568,17 +729,7 @@ export function registerNovelTools(ctx, clientFactory, options) {
568
729
  body['temperature'] = args.temperature;
569
730
  if (args.outline_revision !== undefined)
570
731
  body['outline_revision'] = args.outline_revision;
571
- const response = await client.postJson('/api/write', body, exec.signal);
572
- const writeValue = objectValue(response);
573
- const writeResult = objectValue(writeValue['result'] ?? response);
574
- const writtenChapterId = chapterId || String(writeResult['chapter_id'] ?? '');
575
- try {
576
- const dogDelivery = await deliveryForResponse(client, response, writtenChapterId, exec.signal);
577
- return { ...objectValue(response), dog_delivery: dogDelivery };
578
- }
579
- catch (error) {
580
- return { ...objectValue(response), dog_delivery: { status: 'unavailable', error: error instanceof Error ? error.message : String(error) } };
581
- }
732
+ return await client.postJson('/api/tasks', { type: 'chapter_write', input: body }, exec.signal);
582
733
  },
583
734
  }));
584
735
  ctx.tools.register(defineTool({
@@ -586,6 +737,8 @@ export function registerNovelTools(ctx, clientFactory, options) {
586
737
  description: 'Run OpenWrite\'s multi-dimensional review on a drafted manuscript chapter (LONG-RUNNING). ' +
587
738
  'Pass either the manuscript path (e.g. "data/manuscript/ch_0007.md") or a chapter_id like "ch_0007".',
588
739
  parameters: {
740
+ action: { type: 'string', enum: ['review', 'versions', 'compare'], description: 'Default review; versions lists saved reviews, compare compares a historical revision with the current review.' },
741
+ revision: { type: 'string', description: 'Historical review revision for compare.' },
589
742
  path: { type: 'string', description: 'Manuscript path relative to the novel data root. Takes precedence over chapter_id.' },
590
743
  chapter_id: { type: 'string', description: 'Chapter id (ch_<digits>), resolved client-side to data/manuscript/<id>.md.' },
591
744
  strict: { type: 'boolean', description: 'Enable strict review mode (default false).' },
@@ -596,6 +749,8 @@ export function registerNovelTools(ctx, clientFactory, options) {
596
749
  timeoutMs,
597
750
  async execute(args, exec) {
598
751
  const client = clientFor(exec);
752
+ if (args.action === 'versions' || args.action === 'compare')
753
+ return await client.postJson('/api/review/history', { action: args.action, chapter_id: reviewChapterId(args), revision: args.revision ?? '' }, exec.signal);
599
754
  if (args.dimensions !== undefined && args.dimensions.some(value => !Number.isInteger(value) || value < 1 || value > 37)) {
600
755
  throw new Error('dimensions must contain only integers from 1 to 37');
601
756
  }
@@ -721,11 +876,12 @@ export function registerNovelTools(ctx, clientFactory, options) {
721
876
  name: 'novel_document_change_plan',
722
877
  description: 'Stage and review revision-gated changes to an existing novel document. Use action "preview" with path and edits ' +
723
878
  'to receive an immutable preview_token and real before/after summary. Use "apply" only after author acceptance, ' +
724
- 'passing only that token; "reject" discards it, "retry" rebuilds it against the current source, and "undo" safely ' +
879
+ 'passing that token and confirm: true; "reject" discards it, "retry" rebuilds it against the current source, and "undo" safely ' +
725
880
  'applies an undo_preview_token returned by a prior apply. Each edit uses old_text/new_text or unique start_text/' +
726
881
  'end_text/new_text anchors. The server rechecks the source revision and writes exactly the stored preview.',
727
882
  parameters: {
728
883
  action: { type: 'string', required: true, enum: ['preview', 'apply', 'reject', 'retry', 'undo'], description: 'Plan lifecycle action.' },
884
+ confirm: { type: 'boolean', description: 'Required true for apply and undo after author acceptance.' },
729
885
  path: { type: 'string', description: 'Existing document path for preview, relative to the novel root.' },
730
886
  edits: { type: 'json', description: 'Preview edits: [{old_text,new_text}] or [{start_text,end_text,new_text}].' },
731
887
  preview_token: { type: 'string', description: 'Immutable preview or undo token for apply/reject/retry/undo.' },
@@ -733,14 +889,19 @@ export function registerNovelTools(ctx, clientFactory, options) {
733
889
  output: { schema: JSON_OUTPUT_SCHEMA, render: (_args, value) => renderJson(value) },
734
890
  timeoutMs,
735
891
  async execute(args, exec) {
736
- if (args.action === 'preview' && (!Array.isArray(args.edits) || args.edits.length !== 1)) {
737
- throw new Error('preview requires exactly one edit so the author can accept or reject each change independently');
892
+ if (args.action === 'preview' && (!Array.isArray(args.edits) || (args.edits.length < 1 || args.edits.length > 50))) {
893
+ throw new Error('preview requires between 1 and 50 edits ' +
894
+ `(received ${describeValue(args.edits)})`);
738
895
  }
739
896
  if (args.action !== 'preview' && !args.preview_token?.trim()) {
740
897
  throw new Error(`${args.action} requires preview_token`);
741
898
  }
899
+ if (['apply', 'undo'].includes(args.action) && args.confirm !== true)
900
+ throw new Error('apply/undo requires confirm: true');
742
901
  const client = clientFor(exec);
743
902
  const body = { action: args.action };
903
+ if (args.confirm !== undefined)
904
+ body['confirm'] = args.confirm;
744
905
  if (args.path !== undefined)
745
906
  body['path'] = args.path;
746
907
  if (args.edits !== undefined)
@@ -771,7 +932,7 @@ export function registerNovelTools(ctx, clientFactory, options) {
771
932
  if (!args.change_kind)
772
933
  throw new Error('preview requires change_kind');
773
934
  if (args.change === null || typeof args.change !== 'object' || Array.isArray(args.change)) {
774
- throw new Error('preview requires one change object');
935
+ throw new Error(`preview requires one change object (received ${describeValue(args.change)})`);
775
936
  }
776
937
  }
777
938
  else if (!args.preview_token?.trim()) {
@@ -1189,6 +1350,7 @@ export function registerNovelTools(ctx, clientFactory, options) {
1189
1350
  name: 'novel_tasks_list',
1190
1351
  description: 'List background tasks (queued/running/finished AI jobs) with per-status counts.',
1191
1352
  parameters: {
1353
+ since: { type: 'string', description: 'ISO timestamp filter for requests whose response was lost.' },
1192
1354
  limit: { type: 'integer', description: 'Maximum number of tasks (server default 100).' },
1193
1355
  },
1194
1356
  output: { schema: JSON_OUTPUT_SCHEMA, render: (_args, value) => renderJson(value) },
@@ -1197,6 +1359,8 @@ export function registerNovelTools(ctx, clientFactory, options) {
1197
1359
  async execute(args, exec) {
1198
1360
  const client = clientFor(exec);
1199
1361
  const params = {};
1362
+ if (args.since !== undefined)
1363
+ params['since'] = args.since;
1200
1364
  if (args.limit !== undefined)
1201
1365
  params['limit'] = String(args.limit);
1202
1366
  return await client.getJson('/api/tasks', params, exec.signal);
@@ -1300,8 +1464,9 @@ export function registerNovelTools(ctx, clientFactory, options) {
1300
1464
  }));
1301
1465
  ctx.tools.register(defineTool({
1302
1466
  name: 'novel_task_cancel',
1303
- description: 'Cancel a queued or running background task. Cancellation is permanent for that task; retry with novel_task_retry if the type supports it.',
1467
+ description: 'Request cancellation of a queued or running task. Check cancelled and cancellation_status for the actual outcome; poll novel_task_get while cancellation is requested.',
1304
1468
  parameters: {
1469
+ wait_seconds: { type: 'number', description: 'Wait up to 30 seconds for actual cancellation; running model calls may finish later.' },
1305
1470
  task_id: { type: 'string', required: true, description: 'Task id (tsk_*).' },
1306
1471
  },
1307
1472
  output: { schema: JSON_OUTPUT_SCHEMA, render: (_args, value) => renderJson(value) },
@@ -1310,7 +1475,7 @@ export function registerNovelTools(ctx, clientFactory, options) {
1310
1475
  const client = clientFor(exec);
1311
1476
  if (!/^tsk_[A-Za-z0-9_-]+$/.test(args.task_id))
1312
1477
  throw new Error(`invalid task_id "${args.task_id}"`);
1313
- return await client.postJson(`/api/tasks/${encodeURIComponent(args.task_id)}/cancel`, {}, exec.signal);
1478
+ return await client.postJson(`/api/tasks/${encodeURIComponent(args.task_id)}/cancel`, { wait_seconds: args.wait_seconds ?? 0 }, exec.signal);
1314
1479
  },
1315
1480
  }));
1316
1481
  ctx.tools.register(defineTool({
@@ -1780,7 +1945,7 @@ export function registerNovelTools(ctx, clientFactory, options) {
1780
1945
  ctx.tools.register(defineTool({
1781
1946
  name: 'novel_writing_targets',
1782
1947
  description: 'Immediate compatibility write for targets the author has already authorized exactly; otherwise use ' +
1783
- 'novel_structured_change_plan. Update the project writing targets. Ranges: book_words 10000-5000000, chapter_words 200-12000, ' +
1948
+ 'novel_structured_change_plan. Update the project writing targets. Ranges: book_words 10000-5000000, chapter_words 200-50000, ' +
1784
1949
  'outline_volume_words 100-5000, outline_act_words 80-3000, outline_section_words 50-2000, ' +
1785
1950
  'outline_chapter_words 30-1000. Omitted fields keep their current values.',
1786
1951
  parameters: {
@@ -1853,11 +2018,12 @@ export function registerNovelTools(ctx, clientFactory, options) {
1853
2018
  'confirm: true), "cancel" (run_id + revision, optional reason). All mutating actions are revision-gated: ' +
1854
2019
  'pass the run\'s current revision from a list/get call.',
1855
2020
  parameters: {
1856
- action: { type: 'string', required: true, enum: ['list', 'get', 'record_intervention', 'update_intervention', 'cancel'], description: 'The chapter-run operation.' },
2021
+ action: { type: 'string', required: true, enum: ['list', 'get', 'record_intervention', 'update_intervention', 'cancel', 'archive'], description: 'The chapter-run operation.' },
1857
2022
  run_id: { type: 'string', description: 'Run id (required for get/record_intervention/update_intervention/cancel).' },
1858
2023
  revision: { type: 'string', description: 'Current run revision (required for mutating actions).' },
1859
2024
  chapter_id: { type: 'string', description: 'Chapter filter for list.' },
1860
2025
  statuses: { type: 'array', items: { type: 'string' }, description: 'Status filter for list.' },
2026
+ since: { type: 'string', description: 'ISO timestamp filter for recent runs.' },
1861
2027
  limit: { type: 'integer', description: 'List limit (default 20).' },
1862
2028
  scope: { type: 'string', description: 'Intervention scope (default "chapter").' },
1863
2029
  risk: { type: 'string', description: 'Intervention risk level (default "medium").' },
@@ -1878,7 +2044,7 @@ export function registerNovelTools(ctx, clientFactory, options) {
1878
2044
  async execute(args, exec) {
1879
2045
  const client = clientFor(exec);
1880
2046
  const body = { action: args.action };
1881
- for (const key of ['run_id', 'revision', 'chapter_id', 'statuses', 'limit', 'scope', 'risk', 'request', 'affected_items', 'rewrite_required', 'intervention_id', 'state', 'facts_revision', 'impact', 'proposal', 'confirm', 'reason']) {
2047
+ for (const key of ['run_id', 'revision', 'chapter_id', 'statuses', 'since', 'limit', 'scope', 'risk', 'request', 'affected_items', 'rewrite_required', 'intervention_id', 'state', 'facts_revision', 'impact', 'proposal', 'confirm', 'reason']) {
1882
2048
  const value = args[key];
1883
2049
  if (value !== undefined)
1884
2050
  body[key] = value;
@@ -1992,11 +2158,11 @@ export function registerNovelTools(ctx, clientFactory, options) {
1992
2158
  name: 'novel_manuscript_acceptance_reconcile',
1993
2159
  description: 'Reconcile the canonical manuscript acceptance ledger. Actions: "baseline" (accept current text as the ' +
1994
2160
  'initial baseline), "external" (accept a detected external edit), "resume" (continue a durable incomplete ' +
1995
- 'operation), and "acknowledge" (record that the author reviewed the completed acceptance and impacts). ' +
2161
+ 'operation), "abandon" (release a stale operation without deleting prose), and "acknowledge" (record that the author reviewed the completed acceptance and impacts). ' +
1996
2162
  'Each action is mapped to the corresponding OpenWrite acceptance endpoint. Baseline, external, and ' +
1997
2163
  'acknowledge are confirmation-gated by the server.',
1998
2164
  parameters: {
1999
- action: { type: 'string', required: true, enum: ['baseline', 'external', 'resume', 'acknowledge'], description: 'The reconciliation action.' },
2165
+ action: { type: 'string', required: true, enum: ['baseline', 'external', 'resume', 'acknowledge', 'abandon'], description: 'The reconciliation action.' },
2000
2166
  chapter_id: { type: 'string', description: 'Optional chapter id (ch_<digits>); omit for the project-level operation.' },
2001
2167
  operation_id: { type: 'string', description: 'Durable operation id to resume or acknowledge.' },
2002
2168
  domains: { type: 'array', items: { type: 'string' }, description: 'Affected domains to acknowledge or retry; acknowledge defaults to outline and foreshadowing.' },