@cspeach/cli 1.1.23 → 1.1.24
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/loop.js +124 -0
- package/dist/classifier/client.js +27 -2
- package/dist/commands/plan-continue.js +11 -1
- package/dist/commands/plan-resume.js +38 -1
- package/dist/lane/app-chain.js +297 -0
- package/dist/lane/app-flow.js +55 -0
- package/dist/lane/card.js +148 -0
- package/dist/lane/chain-token.js +59 -0
- package/dist/lane/inputs.js +459 -0
- package/dist/lane/paths.js +21 -0
- package/dist/lane/resolve.js +109 -0
- package/dist/lane/roots.js +113 -0
- package/dist/lane/signals.js +80 -0
- package/dist/lane/store.js +232 -0
- package/dist/lane/types.js +1 -0
- package/dist/one-shot.js +1 -0
- package/dist/projects/extract-plan.js +46 -6
- package/dist/projects/plan-lane.js +214 -0
- package/dist/projects/plan-schema.js +10 -0
- package/dist/projects/promote-command.js +8 -2
- package/dist/projects/save-command.js +71 -25
- package/dist/projects/workspace.js +10 -0
- package/dist/renderer/tool-labels.js +2 -1
- package/dist/repl/resume-standalone.js +3 -0
- package/dist/repl.js +23 -2
- package/dist/skill-catalog.js +14 -6
- package/dist/skills/bundled-skills.js +61 -54
- package/dist/tools/lane/app-lane-resolve.js +329 -0
- package/dist/tools/local-build.js +4 -0
- package/dist/tools/tool-loading.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// cspeach-cli/src/tools/lane/app-lane-resolve.ts
|
|
2
|
+
//
|
|
3
|
+
// App lane routing, slice 1, Task 5 — `app_lane_resolve`. The model fills the
|
|
4
|
+
// signal sheet (values + evidence quotes); everything else is CLI code:
|
|
5
|
+
//
|
|
6
|
+
// sheet check → spec text + quote strength → system facts, pin, standards
|
|
7
|
+
// → resolveLane → lane card (or the headless STOP) → runtime card when a CAP
|
|
8
|
+
// lane has no runtime → sha-pinned decision file → app-flow state → the CLI
|
|
9
|
+
// queues `/abap-plan --from @<specGap>` through the harness slot.
|
|
10
|
+
//
|
|
11
|
+
// Security posture:
|
|
12
|
+
// - The sheet is validated with the strict schema before anything else runs;
|
|
13
|
+
// errors name only the bad keys (sanitised), never values.
|
|
14
|
+
// - The spec path goes through readSpecText (string + realpath containment,
|
|
15
|
+
// denylist, size caps). Errors name the path only, never spec content.
|
|
16
|
+
// - Card strings come from card.ts (sanitised, capped). STOP / summary text
|
|
17
|
+
// passes every model-influenced line through cardLine.
|
|
18
|
+
// - The decision file is written only by store.ts (validated, never overwrites).
|
|
19
|
+
// - The runtime answer is written only by writeRuntimeAnswer, from a card value.
|
|
20
|
+
// - pendingDispatch gets a CLI-built string only: a fixed prefix plus the
|
|
21
|
+
// session's spec-gap path, through the shared chainFromToken (lane/chain-token.ts).
|
|
22
|
+
import { promises as fs } from 'node:fs';
|
|
23
|
+
import * as path from 'node:path';
|
|
24
|
+
import { registerTool, isStandalone } from '../index.js';
|
|
25
|
+
import { PathOutsideRootError } from '../_filesystem-shared.js';
|
|
26
|
+
import { assertNotCliHome, containSpecPath, laneRootFor, specDisplayPath } from '../../lane/roots.js';
|
|
27
|
+
import { signalSheetSchema, withStrength } from '../../lane/signals.js';
|
|
28
|
+
import { factsFromStandalone, factsFromSystemInfo, findQuote, readLanePin, readSpecText, readStandardsLane, writeRuntimeAnswer, } from '../../lane/inputs.js';
|
|
29
|
+
import { BUILT_LANES, resolveLane, uiFor } from '../../lane/resolve.js';
|
|
30
|
+
import { writeLaneDecision } from '../../lane/store.js';
|
|
31
|
+
import { getAppFlow, setLane } from '../../lane/app-flow.js';
|
|
32
|
+
import { chainFromToken } from '../../lane/chain-token.js';
|
|
33
|
+
import { cardLine, interactiveRunLine, laneCardRequest, quoteNotFoundFacts, runtimeCardRequest, shownToken, } from '../../lane/card.js';
|
|
34
|
+
import { cardSlotEmitter } from '../../ui/card-slot.js';
|
|
35
|
+
import { askConfirm } from '../../ui/confirm-request.js';
|
|
36
|
+
import { isHeadless } from '../../renderer/tty.js';
|
|
37
|
+
import { loadConfig } from '../../config/loader.js';
|
|
38
|
+
import { getSapSystemInfo } from '../../sap/system-info.js';
|
|
39
|
+
const fail = (message) => ({ content: `error: ${message}`, is_error: true });
|
|
40
|
+
const NOT_CHOSEN = 'Lane not chosen. Nothing was saved.';
|
|
41
|
+
const STOP = 'STOP — lane needs your choice.';
|
|
42
|
+
const UNKNOWN_FACTS = { source: 'unknown', tier: 'unknown', label: 'unknown' };
|
|
43
|
+
const RUNTIMES = ['btp', 'own-server', 'none'];
|
|
44
|
+
/** A sheet key for an error line: identifier characters only, capped. */
|
|
45
|
+
const keyName = (k) => String(k).replace(/[^A-Za-z0-9_]/g, '').slice(0, 40) || '(unnamed)';
|
|
46
|
+
function sheetError(issues) {
|
|
47
|
+
const bad = new Set();
|
|
48
|
+
let notObject = false;
|
|
49
|
+
for (const i of issues) {
|
|
50
|
+
if (i.code === 'unrecognized_keys' && i.path.length === 0) {
|
|
51
|
+
for (const k of i.keys ?? [])
|
|
52
|
+
bad.add(`${keyName(k)} (unknown key)`);
|
|
53
|
+
}
|
|
54
|
+
else if (i.path.length === 0) {
|
|
55
|
+
notObject = true;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
bad.add(keyName(i.path[0]));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (notObject && bad.size === 0)
|
|
62
|
+
return 'signals must be an object with one entry per signal key.';
|
|
63
|
+
return `the signal sheet is not valid. Fix these keys and call again: ${[...bad].join(', ')}.`;
|
|
64
|
+
}
|
|
65
|
+
async function defaultFacts(ctx) {
|
|
66
|
+
try {
|
|
67
|
+
if (isStandalone(ctx)) {
|
|
68
|
+
return factsFromStandalone((await loadConfig()).standalone);
|
|
69
|
+
}
|
|
70
|
+
return factsFromSystemInfo(await getSapSystemInfo(ctx.adt, ctx.sapAlias));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { ...UNKNOWN_FACTS };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Headless, or no Ink card slot (classic REPL / one-shot): a card could never be answered. */
|
|
77
|
+
function defaultHeadless() {
|
|
78
|
+
return isHeadless() || cardSlotEmitter.listenerCount('confirm') === 0;
|
|
79
|
+
}
|
|
80
|
+
/** M1 — a person is at the terminal but no card can be shown (the classic REPL). */
|
|
81
|
+
function defaultNoCardTerminal() {
|
|
82
|
+
return !isHeadless() && cardSlotEmitter.listenerCount('confirm') === 0;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* True when the app flow's `@spec` token and the given absolute path are the
|
|
86
|
+
* same real file inside cwd or the workspace folder (I3: the same containment
|
|
87
|
+
* as readSpecText).
|
|
88
|
+
*/
|
|
89
|
+
async function sameFile(cwd, token, abs) {
|
|
90
|
+
const tok = token.trim().replace(/^@/, '');
|
|
91
|
+
if (tok === '')
|
|
92
|
+
return false;
|
|
93
|
+
try {
|
|
94
|
+
const { real: a } = await containSpecPath(cwd, tok);
|
|
95
|
+
const b = await fs.realpath(abs);
|
|
96
|
+
const norm = (p) => (process.platform === 'win32' ? path.resolve(p).toLowerCase() : path.resolve(p));
|
|
97
|
+
return norm(a) === norm(b);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const LANE_WORDS = {
|
|
104
|
+
rap: 'on-stack RAP', cap: 'CAP (side-by-side)', none: 'UI only', 'abap-report': 'classic report', segw: 'SEGW',
|
|
105
|
+
};
|
|
106
|
+
function stopText(d, extra, specLine) {
|
|
107
|
+
const lines = [STOP, specLine];
|
|
108
|
+
if (d.needsRuntime && !d.neutral && !d.noAppLane) {
|
|
109
|
+
lines.push(`Recommended lane: ${LANE_WORDS.cap}. The CAP lane needs to know where the app will run.`);
|
|
110
|
+
}
|
|
111
|
+
else if (d.backend && !d.noAppLane) {
|
|
112
|
+
lines.push(`Recommended lane: ${LANE_WORDS[d.backend]}.`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
lines.push('No app lane is available on this system.');
|
|
116
|
+
}
|
|
117
|
+
lines.push('Why:');
|
|
118
|
+
for (const r of [...d.reasons, ...extra])
|
|
119
|
+
lines.push(`- ${cardLine(r)}`);
|
|
120
|
+
if (d.needsRuntime && !d.neutral && !d.noAppLane) {
|
|
121
|
+
lines.push('To confirm: add `[app-lane] runtime = "btp|own-server|none"` to .cspeach/conventions.toml and run /abap-app again.');
|
|
122
|
+
}
|
|
123
|
+
else if (d.backend && !d.noAppLane) {
|
|
124
|
+
lines.push(`To confirm: Add \`[app-lane] backend = "${d.backend}"\` to .cspeach/conventions.toml and run /abap-app again.`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const choices = d.allowed.filter((l) => l !== 'segw').join(', ');
|
|
128
|
+
lines.push(`To choose: add \`[app-lane] backend = "<lane>"\` (one of: ${choices}) to .cspeach/conventions.toml and run /abap-app again.`);
|
|
129
|
+
}
|
|
130
|
+
lines.push('Nothing was saved.');
|
|
131
|
+
return lines.join('\n');
|
|
132
|
+
}
|
|
133
|
+
export async function appLaneResolveHandler(args, ctx, deps = {}) {
|
|
134
|
+
// 0. Task 7 review round 2 — inside an /abap-app flow the lane is chosen
|
|
135
|
+
// only at the lane step (after the spec-gap save, before the plan). Nothing
|
|
136
|
+
// else runs: no card, no file, the flow and the queue stay as they are.
|
|
137
|
+
// No flow at all → today's behaviour.
|
|
138
|
+
const step = getAppFlow(ctx.session)?.expect;
|
|
139
|
+
if (step !== undefined && step !== 'lane') {
|
|
140
|
+
return fail(step === 'spec-gap'
|
|
141
|
+
? 'the app flow is not at the lane step yet: save the spec-gap first.'
|
|
142
|
+
: 'the app flow is not at the lane step: the lane is already chosen and the plan is next.');
|
|
143
|
+
}
|
|
144
|
+
// 1. The sheet, strictly, before anything else.
|
|
145
|
+
const parsed = signalSheetSchema.safeParse(args?.signals);
|
|
146
|
+
if (!parsed.success)
|
|
147
|
+
return fail(sheetError(parsed.error.issues));
|
|
148
|
+
if (typeof args?.specPath !== 'string' || args.specPath.trim() === '') {
|
|
149
|
+
return fail('specPath must be the spec file path, relative to the workspace.');
|
|
150
|
+
}
|
|
151
|
+
const specPath = args.specPath;
|
|
152
|
+
// 2. Spec text (contained) and signal strength.
|
|
153
|
+
let spec;
|
|
154
|
+
try {
|
|
155
|
+
spec = await readSpecText(ctx.cwd, specPath);
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
if (e instanceof PathOutsideRootError)
|
|
159
|
+
return fail('the spec path must be inside the workspace.');
|
|
160
|
+
return fail(cardLine(e instanceof Error ? e.message : 'the spec file cannot be read.'));
|
|
161
|
+
}
|
|
162
|
+
const specSha = spec.sha256;
|
|
163
|
+
const specRel = specDisplayPath(ctx.cwd, spec.abs);
|
|
164
|
+
// The lane check must use the spec /abap-app was started with: a model may
|
|
165
|
+
// not match its quotes against some other workspace file (one it just wrote).
|
|
166
|
+
const flow = getAppFlow(ctx.session);
|
|
167
|
+
if (flow && !(await sameFile(ctx.cwd, flow.specToken, spec.abs))) {
|
|
168
|
+
return fail(cardLine(`This lane check must use the spec you started /abap-app with: ${flow.specToken}`));
|
|
169
|
+
}
|
|
170
|
+
const specLine = cardLine(`Spec: ${specRel}`);
|
|
171
|
+
const signals = withStrength(parsed.data, spec.text, findQuote, spec.normalise);
|
|
172
|
+
// 3–4. Facts, pin, standards.
|
|
173
|
+
const facts = await (deps.facts ?? defaultFacts)(ctx).catch(() => ({ ...UNKNOWN_FACTS }));
|
|
174
|
+
// R18 — the project pin lives on the lane root, next to .cspeach/lane.
|
|
175
|
+
const laneRoot = await laneRootFor(ctx.cwd);
|
|
176
|
+
// Final residual 2 — a lane root that is the CLI home is refused before any read or card.
|
|
177
|
+
try {
|
|
178
|
+
await assertNotCliHome(laneRoot);
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
return fail(cardLine(e instanceof Error ? e.message : String(e)));
|
|
182
|
+
}
|
|
183
|
+
const [pinR, stdR] = await Promise.all([readLanePin(laneRoot), readStandardsLane(ctx.cwd)]);
|
|
184
|
+
const warnings = [pinR.warning, stdR.warning].filter((w) => !!w);
|
|
185
|
+
const runtimeKnown = !!pinR.pin?.runtime || !!stdR.pin?.runtime;
|
|
186
|
+
// 5. Resolve.
|
|
187
|
+
const decision = resolveLane({ signals, facts, pin: pinR.pin, standards: stdR.pin, runtimeKnown });
|
|
188
|
+
const extraFacts = [...quoteNotFoundFacts(signals), ...warnings.map(cardLine)];
|
|
189
|
+
const cardFacts = [specLine, ...extraFacts];
|
|
190
|
+
let finalLane;
|
|
191
|
+
let decidedBy = decision.decidedBy;
|
|
192
|
+
let runtime = pinR.pin?.runtime ?? stdR.pin?.runtime;
|
|
193
|
+
const headless = (deps.headless ?? defaultHeadless)();
|
|
194
|
+
if (headless) {
|
|
195
|
+
// 6. Headless: never a card. Choices that need a person stop here.
|
|
196
|
+
if (decision.neutral || decision.noAppLane || decision.needsRuntime || !decision.backend) {
|
|
197
|
+
return { content: stopText(decision, extraFacts, specLine) };
|
|
198
|
+
}
|
|
199
|
+
finalLane = { backend: decision.backend, ui: decision.ui, ...(decision.floorplan ? { floorplan: decision.floorplan } : {}) };
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// 7. The lane card.
|
|
203
|
+
const confirm = deps.confirm ?? ((req) => askConfirm(req));
|
|
204
|
+
const card = laneCardRequest(decision);
|
|
205
|
+
const res = await confirm({ ...card, facts: [...(card.facts ?? []), ...cardFacts] });
|
|
206
|
+
const value = res.value;
|
|
207
|
+
const offered = new Set(card.choices.map((c) => c.value));
|
|
208
|
+
if (!value || value === 'cancel' || !offered.has(value))
|
|
209
|
+
return { content: NOT_CHOSEN };
|
|
210
|
+
if (value === 'confirm') {
|
|
211
|
+
if (!decision.backend || decision.noAppLane)
|
|
212
|
+
return { content: NOT_CHOSEN };
|
|
213
|
+
finalLane = { backend: decision.backend, ui: decision.ui, ...(decision.floorplan ? { floorplan: decision.floorplan } : {}) };
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
const lane = value.slice('switch:'.length);
|
|
217
|
+
if (!value.startsWith('switch:') || !BUILT_LANES.includes(lane) || !decision.allowed.includes(lane)) {
|
|
218
|
+
return { content: NOT_CHOSEN };
|
|
219
|
+
}
|
|
220
|
+
const usable = (p) => (p && (!p.backend || decision.allowed.includes(p.backend)) ? p : undefined);
|
|
221
|
+
const { ui, floorplan } = uiFor(lane, signals, usable(pinR.pin)?.ui ?? usable(stdR.pin)?.ui);
|
|
222
|
+
finalLane = { backend: lane, ui, ...(floorplan ? { floorplan } : {}) };
|
|
223
|
+
decidedBy = 'developer';
|
|
224
|
+
}
|
|
225
|
+
// 8. A CAP lane needs a runtime.
|
|
226
|
+
if (finalLane.backend === 'cap' && !runtime) {
|
|
227
|
+
const rr = await confirm(runtimeCardRequest());
|
|
228
|
+
const answer = rr.value;
|
|
229
|
+
if (!answer || !RUNTIMES.includes(answer))
|
|
230
|
+
return { content: 'Runtime not chosen. Nothing was saved.' };
|
|
231
|
+
try {
|
|
232
|
+
await assertNotCliHome(laneRoot);
|
|
233
|
+
await writeRuntimeAnswer(laneRoot, answer);
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
const why = e instanceof Error ? e.message : 'the runtime answer could not be saved';
|
|
237
|
+
return {
|
|
238
|
+
content: `${cardLine(why)}\nThe runtime is still unknown, so the lane was not saved. Nothing was saved. ` +
|
|
239
|
+
'Add the runtime to .cspeach/conventions.toml, then run /abap-app again.',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
runtime = answer;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// 9. The decision file, the flow state, the queued plan.
|
|
246
|
+
if (finalLane.backend === 'cap' && runtime)
|
|
247
|
+
finalLane = { ...finalLane, runtime };
|
|
248
|
+
// Task 7 review R10 — the shared chain token: CLI state only (the app flow's
|
|
249
|
+
// saved spec-gap path, never a tool argument), contained to cwd or the save root.
|
|
250
|
+
const gap = flow?.specGapPath ? await chainFromToken(ctx.cwd, flow.specGapPath) : undefined;
|
|
251
|
+
const planCmd = gap ? `/abap-plan --from ${gap.token}` : undefined;
|
|
252
|
+
let written;
|
|
253
|
+
try {
|
|
254
|
+
// I3 — one lane root for the write, every pinned read and from.lane.path.
|
|
255
|
+
await assertNotCliHome(laneRoot);
|
|
256
|
+
written = await writeLaneDecision(laneRoot, {
|
|
257
|
+
spec: { path: specRel, sha256: specSha },
|
|
258
|
+
...(gap ? { specGap: { path: gap.path } } : {}),
|
|
259
|
+
recommendation: decision,
|
|
260
|
+
final: finalLane,
|
|
261
|
+
decidedBy,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
catch (e) {
|
|
265
|
+
return fail(`the lane decision could not be saved: ${e instanceof Error ? cardLine(e.message) : 'write failed'}`);
|
|
266
|
+
}
|
|
267
|
+
setLane(ctx.session, written.path, written.sha256, planCmd);
|
|
268
|
+
const who = decidedBy === 'developer' ? 'the developer'
|
|
269
|
+
: decidedBy === 'pin' ? `project pin (${decision.decidedByPath ?? '.cspeach/conventions.toml'})`
|
|
270
|
+
: decidedBy === 'standards' ? `standards file (${decision.decidedByPath ?? 'unknown file'})`
|
|
271
|
+
: 'the spec and the rules';
|
|
272
|
+
const ui = finalLane.ui === 'none' ? '' : ` + ${finalLane.ui}${finalLane.floorplan ? ` (${finalLane.floorplan})` : ''}`;
|
|
273
|
+
const lines = [
|
|
274
|
+
`Lane: ${finalLane.backend}${ui}${finalLane.runtime ? `, runtime ${finalLane.runtime}` : ''}.`,
|
|
275
|
+
`Decided by: ${cardLine(who)}.`,
|
|
276
|
+
`Decision file: ${written.path}`,
|
|
277
|
+
];
|
|
278
|
+
if (warnings.length)
|
|
279
|
+
lines.push(...warnings.map((w) => `Warning: ${cardLine(w)}`));
|
|
280
|
+
// M1 — the classic REPL has no card listener, so the rules took the lane:
|
|
281
|
+
// tell the person at the terminal in one plain line (no inquirer fallback).
|
|
282
|
+
if (headless && (deps.noCardTerminal ?? defaultNoCardTerminal)()) {
|
|
283
|
+
const shown = `${finalLane.backend}${ui}${finalLane.runtime ? `, runtime ${finalLane.runtime}` : ''}`;
|
|
284
|
+
ctx.chunkEmitter?.emit('info', `Lane chosen by the rules (no confirm card in this terminal): ${shown}.`);
|
|
285
|
+
}
|
|
286
|
+
if (flow?.specGapPath && planCmd) {
|
|
287
|
+
ctx.pendingDispatch?.set(planCmd);
|
|
288
|
+
lines.push('Next: the plan is being created.');
|
|
289
|
+
}
|
|
290
|
+
else if (flow?.specGapPath && headless) {
|
|
291
|
+
// I4(c) — headless / one-shot: no flow reaches a later run.
|
|
292
|
+
lines.push(interactiveRunLine(flow.specToken));
|
|
293
|
+
}
|
|
294
|
+
else if (flow?.specGapPath) {
|
|
295
|
+
// I4(b) — the flow is at the plan step: this typed command keeps it.
|
|
296
|
+
lines.push(`The spec-gap file path could not be used to queue the plan. To go on, run: /abap-plan --from ${shownToken(path.basename(flow.specGapPath), 'spec-gap file')}`);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
lines.push('No spec-gap file in this session yet, so no plan was queued.');
|
|
300
|
+
if (headless)
|
|
301
|
+
lines.push(interactiveRunLine(specRel));
|
|
302
|
+
}
|
|
303
|
+
return { content: lines.join('\n') };
|
|
304
|
+
}
|
|
305
|
+
registerTool({
|
|
306
|
+
name: 'app_lane_resolve',
|
|
307
|
+
description: 'App lane routing for /abap-app. Pass the spec file path and the signal sheet: for each fixed key a value and, ' +
|
|
308
|
+
'when the value is true / a service name, an evidence quote copied word for word from the spec (max 300 chars). ' +
|
|
309
|
+
'The CLI checks the quotes, reads the system facts, the project pin and the standards file, picks the lane, ' +
|
|
310
|
+
'shows the lane card, saves the decision file and queues the plan. Never guess a lane yourself. ' +
|
|
311
|
+
'Keys: app_requested, ui_only_service (string|null), report_only, analytical, transactional, custom_ui, ' +
|
|
312
|
+
'cap_external_users, cap_multi_system, cap_own_release_cycle, cap_ui_on_released_api.',
|
|
313
|
+
isMutating: false,
|
|
314
|
+
requiresSap: false,
|
|
315
|
+
category: 'project',
|
|
316
|
+
flagGated: true,
|
|
317
|
+
input_schema: {
|
|
318
|
+
type: 'object',
|
|
319
|
+
properties: {
|
|
320
|
+
specPath: { type: 'string', description: 'Spec file, relative to the workspace (.md, .txt, .docx, .pdf, .xlsx).' },
|
|
321
|
+
signals: {
|
|
322
|
+
type: 'object',
|
|
323
|
+
description: 'One entry per key: { value, evidence? }. evidence = an exact quote from the spec.',
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
required: ['specPath', 'signals'],
|
|
327
|
+
},
|
|
328
|
+
handler: (args, ctx) => appLaneResolveHandler(args, ctx),
|
|
329
|
+
});
|
|
@@ -82,6 +82,8 @@ export const LOCAL_BUILD_TOOLS = [
|
|
|
82
82
|
'sap_link',
|
|
83
83
|
// CAP phase 2 (Task 11) — S/4 link check: live read vs direct S/4, dev-only update-then-restore.
|
|
84
84
|
'cap_link_smoke',
|
|
85
|
+
// App lane routing (slice 1, Task 5) — reads the spec file like read_document; writes only .cspeach/lane + [app-lane].
|
|
86
|
+
'app_lane_resolve',
|
|
85
87
|
'extend_model_insert',
|
|
86
88
|
'system_capability',
|
|
87
89
|
'agent_run',
|
|
@@ -109,6 +111,8 @@ export const STANDALONE_FILE_TOOLS = [
|
|
|
109
111
|
'file_edit',
|
|
110
112
|
'glob',
|
|
111
113
|
'grep',
|
|
114
|
+
// App lane routing — /abap-app in standalone mode decides the lane on declared facts.
|
|
115
|
+
'app_lane_resolve',
|
|
112
116
|
];
|
|
113
117
|
export function applyStandaloneFileTools(cwd) {
|
|
114
118
|
enableToolFlags([...STANDALONE_FILE_TOOLS]);
|
|
@@ -53,6 +53,8 @@ export const SKILL_STARTERS = Object.freeze({
|
|
|
53
53
|
'abap-transport': TRANSPORT_FAMILY,
|
|
54
54
|
'abap-transport-analysis': [...TRANSPORT_FAMILY, 'sap_atc_run', 'sap_usage_references'],
|
|
55
55
|
'abap-cap': ['fiori_scaffold_fe', 'fiori_render_smoke', 'fiori_draft_smoke'],
|
|
56
|
+
// Both flag-gated (LOCAL_BUILD_TOOLS / STANDALONE_FILE_TOOLS): skipped when off.
|
|
57
|
+
'abap-app': ['app_lane_resolve', 'read_document'],
|
|
56
58
|
'abap-atc-fix': ['sap_atc_run'],
|
|
57
59
|
'abap-cca': ['sap_atc_run', 'sap_usage_references', 'sap_api_state'],
|
|
58
60
|
'abap-clean-core': ['sap_api_state', 'sap_atc_run', 'sap_usage_references'],
|