dsh-openwrite 0.2.26 → 0.2.28

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 (37) hide show
  1. package/README.md +16 -4
  2. package/docs/CLOSED_ISSUE_FOLLOWUPS.md +35 -0
  3. package/docs/CONTEXT_SLOTS.md +60 -0
  4. package/docs/EDITOR_SAFETY_REPAIRS.md +17 -0
  5. package/docs/ISSUES_105_113.md +4 -1
  6. package/docs/ISSUES_121_123.md +97 -0
  7. package/docs/ISSUES_98_100.md +2 -0
  8. package/docs/ISSUE_123_FOLLOWUP.md +54 -0
  9. package/docs/OPENCODE_GO.md +24 -18
  10. package/docs/RELEASE_ACCEPTANCE.md +89 -1
  11. package/docs/SELECTION_UPGRADE_REPAIRS.md +16 -0
  12. package/docs/TOOLS.md +1356 -0
  13. package/dog-tools.mjs +5 -3
  14. package/package.json +5 -4
  15. package/packages/openwrite-bridge/lib/contracts-generated.js +9 -0
  16. package/packages/openwrite-bridge/lib/dog-delivery.js +7 -3
  17. package/packages/openwrite-bridge/lib/managed-runtime.d.ts +1 -0
  18. package/packages/openwrite-bridge/lib/managed-runtime.js +22 -2
  19. package/packages/openwrite-bridge/lib/tool-policy.js +2 -0
  20. package/packages/openwrite-bridge/lib/tools.js +28 -5
  21. package/packages/studio-panel/lib/client.js +821 -544
  22. package/packages/studio-panel/lib/client.js.map +1 -1
  23. package/packages/studio-panel/lib/types/client/VditorBody.d.ts +7 -1
  24. package/packages/studio-panel/lib/types/client/dto.d.ts +2 -0
  25. package/packages/studio-panel/lib/types/client/locales.d.ts +1 -1
  26. package/packages/studio-panel/lib/types/client/manuscript-annotations.d.ts +2 -1
  27. package/packages/studio-panel/lib/types/client/manuscript-overlay.d.ts +2 -1
  28. package/packages/studio-panel/lib/types/client/manuscript-selection.d.ts +18 -5
  29. package/packages/studio-panel/lib/types/client/selection-coordinates.d.ts +15 -0
  30. package/preset-definition.mjs +1 -1
  31. package/release/{openwrite-5.8.14-py3-none-any.whl → openwrite-5.8.20-py3-none-any.whl} +0 -0
  32. package/release/runtime-manifest.json +6 -5
  33. package/release/source.json +3 -3
  34. package/scripts/safe-upgrade.mjs +104 -9
  35. package/vendor/dsh-dog/lib/{core-BAlgZpLq.js → core--byUPJL8.js} +129 -12
  36. package/vendor/dsh-dog/lib/core.js +1 -1
  37. package/vendor/dsh-dog/lib/index.js +54 -103
package/dog-tools.mjs CHANGED
@@ -4,8 +4,10 @@ export function apply(ctx) {
4
4
  for (const tool of ctx.openwriteDog.tools) ctx.effect(() => ctx.tools.register(tool))
5
5
  ctx.effect(() => ctx.tools.guard(execution => {
6
6
  if (!execution.name.startsWith('dog_') || ['dog_status', 'dog_wait', 'dog_validate'].includes(execution.name)) return
7
- if (!execution.agent) return 'DoG requires an Agent session'
8
- if (execution.agent.ctx.get('planMode')?.get(execution.agent).active) return 'DoG execution is disabled in plan mode'
9
- if (!ctx.get('sandboxPolicy') || ctx.get('sandboxPolicy').resolve({ session: execution.agent.session }).mode === 'read-only') return 'DoG execution is disabled in read-only mode'
7
+ if (!execution.agent) return 'DOG_AGENT_SESSION_REQUIRED: DoG execution requires an Agent session; run this tool inside a writing Agent session.'
8
+ if (execution.agent.ctx.get('planMode')?.get(execution.agent)?.active) return 'DOG_PLAN_MODE: DoG execution is disabled in plan mode; leave plan mode before running it.'
9
+ const policy = ctx.get('sandboxPolicy')
10
+ if (!policy) return 'DOG_SANDBOX_POLICY_UNAVAILABLE: The host sandbox policy service is unavailable; check the host configuration.'
11
+ if (policy.resolve({ session: execution.agent.session }).mode === 'read-only') return 'DOG_READ_ONLY: DoG execution requires a writable session; the current sandbox is read-only.'
10
12
  }))
11
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-openwrite",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
4
4
  "description": "OpenWrite 长篇小说领域后端接入 DeepSeek Harness:统一创作 Agent 预设 + 原生创作工作台",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -14,14 +14,15 @@
14
14
  "install-dsh": "bash scripts/install.sh",
15
15
  "dev": "bash scripts/dev.sh",
16
16
  "doctor": "node scripts/plugin-doctor.mjs",
17
- "test:maintenance": "node --test scripts/plugin-doctor.test.mjs scripts/lifecycle.test.mjs packages/openwrite-bridge/scripts/runtime-lifecycle.test.mjs scripts/maintenance.test.mjs scripts/preset-lifecycle.test.mjs scripts/safe-upgrade.test.mjs",
17
+ "test:maintenance": "node --test scripts/plugin-doctor.test.mjs scripts/lifecycle.test.mjs packages/openwrite-bridge/scripts/runtime-lifecycle.test.mjs scripts/maintenance.test.mjs scripts/preset-lifecycle.test.mjs scripts/safe-upgrade.test.mjs scripts/dog-tools.test.mjs",
18
18
  "check": "npm run check:plugin && npm run test:contracts",
19
19
  "test:lifecycle": "node --test scripts/lifecycle.test.mjs",
20
20
  "test:runtime": "node --test packages/openwrite-bridge/scripts/runtime-lifecycle.test.mjs",
21
- "check:plugin": "npm run build && npm run doctor && npm run test:maintenance && npm run test:profiles && npm run test:preset && npm --prefix packages/openwrite-bridge run smoke && npm --prefix packages/studio-panel run smoke && npm run test:epochs && npm run test:components",
21
+ "check:plugin": "npm run build && npm run docs:check && npm run doctor && npm run test:maintenance && npm run test:profiles && npm run test:preset && npm --prefix packages/openwrite-bridge run smoke && npm --prefix packages/studio-panel run smoke && npm run test:epochs && npm run test:components",
22
+ "docs:check": "node scripts/gen-tools-md.mjs --check && node scripts/check-editor-coordinates.mjs",
22
23
  "test:profiles": "node scripts/profile-smoke.mjs",
23
24
  "prepare": "node scripts/prepare.mjs",
24
- "test:managed": "node --test packages/openwrite-bridge/scripts/managed-runtime.test.mjs packages/openwrite-bridge/scripts/tool-policy.test.mjs",
25
+ "test:managed": "node --test packages/openwrite-bridge/scripts/managed-runtime.test.mjs packages/openwrite-bridge/scripts/tool-policy.test.mjs packages/openwrite-bridge/scripts/followup-tools.test.mjs",
25
26
  "test:release": "node scripts/release-smoke.mjs",
26
27
  "test:client-boot": "node scripts/client-boot-smoke.mjs"
27
28
  },
@@ -726,6 +726,12 @@ const SCHEMAS = {
726
726
  "secret"
727
727
  ],
728
728
  "properties": {
729
+ "auth_source": {
730
+ "enum": [
731
+ "api_key",
732
+ "opencode_console"
733
+ ]
734
+ },
729
735
  "capabilities": {
730
736
  "properties": {
731
737
  "chat": {
@@ -772,6 +778,9 @@ const SCHEMAS = {
772
778
  ]
773
779
  },
774
780
  "type": "array"
781
+ },
782
+ "workspace_id": {
783
+ "type": "string"
775
784
  }
776
785
  },
777
786
  "required": [
@@ -216,7 +216,7 @@ export async function materializeChapterDelivery(workspaceValue, chapterId, thre
216
216
  });
217
217
  let reviewStage;
218
218
  if (Object.keys(review).length === 0) {
219
- reviewStage = stage(chapterId, 'review', 'inconclusive', 'missing', {});
219
+ reviewStage = stage(chapterId, 'review', 'inconclusive', 'not_reviewed', {});
220
220
  }
221
221
  else if (!reviewCurrent) {
222
222
  reviewStage = stage(chapterId, 'review', 'inconclusive', reviewStale ? 'stale' : 'unverifiable', {
@@ -225,9 +225,13 @@ export async function materializeChapterDelivery(workspaceValue, chapterId, thre
225
225
  }
226
226
  else {
227
227
  const v2 = record(review['review_v2']);
228
- reviewStage = stage(chapterId, 'review', reviewPassed ? 'pass' : ['inconclusive', 'stale'].includes(currentDeliveryStatus) ? 'inconclusive' : 'fail', 'current', {
228
+ const diagnostics = record(v2['delivery_diagnostics']);
229
+ const reviewState = !reviewPassed && currentDeliveryStatus === 'inconclusive' ? String(diagnostics['code'] ?? v2['delivery_reason'] ?? 'review_inconclusive') : 'current';
230
+ reviewStage = stage(chapterId, 'review', reviewPassed ? 'pass' : ['inconclusive', 'stale'].includes(currentDeliveryStatus) ? 'inconclusive' : 'fail', reviewState, {
229
231
  score: review['score'] ?? null, threshold: effectiveThreshold, passedGate: reviewPassed,
230
- qualityScore: v2['quality_score'] ?? review['score'] ?? null, coverage: v2['coverage'] ?? 1,
232
+ qualityScore: hasV2 ? v2['quality_score'] ?? null : review['score'] ?? null, coverage: v2['coverage'] ?? 1,
233
+ scoreValid: v2['score_valid'] ?? (hasV2 ? v2['quality_score'] != null : review['score'] != null),
234
+ scopeStatus: v2['scope_status'] ?? 'full', deliveryDiagnostics: diagnostics,
231
235
  gateStatus: v2['gate_status'] ?? (hardIssueIds.length > 0 ? 'blocked' : 'pass'),
232
236
  deliveryStatus: currentDeliveryStatus,
233
237
  issueIds, hardIssueIds, sourceRevision: reviewSourceRevision,
@@ -81,6 +81,7 @@ export declare class ManagedRuntime {
81
81
  private connection?;
82
82
  private closed;
83
83
  private allowRevive;
84
+ private cancelEpoch;
84
85
  private generation;
85
86
  private restartAttempts;
86
87
  private recovered;
@@ -173,6 +173,7 @@ export class ManagedRuntime {
173
173
  connection;
174
174
  closed = false;
175
175
  allowRevive = true;
176
+ cancelEpoch = 0;
176
177
  generation = 0;
177
178
  restartAttempts = 0;
178
179
  recovered = false;
@@ -190,15 +191,33 @@ export class ManagedRuntime {
190
191
  }
191
192
  status() { return { ...this.state }; }
192
193
  update(phase, message, extra = {}) {
193
- this.state = { phase, message, ...extra };
194
+ const recoveryError = this.recovered && ['recovering', 'waiting', 'downloading', 'installing'].includes(phase)
195
+ ? this.state.error : undefined;
196
+ this.state = { phase, message, ...(recoveryError ? { error: recoveryError } : {}), ...extra };
194
197
  }
195
198
  ensure() {
196
199
  if (this.closed)
197
200
  return Promise.reject(new Error('OpenWrite was removed'));
198
201
  if (this.connection)
199
202
  return this.verifyConnection(this.connection);
200
- if (this.pending)
203
+ if (this.pending) {
204
+ if (['error', 'cancelled', 'stopped'].includes(this.state.phase)) {
205
+ // A ready child can exit while prepare is still saving active.json.
206
+ // The terminal state is visible before that promise settles; returning
207
+ // it would hand a manual retry the dead connection or its old error.
208
+ const prior = this.pending;
209
+ const epoch = this.cancelEpoch;
210
+ const retry = () => {
211
+ if (epoch !== this.cancelEpoch)
212
+ throw new Error('Setup was cancelled; retry when ready');
213
+ if (this.pending === prior)
214
+ this.pending = undefined;
215
+ return this.ensure();
216
+ };
217
+ return prior.then(retry, retry);
218
+ }
201
219
  return this.pending;
220
+ }
202
221
  this.allowRevive = true;
203
222
  if (this.state.phase === 'error' || this.state.phase === 'cancelled' || this.state.phase === 'stopped') {
204
223
  this.restartAttempts = 0;
@@ -261,6 +280,7 @@ export class ManagedRuntime {
261
280
  }
262
281
  }
263
282
  async cancel() {
283
+ this.cancelEpoch += 1;
264
284
  this.allowRevive = false;
265
285
  this.controller?.abort();
266
286
  await this.pending?.catch(() => { });
@@ -31,6 +31,8 @@ export function novelEffect(name, args) {
31
31
  if (reads.has(name))
32
32
  return 'read';
33
33
  const values = args && typeof args === 'object' ? args : {};
34
+ if (name === 'novel_write_chapter' && values.dry_run === true)
35
+ return 'read';
34
36
  if (readActions[name]?.includes(String(values.action)))
35
37
  return 'read';
36
38
  if (name === 'novel_model_benchmark' && values.action === 'run')
@@ -703,8 +703,10 @@ export function registerNovelTools(ctx, clientFactory, options) {
703
703
  name: 'novel_write_chapter',
704
704
  description: 'Queue a chapter write and immediately return a durable task_id. Poll novel_task_get for progress/result. Omit chapter_id to write ' +
705
705
  'the outline-recommended next chapter; a given chapter_id must match the current recommendation. ' +
706
- 'Preview context first with novel_context_preview and consult novel_outline_read for the recommendation.',
706
+ 'Preview context first with novel_context_preview and consult novel_outline_read for the recommendation. ' +
707
+ 'Cancel with novel_task_cancel. Generated partial prose is retained; resume_from_draft continues it in a new run when the source/context revisions still match.',
707
708
  parameters: {
709
+ dry_run: { type: 'boolean', description: 'Only assemble the current writer input and per-document sizes; no model call and no writing task. Guidance and target_words are included in this preview.' },
708
710
  run_id_v2: { type: 'string', description: 'Resume this completed draft without generating it again.' },
709
711
  resume_from_draft: { type: 'boolean', description: 'Resume the latest uncommitted draft for this chapter.' },
710
712
  target_words: { type: 'integer', description: 'Target word count, 200-50000. When omitted use the outline target, then default 3000.' },
@@ -735,6 +737,8 @@ export function registerNovelTools(ctx, clientFactory, options) {
735
737
  body['temperature'] = args.temperature;
736
738
  if (args.outline_revision !== undefined)
737
739
  body['outline_revision'] = args.outline_revision;
740
+ if (args.dry_run === true)
741
+ return await client.postJson('/api/write', { ...body, dry_run: true }, exec.signal);
738
742
  return await client.postJson('/api/tasks', { type: 'chapter_write', input: body }, exec.signal);
739
743
  },
740
744
  }));
@@ -749,6 +753,8 @@ export function registerNovelTools(ctx, clientFactory, options) {
749
753
  chapter_id: { type: 'string', description: 'Chapter id (ch_<digits>), resolved client-side to data/manuscript/<id>.md.' },
750
754
  strict: { type: 'boolean', description: 'Enable strict review mode (default false).' },
751
755
  dimensions: { type: 'array', items: { type: 'integer' }, description: 'Optional list of review dimension numbers (1-37) to restrict to.' },
756
+ depth: { type: 'string', enum: ['quick', 'full'], description: 'Default full. Quick reviews coherence/canon/prose and safety only; scope_status=partial cannot approve full-chapter delivery. Do not combine quick with dimensions.' },
757
+ domains: { type: 'array', items: { type: 'string' }, description: 'Review domain ids: coherence, character, plot, pacing, prose, canon. Use this or dimensions. Omitted domains remain explicitly unreviewed.' },
752
758
  dog_threshold: { type: 'integer', description: 'DoG aggregate score threshold (default 70); this does not change OpenWrite review.' },
753
759
  },
754
760
  output: { schema: JSON_OUTPUT_SCHEMA, render: (_args, value) => renderJson(value) },
@@ -769,6 +775,10 @@ export function registerNovelTools(ctx, clientFactory, options) {
769
775
  body['strict'] = args.strict;
770
776
  if (args.dimensions !== undefined)
771
777
  body['dimensions'] = args.dimensions;
778
+ if (args.depth !== undefined)
779
+ body['depth'] = args.depth;
780
+ if (args.domains !== undefined)
781
+ body['domains'] = args.domains;
772
782
  const response = await client.postJson('/api/review', body, exec.signal);
773
783
  const value = response !== null && typeof response === 'object' && !Array.isArray(response)
774
784
  ? response
@@ -887,7 +897,9 @@ export function registerNovelTools(ctx, clientFactory, options) {
887
897
  'to receive an immutable preview_token and real before/after summary. Use "apply" only after author acceptance, ' +
888
898
  'passing that token and confirm: true; "reject" discards it, "retry" rebuilds it against the current source, and "undo" safely ' +
889
899
  'applies an undo_preview_token returned by a prior apply. Each edit uses old_text/new_text or unique start_text/' +
890
- 'end_text/new_text anchors. The server rechecks the source revision and writes exactly the stored preview.',
900
+ 'end_text/new_text anchors. Preview returns applied:false; changed/would_change describe only the proposed difference. ' +
901
+ 'OLD_TEXT_NOT_FOUND is an anchor mismatch (400), while revision conflicts are 409; use the exact suggested bytes and reread the source before retrying. ' +
902
+ 'The server rechecks the source revision and writes exactly the stored preview.',
891
903
  parameters: {
892
904
  action: { type: 'string', required: true, enum: ['preview', 'apply', 'reject', 'retry', 'undo'], description: 'Plan lifecycle action.' },
893
905
  confirm: { type: 'boolean', description: 'Required true for apply and undo after author acceptance.' },
@@ -1207,13 +1219,14 @@ export function registerNovelTools(ctx, clientFactory, options) {
1207
1219
  ctx.tools.register(defineTool({
1208
1220
  name: 'novel_revision_create_selection',
1209
1221
  description: 'Create a revision proposal for a text selection in a chapter (LONG-RUNNING, model-generated). ' +
1210
- 'start/end are character offsets; pass the selected text as original_text. The proposal is only staged — ' +
1222
+ 'start/end are Unicode code-point offsets in the exact saved text, including original CR/LF characters; pass the selected text as original_text and the current expected_document_revision. The proposal is only staged — ' +
1211
1223
  'apply it with novel_revision_apply.',
1212
1224
  parameters: {
1213
1225
  chapter_id: { type: 'string', required: true, description: 'Chapter id (ch_<digits>).' },
1214
1226
  start: { type: 'integer', required: true, description: 'Selection start offset.' },
1215
1227
  end: { type: 'integer', required: true, description: 'Selection end offset.' },
1216
1228
  original_text: { type: 'string', description: 'The selected original text.' },
1229
+ expected_document_revision: { type: 'string', description: 'Current exact manuscript revision; reject before generation if the source changed.' },
1217
1230
  instruction: { type: 'string', description: 'Revision instruction (max 4000 characters).' },
1218
1231
  action: { type: 'string', enum: ['rewrite', 'expand', 'compress', 'pace', 'dialogue', 'naturalize', 'custom'], description: 'Revision mode (default "rewrite").' },
1219
1232
  target_units: { type: 'integer', description: 'Target length in writing units (0 = keep similar).' },
@@ -1228,6 +1241,8 @@ export function registerNovelTools(ctx, clientFactory, options) {
1228
1241
  const body = { chapter_id: args.chapter_id, start: args.start, end: args.end };
1229
1242
  if (args.original_text !== undefined)
1230
1243
  body['original_text'] = args.original_text;
1244
+ if (args.expected_document_revision !== undefined)
1245
+ body['expected_document_revision'] = args.expected_document_revision;
1231
1246
  if (args.instruction !== undefined)
1232
1247
  body['instruction'] = args.instruction;
1233
1248
  if (args.action !== undefined)
@@ -1242,10 +1257,15 @@ export function registerNovelTools(ctx, clientFactory, options) {
1242
1257
  ctx.tools.register(defineTool({
1243
1258
  name: 'novel_revision_create_from_review',
1244
1259
  description: 'Create a revision proposal that addresses selected issues from a chapter review (LONG-RUNNING, model-generated). ' +
1245
- 'Issue ids come from novel_review_chapter\'s issue_details. The proposal is only staged — apply it with novel_revision_apply.',
1260
+ 'Issue ids come from novel_review_chapter\'s issue_details. Pass selection to constrain rewriting to an exact manuscript range; overlapping issue evidence never expands that range. The proposal is only staged — apply it with novel_revision_apply.',
1246
1261
  parameters: {
1247
1262
  chapter_id: { type: 'string', required: true, description: 'Chapter id (ch_<digits>).' },
1248
1263
  issue_ids: { type: 'array', required: true, items: { type: 'string' }, description: 'Review issue ids to fix.' },
1264
+ selection: { type: 'object', additionalProperties: false, description: 'Optional exact saved-text range in Unicode code points; preserve raw CR/LF characters.', properties: {
1265
+ start: { type: 'integer', required: true, description: 'Inclusive source code-point offset.' },
1266
+ end: { type: 'integer', required: true, description: 'Exclusive source code-point offset.' },
1267
+ original_text: { type: 'string', required: true, description: 'Exact saved text inside this range.' },
1268
+ } },
1249
1269
  instruction: { type: 'string', description: 'Additional revision instruction.' },
1250
1270
  target_units: { type: 'integer', description: 'Target length in writing units (0 = keep similar).' },
1251
1271
  expected_review_revision: { type: 'string', description: 'Exact review file revision from the chapter work brief.' },
@@ -1260,6 +1280,8 @@ export function registerNovelTools(ctx, clientFactory, options) {
1260
1280
  if (args.issue_ids.length === 0)
1261
1281
  throw new Error('issue_ids must be a non-empty array');
1262
1282
  const body = { chapter_id: args.chapter_id, issue_ids: args.issue_ids };
1283
+ if (args.selection !== undefined)
1284
+ body['selection'] = args.selection;
1263
1285
  if (args.instruction !== undefined)
1264
1286
  body['instruction'] = args.instruction;
1265
1287
  if (args.target_units !== undefined)
@@ -2027,6 +2049,7 @@ export function registerNovelTools(ctx, clientFactory, options) {
2027
2049
  'confirm: true), "cancel" (run_id + revision, optional reason). All mutating actions are revision-gated: ' +
2028
2050
  'pass the run\'s current revision from a list/get call.',
2029
2051
  parameters: {
2052
+ include_archived: { type: 'boolean', description: 'For list: include historical runs moved to chapter_deletions when a chapter was deleted. Archived runs are read-only and include archive_directory.' },
2030
2053
  action: { type: 'string', required: true, enum: ['list', 'get', 'record_intervention', 'update_intervention', 'cancel', 'archive'], description: 'The chapter-run operation.' },
2031
2054
  run_id: { type: 'string', description: 'Run id (required for get/record_intervention/update_intervention/cancel).' },
2032
2055
  revision: { type: 'string', description: 'Current run revision (required for mutating actions).' },
@@ -2053,7 +2076,7 @@ export function registerNovelTools(ctx, clientFactory, options) {
2053
2076
  async execute(args, exec) {
2054
2077
  const client = clientFor(exec);
2055
2078
  const body = { action: args.action };
2056
- 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']) {
2079
+ for (const key of ['run_id', 'revision', 'chapter_id', 'statuses', 'since', 'limit', 'include_archived', 'scope', 'risk', 'request', 'affected_items', 'rewrite_required', 'intervention_id', 'state', 'facts_revision', 'impact', 'proposal', 'confirm', 'reason']) {
2057
2080
  const value = args[key];
2058
2081
  if (value !== undefined)
2059
2082
  body[key] = value;