@cspeach/cli 0.9.0 → 1.1.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 (195) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/intent-system-prompt.js +1 -1
  3. package/dist/agent/loop.js +228 -26
  4. package/dist/agent/providers/license-gate.js +44 -0
  5. package/dist/agent/skill-checkpoint.js +1 -1
  6. package/dist/agent/tool-dispatch.js +15 -0
  7. package/dist/approvals/canonical.js +91 -0
  8. package/dist/approvals/jwt.js +39 -2
  9. package/dist/approvals/op-labels.js +124 -0
  10. package/dist/approvals/render.js +42 -36
  11. package/dist/auth/org-anthropic-key.js +25 -0
  12. package/dist/classifier/client.js +18 -3
  13. package/dist/cli.js +15 -0
  14. package/dist/commands/compact.js +28 -2
  15. package/dist/commands/config-set.js +284 -0
  16. package/dist/commands/config-show.js +20 -0
  17. package/dist/commands/export-audit.js +43 -0
  18. package/dist/commands/help.js +5 -0
  19. package/dist/commands/login.js +31 -14
  20. package/dist/commands/plan-audit-evidence.js +266 -0
  21. package/dist/commands/plan-audit.js +692 -0
  22. package/dist/commands/plan-chain.js +671 -0
  23. package/dist/commands/plan-continue.js +179 -0
  24. package/dist/commands/plan-gate.js +154 -0
  25. package/dist/commands/plan-model-tier.js +83 -0
  26. package/dist/commands/plan-resume.js +728 -46
  27. package/dist/config/loader.js +223 -5
  28. package/dist/config/model-defaults.js +14 -0
  29. package/dist/cost/pricing.js +27 -1
  30. package/dist/doctor/checks/_http-probe.js +1 -0
  31. package/dist/doctor/checks/cert.js +14 -3
  32. package/dist/doctor/checks/sap.js +30 -8
  33. package/dist/doctor/checks/system-roles.js +41 -0
  34. package/dist/doctor/checks/zcspeach.js +19 -4
  35. package/dist/doctor/run.js +2 -0
  36. package/dist/models/resolve.js +61 -0
  37. package/dist/models/server-config.js +155 -0
  38. package/dist/one-shot.js +76 -6
  39. package/dist/projects/answer-blockers.js +137 -0
  40. package/dist/projects/extract-cca.js +111 -17
  41. package/dist/projects/extract-modernize.js +4 -2
  42. package/dist/projects/extract-plan.js +184 -37
  43. package/dist/projects/extract-spec-gap.js +34 -7
  44. package/dist/projects/extract-test-coverage.js +4 -2
  45. package/dist/projects/extract-upgrade.js +116 -23
  46. package/dist/projects/handover-md.js +195 -0
  47. package/dist/projects/index.js +5 -2
  48. package/dist/projects/merge-cca.js +292 -0
  49. package/dist/projects/merge-upgrade.js +173 -0
  50. package/dist/projects/migration.js +103 -1
  51. package/dist/projects/output-paths.js +27 -0
  52. package/dist/projects/plan-run.js +285 -27
  53. package/dist/projects/plan-schema.js +136 -3
  54. package/dist/projects/promote-command.js +25 -2
  55. package/dist/projects/promote.js +128 -0
  56. package/dist/projects/run-lease.js +157 -0
  57. package/dist/projects/save-command.js +259 -21
  58. package/dist/projects/status.js +3 -1
  59. package/dist/projects/validate.js +1 -1
  60. package/dist/projects/workspace.js +164 -20
  61. package/dist/renderer/notices.js +64 -0
  62. package/dist/renderer/progress-chatter.js +8 -0
  63. package/dist/renderer/status-footer.js +22 -12
  64. package/dist/renderer/thinking-heartbeat.js +64 -8
  65. package/dist/renderer/todo-block.js +51 -0
  66. package/dist/renderer/tool-widget.js +55 -4
  67. package/dist/renderer/tty.js +43 -4
  68. package/dist/renderer/verify-chain.js +77 -0
  69. package/dist/repl/at-picker.js +60 -7
  70. package/dist/repl/bracketed-paste.js +28 -19
  71. package/dist/repl/builtin-commands.js +42 -0
  72. package/dist/repl/current-transport.js +10 -0
  73. package/dist/repl/early-line-buffer.js +68 -0
  74. package/dist/repl/history.js +86 -0
  75. package/dist/repl/ink-stdin-guard.js +64 -0
  76. package/dist/repl/inquirer-guard.js +70 -5
  77. package/dist/repl/mode-ceiling.js +16 -0
  78. package/dist/repl/mode-cycle.js +104 -0
  79. package/dist/repl/numbered-menu.js +131 -0
  80. package/dist/repl/post-turn-status.js +26 -6
  81. package/dist/repl/rule8-detector.js +17 -2
  82. package/dist/repl/safety-confirm.js +111 -2
  83. package/dist/repl/safety-mode-state.js +19 -3
  84. package/dist/repl/slash-completer.js +5 -0
  85. package/dist/repl/slash-picker.js +10 -15
  86. package/dist/repl.js +1232 -95
  87. package/dist/rewind/candidates.js +194 -0
  88. package/dist/rewind/cli.js +137 -0
  89. package/dist/rewind/format.js +27 -0
  90. package/dist/rewind/restore.js +245 -0
  91. package/dist/router/classifier.js +150 -6
  92. package/dist/sap/capability-matrix.js +20 -0
  93. package/dist/sap/capability-matrix.json +11236 -0
  94. package/dist/sap/capability.js +146 -0
  95. package/dist/sap/connection-manager.js +19 -1
  96. package/dist/sap/onboarding.js +42 -4
  97. package/dist/session/audit-export.js +459 -0
  98. package/dist/session/context-report.js +163 -0
  99. package/dist/session/pending.js +27 -0
  100. package/dist/session/recap.js +160 -0
  101. package/dist/skill-catalog.js +51 -40
  102. package/dist/skills/bundled-skills.js +272 -1
  103. package/dist/skills/promotion-dispatch.js +23 -0
  104. package/dist/tools/_command-shared.js +36 -12
  105. package/dist/tools/_filesystem-shared.js +139 -4
  106. package/dist/tools/_flag.js +25 -0
  107. package/dist/tools/approval.js +177 -26
  108. package/dist/tools/ask-question.js +400 -7
  109. package/dist/tools/capability/tool.js +74 -0
  110. package/dist/tools/dispatch-skill.js +22 -1
  111. package/dist/tools/extend-model/anchored-insert.js +1414 -0
  112. package/dist/tools/extend-model/tool.js +340 -0
  113. package/dist/tools/filesystem/extract-document.js +57 -0
  114. package/dist/tools/filesystem/file-edit.js +12 -2
  115. package/dist/tools/filesystem/file-read.js +2 -2
  116. package/dist/tools/filesystem/file-write.js +11 -2
  117. package/dist/tools/filesystem/glob.js +11 -0
  118. package/dist/tools/filesystem/grep.js +10 -0
  119. package/dist/tools/filesystem/read-document.js +107 -0
  120. package/dist/tools/fiori/apply.js +50 -0
  121. package/dist/tools/fiori/bin.js +3 -0
  122. package/dist/tools/fiori/catalog/index.js +27 -0
  123. package/dist/tools/fiori/catalog/value-help.js +230 -0
  124. package/dist/tools/fiori/catalog/viz-chart.js +177 -0
  125. package/dist/tools/fiori/cli.js +71 -0
  126. package/dist/tools/fiori/deploy-config.js +73 -0
  127. package/dist/tools/fiori/fe-extend.js +76 -0
  128. package/dist/tools/fiori/fe-scaffold.js +71 -0
  129. package/dist/tools/fiori/floorplan-map.js +19 -0
  130. package/dist/tools/fiori/i18n.js +39 -0
  131. package/dist/tools/fiori/manifest.js +70 -0
  132. package/dist/tools/fiori/render.js +77 -0
  133. package/dist/tools/fiori/samples/data/index.json +13602 -0
  134. package/dist/tools/fiori/samples/data/sources.generated.js +808 -0
  135. package/dist/tools/fiori/samples/loader.js +248 -0
  136. package/dist/tools/fiori/samples/search.js +63 -0
  137. package/dist/tools/fiori/samples/types.js +2 -0
  138. package/dist/tools/fiori/scaffold.js +39 -0
  139. package/dist/tools/fiori/smoke/assertions.js +74 -0
  140. package/dist/tools/fiori/smoke/browser.js +52 -0
  141. package/dist/tools/fiori/smoke/driver.js +89 -0
  142. package/dist/tools/fiori/smoke/freestyle-spec.js +317 -0
  143. package/dist/tools/fiori/smoke/run-smoke.js +149 -0
  144. package/dist/tools/fiori/tools.js +681 -0
  145. package/dist/tools/fiori/types.js +1 -0
  146. package/dist/tools/local-build.js +86 -0
  147. package/dist/tools/local-files.js +31 -0
  148. package/dist/tools/project/_merge-shared.js +68 -0
  149. package/dist/tools/project/cca_merge.js +164 -0
  150. package/dist/tools/project/playbook_get.js +1 -1
  151. package/dist/tools/project/upgrade_merge_progress.js +206 -0
  152. package/dist/tools/sap-read.js +132 -20
  153. package/dist/tools/sap-write.js +550 -21
  154. package/dist/tools/shell/shell_exec.js +41 -6
  155. package/dist/tools/snapshot.js +63 -14
  156. package/dist/tools/subagent/agent_run.js +27 -3
  157. package/dist/tools/subagent/background_run.js +17 -1
  158. package/dist/tools/todo.js +144 -0
  159. package/dist/tools/transport-resolution.js +86 -0
  160. package/dist/tools/transport.js +224 -5
  161. package/dist/tools/write-mode.js +4 -0
  162. package/dist/ui/app.js +378 -21
  163. package/dist/ui/approval-modal.js +49 -16
  164. package/dist/ui/ask-question-emitter.js +14 -0
  165. package/dist/ui/body.js +13 -0
  166. package/dist/ui/context-grid.js +108 -0
  167. package/dist/ui/footer.js +120 -27
  168. package/dist/ui/header.js +7 -0
  169. package/dist/ui/line-resolution.js +35 -8
  170. package/dist/ui/rewind-emitter.js +10 -0
  171. package/dist/ui/rewind-panel.js +81 -0
  172. package/dist/ui/sap-state-store.js +1 -0
  173. package/dist/ui/session-timeline.js +1 -0
  174. package/dist/ui/status-line.js +43 -0
  175. package/dist/ui/text-input.js +214 -0
  176. package/dist/ui/todo-emitter.js +25 -0
  177. package/dist/ui/todo-panel.js +64 -0
  178. package/dist/ui/turn-status-emitter.js +50 -4
  179. package/dist/ui/turn-status.js +18 -3
  180. package/dist/ui/widgets/ask-form.js +242 -0
  181. package/dist/ui/widgets/ask-question-modal.js +21 -8
  182. package/package.json +22 -3
  183. package/bench/README.md +0 -78
  184. package/bench/prompts/abap-document-cds.md +0 -44
  185. package/bench/prompts/abap-explain-bdef-handler.md +0 -57
  186. package/bench/prompts/abap-test-method.md +0 -42
  187. package/bench/results/abap-document-cds/claude-haiku-4-5.md +0 -189
  188. package/bench/results/abap-document-cds/claude-opus-4-7.md +0 -120
  189. package/bench/results/abap-document-cds/claude-sonnet-4-6.md +0 -151
  190. package/bench/results/abap-explain-bdef-handler/claude-haiku-4-5.md +0 -112
  191. package/bench/results/abap-explain-bdef-handler/claude-opus-4-7.md +0 -101
  192. package/bench/results/abap-explain-bdef-handler/claude-sonnet-4-6.md +0 -101
  193. package/bench/results/abap-test-method/claude-haiku-4-5.md +0 -186
  194. package/bench/results/abap-test-method/claude-opus-4-7.md +0 -193
  195. package/bench/results/abap-test-method/claude-sonnet-4-6.md +0 -234
@@ -17,6 +17,28 @@ import { ERR } from '../errors/codes.js';
17
17
  import { presentSafetyConfirmation } from '../repl/safety-confirm.js';
18
18
  import { isSafetyConfirmEnabled } from '../repl/safety-mode-state.js';
19
19
  import { setCurrentTransport } from '../repl/current-transport.js';
20
+ // ── Session ledger: transports CREATED by sap_transport_create this session ──
21
+ //
22
+ // Orphan-hint support (third junk transport this month, 2026-06-12 — D15/D20
23
+ // pattern: /abap-refactor created a fresh TR while ZTTT_GAME was already
24
+ // locked in S4HK903328, leaving the new TR an empty orphan).
25
+ // resolveWriteTransport (transport-resolution.ts) consults this set: when a
26
+ // write is forced into the owning transport, the session current-transport
27
+ // differs, AND the current one came from a create THIS session, it warns that
28
+ // the created TR may be an empty orphan. Deliberately NOT persisted — a TR
29
+ // created in an earlier session may have collected objects since.
30
+ const createdThisSession = new Set();
31
+ export function markTransportCreatedThisSession(trkorr) {
32
+ createdThisSession.add(trkorr);
33
+ }
34
+ export function wasTransportCreatedThisSession(trkorr) {
35
+ return createdThisSession.has(trkorr);
36
+ }
37
+ /** Test hook — the set is a module-scope singleton and would otherwise leak
38
+ * across Vitest tests (same pattern as clearCurrentTransport). */
39
+ export function clearTransportsCreatedThisSession() {
40
+ createdThisSession.clear();
41
+ }
20
42
  // ── sap_transport_create ─────────────────────────────────────────────────────
21
43
  //
22
44
  // Creates a new CTS workbench transport request.
@@ -24,16 +46,43 @@ import { setCurrentTransport } from '../repl/current-transport.js';
24
46
  registerTool({
25
47
  name: 'sap_transport_create',
26
48
  description: 'Create a new CTS transport request (Rule 9 — one dedicated transport per session). '
27
- + 'BEFORE calling this tool, you MUST ask the user for the transport description '
28
- + 'using `ask_question` — DO NOT generate a marketing-style description yourself. '
29
- + 'Use the user\'s exact answer as the `description` argument AND in your '
30
- + '`request_approval` reason. Identical strings prevent the approval mismatch '
31
- + 'failure mode observed live 2026-05-07. '
49
+ + 'RESOLVE FIRST: pass for_object_type/for_object_name ONLY for an EXISTING object you '
50
+ + 'are about to MODIFY/WRITE — the tool then reuses that object owning transport. Do NOT '
51
+ + 'pass an object you are only READING and not changing (e.g. the legacy source in a '
52
+ + 'modernization; the NEW objects go to a fresh transport and the read-only source lock '
53
+ + 'is irrelevant). Creating brand-new objects from scratch? OMIT for_object and a fresh '
54
+ + 'transport is created. '
55
+ + 'Flow: resolve → object locked in an open transport? this tool then does NOT '
56
+ + 'create anything and returns the owning transport — use it → only create when '
57
+ + 'the object is free. '
58
+ + 'Ask the user for the transport description using `ask_question` — DO NOT '
59
+ + 'generate a marketing-style description yourself. When asking, SUGGEST the '
60
+ + 'Rule 9 session format "ABAPForge <YYYY-MM-DD>: <work summary>" as the default; '
61
+ + 'the user may override. Use the user\'s exact answer as the `description` '
62
+ + 'argument AND in your `request_approval` reason (identical strings prevent the '
63
+ + 'approval mismatch failure mode observed live 2026-05-07). '
32
64
  + 'Requires approval_id. Returns the transport number to use in subsequent writes.',
33
65
  isMutating: true,
34
66
  input_schema: {
35
67
  type: 'object',
36
68
  properties: {
69
+ for_object_type: {
70
+ type: 'string',
71
+ description: 'ABAP object type (PROG, CLAS, …) of the object motivating this transport. '
72
+ + 'Pass it together with for_object_name whenever the transport is for work '
73
+ + 'on a known existing object.',
74
+ },
75
+ for_object_name: {
76
+ type: 'string',
77
+ description: 'Name of the object you are about to WRITE/modify (NOT a read-only source object '
78
+ + 'you only read — e.g. the legacy program in a modernization, whose new objects '
79
+ + 'go to a fresh transport).',
80
+ },
81
+ force: {
82
+ type: 'boolean',
83
+ description: 'Set true to create a new transport even though for_object is already '
84
+ + 'locked elsewhere (rare — deliberate parallel transport). Default false.',
85
+ },
37
86
  description: {
38
87
  type: 'string',
39
88
  description: 'Transport description AS PROVIDED BY THE USER (ask via ask_question first). '
@@ -60,6 +109,79 @@ registerTool({
60
109
  required: ['description', 'approval_id'],
61
110
  },
62
111
  handler: async (args, ctx) => {
112
+ // ── Gate 0: owning-transport guard (runs BEFORE the approval gate) ──────
113
+ //
114
+ // Ordering decision (2026-06-12, documented on purpose): the E071 lookup
115
+ // runs BEFORE verifyAndSpendApprovalId. When the motivating object is
116
+ // already locked, the early `already_locked` return performs NO mutation
117
+ // (read-only lookup + session current-transport pointer), so no
118
+ // authorization needs to be — or should be — consumed for it. Concretely:
119
+ // verifyAndSpendApprovalId only burns a nonce for one-shot ops
120
+ // (delete/release; 'create' approvals are reusable within TTL), so the
121
+ // user's approval JWT survives either ordering — but lookup-first means
122
+ // the model gets the useful answer ("use S4HK903328") even when the
123
+ // approval is stale/mismatched, instead of an APPROVAL_INVALID round-trip
124
+ // for a create that should never happen. The approval is verified only on
125
+ // the path that actually creates.
126
+ //
127
+ // Best-effort: a failed lookup must NEVER block the create (VPN flake is
128
+ // normal here) — same contract as resolveWriteTransport.
129
+ let guardNote;
130
+ if (args.for_object_name && !args.force) {
131
+ try {
132
+ const owning = await ctx.adt.transportForObject(args.for_object_type ?? '', args.for_object_name);
133
+ if (owning.transport) {
134
+ // Mirror the create success path: the owning TR becomes the session
135
+ // current-transport so subsequent writes pick it up via the 3-tier
136
+ // fallback. It is NOT marked created-this-session — it wasn't.
137
+ setCurrentTransport(owning.transport);
138
+ // Ownership-aware guidance (I1): "use it" is only honest advice when
139
+ // the connected user owns the lock — SAP rejects a foreign user's
140
+ // writes under someone else's request unless the owner adds a task.
141
+ const me = (ctx.adt.username ?? '').toUpperCase();
142
+ const foreignOwner = owning.owner && owning.owner.toUpperCase() !== me ? owning.owner : undefined;
143
+ const note = foreignOwner
144
+ ? `${args.for_object_name} is locked in ${owning.transport} (owned by ${foreignOwner}) `
145
+ + '— SAP only accepts changes under that request; ask the owner to add you as a '
146
+ + 'task user. Do NOT create a new transport either way.'
147
+ : `${args.for_object_name} is already locked in ${owning.transport} — use it; `
148
+ + 'no new transport created. Set as session current transport for subsequent writes.';
149
+ ctx.chunkEmitter?.emit('info', note);
150
+ return {
151
+ content: JSON.stringify({
152
+ created: false,
153
+ already_locked: true,
154
+ transport: owning.transport,
155
+ ...(owning.owner ? { owner: owning.owner } : {}),
156
+ note,
157
+ }),
158
+ };
159
+ }
160
+ if (owning.candidates.length > 1) {
161
+ // Ambiguous ownership — never pick silently (same stance as
162
+ // resolveWriteTransport). The create proceeds, but the model is
163
+ // told the candidates so it can surface them to the user.
164
+ guardNote =
165
+ `${args.for_object_name} pieces are locked in ${owning.candidates.length} open `
166
+ + `transports (${owning.candidates.join(', ')}) — writes to those pieces will be `
167
+ + 'forced into their owning requests; the new transport may stay empty.';
168
+ }
169
+ else if (owning.capped) {
170
+ // M1: a capped lookup means the E071 row set was truncated — the
171
+ // object COULD be locked in rows we never saw. The create proceeds
172
+ // (best-effort contract), but never silently.
173
+ guardNote =
174
+ 'owning-transport pre-check unreliable (row cap) — proceeding to create; '
175
+ + 'verify in SE03 if unsure.';
176
+ }
177
+ // free → normal create flow.
178
+ }
179
+ catch (err) {
180
+ ctx.chunkEmitter?.emit('info', `owning-transport pre-check failed for ${args.for_object_type ?? ''} `
181
+ + `${args.for_object_name} — proceeding with create `
182
+ + `(${err instanceof Error ? err.message : String(err)})`);
183
+ }
184
+ }
63
185
  // ── Gate 1: Approval ────────────────────────────────────────────────────
64
186
  const verify = await verifyAndSpendApprovalId(args.approval_id, args.description, 'create');
65
187
  if (!verify.ok) {
@@ -84,11 +206,34 @@ registerTool({
84
206
  // subsequent write calls pick it up via the 3-tier fallback without the LLM
85
207
  // needing to pass it explicitly every time.
86
208
  setCurrentTransport(result.transportNumber);
209
+ // Orphan-hint ledger: resolveWriteTransport warns later if this freshly
210
+ // created TR ends up bypassed by an owning-transport force.
211
+ markTransportCreatedThisSession(result.transportNumber);
212
+ // Legacy-caller nudge: the object-context args are how the create guard
213
+ // learns what the transport is FOR. Surfaced in the RESULT payload (not
214
+ // just the human stream) so the model learns the contract.
215
+ const forObjectHint = args.for_object_name
216
+ ? undefined
217
+ : 'Tip: pass for_object_type/for_object_name when the transport is for a known '
218
+ + 'existing object — the tool then reuses the owning transport instead of '
219
+ + 'creating an empty orphan (or check first via sap_transport_for_object).';
220
+ // Rule 9 description format (D20: "Magic fix" accepted verbatim) —
221
+ // non-blocking note when the description doesn't follow the session
222
+ // format. The transport IS created; the user's wording always wins.
223
+ const rule9Format = /^ABAPForge \d{4}-\d{2}-\d{2}: .+/;
224
+ const today = new Date().toISOString().slice(0, 10);
225
+ const rule9Suggestion = rule9Format.test(args.description)
226
+ ? undefined
227
+ : `Description does not follow the Rule 9 session format. Suggested: "ABAPForge ${today}: <work summary>". `
228
+ + 'Non-blocking — keep the user\'s wording if they chose it deliberately.';
87
229
  return {
88
230
  content: JSON.stringify({
89
231
  created: true,
90
232
  transport_number: result.transportNumber,
91
233
  description: result.description,
234
+ ...(rule9Suggestion ? { rule9_suggestion: rule9Suggestion } : {}),
235
+ ...(guardNote ? { guard_note: guardNote } : {}),
236
+ ...(forObjectHint ? { for_object_hint: forObjectHint } : {}),
92
237
  note: 'Use this transport_number in all subsequent sap_set_source / sap_create_object calls this session.',
93
238
  }),
94
239
  };
@@ -102,6 +247,80 @@ registerTool({
102
247
  }
103
248
  },
104
249
  });
250
+ // ── sap_transport_for_object ─────────────────────────────────────────────────
251
+ //
252
+ // Read-only E071/E070 owning-transport lookup (AdtClient.transportForObject).
253
+ // First-class way for skills/models to resolve the transport question BEFORE
254
+ // asking the user anything or creating a request.
255
+ registerTool({
256
+ name: 'sap_transport_for_object',
257
+ description: 'Check whether an ABAP object (or any of its LIMU pieces) is already locked '
258
+ + 'in an OPEN transport request (E071/E070 lookup). Read-only, no approval. '
259
+ + 'Call this BEFORE asking the user about transports or creating one: '
260
+ + 'locked → use the returned owning transport; not locked → a new or '
261
+ + 'existing transport may be used.',
262
+ isMutating: false,
263
+ input_schema: {
264
+ type: 'object',
265
+ properties: {
266
+ object_type: {
267
+ type: 'string',
268
+ description: 'ABAP object type (PROG, CLAS, TABL, …)',
269
+ },
270
+ object_name: {
271
+ type: 'string',
272
+ description: 'ABAP object name (e.g. ZTTT_GAME)',
273
+ },
274
+ },
275
+ required: ['object_type', 'object_name'],
276
+ },
277
+ handler: async (args, ctx) => {
278
+ try {
279
+ const r = await ctx.adt.transportForObject(args.object_type, args.object_name);
280
+ let note;
281
+ if (r.transport) {
282
+ // Ownership-aware guidance (I1) — mirrors the create guard wording.
283
+ const me = (ctx.adt.username ?? '').toUpperCase();
284
+ const foreignOwner = r.owner && r.owner.toUpperCase() !== me ? r.owner : undefined;
285
+ note = foreignOwner
286
+ ? `${args.object_name} is locked in ${r.transport} (owned by ${foreignOwner}) — SAP `
287
+ + 'only accepts changes under that request; ask the owner to add you as a task '
288
+ + 'user. Do NOT create a new transport either way.'
289
+ : `${args.object_name} is already locked in ${r.transport} — use this `
290
+ + 'transport for writes to it; do NOT create a new one.';
291
+ }
292
+ else if (r.capped) {
293
+ note = 'Lookup hit its row cap — the result is unreliable; ownership could not '
294
+ + 'be determined. Treat as unknown (write tools will still auto-resolve).';
295
+ }
296
+ else if (r.candidates.length > 1) {
297
+ note = `${args.object_name} pieces are locked in ${r.candidates.length} open `
298
+ + `transports (${r.candidates.join(', ')}) — ambiguous; ask the user which to use.`;
299
+ }
300
+ else {
301
+ note = `${args.object_name} is not locked in any open transport — a new or `
302
+ + 'existing transport may be used.';
303
+ }
304
+ return {
305
+ content: JSON.stringify({
306
+ object_type: args.object_type,
307
+ object_name: args.object_name,
308
+ transport: r.transport,
309
+ owner: r.owner ?? null,
310
+ candidates: r.candidates,
311
+ capped: r.capped ?? false,
312
+ note,
313
+ }),
314
+ };
315
+ }
316
+ catch (err) {
317
+ return {
318
+ content: JSON.stringify({ error: 'transport_for_object_failed', detail: String(err) }),
319
+ is_error: true,
320
+ };
321
+ }
322
+ },
323
+ });
105
324
  // ── sap_transport_list ───────────────────────────────────────────────────────
106
325
  //
107
326
  // Lists transport requests, optionally filtered by owner and/or status.
@@ -9,12 +9,16 @@
9
9
  */
10
10
  export const WRITE_TOOLS = new Set([
11
11
  'sap_create_object',
12
+ 'sap_create_domain',
13
+ 'sap_create_data_element',
12
14
  'sap_set_source',
15
+ 'sap_set_class_include',
13
16
  'sap_update_method',
14
17
  'sap_delete_object',
15
18
  'sap_activate',
16
19
  'sap_message_maintain',
17
20
  'sap_service_binding_publish',
21
+ 'sap_service_binding_unpublish',
18
22
  'sap_transport_release',
19
23
  ]);
20
24
  export function isWriteTool(name) {