@cspeach/cli 1.1.44 → 1.1.45
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.
- package/.cspeach/project.json +4 -0
- package/dist/agent/parallel-write-guard.js +2 -0
- package/dist/approvals/jwt.js +5 -2
- package/dist/approvals/op-labels.js +39 -4
- package/dist/approvals/render.js +36 -9
- package/dist/approvals/risk-floor.js +14 -3
- package/dist/one-shot.js +1 -0
- package/dist/renderer/progress-chatter.js +9 -2
- package/dist/renderer/tool-labels.js +3 -1
- package/dist/repl.js +1 -0
- package/dist/sap/profile-features.js +5 -0
- package/dist/session/audit-export.js +50 -2
- package/dist/tools/approval.js +59 -1
- package/dist/tools/release-texts.js +9 -0
- package/dist/tools/sap-read.js +6 -2
- package/dist/tools/sap-run.js +163 -0
- package/dist/tools/tool-loading.js +8 -7
- package/dist/tools/transport.js +248 -22
- package/dist/tools/write-mode.js +2 -0
- package/dist/ui/approval-modal.js +11 -3
- package/dist/ui/session-timeline.js +1 -1
- package/package.json +2 -2
|
@@ -33,6 +33,8 @@ export const WRITE_CLASS_TOOLS = new Set([
|
|
|
33
33
|
'sap_create_object',
|
|
34
34
|
// Final review I2 — SAP's RAP generator creates up to 9 objects in one call.
|
|
35
35
|
'sap_generate_objects',
|
|
36
|
+
// Part 3 — a run executes code; it never runs beside a write in the same round.
|
|
37
|
+
'sap_run',
|
|
36
38
|
]);
|
|
37
39
|
export function isWriteClassTool(name) {
|
|
38
40
|
return WRITE_CLASS_TOOLS.has(name);
|
package/dist/approvals/jwt.js
CHANGED
|
@@ -15,6 +15,8 @@ const usedNonces = new Set();
|
|
|
15
15
|
* - approving `modify` covers modify → activate (write then activate).
|
|
16
16
|
* - `delete` / `release` stay single-op (and single-use) — one-shot ops the
|
|
17
17
|
* user must approve explicitly each time.
|
|
18
|
+
* - `run` (Part 3 D2) is single-op and single-use too: one approval runs a
|
|
19
|
+
* class or program once.
|
|
18
20
|
*
|
|
19
21
|
* This removes the op_mismatch APPROVAL_INVALID storm (create approval used,
|
|
20
22
|
* then set_source rejected because it's a different op) without weakening the
|
|
@@ -26,11 +28,12 @@ const OP_COVERAGE = {
|
|
|
26
28
|
delete: new Set(['delete']),
|
|
27
29
|
activate: new Set(['activate']),
|
|
28
30
|
release: new Set(['release']),
|
|
31
|
+
run: new Set(['run']),
|
|
29
32
|
};
|
|
30
33
|
/** create/modify approvals are reusable across their object's lifecycle ops
|
|
31
|
-
* until TTL; delete/release are genuinely one-shot. */
|
|
34
|
+
* until TTL; delete/release/run are genuinely one-shot. */
|
|
32
35
|
function isOneShotOp(op) {
|
|
33
|
-
return op === 'delete' || op === 'release';
|
|
36
|
+
return op === 'delete' || op === 'release' || op === 'run';
|
|
34
37
|
}
|
|
35
38
|
export async function mintApprovalId(payload) {
|
|
36
39
|
const nonce = crypto.randomBytes(16).toString('hex');
|
|
@@ -14,6 +14,7 @@ const VERBS = {
|
|
|
14
14
|
delete: 'Delete',
|
|
15
15
|
activate: 'Activate',
|
|
16
16
|
release: 'Release',
|
|
17
|
+
run: 'Run',
|
|
17
18
|
};
|
|
18
19
|
/**
|
|
19
20
|
* SAP ADT type codes → plain words. The `type` field is free text supplied by
|
|
@@ -45,6 +46,8 @@ const TYPE_WORDS = {
|
|
|
45
46
|
FUNC: 'function module',
|
|
46
47
|
FINC: 'function group include',
|
|
47
48
|
INCL: 'include',
|
|
49
|
+
// Part 3 (T1 ruling): the release and the create card both read "transport request".
|
|
50
|
+
CTS: 'transport request',
|
|
48
51
|
};
|
|
49
52
|
export function humanVerb(op) {
|
|
50
53
|
return VERBS[op] ?? op.charAt(0).toUpperCase() + op.slice(1);
|
|
@@ -86,6 +89,38 @@ export function affectsLine(change) {
|
|
|
86
89
|
const t = sanitizeControlChars(String(change.affects ?? '')).trim();
|
|
87
90
|
return t ? `Affects: ${t}` : null;
|
|
88
91
|
}
|
|
92
|
+
/** Part 3 D12 — the longest request description a release card shows. */
|
|
93
|
+
const RELEASE_DESCRIPTION_MAX = 60;
|
|
94
|
+
const RELEASE_LAST_LINE = 'Cannot be undone. Once released, the request can move to the other systems on the transport route.';
|
|
95
|
+
/** Part 3 D13 — the card a person decides on says what CSPeach checks and where to check it after the release (owner wording 2026-10-17). */
|
|
96
|
+
export const RELEASE_CHECK_LINE = 'CSPeach reads SAP\'s verdict after each step and checks the request afterwards. Check the transport in SE09 after the release.';
|
|
97
|
+
/**
|
|
98
|
+
* Part 3 D12 — the fixed lines under a release change; [] for any other change
|
|
99
|
+
* or when no facts are attached. The facts are SAP's (request_approval reads
|
|
100
|
+
* them from the request before the card is drawn); text from SAP still passes
|
|
101
|
+
* through sanitizeControlChars. A task owned by someone other than the request
|
|
102
|
+
* owner is named (preflight F-15: SAP releases it under the caller's
|
|
103
|
+
* authorization).
|
|
104
|
+
*/
|
|
105
|
+
export function releaseLines(change) {
|
|
106
|
+
const f = change.releaseFacts;
|
|
107
|
+
if (change.op !== 'release' || !f)
|
|
108
|
+
return [];
|
|
109
|
+
const clean = (v) => sanitizeControlChars(String(v ?? '')).trim();
|
|
110
|
+
let description = clean(f.description);
|
|
111
|
+
if (description.length > RELEASE_DESCRIPTION_MAX)
|
|
112
|
+
description = `${description.slice(0, RELEASE_DESCRIPTION_MAX)}…`;
|
|
113
|
+
const owner = clean(f.owner);
|
|
114
|
+
const tasks = Array.isArray(f.tasks) ? f.tasks : [];
|
|
115
|
+
const others = [...new Set(tasks.map((t) => clean(t.owner)).filter((o) => o && o !== owner))];
|
|
116
|
+
return [
|
|
117
|
+
`Request ${clean(f.request)} "${description}" · owner ${owner} · target ${clean(f.target) || 'none'}`,
|
|
118
|
+
`${tasks.length} ${tasks.length === 1 ? 'task' : 'tasks'}: ${tasks.map((t) => clean(t.number)).join(', ')} · ${f.objects === null || f.objects === undefined ? 'objects: could not count' : `${f.objects} ${f.objects === 1 ? 'object' : 'objects'}`}`
|
|
119
|
+
+ (others.length ? ` · other owners: ${others.join(', ')}` : ''),
|
|
120
|
+
RELEASE_LAST_LINE,
|
|
121
|
+
RELEASE_CHECK_LINE,
|
|
122
|
+
];
|
|
123
|
+
}
|
|
89
124
|
/**
|
|
90
125
|
* The transport to DISPLAY on the approval surface for one change — mirrors
|
|
91
126
|
* how the write tools actually resolve it (traced in sap-write.ts, 2026-07-05
|
|
@@ -97,8 +132,8 @@ export function affectsLine(change) {
|
|
|
97
132
|
* resolveWriteTransport(args only, + owning-TR override) — NO session
|
|
98
133
|
* fallback in the actual write, so we only show a transport the request
|
|
99
134
|
* explicitly carries.
|
|
100
|
-
* - activate/release: the tools take no transport arg at all — never
|
|
101
|
-
* a Transport segment.
|
|
135
|
+
* - activate/release/run: the tools take no transport arg at all — never
|
|
136
|
+
* show a Transport segment.
|
|
102
137
|
* When nothing is resolvable, return undefined and the caller OMITS the
|
|
103
138
|
* segment — no "new (session)" promise (a $TMP write never creates one).
|
|
104
139
|
*/
|
|
@@ -109,7 +144,7 @@ export function displayTransport(op, explicit, session) {
|
|
|
109
144
|
case 'modify':
|
|
110
145
|
case 'delete':
|
|
111
146
|
return explicit;
|
|
112
|
-
default: // activate / release
|
|
147
|
+
default: // activate / release / run
|
|
113
148
|
return undefined;
|
|
114
149
|
}
|
|
115
150
|
}
|
|
@@ -132,7 +167,7 @@ export function toSourceLines(content) {
|
|
|
132
167
|
* - modify → [d] Show diff (there IS a before/after to diff)
|
|
133
168
|
* - create → [v] View source, only when the new source is carried
|
|
134
169
|
* - delete → [v] View current source, only when the source is carried
|
|
135
|
-
* - activate/release → nothing to review (no source in the payload)
|
|
170
|
+
* - activate/release/run → nothing to review (no source in the payload)
|
|
136
171
|
*/
|
|
137
172
|
export function viewAffordance(change) {
|
|
138
173
|
switch (change.op) {
|
package/dist/approvals/render.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { select, input } from '@inquirer/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { riskReasonText, shownRiskReasons } from './risk-floor.js';
|
|
4
|
-
import { verbLine, viewAffordance, toSourceLines, affectsLine } from './op-labels.js';
|
|
4
|
+
import { verbLine, viewAffordance, toSourceLines, affectsLine, releaseLines } from './op-labels.js';
|
|
5
5
|
import { shouldUseInk } from '../renderer/tty.js';
|
|
6
6
|
import { approvalEmitter } from '../ui/approval-emitter.js';
|
|
7
7
|
import { withInquirer } from '../repl/inquirer-guard.js';
|
|
@@ -118,22 +118,44 @@ export async function renderPlanGate(summary, changes, transport, effectiveRisk,
|
|
|
118
118
|
// implementation) carries its "Affects:" row right under it. The card's
|
|
119
119
|
// step budget counts rows, and a step is never shown without its row.
|
|
120
120
|
// No affects anywhere: one row per step, exactly as before.
|
|
121
|
+
// Part 3 D12 / F-13 — a release step carries its fact lines the same way:
|
|
122
|
+
// "Apply all" mints it without its per-change card.
|
|
121
123
|
const steps = visibleChanges.map((c, i) => {
|
|
122
124
|
const affects = affectsLine(c);
|
|
123
|
-
return [`${i + 1}. ${verbLine(c)}`, ...(affects ? [` ${affects}`] : [])];
|
|
125
|
+
return [`${i + 1}. ${verbLine(c)}`, ...(affects ? [` ${affects}`] : []), ...releaseLines(c).map((l) => ` ${l}`)];
|
|
124
126
|
});
|
|
125
|
-
|
|
127
|
+
// Fix round 1 (owner ruling) — a release step is ALWAYS on the card, with
|
|
128
|
+
// its fact lines, wherever it sits: its rows have their own budget and
|
|
129
|
+
// never count toward MAX_STEPS. The other steps keep today's budget, so a
|
|
130
|
+
// plan without a release renders exactly as before.
|
|
131
|
+
const isRelease = visibleChanges.map((c) => releaseLines(c).length > 0);
|
|
132
|
+
const totalRows = steps.reduce((n, s, i) => n + (isRelease[i] ? 0 : s.length), 0);
|
|
126
133
|
if (totalRows > MAX_STEPS) {
|
|
127
134
|
// Object names are model-supplied: neutralise control characters
|
|
128
135
|
// before they reach scrollback (the card sanitises its own rows).
|
|
129
136
|
emitTrace(sanitizeControlChars(['Plan', ...steps.flat().map((s) => ` ${s}`)].join('\n')));
|
|
137
|
+
const shownRows = [];
|
|
130
138
|
let shown = 0;
|
|
139
|
+
let others = 0;
|
|
131
140
|
let rows = 0;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
let full = false;
|
|
142
|
+
steps.forEach((s, i) => {
|
|
143
|
+
if (isRelease[i]) {
|
|
144
|
+
shownRows.push(...s);
|
|
145
|
+
shown += 1;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!full && (others === 0 || rows + s.length <= MAX_STEPS)) {
|
|
149
|
+
shownRows.push(...s);
|
|
150
|
+
rows += s.length;
|
|
151
|
+
others += 1;
|
|
152
|
+
shown += 1;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
full = true;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
facts.push(...shownRows, `${getGlyphs().more} ${steps.length - shown} more steps, listed above`);
|
|
137
159
|
}
|
|
138
160
|
else {
|
|
139
161
|
facts.push(...steps.flat());
|
|
@@ -180,6 +202,9 @@ async function classicPlanGate(summary, visibleChanges, transport, effectiveRisk
|
|
|
180
202
|
const affects = affectsLine(c);
|
|
181
203
|
if (affects)
|
|
182
204
|
console.log(` ${chalk.dim(affects)}`);
|
|
205
|
+
// Part 3 D12 / F-13 — a release row's facts, under its line.
|
|
206
|
+
for (const l of releaseLines(c))
|
|
207
|
+
console.log(` ${chalk.dim(l)}`);
|
|
183
208
|
});
|
|
184
209
|
console.log('');
|
|
185
210
|
const choice = await withInquirer(() => select({
|
|
@@ -213,8 +238,10 @@ export async function renderPerChangeApproval(change, effectiveRisk, transport)
|
|
|
213
238
|
choices.push({ value: 'skip', name: 'Skip (tell model why)' }, { value: 'cancel', name: 'Cancel turn' });
|
|
214
239
|
// Part 2b D7 — the process a BAdI implementation runs in, under the verb line.
|
|
215
240
|
const affects = affectsLine(change);
|
|
241
|
+
// Part 3 D12 — a release card's facts, under the verb line.
|
|
242
|
+
const release = releaseLines(change).map((l) => `\n ${chalk.dim(l)}`).join('');
|
|
216
243
|
const choice = await withInquirer(() => select({
|
|
217
|
-
message: `${objLabel} — apply?${affects ? `\n ${chalk.dim(affects)}` : ''}`,
|
|
244
|
+
message: `${objLabel} — apply?${affects ? `\n ${chalk.dim(affects)}` : ''}${release}`,
|
|
218
245
|
default: 'apply',
|
|
219
246
|
theme: inquirerTheme,
|
|
220
247
|
choices,
|
|
@@ -48,8 +48,18 @@ export function effectiveRisk(declared, changes, ctx) {
|
|
|
48
48
|
// Round 4 item 8a — these were both labelled 'shared_transport', so a plan
|
|
49
49
|
// on a transport created seconds earlier read "raised … by shared_transport".
|
|
50
50
|
// Each reason now says what actually raised it.
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// Part 3 D2 — a release cannot be undone, and a run executes code on the
|
|
52
|
+
// system: both are high (never auto-approved), and the reason is always shown.
|
|
53
|
+
if (changes.some((c) => c.op === 'release')) {
|
|
54
|
+
raise('high', 'transport_release');
|
|
55
|
+
if (!raisedBy.includes('transport_release'))
|
|
56
|
+
raisedBy.push('transport_release');
|
|
57
|
+
}
|
|
58
|
+
if (changes.some((c) => c.op === 'run')) {
|
|
59
|
+
raise('high', 'code_run');
|
|
60
|
+
if (!raisedBy.includes('code_run'))
|
|
61
|
+
raisedBy.push('code_run');
|
|
62
|
+
}
|
|
53
63
|
if (changes.some((c) => c.op === 'activate'))
|
|
54
64
|
raise('medium', 'activation');
|
|
55
65
|
if (ctx.recentAtcFailure && changes.some((c) => ctx.recentAtcFailure.includes(c.object)))
|
|
@@ -66,7 +76,7 @@ export function effectiveRisk(declared, changes, ctx) {
|
|
|
66
76
|
const RISK_REASON_TEXT = {
|
|
67
77
|
non_prod_override: 'auto-approve is set to never on this system (a setting, not a risk)',
|
|
68
78
|
delete_op: 'the plan deletes an object',
|
|
69
|
-
transport_release: 'the plan releases a transport',
|
|
79
|
+
transport_release: 'the plan releases a transport; this cannot be undone',
|
|
70
80
|
activation: 'activation makes the change live on the system',
|
|
71
81
|
atc_failed_24h: 'ATC failed on this object in the last 24 hours',
|
|
72
82
|
shared_transport: 'the transport is shared by 3 or more developers',
|
|
@@ -74,6 +84,7 @@ const RISK_REASON_TEXT = {
|
|
|
74
84
|
prod_namespace: 'the object is in a production namespace',
|
|
75
85
|
syntax_errors: 'the last syntax check of this object found errors',
|
|
76
86
|
standard_process: 'this code runs inside a standard SAP process (a BAdI implementation)',
|
|
87
|
+
code_run: 'this runs code on the SAP system',
|
|
77
88
|
};
|
|
78
89
|
/** Round 8 item 4 — every code the floor can raise (each has plain words above). */
|
|
79
90
|
export const RISK_REASON_CODES = Object.keys(RISK_REASON_TEXT);
|
package/dist/one-shot.js
CHANGED
|
@@ -44,6 +44,7 @@ import { globalCredits } from './cost/credits.js';
|
|
|
44
44
|
import './tools/sap-read.js';
|
|
45
45
|
import './tools/sap-write.js';
|
|
46
46
|
import './tools/transport.js';
|
|
47
|
+
import './tools/sap-run.js';
|
|
47
48
|
import './tools/approval.js';
|
|
48
49
|
import './tools/snapshot.js';
|
|
49
50
|
import './tools/ask-question.js';
|
|
@@ -115,8 +115,15 @@ const MESSAGES = {
|
|
|
115
115
|
'Finding modifiable TRs…',
|
|
116
116
|
],
|
|
117
117
|
sap_transport_release: [
|
|
118
|
-
'Releasing
|
|
119
|
-
'
|
|
118
|
+
'Releasing the open tasks, then the request…',
|
|
119
|
+
'Waiting for the release report from SAP…',
|
|
120
|
+
],
|
|
121
|
+
sap_transport_read: [
|
|
122
|
+
'Reading the transport request…',
|
|
123
|
+
],
|
|
124
|
+
sap_run: [
|
|
125
|
+
'Running {name} on SAP…',
|
|
126
|
+
'Waiting for the output…',
|
|
120
127
|
],
|
|
121
128
|
sap_create_object: [
|
|
122
129
|
'Creating {name}…',
|
|
@@ -180,6 +180,8 @@ const RULES = {
|
|
|
180
180
|
sap_transport_for_object: (a) => ({ verb: 'Transport for', object: shortRight(str(a['object_name']) ?? '') }),
|
|
181
181
|
sap_transport_list: (a) => ({ verb: 'Listed transports', object: shortRight(str(a['owner']) ?? '') }),
|
|
182
182
|
sap_transport_release: (a) => ({ verb: 'Released', object: shortRight(str(a['transport']) ?? '') }),
|
|
183
|
+
sap_transport_read: (a) => ({ verb: 'Read transport request', object: shortRight(str(a['transport']) ?? '') }),
|
|
184
|
+
sap_run: sapObject('Ran'),
|
|
183
185
|
sap_create_domain: sapObject('Created domain'),
|
|
184
186
|
sap_create_data_element: sapObject('Created data element'),
|
|
185
187
|
sap_lock: sapObject('Locked'),
|
|
@@ -271,7 +273,7 @@ const PRESENT = {
|
|
|
271
273
|
'Read snapshot': 'Reading snapshot of', 'Cleaned snapshots': 'Cleaning snapshots',
|
|
272
274
|
'Created transport': 'Creating transport', 'Transport for': 'Finding transport for',
|
|
273
275
|
'Listed transports': 'Listing transports', Released: 'Releasing', 'Created domain': 'Creating domain',
|
|
274
|
-
'Created data element': 'Creating data element', 'Read BAdI': 'Reading BAdI', 'Read enhancement options': 'Reading enhancement options of', 'Created BAdI implementation': 'Creating BAdI implementation', 'Created table type': 'Creating table type', 'Read table type': 'Reading table type', 'Listed function group': 'Listing function group', Locked: 'Locking', Unlocked: 'Unlocking',
|
|
276
|
+
'Created data element': 'Creating data element', 'Read BAdI': 'Reading BAdI', 'Read enhancement options': 'Reading enhancement options of', 'Created BAdI implementation': 'Creating BAdI implementation', 'Created table type': 'Creating table type', 'Read table type': 'Reading table type', 'Read transport request': 'Reading transport request', 'Listed function group': 'Listing function group', Locked: 'Locking', Unlocked: 'Unlocking',
|
|
275
277
|
Published: 'Publishing', Unpublished: 'Unpublishing', Messages: 'Maintaining messages in',
|
|
276
278
|
'Number range': 'Maintaining number range', Question: 'Asking', 'Updated todos': 'Updating todos',
|
|
277
279
|
'Ran skill': 'Running skill', 'Read document': 'Reading document', Fetched: 'Fetching',
|
package/dist/repl.js
CHANGED
|
@@ -152,6 +152,7 @@ import { applyLocalBuildConfig, applyStandaloneFileTools } from './tools/local-b
|
|
|
152
152
|
import './tools/sap-read.js';
|
|
153
153
|
import './tools/sap-write.js';
|
|
154
154
|
import './tools/transport.js';
|
|
155
|
+
import './tools/sap-run.js';
|
|
155
156
|
import './tools/approval.js';
|
|
156
157
|
import './tools/snapshot.js';
|
|
157
158
|
import './tools/ask-question.js';
|
|
@@ -35,6 +35,11 @@ export const FEATURES = Object.freeze({
|
|
|
35
35
|
'fugr.modules': { label: 'Function modules and function-group includes', collection: `${A}/functions/groups`, provenOnOurBox: true },
|
|
36
36
|
'prog.includes': { label: 'Program includes', collection: `${A}/programs/includes`, provenOnOurBox: true },
|
|
37
37
|
'ddic.tabletypes': { label: 'Table types', collection: `${A}/ddic/tabletypes`, provenOnOurBox: true },
|
|
38
|
+
// Part 3 (D15): no `since` (no cited release). The collections and the reads are
|
|
39
|
+
// proven on our box; the release is not, and these rows are about the collection.
|
|
40
|
+
'run.classrun': { label: 'Run a class (console)', collection: `${A}/oo/classrun`, provenOnOurBox: true },
|
|
41
|
+
'run.programrun': { label: 'Run a program', collection: `${A}/programs/programrun`, provenOnOurBox: true },
|
|
42
|
+
'cts.requests': { label: 'Transport requests (read, release)', collection: `${A}/cts/transportrequests`, provenOnOurBox: true },
|
|
38
43
|
});
|
|
39
44
|
export const FEATURE_IDS = Object.freeze(Object.keys(FEATURES));
|
|
40
45
|
export function isFeatureId(s) {
|
|
@@ -62,6 +62,8 @@ const WRITE_TOOLS = new Set([
|
|
|
62
62
|
// Final review I2 — SAP's RAP generator (tools/rap/generate-objects.ts) creates
|
|
63
63
|
// the skeleton objects in one call; its row names them.
|
|
64
64
|
'sap_generate_objects',
|
|
65
|
+
// Part 3 — a run executes code on the SAP system under an approval: it is audited.
|
|
66
|
+
'sap_run',
|
|
65
67
|
]);
|
|
66
68
|
/**
|
|
67
69
|
* Non-mutating tools that are interaction/plumbing, not object reads — excluded
|
|
@@ -156,6 +158,29 @@ function approvalCell(rec) {
|
|
|
156
158
|
return `declined ✗${risk}`;
|
|
157
159
|
}
|
|
158
160
|
// ── Object / snapshot extraction ──────────────────────────────────────────
|
|
161
|
+
/**
|
|
162
|
+
* The result as JSON, or its first line as JSON (a run's facts line), or the
|
|
163
|
+
* top-level fields a spilled preview still shows ("status": "stopped" comes
|
|
164
|
+
* before steps[] in the release envelope). {} when none of these read.
|
|
165
|
+
*/
|
|
166
|
+
function parseResultHead(result) {
|
|
167
|
+
const text = result ?? '';
|
|
168
|
+
try {
|
|
169
|
+
return asRecord(JSON.parse(text));
|
|
170
|
+
}
|
|
171
|
+
catch { /* next */ }
|
|
172
|
+
try {
|
|
173
|
+
return asRecord(JSON.parse(text.split('\n', 1)[0] ?? ''));
|
|
174
|
+
}
|
|
175
|
+
catch { /* next */ }
|
|
176
|
+
const top = (k) => new RegExp(`^ {2}"${k}": "([^"]+)"`, 'm').exec(text)?.[1];
|
|
177
|
+
const status = top('status');
|
|
178
|
+
if (!status)
|
|
179
|
+
return {};
|
|
180
|
+
const at = top('stopped_at');
|
|
181
|
+
const verdict = top('stop_verdict');
|
|
182
|
+
return { status, ...(at ? { stopped_at: at } : {}), ...(verdict ? { stop_verdict: verdict } : {}) };
|
|
183
|
+
}
|
|
159
184
|
function writeObject(tool, args, result) {
|
|
160
185
|
switch (tool) {
|
|
161
186
|
case 'sap_transport_create': {
|
|
@@ -180,8 +205,31 @@ function writeObject(tool, args, result) {
|
|
|
180
205
|
// Last resort: name the operation, not the raw description.
|
|
181
206
|
return 'transport';
|
|
182
207
|
}
|
|
183
|
-
case 'sap_transport_release':
|
|
184
|
-
|
|
208
|
+
case 'sap_transport_release': {
|
|
209
|
+
// Part 3 (D17): the row carries SAP's verdict; a stop names its step.
|
|
210
|
+
const tr = str(args.transport) ?? '—';
|
|
211
|
+
const r = parseResultHead(result);
|
|
212
|
+
const status = str(r.status);
|
|
213
|
+
if (!status)
|
|
214
|
+
return tr;
|
|
215
|
+
if (status === 'stopped') {
|
|
216
|
+
const last = Array.isArray(r.steps) && r.steps.length > 0 ? asRecord(r.steps[r.steps.length - 1]) : {};
|
|
217
|
+
const at = str(r.stopped_at) ?? str(last.number);
|
|
218
|
+
const verdict = str(r.stop_verdict) ?? str(last.verdict);
|
|
219
|
+
if (at && verdict)
|
|
220
|
+
return `${tr} · stopped at ${at}: ${verdict}`;
|
|
221
|
+
}
|
|
222
|
+
return `${tr} · ${status}`;
|
|
223
|
+
}
|
|
224
|
+
case 'sap_run': {
|
|
225
|
+
// Part 3: `CLAS ZCL_X · ran` or the error code (the facts line is line 1 of a ran result).
|
|
226
|
+
const r = parseResultHead(result);
|
|
227
|
+
const what = [str(args.type), str(args.name)].filter(Boolean).join(' ') || '—';
|
|
228
|
+
if (r.ran === true)
|
|
229
|
+
return `${what} · ran`;
|
|
230
|
+
const err = str(r.error);
|
|
231
|
+
return err ? `${what} · ${err}` : what;
|
|
232
|
+
}
|
|
185
233
|
case 'sap_update_method': {
|
|
186
234
|
const cls = str(args.class_name) ?? '—';
|
|
187
235
|
const m = str(args.method_name);
|
package/dist/tools/approval.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { notModifiableText } from './release-texts.js';
|
|
3
|
+
import { isModifiable, knownObjectCount } from '@cspeach/sap-client';
|
|
2
4
|
import { registerTool } from './index.js';
|
|
3
5
|
import { isHeadless, shouldUseInk } from '../renderer/tty.js';
|
|
4
6
|
import { effectiveRisk, riskReasonText, typeCode } from '../approvals/risk-floor.js';
|
|
@@ -120,6 +122,55 @@ function isPureInfraEscalation(changes) {
|
|
|
120
122
|
changes.every((c) => c.op === 'create' &&
|
|
121
123
|
INFRA_BASE_OBJECT_TYPES.has((c.type ?? '').trim().toUpperCase())));
|
|
122
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Part 3 D12 — for every release change: drop any model-supplied facts, read
|
|
127
|
+
* the request from SAP and attach SAP's facts, or return the refusal. There is
|
|
128
|
+
* no separate "no SAP" branch: standalone's `ctx.adt` is a Proxy whose every
|
|
129
|
+
* method throws the standalone text, so the throw path IS the standalone path.
|
|
130
|
+
*/
|
|
131
|
+
async function attachReleaseFacts(changes, ctx) {
|
|
132
|
+
for (const c of changes) {
|
|
133
|
+
if (!c)
|
|
134
|
+
continue;
|
|
135
|
+
// Never the model's facts — on any change.
|
|
136
|
+
delete c.releaseFacts;
|
|
137
|
+
if (c.op !== 'release')
|
|
138
|
+
continue;
|
|
139
|
+
if (typeCode(c.type) !== 'CTS') {
|
|
140
|
+
return { error: 'release_type', message: 'A release change has type CTS and names the request number.' };
|
|
141
|
+
}
|
|
142
|
+
const asked = String(c.object ?? '').trim();
|
|
143
|
+
let doc;
|
|
144
|
+
try {
|
|
145
|
+
doc = await ctx.adt.transportRead(asked);
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
149
|
+
return {
|
|
150
|
+
error: 'release_facts_unavailable',
|
|
151
|
+
message: `CSPeach could not read ${asked}, so it cannot show what a release would send. (${why})`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (doc.kind === 'task') {
|
|
155
|
+
return {
|
|
156
|
+
error: 'task_number',
|
|
157
|
+
message: `${asked} is a task of ${doc.number}. Give the request number; CSPeach releases the tasks first, then the request.`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (!isModifiable(doc.status)) {
|
|
161
|
+
return { error: 'not_modifiable', message: notModifiableText(doc) };
|
|
162
|
+
}
|
|
163
|
+
c.releaseFacts = {
|
|
164
|
+
request: doc.number, description: doc.description, owner: doc.owner, target: doc.target,
|
|
165
|
+
status: doc.status, statusText: doc.statusText,
|
|
166
|
+
tasks: doc.tasks.map((t) => ({ number: t.number, owner: t.owner })),
|
|
167
|
+
// The largest count the document supports; null = could not count
|
|
168
|
+
// (the card says so, never "0 objects").
|
|
169
|
+
objects: knownObjectCount(doc),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
123
174
|
registerTool({
|
|
124
175
|
name: 'request_approval',
|
|
125
176
|
description: 'Request user approval for one or more SAP mutations. Returns approval_ids (one per change) to be passed as approval_id on matching mutating tool calls.',
|
|
@@ -135,7 +186,7 @@ registerTool({
|
|
|
135
186
|
items: {
|
|
136
187
|
type: 'object',
|
|
137
188
|
properties: {
|
|
138
|
-
op: { type: 'string', enum: ['create', 'modify', 'delete', 'activate', 'release'] },
|
|
189
|
+
op: { type: 'string', enum: ['create', 'modify', 'delete', 'activate', 'release', 'run'] },
|
|
139
190
|
object: { type: 'string' },
|
|
140
191
|
type: { type: 'string' },
|
|
141
192
|
diff: { type: 'string', description: 'For a CREATE op this MUST be the exact, complete source that will be written — verbatim, character-for-character, NOT a description, summary, or paraphrase. It is shown to the developer in the approval box as the exact thing they are approving, so a one-line summary here means they approve a write they cannot inspect. For a MODIFY op, the real unified diff of the change (again verbatim, not a description).' },
|
|
@@ -216,6 +267,13 @@ registerTool({
|
|
|
216
267
|
if (getEffectiveWriteMode(cfg.write_mode, role) === 'advisory-only') {
|
|
217
268
|
return handleAdvisoryApproval(args, { sapAlias: ctx.sapAlias });
|
|
218
269
|
}
|
|
270
|
+
// Part 3 D12 — a release card carries SAP's own facts about the request,
|
|
271
|
+
// read here before any card or nag; never the model's. After the advisory
|
|
272
|
+
// short-circuit (F-10: advisory mints nothing, so no read is spent on a
|
|
273
|
+
// card that is never drawn). Every refusal mints nothing.
|
|
274
|
+
const releaseRefusal = await attachReleaseFacts(args.changes ?? [], ctx);
|
|
275
|
+
if (releaseRefusal)
|
|
276
|
+
return { content: JSON.stringify(releaseRefusal), is_error: true };
|
|
219
277
|
let sapCfg = cfg.sap[ctx.sapAlias];
|
|
220
278
|
const riskCtx = {
|
|
221
279
|
sapAlias: ctx.sapAlias,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Part 3 — release refusal texts shared by the approval card (approval.ts) and
|
|
3
|
+
* sap_transport_release (transport.ts). Same words as sap-client's own refusal
|
|
4
|
+
* in transportRelease; an empty status never prints as an empty string.
|
|
5
|
+
*/
|
|
6
|
+
export function notModifiableText(doc) {
|
|
7
|
+
return `${doc.number} cannot be released: its status is "${doc.statusText || `status ${doc.status || 'unknown'}`}". `
|
|
8
|
+
+ 'Only a modifiable request can be released. Nothing was sent.';
|
|
9
|
+
}
|
package/dist/tools/sap-read.js
CHANGED
|
@@ -29,6 +29,7 @@ import { byObjectTypeForRead } from '../sap/profile-gate.js';
|
|
|
29
29
|
import { normaliseSyntaxResult, checkSyntax } from './verify.js';
|
|
30
30
|
import { recordSyntaxCheck } from './syntax-state.js';
|
|
31
31
|
import { spillToolResult } from './result-spill.js';
|
|
32
|
+
import { hasManualLocks } from '../sap/session-end.js';
|
|
32
33
|
import { SapError, parseFunctionModuleDocument, parseContainerDocument } from '@cspeach/sap-client';
|
|
33
34
|
// -----------------------------------------------------------------------
|
|
34
35
|
// Task 17 (spec D12, ruling F18) — model-facing formats.
|
|
@@ -719,8 +720,11 @@ registerTool({
|
|
|
719
720
|
required: ['name', 'type'],
|
|
720
721
|
},
|
|
721
722
|
handler: async (args, ctx) => {
|
|
722
|
-
// runUnitTest(type, name) — type first
|
|
723
|
-
|
|
723
|
+
// runUnitTest(type, name, opts) — type first. D18: under a manual lock
|
|
724
|
+
// (sap_lock) the stateful session is kept, so the lock survives exactly as
|
|
725
|
+
// before; otherwise sap-client ends an open session first (a stateful
|
|
726
|
+
// session runs the old load, probe 2026-10-17 §2.3).
|
|
727
|
+
const result = await ctx.adt.runUnitTest(args.type, args.name, { keepSession: hasManualLocks() });
|
|
724
728
|
// B2 false-green guard: a run in which no test actually ran reads to a
|
|
725
729
|
// caller like "nothing failed → green". Wrap the (still-carried) raw result
|
|
726
730
|
// in an explicit UNVERIFIED envelope when:
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADT gap program, Part 3 — sap_run: run one of the person's own classes
|
|
3
|
+
* (IF_OO_ADT_CLASSRUN) or programs and show the output (plan D1–D8, D18).
|
|
4
|
+
*
|
|
5
|
+
* Order (each refusal before anything is spent or sent where it can be):
|
|
6
|
+
* 0. Z/Y names only (D3), before the approval.
|
|
7
|
+
* 0b. Never while a sap_lock lock is held (D18): ending the session would
|
|
8
|
+
* drop the lock, keeping it would run the old load. Before the approval.
|
|
9
|
+
* 1. One approval of op `run` per run (D2): a plan-grant id never runs, and
|
|
10
|
+
* the approval's type must be the type asked for (a CLAS approval never
|
|
11
|
+
* runs a PROG of the same name; the jwt checks object/op/nonce only).
|
|
12
|
+
* 2. WAL pending entry, as sap_activate writes one.
|
|
13
|
+
* 3. runClass / runProgram (sap-client reads SAP's answer: HTTP 200 alone
|
|
14
|
+
* is never "ran"; a 500 page is a dump with its window).
|
|
15
|
+
* 4. WAL finalized.
|
|
16
|
+
* 5. The result: the facts line, then the output, spilled when long (D7).
|
|
17
|
+
*/
|
|
18
|
+
import crypto from 'node:crypto';
|
|
19
|
+
import { registerTool } from './index.js';
|
|
20
|
+
import { verifyAndSpendApprovalId } from '../approvals/jwt.js';
|
|
21
|
+
import { typeCode } from '../approvals/risk-floor.js';
|
|
22
|
+
import { ERR } from '../errors/codes.js';
|
|
23
|
+
import { appendPending, finalizeToolCall } from '../session/pending.js';
|
|
24
|
+
import { hasManualLocks } from '../sap/session-end.js';
|
|
25
|
+
import { spillToolResult } from './result-spill.js';
|
|
26
|
+
import { isCustomerRunnable, notCustomerText, runUrl, SapError } from '@cspeach/sap-client';
|
|
27
|
+
const MANUAL_LOCK_TEXT = 'A lock you took with sap_lock is still held. Unlock it (sap_unlock) first, then ask for the run again.';
|
|
28
|
+
/** Every line right-trimmed (CR included), trailing blank lines dropped. */
|
|
29
|
+
function rightTrimmed(output) {
|
|
30
|
+
return output.split(/\r?\n/).map((l) => l.trimEnd()).join('\n').trimEnd();
|
|
31
|
+
}
|
|
32
|
+
registerTool({
|
|
33
|
+
name: 'sap_run',
|
|
34
|
+
description: 'Run one of your own classes (it must implement IF_OO_ADT_CLASSRUN and be active) or one of your own programs, '
|
|
35
|
+
+ 'and show the output. Only names starting with Z or Y. A program runs with its selection-screen defaults; '
|
|
36
|
+
+ 'values cannot be passed. The run is on the active version. Requires approval_id from a request_approval '
|
|
37
|
+
+ 'change of op `run` (one approval per run; never auto-approved). When SAP answered but did not run anything, '
|
|
38
|
+
+ 'the result says `not_runnable`; when the run dumped, the result says `run_dumped` and gives the window for '
|
|
39
|
+
+ 'sap_short_dump. Long output is saved to a file and previewed.',
|
|
40
|
+
isMutating: true,
|
|
41
|
+
requiresSap: true,
|
|
42
|
+
requires: (a) => (String(a.type ?? '').trim().toUpperCase() === 'PROG' ? 'run.programrun' : 'run.classrun'),
|
|
43
|
+
input_schema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
type: { type: 'string', enum: ['CLAS', 'PROG'], description: 'CLAS for a class run (console), PROG for a program run' },
|
|
47
|
+
name: { type: 'string', description: 'Class or program name, e.g. ZCL_ORDER_DEMO' },
|
|
48
|
+
approval_id: { type: 'string', description: 'JWT from request_approval (a change of op run)' },
|
|
49
|
+
},
|
|
50
|
+
required: ['type', 'name', 'approval_id'],
|
|
51
|
+
},
|
|
52
|
+
handler: async (args, ctx) => {
|
|
53
|
+
const type = String(args.type ?? '').trim().toUpperCase();
|
|
54
|
+
const rawName = String(args.name ?? '').trim();
|
|
55
|
+
const name = rawName.toUpperCase();
|
|
56
|
+
if (type !== 'CLAS' && type !== 'PROG') {
|
|
57
|
+
return { content: JSON.stringify({ error: 'run_refused', message: 'sap_run runs a class (type CLAS) or a program (type PROG).' }), is_error: true };
|
|
58
|
+
}
|
|
59
|
+
// ── 0. Own objects only (D3) — before the approval is spent ────────────
|
|
60
|
+
// runUrl applies the same rule sap-client applies (and refuses a name that is
|
|
61
|
+
// not letters, digits and underscores), so nothing it would refuse after the
|
|
62
|
+
// spend gets past this point.
|
|
63
|
+
let refusal;
|
|
64
|
+
if (!rawName || !isCustomerRunnable(rawName))
|
|
65
|
+
refusal = notCustomerText(rawName);
|
|
66
|
+
else {
|
|
67
|
+
try {
|
|
68
|
+
runUrl(type, rawName);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
refusal = e instanceof Error ? e.message : String(e);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (refusal) {
|
|
75
|
+
return { content: JSON.stringify({ error: 'run_refused', message: refusal }), is_error: true };
|
|
76
|
+
}
|
|
77
|
+
// ── 0b. Never under a manual lock (D18) — before the approval ──────────
|
|
78
|
+
if (hasManualLocks()) {
|
|
79
|
+
return { content: JSON.stringify({ error: 'manual_lock_held', message: MANUAL_LOCK_TEXT }), is_error: true };
|
|
80
|
+
}
|
|
81
|
+
// ── 1. The approval: op run, one per run, never a plan grant (D2) ──────
|
|
82
|
+
const verify = await verifyAndSpendApprovalId(args.approval_id, name, 'run');
|
|
83
|
+
if (!verify.ok) {
|
|
84
|
+
return { content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }), is_error: true };
|
|
85
|
+
}
|
|
86
|
+
if (verify.payload.grant) {
|
|
87
|
+
return { content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: 'grant_cannot_run' }), is_error: true };
|
|
88
|
+
}
|
|
89
|
+
if (typeCode(verify.payload.type) !== type) {
|
|
90
|
+
return { content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: 'type_mismatch' }), is_error: true };
|
|
91
|
+
}
|
|
92
|
+
// ── 2. WAL ─────────────────────────────────────────────────────────────
|
|
93
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
94
|
+
const startedAt = Date.now();
|
|
95
|
+
await appendPending(ctx.session, {
|
|
96
|
+
tool_use_id: toolUseId,
|
|
97
|
+
tool: 'sap_run',
|
|
98
|
+
args: { type, name },
|
|
99
|
+
started_at: new Date().toISOString(),
|
|
100
|
+
});
|
|
101
|
+
const line = (kind, ms) => ctx.chunkEmitter?.emit('info', `run ${type} ${name} · ${kind} · ${ms} ms`);
|
|
102
|
+
// ── 3. The run ─────────────────────────────────────────────────────────
|
|
103
|
+
let r;
|
|
104
|
+
try {
|
|
105
|
+
r = type === 'PROG' ? await ctx.adt.runProgram(name) : await ctx.adt.runClass(name);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
109
|
+
// sap-client's own Z/Y refusal (400) cannot reach here (step 0 ran first).
|
|
110
|
+
// Anything else (a lost connection) may have run: say so, never "refused".
|
|
111
|
+
const refused = err instanceof SapError && err.httpStatus === 400;
|
|
112
|
+
const body = refused
|
|
113
|
+
? { error: 'run_refused', message }
|
|
114
|
+
: { error: 'run_failed', type, name, detail: message };
|
|
115
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(body), true, Date.now() - startedAt);
|
|
116
|
+
line(refused ? 'refused' : 'failed', Date.now() - startedAt);
|
|
117
|
+
return { content: JSON.stringify(body), is_error: true };
|
|
118
|
+
}
|
|
119
|
+
// ── 4/5. WAL closed, the result ────────────────────────────────────────
|
|
120
|
+
if (r.kind === 'ran') {
|
|
121
|
+
const facts = JSON.stringify({
|
|
122
|
+
ran: true, type, name: r.name, lines: r.lines, chars: r.outputBytes,
|
|
123
|
+
elapsed_ms: r.elapsedMs, session_ended: r.sessionEnded,
|
|
124
|
+
});
|
|
125
|
+
await finalizeToolCall(ctx.session, toolUseId, facts, false, Date.now() - startedAt);
|
|
126
|
+
line('ran', r.elapsedMs);
|
|
127
|
+
// The facts line is the first line of the content (not a spill header):
|
|
128
|
+
// the file starts with it too, so tool_output_read line 1 is the facts.
|
|
129
|
+
const out = rightTrimmed(r.output);
|
|
130
|
+
return spillToolResult({ content: out ? `${facts}\n${out}` : facts }, ctx, 'shell', { label: `run ${r.name}` });
|
|
131
|
+
}
|
|
132
|
+
let body;
|
|
133
|
+
if (r.kind === 'not_runnable') {
|
|
134
|
+
body = {
|
|
135
|
+
error: 'not_runnable', type, name: r.name, sap_said: r.sapText, reason: r.reason,
|
|
136
|
+
next_step: 'activate the object (sap_activate) or fix the name, then ask for a new run approval',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
else if (r.kind === 'dumped') {
|
|
140
|
+
const d = r.dump;
|
|
141
|
+
body = {
|
|
142
|
+
error: 'run_dumped', type, name: r.name, message: d?.message,
|
|
143
|
+
...(d?.serverTime ? { sap_server_time: d.serverTime } : {}),
|
|
144
|
+
...(d?.window ? { dump_window: { from: d.window.from, to: d.window.to } } : {}),
|
|
145
|
+
note: 'Output written before the exception is lost; the dump has the stack.',
|
|
146
|
+
next_step: d?.window
|
|
147
|
+
? `sap_short_dump with fromDate and toDate = dump_window (SAP system time), then operation detail on the newest dump whose program names ${r.name}`
|
|
148
|
+
: `sap_short_dump (list), then operation detail on the newest dump whose program names ${r.name}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
body = {
|
|
153
|
+
error: r.reason?.startsWith('No answer') ? 'run_timed_out' : 'run_failed', type, name: r.name,
|
|
154
|
+
...(r.httpStatus !== undefined ? { http_status: r.httpStatus } : {}),
|
|
155
|
+
detail: r.reason ?? r.sapText,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const content = JSON.stringify(body);
|
|
159
|
+
await finalizeToolCall(ctx.session, toolUseId, content, true, Date.now() - startedAt);
|
|
160
|
+
line(r.kind, r.elapsedMs);
|
|
161
|
+
return { content, is_error: true };
|
|
162
|
+
},
|
|
163
|
+
});
|
|
@@ -41,7 +41,7 @@ const FIORI_FAMILY = [
|
|
|
41
41
|
'fiori_sample_get', 'fiori_sample_search', 'fiori_scaffold', 'fiori_scaffold_fe',
|
|
42
42
|
];
|
|
43
43
|
const TRANSPORT_FAMILY = [
|
|
44
|
-
'sap_transport_create', 'sap_transport_for_object', 'sap_transport_list', 'sap_transport_release',
|
|
44
|
+
'sap_transport_create', 'sap_transport_for_object', 'sap_transport_list', 'sap_transport_read', 'sap_transport_release',
|
|
45
45
|
];
|
|
46
46
|
/**
|
|
47
47
|
* Per-skill starter tools: the non-core tools a skill's body names as its
|
|
@@ -65,14 +65,15 @@ export const SKILL_STARTERS = Object.freeze({
|
|
|
65
65
|
'abap-impact': ['sap_usage_references', 'sap_atc_run'],
|
|
66
66
|
'abap-performance': ['sap_usage_references', 'sap_atc_run'],
|
|
67
67
|
'abap-migrate': ['sap_usage_references'],
|
|
68
|
-
|
|
69
|
-
'abap-
|
|
70
|
-
'abap-
|
|
71
|
-
'abap-
|
|
68
|
+
// Part 3 — the skills whose body says "run it" (Task 7's run block) also start with sap_run.
|
|
69
|
+
'abap-dump': ['sap_short_dump', 'sap_snapshot_take', 'sap_run'],
|
|
70
|
+
'abap-incident': ['sap_short_dump', 'sap_snapshot_take', 'sap_atc_run', 'sap_run_unit_test', 'sap_api_state', 'sap_diff', 'sap_run'],
|
|
71
|
+
'abap-eml': ['sap_run_unit_test', 'sap_run'],
|
|
72
|
+
'abap-test': ['sap_run_unit_test', 'sap_set_class_include', 'sap_run'],
|
|
72
73
|
'abap-extend-model': ['extend_model_insert', 'sap_snapshot_take', 'sap_service_binding_publish', 'sap_set_class_include'],
|
|
73
74
|
// RAP generator (2026-10-14): SAP's generator writes the stack; the finish check proves F1–F5.
|
|
74
75
|
// Final review M5 — standalone /abap-rap calls rap_skeleton_resolve before it builds.
|
|
75
|
-
'abap-rap': ['sap_set_class_include', 'sap_service_binding_publish', 'sap_class_includes', 'rap_skeleton_resolve', 'sap_generate_objects', 'rap_finish_check'],
|
|
76
|
+
'abap-rap': ['sap_set_class_include', 'sap_service_binding_publish', 'sap_class_includes', 'rap_skeleton_resolve', 'sap_generate_objects', 'rap_finish_check', 'sap_run'],
|
|
76
77
|
// Plan steps load tools by the PLAN skill (session.tool_set_skill / params.skill), not the
|
|
77
78
|
// delegate (which only rides as the X-CSPeach-Delegate-Skill header), so the RAP
|
|
78
79
|
// skeleton + finish phases need the generator and the finish check here too.
|
|
@@ -84,7 +85,7 @@ export const SKILL_STARTERS = Object.freeze({
|
|
|
84
85
|
'abap-upgrade-verify': ['sap_atc_run'],
|
|
85
86
|
// Selection texts (2026-10-04): a new PARAMETERS / SELECT-OPTIONS gets its text in the same change.
|
|
86
87
|
'abap-refactor': ['sap_get_text_elements', 'sap_set_text_elements'],
|
|
87
|
-
'abap-generate': ['sap_get_text_elements', 'sap_set_text_elements'],
|
|
88
|
+
'abap-generate': ['sap_get_text_elements', 'sap_set_text_elements', 'sap_run'],
|
|
88
89
|
});
|
|
89
90
|
/** The starter tools for a skill; unknown skill (or none) ⇒ []. */
|
|
90
91
|
export function startersForSkill(skill) {
|
package/dist/tools/transport.js
CHANGED
|
@@ -7,14 +7,21 @@
|
|
|
7
7
|
* transportList(user?, status?) → Promise<TransportListResult> line 1048
|
|
8
8
|
* TransportListResult { filterUser, filterStatus, transports: TransportEntry[] }
|
|
9
9
|
* TransportEntry { transportNumber, description?, owner?, status?, type?, target? }
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* transportRead(number) → Promise<TransportRequestDoc> (Part 3)
|
|
11
|
+
* transportRelease(number) → Promise<TransportReleaseResult> (Part 3)
|
|
12
|
+
* status 'released' only on SAP's verdict for every step AND a status read back of R/N.
|
|
12
13
|
*/
|
|
13
14
|
import { bindGrantTransport, isPlanGrantInScope } from '../approvals/plan-grant.js';
|
|
14
15
|
import { registerTool } from './index.js';
|
|
15
16
|
import { verifyAndSpendApprovalId } from '../approvals/jwt.js';
|
|
16
|
-
import { auditLog } from '@cspeach/sap-client';
|
|
17
|
+
import { auditLog, SapNotFoundError, isModifiable, knownObjectCount, allObjectNames } from '@cspeach/sap-client';
|
|
18
|
+
import { typeCode } from '../approvals/risk-floor.js';
|
|
19
|
+
import { spillToolResult } from './result-spill.js';
|
|
20
|
+
import { notModifiableText } from './release-texts.js';
|
|
21
|
+
import { RELEASE_CHECK_LINE } from '../approvals/op-labels.js';
|
|
17
22
|
import { ERR } from '../errors/codes.js';
|
|
23
|
+
import { presentSafetyConfirmation } from '../repl/safety-confirm.js';
|
|
24
|
+
import { isSafetyConfirmEnabled } from '../repl/safety-mode-state.js';
|
|
18
25
|
import { setCurrentTransport, noteConfirmedTransport } from '../repl/current-transport.js';
|
|
19
26
|
import { enrichTransportList } from './transport-fit.js';
|
|
20
27
|
import { reusedRequestRefusal } from './transport-fit-check.js';
|
|
@@ -472,40 +479,259 @@ registerTool({
|
|
|
472
479
|
}
|
|
473
480
|
},
|
|
474
481
|
});
|
|
482
|
+
// ── sap_transport_read ───────────────────────────────────────────────────────
|
|
483
|
+
//
|
|
484
|
+
// Part 3 (D16) — one transport request as SAP's transport organizer shows it.
|
|
485
|
+
// Read only, no approval.
|
|
486
|
+
/** The 404 text, never SAP's generic "Object not found". */
|
|
487
|
+
function notFoundText(n) {
|
|
488
|
+
return `Transport request ${n} does not exist.`;
|
|
489
|
+
}
|
|
490
|
+
/** Gate 0 of the release and the whole of the read: SAP's own document, or the refusal envelope. */
|
|
491
|
+
async function readRequest(ctx, asked) {
|
|
492
|
+
try {
|
|
493
|
+
return { doc: await ctx.adt.transportRead(asked) };
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
const body = err instanceof SapNotFoundError
|
|
497
|
+
? { error: 'not_found', detail: notFoundText(asked) }
|
|
498
|
+
: { error: 'transport_read_failed', detail: err instanceof Error ? err.message : String(err) };
|
|
499
|
+
return { refusal: { content: JSON.stringify(body), is_error: true } };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
registerTool({
|
|
503
|
+
name: 'sap_transport_read',
|
|
504
|
+
description: 'Read one transport request: status, owner, target, its tasks and how many objects it holds. '
|
|
505
|
+
+ 'A task number answers its request with that task. Read only.',
|
|
506
|
+
isMutating: false,
|
|
507
|
+
requiresSap: true,
|
|
508
|
+
requires: 'cts.requests',
|
|
509
|
+
input_schema: {
|
|
510
|
+
type: 'object',
|
|
511
|
+
properties: {
|
|
512
|
+
transport: { type: 'string', description: 'Transport request or task number (e.g. DEVK900123)' },
|
|
513
|
+
},
|
|
514
|
+
required: ['transport'],
|
|
515
|
+
},
|
|
516
|
+
handler: async (args, ctx) => {
|
|
517
|
+
const asked = String(args.transport ?? '').trim().toUpperCase();
|
|
518
|
+
const read = await readRequest(ctx, asked);
|
|
519
|
+
if ('refusal' in read)
|
|
520
|
+
return read.refusal;
|
|
521
|
+
const { doc } = read;
|
|
522
|
+
const objectCount = knownObjectCount(doc);
|
|
523
|
+
const notes = [];
|
|
524
|
+
if (doc.kind === 'task')
|
|
525
|
+
notes.push(`${asked} is a task of ${doc.number}.`);
|
|
526
|
+
if (objectCount === null)
|
|
527
|
+
notes.push(`CSPeach could not count the objects of ${doc.number}.`);
|
|
528
|
+
return {
|
|
529
|
+
content: JSON.stringify({
|
|
530
|
+
kind: doc.kind,
|
|
531
|
+
request: doc.number,
|
|
532
|
+
description: doc.description,
|
|
533
|
+
owner: doc.owner,
|
|
534
|
+
type: doc.type,
|
|
535
|
+
status: doc.status,
|
|
536
|
+
status_text: doc.statusText,
|
|
537
|
+
target: doc.target,
|
|
538
|
+
tasks: doc.tasks.map((t) => ({
|
|
539
|
+
number: t.number, owner: t.owner, status: t.status, status_text: t.statusText, objects: t.objectCount,
|
|
540
|
+
})),
|
|
541
|
+
object_count: objectCount,
|
|
542
|
+
can_release: isModifiable(doc.status) && doc.actions.includes('newreleasejobs'),
|
|
543
|
+
...(notes.length ? { note: notes.join(' ') } : {}),
|
|
544
|
+
}),
|
|
545
|
+
};
|
|
546
|
+
},
|
|
547
|
+
});
|
|
475
548
|
// ── sap_transport_release ────────────────────────────────────────────────────
|
|
476
549
|
//
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
|
|
482
|
-
|
|
550
|
+
// Part 3 (D10–D13, D17) — every open task first, then the request, on SAP's
|
|
551
|
+
// own release verdict (sap-client transportRelease). Gates, in order:
|
|
552
|
+
// 0. read the request: missing, a task number, not modifiable → refused;
|
|
553
|
+
// 0b. the request's objects (its tasks' too) against the inactive list →
|
|
554
|
+
// refused (SAP would abort with abortrelinacobj; its consistency check
|
|
555
|
+
// does not see inactive objects);
|
|
556
|
+
// 1. the approval (op release, type CTS) — spent only after 0/0b passed;
|
|
557
|
+
// 2. the safety card (Rule 9) when safety mode is on.
|
|
558
|
+
// The envelope carries every step's verdict, SAP's texts and raw reports, what
|
|
559
|
+
// was never sent, the status read back, and that the release is not yet proven
|
|
560
|
+
// live. sap-client writes the audit rows (one per step and one closing row);
|
|
561
|
+
// the tool writes none of its own, except when sap-client threw before any.
|
|
562
|
+
/** Verdicts that mean "SAP would release it if something were ignored" (D11). */
|
|
563
|
+
const IGNORE_VERDICTS = new Set(['relwithignlock', 'relObjigchkatc', 'relwithignwarning']);
|
|
564
|
+
const NOT_YET_PROVEN = "Built from SAP's own release interface and tested against recorded answers; not yet run in a live release. "
|
|
565
|
+
+ 'Ask the user to check the transport in SE09 after the release.';
|
|
483
566
|
registerTool({
|
|
484
567
|
name: 'sap_transport_release',
|
|
485
|
-
description: '
|
|
486
|
-
+ '
|
|
568
|
+
description: 'Release a transport request: every open task first, then the request. The verdict comes from SAP\'s own release '
|
|
569
|
+
+ 'report, step by step, and the request status is read back; an HTTP answer alone is never taken as released. '
|
|
570
|
+
+ 'Stops at the first step SAP does not release; never ignores locks, object-check errors or warnings (release '
|
|
571
|
+
+ 'those in SE09 or Eclipse). Built from SAP\'s own release interface and tested against recorded answers; not '
|
|
572
|
+
+ 'yet run in a live release, so ask the user to check the transport in SE09 afterwards. Cannot be undone. '
|
|
573
|
+
+ 'Requires approval_id from a request_approval change of op `release`, type CTS (one approval per release; '
|
|
574
|
+
+ 'never auto-approved).',
|
|
487
575
|
isMutating: true,
|
|
488
576
|
requiresSap: true,
|
|
577
|
+
requires: 'cts.requests',
|
|
489
578
|
input_schema: {
|
|
490
579
|
type: 'object',
|
|
491
580
|
properties: {
|
|
492
581
|
transport: {
|
|
493
582
|
type: 'string',
|
|
494
|
-
description: 'Transport request number (e.g. DEVK900123)',
|
|
583
|
+
description: 'Transport request number to release (e.g. DEVK900123); not a task number',
|
|
495
584
|
},
|
|
496
585
|
approval_id: {
|
|
497
586
|
type: 'string',
|
|
498
|
-
description: 'JWT from request_approval (
|
|
587
|
+
description: 'JWT from request_approval (a change of op release, type CTS)',
|
|
499
588
|
},
|
|
500
589
|
},
|
|
501
|
-
required: ['transport'],
|
|
590
|
+
required: ['transport', 'approval_id'],
|
|
591
|
+
},
|
|
592
|
+
handler: async (args, ctx) => {
|
|
593
|
+
const asked = String(args.transport ?? '').trim().toUpperCase();
|
|
594
|
+
// ── Gate 0: SAP's own facts, read fresh (D10, D12) ──────────────────────
|
|
595
|
+
const read = await readRequest(ctx, asked);
|
|
596
|
+
if ('refusal' in read)
|
|
597
|
+
return read.refusal;
|
|
598
|
+
const { doc } = read;
|
|
599
|
+
if (doc.kind === 'task') {
|
|
600
|
+
return {
|
|
601
|
+
content: JSON.stringify({
|
|
602
|
+
error: 'task_number',
|
|
603
|
+
message: `${asked} is a task of ${doc.number}. Give the request number; CSPeach releases the tasks first, then the request.`,
|
|
604
|
+
}),
|
|
605
|
+
is_error: true,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
if (!isModifiable(doc.status)) {
|
|
609
|
+
return {
|
|
610
|
+
content: JSON.stringify({
|
|
611
|
+
error: 'not_modifiable',
|
|
612
|
+
message: notModifiableText(doc),
|
|
613
|
+
}),
|
|
614
|
+
is_error: true,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
// ── Gate 0b: inactive objects of the request (T14) ──────────────────────
|
|
618
|
+
let warning;
|
|
619
|
+
let inactiveNames;
|
|
620
|
+
try {
|
|
621
|
+
inactiveNames = new Set((await ctx.adt.inactiveObjects()).map((o) => String(o.name ?? '').trim().toUpperCase()));
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
warning = 'CSPeach could not read the inactive list; SAP aborts a release that holds inactive objects.';
|
|
625
|
+
}
|
|
626
|
+
if (inactiveNames) {
|
|
627
|
+
const blocked = allObjectNames(doc).filter((n) => inactiveNames.has(n));
|
|
628
|
+
if (blocked.length > 0) {
|
|
629
|
+
return {
|
|
630
|
+
content: JSON.stringify({
|
|
631
|
+
error: 'release_blocked_inactive',
|
|
632
|
+
objects: blocked,
|
|
633
|
+
message: `These objects of ${doc.number} are inactive: ${blocked.join(', ')}. `
|
|
634
|
+
+ 'Activate them (sap_activate), then ask again. SAP would abort the release.',
|
|
635
|
+
}),
|
|
636
|
+
is_error: true,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// ── Gate 1: Approval (op release, type CTS) ─────────────────────────────
|
|
641
|
+
const verify = await verifyAndSpendApprovalId(args.approval_id, args.transport, 'release');
|
|
642
|
+
if (!verify.ok) {
|
|
643
|
+
return {
|
|
644
|
+
content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
|
|
645
|
+
is_error: true,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
if (typeCode(verify.payload?.type) !== 'CTS') {
|
|
649
|
+
return {
|
|
650
|
+
content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: 'type_mismatch' }),
|
|
651
|
+
is_error: true,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
const startedAt = Date.now();
|
|
655
|
+
// ── Gate 2: Safety confirmation (Rule 9 — a release cannot be undone) ───
|
|
656
|
+
if (isSafetyConfirmEnabled()) {
|
|
657
|
+
const openTasks = doc.tasks.filter((t) => isModifiable(t.status)).map((t) => t.number);
|
|
658
|
+
const objects = knownObjectCount(doc);
|
|
659
|
+
const c = await presentSafetyConfirmation({
|
|
660
|
+
rule: 'RULE_9_TRANSPORT_RELEASE',
|
|
661
|
+
op: 'transport_release',
|
|
662
|
+
object: { type: 'TRANSPORT', name: doc.number },
|
|
663
|
+
what_will_happen: `Releases ${openTasks.length} task(s)${openTasks.length ? ` (${openTasks.join(', ')})` : ''} then request ${doc.number} `
|
|
664
|
+
+ `(${objects === null ? 'an uncounted number of' : objects} objects, target ${doc.target || 'none'}). `
|
|
665
|
+
+ `Cannot be undone; the request may move to other systems on the route. ${RELEASE_CHECK_LINE}`,
|
|
666
|
+
rollback_available: false,
|
|
667
|
+
transport: doc.number,
|
|
668
|
+
args: { ...args },
|
|
669
|
+
});
|
|
670
|
+
if (!c.confirmed) {
|
|
671
|
+
return {
|
|
672
|
+
content: JSON.stringify({ error: 'cancelled_by_user', reason: c.reason }),
|
|
673
|
+
is_error: true,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
// ── Release: tasks, then the request, on SAP's verdict ──────────────────
|
|
678
|
+
let r;
|
|
679
|
+
try {
|
|
680
|
+
r = await ctx.adt.transportRelease(doc.number);
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
// sap-client threw before its first step (its own read): it wrote no row.
|
|
684
|
+
await auditLog('transportRelease', 'CTS', doc.number, 'error', Date.now() - startedAt);
|
|
685
|
+
return {
|
|
686
|
+
content: JSON.stringify({ error: 'transport_release_failed', detail: err instanceof Error ? err.message : String(err) }),
|
|
687
|
+
is_error: true,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
const finalStatus = r.finalStatus?.status;
|
|
691
|
+
// Never `released: true` beside a status that is not R or N.
|
|
692
|
+
const released = r.status === 'released' && (finalStatus === 'R' || finalStatus === 'N');
|
|
693
|
+
let note;
|
|
694
|
+
switch (r.status) {
|
|
695
|
+
case 'released':
|
|
696
|
+
// Worded from `released`, never from r.status alone: "Released." only beside released: true.
|
|
697
|
+
note = released
|
|
698
|
+
? "Released. SAP's answer for each step is in steps[] and sap_answer[]."
|
|
699
|
+
: `SAP reported every step released, but the request reads status ${finalStatus || 'unknown'}. Check it in SE09.`;
|
|
700
|
+
break;
|
|
701
|
+
case 'stopped': {
|
|
702
|
+
const last = r.steps[r.steps.length - 1];
|
|
703
|
+
note = `${r.error ?? ''} Nothing was sent after that step.`.trim();
|
|
704
|
+
if (last && IGNORE_VERDICTS.has(last.verdict)) {
|
|
705
|
+
note += ' CSPeach does not release with anything ignored. Release it in SE09 or Eclipse if that is what you want.';
|
|
706
|
+
}
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
case 'refused':
|
|
710
|
+
note = r.refusal ?? 'Nothing was sent.';
|
|
711
|
+
break;
|
|
712
|
+
default:
|
|
713
|
+
note = r.error ?? `The release ended with status ${r.status}.`;
|
|
714
|
+
}
|
|
715
|
+
const detail = r.refusal ?? r.error;
|
|
716
|
+
const stopStep = r.status === 'stopped' ? r.steps[r.steps.length - 1] : undefined;
|
|
717
|
+
const envelope = {
|
|
718
|
+
released,
|
|
719
|
+
status: r.status,
|
|
720
|
+
// Near the top so a spilled preview (and the audit row read from it) keeps the stopping step.
|
|
721
|
+
...(stopStep ? { stopped_at: stopStep.number, stop_verdict: stopStep.verdict } : {}),
|
|
722
|
+
transport_number: r.transportNumber,
|
|
723
|
+
final_status: finalStatus,
|
|
724
|
+
not_sent: r.notSent ?? [],
|
|
725
|
+
note,
|
|
726
|
+
...(detail ? { error_detail: detail } : {}),
|
|
727
|
+
...(warning ? { warning } : {}),
|
|
728
|
+
not_yet_proven: NOT_YET_PROVEN,
|
|
729
|
+
steps: r.steps.map(({ rawReports: _raw, ...s }) => s),
|
|
730
|
+
sap_answer: r.steps.map((s) => ({ step: s.number, raw: s.rawReports ?? '' })),
|
|
731
|
+
};
|
|
732
|
+
// Pretty-printed with the facts first: a spilled preview keeps the verdict
|
|
733
|
+
// lines (a one-line JSON would be cut at the preview's line limit).
|
|
734
|
+
const failed = r.status === 'stopped' || r.status === 'error' || r.status === 'refused';
|
|
735
|
+
return spillToolResult({ content: JSON.stringify(envelope, null, 2), ...(failed ? { is_error: true } : {}) }, ctx, 'report', { label: `release ${r.transportNumber}` });
|
|
502
736
|
},
|
|
503
|
-
handler: async (args) => ({
|
|
504
|
-
content: JSON.stringify({
|
|
505
|
-
error: 'release_unavailable',
|
|
506
|
-
transport: args.transport,
|
|
507
|
-
message: RELEASE_UNAVAILABLE,
|
|
508
|
-
}),
|
|
509
|
-
is_error: true,
|
|
510
|
-
}),
|
|
511
737
|
});
|
package/dist/tools/write-mode.js
CHANGED
|
@@ -23,6 +23,8 @@ export const WRITE_TOOLS = new Set([
|
|
|
23
23
|
'sap_transport_release',
|
|
24
24
|
// Final review I2 — SAP's RAP generator creates the skeleton objects.
|
|
25
25
|
'sap_generate_objects',
|
|
26
|
+
// Part 3 — a run executes code on the SAP system; advisory mode never runs it.
|
|
27
|
+
'sap_run',
|
|
26
28
|
]);
|
|
27
29
|
export function isWriteTool(name) {
|
|
28
30
|
return WRITE_TOOLS.has(name);
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useRef, useState } from 'react';
|
|
3
3
|
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
4
|
import { riskReasonText, shownRiskReasons } from '../approvals/risk-floor.js';
|
|
5
|
-
import { verbLine, viewAffordance, toSourceLines, affectsLine } from '../approvals/op-labels.js';
|
|
5
|
+
import { verbLine, viewAffordance, toSourceLines, affectsLine, releaseLines } from '../approvals/op-labels.js';
|
|
6
6
|
// BUG-1 (2026-07-05 live smoke): the interactive approval modal mounted
|
|
7
7
|
// DEAD — every keypress ignored — when it appeared after a guarded
|
|
8
8
|
// /abap-plan turn. Root cause is identical to the one AskCard
|
|
@@ -39,10 +39,18 @@ export function riskRows(effectiveRisk, width) {
|
|
|
39
39
|
* decision input too: its "Affects:" text wraps onto up to this many rows.
|
|
40
40
|
*/
|
|
41
41
|
export const AFFECTS_MAX_ROWS = 2;
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* The rows under the verb line: "Affects: …" (none when the change names no
|
|
44
|
+
* process), then — Part 3 D12 — a release card's fact lines, each wrapping
|
|
45
|
+
* onto up to AFFECTS_MAX_ROWS rows. Any other change: exactly as before.
|
|
46
|
+
*/
|
|
43
47
|
export function affectsRows(change, width) {
|
|
48
|
+
const cols = Math.max(1, width - MODAL_CHROME_COLS);
|
|
44
49
|
const line = affectsLine(change);
|
|
45
|
-
return
|
|
50
|
+
return [
|
|
51
|
+
...(line ? wrapRows(line, cols, AFFECTS_MAX_ROWS) : []),
|
|
52
|
+
...releaseLines(change).flatMap((l) => wrapRows(l, cols, AFFECTS_MAX_ROWS)),
|
|
53
|
+
];
|
|
46
54
|
}
|
|
47
55
|
/**
|
|
48
56
|
* The collapsed card's rendered height at `width` (the live-area budget; the
|
|
@@ -14,7 +14,7 @@ const MUTATING_TOOLS = new Set([
|
|
|
14
14
|
'sap_set_source', 'sap_update_method', 'sap_delete_object', 'sap_activate',
|
|
15
15
|
'sap_create_object', 'sap_transport_release', 'sap_message_maintain',
|
|
16
16
|
'sap_number_range_intervals', 'sap_service_binding_publish',
|
|
17
|
-
'sap_service_binding_unpublish',
|
|
17
|
+
'sap_service_binding_unpublish', 'sap_run',
|
|
18
18
|
]);
|
|
19
19
|
export function toTimelineEntry(call) {
|
|
20
20
|
// args shape depends on the tool; extract defensively.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cspeach/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.45",
|
|
4
4
|
"description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, signed skills, safety gates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abap",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@anthropic-ai/sdk": "^0.90.0",
|
|
55
55
|
"@azure/msal-node": "6.0.1",
|
|
56
56
|
"@cspeach/register-core": "0.4.0",
|
|
57
|
-
"@cspeach/sap-client": "0.1.
|
|
57
|
+
"@cspeach/sap-client": "0.1.29",
|
|
58
58
|
"@iarna/toml": "^2.2.5",
|
|
59
59
|
"@inkjs/ui": "^2.0.0",
|
|
60
60
|
"@inquirer/prompts": "^7.0.0",
|