@osovv/vv-opencode 1.5.0 → 1.7.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +41 -10
  3. package/dist/lib/agent-tool-catalog.d.ts +144 -0
  4. package/dist/lib/agent-tool-catalog.js +2551 -0
  5. package/dist/lib/agent-tool-catalog.js.map +1 -0
  6. package/dist/lib/agent-tool-contract.d.ts +200 -0
  7. package/dist/lib/agent-tool-contract.js +407 -0
  8. package/dist/lib/agent-tool-contract.js.map +1 -0
  9. package/dist/lib/workflow-contract.d.ts +12 -0
  10. package/dist/lib/workflow-contract.js +71 -18
  11. package/dist/lib/workflow-contract.js.map +1 -1
  12. package/dist/plugins/hashline-edit/index.js +102 -97
  13. package/dist/plugins/hashline-edit/index.js.map +1 -1
  14. package/dist/plugins/hashline-edit/normalize-edits.d.ts +2 -8
  15. package/dist/plugins/hashline-edit/normalize-edits.js +32 -17
  16. package/dist/plugins/hashline-edit/normalize-edits.js.map +1 -1
  17. package/dist/plugins/hashline-edit/schemas.d.ts +246 -0
  18. package/dist/plugins/hashline-edit/schemas.js +659 -0
  19. package/dist/plugins/hashline-edit/schemas.js.map +1 -0
  20. package/dist/plugins/hashline-edit/str-replace-editor.d.ts +6 -17
  21. package/dist/plugins/hashline-edit/str-replace-editor.js +19 -21
  22. package/dist/plugins/hashline-edit/str-replace-editor.js.map +1 -1
  23. package/dist/plugins/hashline-edit/tool-description.d.ts +1 -0
  24. package/dist/plugins/hashline-edit/tool-description.js +20 -4
  25. package/dist/plugins/hashline-edit/tool-description.js.map +1 -1
  26. package/dist/plugins/system-context-injection/index.js +9 -4
  27. package/dist/plugins/system-context-injection/index.js.map +1 -1
  28. package/dist/plugins/web-tools/fetch-service.d.ts +8 -5
  29. package/dist/plugins/web-tools/fetch-service.js +30 -45
  30. package/dist/plugins/web-tools/fetch-service.js.map +1 -1
  31. package/dist/plugins/web-tools/index.js +23 -5
  32. package/dist/plugins/web-tools/index.js.map +1 -1
  33. package/dist/plugins/web-tools/schemas.d.ts +469 -0
  34. package/dist/plugins/web-tools/schemas.js +523 -0
  35. package/dist/plugins/web-tools/schemas.js.map +1 -0
  36. package/dist/plugins/web-tools/search-service.d.ts +6 -3
  37. package/dist/plugins/web-tools/search-service.js +24 -29
  38. package/dist/plugins/web-tools/search-service.js.map +1 -1
  39. package/dist/plugins/workflow/authority.d.ts +6 -0
  40. package/dist/plugins/workflow/authority.js +45 -10
  41. package/dist/plugins/workflow/authority.js.map +1 -1
  42. package/dist/plugins/workflow/authorization.js +16 -8
  43. package/dist/plugins/workflow/authorization.js.map +1 -1
  44. package/dist/plugins/workflow/checkpoints.d.ts +41 -2
  45. package/dist/plugins/workflow/checkpoints.js +176 -47
  46. package/dist/plugins/workflow/checkpoints.js.map +1 -1
  47. package/dist/plugins/workflow/delegated.d.ts +15 -1
  48. package/dist/plugins/workflow/delegated.js +65 -35
  49. package/dist/plugins/workflow/delegated.js.map +1 -1
  50. package/dist/plugins/workflow/execution.d.ts +15 -30
  51. package/dist/plugins/workflow/execution.js +133 -33
  52. package/dist/plugins/workflow/execution.js.map +1 -1
  53. package/dist/plugins/workflow/index.js +177 -112
  54. package/dist/plugins/workflow/index.js.map +1 -1
  55. package/dist/plugins/workflow/input-validation.d.ts +48 -0
  56. package/dist/plugins/workflow/input-validation.js +536 -0
  57. package/dist/plugins/workflow/input-validation.js.map +1 -0
  58. package/dist/plugins/workflow/inspection.d.ts +60 -0
  59. package/dist/plugins/workflow/inspection.js +316 -0
  60. package/dist/plugins/workflow/inspection.js.map +1 -0
  61. package/dist/plugins/workflow/protocol.d.ts +2 -0
  62. package/dist/plugins/workflow/protocol.js +26 -4
  63. package/dist/plugins/workflow/protocol.js.map +1 -1
  64. package/dist/plugins/workflow/recovery.d.ts +2 -1
  65. package/dist/plugins/workflow/recovery.js +79 -12
  66. package/dist/plugins/workflow/recovery.js.map +1 -1
  67. package/dist/plugins/workflow/repair.d.ts +1 -1
  68. package/dist/plugins/workflow/repair.js +9 -10
  69. package/dist/plugins/workflow/repair.js.map +1 -1
  70. package/dist/plugins/workflow/results.d.ts +2566 -0
  71. package/dist/plugins/workflow/results.js +1111 -0
  72. package/dist/plugins/workflow/results.js.map +1 -0
  73. package/dist/plugins/workflow/schemas.d.ts +214 -24
  74. package/dist/plugins/workflow/schemas.js +350 -89
  75. package/dist/plugins/workflow/schemas.js.map +1 -1
  76. package/dist/plugins/workflow/state.d.ts +3 -2
  77. package/dist/plugins/workflow/state.js +15 -8
  78. package/dist/plugins/workflow/state.js.map +1 -1
  79. package/dist/plugins/workflow/system-instruction.md +17 -1
  80. package/dist/plugins/workflow/tooling.d.ts +2 -18
  81. package/dist/plugins/workflow/tooling.js +477 -460
  82. package/dist/plugins/workflow/tooling.js.map +1 -1
  83. package/dist/plugins/workflow/transactions.d.ts +13 -0
  84. package/dist/plugins/workflow/transactions.js +13 -3
  85. package/dist/plugins/workflow/transactions.js.map +1 -1
  86. package/package.json +6 -3
  87. package/schemas/vvoc/v3.json +1 -1
  88. package/templates/agents/vv-code-reviewer.md +13 -4
  89. package/templates/agents/vv-controller.md +17 -0
  90. package/templates/agents/vv-implementer.md +20 -9
  91. package/templates/agents/vv-spec-reviewer.md +13 -4
  92. package/templates/skills/vv-execute/SKILL.md +3 -3
  93. package/templates/skills/vv-execute/references/tool-contracts.md +1750 -0
  94. package/templates/skills/vv-review/SKILL.md +1 -1
@@ -0,0 +1,2551 @@
1
+ // FILE: src/lib/agent-tool-catalog.ts
2
+ // VERSION: 1.0.0
3
+ // START_MODULE_CONTRACT
4
+ // PURPOSE: Own the pure nine-tool agent-tool catalog: one entry per vvoc-owned registered tool aggregating its descriptor contract, model-facing vocabularies, checked positive/negative operation fixtures, execute-time defaults, state/host prerequisites, conditional requirements, path kinds, result variants, representative errors, a deterministic generated reference renderer, a fixture runner over the actual exported validators/result schemas, and the recorded contract-size baseline.
5
+ // SCOPE: Catalog aggregation and reference generation only. Imports descriptor modules (workflow input-validation/results, edit schemas, web schemas) and shared contract primitives; never imports plugin factories, config, stores, filesystem, network, or workflow state, so no catalog-to-runtime cycle exists. Fixtures are pure synthetic data validated through the owning runtime validators; the catalog grants no permission, state eligibility, or acceptance.
6
+ // DEPENDS: [@opencode-ai/plugin, src/lib/agent-tool-contract.ts, src/plugins/workflow/input-validation.ts, src/plugins/workflow/results.ts, src/plugins/hashline-edit/schemas.ts, src/plugins/web-tools/schemas.ts, zod (types)]
7
+ // LINKS: [M-AGENT-TOOL-CONTRACT, M-WORKFLOW-TOOLING, M-PLUGIN-HASHLINE-EDIT, M-PLUGIN-WEB-TOOLS]
8
+ // ROLE: RUNTIME
9
+ // MAP_MODE: EXPORTS
10
+ // END_MODULE_CONTRACT
11
+ //
12
+ // START_MODULE_MAP
13
+ // CatalogOperation - One checked positive/negative input fixture with coverage metadata.
14
+ // CatalogVocabulary - A model-facing closed vocabulary for one discriminated input field.
15
+ // CatalogDefault - An execute-time default applied by a tool schema.
16
+ // CatalogPathKind - Declared meaning and rules for one path argument.
17
+ // CatalogResultVariant - One checked result-family fixture for a tool.
18
+ // CatalogErrorExample - Representative failure diagnostic path/expectation.
19
+ // CatalogValidationResult - Structural validation outcome exposed for fixture checks.
20
+ // AgentToolCatalogEntry - Aggregated catalog entry for one owned tool.
21
+ // CONTRACT_REFERENCE_PACKAGE_PATH - Package-relative path of the generated reference document.
22
+ // CONTRACT_SIZE_BASELINE - Recorded pre-change contract-size baseline with provenance.
23
+ // agentToolCatalog - The nine owned tool catalog entries.
24
+ // AGENT_TOOL_CATALOG_TOOL_IDS - Sorted tool ids covered by the catalog.
25
+ // catalogToolIds - Sorted tool ids covered by the catalog.
26
+ // measureCatalogContractSize - Current description/input-schema UTF-8 byte measurement.
27
+ // validateAgentToolCatalog - Run every catalog fixture through the actual validators/result schemas.
28
+ // ResultBranchAncestor - Nested object-path requirement a result fixture must satisfy.
29
+ // ResultBranchLocation - One union branch location with its fixture value path.
30
+ // collectResultBranches - Enumerate nested discriminated-union branches in a result schema.
31
+ // resultBranchGaps - Result-schema branches a single entry's fixtures do not cover.
32
+ // resultCoverageGaps - Result-schema union branches not covered by any catalog fixture.
33
+ // findOpaqueInputObjects - Arbitrary (non-closed) object/unconstrained nodes in a published input schema.
34
+ // renderToolContractsReference - Deterministic Markdown reference generated from the catalog.
35
+ // END_MODULE_MAP
36
+ //
37
+ // START_CHANGE_SUMMARY
38
+ // LAST_CHANGE: [C-AGENT-TOOL-CONTRACTS T-008 correction r2 - Array-element traversal with finite-primitive (boolean/number) literal discriminators so batch item and nested provider families are coverage-checked; added the batch-failure fixture and the per-operation/branch negative fixtures for review_only, generic open paths, request_changes/rework, verify/review/bind/amend/record_approval/revoke_authority, delete/rename/replace/prepend, and every freshness/format branch. Prior: honest hashline text+metadata aggregate, nested provider coverage, closed-schema scanning, outcome-validated reference examples.]
39
+ // END_CHANGE_SUMMARY
40
+ import { tool } from "@opencode-ai/plugin";
41
+ import { AGENT_TOOL_CONTRACT_REVISION, PACKAGE_NAME, PACKAGE_VERSION, } from "./agent-tool-contract.js";
42
+ import { validateWorkflowToolInput, workflowToolContracts, } from "../plugins/workflow/input-validation.js";
43
+ import { workflowToolResultSchemas } from "../plugins/workflow/results.js";
44
+ import { HASHLINE_EDIT_TOOL_ID, STR_REPLACE_EDITOR_TOOL_ID, hashlineEditContract, hashlineEditMetadataSchema, strReplaceEditorContract, strReplaceEditorResultSchema, validateHashlineEditToolInput, validateStrReplaceEditorToolInput, } from "../plugins/hashline-edit/schemas.js";
45
+ import { WEB_FETCH_DEFAULT_TIMEOUT_SECONDS, WEB_SEARCH_DEFAULT_COUNT, WEB_FETCH_TOOL_ID, WEB_SEARCH_TOOL_ID, validateWebFetchToolInput, validateWebSearchToolInput, webFetchContract, webFetchResultSchema, webSearchContract, webSearchResultSchema, } from "../plugins/web-tools/schemas.js";
46
+ // END_BLOCK_TYPES
47
+ // START_BLOCK_SHARED_FIXTURES
48
+ /** Read-only execution view fixture shared by generic result families. */
49
+ function executionView(overrides = {}) {
50
+ return {
51
+ runId: "run-1",
52
+ sessionId: "session-1",
53
+ executionKey: "exec-1",
54
+ sourceKind: "conversation-scoped",
55
+ goal: "Deliver the scoped work.",
56
+ state: "active",
57
+ revision: 1,
58
+ tasks: [],
59
+ checkpoints: [],
60
+ ...overrides,
61
+ };
62
+ }
63
+ /** A minimal valid closed work-item input entry. */
64
+ function item(overrides = {}) {
65
+ return {
66
+ key: "task-key",
67
+ title: "Task title",
68
+ mode: "implementation",
69
+ requiredReviewers: ["spec"],
70
+ ...overrides,
71
+ };
72
+ }
73
+ function wrap(validation) {
74
+ return validation.ok
75
+ ? { ok: true, data: validation.data }
76
+ : { ok: false, issues: validation.issues };
77
+ }
78
+ // END_BLOCK_SHARED_FIXTURES
79
+ // START_BLOCK_WORKFLOW_ENTRIES
80
+ const workflowEntries = [
81
+ {
82
+ toolId: "work_item_open",
83
+ summary: "Open one or more work items idempotently, or register/append generic execution tasks.",
84
+ contract: workflowToolContracts[0],
85
+ validate: (raw) => wrap(validateWorkflowToolInput("work_item_open", raw)),
86
+ resultSchema: workflowToolResultSchemas.work_item_open,
87
+ vocabularies: [
88
+ { field: "items[].mode", values: ["implementation", "review_only", "delegated"] },
89
+ { field: "execution.source.kind", values: ["conversation-scoped", "provided-plan"] },
90
+ ],
91
+ operations: [
92
+ {
93
+ id: "work_item_open:standalone-implementation",
94
+ label: "standalone implementation with reviewers",
95
+ expect: "accept",
96
+ input: { items: [item()] },
97
+ covers: { "items[].mode": "implementation" },
98
+ },
99
+ {
100
+ id: "work_item_open:standalone-review-only",
101
+ label: "standalone review_only with reviewers",
102
+ expect: "accept",
103
+ input: { items: [item({ mode: "review_only" })] },
104
+ covers: { "items[].mode": "review_only" },
105
+ },
106
+ {
107
+ id: "work_item_open:standalone-delegated",
108
+ label: "standalone delegated with empty reviewers and a write scope",
109
+ expect: "accept",
110
+ input: {
111
+ items: [
112
+ item({
113
+ mode: "delegated",
114
+ requiredReviewers: [],
115
+ writeScope: ["src/lib/a.ts"],
116
+ }),
117
+ ],
118
+ },
119
+ covers: { "items[].mode": "delegated" },
120
+ },
121
+ {
122
+ id: "work_item_open:generic-register-conversation",
123
+ label: "generic register from a conversation-scoped source",
124
+ expect: "accept",
125
+ input: {
126
+ items: [
127
+ item({
128
+ mode: "delegated",
129
+ requiredReviewers: [],
130
+ writeScope: ["src/lib/a.ts"],
131
+ taskId: "T-100",
132
+ }),
133
+ ],
134
+ execution: {
135
+ executionKey: "exec-1",
136
+ source: { kind: "conversation-scoped" },
137
+ goal: "Deliver the scoped work.",
138
+ boundary: { files: ["src/lib/a.ts"], directories: ["src/lib/"] },
139
+ },
140
+ },
141
+ covers: { "execution.source.kind": "conversation-scoped" },
142
+ prerequisites: ["trusted workspace root from the plugin context"],
143
+ },
144
+ {
145
+ id: "work_item_open:generic-register-provided-plan",
146
+ label: "generic register from a provided plan reference",
147
+ expect: "accept",
148
+ input: {
149
+ items: [
150
+ item({
151
+ mode: "delegated",
152
+ requiredReviewers: [],
153
+ writeScope: ["src/lib/a.ts"],
154
+ taskId: "T-101",
155
+ }),
156
+ ],
157
+ execution: {
158
+ executionKey: "exec-2",
159
+ source: { kind: "provided-plan", reference: "docs/plan.xml", sha256: "abc123" },
160
+ goal: "Deliver the provided plan.",
161
+ boundary: { files: ["src/lib/a.ts"], directories: [] },
162
+ },
163
+ },
164
+ covers: { "execution.source.kind": "provided-plan" },
165
+ prerequisites: ["trusted workspace root from the plugin context"],
166
+ },
167
+ {
168
+ id: "work_item_open:generic-append",
169
+ label: "append to an existing generic execution",
170
+ expect: "accept",
171
+ input: {
172
+ items: [
173
+ item({
174
+ mode: "delegated",
175
+ requiredReviewers: [],
176
+ writeScope: ["src/lib/a.ts"],
177
+ taskId: "T-102",
178
+ }),
179
+ ],
180
+ runId: "run-existing",
181
+ amendmentId: "amend-1",
182
+ rationale: "Append the follow-up.",
183
+ },
184
+ prerequisites: ["existing generic execution runId"],
185
+ },
186
+ {
187
+ id: "work_item_open:reject-unknown-key",
188
+ label: "unknown nested item key is rejected",
189
+ expect: "reject",
190
+ input: { items: [item({ typo: true })] },
191
+ tags: ["unknown_key"],
192
+ },
193
+ {
194
+ id: "work_item_open:reject-unsupported-mode",
195
+ label: "an unsupported item mode is rejected",
196
+ expect: "reject",
197
+ input: { items: [item({ mode: "review" })] },
198
+ tags: ["invalid_value"],
199
+ },
200
+ {
201
+ id: "work_item_open:reject-empty-items",
202
+ label: "empty batch is rejected",
203
+ expect: "reject",
204
+ input: { items: [] },
205
+ tags: ["missing_required"],
206
+ },
207
+ {
208
+ id: "work_item_open:reject-empty-reviewers",
209
+ label: "standalone implementation with empty reviewers is rejected",
210
+ expect: "reject",
211
+ input: { items: [item({ requiredReviewers: [] })] },
212
+ tags: ["missing_required"],
213
+ },
214
+ {
215
+ id: "work_item_open:reject-execution-and-runid",
216
+ label: "execution and runId are mutually exclusive",
217
+ expect: "reject",
218
+ input: {
219
+ items: [item({ mode: "delegated", requiredReviewers: [], writeScope: ["src/lib/a.ts"] })],
220
+ runId: "run-1",
221
+ execution: {
222
+ executionKey: "exec-1",
223
+ source: { kind: "conversation-scoped" },
224
+ goal: "Deliver.",
225
+ boundary: { files: ["src/lib/a.ts"], directories: [] },
226
+ },
227
+ },
228
+ tags: ["conflict"],
229
+ },
230
+ {
231
+ id: "work_item_open:reject-unsupported-source-kind",
232
+ label: "native-package source kind is not accepted by the generic descriptor",
233
+ expect: "reject",
234
+ input: {
235
+ items: [item({ mode: "delegated", requiredReviewers: [], writeScope: ["src/lib/a.ts"] })],
236
+ execution: {
237
+ executionKey: "exec-1",
238
+ source: { kind: "native-package" },
239
+ goal: "Deliver.",
240
+ boundary: { files: ["src/lib/a.ts"], directories: [] },
241
+ },
242
+ },
243
+ tags: ["invalid_value"],
244
+ },
245
+ {
246
+ id: "work_item_open:reject-provided-plan-missing-reference",
247
+ label: "provided plan without a reference is rejected",
248
+ expect: "reject",
249
+ input: {
250
+ items: [item({ mode: "delegated", requiredReviewers: [], writeScope: ["src/lib/a.ts"] })],
251
+ execution: {
252
+ executionKey: "exec-1",
253
+ source: { kind: "provided-plan" },
254
+ goal: "Deliver.",
255
+ boundary: { files: ["src/lib/a.ts"], directories: [] },
256
+ },
257
+ },
258
+ tags: ["missing_required"],
259
+ },
260
+ {
261
+ id: "work_item_open:reject-delegated-reviewers",
262
+ label: "standalone delegated with reviewers is rejected",
263
+ expect: "reject",
264
+ input: {
265
+ items: [
266
+ item({ mode: "delegated", requiredReviewers: ["spec"], writeScope: ["src/lib/a.ts"] }),
267
+ ],
268
+ },
269
+ tags: ["conflict"],
270
+ },
271
+ {
272
+ id: "work_item_open:reject-review-only-empty-reviewers",
273
+ label: "standalone review_only with empty reviewers is rejected",
274
+ expect: "reject",
275
+ input: { items: [item({ mode: "review_only", requiredReviewers: [] })] },
276
+ tags: ["missing_required"],
277
+ },
278
+ {
279
+ id: "work_item_open:reject-generic-register-missing-goal",
280
+ label: "generic register from a conversation source without a goal is rejected",
281
+ expect: "reject",
282
+ input: {
283
+ items: [item({ mode: "delegated", requiredReviewers: [], writeScope: ["src/lib/a.ts"] })],
284
+ execution: {
285
+ executionKey: "exec-1",
286
+ source: { kind: "conversation-scoped" },
287
+ goal: " ",
288
+ boundary: { files: ["src/lib/a.ts"], directories: [] },
289
+ },
290
+ },
291
+ tags: ["missing_required"],
292
+ },
293
+ {
294
+ id: "work_item_open:reject-generic-append-missing-amendment",
295
+ label: "generic append without amendment context is rejected",
296
+ expect: "reject",
297
+ input: {
298
+ items: [item({ mode: "delegated", requiredReviewers: [], writeScope: ["src/lib/a.ts"] })],
299
+ runId: "run-existing",
300
+ },
301
+ tags: ["missing_required"],
302
+ },
303
+ ],
304
+ defaults: [],
305
+ prerequisites: [
306
+ "trusted workspace root from the plugin context for every generic registration",
307
+ "existing generic runId for appends",
308
+ ],
309
+ conditionals: [
310
+ "execution and runId are mutually exclusive",
311
+ "amendmentId and rationale are only valid with a runId append",
312
+ "generic items require mode delegated and a non-empty write scope of exact files",
313
+ "standalone implementation/review_only require a unique non-empty reviewer set and forbid writeScope",
314
+ ],
315
+ pathKinds: [
316
+ {
317
+ field: "items[].writeScope[]",
318
+ kind: "exact-file",
319
+ rules: "workspace-relative exact file paths; wildcards, traversal, absolute/home/drive paths, backslashes, and trailing separators are rejected",
320
+ },
321
+ {
322
+ field: "execution.boundary.files[]",
323
+ kind: "exact-file",
324
+ rules: "workspace-relative exact file paths inside the execution boundary",
325
+ },
326
+ {
327
+ field: "execution.boundary.directories[]",
328
+ kind: "directory-subtree",
329
+ rules: "workspace-relative directory subtrees; one trailing separator is normalized away",
330
+ },
331
+ ],
332
+ results: [
333
+ {
334
+ id: "work_item_open:batch",
335
+ label: "standalone batch envelope (per-item ok/failure, no top-level ok)",
336
+ fixture: {
337
+ tool: "work_item_open",
338
+ sessionId: "session-1",
339
+ items: [
340
+ {
341
+ ok: true,
342
+ reused: false,
343
+ workItemId: "wi-1",
344
+ header: "VVOC_WORK_ITEM_ID: wi-1",
345
+ key: "task-key",
346
+ title: "Task title",
347
+ mode: "implementation",
348
+ requiredReviewers: ["spec"],
349
+ state: "open",
350
+ specReviewCount: 0,
351
+ codeReviewCount: 0,
352
+ reviewRound: 0,
353
+ completedReviewRoundCount: 0,
354
+ createdAt: "2026-01-01T00:00:00.000Z",
355
+ updatedAt: "2026-01-01T00:00:00.000Z",
356
+ },
357
+ ],
358
+ },
359
+ },
360
+ {
361
+ id: "work_item_open:batch-failure",
362
+ label: "standalone batch envelope with a per-item failure",
363
+ fixture: {
364
+ tool: "work_item_open",
365
+ sessionId: "session-1",
366
+ items: [
367
+ {
368
+ ok: false,
369
+ errorCode: "ALREADY_OPEN",
370
+ category: "state",
371
+ message: "ALREADY_OPEN: work item is already open",
372
+ existingWorkItemId: "wi-1",
373
+ state: "open",
374
+ prerequisite: "no open item with the same key",
375
+ nextAction: "reuse the existing work item",
376
+ },
377
+ ],
378
+ },
379
+ },
380
+ {
381
+ id: "work_item_open:register",
382
+ label: "generic execution registration",
383
+ fixture: {
384
+ tool: "work_item_open",
385
+ sessionId: "session-1",
386
+ ok: true,
387
+ action: "register",
388
+ runId: "run-1",
389
+ reused: false,
390
+ execution: executionView(),
391
+ },
392
+ },
393
+ {
394
+ id: "work_item_open:amend",
395
+ label: "generic execution amendment",
396
+ fixture: {
397
+ tool: "work_item_open",
398
+ sessionId: "session-1",
399
+ ok: true,
400
+ action: "amend",
401
+ runId: "run-1",
402
+ revision: 2,
403
+ execution: executionView(),
404
+ },
405
+ },
406
+ {
407
+ id: "work_item_open:failure",
408
+ label: "bounded owned failure",
409
+ fixture: {
410
+ tool: "work_item_open",
411
+ sessionId: "session-1",
412
+ ok: false,
413
+ errorCode: "INVALID_INPUT",
414
+ category: "input",
415
+ message: "INVALID_INPUT: items[0].typo: unrecognized key",
416
+ },
417
+ },
418
+ ],
419
+ errors: [
420
+ { path: "items[0].typo", expectation: "unknown nested item key is rejected with its path" },
421
+ {
422
+ path: "execution.source.reference",
423
+ expectation: "provided plan without a reference names the source field",
424
+ },
425
+ ],
426
+ normalization: [
427
+ "documented trims are applied before bounds; unknown keys are never stripped",
428
+ "reviewer and stage enums are canonical; unsupported values reject rather than filter",
429
+ ],
430
+ },
431
+ {
432
+ toolId: "work_item_list",
433
+ summary: "List current-session work items, native plan runs, and generic executions with contract identity.",
434
+ contract: workflowToolContracts[1],
435
+ validate: (raw) => wrap(validateWorkflowToolInput("work_item_list", raw)),
436
+ resultSchema: workflowToolResultSchemas.work_item_list,
437
+ vocabularies: [],
438
+ operations: [
439
+ {
440
+ id: "work_item_list:default",
441
+ label: "list without includeClosed",
442
+ expect: "accept",
443
+ input: {},
444
+ },
445
+ {
446
+ id: "work_item_list:include-closed",
447
+ label: "list including closed items",
448
+ expect: "accept",
449
+ input: { includeClosed: true },
450
+ },
451
+ {
452
+ id: "work_item_list:reject-unknown-key",
453
+ label: "unknown key is rejected",
454
+ expect: "reject",
455
+ input: { includeClosed: false, extra: true },
456
+ tags: ["unknown_key"],
457
+ },
458
+ {
459
+ id: "work_item_list:reject-wrong-type",
460
+ label: "non-boolean includeClosed is rejected",
461
+ expect: "reject",
462
+ input: { includeClosed: "true" },
463
+ tags: ["invalid_value"],
464
+ },
465
+ {
466
+ id: "work_item_list:reject-unknown-key-default",
467
+ label: "unknown key without includeClosed is rejected",
468
+ expect: "reject",
469
+ input: { extra: true },
470
+ tags: ["unknown_key"],
471
+ },
472
+ {
473
+ id: "work_item_list:reject-unknown-key-include-closed",
474
+ label: "unknown key with includeClosed true is rejected",
475
+ expect: "reject",
476
+ input: { includeClosed: true, extra: true },
477
+ tags: ["unknown_key"],
478
+ },
479
+ ],
480
+ defaults: [],
481
+ prerequisites: ["same-session store hydration from the plugin context"],
482
+ conditionals: ["includeClosed defaults to false and is optional"],
483
+ pathKinds: [],
484
+ results: [
485
+ {
486
+ id: "work_item_list:view",
487
+ label: "inspection view with loaded contract identity",
488
+ fixture: {
489
+ tool: "work_item_list",
490
+ sessionId: "session-1",
491
+ includeClosed: false,
492
+ items: [],
493
+ contract: {
494
+ packageName: PACKAGE_NAME,
495
+ packageVersion: PACKAGE_VERSION,
496
+ toolContractRevision: AGENT_TOOL_CONTRACT_REVISION,
497
+ referencePath: "templates/skills/vv-execute/references/tool-contracts.md",
498
+ },
499
+ },
500
+ },
501
+ {
502
+ id: "work_item_list:failure",
503
+ label: "bounded owned failure",
504
+ fixture: {
505
+ tool: "work_item_list",
506
+ sessionId: "session-1",
507
+ ok: false,
508
+ errorCode: "PERSISTENCE_FAILED",
509
+ category: "persistence",
510
+ message: "PERSISTENCE_FAILED: state could not be read",
511
+ },
512
+ },
513
+ ],
514
+ errors: [
515
+ { path: "includeClosed", expectation: "a non-boolean value is rejected rather than coerced" },
516
+ ],
517
+ normalization: ["includeClosed is read strictly; foreign-session data is never exposed"],
518
+ },
519
+ {
520
+ toolId: "work_item_close",
521
+ summary: "Close a same-session work item once its reviews are complete and no concerns remain open.",
522
+ contract: workflowToolContracts[2],
523
+ validate: (raw) => wrap(validateWorkflowToolInput("work_item_close", raw)),
524
+ resultSchema: workflowToolResultSchemas.work_item_close,
525
+ vocabularies: [],
526
+ operations: [
527
+ {
528
+ id: "work_item_close:close",
529
+ label: "close a ready work item",
530
+ expect: "accept",
531
+ input: { workItemId: "wi-1" },
532
+ prerequisites: ["work item is ready_to_close"],
533
+ },
534
+ {
535
+ id: "work_item_close:reject-unknown-key",
536
+ label: "unknown key is rejected",
537
+ expect: "reject",
538
+ input: { workItemId: "wi-1", extra: true },
539
+ tags: ["unknown_key"],
540
+ },
541
+ {
542
+ id: "work_item_close:reject-blank-id",
543
+ label: "blank work item id is rejected",
544
+ expect: "reject",
545
+ input: { workItemId: " " },
546
+ tags: ["missing_required"],
547
+ },
548
+ ],
549
+ defaults: [],
550
+ prerequisites: ["same-session work item in ready_to_close state"],
551
+ conditionals: ["open concerns or pending reviews refuse the close with the unmet prerequisite"],
552
+ pathKinds: [],
553
+ results: [
554
+ {
555
+ id: "work_item_close:success",
556
+ label: "closed work item",
557
+ fixture: {
558
+ tool: "work_item_close",
559
+ sessionId: "session-1",
560
+ ok: true,
561
+ workItemId: "wi-1",
562
+ header: "VVOC_WORK_ITEM_ID: wi-1",
563
+ state: "closed",
564
+ closedAt: "2026-01-01T00:00:00.000Z",
565
+ },
566
+ },
567
+ {
568
+ id: "work_item_close:failure",
569
+ label: "bounded owned failure",
570
+ fixture: {
571
+ tool: "work_item_close",
572
+ sessionId: "session-1",
573
+ ok: false,
574
+ errorCode: "READY_TO_CLOSE_REQUIRED",
575
+ category: "state",
576
+ message: "READY_TO_CLOSE_REQUIRED: pending reviews remain",
577
+ state: "awaiting_reviews",
578
+ },
579
+ },
580
+ ],
581
+ errors: [
582
+ { path: "workItemId", expectation: "blank ids are rejected before any state mutation" },
583
+ ],
584
+ normalization: ["the id is trimmed; unknown fields never reach the store"],
585
+ },
586
+ {
587
+ toolId: "work_item_decide",
588
+ summary: "Accept, request changes, authorize bounded rework, or recover a stopped delegated attempt.",
589
+ contract: workflowToolContracts[3],
590
+ validate: (raw) => wrap(validateWorkflowToolInput("work_item_decide", raw)),
591
+ resultSchema: workflowToolResultSchemas.work_item_decide,
592
+ vocabularies: [
593
+ { field: "decision", values: ["accept", "request_changes", "rework", "recover"] },
594
+ ],
595
+ operations: [
596
+ {
597
+ id: "work_item_decide:accept",
598
+ label: "accept a completed attempt with evidence",
599
+ expect: "accept",
600
+ input: {
601
+ workItemId: "wi-1",
602
+ attempt: 1,
603
+ decision: "accept",
604
+ rationale: "Verified against the acceptance criteria.",
605
+ evidence: ["bun test"],
606
+ },
607
+ covers: { decision: "accept" },
608
+ prerequisites: ["latest attempt is terminal and acceptance is applicable"],
609
+ },
610
+ {
611
+ id: "work_item_decide:request-changes",
612
+ label: "request changes with a concerns disposition",
613
+ expect: "accept",
614
+ input: {
615
+ workItemId: "wi-1",
616
+ attempt: 1,
617
+ decision: "request_changes",
618
+ rationale: "Fix the edge case.",
619
+ evidence: ["review note"],
620
+ concernsDisposition: "Resolved after rework.",
621
+ },
622
+ covers: { decision: "request_changes" },
623
+ },
624
+ {
625
+ id: "work_item_decide:rework",
626
+ label: "authorize rework from a failed checkpoint",
627
+ expect: "accept",
628
+ input: {
629
+ workItemId: "wi-1",
630
+ attempt: 1,
631
+ decision: "rework",
632
+ runId: "run-1",
633
+ checkpointId: "C-1",
634
+ rationale: "Checkpoint failed.",
635
+ },
636
+ covers: { decision: "rework" },
637
+ prerequisites: ["a failed checkpoint bound to an accepted task"],
638
+ },
639
+ {
640
+ id: "work_item_decide:recover",
641
+ label: "recover a stopped unaccepted task",
642
+ expect: "accept",
643
+ input: {
644
+ workItemId: "wi-1",
645
+ attempt: 2,
646
+ decision: "recover",
647
+ recoveryId: "rec-1",
648
+ diagnosis: "Both attempts stopped.",
649
+ changedCondition: "Packet clarified.",
650
+ verification: ["bun test"],
651
+ },
652
+ covers: { decision: "recover" },
653
+ prerequisites: ["stable identity, bounded diagnosis, and state-specific authorization"],
654
+ },
655
+ {
656
+ id: "work_item_decide:recover-authority",
657
+ label: "recover funded by a recorded advance authority",
658
+ expect: "accept",
659
+ input: {
660
+ workItemId: "wi-1",
661
+ attempt: 2,
662
+ decision: "recover",
663
+ recoveryId: "rec-2",
664
+ diagnosis: "Reserve advance.",
665
+ changedCondition: "Authority recorded.",
666
+ verification: ["bun test"],
667
+ authorityId: "auth-1",
668
+ runId: "run-1",
669
+ },
670
+ prerequisites: ["recorded advance authority on the owning run"],
671
+ },
672
+ {
673
+ id: "work_item_decide:reject-unknown-key",
674
+ label: "unknown key is rejected",
675
+ expect: "reject",
676
+ input: {
677
+ workItemId: "wi-1",
678
+ attempt: 1,
679
+ decision: "accept",
680
+ rationale: "ok",
681
+ evidence: ["x"],
682
+ typo: true,
683
+ },
684
+ tags: ["unknown_key"],
685
+ },
686
+ {
687
+ id: "work_item_decide:reject-unsupported-decision",
688
+ label: "an unsupported decision family is rejected",
689
+ expect: "reject",
690
+ input: { workItemId: "wi-1", attempt: 1, decision: "approve" },
691
+ tags: ["invalid_value"],
692
+ },
693
+ {
694
+ id: "work_item_decide:reject-missing-evidence",
695
+ label: "accept without evidence is rejected",
696
+ expect: "reject",
697
+ input: { workItemId: "wi-1", attempt: 1, decision: "accept", rationale: "ok" },
698
+ tags: ["missing_required"],
699
+ },
700
+ {
701
+ id: "work_item_decide:reject-request-changes-missing-evidence",
702
+ label: "request_changes without evidence is rejected",
703
+ expect: "reject",
704
+ input: { workItemId: "wi-1", attempt: 1, decision: "request_changes" },
705
+ tags: ["missing_required"],
706
+ },
707
+ {
708
+ id: "work_item_decide:reject-rework-missing-checkpoint",
709
+ label: "rework without its failed-checkpoint binding is rejected",
710
+ expect: "reject",
711
+ input: { workItemId: "wi-1", attempt: 1, decision: "rework" },
712
+ tags: ["missing_required"],
713
+ },
714
+ {
715
+ id: "work_item_decide:reject-unconsumed-field",
716
+ label: "a field for another decision is rejected, not ignored",
717
+ expect: "reject",
718
+ input: {
719
+ workItemId: "wi-1",
720
+ attempt: 1,
721
+ decision: "accept",
722
+ rationale: "ok",
723
+ evidence: ["x"],
724
+ recoveryId: "rec-1",
725
+ },
726
+ tags: ["conflict"],
727
+ },
728
+ {
729
+ id: "work_item_decide:reject-runid-without-authority",
730
+ label: "recover runId without authorityId is rejected",
731
+ expect: "reject",
732
+ input: {
733
+ workItemId: "wi-1",
734
+ attempt: 2,
735
+ decision: "recover",
736
+ runId: "run-1",
737
+ recoveryId: "rec-1",
738
+ diagnosis: "d",
739
+ changedCondition: "c",
740
+ verification: ["v"],
741
+ },
742
+ tags: ["conflict"],
743
+ },
744
+ ],
745
+ defaults: [],
746
+ prerequisites: [
747
+ "latest completed attempt identity and terminal status",
748
+ "recorded concerns disposition when the terminal record requires one",
749
+ ],
750
+ conditionals: [
751
+ "accept/request_changes require balanced rationale and evidence",
752
+ "rework requires its failed checkpoint binding",
753
+ "recover requires recoveryId, diagnosis, changedCondition, and verification; runId only accompanies authorityId",
754
+ "concernsDisposition is conditional on the recorded terminal status, not a caller-supplied status",
755
+ ],
756
+ pathKinds: [],
757
+ results: [
758
+ {
759
+ id: "work_item_decide:accept-or-request-changes",
760
+ label: "decision outcome (accept/request_changes)",
761
+ fixture: {
762
+ tool: "work_item_decide",
763
+ sessionId: "session-1",
764
+ ok: true,
765
+ action: "accept",
766
+ workItemId: "wi-1",
767
+ attempt: 1,
768
+ decisionId: "decision-1",
769
+ state: "ready_to_close",
770
+ },
771
+ },
772
+ {
773
+ id: "work_item_decide:rework",
774
+ label: "rework authorization outcome",
775
+ fixture: {
776
+ tool: "work_item_decide",
777
+ sessionId: "session-1",
778
+ ok: true,
779
+ action: "rework",
780
+ workItemId: "wi-1",
781
+ reworkId: "rework-1",
782
+ grantedAttempts: 1,
783
+ state: "awaiting_implementer",
784
+ },
785
+ },
786
+ {
787
+ id: "work_item_decide:recover",
788
+ label: "recovery outcome",
789
+ fixture: {
790
+ tool: "work_item_decide",
791
+ sessionId: "session-1",
792
+ ok: true,
793
+ action: "recover",
794
+ workItemId: "wi-1",
795
+ recoveryId: "rec-1",
796
+ kind: "resume",
797
+ attemptBudget: 3,
798
+ remainingAttempts: 2,
799
+ state: "awaiting_implementer",
800
+ nextAction: "launch_implementer",
801
+ },
802
+ },
803
+ {
804
+ id: "work_item_decide:failure",
805
+ label: "bounded owned failure",
806
+ fixture: {
807
+ tool: "work_item_decide",
808
+ sessionId: "session-1",
809
+ ok: false,
810
+ errorCode: "INVALID_ATTEMPT",
811
+ category: "state",
812
+ message: "INVALID_ATTEMPT: no completed attempt matches",
813
+ attempt: 2,
814
+ nextAction: "call work_item_list to read the latest attempt",
815
+ },
816
+ },
817
+ ],
818
+ errors: [
819
+ { path: "evidence", expectation: "missing required evidence names the field" },
820
+ {
821
+ path: "runId",
822
+ expectation: "runId without authorityId is a conflict, not silently dropped",
823
+ },
824
+ ],
825
+ normalization: ["decision-scoped fields are rejected when another decision is selected"],
826
+ },
827
+ {
828
+ toolId: "work_checkpoint",
829
+ summary: "Register, start, verify, review, bind, amend, complete, or authorize/recover checkpoints and authority.",
830
+ contract: workflowToolContracts[4],
831
+ validate: (raw) => wrap(validateWorkflowToolInput("work_checkpoint", raw)),
832
+ resultSchema: workflowToolResultSchemas.work_checkpoint,
833
+ vocabularies: [
834
+ {
835
+ field: "action",
836
+ values: [
837
+ "register",
838
+ "start",
839
+ "verify",
840
+ "recover",
841
+ "review",
842
+ "bind",
843
+ "complete",
844
+ "amend",
845
+ "authorize",
846
+ "record_approval",
847
+ "revoke_authority",
848
+ ],
849
+ },
850
+ ],
851
+ operations: [
852
+ {
853
+ id: "work_checkpoint:register-native",
854
+ label: "register an approved native plan package",
855
+ expect: "accept",
856
+ input: { action: "register", planPath: ".vvoc/specs/x/plan.xml" },
857
+ covers: { action: "register" },
858
+ prerequisites: ["approved native plan loader and trusted workspace root"],
859
+ },
860
+ {
861
+ id: "work_checkpoint:register-generic",
862
+ label: "generic register appends a task batch to an existing run",
863
+ expect: "accept",
864
+ input: {
865
+ action: "register",
866
+ runId: "run-1",
867
+ amendmentId: "amend-1",
868
+ rationale: "Append a task batch to the existing run.",
869
+ tasks: [item({ mode: "delegated", requiredReviewers: [], writeScope: ["src/lib/a.ts"] })],
870
+ },
871
+ covers: { action: "register" },
872
+ prerequisites: ["existing generic execution runId"],
873
+ },
874
+ {
875
+ id: "work_checkpoint:start",
876
+ label: "start a declared checkpoint generation",
877
+ expect: "accept",
878
+ input: { action: "start", runId: "run-1", checkpointId: "C-1" },
879
+ covers: { action: "start" },
880
+ prerequisites: ["covered tasks accepted and dependencies satisfied"],
881
+ },
882
+ {
883
+ id: "work_checkpoint:verify",
884
+ label: "verify a checkpoint and optionally seal",
885
+ expect: "accept",
886
+ input: { action: "verify", runId: "run-1", checkpointId: "C-1", complete: true },
887
+ covers: { action: "verify" },
888
+ prerequisites: ["linked reviewer records satisfy the checkpoint"],
889
+ },
890
+ {
891
+ id: "work_checkpoint:recover",
892
+ label: "recover a stopped or exhausted checkpoint",
893
+ expect: "accept",
894
+ input: {
895
+ action: "recover",
896
+ runId: "run-1",
897
+ checkpointId: "C-1",
898
+ recoveryId: "rec-1",
899
+ diagnosis: "Generation stopped.",
900
+ changedCondition: "Fresh reviewer assigned.",
901
+ verification: ["bun test"],
902
+ },
903
+ covers: { action: "recover" },
904
+ },
905
+ {
906
+ id: "work_checkpoint:review",
907
+ label: "record a linked reviewer result",
908
+ expect: "accept",
909
+ input: { action: "review", runId: "run-1", checkpointId: "C-1", reviewer: "code" },
910
+ covers: { action: "review" },
911
+ },
912
+ {
913
+ id: "work_checkpoint:bind",
914
+ label: "record the linked reviewer outcome for the generation (launch binding is a host hook)",
915
+ expect: "accept",
916
+ input: { action: "bind", runId: "run-1", checkpointId: "C-1" },
917
+ covers: { action: "bind" },
918
+ },
919
+ {
920
+ id: "work_checkpoint:complete",
921
+ label: "seal an eligible completed execution",
922
+ expect: "accept",
923
+ input: { action: "complete", runId: "run-1", rationale: "All tasks accepted." },
924
+ covers: { action: "complete" },
925
+ },
926
+ {
927
+ id: "work_checkpoint:amend",
928
+ label: "amend an execution with a new task batch",
929
+ expect: "accept",
930
+ input: {
931
+ action: "amend",
932
+ runId: "run-1",
933
+ amendmentId: "amend-1",
934
+ rationale: "Add coverage.",
935
+ },
936
+ covers: { action: "amend" },
937
+ },
938
+ {
939
+ id: "work_checkpoint:authorize",
940
+ label: "authorize delegatable stages and reserved stops",
941
+ expect: "accept",
942
+ input: {
943
+ action: "authorize",
944
+ runId: "run-1",
945
+ authorityId: "auth-1",
946
+ messageId: "msg-1",
947
+ stages: ["implementation"],
948
+ reservedStops: ["specification"],
949
+ },
950
+ covers: { action: "authorize" },
951
+ prerequisites: ["eligible root-user authorization message"],
952
+ },
953
+ {
954
+ id: "work_checkpoint:record-approval",
955
+ label: "record a stage approval",
956
+ expect: "accept",
957
+ input: {
958
+ action: "record_approval",
959
+ runId: "run-1",
960
+ authorityId: "auth-1",
961
+ approvalId: "appr-1",
962
+ stage: "implementation",
963
+ artifactPath: "src/lib/a.ts",
964
+ artifactSha256: "abc123",
965
+ },
966
+ covers: { action: "record_approval" },
967
+ },
968
+ {
969
+ id: "work_checkpoint:revoke-authority",
970
+ label: "revoke authority (narrow or full)",
971
+ expect: "accept",
972
+ input: {
973
+ action: "revoke_authority",
974
+ runId: "run-1",
975
+ authorityId: "auth-1",
976
+ revocationId: "revoke-1",
977
+ },
978
+ covers: { action: "revoke_authority" },
979
+ },
980
+ {
981
+ id: "work_checkpoint:revoke-narrow",
982
+ label: "narrow revocation keeps surviving stages",
983
+ expect: "accept",
984
+ input: {
985
+ action: "revoke_authority",
986
+ runId: "run-1",
987
+ authorityId: "auth-1",
988
+ revocationId: "revoke-2",
989
+ stages: ["verification"],
990
+ rationale: "Keep the final stage.",
991
+ },
992
+ },
993
+ {
994
+ id: "work_checkpoint:reject-unknown-key",
995
+ label: "unknown key is rejected",
996
+ expect: "reject",
997
+ input: { action: "start", runId: "run-1", checkpointId: "C-1", nestedUnknown: { deep: 1 } },
998
+ tags: ["unknown_key"],
999
+ },
1000
+ {
1001
+ id: "work_checkpoint:reject-unsupported-action",
1002
+ label: "an unsupported checkpoint/authority action is rejected",
1003
+ expect: "reject",
1004
+ input: { action: "unknown_action" },
1005
+ tags: ["invalid_value"],
1006
+ },
1007
+ {
1008
+ id: "work_checkpoint:reject-start-missing-runid",
1009
+ label: "start without runId is rejected",
1010
+ expect: "reject",
1011
+ input: { action: "start", checkpointId: "C-1" },
1012
+ tags: ["missing_required"],
1013
+ },
1014
+ {
1015
+ id: "work_checkpoint:reject-register-both-routes",
1016
+ label: "register with planPath and runId is rejected",
1017
+ expect: "reject",
1018
+ input: { action: "register", planPath: ".vvoc/specs/x/plan.xml", runId: "run-1" },
1019
+ tags: ["conflict"],
1020
+ },
1021
+ {
1022
+ id: "work_checkpoint:reject-reserved-stop-typo",
1023
+ label: "a misspelled reserved stop rejects the whole authority operation",
1024
+ expect: "reject",
1025
+ input: {
1026
+ action: "authorize",
1027
+ runId: "run-1",
1028
+ authorityId: "auth-1",
1029
+ messageId: "msg-1",
1030
+ stages: ["implementation"],
1031
+ reservedStops: ["verificaton"],
1032
+ },
1033
+ tags: ["invalid_value"],
1034
+ },
1035
+ {
1036
+ id: "work_checkpoint:reject-unconsumed-action-field",
1037
+ label: "a field for another action is rejected",
1038
+ expect: "reject",
1039
+ input: { action: "complete", runId: "run-1", planPath: ".vvoc/specs/x/plan.xml" },
1040
+ tags: ["conflict"],
1041
+ },
1042
+ {
1043
+ id: "work_checkpoint:reject-recover-incomplete",
1044
+ label: "recover without its bounded fields is rejected",
1045
+ expect: "reject",
1046
+ input: { action: "recover", runId: "run-1", checkpointId: "C-1", recoveryId: "rec-1" },
1047
+ tags: ["missing_required"],
1048
+ },
1049
+ {
1050
+ id: "work_checkpoint:reject-verify-missing-checkpoint",
1051
+ label: "verify without a checkpoint id is rejected",
1052
+ expect: "reject",
1053
+ input: { action: "verify", runId: "run-1" },
1054
+ tags: ["missing_required"],
1055
+ },
1056
+ {
1057
+ id: "work_checkpoint:reject-review-missing-checkpoint",
1058
+ label: "review without a checkpoint id is rejected",
1059
+ expect: "reject",
1060
+ input: { action: "review", runId: "run-1" },
1061
+ tags: ["missing_required"],
1062
+ },
1063
+ {
1064
+ id: "work_checkpoint:reject-bind-missing-checkpoint",
1065
+ label: "bind without a checkpoint id is rejected",
1066
+ expect: "reject",
1067
+ input: { action: "bind", runId: "run-1" },
1068
+ tags: ["missing_required"],
1069
+ },
1070
+ {
1071
+ id: "work_checkpoint:reject-amend-missing-amendment",
1072
+ label: "amend without amendment context is rejected",
1073
+ expect: "reject",
1074
+ input: { action: "amend", runId: "run-1" },
1075
+ tags: ["missing_required"],
1076
+ },
1077
+ {
1078
+ id: "work_checkpoint:reject-record-approval-missing-approval",
1079
+ label: "record_approval without approval identity is rejected",
1080
+ expect: "reject",
1081
+ input: { action: "record_approval", runId: "run-1" },
1082
+ tags: ["missing_required"],
1083
+ },
1084
+ {
1085
+ id: "work_checkpoint:reject-revoke-authority-missing-revocation",
1086
+ label: "revoke_authority without revocation identity is rejected",
1087
+ expect: "reject",
1088
+ input: { action: "revoke_authority", runId: "run-1" },
1089
+ tags: ["missing_required"],
1090
+ },
1091
+ ],
1092
+ defaults: [],
1093
+ prerequisites: [
1094
+ "source (native planPath or generic runId) resolved before source-dependent validation",
1095
+ "eligible root-user authorization message for authorize",
1096
+ ],
1097
+ conditionals: [
1098
+ "native register uses planPath and rejects runId; generic register appends a task batch to an existing runId with amendmentId and rationale",
1099
+ "generic review, bind, and verify consume the linked reviewer items' recorded outcomes; the reviewer callID launch binding happens in the host hook, not through this action",
1100
+ "native verify with complete:true seals only an eligible final checkpoint; generic executions seal through the complete action",
1101
+ "work_checkpoint recover consumes userMessageId only for native-package checkpoints: a generic checkpoint recover rejects userMessageId (a stopped generation resumes cost-free, an exhausted one needs a recorded advance authorityId with its runId). work_item_decide recover accepts userMessageId for standalone, native, and generic execution tasks",
1102
+ "authorize/record_approval/revoke_authority validate every supplied stage/stop before any ledger write",
1103
+ ],
1104
+ pathKinds: [
1105
+ {
1106
+ field: "planPath",
1107
+ kind: "exact-file",
1108
+ rules: "workspace-relative approved native plan path; native register only",
1109
+ },
1110
+ {
1111
+ field: "fileBoundary[]",
1112
+ kind: "exact-file",
1113
+ rules: "workspace-relative exact files recorded with an authority grant",
1114
+ },
1115
+ {
1116
+ field: "artifactPath",
1117
+ kind: "exact-file",
1118
+ rules: "approved artifact path recorded with a stage approval",
1119
+ },
1120
+ ],
1121
+ results: [
1122
+ {
1123
+ id: "work_checkpoint:register",
1124
+ label: "registration/amendment summary",
1125
+ fixture: {
1126
+ tool: "work_checkpoint",
1127
+ sessionId: "session-1",
1128
+ ok: true,
1129
+ action: "register",
1130
+ runId: "run-1",
1131
+ reused: false,
1132
+ tasks: 1,
1133
+ checkpoints: 0,
1134
+ },
1135
+ },
1136
+ {
1137
+ id: "work_checkpoint:amend",
1138
+ label: "generic amendment view",
1139
+ fixture: {
1140
+ tool: "work_checkpoint",
1141
+ sessionId: "session-1",
1142
+ runId: "run-1",
1143
+ ok: true,
1144
+ action: "amend",
1145
+ revision: 2,
1146
+ execution: executionView(),
1147
+ },
1148
+ },
1149
+ {
1150
+ id: "work_checkpoint:start",
1151
+ label: "started generation with reviewers to launch",
1152
+ fixture: {
1153
+ tool: "work_checkpoint",
1154
+ sessionId: "session-1",
1155
+ ok: true,
1156
+ action: "start",
1157
+ runId: "run-1",
1158
+ checkpointId: "C-1",
1159
+ reviewWorkItemId: "wi-2",
1160
+ header: "VVOC_WORK_ITEM_ID: wi-2",
1161
+ reviewersToLaunch: ["code"],
1162
+ },
1163
+ },
1164
+ {
1165
+ id: "work_checkpoint:verify-native",
1166
+ label: "native verify outcome",
1167
+ fixture: {
1168
+ tool: "work_checkpoint",
1169
+ sessionId: "session-1",
1170
+ ok: true,
1171
+ action: "verify",
1172
+ runId: "run-1",
1173
+ checkpointId: "C-1",
1174
+ outcome: "passed",
1175
+ snapshotCurrent: true,
1176
+ sealedRun: true,
1177
+ },
1178
+ },
1179
+ {
1180
+ id: "work_checkpoint:review-bind-verify",
1181
+ label: "generic review/bind/verify outcome",
1182
+ fixture: {
1183
+ tool: "work_checkpoint",
1184
+ sessionId: "session-1",
1185
+ ok: true,
1186
+ action: "bind",
1187
+ runId: "run-1",
1188
+ checkpointId: "C-1",
1189
+ reviewer: "code",
1190
+ outcome: "in_progress",
1191
+ checkpointStatus: "in_review",
1192
+ },
1193
+ },
1194
+ {
1195
+ id: "work_checkpoint:recover",
1196
+ label: "checkpoint recovery outcome",
1197
+ fixture: {
1198
+ tool: "work_checkpoint",
1199
+ sessionId: "session-1",
1200
+ ok: true,
1201
+ action: "recover",
1202
+ runId: "run-1",
1203
+ checkpointId: "C-1",
1204
+ recoveryId: "rec-1",
1205
+ kind: "resume",
1206
+ checkpointStatus: "pending",
1207
+ },
1208
+ },
1209
+ {
1210
+ id: "work_checkpoint:complete",
1211
+ label: "sealed execution",
1212
+ fixture: {
1213
+ tool: "work_checkpoint",
1214
+ sessionId: "session-1",
1215
+ runId: "run-1",
1216
+ ok: true,
1217
+ action: "complete",
1218
+ reviewStatus: "independently_reviewed",
1219
+ execution: executionView({ state: "sealed" }),
1220
+ },
1221
+ },
1222
+ {
1223
+ id: "work_checkpoint:authorize",
1224
+ label: "authority grant/extension",
1225
+ fixture: {
1226
+ tool: "work_checkpoint",
1227
+ sessionId: "session-1",
1228
+ runId: "run-1",
1229
+ ok: true,
1230
+ action: "authorize",
1231
+ authorityId: "auth-1",
1232
+ reused: false,
1233
+ units: 3,
1234
+ availableUnits: 3,
1235
+ },
1236
+ },
1237
+ {
1238
+ id: "work_checkpoint:record-approval",
1239
+ label: "stage approval record",
1240
+ fixture: {
1241
+ tool: "work_checkpoint",
1242
+ sessionId: "session-1",
1243
+ runId: "run-1",
1244
+ ok: true,
1245
+ action: "record_approval",
1246
+ approvalId: "appr-1",
1247
+ stage: "implementation",
1248
+ provenance: "user_observed",
1249
+ },
1250
+ },
1251
+ {
1252
+ id: "work_checkpoint:revoke-authority",
1253
+ label: "authority revocation",
1254
+ fixture: {
1255
+ tool: "work_checkpoint",
1256
+ sessionId: "session-1",
1257
+ runId: "run-1",
1258
+ ok: true,
1259
+ action: "revoke_authority",
1260
+ authorityId: "auth-1",
1261
+ kind: "narrow",
1262
+ availableUnits: 2,
1263
+ stages: ["verification"],
1264
+ },
1265
+ },
1266
+ {
1267
+ id: "work_checkpoint:failure",
1268
+ label: "bounded owned failure",
1269
+ fixture: {
1270
+ tool: "work_checkpoint",
1271
+ sessionId: "session-1",
1272
+ ok: false,
1273
+ errorCode: "CHECKPOINT_NOT_FOUND",
1274
+ category: "state",
1275
+ message: "CHECKPOINT_NOT_FOUND: no checkpoint C-9 on the run",
1276
+ prerequisite: "a checkpoint bound to the target run",
1277
+ },
1278
+ },
1279
+ ],
1280
+ errors: [
1281
+ {
1282
+ path: "reservedStops[0]",
1283
+ expectation: "a misspelled stage rejects and never becomes a silent omission",
1284
+ },
1285
+ {
1286
+ path: "action",
1287
+ expectation: "a field consumed by another action is rejected, not ignored",
1288
+ },
1289
+ ],
1290
+ normalization: ["unknown action fields are rejected; no stage/stop is filtered out"],
1291
+ },
1292
+ ];
1293
+ // END_BLOCK_WORKFLOW_ENTRIES
1294
+ // START_BLOCK_EDIT_ENTRIES
1295
+ /**
1296
+ * Honest aggregate of the two actual hashline_edit result surfaces: the registered
1297
+ * text/Error string return value and the separately published metadata envelope.
1298
+ * The metadata object is never presented as the registered return value. Composed
1299
+ * here from the existing descriptor schemas so no runtime producer changes.
1300
+ */
1301
+ const hashlineEditResultSchema = tool.schema.union([
1302
+ tool.schema.string(),
1303
+ hashlineEditMetadataSchema,
1304
+ ]);
1305
+ const editEntries = [
1306
+ {
1307
+ toolId: HASHLINE_EDIT_TOOL_ID,
1308
+ summary: "Apply exact hash-anchored line edits, range replacements, boundary inserts, delete, or rename.",
1309
+ contract: hashlineEditContract,
1310
+ validate: validateHashlineEditToolInput,
1311
+ resultSchema: hashlineEditResultSchema,
1312
+ vocabularies: [
1313
+ { field: "edits[].op", values: ["replace", "replace_range", "append", "prepend"] },
1314
+ ],
1315
+ operations: [
1316
+ {
1317
+ id: "hashline_edit:replace",
1318
+ label: "replace one line at pos",
1319
+ expect: "accept",
1320
+ input: { filePath: "/tmp/a.ts", edits: [{ op: "replace", pos: "2#VK#ZZ", lines: ["x"] }] },
1321
+ covers: { "edits[].op": "replace" },
1322
+ },
1323
+ {
1324
+ id: "hashline_edit:replace-with-end",
1325
+ label: "replace with end is an inclusive range",
1326
+ expect: "accept",
1327
+ input: {
1328
+ filePath: "/tmp/a.ts",
1329
+ edits: [{ op: "replace", pos: "2#VK#ZZ", end: "3#MB#ZZ", lines: ["x"] }],
1330
+ },
1331
+ },
1332
+ {
1333
+ id: "hashline_edit:replace-range",
1334
+ label: "replace_range requires both anchors",
1335
+ expect: "accept",
1336
+ input: {
1337
+ filePath: "/tmp/a.ts",
1338
+ edits: [{ op: "replace_range", pos: "2#VK#ZZ", end: "3#MB#ZZ", lines: ["x"] }],
1339
+ },
1340
+ covers: { "edits[].op": "replace_range" },
1341
+ },
1342
+ {
1343
+ id: "hashline_edit:delete-lines-null",
1344
+ label: "deletion via lines null",
1345
+ expect: "accept",
1346
+ input: {
1347
+ filePath: "/tmp/a.ts",
1348
+ edits: [{ op: "replace_range", pos: "2#VK#ZZ", end: "3#MB#ZZ", lines: null }],
1349
+ },
1350
+ },
1351
+ {
1352
+ id: "hashline_edit:append-boundary",
1353
+ label: "append without an anchor inserts at the file boundary",
1354
+ expect: "accept",
1355
+ input: { filePath: "/tmp/a.ts", edits: [{ op: "append", lines: ["x"] }] },
1356
+ covers: { "edits[].op": "append" },
1357
+ },
1358
+ {
1359
+ id: "hashline_edit:prepend-end-fallback",
1360
+ label: "prepend with an end-anchor fallback",
1361
+ expect: "accept",
1362
+ input: { filePath: "/tmp/a.ts", edits: [{ op: "prepend", end: "3#MB#ZZ", lines: ["x"] }] },
1363
+ covers: { "edits[].op": "prepend" },
1364
+ },
1365
+ {
1366
+ id: "hashline_edit:delete-file",
1367
+ label: "delete mode with an empty edits list",
1368
+ expect: "accept",
1369
+ input: { filePath: "/tmp/a.ts", delete: true, edits: [] },
1370
+ },
1371
+ {
1372
+ id: "hashline_edit:rename",
1373
+ label: "rename after edits",
1374
+ expect: "accept",
1375
+ input: {
1376
+ filePath: "/tmp/a.ts",
1377
+ rename: "/tmp/b.ts",
1378
+ edits: [{ op: "append", lines: ["x"] }],
1379
+ },
1380
+ },
1381
+ {
1382
+ id: "hashline_edit:reject-unknown-nested-key",
1383
+ label: "unknown nested edit key is rejected",
1384
+ expect: "reject",
1385
+ input: { filePath: "/tmp/a.ts", edits: [{ op: "append", lines: ["x"], typo: 1 }] },
1386
+ tags: ["unknown_key"],
1387
+ },
1388
+ {
1389
+ id: "hashline_edit:reject-delete-rename",
1390
+ label: "delete and rename conflict",
1391
+ expect: "reject",
1392
+ input: { filePath: "/tmp/a.ts", delete: true, rename: "/tmp/b.ts", edits: [] },
1393
+ tags: ["conflict"],
1394
+ },
1395
+ {
1396
+ id: "hashline_edit:reject-replace-range-missing-end",
1397
+ label: "replace_range missing end is rejected",
1398
+ expect: "reject",
1399
+ input: {
1400
+ filePath: "/tmp/a.ts",
1401
+ edits: [{ op: "replace_range", pos: "2#VK#ZZ", lines: ["x"] }],
1402
+ },
1403
+ tags: ["missing_required"],
1404
+ },
1405
+ {
1406
+ id: "hashline_edit:reject-conflicting-insert-anchors",
1407
+ label: "append with conflicting pos/end anchors is rejected",
1408
+ expect: "reject",
1409
+ input: {
1410
+ filePath: "/tmp/a.ts",
1411
+ edits: [{ op: "append", pos: "2#VK#ZZ", end: "3#MB#ZZ", lines: ["x"] }],
1412
+ },
1413
+ tags: ["conflict"],
1414
+ },
1415
+ {
1416
+ id: "hashline_edit:reject-unsupported-op",
1417
+ label: "unsupported op is rejected",
1418
+ expect: "reject",
1419
+ input: { filePath: "/tmp/a.ts", edits: [{ op: "set_line", pos: "2#VK#ZZ", lines: ["x"] }] },
1420
+ tags: ["invalid_value"],
1421
+ },
1422
+ {
1423
+ id: "hashline_edit:reject-replace-missing-pos",
1424
+ label: "replace without a pos anchor is rejected",
1425
+ expect: "reject",
1426
+ input: { filePath: "/tmp/a.ts", edits: [{ op: "replace", lines: ["x"] }] },
1427
+ tags: ["missing_required"],
1428
+ },
1429
+ {
1430
+ id: "hashline_edit:reject-prepend-conflicting-anchors",
1431
+ label: "prepend with conflicting pos/end anchors is rejected",
1432
+ expect: "reject",
1433
+ input: {
1434
+ filePath: "/tmp/a.ts",
1435
+ edits: [{ op: "prepend", pos: "2#VK#ZZ", end: "3#MB#ZZ", lines: ["x"] }],
1436
+ },
1437
+ tags: ["conflict"],
1438
+ },
1439
+ {
1440
+ id: "hashline_edit:reject-delete-blank-path",
1441
+ label: "delete with a blank path is rejected",
1442
+ expect: "reject",
1443
+ input: { filePath: "", delete: true, edits: [] },
1444
+ tags: ["missing_required"],
1445
+ },
1446
+ {
1447
+ id: "hashline_edit:reject-rename-blank-path",
1448
+ label: "rename with a blank source path is rejected",
1449
+ expect: "reject",
1450
+ input: {
1451
+ filePath: "",
1452
+ rename: "/tmp/b.ts",
1453
+ edits: [{ op: "append", lines: ["x"] }],
1454
+ },
1455
+ tags: ["missing_required"],
1456
+ },
1457
+ ],
1458
+ defaults: [],
1459
+ prerequisites: [
1460
+ "absolute existing file path and current-file anchor validation",
1461
+ "model visibility for the routed edit tool",
1462
+ ],
1463
+ conditionals: [
1464
+ "replace requires pos; replace_range requires pos and end",
1465
+ "append/prepend accept one anchor or none; null/[] deletes for replace/replace_range",
1466
+ "delete requires an empty edits list and forbids rename",
1467
+ ],
1468
+ pathKinds: [
1469
+ {
1470
+ field: "filePath",
1471
+ kind: "absolute-file",
1472
+ rules: "absolute path; spaces inside the name are preserved",
1473
+ },
1474
+ {
1475
+ field: "rename",
1476
+ kind: "absolute-file",
1477
+ rules: "non-empty absolute destination path when provided",
1478
+ },
1479
+ ],
1480
+ results: [
1481
+ {
1482
+ id: "hashline_edit:text-success",
1483
+ label: "model-visible success text returned to the host",
1484
+ fixture: "Successfully applied 1 edit to /tmp/a.ts",
1485
+ },
1486
+ {
1487
+ id: "hashline_edit:text-error",
1488
+ label: "model-visible Error text for a rejected edit",
1489
+ fixture: "Error: hash mismatch - anchor 2#VK#ZZ not found in the current file",
1490
+ },
1491
+ {
1492
+ id: "hashline_edit:success-metadata",
1493
+ label: "separately published bounded success metadata with filediff",
1494
+ fixture: {
1495
+ filePath: "/tmp/a.ts",
1496
+ path: "/tmp/a.ts",
1497
+ file: "/tmp/a.ts",
1498
+ noopEdits: 0,
1499
+ deduplicatedEdits: 0,
1500
+ firstChangedLine: 2,
1501
+ editMode: "hashline_edit",
1502
+ providerID: "deepseek",
1503
+ modelID: "deepseek-v4-flash",
1504
+ filediff: {
1505
+ file: "/tmp/a.ts",
1506
+ path: "/tmp/a.ts",
1507
+ filePath: "/tmp/a.ts",
1508
+ before: "a\n",
1509
+ after: "b\n",
1510
+ },
1511
+ },
1512
+ },
1513
+ ],
1514
+ errors: [
1515
+ { path: "edits[0].typo", expectation: "unknown nested key names the edit index" },
1516
+ { path: "edits[0].end", expectation: "conflicting insert anchors name the offending field" },
1517
+ ],
1518
+ normalization: [
1519
+ "literal payloads are applied byte-for-byte; empty content is preserved",
1520
+ "current-file and stale-anchor checks remain authoritative after structural validation",
1521
+ ],
1522
+ },
1523
+ {
1524
+ toolId: STR_REPLACE_EDITOR_TOOL_ID,
1525
+ summary: "View, create, exactly replace, or insert into a file with the dsh command surface.",
1526
+ contract: strReplaceEditorContract,
1527
+ validate: validateStrReplaceEditorToolInput,
1528
+ resultSchema: strReplaceEditorResultSchema,
1529
+ vocabularies: [{ field: "command", values: ["view", "create", "str_replace", "insert"] }],
1530
+ operations: [
1531
+ {
1532
+ id: "str_replace_editor:view",
1533
+ label: "view with an end-of-file range",
1534
+ expect: "accept",
1535
+ input: { command: "view", path: "/tmp/a.ts", view_range: [2, -1] },
1536
+ covers: { command: "view" },
1537
+ },
1538
+ {
1539
+ id: "str_replace_editor:create",
1540
+ label: "create with an explicitly empty file_text",
1541
+ expect: "accept",
1542
+ input: { command: "create", path: "/tmp/a.ts", file_text: "" },
1543
+ covers: { command: "create" },
1544
+ },
1545
+ {
1546
+ id: "str_replace_editor:str-replace-explicit-empty",
1547
+ label: "str_replace with an explicit empty new_str (deletion)",
1548
+ expect: "accept",
1549
+ input: { command: "str_replace", path: "/tmp/a.ts", old_str: "x", new_str: "" },
1550
+ covers: { command: "str_replace" },
1551
+ },
1552
+ {
1553
+ id: "str_replace_editor:str-replace-omitted-new-str",
1554
+ label: "str_replace with an omitted new_str defaults to deletion",
1555
+ expect: "accept",
1556
+ input: { command: "str_replace", path: "/tmp/a.ts", old_str: "x" },
1557
+ },
1558
+ {
1559
+ id: "str_replace_editor:insert",
1560
+ label: "insert with an integer insert_line and empty new_str",
1561
+ expect: "accept",
1562
+ input: { command: "insert", path: "/tmp/a.ts", insert_line: 0, new_str: "" },
1563
+ covers: { command: "insert" },
1564
+ },
1565
+ {
1566
+ id: "str_replace_editor:reject-unknown-key",
1567
+ label: "unknown key is rejected",
1568
+ expect: "reject",
1569
+ input: { command: "view", path: "/tmp/a.ts", nested: { deep: 1 } },
1570
+ tags: ["unknown_key"],
1571
+ },
1572
+ {
1573
+ id: "str_replace_editor:reject-unsupported-command",
1574
+ label: "an unsupported command is rejected",
1575
+ expect: "reject",
1576
+ input: { command: "delete", path: "/tmp/a.ts" },
1577
+ tags: ["invalid_value"],
1578
+ },
1579
+ {
1580
+ id: "str_replace_editor:reject-unconsumed-field",
1581
+ label: "a field for another command is rejected",
1582
+ expect: "reject",
1583
+ input: { command: "view", path: "/tmp/a.ts", old_str: "x" },
1584
+ tags: ["conflict"],
1585
+ },
1586
+ {
1587
+ id: "str_replace_editor:reject-create-missing-file-text",
1588
+ label: "create without file_text is rejected",
1589
+ expect: "reject",
1590
+ input: { command: "create", path: "/tmp/a.ts" },
1591
+ tags: ["missing_required"],
1592
+ },
1593
+ {
1594
+ id: "str_replace_editor:reject-empty-old-str",
1595
+ label: "str_replace requires a non-empty old_str",
1596
+ expect: "reject",
1597
+ input: { command: "str_replace", path: "/tmp/a.ts", old_str: "", new_str: "x" },
1598
+ tags: ["invalid_value"],
1599
+ },
1600
+ {
1601
+ id: "str_replace_editor:reject-insert-missing-line",
1602
+ label: "insert without insert_line is rejected",
1603
+ expect: "reject",
1604
+ input: { command: "insert", path: "/tmp/a.ts", new_str: "x" },
1605
+ tags: ["missing_required"],
1606
+ },
1607
+ {
1608
+ id: "str_replace_editor:reject-view-range-shape",
1609
+ label: "view_range must be exactly two integers",
1610
+ expect: "reject",
1611
+ input: { command: "view", path: "/tmp/a.ts", view_range: [1] },
1612
+ tags: ["invalid_value"],
1613
+ },
1614
+ {
1615
+ id: "str_replace_editor:reject-view-range-order",
1616
+ label: "view_range end must be -1 or >= start",
1617
+ expect: "reject",
1618
+ input: { command: "view", path: "/tmp/a.ts", view_range: [3, 2] },
1619
+ tags: ["invalid_value"],
1620
+ },
1621
+ ],
1622
+ defaults: [],
1623
+ prerequisites: [
1624
+ "path existence and directory checks in the editor",
1625
+ "current-file freshness for str_replace and insert",
1626
+ ],
1627
+ conditionals: [
1628
+ "create requires file_text (explicit empty allowed)",
1629
+ "str_replace requires a non-empty old_str; omitted new_str deletes",
1630
+ "insert requires insert_line >= 0 and new_str; view_range length is exactly 2",
1631
+ ],
1632
+ pathKinds: [
1633
+ {
1634
+ field: "path",
1635
+ kind: "absolute-file",
1636
+ rules: "absolute file or directory path; non-empty",
1637
+ },
1638
+ ],
1639
+ results: [
1640
+ {
1641
+ id: "str_replace_editor:ok",
1642
+ label: "successful ok/output envelope",
1643
+ fixture: { ok: true, output: "done" },
1644
+ },
1645
+ {
1646
+ id: "str_replace_editor:error",
1647
+ label: "error envelope",
1648
+ fixture: { ok: false, error: "nope" },
1649
+ },
1650
+ ],
1651
+ errors: [
1652
+ { path: "path", expectation: "blank paths are rejected before any mutation" },
1653
+ { path: "view_range", expectation: "a non-two-element range is diagnosed" },
1654
+ ],
1655
+ normalization: ["model visibility is checked before the tool is used"],
1656
+ },
1657
+ ];
1658
+ // END_BLOCK_EDIT_ENTRIES
1659
+ // START_BLOCK_WEB_ENTRIES
1660
+ const webEntries = [
1661
+ {
1662
+ toolId: WEB_SEARCH_TOOL_ID,
1663
+ summary: "Search the configured provider and return ranked Markdown results.",
1664
+ contract: webSearchContract,
1665
+ validate: validateWebSearchToolInput,
1666
+ resultSchema: webSearchResultSchema,
1667
+ vocabularies: [{ field: "freshness", values: ["day", "week", "month", "year"] }],
1668
+ operations: [
1669
+ {
1670
+ id: "web_search:default-count",
1671
+ label: "query only applies the documented count default",
1672
+ expect: "accept",
1673
+ input: { query: "vvoc" },
1674
+ },
1675
+ {
1676
+ id: "web_search:freshness-day",
1677
+ label: "freshness day",
1678
+ expect: "accept",
1679
+ input: { query: "vvoc", freshness: "day" },
1680
+ covers: { freshness: "day" },
1681
+ },
1682
+ {
1683
+ id: "web_search:freshness-week",
1684
+ label: "freshness week",
1685
+ expect: "accept",
1686
+ input: { query: "vvoc", freshness: "week" },
1687
+ covers: { freshness: "week" },
1688
+ },
1689
+ {
1690
+ id: "web_search:freshness-month",
1691
+ label: "freshness month",
1692
+ expect: "accept",
1693
+ input: { query: "vvoc", freshness: "month" },
1694
+ covers: { freshness: "month" },
1695
+ },
1696
+ {
1697
+ id: "web_search:freshness-year",
1698
+ label: "freshness year",
1699
+ expect: "accept",
1700
+ input: { query: "vvoc", freshness: "year" },
1701
+ covers: { freshness: "year" },
1702
+ },
1703
+ {
1704
+ id: "web_search:max-count",
1705
+ label: "explicit integer count at the maximum",
1706
+ expect: "accept",
1707
+ input: { query: "vvoc", count: 20 },
1708
+ },
1709
+ {
1710
+ id: "web_search:reject-unknown-key",
1711
+ label: "unknown key is rejected",
1712
+ expect: "reject",
1713
+ input: { query: "vvoc", extra: true },
1714
+ tags: ["unknown_key"],
1715
+ },
1716
+ {
1717
+ id: "web_search:reject-count-low",
1718
+ label: "count below the minimum is rejected",
1719
+ expect: "reject",
1720
+ input: { query: "vvoc", count: 0 },
1721
+ tags: ["invalid_value"],
1722
+ },
1723
+ {
1724
+ id: "web_search:reject-count-high",
1725
+ label: "count above the maximum is rejected",
1726
+ expect: "reject",
1727
+ input: { query: "vvoc", count: 21 },
1728
+ tags: ["invalid_value"],
1729
+ },
1730
+ {
1731
+ id: "web_search:reject-fractional-count",
1732
+ label: "a fractional count is rejected, not coerced",
1733
+ expect: "reject",
1734
+ input: { query: "vvoc", count: 1.5 },
1735
+ tags: ["invalid_value"],
1736
+ },
1737
+ {
1738
+ id: "web_search:reject-string-count",
1739
+ label: "a string count is rejected, not coerced",
1740
+ expect: "reject",
1741
+ input: { query: "vvoc", count: "8" },
1742
+ tags: ["invalid_value"],
1743
+ },
1744
+ {
1745
+ id: "web_search:reject-unknown-freshness",
1746
+ label: "an unsupported freshness window is rejected",
1747
+ expect: "reject",
1748
+ input: { query: "vvoc", freshness: "hour" },
1749
+ tags: ["invalid_value"],
1750
+ },
1751
+ {
1752
+ id: "web_search:reject-freshness-day-invalid-count",
1753
+ label: "freshness day with an out-of-range count is rejected",
1754
+ expect: "reject",
1755
+ input: { query: "vvoc", freshness: "day", count: 0 },
1756
+ tags: ["invalid_value"],
1757
+ },
1758
+ {
1759
+ id: "web_search:reject-freshness-week-invalid-count",
1760
+ label: "freshness week with an out-of-range count is rejected",
1761
+ expect: "reject",
1762
+ input: { query: "vvoc", freshness: "week", count: 0 },
1763
+ tags: ["invalid_value"],
1764
+ },
1765
+ {
1766
+ id: "web_search:reject-freshness-month-invalid-count",
1767
+ label: "freshness month with an out-of-range count is rejected",
1768
+ expect: "reject",
1769
+ input: { query: "vvoc", freshness: "month", count: 0 },
1770
+ tags: ["invalid_value"],
1771
+ },
1772
+ {
1773
+ id: "web_search:reject-freshness-year-invalid-count",
1774
+ label: "freshness year with an out-of-range count is rejected",
1775
+ expect: "reject",
1776
+ input: { query: "vvoc", freshness: "year", count: 0 },
1777
+ tags: ["invalid_value"],
1778
+ },
1779
+ {
1780
+ id: "web_search:reject-credential",
1781
+ label: "credential or provider selection is not a caller argument",
1782
+ expect: "reject",
1783
+ input: { query: "vvoc", credential: "secret", provider: "brave" },
1784
+ tags: ["unknown_key"],
1785
+ },
1786
+ ],
1787
+ defaults: [
1788
+ { field: "count", value: WEB_SEARCH_DEFAULT_COUNT, note: "re-applied at execute time" },
1789
+ ],
1790
+ prerequisites: [
1791
+ "configured provider and permission prompt",
1792
+ "resolved credential (env or config) for non-native providers",
1793
+ ],
1794
+ conditionals: ["count is an integer 1..20; freshness is optional"],
1795
+ pathKinds: [],
1796
+ results: [
1797
+ {
1798
+ id: "web_search:exa",
1799
+ label: "ranked Markdown result from the Exa provider",
1800
+ fixture: {
1801
+ title: "web_search: vvoc",
1802
+ output: "# Results\n\n1. ...",
1803
+ metadata: { provider: "exa", resultCount: 2, credentialSource: "env" },
1804
+ },
1805
+ },
1806
+ {
1807
+ id: "web_search:brave",
1808
+ label: "ranked Markdown result from the Brave provider",
1809
+ fixture: {
1810
+ title: "web_search: vvoc",
1811
+ output: "# Results\n\n1. ...",
1812
+ metadata: { provider: "brave", resultCount: 3, credentialSource: "config" },
1813
+ },
1814
+ },
1815
+ {
1816
+ id: "web_search:zai",
1817
+ label: "ranked Markdown result from the regional Z.AI provider",
1818
+ fixture: {
1819
+ title: "web_search: vvoc",
1820
+ output: "# Results\n\n1. ...",
1821
+ metadata: {
1822
+ provider: "zai",
1823
+ region: "international",
1824
+ resultCount: 1,
1825
+ credentialSource: "env",
1826
+ },
1827
+ },
1828
+ },
1829
+ ],
1830
+ errors: [
1831
+ { path: "count", expectation: "an out-of-range count is rejected before dispatch" },
1832
+ { path: "credential", expectation: "unknown credential/provider fields are rejected" },
1833
+ ],
1834
+ normalization: [
1835
+ "the query text is preserved verbatim; provider/credential fields are never accepted",
1836
+ ],
1837
+ },
1838
+ {
1839
+ toolId: WEB_FETCH_TOOL_ID,
1840
+ summary: "Fetch a known HTTP(S) URL and return Markdown, text, HTML, or a media attachment.",
1841
+ contract: webFetchContract,
1842
+ validate: validateWebFetchToolInput,
1843
+ resultSchema: webFetchResultSchema,
1844
+ vocabularies: [{ field: "format", values: ["markdown", "text", "html"] }],
1845
+ operations: [
1846
+ {
1847
+ id: "web_fetch:default-format",
1848
+ label: "url only applies markdown and the timeout default",
1849
+ expect: "accept",
1850
+ input: { url: "https://example.test/page" },
1851
+ covers: { format: "markdown" },
1852
+ },
1853
+ {
1854
+ id: "web_fetch:format-text",
1855
+ label: "text format with a fractional positive timeout",
1856
+ expect: "accept",
1857
+ input: { url: "https://example.test/page", format: "text", timeout: 0.5 },
1858
+ covers: { format: "text" },
1859
+ },
1860
+ {
1861
+ id: "web_fetch:format-html",
1862
+ label: "html format at the timeout maximum",
1863
+ expect: "accept",
1864
+ input: { url: "https://example.test/page", format: "html", timeout: 120 },
1865
+ covers: { format: "html" },
1866
+ },
1867
+ {
1868
+ id: "web_fetch:reject-unknown-key",
1869
+ label: "unknown key is rejected",
1870
+ expect: "reject",
1871
+ input: { url: "https://example.test/page", extra: true },
1872
+ tags: ["unknown_key"],
1873
+ },
1874
+ {
1875
+ id: "web_fetch:reject-relative-url",
1876
+ label: "a relative URL is rejected",
1877
+ expect: "reject",
1878
+ input: { url: "/page" },
1879
+ tags: ["invalid_value"],
1880
+ },
1881
+ {
1882
+ id: "web_fetch:reject-file-scheme",
1883
+ label: "the file scheme is unsupported",
1884
+ expect: "reject",
1885
+ input: { url: "file:///tmp/secret" },
1886
+ tags: ["invalid_value"],
1887
+ },
1888
+ {
1889
+ id: "web_fetch:reject-data-scheme",
1890
+ label: "the data scheme is unsupported",
1891
+ expect: "reject",
1892
+ input: { url: "data:text/plain,hello" },
1893
+ tags: ["invalid_value"],
1894
+ },
1895
+ {
1896
+ id: "web_fetch:reject-unsupported-format",
1897
+ label: "an unsupported format is rejected",
1898
+ expect: "reject",
1899
+ input: { url: "https://example.test/page", format: "pdf" },
1900
+ tags: ["invalid_value"],
1901
+ },
1902
+ {
1903
+ id: "web_fetch:reject-markdown-invalid-timeout",
1904
+ label: "markdown with a non-positive timeout is rejected",
1905
+ expect: "reject",
1906
+ input: { url: "https://example.test/page", format: "markdown", timeout: 0 },
1907
+ tags: ["invalid_value"],
1908
+ },
1909
+ {
1910
+ id: "web_fetch:reject-text-invalid-timeout",
1911
+ label: "text with a non-positive timeout is rejected",
1912
+ expect: "reject",
1913
+ input: { url: "https://example.test/page", format: "text", timeout: 0 },
1914
+ tags: ["invalid_value"],
1915
+ },
1916
+ {
1917
+ id: "web_fetch:reject-html-invalid-timeout",
1918
+ label: "html with a non-positive timeout is rejected",
1919
+ expect: "reject",
1920
+ input: { url: "https://example.test/page", format: "html", timeout: 0 },
1921
+ tags: ["invalid_value"],
1922
+ },
1923
+ {
1924
+ id: "web_fetch:reject-zero-timeout",
1925
+ label: "a non-positive timeout is rejected",
1926
+ expect: "reject",
1927
+ input: { url: "https://example.test/page", timeout: 0 },
1928
+ tags: ["invalid_value"],
1929
+ },
1930
+ {
1931
+ id: "web_fetch:reject-string-timeout",
1932
+ label: "a string timeout is rejected, not coerced",
1933
+ expect: "reject",
1934
+ input: { url: "https://example.test/page", timeout: "30" },
1935
+ tags: ["invalid_value"],
1936
+ },
1937
+ {
1938
+ id: "web_fetch:reject-credential",
1939
+ label: "credential or provider selection is not a caller argument",
1940
+ expect: "reject",
1941
+ input: { url: "https://example.test/page", apiKey: "secret", provider: "spider" },
1942
+ tags: ["unknown_key"],
1943
+ },
1944
+ ],
1945
+ defaults: [
1946
+ { field: "format", value: "markdown", note: "re-applied at execute time" },
1947
+ {
1948
+ field: "timeout",
1949
+ value: WEB_FETCH_DEFAULT_TIMEOUT_SECONDS,
1950
+ note: "re-applied at execute time; positive and at most 120",
1951
+ },
1952
+ ],
1953
+ prerequisites: [
1954
+ "configured provider and permission prompt",
1955
+ "resolved credential for spider/zai providers",
1956
+ ],
1957
+ conditionals: ["format defaults to markdown; timeout is positive and at most 120"],
1958
+ pathKinds: [
1959
+ {
1960
+ field: "url",
1961
+ kind: "url",
1962
+ rules: "absolute http(s) URL; the URL is requested unchanged and never echoed in diagnostics",
1963
+ },
1964
+ ],
1965
+ results: [
1966
+ {
1967
+ id: "web_fetch:text-native",
1968
+ label: "native textual result envelope",
1969
+ fixture: {
1970
+ title: "web_fetch: https://example.test/page",
1971
+ output: "body",
1972
+ metadata: { provider: "native", format: "markdown", status: 200 },
1973
+ },
1974
+ },
1975
+ {
1976
+ id: "web_fetch:text-spider",
1977
+ label: "Spider textual result envelope with request timing",
1978
+ fixture: {
1979
+ title: "web_fetch: https://example.test/page",
1980
+ output: "body",
1981
+ metadata: {
1982
+ provider: "spider",
1983
+ format: "html",
1984
+ credentialSource: "env",
1985
+ status: 200,
1986
+ durationMs: 12,
1987
+ },
1988
+ },
1989
+ },
1990
+ {
1991
+ id: "web_fetch:text-zai",
1992
+ label: "regional Z.AI textual result envelope with reader metadata",
1993
+ fixture: {
1994
+ title: "web_fetch: https://example.test/page",
1995
+ output: "body",
1996
+ metadata: {
1997
+ provider: "zai",
1998
+ region: "china",
1999
+ format: "text",
2000
+ credentialSource: "config",
2001
+ status: 200,
2002
+ requestId: "req-1",
2003
+ model: "reader",
2004
+ created: 1,
2005
+ title: "Example",
2006
+ },
2007
+ },
2008
+ },
2009
+ {
2010
+ id: "web_fetch:media-native",
2011
+ label: "native media result envelope with a real attachment",
2012
+ fixture: {
2013
+ title: "web_fetch: https://example.test/image.png",
2014
+ output: "Fetched as an image/png attachment.",
2015
+ metadata: { provider: "native", format: "markdown", status: 200 },
2016
+ attachments: [{ type: "file", mime: "image/png", url: "data:image/png;base64,AA==" }],
2017
+ },
2018
+ },
2019
+ {
2020
+ id: "web_fetch:media-spider",
2021
+ label: "Spider media result envelope with request timing",
2022
+ fixture: {
2023
+ title: "web_fetch: https://example.test/image.png",
2024
+ output: "Fetched as an image/png attachment.",
2025
+ metadata: {
2026
+ provider: "spider",
2027
+ format: "markdown",
2028
+ credentialSource: "env",
2029
+ durationMs: 7,
2030
+ },
2031
+ attachments: [{ type: "file", mime: "image/png", url: "data:image/png;base64,AA==" }],
2032
+ },
2033
+ },
2034
+ {
2035
+ id: "web_fetch:media-zai",
2036
+ label: "regional Z.AI media result envelope with a real attachment",
2037
+ fixture: {
2038
+ title: "web_fetch: https://example.test/image.png",
2039
+ output: "Fetched as an image/png attachment.",
2040
+ metadata: {
2041
+ provider: "zai",
2042
+ region: "international",
2043
+ format: "markdown",
2044
+ credentialSource: "env",
2045
+ },
2046
+ attachments: [{ type: "file", mime: "image/png", url: "data:image/png;base64,AA==" }],
2047
+ },
2048
+ },
2049
+ ],
2050
+ errors: [
2051
+ { path: "url", expectation: "a non-http(s) URL is rejected without echoing the raw URL" },
2052
+ { path: "timeout", expectation: "an out-of-bounds timeout is rejected before dispatch" },
2053
+ ],
2054
+ normalization: [
2055
+ "document and binary content stay declared opaque; credentials are never accepted",
2056
+ ],
2057
+ },
2058
+ ];
2059
+ // END_BLOCK_WEB_ENTRIES
2060
+ // START_BLOCK_CATALOG
2061
+ /** Package-relative path of the generated reference document. */
2062
+ export const CONTRACT_REFERENCE_PACKAGE_PATH = "templates/skills/vv-execute/references/tool-contracts.md";
2063
+ /**
2064
+ * Recorded pre-change contract-size baseline with provenance.
2065
+ * Measured once from the clean starting commit; future checks compare against the
2066
+ * catalog-derived current measurement rather than reading mutable git history.
2067
+ */
2068
+ export const CONTRACT_SIZE_BASELINE = {
2069
+ provenance: {
2070
+ commit: "f4319f8",
2071
+ sdk: "@opencode-ai/plugin@1.18.2",
2072
+ method: "source-extraction",
2073
+ detail: 'UTF-8 byte sizes measured at the clean starting commit f4319f8 by extracting each owned tool\'s model-facing description string and projecting its pre-change registered argument map with the pinned SDK (io:"input"). This is a declaration/projection size, not a provider-exact token count and not a model-quality or model-effectiveness claim.',
2074
+ procedure: 'Reproduce the baseline: `git show f4319f8:<tool-source> | extract the model-facing description`, then project each pre-change registered argument map with @opencode-ai/plugin@1.18.2 via tool.schema.toJSONSchema(schema, { io: "input" }) and sum Buffer.byteLength(value, "utf8"). "After" is the catalog-derived `measureCatalogContractSize()` value printed below as "Current model-facing size"; compare it to this baseline. Numbers reported are exact UTF-8 byte counts, never estimated token counts.',
2075
+ },
2076
+ before: {
2077
+ descriptionBytes: 8796,
2078
+ inputSchemaBytes: 6815,
2079
+ },
2080
+ };
2081
+ /** The nine owned tool catalog entries in stable registration order. */
2082
+ export const agentToolCatalog = [
2083
+ ...workflowEntries,
2084
+ ...editEntries,
2085
+ ...webEntries,
2086
+ ];
2087
+ /** Sorted tool ids covered by the catalog. */
2088
+ export const AGENT_TOOL_CATALOG_TOOL_IDS = agentToolCatalog
2089
+ .map((entry) => entry.toolId)
2090
+ .sort();
2091
+ /** Sorted tool ids covered by the catalog. */
2092
+ export function catalogToolIds() {
2093
+ return [...AGENT_TOOL_CATALOG_TOOL_IDS];
2094
+ }
2095
+ /** Current description/input-schema UTF-8 byte measurement derived from the catalog. */
2096
+ export function measureCatalogContractSize(entries = agentToolCatalog) {
2097
+ let descriptionBytes = 0;
2098
+ let inputSchemaBytes = 0;
2099
+ for (const entry of entries) {
2100
+ descriptionBytes += Buffer.byteLength(entry.contract.description, "utf8");
2101
+ inputSchemaBytes += Buffer.byteLength(JSON.stringify(entry.contract.inputJsonSchema), "utf8");
2102
+ }
2103
+ return { descriptionBytes, inputSchemaBytes, totalBytes: descriptionBytes + inputSchemaBytes };
2104
+ }
2105
+ // END_BLOCK_CATALOG
2106
+ // START_BLOCK_FIXTURE_RUNNER
2107
+ function firstIssuePath(issues) {
2108
+ const first = issues?.[0];
2109
+ return first ? first.path : "(root)";
2110
+ }
2111
+ /**
2112
+ * Run every catalog fixture through the actual exporting validator, and every
2113
+ * result-family fixture through the actual result schema. This is the shared
2114
+ * fixture gate used by both the catalog test and scripts/check-tool-contracts.ts.
2115
+ */
2116
+ export function validateAgentToolCatalog() {
2117
+ const failures = [];
2118
+ let checked = 0;
2119
+ for (const entry of agentToolCatalog) {
2120
+ for (const operation of entry.operations) {
2121
+ checked += 1;
2122
+ const result = entry.validate(operation.input);
2123
+ const expected = operation.expect === "accept";
2124
+ if (result.ok !== expected) {
2125
+ failures.push(`${operation.id}: expected ${operation.expect}, got ${result.ok ? "accept" : "reject"} at ${firstIssuePath(result.issues)}`);
2126
+ }
2127
+ }
2128
+ for (const variant of entry.results) {
2129
+ checked += 1;
2130
+ const parsed = entry.resultSchema.safeParse(variant.fixture);
2131
+ if (!parsed.success) {
2132
+ failures.push(`${entry.toolId}/${variant.id}: result fixture does not match the result schema`);
2133
+ }
2134
+ }
2135
+ }
2136
+ return { ok: failures.length === 0, failures, checked };
2137
+ }
2138
+ /**
2139
+ * Enumerate every discriminated-union branch reachable from a result schema,
2140
+ * including unions nested inside object properties (provider metadata variants)
2141
+ * and their sub-branches. The `valuePath` is the property path into a fixture
2142
+ * value that this branch validates, so nested variants are checked materially
2143
+ * rather than only at the outer union.
2144
+ */
2145
+ function isSchemaObject(value) {
2146
+ return (typeof value === "object" &&
2147
+ value !== null &&
2148
+ typeof value.safeParse === "function");
2149
+ }
2150
+ /** Sentinel path segment meaning "each element of the array at this position". */
2151
+ const ARRAY_PATH_SEGMENT = "[]";
2152
+ function zodLiteralValue(schema) {
2153
+ if (!isSchemaObject(schema))
2154
+ return undefined;
2155
+ const value = schema.value;
2156
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean"
2157
+ ? value
2158
+ : undefined;
2159
+ }
2160
+ function literalToken(value) {
2161
+ return `${typeof value}:${String(value)}`;
2162
+ }
2163
+ /**
2164
+ * A nested union is a material result family only when at least two of its object
2165
+ * options are discriminated by distinct literal values on one property (for example
2166
+ * the provider metadata variants or a boolean `ok` batch item union).
2167
+ * Optional/nullable wrappers, enums, and type unions are not coverage families.
2168
+ */
2169
+ function isDiscriminatedResultUnion(options) {
2170
+ const objectOptions = options.filter(isSchemaObject);
2171
+ if (objectOptions.length < 2)
2172
+ return false;
2173
+ const valuesByKey = new Map();
2174
+ for (const option of objectOptions) {
2175
+ const shape = option.shape;
2176
+ if (!shape)
2177
+ continue;
2178
+ for (const [key, value] of Object.entries(shape)) {
2179
+ const literal = zodLiteralValue(value);
2180
+ if (literal === undefined)
2181
+ continue;
2182
+ if (!valuesByKey.has(key))
2183
+ valuesByKey.set(key, new Set());
2184
+ valuesByKey.get(key).add(literalToken(literal));
2185
+ }
2186
+ }
2187
+ return [...valuesByKey.values()].some((values) => values.size >= 2);
2188
+ }
2189
+ export function collectResultBranches(schema) {
2190
+ const branches = [];
2191
+ const visit = (node, nodePath, valuePath, isRootUnion, ancestors, pathStack) => {
2192
+ if (typeof node !== "object" || node === null)
2193
+ return;
2194
+ // Self-referential schema nodes must not loop forever: skip a node already on
2195
+ // the current descent path rather than silently re-expanding it.
2196
+ if (pathStack.has(node))
2197
+ return;
2198
+ const nextStack = new Set(pathStack).add(node);
2199
+ const candidate = node;
2200
+ // Discriminated unions and enums first, then closed objects, then arrays, then
2201
+ // optional/nullable/default wrappers (whose `unwrap` may also expose an inner
2202
+ // array/union and must not shadow the array context).
2203
+ if (Array.isArray(candidate.options)) {
2204
+ const options = candidate.options.filter(isSchemaObject);
2205
+ // Enum unions expose primitive options; they are vocabularies, not result branches.
2206
+ if (options.length < 2)
2207
+ return;
2208
+ if (isRootUnion || isDiscriminatedResultUnion(options)) {
2209
+ options.forEach((option, index) => {
2210
+ const branchPath = `${nodePath}.union[${index}]`;
2211
+ branches.push({ path: branchPath, valuePath, schema: option, ancestors });
2212
+ visit(option, branchPath, valuePath, false, [...ancestors, { valuePath, schema: option }], nextStack);
2213
+ });
2214
+ return;
2215
+ }
2216
+ // Non-material nested union: keep descending so a deeper provider union is
2217
+ // still reachable, without claiming this wrapper as a coverage family.
2218
+ options.forEach((option, index) => visit(option, `${nodePath}.option[${index}]`, valuePath, false, ancestors, nextStack));
2219
+ return;
2220
+ }
2221
+ if (candidate.shape) {
2222
+ for (const [key, value] of Object.entries(candidate.shape)) {
2223
+ visit(value, `${nodePath}.${key}`, [...valuePath, key], false, ancestors, nextStack);
2224
+ }
2225
+ return;
2226
+ }
2227
+ // Array elements are descended with an array path segment so unions nested
2228
+ // inside list values (batch items, provider lists) are material families too.
2229
+ if (candidate.element !== undefined) {
2230
+ visit(candidate.element, `${nodePath}[]`, [...valuePath, ARRAY_PATH_SEGMENT], false, ancestors, nextStack);
2231
+ return;
2232
+ }
2233
+ if (typeof candidate.unwrap === "function") {
2234
+ visit(candidate.unwrap(), nodePath, valuePath, isRootUnion, ancestors, nextStack);
2235
+ }
2236
+ };
2237
+ visit(schema, "(root)", [], true, [], new Set());
2238
+ return branches;
2239
+ }
2240
+ /** Every value reachable through a path, fanning out across array elements. */
2241
+ function resolvePathValues(value, path) {
2242
+ if (path.length === 0)
2243
+ return [value];
2244
+ const [head, ...rest] = path;
2245
+ if (head === ARRAY_PATH_SEGMENT) {
2246
+ if (!Array.isArray(value))
2247
+ return [];
2248
+ return value.flatMap((item) => resolvePathValues(item, rest));
2249
+ }
2250
+ if (value === null || typeof value !== "object" || Array.isArray(value))
2251
+ return [];
2252
+ return resolvePathValues(value[head], rest);
2253
+ }
2254
+ function matchesAnyAncestor(fixture, ancestors) {
2255
+ return ancestors.every((ancestor) => resolvePathValues(fixture, ancestor.valuePath).some((target) => {
2256
+ if (target === undefined)
2257
+ return false;
2258
+ const schema = ancestor.schema;
2259
+ return typeof schema.safeParse === "function" && schema.safeParse(target).success;
2260
+ }));
2261
+ }
2262
+ /** Result-schema union branches a single catalog entry's fixtures do not cover. */
2263
+ export function resultBranchGaps(entry) {
2264
+ const branches = collectResultBranches(entry.resultSchema);
2265
+ const covered = new Set();
2266
+ for (const variant of entry.results) {
2267
+ branches.forEach((branch, index) => {
2268
+ if (!matchesAnyAncestor(variant.fixture, branch.ancestors))
2269
+ return;
2270
+ const schema = branch.schema;
2271
+ const matches = resolvePathValues(variant.fixture, branch.valuePath).some((target) => target !== undefined &&
2272
+ typeof schema.safeParse === "function" &&
2273
+ schema.safeParse(target).success);
2274
+ if (matches)
2275
+ covered.add(index);
2276
+ });
2277
+ }
2278
+ const gaps = [];
2279
+ branches.forEach((branch, index) => {
2280
+ if (!covered.has(index)) {
2281
+ gaps.push(`${entry.toolId}: uncovered result branch ${branch.path}`);
2282
+ }
2283
+ });
2284
+ return gaps;
2285
+ }
2286
+ /** Result-schema union branches not covered by any catalog result fixture. */
2287
+ export function resultCoverageGaps(entries = agentToolCatalog) {
2288
+ return entries.flatMap((entry) => resultBranchGaps(entry));
2289
+ }
2290
+ // END_BLOCK_FIXTURE_RUNNER
2291
+ // START_BLOCK_OPAQUE_SCAN
2292
+ const OPAQUE_SCAN_KEYWORDS = [
2293
+ "type",
2294
+ "enum",
2295
+ "const",
2296
+ "anyOf",
2297
+ "oneOf",
2298
+ "allOf",
2299
+ "not",
2300
+ "if",
2301
+ "then",
2302
+ "else",
2303
+ "properties",
2304
+ "additionalProperties",
2305
+ "patternProperties",
2306
+ "propertyNames",
2307
+ "items",
2308
+ "prefixItems",
2309
+ "contains",
2310
+ "$ref",
2311
+ ];
2312
+ function isPlainObject(value) {
2313
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2314
+ }
2315
+ function isObjectLike(record) {
2316
+ const typeValue = record.type;
2317
+ if (typeof typeValue === "string")
2318
+ return typeValue === "object";
2319
+ if (Array.isArray(typeValue))
2320
+ return typeValue.includes("object");
2321
+ return record.properties !== undefined || record.additionalProperties !== undefined;
2322
+ }
2323
+ function resolveLocalRef(ref, root) {
2324
+ if (typeof ref !== "string" || !ref.startsWith("#/"))
2325
+ return undefined;
2326
+ const segments = ref
2327
+ .slice(2)
2328
+ .split("/")
2329
+ .map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
2330
+ let current = root;
2331
+ for (const segment of segments) {
2332
+ if (!isPlainObject(current))
2333
+ return undefined;
2334
+ current = current[segment];
2335
+ }
2336
+ return current;
2337
+ }
2338
+ /**
2339
+ * Find arbitrary (non-closed) object or unconstrained nodes in a published input schema.
2340
+ * A node is opaque when it accepts an arbitrary value: an object that does not close
2341
+ * `additionalProperties` to false (including a missing `additionalProperties`), an empty
2342
+ * or keyword-free schema, a `true` schema, or an unresolvable `$ref`. Known object
2343
+ * properties, unions (`anyOf`/`oneOf`/`allOf`), array items, and resolvable local refs
2344
+ * are visited. Genuine opaque input regions are not permitted for public inputs; there is
2345
+ * no whole-tool exemption.
2346
+ */
2347
+ export function findOpaqueInputObjects(schema, basePath = "(root)", root = schema, visiting = new Set()) {
2348
+ const found = [];
2349
+ if (schema === true) {
2350
+ found.push(`${basePath}: unconstrained schema (true)`);
2351
+ return found;
2352
+ }
2353
+ if (schema === false || !isPlainObject(schema))
2354
+ return found;
2355
+ if (typeof schema.$ref === "string") {
2356
+ if (visiting.has(schema.$ref))
2357
+ return found;
2358
+ const resolved = resolveLocalRef(schema.$ref, root);
2359
+ if (resolved === undefined) {
2360
+ found.push(`${basePath}: unresolvable $ref "${schema.$ref}"`);
2361
+ return found;
2362
+ }
2363
+ const nextVisiting = new Set(visiting);
2364
+ nextVisiting.add(schema.$ref);
2365
+ found.push(...findOpaqueInputObjects(resolved, basePath, root, nextVisiting));
2366
+ return found;
2367
+ }
2368
+ const hasKeyword = OPAQUE_SCAN_KEYWORDS.some((keyword) => schema[keyword] !== undefined);
2369
+ if (!hasKeyword) {
2370
+ found.push(`${basePath}: unconstrained schema`);
2371
+ return found;
2372
+ }
2373
+ if (isObjectLike(schema)) {
2374
+ if (schema.additionalProperties !== false) {
2375
+ found.push(`${basePath}: open object (additionalProperties must be false${schema.additionalProperties === undefined ? "; none was declared" : ""})`);
2376
+ }
2377
+ const properties = schema.properties;
2378
+ if (isPlainObject(properties)) {
2379
+ for (const [key, value] of Object.entries(properties)) {
2380
+ found.push(...findOpaqueInputObjects(value, `${basePath}.${key}`, root, visiting));
2381
+ }
2382
+ }
2383
+ const patternProperties = schema.patternProperties;
2384
+ if (isPlainObject(patternProperties)) {
2385
+ for (const [key, value] of Object.entries(patternProperties)) {
2386
+ found.push(...findOpaqueInputObjects(value, `${basePath}.pattern(${key})`, root, visiting));
2387
+ }
2388
+ }
2389
+ }
2390
+ for (const keyword of ["anyOf", "oneOf", "allOf", "prefixItems"]) {
2391
+ const branches = schema[keyword];
2392
+ if (Array.isArray(branches)) {
2393
+ branches.forEach((branch, index) => found.push(...findOpaqueInputObjects(branch, `${basePath}.${keyword}[${index}]`, root, visiting)));
2394
+ }
2395
+ }
2396
+ if (schema.items !== undefined) {
2397
+ if (Array.isArray(schema.items)) {
2398
+ schema.items.forEach((branch, index) => found.push(...findOpaqueInputObjects(branch, `${basePath}.items[${index}]`, root, visiting)));
2399
+ }
2400
+ else {
2401
+ found.push(...findOpaqueInputObjects(schema.items, `${basePath}[]`, root, visiting));
2402
+ }
2403
+ }
2404
+ return found;
2405
+ }
2406
+ // END_BLOCK_OPAQUE_SCAN
2407
+ // START_BLOCK_REFERENCE
2408
+ function schemaTypeLabel(schema) {
2409
+ if (typeof schema !== "object" || schema === null || Array.isArray(schema))
2410
+ return "unknown";
2411
+ const record = schema;
2412
+ if (Array.isArray(record.enum))
2413
+ return `enum(${record.enum.join(" | ")})`;
2414
+ if (typeof record.type === "string")
2415
+ return record.type;
2416
+ if (Array.isArray(record.anyOf))
2417
+ return "union";
2418
+ return "object";
2419
+ }
2420
+ /** Escape Markdown table cell content so prose pipes cannot break the table. */
2421
+ function escapeTableCell(value) {
2422
+ return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
2423
+ }
2424
+ function topLevelFieldRows(entry) {
2425
+ const projection = entry.contract.inputJsonSchema;
2426
+ const properties = (projection.properties ?? {});
2427
+ const required = new Set(projection.required ?? []);
2428
+ const rows = [];
2429
+ for (const [name, fieldSchema] of Object.entries(properties)) {
2430
+ const description = typeof fieldSchema.description === "string"
2431
+ ? escapeTableCell(fieldSchema.description.replace(/\s+/g, " ").slice(0, 160))
2432
+ : "";
2433
+ rows.push(`| \`${escapeTableCell(name)}\` | ${escapeTableCell(schemaTypeLabel(fieldSchema))} | ${required.has(name) ? "yes" : "no"} | ${description} |`);
2434
+ }
2435
+ return rows;
2436
+ }
2437
+ /** Render one checked example as a real multi-line JSON fenced block under its list item. */
2438
+ function exampleBlock(operation, outcome) {
2439
+ const lines = [];
2440
+ lines.push(`- \`${operation.id}\` (${outcome}):`);
2441
+ lines.push(" ```json");
2442
+ for (const jsonLine of JSON.stringify(operation.input, null, 2).split("\n")) {
2443
+ lines.push(` ${jsonLine}`);
2444
+ }
2445
+ lines.push(" ```");
2446
+ return lines;
2447
+ }
2448
+ /**
2449
+ * Render the deterministic, on-demand tool-contracts reference from the catalog.
2450
+ * No timestamps, absolute paths, or environment values are embedded, so the
2451
+ * output is byte-stable and can be checked for currency by `bun run contracts:check`.
2452
+ * Each checked example's outcome is derived from running the actual validator, so
2453
+ * mislabeled metadata cannot be published as a passing example. `entries` is
2454
+ * injectable so self-tests can validate an isolated catalog without the global one.
2455
+ */
2456
+ export function renderToolContractsReference(entries = agentToolCatalog) {
2457
+ const size = measureCatalogContractSize(entries);
2458
+ const baseline = CONTRACT_SIZE_BASELINE;
2459
+ const lines = [];
2460
+ lines.push("# vvoc agent tool contracts");
2461
+ lines.push("");
2462
+ lines.push("Generated from the pure tool catalog. Do not edit by hand; run `bun run contracts:generate`.");
2463
+ lines.push("");
2464
+ lines.push(`- Package: \`${PACKAGE_NAME}@${PACKAGE_VERSION}\``);
2465
+ lines.push(`- Tool contract revision: \`${AGENT_TOOL_CONTRACT_REVISION}\``);
2466
+ lines.push(`- Reference path: \`${CONTRACT_REFERENCE_PACKAGE_PATH}\``);
2467
+ lines.push(`- Current model-facing size: descriptions ${size.descriptionBytes} bytes, published input schemas ${size.inputSchemaBytes} bytes`);
2468
+ lines.push(`- Baseline (commit ${baseline.provenance.commit}, SDK ${baseline.provenance.sdk}, ${baseline.provenance.method}): descriptions ${baseline.before.descriptionBytes} bytes, projected input schemas ${baseline.before.inputSchemaBytes} bytes`);
2469
+ lines.push("");
2470
+ lines.push("> Schema acceptance is structural only. It is not authorization, not proof of evidence,");
2471
+ lines.push("> not reviewer acceptance, and not permission for a workflow transition. A declared");
2472
+ lines.push("> `writeScope` is an edit boundary, not a universal sandbox, and no tool here grants a");
2473
+ lines.push("> state transition on its own.");
2474
+ lines.push("");
2475
+ lines.push("## Reading guide");
2476
+ lines.push("");
2477
+ lines.push("This is an on-demand reference; load only the tool section you need rather than the whole manual. Each tool section lists a closed field table, closed vocabularies, execute-time defaults, state/host prerequisites, conditional requirements, declared path kinds, result families, representative failures, and checked accept/reject examples. Tool sections: " +
2478
+ entries.map((entry) => `\`${entry.toolId}\``).join(", ") +
2479
+ ".");
2480
+ lines.push("");
2481
+ lines.push("## Tools");
2482
+ lines.push("");
2483
+ for (const entry of entries) {
2484
+ lines.push(`### \`${entry.toolId}\``);
2485
+ lines.push("");
2486
+ const firstDescriptionLine = entry.contract.description.split("\n")[0]?.trim() ?? "";
2487
+ const descriptionSummary = firstDescriptionLine.length > 240
2488
+ ? `${firstDescriptionLine.slice(0, 239)}…`
2489
+ : firstDescriptionLine;
2490
+ lines.push(`- Summary: ${entry.summary}`);
2491
+ lines.push(`- Description: ${descriptionSummary}`);
2492
+ lines.push("");
2493
+ lines.push("| field | type | required | description |");
2494
+ lines.push("| --- | --- | --- | --- |");
2495
+ for (const row of topLevelFieldRows(entry))
2496
+ lines.push(row);
2497
+ lines.push("");
2498
+ if (entry.vocabularies.length > 0) {
2499
+ lines.push("Closed vocabularies:");
2500
+ for (const vocabulary of entry.vocabularies) {
2501
+ lines.push(`- \`${vocabulary.field}\`: ${vocabulary.values.join(" | ")}`);
2502
+ }
2503
+ lines.push("");
2504
+ }
2505
+ if (entry.defaults.length > 0) {
2506
+ lines.push("Execute-time defaults:");
2507
+ for (const entryDefault of entry.defaults) {
2508
+ lines.push(`- \`${entryDefault.field}\` = \`${JSON.stringify(entryDefault.value)}\` (${entryDefault.note})`);
2509
+ }
2510
+ lines.push("");
2511
+ }
2512
+ if (entry.prerequisites.length > 0) {
2513
+ lines.push("State/host prerequisites:");
2514
+ for (const prerequisite of entry.prerequisites)
2515
+ lines.push(`- ${prerequisite}`);
2516
+ lines.push("");
2517
+ }
2518
+ if (entry.conditionals.length > 0) {
2519
+ lines.push("Conditional requirements:");
2520
+ for (const conditional of entry.conditionals)
2521
+ lines.push(`- ${conditional}`);
2522
+ lines.push("");
2523
+ }
2524
+ if (entry.pathKinds.length > 0) {
2525
+ lines.push("Path kinds:");
2526
+ for (const pathKind of entry.pathKinds) {
2527
+ lines.push(`- \`${pathKind.field}\` (${pathKind.kind}): ${pathKind.rules}`);
2528
+ }
2529
+ lines.push("");
2530
+ }
2531
+ lines.push("Result families:");
2532
+ for (const variant of entry.results)
2533
+ lines.push(`- \`${variant.id}\`: ${variant.label}`);
2534
+ lines.push("");
2535
+ if (entry.errors.length > 0) {
2536
+ lines.push("Representative failures:");
2537
+ for (const error of entry.errors)
2538
+ lines.push(`- \`${error.path}\`: ${error.expectation}`);
2539
+ lines.push("");
2540
+ }
2541
+ lines.push("Checked examples:");
2542
+ for (const operation of entry.operations) {
2543
+ const outcome = entry.validate(operation.input).ok ? "accept" : "reject";
2544
+ lines.push(...exampleBlock(operation, outcome));
2545
+ }
2546
+ lines.push("");
2547
+ }
2548
+ return `${lines.join("\n").trimEnd()}\n`;
2549
+ }
2550
+ // END_BLOCK_REFERENCE
2551
+ //# sourceMappingURL=agent-tool-catalog.js.map