@cspeach/cli 1.1.51 → 1.1.52
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/dist/agent/chat-prompt.js +4 -1
- package/dist/agent/conversation-prompt.js +30 -13
- package/dist/agent/inline-skill-block.js +341 -0
- package/dist/agent/loop.js +208 -41
- package/dist/agent/provider-shape.js +8 -4
- package/dist/agent/providers/ai-hub-provider.js +2 -2
- package/dist/agent/providers/azure-provider.js +2 -2
- package/dist/agent/providers/byok-provider.js +2 -2
- package/dist/agent/providers/local-provider.js +2 -2
- package/dist/agent/repair-partial.js +12 -0
- package/dist/agent/skill-block-key.js +134 -0
- package/dist/agent/steering-queue.js +50 -14
- package/dist/agent/system-prompt.js +22 -0
- package/dist/agent/tool-dispatch.js +19 -7
- package/dist/agent/turn-watchdog.js +1 -1
- package/dist/approvals/advisory-render.js +6 -3
- package/dist/approvals/approval-prompt.js +9 -4
- package/dist/approvals/op-labels.js +14 -0
- package/dist/approvals/plan-grant.js +8 -0
- package/dist/approvals/render.js +19 -8
- package/dist/approvals/risk-floor.js +32 -4
- package/dist/approvals/visible-changes.js +50 -0
- package/dist/cli.js +10 -2
- package/dist/commands/compact.js +6 -1
- package/dist/commands/config-set.js +13 -0
- package/dist/commands/config-show.js +16 -4
- package/dist/commands/export-conversation.js +7 -4
- package/dist/commands/finish-check-guard.js +124 -0
- package/dist/commands/plan-reground.js +27 -4
- package/dist/commands/plan-resume.js +111 -1
- package/dist/config/loader.js +5 -1
- package/dist/cost/cost-log.js +6 -0
- package/dist/cost/system-block.js +6 -0
- package/dist/models/server-config.js +1 -1
- package/dist/projects/save-command.js +5 -1
- package/dist/projects/yes-no.js +5 -1
- package/dist/renderer/change-card.js +3 -1
- package/dist/renderer/steering-echo.js +53 -8
- package/dist/renderer/tool-labels.js +3 -1
- package/dist/repl/chat-lane.js +5 -2
- package/dist/repl/inline-mode.js +51 -0
- package/dist/repl/leftover.js +90 -0
- package/dist/repl/reroute-turn.js +7 -1
- package/dist/repl/turn-stop.js +64 -0
- package/dist/repl.js +144 -22
- package/dist/router/classifier.js +1 -1
- package/dist/sap/reachability-state.js +7 -0
- package/dist/sap/unreachable.js +2 -2
- package/dist/session/queued-step.js +3 -1
- package/dist/session/user-prompt.js +10 -1
- package/dist/skills/manifest-client.js +3 -2
- package/dist/skills/preamble.js +6 -4
- package/dist/skills/source-authed.js +3 -2
- package/dist/tools/approval.js +37 -7
- package/dist/tools/dispatch-skill.js +1 -1
- package/dist/tools/hand-off.js +49 -7
- package/dist/tools/lane/app-lane-resolve.js +9 -0
- package/dist/tools/method-only-change.js +241 -0
- package/dist/tools/rap/skeleton-resolve.js +4 -0
- package/dist/tools/sap-write.js +78 -3
- package/dist/tools/snapshot.js +7 -2
- package/dist/tools/subagent/agent_run.js +14 -0
- package/dist/tools/tool-loading.js +16 -8
- package/dist/tools/transport-fit-check.js +70 -0
- package/dist/tools/transport.js +17 -8
- package/dist/tools/write-pipeline.js +46 -5
- package/dist/ui/app.js +112 -13
- package/dist/ui/approval-modal.js +11 -6
- package/dist/ui/ascii-chars.js +21 -3
- package/dist/ui/footer.js +128 -7
- package/dist/ui/interrupt.js +35 -1
- package/dist/ui/live-area.js +21 -4
- package/dist/ui/queue-box-emitter.js +14 -0
- package/dist/ui/rewind-panel.js +2 -1
- package/dist/ui/session-timeline.js +2 -1
- package/dist/ui/text-input.js +31 -2
- package/dist/ui/widgets/bar-chart.js +2 -1
- package/dist/ui/widgets/confirm-card.js +2 -1
- package/dist/ui/widgets/dep-graph.js +2 -1
- package/dist/ui/widgets/diff-viewer.js +2 -1
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/shortcuts-sheet.js +2 -2
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/util/connect-retry.js +65 -0
- package/package.json +1 -1
package/dist/tools/hand-off.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* chat turn then ends and the workflow runs with its own plan + approvals.
|
|
6
6
|
* No extra consent card (D5): the workflow asks before any write.
|
|
7
7
|
* Refused outside the chat lane; dispatch_skill stays the skills' own switch.
|
|
8
|
+
* Stage 1A (T1A.4): in an inline session (`ctx.inlineSkills`) a playbook turn
|
|
9
|
+
* may hand off too — still only queues — except while a plan is in charge or
|
|
10
|
+
* the slot already holds a step (the harness's next-phase hop wins).
|
|
8
11
|
*
|
|
9
12
|
* Flag-gated: only a chat-lane process enables it (Task 11), so a skills-lane
|
|
10
13
|
* session never lists it and its tools array stays byte-identical to today.
|
|
@@ -14,7 +17,37 @@ import { isPlanResumeCommand } from '../commands/plan-resume.js';
|
|
|
14
17
|
import { SKILL_CATALOG, resolveSkillAlias } from '../skill-catalog.js';
|
|
15
18
|
/** Longest request hand_off queues (after whitespace is collapsed). */
|
|
16
19
|
export const HAND_OFF_REQUEST_MAX = 500;
|
|
20
|
+
/**
|
|
21
|
+
* I3 (owner, 2026-10-22) — one hand-off per person message. Refusal code when
|
|
22
|
+
* the running turn was itself started by a hand_off: no workflow-to-workflow
|
|
23
|
+
* chain of reads and credits without a new person message. A new person
|
|
24
|
+
* message resets it (the REPL passes who started each turn as `ctx.turnBy`).
|
|
25
|
+
* dispatch_skill after the person's pick is not capped.
|
|
26
|
+
*/
|
|
27
|
+
export const HAND_OFF_CHAIN_CAPPED = 'HAND_OFF_CHAIN_CAPPED';
|
|
17
28
|
export const HAND_OFF_LINE = (skill) => `→ /${skill}`;
|
|
29
|
+
/** The success note when the model wrote nothing before the call in this turn. */
|
|
30
|
+
export const HAND_OFF_NOTE = 'The workflow runs next and asks before it writes. End the turn now. If you wrote a line before the call, add nothing. If you wrote nothing, add one short line saying the workflow takes it from here. Do not print the command.';
|
|
31
|
+
/**
|
|
32
|
+
* LT-3 (live check 2026-10-26) — the model often skipped the "if you wrote a
|
|
33
|
+
* line" test and announced the hand-off twice. The loop knows whether text
|
|
34
|
+
* came first in this turn and then gives this plain note instead.
|
|
35
|
+
*/
|
|
36
|
+
export const HAND_OFF_NOTE_ALREADY_SAID = 'The workflow runs next and asks before it writes. You already told the developer in this turn, so end the turn now and write nothing more. Do not print the command.';
|
|
37
|
+
/** The hand_off success content with the note for a turn that already said its line. Anything else unchanged. */
|
|
38
|
+
export function settleHandOffNote(content, saidBefore) {
|
|
39
|
+
if (!saidBefore)
|
|
40
|
+
return content;
|
|
41
|
+
try {
|
|
42
|
+
const r = JSON.parse(content);
|
|
43
|
+
if (r.ok !== true || r.note !== HAND_OFF_NOTE)
|
|
44
|
+
return content;
|
|
45
|
+
return JSON.stringify({ ...r, note: HAND_OFF_NOTE_ALREADY_SAID });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return content;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
18
51
|
export const buildHandOffCommand = (skill, request) => `/${skill} ${request}`;
|
|
19
52
|
const CATALOG_NAMES = new Set(SKILL_CATALOG.map((s) => s.name));
|
|
20
53
|
/**
|
|
@@ -45,15 +78,25 @@ registerTool({
|
|
|
45
78
|
type: 'object',
|
|
46
79
|
properties: {
|
|
47
80
|
skill: { type: 'string', description: 'Workflow name without the slash, e.g. "abap-refactor".' },
|
|
48
|
-
request: { type: 'string', description: 'What the workflow should do, with object names. One sentence, at most 500 characters, no flags.' },
|
|
81
|
+
request: { type: 'string', description: 'What the workflow should do, with object names. One sentence, at most 500 characters, no flags. Never put a transport description in it unless the person typed it.' },
|
|
49
82
|
},
|
|
50
83
|
required: ['skill', 'request'],
|
|
51
84
|
},
|
|
52
85
|
handler: async (args, ctx) => {
|
|
53
86
|
const bad = (error, reason) => ({ is_error: true, content: JSON.stringify({ error, reason }) });
|
|
54
|
-
|
|
87
|
+
// Stage 1A (T1A.4) — in an inline session a playbook turn may hand off too.
|
|
88
|
+
const playbookTurn = ctx.session?.lane !== 'chat';
|
|
89
|
+
if (playbookTurn && !ctx.inlineSkills) {
|
|
55
90
|
return bad('HAND_OFF_OUTSIDE_CHAT', 'hand_off is only for the chat; in a workflow, continue with the workflow (dispatch_skill switches workflows after the person agreed).');
|
|
56
91
|
}
|
|
92
|
+
// The plan harness queues its next phase through the same slot; never race it.
|
|
93
|
+
if (playbookTurn && (ctx.session?.plan_in_flight || ctx.session?.skill === 'abap-plan')) {
|
|
94
|
+
return bad('HAND_OFF_PLAN_IN_FLIGHT', 'finish this step; after the plan, the person can ask for that in chat');
|
|
95
|
+
}
|
|
96
|
+
// I3 — one hand-off per person message.
|
|
97
|
+
if (ctx.turnBy === 'hand_off') {
|
|
98
|
+
return bad(HAND_OFF_CHAIN_CAPPED, "this workflow was already handed off from the person's message; one hand-off per message. Finish this workflow's answer, then let the person decide the next step: offer that workflow as a suggestion in your answer instead of calling hand_off. Do not call dispatch_skill either unless the person picks it.");
|
|
99
|
+
}
|
|
57
100
|
const skill = normaliseSkill(args.skill);
|
|
58
101
|
if (!skill) {
|
|
59
102
|
return bad('HAND_OFF_UNKNOWN_SKILL', `unknown workflow "${String(args.skill ?? '')}". Use one of: ${SKILL_CATALOG.map((s) => s.name).join(', ')}`);
|
|
@@ -74,13 +117,12 @@ registerTool({
|
|
|
74
117
|
if (!ctx.pendingDispatch) {
|
|
75
118
|
return bad('DISPATCH_UNSUPPORTED', 'no dispatch slot in this context (one-shot or test); tell the developer which /abap-… command to run instead');
|
|
76
119
|
}
|
|
120
|
+
if (playbookTurn && ctx.pendingDispatch.get() !== null) {
|
|
121
|
+
return bad('HAND_OFF_SLOT_TAKEN', 'a step is already queued; end the turn');
|
|
122
|
+
}
|
|
77
123
|
ctx.pendingDispatch.set(command, { by: 'hand_off', line: HAND_OFF_LINE(skill) });
|
|
78
124
|
return {
|
|
79
|
-
content: JSON.stringify({
|
|
80
|
-
ok: true,
|
|
81
|
-
queued: command,
|
|
82
|
-
note: 'The workflow runs next and asks before it writes. End the turn now with at most one short line; do not print the command.',
|
|
83
|
-
}),
|
|
125
|
+
content: JSON.stringify({ ok: true, queued: command, note: HAND_OFF_NOTE }),
|
|
84
126
|
};
|
|
85
127
|
},
|
|
86
128
|
});
|
|
@@ -36,6 +36,9 @@ import { askConfirm } from '../../ui/confirm-request.js';
|
|
|
36
36
|
import { isHeadless } from '../../renderer/tty.js';
|
|
37
37
|
import { loadConfig } from '../../config/loader.js';
|
|
38
38
|
import { getSapSystemInfo } from '../../sap/system-info.js';
|
|
39
|
+
import { STOPPED_BEFORE_START_RESULT } from '../../agent/repair-partial.js';
|
|
40
|
+
/** LT-15 D6 (review M2) — after a stop (Esc / Ctrl+C) no card opens and nothing is saved. */
|
|
41
|
+
const stopped = (ctx) => (ctx.signal?.aborted ? { content: STOPPED_BEFORE_START_RESULT, is_error: true } : null);
|
|
39
42
|
const fail = (message) => ({ content: `error: ${message}`, is_error: true });
|
|
40
43
|
const NOT_CHOSEN = 'Lane not chosen. Nothing was saved.';
|
|
41
44
|
const STOP = 'STOP — lane needs your choice.';
|
|
@@ -200,6 +203,9 @@ export async function appLaneResolveHandler(args, ctx, deps = {}) {
|
|
|
200
203
|
}
|
|
201
204
|
else {
|
|
202
205
|
// 7. The lane card.
|
|
206
|
+
const stopLane = stopped(ctx);
|
|
207
|
+
if (stopLane)
|
|
208
|
+
return stopLane;
|
|
203
209
|
const confirm = deps.confirm ?? ((req) => askConfirm(req));
|
|
204
210
|
const card = laneCardRequest(decision);
|
|
205
211
|
const res = await confirm({ ...card, facts: [...(card.facts ?? []), ...cardFacts] });
|
|
@@ -224,6 +230,9 @@ export async function appLaneResolveHandler(args, ctx, deps = {}) {
|
|
|
224
230
|
}
|
|
225
231
|
// 8. A CAP lane needs a runtime.
|
|
226
232
|
if (finalLane.backend === 'cap' && !runtime) {
|
|
233
|
+
const stopRuntime = stopped(ctx);
|
|
234
|
+
if (stopRuntime)
|
|
235
|
+
return stopRuntime;
|
|
227
236
|
const rr = await confirm(runtimeCardRequest());
|
|
228
237
|
const answer = rr.value;
|
|
229
238
|
if (!answer || !RUNTIMES.includes(answer))
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule 7a in code (2026-10-22, owner go).
|
|
3
|
+
*
|
|
4
|
+
* sap_set_source on an existing class replaces the whole source. When the
|
|
5
|
+
* only thing that changed is the inside of one or more method bodies, the
|
|
6
|
+
* write must go through sap_update_method instead (one call per method): a
|
|
7
|
+
* whole-class rewrite can silently drop parameters, alter untouched methods
|
|
8
|
+
* or lose local definitions.
|
|
9
|
+
*
|
|
10
|
+
* `methodOnlyChange(current, proposed)` splits both sources into
|
|
11
|
+
* - the frame: every line outside method bodies (definition part(s), the
|
|
12
|
+
* IMPLEMENTATION header, the `METHOD x.` / `ENDMETHOD.` lines, ENDCLASS);
|
|
13
|
+
* - each method body: the lines strictly between `METHOD x.` and `ENDMETHOD.`.
|
|
14
|
+
* It returns the names of the changed methods ONLY when the frame is
|
|
15
|
+
* identical, the methods are the same (names, order) and at least one body
|
|
16
|
+
* differs. Anything else returns undefined, and the write goes on as today.
|
|
17
|
+
*
|
|
18
|
+
* Normalised: line endings (CRLF / LF / CR) and trailing whitespace (per
|
|
19
|
+
* line, and blank lines at the end of the source). Nothing else.
|
|
20
|
+
*
|
|
21
|
+
* Conservative: when the split is not certain, return undefined (never
|
|
22
|
+
* refuse). That covers unbalanced METHOD / ENDMETHOD, a METHOD statement with
|
|
23
|
+
* more than its name (AMDP `BY DATABASE PROCEDURE …`), the words METHOD /
|
|
24
|
+
* ENDMETHOD anywhere else in code, macros (DEFINE), INCLUDE programs, an
|
|
25
|
+
* unterminated literal, and any changed method that sap_update_method could
|
|
26
|
+
* not address exactly (its name appears more than once as `METHOD name.`, or
|
|
27
|
+
* its body holds the text `ENDMETHOD.`).
|
|
28
|
+
*
|
|
29
|
+
* Never guarded, by design: a pragma or anything else after the name on the
|
|
30
|
+
* METHOD line (e.g. ADT's ZCX exception-class template), AMDP methods, a
|
|
31
|
+
* component or parameter named "method", and a class with a pending inactive
|
|
32
|
+
* version (anyone's: sap_update_method would refuse it with 409).
|
|
33
|
+
*/
|
|
34
|
+
/** The audit-line detail: at most 5 names, then "+N more" (the audit log keeps 200 chars). */
|
|
35
|
+
export function methodOnlyAuditDetail(methods) {
|
|
36
|
+
const shown = methods.slice(0, 5).join(',');
|
|
37
|
+
const more = methods.length > 5 ? ` +${methods.length - 5} more` : '';
|
|
38
|
+
return `refused: method_only_change (Rule 7a) methods=${shown}${more}`;
|
|
39
|
+
}
|
|
40
|
+
/** The model-facing refusal for a method-only change. */
|
|
41
|
+
export function methodOnlyRefusal(methods) {
|
|
42
|
+
return {
|
|
43
|
+
error: 'method_only_change',
|
|
44
|
+
detail: `Only the bodies of these methods changed: ${methods.join(', ')}. `
|
|
45
|
+
+ 'Use sap_update_method once per method instead of rewriting the whole class (Rule 7a). '
|
|
46
|
+
+ 'The class definition and the other methods stay untouched that way. '
|
|
47
|
+
+ 'Your approval id still works for those calls.',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const METHOD_START = /^METHOD\s+([^\s.]+)\s*\.$/i;
|
|
51
|
+
const METHOD_END = /^ENDMETHOD\s*\.$/i;
|
|
52
|
+
/** METHOD / ENDMETHOD as a word: not part of a longer name (METHODS, ls_x-method, lo->method, if~method). */
|
|
53
|
+
const KEYWORD = /(?<![\w\-~>\/=%$])(END)?METHOD(?![\w\-~\/%$])/gi;
|
|
54
|
+
const DEFINE_STMT = /(^|\.)\s*DEFINE\s/i;
|
|
55
|
+
/** An INCLUDE program; `INCLUDE TYPE` / `INCLUDE STRUCTURE` inside TYPES / DATA are not. */
|
|
56
|
+
const INCLUDE_STMT = /(^|\.)\s*INCLUDE\s+(?!(TYPE|STRUCTURE)\s)/i;
|
|
57
|
+
/** Names (upper case) of the methods whose bodies alone changed; undefined when the write must go on. */
|
|
58
|
+
export function methodOnlyChange(current, proposed) {
|
|
59
|
+
const a = splitClassSource(current);
|
|
60
|
+
const b = splitClassSource(proposed);
|
|
61
|
+
if (!a || !b)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (a.methods.length === 0 || a.methods.length !== b.methods.length)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (!sameLines(a.frame, b.frame))
|
|
66
|
+
return undefined;
|
|
67
|
+
const changed = [];
|
|
68
|
+
for (let i = 0; i < a.methods.length; i++) {
|
|
69
|
+
const ma = a.methods[i];
|
|
70
|
+
const mb = b.methods[i];
|
|
71
|
+
if (ma.name.toUpperCase() !== mb.name.toUpperCase())
|
|
72
|
+
return undefined;
|
|
73
|
+
if (!sameLines(ma.body, mb.body))
|
|
74
|
+
changed.push(ma.name.toUpperCase());
|
|
75
|
+
}
|
|
76
|
+
if (changed.length === 0)
|
|
77
|
+
return undefined;
|
|
78
|
+
for (const name of changed) {
|
|
79
|
+
if (!updateMethodCanAddress(current, name) || !updateMethodCanAddress(proposed, name))
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return changed;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* sap_update_method (sap-client replaceMethodBlock) takes the FIRST
|
|
86
|
+
* `METHOD name.` match anywhere in the source and the body up to the first
|
|
87
|
+
* `ENDMETHOD.` after it. Only point at it when that lands on our block.
|
|
88
|
+
*/
|
|
89
|
+
function updateMethodCanAddress(source, name) {
|
|
90
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
91
|
+
const starts = source.match(new RegExp(`\\bMETHOD\\s+${escaped}\\s*\\.`, 'gi')) ?? [];
|
|
92
|
+
if (starts.length !== 1)
|
|
93
|
+
return false;
|
|
94
|
+
const split = splitClassSource(source);
|
|
95
|
+
const block = split?.methods.find((m) => m.name.toUpperCase() === name);
|
|
96
|
+
return !!block && !block.body.some((l) => /\bENDMETHOD\s*\./i.test(l));
|
|
97
|
+
}
|
|
98
|
+
function sameLines(x, y) {
|
|
99
|
+
return x.length === y.length && x.every((l, i) => l === y[i]);
|
|
100
|
+
}
|
|
101
|
+
/** The frame / body split, or undefined when it is not certain. Exported for tests. */
|
|
102
|
+
export function splitClassSource(source) {
|
|
103
|
+
const lines = source.replace(/\s+$/, '').split(/\r\n|\r|\n/).map((l) => l.replace(/\s+$/, ''));
|
|
104
|
+
const frame = [];
|
|
105
|
+
const methods = [];
|
|
106
|
+
let open;
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
if (line.startsWith('*') || line.trimStart().startsWith('"')) {
|
|
109
|
+
(open ? open.body : frame).push(line);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const code = codeOf(line);
|
|
113
|
+
if (code === undefined)
|
|
114
|
+
return undefined;
|
|
115
|
+
if (DEFINE_STMT.test(code) || INCLUDE_STMT.test(code))
|
|
116
|
+
return undefined;
|
|
117
|
+
const stmt = code.trim();
|
|
118
|
+
const start = METHOD_START.exec(stmt);
|
|
119
|
+
if (start) {
|
|
120
|
+
if (open)
|
|
121
|
+
return undefined;
|
|
122
|
+
frame.push(line);
|
|
123
|
+
open = { name: start[1], body: [] };
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (METHOD_END.test(stmt)) {
|
|
127
|
+
if (!open)
|
|
128
|
+
return undefined;
|
|
129
|
+
methods.push(open);
|
|
130
|
+
open = undefined;
|
|
131
|
+
frame.push(line);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (hasStrayKeyword(code))
|
|
135
|
+
return undefined;
|
|
136
|
+
(open ? open.body : frame).push(line);
|
|
137
|
+
}
|
|
138
|
+
if (open)
|
|
139
|
+
return undefined;
|
|
140
|
+
return { frame, methods };
|
|
141
|
+
}
|
|
142
|
+
/** METHOD / ENDMETHOD in code other than a whole `METHOD x.` / `ENDMETHOD.` line. `CALL METHOD` is a call. */
|
|
143
|
+
function hasStrayKeyword(code) {
|
|
144
|
+
for (const m of code.matchAll(KEYWORD)) {
|
|
145
|
+
const isEnd = !!m[1];
|
|
146
|
+
if (!isEnd && /\bCALL\s+$/i.test(code.slice(0, m.index)))
|
|
147
|
+
continue;
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The code part of one line: literal contents blanked, a trailing `"`
|
|
154
|
+
* comment dropped. Undefined when a literal does not close on the line.
|
|
155
|
+
*/
|
|
156
|
+
function codeOf(line) {
|
|
157
|
+
let out = '';
|
|
158
|
+
for (let i = 0; i < line.length; i++) {
|
|
159
|
+
const ch = line[i];
|
|
160
|
+
if (ch === '"')
|
|
161
|
+
break;
|
|
162
|
+
if (ch === "'" || ch === '`') {
|
|
163
|
+
let j = i + 1;
|
|
164
|
+
for (; j < line.length; j++) {
|
|
165
|
+
if (line[j] === ch) {
|
|
166
|
+
if (line[j + 1] === ch) {
|
|
167
|
+
j++;
|
|
168
|
+
continue;
|
|
169
|
+
} // doubled quote = escaped
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (j >= line.length)
|
|
174
|
+
return undefined;
|
|
175
|
+
out += ch + ' '.repeat(j - i - 1) + ch;
|
|
176
|
+
i = j;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (ch === '|') {
|
|
180
|
+
let j = i + 1;
|
|
181
|
+
for (; j < line.length; j++) {
|
|
182
|
+
if (line[j] === '\\') {
|
|
183
|
+
j++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (line[j] === '|')
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
if (j >= line.length)
|
|
190
|
+
return undefined;
|
|
191
|
+
out += '|' + ' '.repeat(j - i - 1) + '|';
|
|
192
|
+
i = j;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
out += ch;
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Runs the guard for one sap_set_source call. Classes only (the caller checks
|
|
201
|
+
* the type). Returns `refuse` with the changed methods, or `go` with the read
|
|
202
|
+
* outcome for the snapshot to reuse (undefined when nothing was read).
|
|
203
|
+
*
|
|
204
|
+
* Before refusing, the class must have NO pending inactive version — the same
|
|
205
|
+
* check sap-client's updateMethod makes (it refuses with 409 when the class is
|
|
206
|
+
* listed). Otherwise refusing would leave no way to write the class at all.
|
|
207
|
+
* That second read happens only on the refusal path; if it fails, never refuse.
|
|
208
|
+
*/
|
|
209
|
+
export async function runMethodOnlyGuard(adt, g) {
|
|
210
|
+
if (typeof adt?.getSource !== 'function')
|
|
211
|
+
return { go: undefined };
|
|
212
|
+
let outcome;
|
|
213
|
+
try {
|
|
214
|
+
outcome = { source: await adt.getSource(g.type, g.name) };
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
outcome = { error };
|
|
218
|
+
}
|
|
219
|
+
if (!('source' in outcome) || typeof outcome.source !== 'string')
|
|
220
|
+
return { go: outcome };
|
|
221
|
+
const changed = methodOnlyChange(outcome.source, g.source);
|
|
222
|
+
if (!changed)
|
|
223
|
+
return { go: outcome };
|
|
224
|
+
if (await classIsInactive(adt, g.name) !== false)
|
|
225
|
+
return { go: outcome };
|
|
226
|
+
return { refuse: changed };
|
|
227
|
+
}
|
|
228
|
+
/** true / false from the inactive-objects list; undefined when it could not be checked. */
|
|
229
|
+
async function classIsInactive(adt, name) {
|
|
230
|
+
if (typeof adt.inactiveObjects !== 'function')
|
|
231
|
+
return undefined;
|
|
232
|
+
try {
|
|
233
|
+
const upper = name.toUpperCase();
|
|
234
|
+
const list = await adt.inactiveObjects();
|
|
235
|
+
return list.some((o) => String(o.name ?? '').toUpperCase() === upper
|
|
236
|
+
&& (o.type === '' || String(o.type ?? '').toUpperCase().startsWith('CLAS')));
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -24,6 +24,7 @@ import { PKG_RE } from '../../projects/plan-objects.js';
|
|
|
24
24
|
import { askConfirm } from '../../ui/confirm-request.js';
|
|
25
25
|
import { cardSlotEmitter } from '../../ui/card-slot.js';
|
|
26
26
|
import { isHeadless } from '../../renderer/tty.js';
|
|
27
|
+
import { STOPPED_BEFORE_START_RESULT } from '../../agent/repair-partial.js';
|
|
27
28
|
const fail = (m) => ({ content: `error: ${m}`, is_error: true });
|
|
28
29
|
const NOT_CHOSEN = 'Skeleton not chosen. Nothing was saved.';
|
|
29
30
|
const TABLE_RE = /^[ZY][A-Z0-9_]{1,15}$/;
|
|
@@ -200,6 +201,9 @@ export async function rapSkeletonResolveHandler(args, ctx, deps = {}) {
|
|
|
200
201
|
ctx.chunkEmitter?.emit('info', `RAP skeleton chosen by the rules (no confirm card here): ${SKELETON_WHO[decision.by]}.`);
|
|
201
202
|
}
|
|
202
203
|
else {
|
|
204
|
+
// LT-15 D6 (review M2) — stopped during the reads above: no card, nothing saved.
|
|
205
|
+
if (ctx.signal?.aborted)
|
|
206
|
+
return { content: STOPPED_BEFORE_START_RESULT, is_error: true };
|
|
203
207
|
const card = skeletonCardRequest(decision, blocked?.reason ?? null, names, objects9);
|
|
204
208
|
const res = await (deps.confirm ?? ((r) => askConfirm(r)))(card);
|
|
205
209
|
const value = res.value;
|
package/dist/tools/sap-write.js
CHANGED
|
@@ -25,13 +25,14 @@ import crypto from 'node:crypto';
|
|
|
25
25
|
import { registerTool } from './index.js';
|
|
26
26
|
import { byObjectType } from '../sap/profile-gate.js';
|
|
27
27
|
import { verifyAndSpendApprovalId } from '../approvals/jwt.js';
|
|
28
|
+
import { writtenObjectName } from '../approvals/op-labels.js';
|
|
28
29
|
import { grantActivateMismatch } from '../approvals/grant-claims.js';
|
|
29
30
|
import { getLook } from '../renderer/look.js';
|
|
30
31
|
import { verifyActive } from './verify.js';
|
|
31
32
|
import { hasRecordedSyntaxErrorsByName, clearSyntaxErrorsAfterActivation } from './syntax-state.js';
|
|
32
33
|
import { teamFixOffers, cspeachWritesThisSession } from '../register/offer.js';
|
|
33
34
|
import { appendPending, finalizeToolCall } from '../session/pending.js';
|
|
34
|
-
import { checkTransportFit, reusedRequestRefusal } from './transport-fit-check.js';
|
|
35
|
+
import { checkTransportFit, checkTransportNeeded, reusedRequestRefusal, noteTransportNeededAsked, takeTransportNeededAsked, } from './transport-fit-check.js';
|
|
35
36
|
import { typeCode } from '../approvals/risk-floor.js';
|
|
36
37
|
import { auditLog, SapError, hasClassicParameterBlock } from '@cspeach/sap-client';
|
|
37
38
|
import { ERR } from '../errors/codes.js';
|
|
@@ -56,6 +57,9 @@ const SET_SOURCE_SPEC = {
|
|
|
56
57
|
beforeApproval: (a) => (typeCode(a.type) === 'FUNC' && hasClassicParameterBlock(String(a.source ?? ''))
|
|
57
58
|
? { error: 'classic_parameter_block', detail: FUNC_CLASSIC_BLOCK_TEXT }
|
|
58
59
|
: undefined),
|
|
60
|
+
// Rule 7a in code (2026-10-22): on an existing class, a rewrite whose only
|
|
61
|
+
// change is inside method bodies is refused — sap_update_method per method.
|
|
62
|
+
methodOnlyGuard: (a) => ({ type: a.type, name: a.name, source: String(a.source ?? '') }),
|
|
59
63
|
approval: {
|
|
60
64
|
object: (a) => a.name,
|
|
61
65
|
op: 'modify',
|
|
@@ -94,7 +98,7 @@ const SET_SOURCE_SPEC = {
|
|
|
94
98
|
syntax: { ok: syntax.ok, errors: syntax.errors },
|
|
95
99
|
activation: chainActivation(act),
|
|
96
100
|
card: {
|
|
97
|
-
object: a.name,
|
|
101
|
+
object: writtenObjectName(a.type, a.name), // LT-11: DDLX / DCLS / BDEF name their kind
|
|
98
102
|
what: 'source',
|
|
99
103
|
transport: pre.transport,
|
|
100
104
|
oldSource,
|
|
@@ -163,10 +167,26 @@ const UPDATE_METHOD_SPEC = {
|
|
|
163
167
|
methodSource: a.method_source,
|
|
164
168
|
transport: pre.transport ?? ctx.currentTransport?.get() ?? '',
|
|
165
169
|
};
|
|
170
|
+
// LT-15 D6 — a stop (Esc / Ctrl+C) during the class read below opens no
|
|
171
|
+
// preview card: the hook is not called and the pipeline returns INTERRUPTED.
|
|
172
|
+
// No signal on the ctx: the hook is passed through as before.
|
|
173
|
+
const previewHook = ctx.previewHook;
|
|
174
|
+
let stoppedBeforeCard = false;
|
|
175
|
+
const hook = ctx.signal
|
|
176
|
+
? (input) => {
|
|
177
|
+
if (ctx.signal?.aborted) {
|
|
178
|
+
stoppedBeforeCard = true;
|
|
179
|
+
return Promise.resolve({ confirmed: false });
|
|
180
|
+
}
|
|
181
|
+
return previewHook(input);
|
|
182
|
+
}
|
|
183
|
+
: previewHook;
|
|
166
184
|
try {
|
|
167
|
-
await interceptUpdateMethod(minimal, call,
|
|
185
|
+
await interceptUpdateMethod(minimal, call, hook);
|
|
168
186
|
}
|
|
169
187
|
catch (err) {
|
|
188
|
+
if (stoppedBeforeCard)
|
|
189
|
+
return { interrupted: true };
|
|
170
190
|
if (err instanceof CancelledByUserError)
|
|
171
191
|
return { error: { error: 'cancelled_by_user', detail: err.message } };
|
|
172
192
|
return { error: { error: 'preview_failed', detail: String(err) } };
|
|
@@ -302,6 +322,42 @@ registerTool({
|
|
|
302
322
|
});
|
|
303
323
|
const isDevcCreate = (a) => String(a.type ?? '').toUpperCase().split('/')[0].trim() === 'DEVC';
|
|
304
324
|
const devcTransportCreated = (pre) => pre.extra['devc_transport_created'];
|
|
325
|
+
/** LT-1 — no request resolved for a create (explicit, project and session all empty, or blank). */
|
|
326
|
+
const noTransportChosen = (pre) => !String(pre.transport ?? '').trim();
|
|
327
|
+
/**
|
|
328
|
+
* LT-1 — the transportability check for a create with no request chosen. A
|
|
329
|
+
* refusal in the REPL session arms the adoption below (fix round 1, M-3); a
|
|
330
|
+
* sub-agent ctx (no currentTransport, agent_run.ts) never arms it.
|
|
331
|
+
*/
|
|
332
|
+
async function transportNeededGate(ctx, o) {
|
|
333
|
+
const need = await checkTransportNeeded(ctx.adt, o);
|
|
334
|
+
if (need.ok)
|
|
335
|
+
return undefined;
|
|
336
|
+
if (ctx.currentTransport)
|
|
337
|
+
noteTransportNeededAsked();
|
|
338
|
+
return need.refusal;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* LT-1 — a create that landed on a request the model named right after a
|
|
342
|
+
* transport_needed question (the person's pick of an open request) makes it
|
|
343
|
+
* the session transport, as sap_transport_create does for a new one.
|
|
344
|
+
* - Fix round 1, M-3: only after that question; any other explicit request
|
|
345
|
+
* is used for that create only, as before. The next landed create uses the
|
|
346
|
+
* question up, whichever request it went to.
|
|
347
|
+
* - Fix round 1, I-1: a ctx without currentTransport (a sub-agent) never
|
|
348
|
+
* writes the process-wide session transport and never uses the question up.
|
|
349
|
+
* - Never when the session already holds a request.
|
|
350
|
+
*/
|
|
351
|
+
function adoptExplicitTransport(transport, ctx) {
|
|
352
|
+
if (!ctx.currentTransport)
|
|
353
|
+
return;
|
|
354
|
+
if (!takeTransportNeededAsked())
|
|
355
|
+
return;
|
|
356
|
+
const t = String(transport ?? '').trim().toUpperCase();
|
|
357
|
+
if (!t || (ctx.currentTransport.get() ?? null) !== null)
|
|
358
|
+
return;
|
|
359
|
+
setCurrentTransport(t);
|
|
360
|
+
}
|
|
305
361
|
const CREATE_OBJECT_SPEC = {
|
|
306
362
|
tool: 'sap_create_object',
|
|
307
363
|
// Part 2c D6 (review I-4): a module or include needs its group — refused
|
|
@@ -323,9 +379,15 @@ const CREATE_OBJECT_SPEC = {
|
|
|
323
379
|
snapshot: { kind: 'none', reason: 'a new object has no prior source' },
|
|
324
380
|
// 1.1.31: does the chosen request fit the package? (see transport-fit-check.ts)
|
|
325
381
|
// A package create decides its own transport in the walGate (§10a), so only objects.
|
|
382
|
+
// LT-1: no request chosen at all → refused when SAP says the package is transportable.
|
|
326
383
|
preGate: async (a, ctx, pre) => {
|
|
327
384
|
if (isDevcCreate(a))
|
|
328
385
|
return { ok: true };
|
|
386
|
+
if (noTransportChosen(pre)) {
|
|
387
|
+
const refusal = await transportNeededGate(ctx, { type: a.type, name: a.name, pkg: a.package });
|
|
388
|
+
if (refusal)
|
|
389
|
+
return { error: refusal };
|
|
390
|
+
}
|
|
329
391
|
const fit = await checkTransportFit(ctx.adt, { type: a.type, name: a.name, pkg: a.package, transport: pre.transport });
|
|
330
392
|
return fit.ok ? { ok: true } : { error: fit.refusal };
|
|
331
393
|
},
|
|
@@ -397,6 +459,9 @@ const CREATE_OBJECT_SPEC = {
|
|
|
397
459
|
audit: { method: 'createObject', type: (a) => a.type, name: (a) => a.name },
|
|
398
460
|
verify: { syntax: false, activation: false },
|
|
399
461
|
walResult: (_a, r) => r,
|
|
462
|
+
// LT-1: the request the person picked becomes the session transport (only when none is set).
|
|
463
|
+
after: (a, _r, _pre, ctx) => { if (!isDevcCreate(a))
|
|
464
|
+
adoptExplicitTransport(a.transport, ctx); },
|
|
400
465
|
envelope: (a, r, pre) => {
|
|
401
466
|
if (isDevcCreate(a)) {
|
|
402
467
|
const pkgName = r.objectName || String(a.name).toUpperCase();
|
|
@@ -666,6 +731,14 @@ function ddicCreateSpec(tool, type, auditType, create) {
|
|
|
666
731
|
},
|
|
667
732
|
transport: { mode: 'three-tier' },
|
|
668
733
|
snapshot: { kind: 'none', reason: 'a new object has no prior source' },
|
|
734
|
+
// LT-1: no request chosen at all → refused when SAP says the package is
|
|
735
|
+
// transportable. No fit check for a chosen request (as before).
|
|
736
|
+
preGate: async (a, ctx, pre) => {
|
|
737
|
+
if (!noTransportChosen(pre))
|
|
738
|
+
return { ok: true };
|
|
739
|
+
const refusal = await transportNeededGate(ctx, { type, name: a.name, pkg: a.package });
|
|
740
|
+
return refusal ? { error: refusal } : { ok: true };
|
|
741
|
+
},
|
|
669
742
|
walArgs: (a) => ({ name: a.name, type, package: a.package }),
|
|
670
743
|
call: async (a, ctx, pre) => {
|
|
671
744
|
const r = await create(a, ctx, pre.transport);
|
|
@@ -694,6 +767,8 @@ function ddicCreateSpec(tool, type, auditType, create) {
|
|
|
694
767
|
audit: { method: 'createObject', type: () => auditType, name: (a) => a.name },
|
|
695
768
|
verify: { syntax: false, activation: false },
|
|
696
769
|
walResult: (_a, r) => r,
|
|
770
|
+
// LT-1: the request the person picked becomes the session transport (only when none is set).
|
|
771
|
+
after: (a, _r, _pre, ctx) => adoptExplicitTransport(a.transport, ctx),
|
|
697
772
|
envelope: (_a, r) => ({
|
|
698
773
|
created: true,
|
|
699
774
|
active: true,
|
package/dist/tools/snapshot.js
CHANGED
|
@@ -62,10 +62,15 @@ registerTool({
|
|
|
62
62
|
return { content: JSON.stringify({ snapshot: entry, size: source.length }) };
|
|
63
63
|
},
|
|
64
64
|
});
|
|
65
|
-
export async function autoSnapshot(objectName, objectType, adt) {
|
|
65
|
+
export async function autoSnapshot(objectName, objectType, adt, preRead) {
|
|
66
66
|
let src;
|
|
67
67
|
try {
|
|
68
|
-
|
|
68
|
+
if (preRead === undefined)
|
|
69
|
+
src = await adt.getSource(objectType, objectName);
|
|
70
|
+
else if ('source' in preRead)
|
|
71
|
+
src = preRead.source;
|
|
72
|
+
else
|
|
73
|
+
throw preRead.error;
|
|
69
74
|
}
|
|
70
75
|
catch (err) {
|
|
71
76
|
if (isNotFound(err))
|
|
@@ -34,6 +34,8 @@ import { collectTurnAssistantText } from '../../agent/turn-assistant-text.js';
|
|
|
34
34
|
export const MAX_AGENT_DEPTH = 3;
|
|
35
35
|
export const DEFAULT_MAX_TOKENS = 100_000;
|
|
36
36
|
export const HARD_MAX_TOKENS = 200_000;
|
|
37
|
+
/** LT-15 F3 — the result of a child turn the person stopped (Esc / Ctrl+C). */
|
|
38
|
+
export const SUBAGENT_INTERRUPTED_LINE = 'interrupted: the subagent was stopped before it finished. What it did before the stop is unknown: check before relying on it.';
|
|
37
39
|
// Same NAME_PATTERN shape playbook_get uses — alphanumerics + `:_-`, ≤64.
|
|
38
40
|
const SKILL_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9:_-]{0,63}$/;
|
|
39
41
|
export async function agentRunHandler(args, ctx) {
|
|
@@ -124,6 +126,9 @@ export async function agentRunHandler(args, ctx) {
|
|
|
124
126
|
skillSource: ctx.skillSource,
|
|
125
127
|
// Task 18 fix M4 — an agent_run child never starts reader subagents.
|
|
126
128
|
subagent: true,
|
|
129
|
+
// LT-15 F3 — the parent's stop (Esc / Ctrl+C) reaches the child: its tool
|
|
130
|
+
// dispatch and the D6 card guards see it. No parent signal: no key.
|
|
131
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
127
132
|
// previewHook + pendingDispatch + currentTransport + todoEmitter
|
|
128
133
|
// intentionally omitted.
|
|
129
134
|
};
|
|
@@ -166,9 +171,18 @@ export async function agentRunHandler(args, ctx) {
|
|
|
166
171
|
chunkEmitter: nullSinkEmitter,
|
|
167
172
|
maxTokensOverride,
|
|
168
173
|
toolFilter,
|
|
174
|
+
// LT-15 F3 — the child turn stops at its own next safe point.
|
|
175
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
169
176
|
});
|
|
170
177
|
}
|
|
171
178
|
catch (err) {
|
|
179
|
+
// LT-15 F3 — stopped by the person: say so plainly, with what the child
|
|
180
|
+
// said before the stop. Every child tool_use is already paired (the loop
|
|
181
|
+
// records results before its next round, where the abort ends the turn).
|
|
182
|
+
if (ctx.signal?.aborted) {
|
|
183
|
+
const partial = collectTurnAssistantText(childSession.messages, 0).trim();
|
|
184
|
+
return { content: partial ? `${SUBAGENT_INTERRUPTED_LINE}\n\nWhat it said before the stop:\n${partial}` : SUBAGENT_INTERRUPTED_LINE, is_error: true };
|
|
185
|
+
}
|
|
172
186
|
const msg = err instanceof Error ? err.message : String(err);
|
|
173
187
|
return { content: `error: subagent failed — ${msg}`, is_error: true };
|
|
174
188
|
}
|
|
@@ -43,6 +43,11 @@ const FIORI_FAMILY = [
|
|
|
43
43
|
const TRANSPORT_FAMILY = [
|
|
44
44
|
'sap_transport_create', 'sap_transport_for_object', 'sap_transport_list', 'sap_transport_read', 'sap_transport_release',
|
|
45
45
|
];
|
|
46
|
+
// LT-9 (1A fix wave, 2026-10-24): the build skills create the session transport
|
|
47
|
+
// in place. Live: /abap-rap's search for "transport" came back with loaded
|
|
48
|
+
// tools only (the ATC crowd-out again), so it offered SE09 and handed off to
|
|
49
|
+
// /abap-transport. Release and read stay with /abap-transport.
|
|
50
|
+
const SESSION_TRANSPORT = ['sap_transport_create', 'sap_transport_list', 'sap_transport_for_object'];
|
|
46
51
|
/**
|
|
47
52
|
* Per-skill starter tools: the non-core tools a skill's body names as its
|
|
48
53
|
* working set (counted from `.claude/skills/<skill>/SKILL.md`). A tool left
|
|
@@ -51,7 +56,8 @@ const TRANSPORT_FAMILY = [
|
|
|
51
56
|
*/
|
|
52
57
|
export const SKILL_STARTERS = Object.freeze({
|
|
53
58
|
'abap-fiori-build': FIORI_FAMILY,
|
|
54
|
-
|
|
59
|
+
// ATC fix (2026-10-23): the body runs sap_atc_run before release; the API's search for "atc" is crowded out.
|
|
60
|
+
'abap-transport': [...TRANSPORT_FAMILY, 'sap_atc_run'],
|
|
55
61
|
'abap-transport-analysis': [...TRANSPORT_FAMILY, 'sap_atc_run', 'sap_usage_references'],
|
|
56
62
|
'abap-cap': ['fiori_scaffold_fe', 'fiori_render_smoke', 'fiori_draft_smoke'],
|
|
57
63
|
// Both flag-gated (LOCAL_BUILD_TOOLS / STANDALONE_FILE_TOOLS): skipped when off.
|
|
@@ -59,7 +65,7 @@ export const SKILL_STARTERS = Object.freeze({
|
|
|
59
65
|
'abap-atc-fix': ['sap_atc_run'],
|
|
60
66
|
'abap-cca': ['team_estate', 'sap_atc_run', 'sap_usage_references', 'sap_api_state'],
|
|
61
67
|
'abap-clean-core': ['sap_api_state', 'sap_atc_run', 'sap_usage_references', 'team_findings', 'team_lane_move'],
|
|
62
|
-
'abap-data-model': ['sap_create_domain', 'sap_create_data_element'],
|
|
68
|
+
'abap-data-model': ['sap_create_domain', 'sap_create_data_element', ...SESSION_TRANSPORT],
|
|
63
69
|
'abap-document': ['sap_usage_references'],
|
|
64
70
|
'abap-handover': ['sap_usage_references'],
|
|
65
71
|
'abap-impact': ['sap_usage_references', 'sap_atc_run'],
|
|
@@ -70,22 +76,24 @@ export const SKILL_STARTERS = Object.freeze({
|
|
|
70
76
|
'abap-incident': ['sap_short_dump', 'sap_snapshot_take', 'sap_atc_run', 'sap_run_unit_test', 'sap_api_state', 'sap_diff', 'sap_run'],
|
|
71
77
|
'abap-eml': ['sap_run_unit_test', 'sap_run'],
|
|
72
78
|
'abap-test': ['sap_run_unit_test', 'sap_set_class_include', 'sap_run'],
|
|
73
|
-
'abap-extend-model': ['extend_model_insert', 'sap_snapshot_take', 'sap_service_binding_publish', 'sap_set_class_include'],
|
|
79
|
+
'abap-extend-model': ['extend_model_insert', 'sap_snapshot_take', 'sap_service_binding_publish', 'sap_set_class_include', ...SESSION_TRANSPORT],
|
|
74
80
|
// RAP generator (2026-10-14): SAP's generator writes the stack; the finish check proves F1–F5.
|
|
75
81
|
// Final review M5 — standalone /abap-rap calls rap_skeleton_resolve before it builds.
|
|
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'],
|
|
82
|
+
'abap-rap': ['sap_set_class_include', 'sap_service_binding_publish', 'sap_class_includes', 'rap_skeleton_resolve', 'sap_generate_objects', 'rap_finish_check', 'sap_run', ...SESSION_TRANSPORT],
|
|
77
83
|
// Plan steps load tools by the PLAN skill (session.tool_set_skill / params.skill), not the
|
|
78
84
|
// delegate (which only rides as the X-CSPeach-Delegate-Skill header), so the RAP
|
|
79
85
|
// skeleton + finish phases need the generator and the finish check here too.
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
// ATC fix (2026-10-23): abap-plan writes "ATC clean" into exit gates; the API's tool search for
|
|
87
|
+
// "atc" is crowded out by loaded tools ("batch", "dispatch"), so the plan step loads sap_atc_run.
|
|
88
|
+
'abap-plan': ['rap_skeleton_resolve', 'sap_generate_objects', 'rap_finish_check', 'sap_atc_run', ...SESSION_TRANSPORT],
|
|
89
|
+
'abap-modernize': ['sap_api_state', 'sap_snapshot_take', 'sap_service_binding_publish', 'sap_atc_run', ...SESSION_TRANSPORT],
|
|
82
90
|
'abap-segw': ['sap_snapshot_take', 'sap_odata_test'],
|
|
83
91
|
'abap-upgrade-fix': ['sap_snapshot_take', 'glob'],
|
|
84
92
|
'abap-upgrade-scan': ['sap_atc_run'],
|
|
85
93
|
'abap-upgrade-verify': ['sap_atc_run'],
|
|
86
94
|
// Selection texts (2026-10-04): a new PARAMETERS / SELECT-OPTIONS gets its text in the same change.
|
|
87
|
-
'abap-refactor': ['sap_get_text_elements', 'sap_set_text_elements'],
|
|
88
|
-
'abap-generate': ['sap_get_text_elements', 'sap_set_text_elements', 'sap_run'],
|
|
95
|
+
'abap-refactor': ['sap_get_text_elements', 'sap_set_text_elements', ...SESSION_TRANSPORT],
|
|
96
|
+
'abap-generate': ['sap_get_text_elements', 'sap_set_text_elements', 'sap_run', ...SESSION_TRANSPORT],
|
|
89
97
|
// Chat lane (D5): the hand-off tool is loaded on chat turns; flag-gated, so a skills-lane session never lists it.
|
|
90
98
|
// `_chat` is the one SKILL_STARTERS key that is not a catalog skill (pinned in tool-loading.test.ts).
|
|
91
99
|
'_chat': ['hand_off'],
|