@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
@@ -15,6 +15,7 @@
15
15
  * lock(type, name) → Promise<LockResult> line 1914
16
16
  * unlock(type, name, lockHandle) → Promise<void> line 1949
17
17
  * serviceBindingPublish(name) → Promise<{status,error?}> line 2658
18
+ * serviceBindingUnpublish(name) → Promise<{status,error?}>
18
19
  * messageMaintain (STUB — not yet in sap-client)
19
20
  * numberRangeIntervals (STUB — not yet in sap-client)
20
21
  *
@@ -24,18 +25,65 @@ import crypto from 'node:crypto';
24
25
  import { registerTool } from './index.js';
25
26
  import { verifyAndSpendApprovalId } from '../approvals/jwt.js';
26
27
  import { autoSnapshot } from './snapshot.js';
28
+ import { buildWriteChainLine } from '../renderer/verify-chain.js';
27
29
  import { verifySyntax, verifyActive } from './verify.js';
28
30
  import { appendPending, finalizeToolCall } from '../session/pending.js';
29
31
  import { auditLog } from '@cspeach/sap-client';
30
32
  import { ERR } from '../errors/codes.js';
31
33
  import { interceptUpdateMethod, CancelledByUserError, } from './update-method-intercept.js';
34
+ import { resolveWriteTransport } from './transport-resolution.js';
32
35
  import { presentSafetyConfirmation } from '../repl/safety-confirm.js';
33
36
  import { isSafetyConfirmEnabled } from '../repl/safety-mode-state.js';
37
+ // ── Transcript visibility helpers (D16-display + tour slide-7, 2026-06-12) ──
38
+ //
39
+ // Rule 7 auto-snapshots always fired but the transcript never said so
40
+ // (battery D16-display). And the tour shows writes as
41
+ // "① snapshot ✓ ② written ✓ ③ syntax ✓ ④ activated ✓" while real
42
+ // transcripts showed an opaque tool row. Both lines below are HUMAN-stream
43
+ // only (ctx.chunkEmitter) — tool RESULT payloads the model sees are
44
+ // unchanged, and subagent ctxs (no chunkEmitter) stay silent.
45
+ /** Dim one-liner when the Rule 7 auto-snapshot actually captured source. */
46
+ function emitSnapshotNotice(ctx, snap, type, name) {
47
+ if (snap?.taken && snap.entry) {
48
+ ctx.chunkEmitter?.emit('info', `snapshot ✓ ${type} ${name} ${snap.entry.timestamp}`);
49
+ }
50
+ }
51
+ /** Distinct LOUD note when the write proceeds against a fresh/never-activated
52
+ * shell that has no committed active version — nothing to snapshot (Rule 7 is
53
+ * satisfied because there is no prior source to protect). Kept separate from
54
+ * the 'new_object' (genuine 404) path so the transcript is honest about which
55
+ * case it was — this is the RAP behaviour-pool ZBP first-write. */
56
+ function emitNewShellNote(ctx, type, name) {
57
+ ctx.chunkEmitter?.emit('warn', `${type} ${name}: new/empty shell — no active version, no prior source to snapshot; proceeding with first write`);
58
+ }
59
+ /** Map the snapshot-gate outcome onto the chain line's honest states.
60
+ * `undefined` covers the best-effort gates whose failure was swallowed.
61
+ * 'object_not_found' is a genuine ADT 404 only; any other read failure maps
62
+ * to 'source_read_failed' (+ short detail) — never to "(new object)".
63
+ * 'new_shell' (exists but no active version — nothing to save) reuses the
64
+ * 'new_object' chain glyph; the honest which-case distinction is carried by
65
+ * the LOUD emitNewShellNote line, not the compact chain. */
66
+ function snapshotChainState(snap) {
67
+ if (snap?.taken)
68
+ return { snapshot: 'taken' };
69
+ if (snap?.reason === 'object_not_found' || snap?.reason === 'new_shell') {
70
+ return { snapshot: 'new_object' };
71
+ }
72
+ if (snap?.reason === 'source_read_failed') {
73
+ return { snapshot: 'source_read_failed', snapshotDetail: snap.detail };
74
+ }
75
+ return { snapshot: 'best_effort_failed' };
76
+ }
77
+ /** Compact ①②③④ pipeline summary — derived from the real gate results. */
78
+ function emitWriteChain(ctx, input) {
79
+ ctx.chunkEmitter?.emit('chunk', buildWriteChainLine(input) + '\n');
80
+ }
34
81
  registerTool({
35
82
  name: 'sap_set_source',
36
83
  description: 'Overwrite the full source of an ABAP object. Requires approval_id. '
37
84
  + 'Use for class-definition changes, new methods, or any change not confined to '
38
- + 'a single method body. For method-body-only edits use sap_update_method (Rule 7a).',
85
+ + 'a single method body. For method-body-only edits use sap_update_method (Rule 7a). '
86
+ + 'The transport auto-resolves to the owning request if the object is already locked.',
39
87
  isMutating: true,
40
88
  input_schema: {
41
89
  type: 'object',
@@ -57,7 +105,7 @@ registerTool({
57
105
  is_error: true,
58
106
  };
59
107
  }
60
- const toolUseId = crypto.randomUUID();
108
+ const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
61
109
  const startedAt = Date.now();
62
110
  // ── Rule 9 conflict warning ──────────────────────────────────────────────
63
111
  // Emit a warning if the LLM passed an explicit transport that differs from
@@ -69,9 +117,16 @@ registerTool({
69
117
  ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
70
118
  }
71
119
  }
72
- // ── Gate 2: Snapshot (Rule 7) ────────────────────────────────────────────
120
+ // ── Rule 9 owning-transport check (D15/D20) ──────────────────────────────
121
+ // If the object is already locked in an open request, write under THAT
122
+ // request — any other transport is a guaranteed CTS lock conflict.
123
+ const resolved = await resolveWriteTransport(ctx, args.type, args.name, args.transport);
124
+ // ── Gate 2: Snapshot (Rule 7) — HARD gate ────────────────────────────────
125
+ // Only a genuine 404 (new object) may proceed snapshot-less. A read
126
+ // failure on a possibly-EXISTING object means no recovery copy → block.
127
+ let snapOutcome;
73
128
  try {
74
- await autoSnapshot(args.name, args.type, ctx.adt);
129
+ snapOutcome = await autoSnapshot(args.name, args.type, ctx.adt);
75
130
  }
76
131
  catch (err) {
77
132
  return {
@@ -79,6 +134,22 @@ registerTool({
79
134
  is_error: true,
80
135
  };
81
136
  }
137
+ if (snapOutcome?.reason === 'source_read_failed') {
138
+ return {
139
+ content: JSON.stringify({
140
+ error: ERR.SNAPSHOT_FAILED,
141
+ reason: 'source_read_failed',
142
+ detail: snapOutcome.detail,
143
+ recovery: 'Pre-write source read failed (non-404) and no committed active version could be confirmed — the object may exist with source we could not read, so no snapshot was possible. If this is a live connection/auth problem, fix it and retry. If this is a freshly-created shell that got edit-locked by an earlier aborted write, recover it directly: unlock it (or activate the empty shell, then delete it) and re-run the create+write. Do not bypass Rule 7 by writing over source you never read.',
144
+ }),
145
+ is_error: true,
146
+ };
147
+ }
148
+ // Fresh/never-activated shell (no active version → nothing to snapshot):
149
+ // proceed like a new object, but say so LOUDLY and distinctly.
150
+ if (snapOutcome?.reason === 'new_shell')
151
+ emitNewShellNote(ctx, args.type, args.name);
152
+ emitSnapshotNotice(ctx, snapOutcome, args.type, args.name);
82
153
  // ── Gate 3: Safety confirmation (Rule 7) ─────────────────────────────────
83
154
  if (isSafetyConfirmEnabled()) {
84
155
  const r = await presentSafetyConfirmation({
@@ -87,7 +158,7 @@ registerTool({
87
158
  object: { type: args.type, name: args.name },
88
159
  what_will_happen: 'Overwrites full object source; snapshot is taken first.',
89
160
  rollback_available: true,
90
- transport: args.transport ?? ctx.currentTransport?.get() ?? '',
161
+ transport: resolved.transport ?? ctx.currentTransport?.get() ?? '',
91
162
  });
92
163
  if (!r.confirmed) {
93
164
  return {
@@ -114,7 +185,7 @@ registerTool({
114
185
  // Regression evidence + diagnosis: memory/project_phase3_lite_regression.md.
115
186
  let writeResult;
116
187
  try {
117
- writeResult = await ctx.adt.setSource(args.type, args.name, args.source, args.transport);
188
+ writeResult = await ctx.adt.setSource(args.type, args.name, args.source, resolved.transport);
118
189
  }
119
190
  catch (err) {
120
191
  const errStr = String(err);
@@ -140,12 +211,19 @@ registerTool({
140
211
  // ── Gate 4: Verify — syntax check (Rule 10) ──────────────────────────────
141
212
  const syntax = await verifySyntax(ctx.adt, args.name, args.type);
142
213
  await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ written: true, syntax }), false, Date.now() - startedAt);
214
+ // Tour slide-7 chain — derived from the gates that actually ran.
215
+ emitWriteChain(ctx, {
216
+ ...snapshotChainState(snapOutcome),
217
+ written: true,
218
+ syntax: { ok: syntax.ok, errors: syntax.errors },
219
+ });
143
220
  return {
144
221
  content: JSON.stringify({
145
222
  written: true,
146
223
  syntax_ok: syntax.ok,
147
224
  errors: syntax.errors,
148
225
  warnings: syntax.warnings,
226
+ ...(resolved.owning ? { transport_used: resolved.owning, transport_note: resolved.note } : {}),
149
227
  next_step: syntax.ok
150
228
  ? 'activation_allowed'
151
229
  : 'activation_blocked_until_syntax_errors_fixed',
@@ -166,7 +244,8 @@ registerTool({
166
244
  description: 'Replace the body of a single method in an existing class. '
167
245
  + 'Preferred over sap_set_source for method-body-only changes (Rule 7a) — '
168
246
  + 'does NOT rewrite the class definition. '
169
- + 'method_source must be the lines between METHOD x. and ENDMETHOD. (no wrappers).',
247
+ + 'method_source must be the lines between METHOD x. and ENDMETHOD. (no wrappers). '
248
+ + 'The transport auto-resolves to the owning request if the class is already locked.',
170
249
  isMutating: true,
171
250
  input_schema: {
172
251
  type: 'object',
@@ -188,7 +267,7 @@ registerTool({
188
267
  is_error: true,
189
268
  };
190
269
  }
191
- const toolUseId = crypto.randomUUID();
270
+ const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
192
271
  const startedAt = Date.now();
193
272
  // ── Rule 9 conflict warning ──────────────────────────────────────────────
194
273
  // Emit a warning if the LLM passed an explicit transport that differs from
@@ -200,9 +279,15 @@ registerTool({
200
279
  ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
201
280
  }
202
281
  }
203
- // ── Gate 2: Snapshot (Rule 7) ────────────────────────────────────────────
282
+ // ── Rule 9 owning-transport check (D15/D20) ──────────────────────────────
283
+ const resolved = await resolveWriteTransport(ctx, 'CLAS', args.class_name, args.transport);
284
+ // ── Gate 2: Snapshot (Rule 7) — HARD gate ────────────────────────────────
285
+ // sap_update_method always targets an EXISTING class, so a read failure
286
+ // (non-404) means no recovery copy → block. (A genuine 404 falls through
287
+ // here but the preview gate below fails on the same missing class.)
288
+ let snapOutcome;
204
289
  try {
205
- await autoSnapshot(args.class_name, 'CLAS', ctx.adt);
290
+ snapOutcome = await autoSnapshot(args.class_name, 'CLAS', ctx.adt);
206
291
  }
207
292
  catch (err) {
208
293
  return {
@@ -210,6 +295,22 @@ registerTool({
210
295
  is_error: true,
211
296
  };
212
297
  }
298
+ if (snapOutcome?.reason === 'source_read_failed') {
299
+ return {
300
+ content: JSON.stringify({
301
+ error: ERR.SNAPSHOT_FAILED,
302
+ reason: 'source_read_failed',
303
+ detail: snapOutcome.detail,
304
+ recovery: 'Pre-write source read failed (non-404) and no committed active version could be confirmed — the class may exist with source we could not read, so no snapshot was possible. If this is a live connection/auth problem, fix it and retry. If this is a freshly-created class shell that got edit-locked by an earlier aborted write, recover it directly: unlock it (or activate the empty shell, then delete it) and re-run the create+write. Do not bypass Rule 7 by writing over source you never read.',
305
+ }),
306
+ is_error: true,
307
+ };
308
+ }
309
+ // Fresh/never-activated class shell (no active version → nothing to
310
+ // snapshot): proceed like a new object, but say so LOUDLY and distinctly.
311
+ if (snapOutcome?.reason === 'new_shell')
312
+ emitNewShellNote(ctx, 'CLAS', args.class_name);
313
+ emitSnapshotNotice(ctx, snapOutcome, 'CLAS', args.class_name);
213
314
  // ── Gate 3: Diff preview & confirmation (Rule 7a) ────────────────────────
214
315
  if (!ctx.previewHook) {
215
316
  return {
@@ -227,7 +328,7 @@ registerTool({
227
328
  className: args.class_name,
228
329
  methodName: args.method_name,
229
330
  methodSource: args.method_source,
230
- transport: args.transport ?? ctx.currentTransport?.get() ?? '',
331
+ transport: resolved.transport ?? ctx.currentTransport?.get() ?? '',
231
332
  };
232
333
  try {
233
334
  await interceptUpdateMethod(minimal, call, ctx.previewHook);
@@ -263,7 +364,7 @@ registerTool({
263
364
  // silently overwriting the inactive-only declarations.
264
365
  let updateResult;
265
366
  try {
266
- updateResult = await ctx.adt.updateMethod(args.class_name, args.method_name, args.method_source, args.transport);
367
+ updateResult = await ctx.adt.updateMethod(args.class_name, args.method_name, args.method_source, resolved.transport);
267
368
  }
268
369
  catch (err) {
269
370
  const errStr = String(err);
@@ -302,12 +403,19 @@ registerTool({
302
403
  // ── Gate 4: Verify — syntax check (Rule 10) ──────────────────────────────
303
404
  const syntax = await verifySyntax(ctx.adt, args.class_name, 'CLAS');
304
405
  await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ updated: true, syntax }), false, Date.now() - startedAt);
406
+ // Tour slide-7 chain — derived from the gates that actually ran.
407
+ emitWriteChain(ctx, {
408
+ ...snapshotChainState(snapOutcome),
409
+ written: true,
410
+ syntax: { ok: syntax.ok, errors: syntax.errors },
411
+ });
305
412
  return {
306
413
  content: JSON.stringify({
307
414
  updated: true,
308
415
  syntax_ok: syntax.ok,
309
416
  errors: syntax.errors,
310
417
  warnings: syntax.warnings,
418
+ ...(resolved.owning ? { transport_used: resolved.owning, transport_note: resolved.note } : {}),
311
419
  next_step: syntax.ok
312
420
  ? 'activation_allowed'
313
421
  : 'activation_blocked_until_syntax_errors_fixed',
@@ -324,7 +432,8 @@ registerTool({
324
432
  name: 'sap_delete_object',
325
433
  description: 'Delete an ABAP object. Requires approval_id. '
326
434
  + 'A snapshot is taken automatically before deletion. '
327
- + 'Post-check: confirms the object no longer exists.',
435
+ + 'Post-check: confirms the object no longer exists. '
436
+ + 'The transport auto-resolves to the owning request if the object is already locked.',
328
437
  isMutating: true,
329
438
  input_schema: {
330
439
  type: 'object',
@@ -345,7 +454,7 @@ registerTool({
345
454
  is_error: true,
346
455
  };
347
456
  }
348
- const toolUseId = crypto.randomUUID();
457
+ const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
349
458
  const startedAt = Date.now();
350
459
  // ── Rule 9 conflict warning ──────────────────────────────────────────────
351
460
  // Emit a warning if the LLM passed an explicit transport that differs from
@@ -357,13 +466,17 @@ registerTool({
357
466
  ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
358
467
  }
359
468
  }
469
+ // ── Rule 9 owning-transport check (D15/D20) ──────────────────────────────
470
+ const resolved = await resolveWriteTransport(ctx, args.type, args.name, args.transport);
360
471
  // ── Gate 2: Snapshot (Rule 7) — best-effort, object may not have source ──
472
+ let snapOutcome;
361
473
  try {
362
- await autoSnapshot(args.name, args.type, ctx.adt);
474
+ snapOutcome = await autoSnapshot(args.name, args.type, ctx.adt);
363
475
  }
364
476
  catch {
365
477
  // Silently ignore — object may be a shell with no readable source.
366
478
  }
479
+ emitSnapshotNotice(ctx, snapOutcome, args.type, args.name);
367
480
  // ── Gate 3: Safety confirmation (Rule 7 — delete is irreversible-ish) ────
368
481
  if (isSafetyConfirmEnabled()) {
369
482
  const r = await presentSafetyConfirmation({
@@ -372,7 +485,7 @@ registerTool({
372
485
  object: { type: args.type, name: args.name },
373
486
  what_will_happen: 'Deletes object from SAP; snapshot is taken first; recovery via sap_snapshot_read + sap_set_source (LLM composes the rollback).',
374
487
  rollback_available: true,
375
- transport: args.transport ?? ctx.currentTransport?.get() ?? '',
488
+ transport: resolved.transport ?? ctx.currentTransport?.get() ?? '',
376
489
  });
377
490
  if (!r.confirmed) {
378
491
  return {
@@ -394,7 +507,7 @@ registerTool({
394
507
  // 4xx. Must check status / error on the return value.
395
508
  let deleteResult;
396
509
  try {
397
- deleteResult = await ctx.adt.deleteObject(args.type, args.name, args.transport);
510
+ deleteResult = await ctx.adt.deleteObject(args.type, args.name, resolved.transport);
398
511
  }
399
512
  catch (err) {
400
513
  const errStr = String(err);
@@ -423,7 +536,11 @@ registerTool({
423
536
  .catch(() => false);
424
537
  await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ deleted: !stillExists }), false, Date.now() - startedAt);
425
538
  return {
426
- content: JSON.stringify({ deleted: !stillExists, still_exists: stillExists }),
539
+ content: JSON.stringify({
540
+ deleted: !stillExists,
541
+ still_exists: stillExists,
542
+ ...(resolved.owning ? { transport_used: resolved.owning, transport_note: resolved.note } : {}),
543
+ }),
427
544
  };
428
545
  },
429
546
  });
@@ -448,6 +565,11 @@ registerTool({
448
565
  description: { type: 'string', description: 'Short description' },
449
566
  transport: { type: 'string', description: 'Target transport (optional — required for workbench packages)' },
450
567
  approval_id: { type: 'string', description: 'JWT from request_approval' },
568
+ // SRVB-only: a service binding must name the activated service definition it
569
+ // binds, plus the protocol/version. Required for type=SRVB; ignored otherwise.
570
+ serviceDefinition: { type: 'string', description: 'SRVB ONLY — REQUIRED when type=SRVB: name of the activated service definition (SRVD) to bind, e.g. ZUI_MYSERVICE.' },
571
+ bindingType: { type: 'string', description: 'SRVB ONLY (optional): binding protocol. Default ODATA.' },
572
+ bindingVersion: { type: 'string', description: 'SRVB ONLY (optional): binding version. Default V4 (OData V4 UI). Use V2 for OData V2.' },
451
573
  },
452
574
  required: ['name', 'type', 'package', 'description', 'approval_id'],
453
575
  },
@@ -460,7 +582,7 @@ registerTool({
460
582
  is_error: true,
461
583
  };
462
584
  }
463
- const toolUseId = crypto.randomUUID();
585
+ const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
464
586
  const startedAt = Date.now();
465
587
  // No snapshot — new object has no prior source.
466
588
  // ── Rule 9 conflict warning + 3-tier transport fallback ──────────────────
@@ -486,7 +608,11 @@ registerTool({
486
608
  // createObject(objtype, name, packageName, description, transport?, extras?)
487
609
  let result;
488
610
  try {
489
- result = await ctx.adt.createObject(args.type, args.name, args.package, args.description, effectiveTransport);
611
+ result = await ctx.adt.createObject(args.type, args.name, args.package, args.description, effectiveTransport, {
612
+ serviceDefinition: args.serviceDefinition,
613
+ bindingType: args.bindingType,
614
+ bindingVersion: args.bindingVersion,
615
+ });
490
616
  await auditLog('createObject', args.type, args.name, 'success', Date.now() - startedAt);
491
617
  }
492
618
  catch (err) {
@@ -516,6 +642,386 @@ registerTool({
516
642
  };
517
643
  },
518
644
  });
645
+ // ── sap_set_class_include ─────────────────────────────────────────────────────
646
+ //
647
+ // Signature: setClassInclude(className, includeType, source, transport?)
648
+ // → Promise<ClassIncludeResult>
649
+ // Writes a class local-include source (CCDEF/CCIMP/CCMAC/test classes). This is
650
+ // how RAP behaviour-pool handlers (which live in CCIMP / locals_imp) get landed.
651
+ // AdtClient locks the CLASS object, tries a ladder of PUT URL/content-type
652
+ // variants (first 2xx wins), and always unlocks. Snapshot is best-effort:
653
+ // the include source may not be independently readable, so a failed snapshot
654
+ // must not block the write (consistent with sap_delete_object).
655
+ registerTool({
656
+ name: 'sap_set_class_include',
657
+ description: 'Write a class local-include source (CCDEF locals_def, CCIMP locals_imp, '
658
+ + 'CCMAC macros, or test classes). Required for RAP behaviour-pool handlers, '
659
+ + 'which live in the CCIMP (locals_imp) include. Requires approval_id. '
660
+ + 'After writing, activate the class via sap_activate. '
661
+ + 'The transport auto-resolves to the owning request if the class is already locked.',
662
+ isMutating: true,
663
+ input_schema: {
664
+ type: 'object',
665
+ properties: {
666
+ className: { type: 'string', description: 'ABAP class name (e.g. ZBP_FOO)' },
667
+ includeType: {
668
+ type: 'string',
669
+ enum: ['locals_def', 'locals_imp', 'macros', 'testclasses'],
670
+ description: 'Which local include to write (locals_imp = CCIMP, where RAP handlers live)',
671
+ },
672
+ source: { type: 'string', description: 'Include source body' },
673
+ transport: { type: 'string', description: 'Transport request number (optional)' },
674
+ approval_id: { type: 'string', description: 'JWT from request_approval' },
675
+ },
676
+ required: ['className', 'includeType', 'source', 'approval_id'],
677
+ },
678
+ handler: async (args, ctx) => {
679
+ // ── Gate 1: Approval ────────────────────────────────────────────────────
680
+ const verify = await verifyAndSpendApprovalId(args.approval_id, args.className, 'modify');
681
+ if (!verify.ok) {
682
+ return {
683
+ content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
684
+ is_error: true,
685
+ };
686
+ }
687
+ const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
688
+ const startedAt = Date.now();
689
+ // ── Rule 9 conflict warning + 3-tier transport fallback ──────────────────
690
+ {
691
+ const sessionTransport = ctx.currentTransport?.get() ?? null;
692
+ if (args.transport && sessionTransport && args.transport !== sessionTransport) {
693
+ ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
694
+ }
695
+ }
696
+ const effectiveTransport = args.transport ?? ctx.currentTransport?.get() ?? undefined;
697
+ // ── Rule 9 owning-transport check (D15/D20) ──────────────────────────────
698
+ // The CCIMP piece (LIMU CINC) is exactly the kind of object that sits
699
+ // locked in a request from earlier work — battery S1's 409 root cause.
700
+ const resolved = await resolveWriteTransport(ctx, 'CLAS', args.className, effectiveTransport);
701
+ // ── Gate 2: Snapshot (Rule 7) — best-effort, include may not be readable ─
702
+ let snapOutcome;
703
+ try {
704
+ snapOutcome = await autoSnapshot(args.className, 'CLAS', ctx.adt);
705
+ }
706
+ catch {
707
+ // Silently ignore — include source may not be independently readable.
708
+ }
709
+ emitSnapshotNotice(ctx, snapOutcome, 'CLAS', args.className);
710
+ // ── WAL: record pending entry ────────────────────────────────────────────
711
+ await appendPending(ctx.session, {
712
+ tool_use_id: toolUseId,
713
+ tool: 'sap_set_class_include',
714
+ args: { className: args.className, includeType: args.includeType },
715
+ started_at: new Date().toISOString(),
716
+ });
717
+ // ── Write ────────────────────────────────────────────────────────────────
718
+ // setClassInclude THROWS only when it cannot acquire the class lock;
719
+ // for the all-variants-failed case it RETURNS { written:false, ... }.
720
+ // Both paths must surface as an error envelope.
721
+ let result;
722
+ try {
723
+ result = await ctx.adt.setClassInclude(args.className, args.includeType, args.source, resolved.transport);
724
+ }
725
+ catch (err) {
726
+ const errStr = String(err);
727
+ await finalizeToolCall(ctx.session, toolUseId, errStr, true, Date.now() - startedAt);
728
+ return {
729
+ content: JSON.stringify({ error: 'class_include_write_failed', detail: errStr }),
730
+ is_error: true,
731
+ };
732
+ }
733
+ if (!result.written) {
734
+ // D14: `result.error` now carries SAP's T100 diagnosis verbatim when a
735
+ // variant hit a CTS/lock conflict — pass it through unaltered, plus the
736
+ // structured owning transport so the model can retry correctly instead
737
+ // of declaring the change manual-only.
738
+ const detail = result.error ?? `All PUT variants failed (last HTTP ${result.httpStatus})`;
739
+ await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
740
+ return {
741
+ content: JSON.stringify({
742
+ error: 'class_include_write_failed',
743
+ detail,
744
+ http_status: result.httpStatus,
745
+ ...(result.lockedInTransport ? {
746
+ locked_in_transport: result.lockedInTransport,
747
+ recovery: `Retry this exact write with transport=${result.lockedInTransport} — that request already owns the object.`,
748
+ } : {}),
749
+ attempts: result.attempts,
750
+ }),
751
+ is_error: true,
752
+ };
753
+ }
754
+ await auditLog('setClassInclude', 'CLAS', args.className, 'success', Date.now() - startedAt, `include=${args.includeType}`);
755
+ await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ written: true, variant: result.variantUsed }), false, Date.now() - startedAt);
756
+ // Tour slide-7 chain. Honest states: includes have no standalone syntax
757
+ // gate (checked at activation), so ③ renders as deferred, never faked.
758
+ emitWriteChain(ctx, {
759
+ ...snapshotChainState(snapOutcome),
760
+ written: true,
761
+ syntax: 'not_checked',
762
+ });
763
+ return {
764
+ content: JSON.stringify({
765
+ written: true,
766
+ variant: result.variantUsed,
767
+ ...(resolved.owning ? { transport_used: resolved.owning, transport_note: resolved.note } : {}),
768
+ next_step: 'activate_the_class',
769
+ }),
770
+ };
771
+ },
772
+ });
773
+ // ── sap_create_domain ────────────────────────────────────────────────────────
774
+ //
775
+ // Signature: createDomain(spec, transport?) → Promise<DdicCreateResult>
776
+ // AdtClient creates → activates → verifies in one call (Rule 10 internal).
777
+ // No snapshot needed: new object has no prior source.
778
+ registerTool({
779
+ name: 'sap_create_domain',
780
+ description: 'Create a DDIC domain (DOMA), then activate and verify it. '
781
+ + 'Requires approval_id. No snapshot needed (new object). '
782
+ + 'Use the domain in a data element via sap_create_data_element.',
783
+ isMutating: true,
784
+ input_schema: {
785
+ type: 'object',
786
+ properties: {
787
+ name: { type: 'string', description: 'Domain name (Z*/Y*)' },
788
+ description: { type: 'string', description: 'Short description' },
789
+ package: { type: 'string', description: 'Development package (Z*/Y* or $TMP)' },
790
+ dataType: { type: 'string', description: 'ABAP data type (CHAR/NUMC/DEC/CURR/QUAN/INT1/INT2/INT4/DATS/TIMS/…)' },
791
+ length: { type: 'number', description: 'Domain length (number of characters/digits)' },
792
+ decimals: { type: 'number', description: 'Number of decimal places (DEC/CURR/QUAN)' },
793
+ outputLength: { type: 'number', description: 'Output length (optional)' },
794
+ lowercase: { type: 'boolean', description: 'Allow lowercase (optional)' },
795
+ signExists: { type: 'boolean', description: 'Value can be negative (optional)' },
796
+ valueTable: { type: 'string', description: 'Check / value table (optional)' },
797
+ fixedValues: {
798
+ type: 'array',
799
+ description: 'Fixed value list (optional)',
800
+ items: {
801
+ type: 'object',
802
+ properties: {
803
+ value: { type: 'string', description: 'Fixed value' },
804
+ text: { type: 'string', description: 'Description text' },
805
+ },
806
+ required: ['value', 'text'],
807
+ },
808
+ },
809
+ transport: { type: 'string', description: 'Target transport (optional)' },
810
+ approval_id: { type: 'string', description: 'JWT from request_approval' },
811
+ },
812
+ required: ['name', 'description', 'package', 'dataType', 'length', 'approval_id'],
813
+ },
814
+ handler: async (args, ctx) => {
815
+ // ── Gate 1: Approval ────────────────────────────────────────────────────
816
+ const verify = await verifyAndSpendApprovalId(args.approval_id, args.name, 'create');
817
+ if (!verify.ok) {
818
+ return {
819
+ content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
820
+ is_error: true,
821
+ };
822
+ }
823
+ const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
824
+ const startedAt = Date.now();
825
+ // No snapshot — new object has no prior source.
826
+ // ── Rule 9 conflict warning + 3-tier transport fallback ──────────────────
827
+ {
828
+ const sessionTransport = ctx.currentTransport?.get() ?? null;
829
+ if (args.transport && sessionTransport && args.transport !== sessionTransport) {
830
+ ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
831
+ }
832
+ }
833
+ const effectiveTransport = args.transport ?? ctx.currentTransport?.get() ?? undefined;
834
+ // ── WAL: record pending entry ────────────────────────────────────────────
835
+ await appendPending(ctx.session, {
836
+ tool_use_id: toolUseId,
837
+ tool: 'sap_create_domain',
838
+ args: { name: args.name, type: 'DOMA', package: args.package },
839
+ started_at: new Date().toISOString(),
840
+ });
841
+ // ── Build spec (omit undefined optionals) ────────────────────────────────
842
+ const spec = {
843
+ name: args.name,
844
+ description: args.description,
845
+ package: args.package,
846
+ dataType: args.dataType,
847
+ length: args.length,
848
+ };
849
+ if (args.decimals !== undefined)
850
+ spec.decimals = args.decimals;
851
+ if (args.outputLength !== undefined)
852
+ spec.outputLength = args.outputLength;
853
+ if (args.lowercase !== undefined)
854
+ spec.lowercase = args.lowercase;
855
+ if (args.signExists !== undefined)
856
+ spec.signExists = args.signExists;
857
+ if (args.valueTable !== undefined)
858
+ spec.valueTable = args.valueTable;
859
+ if (args.fixedValues !== undefined)
860
+ spec.fixedValues = args.fixedValues;
861
+ // ── Create → activate → verify (AdtClient does all three) ────────────────
862
+ let result;
863
+ try {
864
+ result = await ctx.adt.createDomain(spec, effectiveTransport);
865
+ await auditLog('createObject', 'DOMA/DD', args.name, 'success', Date.now() - startedAt);
866
+ }
867
+ catch (err) {
868
+ const errStr = String(err);
869
+ await finalizeToolCall(ctx.session, toolUseId, errStr, true, Date.now() - startedAt);
870
+ return {
871
+ content: JSON.stringify({ error: 'create_failed', detail: errStr }),
872
+ is_error: true,
873
+ };
874
+ }
875
+ if (!result.created) {
876
+ await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
877
+ return {
878
+ content: JSON.stringify({ error: 'create_failed', detail: result.error }),
879
+ is_error: true,
880
+ };
881
+ }
882
+ if (!result.active) {
883
+ // Rule 10: created but did not activate — surface as an error.
884
+ await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
885
+ return {
886
+ content: JSON.stringify({ error: 'activation_failed', object: result.name, detail: result.error }),
887
+ is_error: true,
888
+ };
889
+ }
890
+ await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), false, Date.now() - startedAt);
891
+ return {
892
+ content: JSON.stringify({
893
+ created: true,
894
+ active: true,
895
+ object: result.name,
896
+ type: result.type,
897
+ next_step: 'use_in_table_field',
898
+ }),
899
+ };
900
+ },
901
+ });
902
+ // ── sap_create_data_element ──────────────────────────────────────────────────
903
+ //
904
+ // Signature: createDataElement(spec, transport?) → Promise<DdicCreateResult>
905
+ // AdtClient creates → activates → verifies in one call (Rule 10 internal).
906
+ // No snapshot needed: new object has no prior source.
907
+ registerTool({
908
+ name: 'sap_create_data_element',
909
+ description: 'Create a DDIC data element (DTEL) bound to an existing domain, then '
910
+ + 'activate and verify it. Requires approval_id. No snapshot needed (new object). '
911
+ + 'Use the data element as a table/structure field type.',
912
+ isMutating: true,
913
+ input_schema: {
914
+ type: 'object',
915
+ properties: {
916
+ name: { type: 'string', description: 'Data element name (Z*/Y*)' },
917
+ description: { type: 'string', description: 'Short description' },
918
+ package: { type: 'string', description: 'Development package (Z*/Y* or $TMP)' },
919
+ domainName: { type: 'string', description: 'Referenced domain name' },
920
+ dataType: { type: 'string', description: "Referenced domain's data type (CHAR/NUMC/CURR/…)" },
921
+ length: { type: 'number', description: 'Domain length' },
922
+ decimals: { type: 'number', description: 'Decimal places (optional)' },
923
+ labels: {
924
+ type: 'object',
925
+ description: 'Field labels',
926
+ properties: {
927
+ short: { type: 'string', description: 'Short label' },
928
+ medium: { type: 'string', description: 'Medium label' },
929
+ long: { type: 'string', description: 'Long label' },
930
+ heading: { type: 'string', description: 'Heading label' },
931
+ },
932
+ required: ['short', 'medium', 'long', 'heading'],
933
+ },
934
+ searchHelp: { type: 'string', description: 'Attached search help name (optional)' },
935
+ searchHelpParameter: { type: 'string', description: 'Search help parameter (optional)' },
936
+ transport: { type: 'string', description: 'Target transport (optional)' },
937
+ approval_id: { type: 'string', description: 'JWT from request_approval' },
938
+ },
939
+ required: ['name', 'description', 'package', 'domainName', 'dataType', 'length', 'labels', 'approval_id'],
940
+ },
941
+ handler: async (args, ctx) => {
942
+ // ── Gate 1: Approval ────────────────────────────────────────────────────
943
+ const verify = await verifyAndSpendApprovalId(args.approval_id, args.name, 'create');
944
+ if (!verify.ok) {
945
+ return {
946
+ content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
947
+ is_error: true,
948
+ };
949
+ }
950
+ const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
951
+ const startedAt = Date.now();
952
+ // No snapshot — new object has no prior source.
953
+ // ── Rule 9 conflict warning + 3-tier transport fallback ──────────────────
954
+ {
955
+ const sessionTransport = ctx.currentTransport?.get() ?? null;
956
+ if (args.transport && sessionTransport && args.transport !== sessionTransport) {
957
+ ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
958
+ }
959
+ }
960
+ const effectiveTransport = args.transport ?? ctx.currentTransport?.get() ?? undefined;
961
+ // ── WAL: record pending entry ────────────────────────────────────────────
962
+ await appendPending(ctx.session, {
963
+ tool_use_id: toolUseId,
964
+ tool: 'sap_create_data_element',
965
+ args: { name: args.name, type: 'DTEL', package: args.package },
966
+ started_at: new Date().toISOString(),
967
+ });
968
+ // ── Build spec (omit undefined optionals) ────────────────────────────────
969
+ const spec = {
970
+ name: args.name,
971
+ description: args.description,
972
+ package: args.package,
973
+ domainName: args.domainName,
974
+ dataType: args.dataType,
975
+ length: args.length,
976
+ labels: args.labels,
977
+ };
978
+ if (args.decimals !== undefined)
979
+ spec.decimals = args.decimals;
980
+ if (args.searchHelp !== undefined)
981
+ spec.searchHelp = args.searchHelp;
982
+ if (args.searchHelpParameter !== undefined)
983
+ spec.searchHelpParameter = args.searchHelpParameter;
984
+ // ── Create → activate → verify (AdtClient does all three) ────────────────
985
+ let result;
986
+ try {
987
+ result = await ctx.adt.createDataElement(spec, effectiveTransport);
988
+ await auditLog('createObject', 'DTEL/DE', args.name, 'success', Date.now() - startedAt);
989
+ }
990
+ catch (err) {
991
+ const errStr = String(err);
992
+ await finalizeToolCall(ctx.session, toolUseId, errStr, true, Date.now() - startedAt);
993
+ return {
994
+ content: JSON.stringify({ error: 'create_failed', detail: errStr }),
995
+ is_error: true,
996
+ };
997
+ }
998
+ if (!result.created) {
999
+ await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
1000
+ return {
1001
+ content: JSON.stringify({ error: 'create_failed', detail: result.error }),
1002
+ is_error: true,
1003
+ };
1004
+ }
1005
+ if (!result.active) {
1006
+ // Rule 10: created but did not activate — surface as an error.
1007
+ await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
1008
+ return {
1009
+ content: JSON.stringify({ error: 'activation_failed', object: result.name, detail: result.error }),
1010
+ is_error: true,
1011
+ };
1012
+ }
1013
+ await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), false, Date.now() - startedAt);
1014
+ return {
1015
+ content: JSON.stringify({
1016
+ created: true,
1017
+ active: true,
1018
+ object: result.name,
1019
+ type: result.type,
1020
+ next_step: 'use_in_table_field',
1021
+ }),
1022
+ };
1023
+ },
1024
+ });
519
1025
  // ── sap_activate ─────────────────────────────────────────────────────────────
520
1026
  //
521
1027
  // Signature: activate(type, name) → Promise<ActivationResult>
@@ -565,7 +1071,7 @@ registerTool({
565
1071
  is_error: true,
566
1072
  };
567
1073
  }
568
- const toolUseId = crypto.randomUUID();
1074
+ const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
569
1075
  const startedAt = Date.now();
570
1076
  // ── WAL: record pending entry ────────────────────────────────────────────
571
1077
  await appendPending(ctx.session, {
@@ -700,6 +1206,29 @@ registerTool({
700
1206
  return { content: JSON.stringify(result) };
701
1207
  },
702
1208
  });
1209
+ // ── sap_service_binding_unpublish ─────────────────────────────────────────────
1210
+ //
1211
+ // Unpublish a service binding (inverse of publish). REQUIRED before deleting a
1212
+ // published binding — SAP returns HTTP 412 "… is published" otherwise (Bug #6).
1213
+ // Signature: serviceBindingUnpublish(name) → Promise<{ status: number; error?: string }>
1214
+ // NOTE: the unpublishjobs endpoint is inferred-not-yet-live-verified (VPN down).
1215
+ registerTool({
1216
+ name: 'sap_service_binding_unpublish',
1217
+ description: 'Unpublish an ADT service binding so its OData endpoint is removed. '
1218
+ + 'Required before deleting a published binding (SAP returns HTTP 412 "is published" otherwise).',
1219
+ isMutating: true,
1220
+ input_schema: {
1221
+ type: 'object',
1222
+ properties: {
1223
+ name: { type: 'string', description: 'Service binding name (e.g. ZSALES_UI_V4)' },
1224
+ },
1225
+ required: ['name'],
1226
+ },
1227
+ handler: async (args, ctx) => {
1228
+ const result = await ctx.adt.serviceBindingUnpublish(args.name);
1229
+ return { content: JSON.stringify(result) };
1230
+ },
1231
+ });
703
1232
  // ── sap_message_maintain (STUB) ───────────────────────────────────────────────
704
1233
  //
705
1234
  // STUB — MSAG message-class maintenance requires XML message-class-format