@documentation-robotics/cli 0.3.0 → 0.4.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 (182) hide show
  1. package/README.md +10 -14
  2. package/dist/analyzers/active-analyzer.js +62 -0
  3. package/dist/analyzers/active-analyzer.js.map +7 -0
  4. package/dist/analyzers/cards/build.js +21 -1
  5. package/dist/analyzers/cards/build.js.map +2 -2
  6. package/dist/analyzers/cards/extract.js +138 -0
  7. package/dist/analyzers/cards/extract.js.map +7 -0
  8. package/dist/analyzers/cards/routes.js +96 -0
  9. package/dist/analyzers/cards/routes.js.map +7 -0
  10. package/dist/analyzers/cbm-analyzer.js +76 -4
  11. package/dist/analyzers/cbm-analyzer.js.map +2 -2
  12. package/dist/analyzers/cbm-responses.js +12 -6
  13. package/dist/analyzers/cbm-responses.js.map +2 -2
  14. package/dist/analyzers/code-changes.js +159 -0
  15. package/dist/analyzers/code-changes.js.map +7 -0
  16. package/dist/analyzers/edge-relationships.js +6 -2
  17. package/dist/analyzers/edge-relationships.js.map +2 -2
  18. package/dist/analyzers/extraction-plan.js +541 -0
  19. package/dist/analyzers/extraction-plan.js.map +7 -0
  20. package/dist/analyzers/index-job.js +229 -0
  21. package/dist/analyzers/index-job.js.map +7 -0
  22. package/dist/cli.js +12 -10
  23. package/dist/cli.js.map +2 -2
  24. package/dist/coding-agents/drbot-mcp.js +120 -18
  25. package/dist/coding-agents/drbot-mcp.js.map +2 -2
  26. package/dist/commands/add.js +4 -2
  27. package/dist/commands/add.js.map +2 -2
  28. package/dist/commands/analyzer.js +35 -35
  29. package/dist/commands/analyzer.js.map +2 -2
  30. package/dist/commands/changeset-verify.js +16 -8
  31. package/dist/commands/changeset-verify.js.map +2 -2
  32. package/dist/commands/changeset.js +41 -11
  33. package/dist/commands/changeset.js.map +2 -2
  34. package/dist/commands/init.js +9 -26
  35. package/dist/commands/init.js.map +2 -2
  36. package/dist/commands/mcp.js +35 -31
  37. package/dist/commands/mcp.js.map +3 -3
  38. package/dist/commands/relationship.js +2 -12
  39. package/dist/commands/relationship.js.map +2 -2
  40. package/dist/commands/reports.js +3 -3
  41. package/dist/commands/reports.js.map +2 -2
  42. package/dist/commands/update.js +14 -13
  43. package/dist/commands/update.js.map +2 -2
  44. package/dist/commands/upgrade.js +28 -120
  45. package/dist/commands/upgrade.js.map +2 -2
  46. package/dist/commands/validate.js +19 -3
  47. package/dist/commands/validate.js.map +2 -2
  48. package/dist/commands/version.js +1 -1
  49. package/dist/commands/visualize.js +30 -12
  50. package/dist/commands/visualize.js.map +2 -2
  51. package/dist/core/annotation-store.js +15 -5
  52. package/dist/core/annotation-store.js.map +2 -2
  53. package/dist/core/catalog-migration.js +2 -1
  54. package/dist/core/catalog-migration.js.map +2 -2
  55. package/dist/core/changeset-base-index.js +7 -0
  56. package/dist/core/changeset-base-index.js.map +2 -2
  57. package/dist/core/changeset-brief-compact.js +256 -0
  58. package/dist/core/changeset-brief-compact.js.map +7 -0
  59. package/dist/core/changeset-verify.js +356 -26
  60. package/dist/core/changeset-verify.js.map +3 -3
  61. package/dist/core/extraction-observability.js +161 -0
  62. package/dist/core/extraction-observability.js.map +7 -0
  63. package/dist/core/migration-registry.js +13 -1
  64. package/dist/core/migration-registry.js.map +2 -2
  65. package/dist/core/project-init.js +53 -0
  66. package/dist/core/project-init.js.map +7 -0
  67. package/dist/core/relationship-staging.js +94 -1
  68. package/dist/core/relationship-staging.js.map +2 -2
  69. package/dist/core/staged-changeset-storage.js +83 -3
  70. package/dist/core/staged-changeset-storage.js.map +2 -2
  71. package/dist/core/staging-area.js +85 -8
  72. package/dist/core/staging-area.js.map +2 -2
  73. package/dist/core/upgrade-plan.js +117 -0
  74. package/dist/core/upgrade-plan.js.map +7 -0
  75. package/dist/generated/relationship-index.js +124 -15
  76. package/dist/generated/relationship-index.js.map +2 -2
  77. package/dist/integrations/claude_code/.claude-plugin/plugin.json +24 -0
  78. package/dist/integrations/claude_code/agents/dr-architect.md +19 -5
  79. package/dist/integrations/claude_code/agents/dr-extractor.md +7 -1
  80. package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
  81. package/dist/integrations/claude_code/commands/dr-map.md +7 -1
  82. package/dist/integrations/claude_code/commands/dr-plan.md +42 -16
  83. package/dist/integrations/claude_code/docs/MCP_SERVER.md +28 -31
  84. package/dist/integrations/claude_code/docs/README.md +20 -1
  85. package/dist/integrations/claude_code/plugin-agents/dr-advisor.md +255 -0
  86. package/dist/integrations/claude_code/plugin-agents/dr-architect.md +306 -0
  87. package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +3 -0
  88. package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +90 -31
  89. package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +5 -0
  90. package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +5 -0
  91. package/dist/integrations/github_copilot/agents/dr-architect.md +2 -0
  92. package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +3 -0
  93. package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +88 -30
  94. package/dist/mcp/changeset-only.js +14 -3
  95. package/dist/mcp/changeset-only.js.map +2 -2
  96. package/dist/mcp/project-root.js +85 -0
  97. package/dist/mcp/project-root.js.map +7 -0
  98. package/dist/mcp/server-instructions.js +27 -0
  99. package/dist/mcp/server-instructions.js.map +7 -0
  100. package/dist/mcp/strict-schema.js +52 -0
  101. package/dist/mcp/strict-schema.js.map +7 -0
  102. package/dist/mcp/tool-registry.js +39 -5
  103. package/dist/mcp/tool-registry.js.map +2 -2
  104. package/dist/mcp/tools/changeset-analysis-tools.js +25 -5
  105. package/dist/mcp/tools/changeset-analysis-tools.js.map +3 -3
  106. package/dist/mcp/tools/changeset-commit.js +224 -0
  107. package/dist/mcp/tools/changeset-commit.js.map +7 -0
  108. package/dist/mcp/tools/changeset-editing.js +133 -57
  109. package/dist/mcp/tools/changeset-editing.js.map +3 -3
  110. package/dist/mcp/tools/changeset-lifecycle.js +1 -1
  111. package/dist/mcp/tools/changeset-lifecycle.js.map +2 -2
  112. package/dist/mcp/tools/changeset-verify.js +12 -6
  113. package/dist/mcp/tools/changeset-verify.js.map +2 -2
  114. package/dist/mcp/tools/codebase-extract.js +836 -0
  115. package/dist/mcp/tools/codebase-extract.js.map +7 -0
  116. package/dist/mcp/tools/codebase-index.js +125 -0
  117. package/dist/mcp/tools/codebase-index.js.map +7 -0
  118. package/dist/mcp/tools/codebase-shared.js +265 -0
  119. package/dist/mcp/tools/codebase-shared.js.map +7 -0
  120. package/dist/mcp/tools/codebase-verify.js +48 -0
  121. package/dist/mcp/tools/codebase-verify.js.map +7 -0
  122. package/dist/mcp/tools/index.js +15 -0
  123. package/dist/mcp/tools/index.js.map +2 -2
  124. package/dist/mcp/tools/model-add.js +189 -73
  125. package/dist/mcp/tools/model-add.js.map +2 -2
  126. package/dist/mcp/tools/model-update.js +16 -12
  127. package/dist/mcp/tools/model-update.js.map +2 -2
  128. package/dist/mcp/tools/model-validate.js +46 -4
  129. package/dist/mcp/tools/model-validate.js.map +2 -2
  130. package/dist/mcp/tools/project-status-collectors.js +381 -0
  131. package/dist/mcp/tools/project-status-collectors.js.map +7 -0
  132. package/dist/mcp/tools/project-status.js +237 -0
  133. package/dist/mcp/tools/project-status.js.map +7 -0
  134. package/dist/mcp/tools/project-tools.js +505 -0
  135. package/dist/mcp/tools/project-tools.js.map +7 -0
  136. package/dist/mcp/tools/relationship-tools.js +89 -20
  137. package/dist/mcp/tools/relationship-tools.js.map +2 -2
  138. package/dist/mcp/tools/review-open.js +210 -0
  139. package/dist/mcp/tools/review-open.js.map +7 -0
  140. package/dist/mcp/tools/shared.js +10 -2
  141. package/dist/mcp/tools/shared.js.map +2 -2
  142. package/dist/reports/model-report-orchestrator.js +31 -2
  143. package/dist/reports/model-report-orchestrator.js.map +2 -2
  144. package/dist/schemas/bundled/analyzers/cbm.json +22 -10
  145. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  146. package/dist/schemas/bundled/api.json +1 -1
  147. package/dist/schemas/bundled/apm.json +1 -1
  148. package/dist/schemas/bundled/application.json +1 -1
  149. package/dist/schemas/bundled/base.json +1 -1
  150. package/dist/schemas/bundled/business.json +1 -1
  151. package/dist/schemas/bundled/data-model.json +1 -1
  152. package/dist/schemas/bundled/data-store.json +1 -1
  153. package/dist/schemas/bundled/manifest.json +3 -3
  154. package/dist/schemas/bundled/motivation.json +1 -1
  155. package/dist/schemas/bundled/navigation.json +1 -1
  156. package/dist/schemas/bundled/product.json +1 -1
  157. package/dist/schemas/bundled/security.json +1 -1
  158. package/dist/schemas/bundled/technology.json +1 -1
  159. package/dist/schemas/bundled/testing.json +145 -1
  160. package/dist/schemas/bundled/ux.json +1 -1
  161. package/dist/server/server.js +508 -237
  162. package/dist/server/server.js.map +3 -3
  163. package/dist/server/viewer-registry.js +264 -0
  164. package/dist/server/viewer-registry.js.map +7 -0
  165. package/dist/telemetry/index.js +1 -1
  166. package/dist/utils/id-generator.js +5 -2
  167. package/dist/utils/id-generator.js.map +2 -2
  168. package/dist/utils/source-reference.js +58 -27
  169. package/dist/utils/source-reference.js.map +2 -2
  170. package/dist/utils/spec-version.js +2 -2
  171. package/dist/utils/spec-version.js.map +1 -1
  172. package/dist/validators/model-warnings.js +28 -0
  173. package/dist/validators/model-warnings.js.map +7 -0
  174. package/dist/viewer/README.md +6 -6
  175. package/dist/viewer/assets/main-CUOQGGRA.css +1 -0
  176. package/dist/viewer/assets/main-DvH3gYrI.js +84 -0
  177. package/dist/viewer/index.html +2 -2
  178. package/dist/viewer/manifest.json +10 -10
  179. package/dist/viewer/version.json +1 -1
  180. package/package.json +1 -1
  181. package/dist/viewer/assets/main-CxzcuU_o.js +0 -84
  182. package/dist/viewer/assets/main-j-7ULYpm.css +0 -1
@@ -23,19 +23,28 @@ graph checks the plan for you. The full design is `docs/CHANGE_MODELING.md` in t
23
23
 
24
24
  ## Before you start
25
25
 
26
- - You need the `dr` MCP server (`dr mcp`, registered in Copilot's MCP configuration with the
27
- `DR_MCP_API_KEY` it prints on start; see `integrations/claude_code/docs/MCP_SERVER.md`). The
26
+ - You need the `dr` MCP server (`dr mcp`, registered in Copilot's MCP configuration; see
27
+ `integrations/claude_code/docs/MCP_SERVER.md`). The
28
28
  tool names below are the server's own; Copilot may show them with a server prefix. If the tools
29
29
  are missing, tell the user to register the server and stop. Don't fall back to prose.
30
30
  - Staging writes files under `documentation-robotics/changesets/`.
31
- - You never commit, apply or discard the plan. Those are the human's steps, and commit and apply are
32
- not MCP tools.
31
+ - If the project has no model yet, propose a name and confirm it with the human before
32
+ `project_init`. Then ask whether to bootstrap the model from the code now or let it grow with
33
+ each change, and wait for their answer.
34
+ - When `codebase_extract` returns cards (in a bootstrap, or when you verify), ask the developer about
35
+ the ones you can't place with confidence: whether a card is a model element at all, which type it
36
+ is, or whether to reject it. Don't guess. Collect them and ask once, in one short list (the card,
37
+ your best guess, the question), then stage or reject from the answers.
38
+ - You never land or discard the plan on your own. Commit it (`changeset_commit`) only when the
39
+ human asks you to land it; they confirm it in a dialog. Apply is not an MCP tool. Discard only when
40
+ they agree.
33
41
 
34
42
  ## The loop
35
43
 
36
44
  1. **Frame.** `changeset_list` first: if a changeset for this change exists, `changeset_activate` it
37
- and continue. Otherwise `changeset_create` with a short kebab-case `name` and a `description`
38
- that states the intent: what the change is for and what done looks like. Find the goal,
45
+ and continue. Otherwise propose a short kebab-case `name` and a `description` that states the
46
+ intent (what the change is for and what done looks like), and confirm both with the human before
47
+ you call `changeset_create` with them. Wait for their answer. Find the goal,
39
48
  requirement or feature it serves (`model_search` in motivation, business, product). If there is
40
49
  none, stage one, or ask the human what the change is for.
41
50
  2. **Orient.** `model_search` / `model_show` / `model_trace` for the elements the change touches.
@@ -46,19 +55,29 @@ graph checks the plan for you. The full design is `docs/CHANGE_MODELING.md` in t
46
55
  element the change adds or alters.
47
56
  3. **Draft.** Stage the change in every layer it reaches: `model_add`, `model_update`,
48
57
  `model_delete`, `relationship_add`, `relationship_delete`. Relationships may join elements that
49
- exist only in the changeset. Pass `sourceFile`, `sourceProvenance` (`manual` for a planned location) and `sourceSymbol` when you know where the code goes.
58
+ exist only in the changeset. When you wire several, use the lists: `model_add` with
59
+ `elements: [...]` (plus `relationships` that may name elements added in the same call), and
60
+ `relationship_add` with `relationships: [{source, predicate, target, properties?}]`. Each list is
61
+ staged in one write; an invalid item is skipped and reported, so read the per-item results. For
62
+ a planned element with no code yet, send no source fields. Pass `sourceFile` and `sourceSymbol`
63
+ (with `sourceProvenance: "manual"`) only when you know where the code goes.
50
64
  Change one attribute with `model_update` `attributePatch`, not by repeating every attribute. Fix a
51
65
  mistake with `changeset_unstage` (one element, one relationship, or `source` or `target` alone for
52
- every staged connection at that end), not by staging the reverse.
66
+ every staged connection at that end), not by staging the reverse. To drop several, send them as
67
+ `changes: [...]`: one call, one write, a result per item.
53
68
  4. **Pressure-test.** `changeset_brief`, then `changeset_impact` for anything the brief questions.
69
+ The brief is compact by default; pass `detail: "full"` only when you need every section in full.
54
70
  Steps 3 and 4 are a loop: a plan comes into focus over many passes. Each pass, stage a little,
55
71
  then:
56
72
  - Pass the last brief's `revisionToken` as `since`; read the `delta` first. It covers unstages
57
73
  too (`unstaged`). If it warns (a hand-edited `changes.yaml`, or a reach back past the last 200
58
74
  unstages), read the brief itself.
59
- - Work through `unwired` for each element you added or changed. Where `existingCount` is above
60
- 0, link an existing element; otherwise decide whether the change needs a new one. Use
61
- `unwiredLayers` to see one layer in full, and `unwired: "none"` when you only need the gaps.
75
+ - Work through `unwired` for each element you added or changed. The default brief only counts
76
+ it per layer, and `sections` returns only the sections it names, so list the entries with
77
+ `sections: ["unwired"], unwiredLayers: [<the layers the change reaches>]`, narrowed further with
78
+ `layers` or `ids` if you like. Always pass a filter: unfiltered it runs to hundreds of KB on a
79
+ large model. Where `existingCount` is above 0, link an existing element; otherwise decide
80
+ whether the change needs a new one.
62
81
  - Walk `changeset_impact` with `from` set to what you just added, not the whole changeset.
63
82
  - Read the plan with `changeset_show` `detail: "summary"`, not the full snapshots.
64
83
 
@@ -77,29 +96,63 @@ graph checks the plan for you. The full design is `docs/CHANGE_MODELING.md` in t
77
96
  (`changeset_update`). `model_validate` should pass on the projected model.
78
97
 
79
98
  5. **Review.** Stop and show the human the brief: changes by layer, gaps fixed and gaps accepted,
80
- impact, code touched. Ask them to look at it in `dr visualize` and comment on elements there.
81
- Read their comments with `annotation_list`, answer with `annotation_reply`, revise, and run the
82
- brief again. Don't start building until they agree.
83
- 6. **Build.** Build from the brief. Its "code touched" list is what you will edit or must not
99
+ impact, code touched. Call `review_open` and give them its `url`: it opens the plan on the graph
100
+ in the viewer, reusing the viewer already running for the project or starting one. The human
101
+ comments on elements there, including ones the plan adds. Read their comments with
102
+ `annotation_list`, answer with `annotation_reply`, revise, and run the brief again. Don't start building until they agree.
103
+ 6. **Build.** Build from the brief. At the start of the build, read it with
104
+ `sections: ["changes"]`: each changed element's attributes and changed fields, and its
105
+ connections as `predicate → id` lines. For a large plan, read one layer at a time with
106
+ `sections: ["changes"], layers: ["<layer>"]`, taking the layers from the default brief's
107
+ `byLayer`. The default brief's "code touched" list is what you will edit or must not
84
108
  break. Keep the changeset staged and commit only the plan's own directory
85
109
  (`documentation-robotics/changesets/<id>/`, with the id `changeset_show` reports) on the branch
86
110
  with the code. Don't add the whole `changesets/` directory: the `.active` marker and any
87
111
  `<name>-as-built` changeset stay out of the commit. If the build has to differ from the plan,
88
- change the plan to match and say why.
89
- 7. **Verify.** Write down what was built, separately from the plan: `changeset_create`
90
- `<name>-as-built`, stage what the diff actually contains (read the code, not the plan), then
91
- `changeset_verify` with `planned: <name>` and `against: {"changesetId": "<name>-as-built"}`.
92
- Fix each missing, unplanned or divergent item in the code or in the plan. Look for missing
93
- connections in the as-built too: for each element, compare what the spec models for its type
94
- with what was staged, and look for evidence of each absent one in the code (and the analyzer's
95
- graph, `dr analyzer callers` / `callees`, when it is indexed). Confirm or reject
96
- every `probable` match. `changeset_activate` the plan again afterwards. Tell the human the
97
- as-built changeset can be discarded; discard it (`changeset_discard`, `confirm: true`) only when
98
- they agree. Until then it stays out of git: when you commit fixes, add only the plan's
99
- directory again. If the as-built changes are committed to the model instead of kept as a changeset,
100
- `against: {"base": true}` compares the plan with the model.
101
- 8. **Land.** Hand over: the PR carries the code and the staged changeset. The human commits it,
102
- in the viewer or with `dr changeset commit`.
112
+ change the plan to match and say why. Don't verify during the build: no `codebase_extract`, no
113
+ as-built changeset and no `changeset_verify` until the build is committed. Verify is the next
114
+ step, on its own.
115
+ 7. **Verify.** Verify as its own step, after the build (the code and the plan's directory) is
116
+ committed, not in the turn that builds it. Extract what was built, separately from the plan. The
117
+ as-built changeset comes from `codebase_extract` and the code. Never hand-write it, and never copy
118
+ plan elements into it: name each element after its card (the code's symbol), not after the plan's
119
+ element, and don't add planned elements extraction can't see (tests, intent) to make the verdict
120
+ match. Re-index the code once the build is committed: `codebase_index` with `wait: 30` (it re-indexes when source files changed since the last
121
+ index, committed or not), then `status: true` and `wait: 30` until the job is done; don't call it
122
+ in a quick loop. If the result reports `ignoreFile` (the `.cbmignore` it wrote so DR's own folders
123
+ aren't indexed), tell the developer to commit that file. Then `codebase_extract`
124
+ with `since` set to the branch the work started from (e.g. `main`) and
125
+ `changeset: "<name>-as-built"`. It stages what the rules decide in the changed files and leaves
126
+ `<name>-as-built` active. If it staged nothing, it makes no changeset: `changeset_create` with the
127
+ name it gives before you `model_add` its cards. For each card it returns that is a model element, stage it with
128
+ `model_add` (`sourceFile` and `sourceSymbol` from the card, `sourceProvenance: "extracted"`),
129
+ reading the code, not the plan, and ask the developer about the cards you can't place, in one short
130
+ list;
131
+ then call `codebase_extract` again to stage their relationships, and `model_delete` the
132
+ `candidateDeletes` that are really gone. Then `changeset_verify` with `planned: <name>` and
133
+ `against: {"changesetId": "<name>-as-built"}`. Fix each missing, unplanned or divergent item in
134
+ the code or in the plan. `unverifiable` items are planned parts extraction can't see (motivation,
135
+ re-exports, TypeScript tests), and `notes` are description-only differences: neither needs a
136
+ plan edit. Never edit the plan to match extracted placeholder text. A verdict of `unverifiable`
137
+ means the plan has nothing extraction can check; say so rather than call it a failure. Look for missing connections in the as-built too: for each element,
138
+ compare what the spec models for its type with what was staged, and look for evidence of each
139
+ absent one in the code (and the analyzer's graph, `dr analyzer callers` / `callees`). Confirm or
140
+ reject every `probable` match. `changeset_activate` the plan again afterwards (the extract
141
+ result's `previousActive`). After verify, the plan is what lands (step 8), not the as-built
142
+ changeset. Ask the human whether to discard the as-built changeset (`changeset_discard`,
143
+ `confirm: true`) or keep it as a record, and discard it only when they agree. Don't land it:
144
+ landing it first changes the plan's base, so the plan then needs `force`, and the plan's
145
+ descriptions don't apply to elements already in the base. It stays out of git: when you commit
146
+ fixes, add only the plan's directory again. If the as-built changes were landed anyway,
147
+ `against: {"base": true, "extracted": true}` compares the plan with the model, with extraction's blind spots applied.
148
+ 8. **Land.** The PR carries the code and the staged plan, and the human commits the plan in it, not
149
+ the as-built changeset. When they ask you to land it, call `changeset_commit` with its name. The server asks them
150
+ to confirm in a dialog that shows the name, intent, counts by operation, layers and drift, and
151
+ commits only if they confirm; if they decline, leave it staged. If it is refused for drift, show
152
+ them what drifted (`changeset_brief`) and pass `force: true` only if they still want it. Then add
153
+ the model's changed files and the changeset's directory to the branch. If it is refused, they land
154
+ it in the viewer (`review_open`, then Apply on the changeset), or you call it again from an
155
+ interactive session. Never point them to the `dr` CLI.
103
156
 
104
157
  ## Tolerance
105
158
 
@@ -109,9 +162,14 @@ Tools report odd input and carry on instead of failing. Read the report each tim
109
162
  - `changeset_unstage` reports `not-staged` when there was nothing to drop.
110
163
  - In a changeset-only session (DrBot with `--with-changesets`), a write with no active changeset is
111
164
  refused. Call `changeset_create` or `changeset_activate` and retry.
165
+ - `changeset_commit` is refused when the human declines or cancels the dialog, when the client
166
+ can't show one (no MCP elicitation), and in DrBot, which doesn't have it. Nothing is written; the
167
+ human lands it in the viewer (`review_open`, then Apply), or from an interactive session.
112
168
  - DrBot cannot write annotations (`annotation_create` and `annotation_reply` are not among its
113
169
  tools). There, record accepted gaps in the description with `changeset_update` and answer the
114
170
  human's comments in the chat.
171
+ - DrBot has no `review_open`: it runs in the viewer, so the human already has the plan in front
172
+ of them. Point them to the changeset in the viewer's Changesets view instead.
115
173
 
116
174
  ## Keep it short
117
175
 
@@ -8,7 +8,12 @@ const BASE_MODEL_WRITE_TOOLS = /* @__PURE__ */ new Set([
8
8
  "model_delete",
9
9
  "relationship_add",
10
10
  "relationship_delete"
11
- ]);
11
+ ]), OUTSIDE_CHANGESET_TOOLS = /* @__PURE__ */ new Set(["project_init", "project_upgrade"]);
12
+ function outsideChangesetError(toolName) {
13
+ return new CLIError(`This session can only change the model through a changeset, and "${toolName}" writes outside one.`, ErrorCategory.USER, [
14
+ toolName === "project_init" ? "Ask the user to create the model themselves (dr init, or project_init in a session without changeset-only mode)" : "Ask the user to upgrade the model themselves (dr upgrade, or project_upgrade in a session without changeset-only mode)"
15
+ ], { operation: toolName, context: `${CHANGESET_ONLY_ENV}=${process.env[CHANGESET_ONLY_ENV] ?? "true"}` });
16
+ }
12
17
  function changesetOnlyError(toolName) {
13
18
  const retry = toolName ? `Then call "${toolName}" again` : "Then retry the call";
14
19
  return new CLIError("This session can only change the model through a changeset, and no changeset is active.", ErrorCategory.USER, [
@@ -17,7 +22,11 @@ function changesetOnlyError(toolName) {
17
22
  ], { operation: toolName ?? "save model", context: `${CHANGESET_ONLY_ENV}=${process.env[CHANGESET_ONLY_ENV] ?? "true"}` });
18
23
  }
19
24
  async function changesetOnlyRefusal(toolName, rootPath, loadModel, env = process.env) {
20
- if (!isChangesetOnly(env) || !BASE_MODEL_WRITE_TOOLS.has(toolName))
25
+ if (!isChangesetOnly(env))
26
+ return null;
27
+ if (OUTSIDE_CHANGESET_TOOLS.has(toolName))
28
+ return outsideChangesetError(toolName);
29
+ if (!BASE_MODEL_WRITE_TOOLS.has(toolName))
21
30
  return null;
22
31
  try {
23
32
  const model = await loadModel(rootPath);
@@ -28,7 +37,9 @@ async function changesetOnlyRefusal(toolName, rootPath, loadModel, env = process
28
37
  }
29
38
  export {
30
39
  BASE_MODEL_WRITE_TOOLS,
40
+ OUTSIDE_CHANGESET_TOOLS,
31
41
  changesetOnlyError,
32
- changesetOnlyRefusal
42
+ changesetOnlyRefusal,
43
+ outsideChangesetError
33
44
  };
34
45
  //# sourceMappingURL=changeset-only.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/mcp/changeset-only.ts"],
4
- "sourcesContent": ["/**\n * Changeset-only mode on the MCP server.\n *\n * When `dr mcp` runs with DR_CHANGESET_ONLY=true (DrBot under `dr visualize --with-changesets`\n * starts it that way), an agent may change the model only by staging into an active changeset.\n * The tools below write the base model when no changeset is active, so the tool registry asks\n * `changesetOnlyRefusal()` before running them and returns its refusal instead: the agent is told\n * to call changeset_create (or changeset_activate) and retry.\n *\n * This check runs before the handler so a refused call never touches the cached in-memory model.\n * Any other base-model save still hits the core guard (core/changeset-only-guard.ts), whose\n * ChangesetOnlyError runTool turns into the same refusal, after dropping the model cache so a\n * half-applied in-memory edit can't linger.\n */\n\nimport type { Model } from \"../core/model.js\";\nimport { isChangesetOnly, CHANGESET_ONLY_ENV } from \"../core/changeset-only-guard.js\";\nimport { StagingAreaManager } from \"../core/staging-area.js\";\nimport { CLIError, ErrorCategory, getErrorMessage } from \"../utils/errors.js\";\nimport { activeStagedChangeset } from \"./tools/changeset-shared.js\";\n\n/** Tools that write the base model when no changeset is active. */\nexport const BASE_MODEL_WRITE_TOOLS: ReadonlySet<string> = new Set([\n \"model_add\",\n \"model_update\",\n \"model_delete\",\n \"relationship_add\",\n \"relationship_delete\",\n]);\n\n/** The error an agent gets for a base-model write in changeset-only mode. */\nexport function changesetOnlyError(toolName?: string): CLIError {\n const retry = toolName ? `Then call \"${toolName}\" again` : \"Then retry the call\";\n return new CLIError(\n \"This session can only change the model through a changeset, and no changeset is active.\",\n ErrorCategory.USER,\n [\n 'Call \"changeset_create\" with a name for the change (it activates the new changeset), or ' +\n '\"changeset_activate\" to resume one from \"changeset_list\"',\n `${retry}: it will be staged in the changeset for the user to review and commit`,\n ],\n { operation: toolName ?? \"save model\", context: `${CHANGESET_ONLY_ENV}=${process.env[CHANGESET_ONLY_ENV] ?? \"true\"}` }\n );\n}\n\n/**\n * The refusal for this call, or null when it may run: always null outside changeset-only mode,\n * for tools that don't write the base model, and when a staged changeset is active.\n *\n * If the model or the active changeset can't be read, the check steps aside (null, logged) and\n * the tool reports that failure itself; the core guard still stops any base-model save.\n */\nexport async function changesetOnlyRefusal(\n toolName: string,\n rootPath: string | undefined,\n loadModel: (rootPath?: string) => Promise<Model>,\n env: NodeJS.ProcessEnv = process.env\n): Promise<CLIError | null> {\n if (!isChangesetOnly(env) || !BASE_MODEL_WRITE_TOOLS.has(toolName)) {\n return null;\n }\n try {\n const model = await loadModel(rootPath);\n const active = await activeStagedChangeset(new StagingAreaManager(model.rootPath, model));\n return active ? null : changesetOnlyError(toolName);\n } catch (error) {\n console.error(`[mcp] changeset-only check for ${toolName} skipped: ${getErrorMessage(error)}`);\n return null;\n }\n}\n"],
5
- "mappings": "AAgBA,SAAS,iBAAiB,0BAA0B;AACpD,SAAS,0BAA0B;AACnC,SAAS,UAAU,eAAe,uBAAuB;AACzD,SAAS,6BAA6B;AAG/B,MAAM,yBAA8C,oBAAI,IAAI;EACjE;EACA;EACA;EACA;EACA;CACD;AAGK,SAAU,mBAAmB,UAAiB;AAClD,QAAM,QAAQ,WAAW,cAAc,QAAQ,YAAY;AAC3D,SAAO,IAAI,SACT,2FACA,cAAc,MACd;IACE;IAEA,GAAG,KAAK;KAEV,EAAE,WAAW,YAAY,cAAc,SAAS,GAAG,kBAAkB,IAAI,QAAQ,IAAI,kBAAkB,KAAK,MAAM,GAAE,CAAE;AAE1H;AASA,eAAsB,qBACpB,UACA,UACA,WACA,MAAyB,QAAQ,KAAG;AAEpC,MAAI,CAAC,gBAAgB,GAAG,KAAK,CAAC,uBAAuB,IAAI,QAAQ;AAC/D,WAAO;AAET,MAAI;AACF,UAAM,QAAQ,MAAM,UAAU,QAAQ;AAEtC,WADe,MAAM,sBAAsB,IAAI,mBAAmB,MAAM,UAAU,KAAK,CAAC,IACxE,OAAO,mBAAmB,QAAQ;EACpD,SAAS,OAAO;AACd,mBAAQ,MAAM,kCAAkC,QAAQ,aAAa,gBAAgB,KAAK,CAAC,EAAE,GACtF;EACT;AACF;",
4
+ "sourcesContent": ["/**\n * Changeset-only mode on the MCP server.\n *\n * When `dr mcp` runs with DR_CHANGESET_ONLY=true (DrBot under `dr visualize --with-changesets`\n * starts it that way), an agent may change the model only by staging into an active changeset.\n * The tools below write the base model when no changeset is active, so the tool registry asks\n * `changesetOnlyRefusal()` before running them and returns its refusal instead: the agent is told\n * to call changeset_create (or changeset_activate) and retry.\n *\n * This check runs before the handler so a refused call never touches the cached in-memory model.\n * Any other base-model save still hits the core guard (core/changeset-only-guard.ts), whose\n * ChangesetOnlyError runTool turns into the same refusal, after dropping the model cache so a\n * half-applied in-memory edit can't linger.\n */\n\nimport type { Model } from \"../core/model.js\";\nimport { isChangesetOnly, CHANGESET_ONLY_ENV } from \"../core/changeset-only-guard.js\";\nimport { StagingAreaManager } from \"../core/staging-area.js\";\nimport { CLIError, ErrorCategory, getErrorMessage } from \"../utils/errors.js\";\nimport { activeStagedChangeset } from \"./tools/changeset-shared.js\";\n\n/** Tools that write the base model when no changeset is active. */\nexport const BASE_MODEL_WRITE_TOOLS: ReadonlySet<string> = new Set([\n \"model_add\",\n \"model_update\",\n \"model_delete\",\n \"relationship_add\",\n \"relationship_delete\",\n]);\n\n/**\n * Tools that write outside any changeset (they create or migrate the model itself), refused in\n * changeset-only mode whether or not a changeset is active.\n */\nexport const OUTSIDE_CHANGESET_TOOLS: ReadonlySet<string> = new Set([\"project_init\", \"project_upgrade\"]);\n\n/** The error an agent gets for a tool that writes outside a changeset in changeset-only mode. */\nexport function outsideChangesetError(toolName: string): CLIError {\n return new CLIError(\n `This session can only change the model through a changeset, and \"${toolName}\" writes outside one.`,\n ErrorCategory.USER,\n [\n toolName === \"project_init\"\n ? \"Ask the user to create the model themselves (dr init, or project_init in a session without changeset-only mode)\"\n : \"Ask the user to upgrade the model themselves (dr upgrade, or project_upgrade in a session without changeset-only mode)\",\n ],\n { operation: toolName, context: `${CHANGESET_ONLY_ENV}=${process.env[CHANGESET_ONLY_ENV] ?? \"true\"}` }\n );\n}\n\n/** The error an agent gets for a base-model write in changeset-only mode. */\nexport function changesetOnlyError(toolName?: string): CLIError {\n const retry = toolName ? `Then call \"${toolName}\" again` : \"Then retry the call\";\n return new CLIError(\n \"This session can only change the model through a changeset, and no changeset is active.\",\n ErrorCategory.USER,\n [\n 'Call \"changeset_create\" with a name for the change (it activates the new changeset), or ' +\n '\"changeset_activate\" to resume one from \"changeset_list\"',\n `${retry}: it will be staged in the changeset for the user to review and commit`,\n ],\n { operation: toolName ?? \"save model\", context: `${CHANGESET_ONLY_ENV}=${process.env[CHANGESET_ONLY_ENV] ?? \"true\"}` }\n );\n}\n\n/**\n * The refusal for this call, or null when it may run: always null outside changeset-only mode,\n * for tools that don't write the base model, and when a staged changeset is active. Tools that\n * write outside any changeset (project_init, project_upgrade) are always refused in the mode.\n *\n * If the model or the active changeset can't be read, the check steps aside (null, logged) and\n * the tool reports that failure itself; the core guard still stops any base-model save.\n */\nexport async function changesetOnlyRefusal(\n toolName: string,\n rootPath: string | undefined,\n loadModel: (rootPath?: string) => Promise<Model>,\n env: NodeJS.ProcessEnv = process.env\n): Promise<CLIError | null> {\n if (!isChangesetOnly(env)) {\n return null;\n }\n if (OUTSIDE_CHANGESET_TOOLS.has(toolName)) {\n return outsideChangesetError(toolName);\n }\n if (!BASE_MODEL_WRITE_TOOLS.has(toolName)) {\n return null;\n }\n try {\n const model = await loadModel(rootPath);\n const active = await activeStagedChangeset(new StagingAreaManager(model.rootPath, model));\n return active ? null : changesetOnlyError(toolName);\n } catch (error) {\n console.error(`[mcp] changeset-only check for ${toolName} skipped: ${getErrorMessage(error)}`);\n return null;\n }\n}\n"],
5
+ "mappings": "AAgBA,SAAS,iBAAiB,0BAA0B;AACpD,SAAS,0BAA0B;AACnC,SAAS,UAAU,eAAe,uBAAuB;AACzD,SAAS,6BAA6B;AAG/B,MAAM,yBAA8C,oBAAI,IAAI;EACjE;EACA;EACA;EACA;EACA;CACD,GAMY,0BAA+C,oBAAI,IAAI,CAAC,gBAAgB,iBAAiB,CAAC;AAGjG,SAAU,sBAAsB,UAAgB;AACpD,SAAO,IAAI,SACT,oEAAoE,QAAQ,yBAC5E,cAAc,MACd;IACE,aAAa,iBACT,oHACA;KAEN,EAAE,WAAW,UAAU,SAAS,GAAG,kBAAkB,IAAI,QAAQ,IAAI,kBAAkB,KAAK,MAAM,GAAE,CAAE;AAE1G;AAGM,SAAU,mBAAmB,UAAiB;AAClD,QAAM,QAAQ,WAAW,cAAc,QAAQ,YAAY;AAC3D,SAAO,IAAI,SACT,2FACA,cAAc,MACd;IACE;IAEA,GAAG,KAAK;KAEV,EAAE,WAAW,YAAY,cAAc,SAAS,GAAG,kBAAkB,IAAI,QAAQ,IAAI,kBAAkB,KAAK,MAAM,GAAE,CAAE;AAE1H;AAUA,eAAsB,qBACpB,UACA,UACA,WACA,MAAyB,QAAQ,KAAG;AAEpC,MAAI,CAAC,gBAAgB,GAAG;AACtB,WAAO;AAET,MAAI,wBAAwB,IAAI,QAAQ;AACtC,WAAO,sBAAsB,QAAQ;AAEvC,MAAI,CAAC,uBAAuB,IAAI,QAAQ;AACtC,WAAO;AAET,MAAI;AACF,UAAM,QAAQ,MAAM,UAAU,QAAQ;AAEtC,WADe,MAAM,sBAAsB,IAAI,mBAAmB,MAAM,UAAU,KAAK,CAAC,IACxE,OAAO,mBAAmB,QAAQ;EACpD,SAAS,OAAO;AACd,mBAAQ,MAAM,kCAAkC,QAAQ,aAAa,gBAAgB,KAAK,CAAC,EAAE,GACtF;EACT;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,85 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { findProjectRoot } from "../utils/project-paths.js";
3
+ import { getErrorMessage } from "../utils/errors.js";
4
+ let current = { root: process.cwd(), source: "cwd" }, settled = Promise.resolve();
5
+ function currentProjectRoot() {
6
+ return { ...current };
7
+ }
8
+ function whenProjectRootSettled() {
9
+ return settled;
10
+ }
11
+ function markEnvProjectRoot(root) {
12
+ current = { root, source: "env" };
13
+ }
14
+ function resetProjectRoot(root = process.cwd()) {
15
+ current = { root, source: "cwd" }, settled = Promise.resolve();
16
+ }
17
+ function rootUrisToPaths(uris) {
18
+ const paths = [];
19
+ for (const uri of uris)
20
+ if (uri.startsWith("file:"))
21
+ try {
22
+ paths.push(fileURLToPath(uri));
23
+ } catch {
24
+ }
25
+ return paths;
26
+ }
27
+ async function pickProjectRoot(paths, findProject = findProjectRoot) {
28
+ for (const path of paths)
29
+ if (await findProject(path))
30
+ return path;
31
+ return paths[0];
32
+ }
33
+ function followClientRoots(listRoots, options = {}) {
34
+ const chdir = options.chdir ?? ((path) => process.chdir(path)), log = options.log ?? ((message) => process.stderr.write(`[mcp] ${message}
35
+ `)), timeoutMs = options.timeoutMs ?? 5e3, previous = settled;
36
+ return settled = (async () => {
37
+ if (await previous, current.source === "env")
38
+ return;
39
+ let uris;
40
+ try {
41
+ const { roots } = await withTimeout(listRoots(), timeoutMs);
42
+ uris = roots.map((root2) => root2.uri);
43
+ } catch (error) {
44
+ log(`client roots unavailable (${getErrorMessage(error)}); serving from ${current.root}`);
45
+ return;
46
+ }
47
+ const root = await pickProjectRoot(rootUrisToPaths(uris));
48
+ if (!root || root === current.root) {
49
+ root && (current = { root, source: "client-roots" });
50
+ return;
51
+ }
52
+ try {
53
+ chdir(root);
54
+ } catch (error) {
55
+ log(`client root ${root} is not usable (${getErrorMessage(error)}); serving from ${current.root}`);
56
+ return;
57
+ }
58
+ current = { root, source: "client-roots" };
59
+ try {
60
+ await options.onChange?.(root);
61
+ } catch (error) {
62
+ log(`after moving to ${root}: ${getErrorMessage(error)}`);
63
+ }
64
+ })(), settled;
65
+ }
66
+ function withTimeout(promise, ms) {
67
+ return new Promise((resolve, reject) => {
68
+ const timer = setTimeout(() => reject(new Error(`no answer within ${ms}ms`)), ms);
69
+ promise.then((value) => {
70
+ clearTimeout(timer), resolve(value);
71
+ }, (error) => {
72
+ clearTimeout(timer), reject(error);
73
+ });
74
+ });
75
+ }
76
+ export {
77
+ currentProjectRoot,
78
+ followClientRoots,
79
+ markEnvProjectRoot,
80
+ pickProjectRoot,
81
+ resetProjectRoot,
82
+ rootUrisToPaths,
83
+ whenProjectRootSettled
84
+ };
85
+ //# sourceMappingURL=project-root.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/mcp/project-root.ts"],
4
+ "sourcesContent": ["/**\n * Which project the MCP server serves when a tool call doesn't name one with `rootPath`.\n *\n * In order of precedence:\n * 1. DR_MCP_ROOT_PATH, set by a launcher that names the model (DrBot under `dr visualize`).\n * 2. The client's MCP roots: the folder the editor or agent session has open. This lets one\n * server registered per user (a Claude Code plugin) serve whichever project is in front of\n * the developer, whatever directory the client happens to start it in.\n * 3. The server's own working directory.\n *\n * The server moves its working directory to the chosen root, so every tool, and the CLI code\n * under it, finds the project the same way it does from a terminal in that folder.\n */\n\nimport { fileURLToPath } from \"node:url\";\nimport { findProjectRoot } from \"../utils/project-paths.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\n\nexport type ProjectRootSource = \"env\" | \"client-roots\" | \"cwd\";\n\nexport interface ProjectRoot {\n /** Directory the server serves from (its working directory). */\n root: string;\n /** Where that directory came from. */\n source: ProjectRootSource;\n}\n\nlet current: ProjectRoot = { root: process.cwd(), source: \"cwd\" };\nlet settled: Promise<void> = Promise.resolve();\n\n/** The directory the server serves from, and where it came from. */\nexport function currentProjectRoot(): ProjectRoot {\n return { ...current };\n}\n\n/**\n * Resolves once the server knows which project to serve. Tool calls and model loads wait on\n * this so none runs against the wrong folder while the client's roots are still being asked for.\n */\nexport function whenProjectRootSettled(): Promise<void> {\n return settled;\n}\n\n/** Records that a launcher chose the root (DR_MCP_ROOT_PATH); client roots are then ignored. */\nexport function markEnvProjectRoot(root: string): void {\n current = { root, source: \"env\" };\n}\n\n/** Test-only: forget any chosen root and pending resolution. */\nexport function resetProjectRoot(root: string = process.cwd()): void {\n current = { root, source: \"cwd\" };\n settled = Promise.resolve();\n}\n\n/** Local paths for the `file://` root URIs; roots with other schemes are skipped. */\nexport function rootUrisToPaths(uris: readonly string[]): string[] {\n const paths: string[] = [];\n for (const uri of uris) {\n if (!uri.startsWith(\"file:\")) continue;\n try {\n paths.push(fileURLToPath(uri));\n } catch {\n // A malformed file URI can't name a folder; skip it.\n }\n }\n return paths;\n}\n\n/**\n * The root to serve: the first one inside a DR project (a documentation-robotics/ folder at or\n * above it), else the first root, so a project without a model yet is still the one in front\n * of the developer. Undefined when there are no usable roots.\n */\nexport async function pickProjectRoot(\n paths: readonly string[],\n findProject: (path: string) => Promise<string | null> = findProjectRoot\n): Promise<string | undefined> {\n for (const path of paths) {\n if (await findProject(path)) return path;\n }\n return paths[0];\n}\n\nexport interface FollowClientRootsOptions {\n /** Called after the server moved to a different root (drop cached models, warm the new one). */\n onChange?: (root: string) => void | Promise<void>;\n /** Give up on a client that doesn't answer roots/list within this time (default 5s). */\n timeoutMs?: number;\n chdir?: (path: string) => void;\n log?: (message: string) => void;\n}\n\n/**\n * Asks the client for its roots and moves the server to the chosen one. Call it when the\n * client has initialized and again when it says its roots changed. Never throws: a client that\n * fails or doesn't answer leaves the server where it is. A launcher's DR_MCP_ROOT_PATH wins.\n */\nexport function followClientRoots(\n listRoots: () => Promise<{ roots: ReadonlyArray<{ uri: string }> }>,\n options: FollowClientRootsOptions = {}\n): Promise<void> {\n const chdir = options.chdir ?? ((path: string) => process.chdir(path));\n const log = options.log ?? ((message: string) => process.stderr.write(`[mcp] ${message}\\n`));\n const timeoutMs = options.timeoutMs ?? 5000;\n\n const previous = settled;\n // Assigned before any await, so a tool call that arrives next already waits on it.\n settled = (async () => {\n await previous;\n if (current.source === \"env\") return;\n\n let uris: string[];\n try {\n const { roots } = await withTimeout(listRoots(), timeoutMs);\n uris = roots.map((root) => root.uri);\n } catch (error) {\n log(`client roots unavailable (${getErrorMessage(error)}); serving from ${current.root}`);\n return;\n }\n\n const root = await pickProjectRoot(rootUrisToPaths(uris));\n if (!root || root === current.root) {\n if (root) current = { root, source: \"client-roots\" };\n return;\n }\n try {\n chdir(root);\n } catch (error) {\n log(`client root ${root} is not usable (${getErrorMessage(error)}); serving from ${current.root}`);\n return;\n }\n current = { root, source: \"client-roots\" };\n try {\n await options.onChange?.(root);\n } catch (error) {\n log(`after moving to ${root}: ${getErrorMessage(error)}`);\n }\n })();\n return settled;\n}\n\nfunction withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(`no answer within ${ms}ms`)), ms);\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (error) => {\n clearTimeout(timer);\n reject(error);\n }\n );\n });\n}\n"],
5
+ "mappings": "AAcA,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB;AAChC,SAAS,uBAAuB;AAWhC,IAAI,UAAuB,EAAE,MAAM,QAAQ,IAAG,GAAI,QAAQ,MAAK,GAC3D,UAAyB,QAAQ,QAAO;AAGtC,SAAU,qBAAkB;AAChC,SAAO,EAAE,GAAG,QAAO;AACrB;AAMM,SAAU,yBAAsB;AACpC,SAAO;AACT;AAGM,SAAU,mBAAmB,MAAY;AAC7C,YAAU,EAAE,MAAM,QAAQ,MAAK;AACjC;AAGM,SAAU,iBAAiB,OAAe,QAAQ,IAAG,GAAE;AAC3D,YAAU,EAAE,MAAM,QAAQ,MAAK,GAC/B,UAAU,QAAQ,QAAO;AAC3B;AAGM,SAAU,gBAAgB,MAAuB;AACrD,QAAM,QAAkB,CAAA;AACxB,aAAW,OAAO;AAChB,QAAK,IAAI,WAAW,OAAO;AAC3B,UAAI;AACF,cAAM,KAAK,cAAc,GAAG,CAAC;MAC/B,QAAQ;MAER;AAEF,SAAO;AACT;AAOA,eAAsB,gBACpB,OACA,cAAwD,iBAAe;AAEvE,aAAW,QAAQ;AACjB,QAAI,MAAM,YAAY,IAAI;AAAG,aAAO;AAEtC,SAAO,MAAM,CAAC;AAChB;AAgBM,SAAU,kBACd,WACA,UAAoC,CAAA,GAAE;AAEtC,QAAM,QAAQ,QAAQ,UAAU,CAAC,SAAiB,QAAQ,MAAM,IAAI,IAC9D,MAAM,QAAQ,QAAQ,CAAC,YAAoB,QAAQ,OAAO,MAAM,SAAS,OAAO;CAAI,IACpF,YAAY,QAAQ,aAAa,KAEjC,WAAW;AAEjB,oBAAW,YAAW;AAEpB,QADA,MAAM,UACF,QAAQ,WAAW;AAAO;AAE9B,QAAI;AACJ,QAAI;AACF,YAAM,EAAE,MAAK,IAAK,MAAM,YAAY,UAAS,GAAI,SAAS;AAC1D,aAAO,MAAM,IAAI,CAACA,UAASA,MAAK,GAAG;IACrC,SAAS,OAAO;AACd,UAAI,6BAA6B,gBAAgB,KAAK,CAAC,mBAAmB,QAAQ,IAAI,EAAE;AACxF;IACF;AAEA,UAAM,OAAO,MAAM,gBAAgB,gBAAgB,IAAI,CAAC;AACxD,QAAI,CAAC,QAAQ,SAAS,QAAQ,MAAM;AAClC,MAAI,SAAM,UAAU,EAAE,MAAM,QAAQ,eAAc;AAClD;IACF;AACA,QAAI;AACF,YAAM,IAAI;IACZ,SAAS,OAAO;AACd,UAAI,eAAe,IAAI,mBAAmB,gBAAgB,KAAK,CAAC,mBAAmB,QAAQ,IAAI,EAAE;AACjG;IACF;AACA,cAAU,EAAE,MAAM,QAAQ,eAAc;AACxC,QAAI;AACF,YAAM,QAAQ,WAAW,IAAI;IAC/B,SAAS,OAAO;AACd,UAAI,mBAAmB,IAAI,KAAK,gBAAgB,KAAK,CAAC,EAAE;IAC1D;EACF,GAAE,GACK;AACT;AAEA,SAAS,YAAe,SAAqB,IAAU;AACrD,SAAO,IAAI,QAAW,CAAC,SAAS,WAAU;AACxC,UAAM,QAAQ,WAAW,MAAM,OAAO,IAAI,MAAM,oBAAoB,EAAE,IAAI,CAAC,GAAG,EAAE;AAChF,YAAQ,KACN,CAAC,UAAS;AACR,mBAAa,KAAK,GAClB,QAAQ,KAAK;IACf,GACA,CAAC,UAAS;AACR,mBAAa,KAAK,GAClB,OAAO,KAAK;IACd,CAAC;EAEL,CAAC;AACH;",
6
+ "names": ["root"]
7
+ }
@@ -0,0 +1,27 @@
1
+ const SERVER_INSTRUCTIONS = `Documentation Robotics (DR) keeps an architecture model of the project in documentation-robotics/: layers from motivation through application, API, data and testing, linked by typed relationships. The base model describes the code as built. A planned change lives in a changeset until it lands with the code. Do each step with these tools; never tell the user to run dr for something a tool does.
2
+
3
+ Start with project_status: it reports where the project stands and ends with nextStep: the tool to call next (and nextStep.also alongside it). Call it again after a step. Pass rootPath only for another project.
4
+
5
+ Confirm with the user, and wait, before you create: the model's name (project_init); whether to bootstrap the model from the code now or grow it with each change; a changeset's name and intent (changeset_create). Propose each, don't decide it.
6
+
7
+ Set up: if the model's spec is behind, project_upgrade previews the migration; show the user and call it with apply: true only when they approve.
8
+
9
+ Read the model: model_search, model_show, model_trace, model_list. spec_type gives a type's attributes and the relationships the catalog allows. Element ids are {layer}.{type}.{kebab-name}.
10
+
11
+ Plan a change (the /dr-plan workflow):
12
+ 1. changeset_create opens a changeset and makes it active. While one is active, model and relationship writes stage into it, and the reads show the model as the plan leaves it. Wire several at once: model_add with elements: [...] (and relationships among them), relationship_add with relationships: [...]; bad items are skipped and reported.
13
+ 2. Check each type with spec_type, and link every new element to what it serves, realizes or depends on.
14
+ 3. Pressure-test with changeset_brief (it counts unwired links; list them with sections: ["unwired"], unwiredLayers: [...], never unfiltered) and changeset_impact. Revise with changeset_update and changeset_unstage (changes: [...] for several). changeset_deactivate sets a plan aside; changeset_activate resumes it.
15
+ 4. Review: give the user review_open's url (retry if the viewer is still starting). They comment there; read with annotation_list, answer with annotation_reply.
16
+ 5. Build from changeset_brief with sections: ["changes"] (large plan: layers: ["<layer>"], one at a time). Don't extract or verify during the build.
17
+ 6. Verify as its own step, once the build's code is committed to git: codebase_extract with since (the base branch) stages what was built into an as-built changeset, never written by hand or copied from the plan; changeset_verify compares the plan with it. Its unverifiable items and notes need no plan edit. The as-built changeset is left active: changeset_activate the plan (previousActive) before editing it. Then the plan lands; the as-built changeset is discarded or kept as a record, not landed.
18
+
19
+ Code: codebase_index indexes in the background. Call it with wait: 30, then with status: true and wait: 30 until job.state is done (or failed: report its error and logPath). If it reports ignoreFile, tell the user to commit it. codebase_verify checks only the API layer; on code with no routes its findings are expected, not drift.
20
+
21
+ Model the code: codebase_extract stages what the rules decide into an as-built changeset and leaves it active. If it staged nothing it makes no changeset: call changeset_create with the name it gives before model_add. Stage its element cards with model_add (sourceFile and sourceSymbol from the card, sourceProvenance: "extracted"), then call it again for their relationships. Ask the user about cards you can't place, in one short list. For a bootstrap, call it without since.
22
+
23
+ Land a change with changeset_commit only when the user asks to land it; they confirm it in a dialog. It is refused if they decline, if the client can't show the dialog, or on drift unless they agree to force: true. If it is refused, they land it in the viewer (review_open, then Apply) or an interactive session, never the dr CLI. Never commit on your own initiative. changeset_discard deletes staged work for good; use it only when the user asks.`;
24
+ export {
25
+ SERVER_INSTRUCTIONS
26
+ };
27
+ //# sourceMappingURL=server-instructions.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/mcp/server-instructions.ts"],
4
+ "sourcesContent": ["/**\n * Instructions the dr MCP server sends in its initialize result. Clients such as Claude Code\n * load them into the session at start (alongside only the tool names), so this is the agent's\n * map of how the tools fit together. Claude Code caps it at 4096 characters; a test holds it\n * under that and checks that every tool named here exists.\n */\nexport const SERVER_INSTRUCTIONS = `Documentation Robotics (DR) keeps an architecture model of the project in documentation-robotics/: layers from motivation through application, API, data and testing, linked by typed relationships. The base model describes the code as built. A planned change lives in a changeset until it lands with the code. Do each step with these tools; never tell the user to run dr for something a tool does.\n\nStart with project_status: it reports where the project stands and ends with nextStep: the tool to call next (and nextStep.also alongside it). Call it again after a step. Pass rootPath only for another project.\n\nConfirm with the user, and wait, before you create: the model's name (project_init); whether to bootstrap the model from the code now or grow it with each change; a changeset's name and intent (changeset_create). Propose each, don't decide it.\n\nSet up: if the model's spec is behind, project_upgrade previews the migration; show the user and call it with apply: true only when they approve.\n\nRead the model: model_search, model_show, model_trace, model_list. spec_type gives a type's attributes and the relationships the catalog allows. Element ids are {layer}.{type}.{kebab-name}.\n\nPlan a change (the /dr-plan workflow):\n1. changeset_create opens a changeset and makes it active. While one is active, model and relationship writes stage into it, and the reads show the model as the plan leaves it. Wire several at once: model_add with elements: [...] (and relationships among them), relationship_add with relationships: [...]; bad items are skipped and reported.\n2. Check each type with spec_type, and link every new element to what it serves, realizes or depends on.\n3. Pressure-test with changeset_brief (it counts unwired links; list them with sections: [\"unwired\"], unwiredLayers: [...], never unfiltered) and changeset_impact. Revise with changeset_update and changeset_unstage (changes: [...] for several). changeset_deactivate sets a plan aside; changeset_activate resumes it.\n4. Review: give the user review_open's url (retry if the viewer is still starting). They comment there; read with annotation_list, answer with annotation_reply.\n5. Build from changeset_brief with sections: [\"changes\"] (large plan: layers: [\"<layer>\"], one at a time). Don't extract or verify during the build.\n6. Verify as its own step, once the build's code is committed to git: codebase_extract with since (the base branch) stages what was built into an as-built changeset, never written by hand or copied from the plan; changeset_verify compares the plan with it. Its unverifiable items and notes need no plan edit. The as-built changeset is left active: changeset_activate the plan (previousActive) before editing it. Then the plan lands; the as-built changeset is discarded or kept as a record, not landed.\n\nCode: codebase_index indexes in the background. Call it with wait: 30, then with status: true and wait: 30 until job.state is done (or failed: report its error and logPath). If it reports ignoreFile, tell the user to commit it. codebase_verify checks only the API layer; on code with no routes its findings are expected, not drift.\n\nModel the code: codebase_extract stages what the rules decide into an as-built changeset and leaves it active. If it staged nothing it makes no changeset: call changeset_create with the name it gives before model_add. Stage its element cards with model_add (sourceFile and sourceSymbol from the card, sourceProvenance: \"extracted\"), then call it again for their relationships. Ask the user about cards you can't place, in one short list. For a bootstrap, call it without since.\n\nLand a change with changeset_commit only when the user asks to land it; they confirm it in a dialog. It is refused if they decline, if the client can't show the dialog, or on drift unless they agree to force: true. If it is refused, they land it in the viewer (review_open, then Apply) or an interactive session, never the dr CLI. Never commit on your own initiative. changeset_discard deletes staged work for good; use it only when the user asks.`;\n"],
5
+ "mappings": "AAMO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;",
6
+ "names": []
7
+ }
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ function editDistance(a, b) {
3
+ const row = Array.from({ length: b.length + 1 }, (_, j) => j);
4
+ for (let i = 1; i <= a.length; i++) {
5
+ let diagonal = row[0];
6
+ row[0] = i;
7
+ for (let j = 1; j <= b.length; j++) {
8
+ const above = row[j];
9
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1)), diagonal = above;
10
+ }
11
+ }
12
+ return row[b.length];
13
+ }
14
+ function meantName(key, accepted) {
15
+ const lower = key.toLowerCase(), exact = accepted.find((name) => name.toLowerCase() === `${lower}id` || `${name.toLowerCase()}id` === lower || name.toLowerCase() === lower);
16
+ if (exact)
17
+ return exact;
18
+ const truncated = accepted.find((name) => {
19
+ const n = name.toLowerCase();
20
+ return Math.min(n.length, lower.length) >= 4 && (n.startsWith(lower) || lower.startsWith(n));
21
+ });
22
+ if (truncated)
23
+ return truncated;
24
+ const allowed = Math.min(2, Math.floor(key.length / 4));
25
+ let best;
26
+ for (const name of accepted) {
27
+ const distance = editDistance(lower, name.toLowerCase());
28
+ distance <= allowed && (!best || distance < best.distance) && (best = { name, distance });
29
+ }
30
+ return best?.name;
31
+ }
32
+ function hintFor(accepted, unknown) {
33
+ const hints = unknown.map((key) => {
34
+ const meant = meantName(key, accepted);
35
+ return meant ? `'${meant}' for '${key}'` : void 0;
36
+ }).filter((hint) => hint !== void 0);
37
+ return hints.length > 0 ? ` (did you mean ${hints.join(", ")}?)` : "";
38
+ }
39
+ function unknownArgumentsMessage(toolName, accepted, unknown) {
40
+ return `${toolName} doesn't take ${unknown.map((key) => `'${key}'`).join(", ")}${hintFor(accepted, unknown)}. It takes: ${accepted.join(", ")}.`;
41
+ }
42
+ function strictItem(what, shape) {
43
+ const accepted = Object.keys(shape);
44
+ return z.strictObject(shape, {
45
+ error: (issue) => issue.code === "unrecognized_keys" ? `${what} doesn't take ${issue.keys.map((key) => `'${key}'`).join(", ")}${hintFor(accepted, issue.keys)}. It takes: ${accepted.join(", ")}.` : void 0
46
+ });
47
+ }
48
+ export {
49
+ strictItem,
50
+ unknownArgumentsMessage
51
+ };
52
+ //# sourceMappingURL=strict-schema.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/mcp/strict-schema.ts"],
4
+ "sourcesContent": ["/**\n * Strict argument schemas for the MCP tools. A tool refuses an argument it doesn't take, at the\n * top level and inside the objects its arrays carry: the SDK would strip an unknown key, so a\n * misnamed argument (`changeset` for `changesetId`, `sourceSymbl` for `sourceSymbol`) silently\n * changed what the call did.\n */\n\nimport { z, type ZodRawShape } from \"zod\";\n\n/** Edit distance between two short strings (insertions, deletions, substitutions). */\nfunction editDistance(a: string, b: string): number {\n const row = Array.from({ length: b.length + 1 }, (_, j) => j);\n for (let i = 1; i <= a.length; i++) {\n let diagonal = row[0];\n row[0] = i;\n for (let j = 1; j <= b.length; j++) {\n const above = row[j];\n row[j] = Math.min(row[j] + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));\n diagonal = above;\n }\n }\n return row[b.length];\n}\n\n/**\n * The accepted name an unknown key most likely meant: the same name with or without an `Id`\n * suffix, in any case; one cut short or run on (`extract`); or one a couple of letters away\n * (`sourceSymbl`).\n */\nfunction meantName(key: string, accepted: string[]): string | undefined {\n const lower = key.toLowerCase();\n const exact = accepted.find(\n (name) => name.toLowerCase() === `${lower}id` || `${name.toLowerCase()}id` === lower || name.toLowerCase() === lower\n );\n if (exact) return exact;\n // A name cut short or run on (`extract` for `extracted`, `sections` for `section`).\n const truncated = accepted.find((name) => {\n const n = name.toLowerCase();\n return Math.min(n.length, lower.length) >= 4 && (n.startsWith(lower) || lower.startsWith(n));\n });\n if (truncated) return truncated;\n const allowed = Math.min(2, Math.floor(key.length / 4));\n let best: { name: string; distance: number } | undefined;\n for (const name of accepted) {\n const distance = editDistance(lower, name.toLowerCase());\n if (distance <= allowed && (!best || distance < best.distance)) best = { name, distance };\n }\n return best?.name;\n}\n\n/** \" (did you mean 'x' for 'y'?)\", or \"\" when no key has a likely match. */\nfunction hintFor(accepted: string[], unknown: string[]): string {\n const hints = unknown\n .map((key) => {\n const meant = meantName(key, accepted);\n return meant ? `'${meant}' for '${key}'` : undefined;\n })\n .filter((hint): hint is string => hint !== undefined);\n return hints.length > 0 ? ` (did you mean ${hints.join(\", \")}?)` : \"\";\n}\n\n/** The message for arguments a tool doesn't take, with the argument that was probably meant. */\nexport function unknownArgumentsMessage(toolName: string, accepted: string[], unknown: string[]): string {\n return `${toolName} doesn't take ${unknown.map((key) => `'${key}'`).join(\", \")}${hintFor(accepted, unknown)}. It takes: ${accepted.join(\", \")}.`;\n}\n\n/**\n * A strict object inside a tool's arguments (an `elements[]` item, `against`): a key it doesn't\n * take is refused, naming where it was (`what`), the likely match, and the keys it does take.\n */\nexport function strictItem<T extends ZodRawShape>(what: string, shape: T) {\n const accepted = Object.keys(shape);\n return z.strictObject(shape, {\n error: (issue) =>\n issue.code === \"unrecognized_keys\"\n ? `${what} doesn't take ${issue.keys.map((key) => `'${key}'`).join(\", \")}${hintFor(accepted, issue.keys)}. It takes: ${accepted.join(\", \")}.`\n : undefined,\n });\n}\n"],
5
+ "mappings": "AAOA,SAAS,SAA2B;AAGpC,SAAS,aAAa,GAAW,GAAS;AACxC,QAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAC,GAAI,CAAC,GAAG,MAAM,CAAC;AAC5D,WAAS,IAAI,GAAG,KAAK,EAAE,QAAQ,KAAK;AAClC,QAAI,WAAW,IAAI,CAAC;AACpB,QAAI,CAAC,IAAI;AACT,aAAS,IAAI,GAAG,KAAK,EAAE,QAAQ,KAAK;AAClC,YAAM,QAAQ,IAAI,CAAC;AACnB,UAAI,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,GAAG,YAAY,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE,GACxF,WAAW;IACb;EACF;AACA,SAAO,IAAI,EAAE,MAAM;AACrB;AAOA,SAAS,UAAU,KAAa,UAAkB;AAChD,QAAM,QAAQ,IAAI,YAAW,GACvB,QAAQ,SAAS,KACrB,CAAC,SAAS,KAAK,YAAW,MAAO,GAAG,KAAK,QAAQ,GAAG,KAAK,YAAW,CAAE,SAAS,SAAS,KAAK,YAAW,MAAO,KAAK;AAEtH,MAAI;AAAO,WAAO;AAElB,QAAM,YAAY,SAAS,KAAK,CAAC,SAAQ;AACvC,UAAM,IAAI,KAAK,YAAW;AAC1B,WAAO,KAAK,IAAI,EAAE,QAAQ,MAAM,MAAM,KAAK,MAAM,EAAE,WAAW,KAAK,KAAK,MAAM,WAAW,CAAC;EAC5F,CAAC;AACD,MAAI;AAAW,WAAO;AACtB,QAAM,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,SAAS,CAAC,CAAC;AACtD,MAAI;AACJ,aAAW,QAAQ,UAAU;AAC3B,UAAM,WAAW,aAAa,OAAO,KAAK,YAAW,CAAE;AACvD,IAAI,YAAY,YAAY,CAAC,QAAQ,WAAW,KAAK,cAAW,OAAO,EAAE,MAAM,SAAQ;EACzF;AACA,SAAO,MAAM;AACf;AAGA,SAAS,QAAQ,UAAoB,SAAiB;AACpD,QAAM,QAAQ,QACX,IAAI,CAAC,QAAO;AACX,UAAM,QAAQ,UAAU,KAAK,QAAQ;AACrC,WAAO,QAAQ,IAAI,KAAK,UAAU,GAAG,MAAM;EAC7C,CAAC,EACA,OAAO,CAAC,SAAyB,SAAS,MAAS;AACtD,SAAO,MAAM,SAAS,IAAI,kBAAkB,MAAM,KAAK,IAAI,CAAC,OAAO;AACrE;AAGM,SAAU,wBAAwB,UAAkB,UAAoB,SAAiB;AAC7F,SAAO,GAAG,QAAQ,iBAAiB,QAAQ,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,EAAE,KAAK,IAAI,CAAC,GAAG,QAAQ,UAAU,OAAO,CAAC,eAAe,SAAS,KAAK,IAAI,CAAC;AAC/I;AAMM,SAAU,WAAkC,MAAc,OAAQ;AACtE,QAAM,WAAW,OAAO,KAAK,KAAK;AAClC,SAAO,EAAE,aAAa,OAAO;IAC3B,OAAO,CAAC,UACN,MAAM,SAAS,sBACX,GAAG,IAAI,iBAAiB,MAAM,KAAK,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,EAAE,KAAK,IAAI,CAAC,GAAG,QAAQ,UAAU,MAAM,IAAI,CAAC,eAAe,SAAS,KAAK,IAAI,CAAC,MACxI;GACP;AACH;",
6
+ "names": []
7
+ }
@@ -1,9 +1,18 @@
1
+ import { z } from "zod";
1
2
  import { isTelemetryEnabled, startActiveSpan } from "../telemetry/index.js";
3
+ import { unknownArgumentsMessage } from "./strict-schema.js";
2
4
  import { annotationCreateTool, annotationDeleteTool, annotationGetTool, annotationListTool, annotationReplyTool, annotationUpdateTool, changesetActivateTool, changesetBriefTool, changesetCreateTool, changesetDeactivateTool, changesetImpactTool, changesetListTool, changesetShowTool, chatLaunchTool, chatStatusTool, modelAddTool, modelDeleteTool, modelExportTool, modelInfoTool, modelListTool, modelReloadTool, modelSearchTool, modelShowTool, modelStatsTool, modelTraceTool, modelUpdateTool, modelValidateTool, relationshipAddTool, relationshipDeleteTool, specTypeTool } from "./tools/index.js";
3
5
  import { changesetDiscardTool, changesetUnstageTool, changesetUpdateTool } from "./tools/index.js";
4
6
  import { changesetVerifyTool } from "./tools/index.js";
5
- import { errorResult, loadModel } from "./tools/shared.js";
7
+ import { projectInitTool, projectUpgradeTool } from "./tools/index.js";
8
+ import { codebaseIndexTool, codebaseVerifyTool } from "./tools/index.js";
9
+ import { changesetCommitTool } from "./tools/index.js";
10
+ import { reviewOpenTool } from "./tools/index.js";
11
+ import { projectStatusTool } from "./tools/index.js";
12
+ import { codebaseExtractTool } from "./tools/index.js";
13
+ import { errorResult, loadModel, rootPathSchema } from "./tools/shared.js";
6
14
  import { changesetOnlyRefusal } from "./changeset-only.js";
15
+ import { whenProjectRootSettled } from "./project-root.js";
7
16
  const MODEL_TOOLS = [
8
17
  modelListTool,
9
18
  modelShowTool,
@@ -38,14 +47,36 @@ const MODEL_TOOLS = [
38
47
  changesetUpdateTool,
39
48
  changesetUnstageTool,
40
49
  changesetDiscardTool,
41
- changesetVerifyTool
50
+ changesetVerifyTool,
51
+ projectInitTool,
52
+ projectUpgradeTool,
53
+ codebaseIndexTool,
54
+ codebaseVerifyTool,
55
+ changesetCommitTool,
56
+ reviewOpenTool,
57
+ projectStatusTool,
58
+ codebaseExtractTool
42
59
  ];
60
+ function registeredToolNames() {
61
+ return MODEL_TOOLS.map((tool) => tool.name);
62
+ }
63
+ function registeredInputSchema(toolName, shape) {
64
+ const full = "rootPath" in shape ? shape : { ...shape, rootPath: rootPathSchema }, accepted = Object.keys(full);
65
+ return z.strictObject(full, {
66
+ error: (issue) => issue.code === "unrecognized_keys" ? unknownArgumentsMessage(toolName, accepted, issue.keys) : void 0
67
+ });
68
+ }
43
69
  class McpToolRegistry {
44
70
  /** Registers every model tool onto the given MCP server instance. */
45
71
  registerAll(server) {
46
72
  for (const tool of MODEL_TOOLS)
47
- server.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, (args) => startActiveSpan(`mcp.tool.${tool.name}`, async (span) => {
48
- const refusal = await changesetOnlyRefusal(tool.name, args.rootPath, loadModel), result = refusal ? errorResult(refusal) : await tool.handler(args);
73
+ server.registerTool(tool.name, {
74
+ description: tool.description,
75
+ inputSchema: registeredInputSchema(tool.name, tool.inputSchema),
76
+ ...tool._meta ? { _meta: tool._meta } : {}
77
+ }, (args, extra) => startActiveSpan(`mcp.tool.${tool.name}`, async (span) => {
78
+ await whenProjectRootSettled();
79
+ const refusal = await changesetOnlyRefusal(tool.name, args.rootPath, loadModel), result = refusal ? errorResult(refusal) : await tool.handler(args, { server: server.server, requestId: extra?.requestId, signal: extra?.signal });
49
80
  if (isTelemetryEnabled && result?.isError) {
50
81
  const firstContent = result.content?.[0], message = firstContent?.type === "text" ? firstContent.text : "Tool returned an error";
51
82
  span.setStatus({ code: 2, message });
@@ -55,6 +86,9 @@ class McpToolRegistry {
55
86
  }
56
87
  }
57
88
  export {
58
- McpToolRegistry
89
+ McpToolRegistry,
90
+ registeredInputSchema,
91
+ registeredToolNames,
92
+ unknownArgumentsMessage
59
93
  };
60
94
  //# sourceMappingURL=tool-registry.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/mcp/tool-registry.ts"],
4
- "sourcesContent": ["/**\n * McpToolRegistry \u2014 registers the DR model tool surface onto an MCP server.\n *\n * Read-only tools: model_list, model_show, model_search, model_stats,\n * model_info, model_trace.\n * Mutation tools: model_add, model_update, model_delete (via MutationHandler).\n * Pipeline tools: model_validate, model_export (via the shared validator\n * pipeline and export handlers).\n * Lifecycle tools: model_reload (force a full re-read from disk).\n * Changeset tools: changeset_list, changeset_show (via StagingAreaManager).\n * Change-modeling tools (docs/CHANGE_MODELING.md): changeset_create, changeset_activate,\n * changeset_deactivate, relationship_add, relationship_delete, spec_type, changeset_impact,\n * changeset_brief, changeset_verify. Commit and apply are deliberately not exposed: landing a change\n * is the human's step.\n * Changeset editing (changeset-editing.ts): changeset_update, changeset_unstage, and\n * changeset_discard (requires confirm: true; it deletes the staged changes).\n * Chat tools: chat_status, chat_launch (via the coding-agents chat clients).\n * Annotation tools: annotation_list, annotation_get, annotation_create,\n * annotation_update, annotation_delete, annotation_reply (via AnnotationStore),\n * mirroring the REST server's /api/annotations/* endpoints.\n */\n\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { isTelemetryEnabled, startActiveSpan } from \"../telemetry/index.js\";\nimport {\n annotationCreateTool,\n annotationDeleteTool,\n annotationGetTool,\n annotationListTool,\n annotationReplyTool,\n annotationUpdateTool,\n changesetActivateTool,\n changesetBriefTool,\n changesetCreateTool,\n changesetDeactivateTool,\n changesetImpactTool,\n changesetListTool,\n changesetShowTool,\n chatLaunchTool,\n chatStatusTool,\n modelAddTool,\n modelDeleteTool,\n modelExportTool,\n modelInfoTool,\n modelListTool,\n modelReloadTool,\n modelSearchTool,\n modelShowTool,\n modelStatsTool,\n modelTraceTool,\n modelUpdateTool,\n modelValidateTool,\n relationshipAddTool,\n relationshipDeleteTool,\n specTypeTool,\n type McpToolDefinition,\n} from \"./tools/index.js\";\nimport { changesetDiscardTool, changesetUnstageTool, changesetUpdateTool } from \"./tools/index.js\";\nimport { changesetVerifyTool } from \"./tools/index.js\";\nimport { errorResult, loadModel } from \"./tools/shared.js\";\nimport { changesetOnlyRefusal } from \"./changeset-only.js\";\n\nconst MODEL_TOOLS: McpToolDefinition[] = [\n modelListTool,\n modelShowTool,\n modelSearchTool,\n modelStatsTool,\n modelInfoTool,\n modelTraceTool,\n modelAddTool,\n modelUpdateTool,\n modelDeleteTool,\n modelValidateTool,\n modelExportTool,\n modelReloadTool,\n changesetListTool,\n changesetShowTool,\n changesetCreateTool,\n changesetActivateTool,\n changesetDeactivateTool,\n relationshipAddTool,\n relationshipDeleteTool,\n specTypeTool,\n changesetImpactTool,\n changesetBriefTool,\n chatStatusTool,\n chatLaunchTool,\n annotationListTool,\n annotationGetTool,\n annotationCreateTool,\n annotationUpdateTool,\n annotationDeleteTool,\n annotationReplyTool,\n changesetUpdateTool,\n changesetUnstageTool,\n changesetDiscardTool,\n changesetVerifyTool,\n];\n\nexport class McpToolRegistry {\n /** Registers every model tool onto the given MCP server instance. */\n registerAll(server: McpServer): void {\n for (const tool of MODEL_TOOLS) {\n server.registerTool(\n tool.name,\n { description: tool.description, inputSchema: tool.inputSchema },\n (args: Parameters<typeof tool.handler>[0]) =>\n startActiveSpan(\n `mcp.tool.${tool.name}`,\n async (span) => {\n // Changeset-only mode (DR_CHANGESET_ONLY): refuse base-model writes up front.\n const refusal = await changesetOnlyRefusal(tool.name, (args as { rootPath?: string }).rootPath, loadModel);\n const result = refusal ? errorResult(refusal) : await tool.handler(args);\n if (isTelemetryEnabled && result?.isError) {\n const firstContent = result.content?.[0];\n const message =\n firstContent?.type === \"text\" ? firstContent.text : \"Tool returned an error\";\n span.setStatus({ code: 2, message });\n }\n return result;\n },\n { \"mcp.tool.name\": tool.name }\n )\n );\n }\n }\n}\n"],
5
- "mappings": "AAuBA,SAAS,oBAAoB,uBAAuB;AACpD,SACE,sBACA,sBACA,mBACA,oBACA,qBACA,sBACA,uBACA,oBACA,qBACA,yBACA,qBACA,mBACA,mBACA,gBACA,gBACA,cACA,iBACA,iBACA,eACA,eACA,iBACA,iBACA,eACA,gBACA,gBACA,iBACA,mBACA,qBACA,wBACA,oBAEK;AACP,SAAS,sBAAsB,sBAAsB,2BAA2B;AAChF,SAAS,2BAA2B;AACpC,SAAS,aAAa,iBAAiB;AACvC,SAAS,4BAA4B;AAErC,MAAM,cAAmC;EACvC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI,MAAO,gBAAe;;EAE1B,YAAY,QAAiB;AAC3B,eAAW,QAAQ;AACjB,aAAO,aACL,KAAK,MACL,EAAE,aAAa,KAAK,aAAa,aAAa,KAAK,YAAW,GAC9D,CAAC,SACC,gBACE,YAAY,KAAK,IAAI,IACrB,OAAO,SAAQ;AAEb,cAAM,UAAU,MAAM,qBAAqB,KAAK,MAAO,KAA+B,UAAU,SAAS,GACnG,SAAS,UAAU,YAAY,OAAO,IAAI,MAAM,KAAK,QAAQ,IAAI;AACvE,YAAI,sBAAsB,QAAQ,SAAS;AACzC,gBAAM,eAAe,OAAO,UAAU,CAAC,GACjC,UACJ,cAAc,SAAS,SAAS,aAAa,OAAO;AACtD,eAAK,UAAU,EAAE,MAAM,GAAG,QAAO,CAAE;QACrC;AACA,eAAO;MACT,GACA,EAAE,iBAAiB,KAAK,KAAI,CAAE,CAC/B;EAGT;;",
4
+ "sourcesContent": ["/**\n * McpToolRegistry \u2014 registers the DR model tool surface onto an MCP server.\n *\n * Read-only tools: model_list, model_show, model_search, model_stats,\n * model_info, model_trace.\n * Mutation tools: model_add, model_update, model_delete (via MutationHandler).\n * Pipeline tools: model_validate, model_export (via the shared validator\n * pipeline and export handlers).\n * Lifecycle tools: model_reload (force a full re-read from disk).\n * Changeset tools: changeset_list, changeset_show (via StagingAreaManager).\n * Change-modeling tools (docs/CHANGE_MODELING.md): changeset_create, changeset_activate,\n * changeset_deactivate, relationship_add, relationship_delete, spec_type, changeset_impact,\n * changeset_brief, changeset_verify. Apply is not exposed.\n * Landing (changeset-commit.ts): changeset_commit commits a staged changeset only after a person\n * confirms it in an elicitation dialog. It is listed with _meta \"anthropic/requiresUserInteraction\",\n * is refused in changeset-only mode, and is never on DrBot's allowlists.\n * Changeset editing (changeset-editing.ts): changeset_update, changeset_unstage, and\n * changeset_discard (requires confirm: true; it deletes the staged changes).\n * Chat tools: chat_status, chat_launch (via the coding-agents chat clients).\n * Codebase tools: codebase_index (starts a background index and returns at once; poll with\n * status: true), codebase_verify (the code's routes against the model's API). Neither writes the model.\n * codebase_extract stages what the extraction cascade finds into an as-built changeset (never the base).\n * Annotation tools: annotation_list, annotation_get, annotation_create,\n * annotation_update, annotation_delete, annotation_reply (via AnnotationStore),\n * mirroring the REST server's /api/annotations/* endpoints.\n * Project tools (project-tools.ts): project_init (create a model, no prompts) and\n * project_upgrade (preview a spec migration; apply it with apply: true).\n * Review tools: review_open (a viewer link to a changeset; reuses or starts `dr visualize`).\n * Project tools: project_status (where the repo stands with DR, and the next step).\n */\n\nimport { z, type ZodRawShape } from \"zod\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { RequestId } from \"@modelcontextprotocol/sdk/types.js\";\nimport { isTelemetryEnabled, startActiveSpan } from \"../telemetry/index.js\";\nimport { unknownArgumentsMessage } from \"./strict-schema.js\";\nimport {\n annotationCreateTool,\n annotationDeleteTool,\n annotationGetTool,\n annotationListTool,\n annotationReplyTool,\n annotationUpdateTool,\n changesetActivateTool,\n changesetBriefTool,\n changesetCreateTool,\n changesetDeactivateTool,\n changesetImpactTool,\n changesetListTool,\n changesetShowTool,\n chatLaunchTool,\n chatStatusTool,\n modelAddTool,\n modelDeleteTool,\n modelExportTool,\n modelInfoTool,\n modelListTool,\n modelReloadTool,\n modelSearchTool,\n modelShowTool,\n modelStatsTool,\n modelTraceTool,\n modelUpdateTool,\n modelValidateTool,\n relationshipAddTool,\n relationshipDeleteTool,\n specTypeTool,\n type McpToolDefinition,\n} from \"./tools/index.js\";\nimport { changesetDiscardTool, changesetUnstageTool, changesetUpdateTool } from \"./tools/index.js\";\nimport { changesetVerifyTool } from \"./tools/index.js\";\nimport { projectInitTool, projectUpgradeTool } from \"./tools/index.js\";\nimport { codebaseIndexTool, codebaseVerifyTool } from \"./tools/index.js\";\nimport { changesetCommitTool } from \"./tools/index.js\";\nimport { reviewOpenTool } from \"./tools/index.js\";\nimport { projectStatusTool } from \"./tools/index.js\";\nimport { codebaseExtractTool } from \"./tools/index.js\";\nimport { errorResult, loadModel, rootPathSchema } from \"./tools/shared.js\";\nimport { changesetOnlyRefusal } from \"./changeset-only.js\";\nimport { whenProjectRootSettled } from \"./project-root.js\";\n\nconst MODEL_TOOLS: McpToolDefinition[] = [\n modelListTool,\n modelShowTool,\n modelSearchTool,\n modelStatsTool,\n modelInfoTool,\n modelTraceTool,\n modelAddTool,\n modelUpdateTool,\n modelDeleteTool,\n modelValidateTool,\n modelExportTool,\n modelReloadTool,\n changesetListTool,\n changesetShowTool,\n changesetCreateTool,\n changesetActivateTool,\n changesetDeactivateTool,\n relationshipAddTool,\n relationshipDeleteTool,\n specTypeTool,\n changesetImpactTool,\n changesetBriefTool,\n chatStatusTool,\n chatLaunchTool,\n annotationListTool,\n annotationGetTool,\n annotationCreateTool,\n annotationUpdateTool,\n annotationDeleteTool,\n annotationReplyTool,\n changesetUpdateTool,\n changesetUnstageTool,\n changesetDiscardTool,\n changesetVerifyTool,\n projectInitTool,\n projectUpgradeTool,\n codebaseIndexTool,\n codebaseVerifyTool,\n changesetCommitTool,\n reviewOpenTool,\n projectStatusTool,\n codebaseExtractTool,\n];\n\n/** Names of every tool the registry registers. */\nexport function registeredToolNames(): string[] {\n return MODEL_TOOLS.map((tool) => tool.name);\n}\n\nexport { unknownArgumentsMessage };\n\n/**\n * The schema a tool is registered with: its arguments plus the optional rootPath every tool takes,\n * strict, so tools/list says no other argument is accepted and a call with one is refused with\n * unknownArgumentsMessage.\n */\nexport function registeredInputSchema(toolName: string, shape: ZodRawShape) {\n const full: ZodRawShape = \"rootPath\" in shape ? shape : { ...shape, rootPath: rootPathSchema };\n const accepted = Object.keys(full);\n return z.strictObject(full, {\n error: (issue) =>\n issue.code === \"unrecognized_keys\" ? unknownArgumentsMessage(toolName, accepted, issue.keys) : undefined,\n });\n}\n\nexport class McpToolRegistry {\n /** Registers every model tool onto the given MCP server instance. */\n registerAll(server: McpServer): void {\n for (const tool of MODEL_TOOLS) {\n server.registerTool(\n tool.name,\n {\n description: tool.description,\n inputSchema: registeredInputSchema(tool.name, tool.inputSchema),\n ...(tool._meta ? { _meta: tool._meta } : {}),\n },\n (args: Parameters<typeof tool.handler>[0], extra?: { requestId?: RequestId; signal?: AbortSignal }) =>\n startActiveSpan(\n `mcp.tool.${tool.name}`,\n async (span) => {\n // Tools that don't load the model still read the project from the working\n // directory, so wait until the client's roots have settled which project that is.\n await whenProjectRootSettled();\n // Changeset-only mode (DR_CHANGESET_ONLY): refuse base-model writes up front.\n const refusal = await changesetOnlyRefusal(tool.name, (args as { rootPath?: string }).rootPath, loadModel);\n const result = refusal\n ? errorResult(refusal)\n : await tool.handler(args, { server: server.server, requestId: extra?.requestId, signal: extra?.signal });\n if (isTelemetryEnabled && result?.isError) {\n const firstContent = result.content?.[0];\n const message =\n firstContent?.type === \"text\" ? firstContent.text : \"Tool returned an error\";\n span.setStatus({ code: 2, message });\n }\n return result;\n },\n { \"mcp.tool.name\": tool.name }\n )\n );\n }\n }\n}\n"],
5
+ "mappings": "AA+BA,SAAS,SAA2B;AAGpC,SAAS,oBAAoB,uBAAuB;AACpD,SAAS,+BAA+B;AACxC,SACE,sBACA,sBACA,mBACA,oBACA,qBACA,sBACA,uBACA,oBACA,qBACA,yBACA,qBACA,mBACA,mBACA,gBACA,gBACA,cACA,iBACA,iBACA,eACA,eACA,iBACA,iBACA,eACA,gBACA,gBACA,iBACA,mBACA,qBACA,wBACA,oBAEK;AACP,SAAS,sBAAsB,sBAAsB,2BAA2B;AAChF,SAAS,2BAA2B;AACpC,SAAS,iBAAiB,0BAA0B;AACpD,SAAS,mBAAmB,0BAA0B;AACtD,SAAS,2BAA2B;AACpC,SAAS,sBAAsB;AAC/B,SAAS,yBAAyB;AAClC,SAAS,2BAA2B;AACpC,SAAS,aAAa,WAAW,sBAAsB;AACvD,SAAS,4BAA4B;AACrC,SAAS,8BAA8B;AAEvC,MAAM,cAAmC;EACvC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAII,SAAU,sBAAmB;AACjC,SAAO,YAAY,IAAI,CAAC,SAAS,KAAK,IAAI;AAC5C;AASM,SAAU,sBAAsB,UAAkB,OAAkB;AACxE,QAAM,OAAoB,cAAc,QAAQ,QAAQ,EAAE,GAAG,OAAO,UAAU,eAAc,GACtF,WAAW,OAAO,KAAK,IAAI;AACjC,SAAO,EAAE,aAAa,MAAM;IAC1B,OAAO,CAAC,UACN,MAAM,SAAS,sBAAsB,wBAAwB,UAAU,UAAU,MAAM,IAAI,IAAI;GAClG;AACH;AAEM,MAAO,gBAAe;;EAE1B,YAAY,QAAiB;AAC3B,eAAW,QAAQ;AACjB,aAAO,aACL,KAAK,MACL;QACE,aAAa,KAAK;QAClB,aAAa,sBAAsB,KAAK,MAAM,KAAK,WAAW;QAC9D,GAAI,KAAK,QAAQ,EAAE,OAAO,KAAK,MAAK,IAAK,CAAA;SAE3C,CAAC,MAA0C,UACzC,gBACE,YAAY,KAAK,IAAI,IACrB,OAAO,SAAQ;AAGb,cAAM,uBAAsB;AAE5B,cAAM,UAAU,MAAM,qBAAqB,KAAK,MAAO,KAA+B,UAAU,SAAS,GACnG,SAAS,UACX,YAAY,OAAO,IACnB,MAAM,KAAK,QAAQ,MAAM,EAAE,QAAQ,OAAO,QAAQ,WAAW,OAAO,WAAW,QAAQ,OAAO,OAAM,CAAE;AAC1G,YAAI,sBAAsB,QAAQ,SAAS;AACzC,gBAAM,eAAe,OAAO,UAAU,CAAC,GACjC,UACJ,cAAc,SAAS,SAAS,aAAa,OAAO;AACtD,eAAK,UAAU,EAAE,MAAM,GAAG,QAAO,CAAE;QACrC;AACA,eAAO;MACT,GACA,EAAE,iBAAiB,KAAK,KAAI,CAAE,CAC/B;EAGT;;",
6
6
  "names": []
7
7
  }