@cspeach/cli 1.1.13 → 1.1.15

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.
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Project Register on disk (spec §3, §3a).
3
+ *
4
+ * Two places, both chosen by a person, never by the model:
5
+ * - the TEAM PROJECT FOLDER the lead creates: cspeach-team.json, baseline/,
6
+ * lanes.json, rulings/. Collected records sit under writers/.
7
+ * - this laptop's OUTBOX: where this machine's records are written. It mirrors
8
+ * the project-folder layout (writers/<id>/records/…) so that handing records
9
+ * in is a plain folder copy. By default it lives under ~/.cspeach; a developer
10
+ * may point it inside their own OneDrive-synced folder instead.
11
+ *
12
+ * CSPeach writes only to paths on this list. It never writes to a network
13
+ * location on its own initiative.
14
+ */
15
+ import { createHash, randomBytes } from 'node:crypto';
16
+ import fs from 'node:fs';
17
+ import os from 'node:os';
18
+ import path from 'node:path';
19
+ import { cspeachRoot } from '../config/paths.js';
20
+ export const TEAM_FILE = 'cspeach-team.json';
21
+ export const BASELINE_FILE = path.join('baseline', 'objects.json');
22
+ export const LANES_FILE = 'lanes.json';
23
+ export class RegisterError extends Error {
24
+ }
25
+ export const defaultRand = (bytes) => randomBytes(bytes).toString('hex');
26
+ const stateFile = () => path.join(cspeachRoot(), 'register.json');
27
+ export function loadState() {
28
+ try {
29
+ const s = JSON.parse(stripBom(fs.readFileSync(stateFile(), 'utf8')));
30
+ if (s && typeof s === 'object' && s.projects)
31
+ return { active: s.active ?? null, projects: s.projects };
32
+ }
33
+ catch { /* first use, or unreadable: start empty */ }
34
+ return { active: null, projects: {} };
35
+ }
36
+ export function saveState(state) {
37
+ fs.mkdirSync(cspeachRoot(), { recursive: true });
38
+ fs.writeFileSync(stateFile(), `${JSON.stringify(state, null, 2)}\n`);
39
+ }
40
+ /** The team project this laptop is recording against, or null. Cheap: one small file read. */
41
+ export function activeProject() {
42
+ const s = loadState();
43
+ if (!s.active || !s.projects[s.active])
44
+ return null;
45
+ return { projectId: s.active, ...s.projects[s.active] };
46
+ }
47
+ export function defaultOutboxDir(projectId) {
48
+ return path.join(cspeachRoot(), 'register', projectId, 'outbox');
49
+ }
50
+ /** `mike-khan-7f3a`: the person plus this machine, so one file never has two writers. */
51
+ export function writerIdFor(name, machine = os.hostname()) {
52
+ const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'writer';
53
+ return `${slug}-${createHash('sha256').update(machine.toLowerCase()).digest('hex').slice(0, 4)}`;
54
+ }
55
+ export function projectIdFor(name, rand = defaultRand) {
56
+ const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'project';
57
+ return `${slug}-${rand(2)}`;
58
+ }
59
+ export const sha256 = (text) => createHash('sha256').update(text).digest('hex');
60
+ /**
61
+ * A byte-order mark, taken off by NUMBER. Notepad, Excel and a few sync tools
62
+ * put one in front of a file they save as UTF-8; `JSON.parse` then refuses the
63
+ * whole file. Written as a character in a regex it is invisible in the source —
64
+ * which is how two of these were copied around and a third was forgotten — so
65
+ * this is the one place that knows about it, and it names the code point.
66
+ */
67
+ export const stripBom = (text) => (text.charCodeAt(0) === 0xfeff ? text.slice(1) : text);
68
+ function readJson(file) {
69
+ let text;
70
+ try {
71
+ text = fs.readFileSync(file, 'utf8');
72
+ }
73
+ catch {
74
+ throw new RegisterError(`Cannot read ${file}`);
75
+ }
76
+ try {
77
+ return JSON.parse(stripBom(text));
78
+ }
79
+ catch {
80
+ throw new RegisterError(`${file} is not valid JSON`);
81
+ }
82
+ }
83
+ export function readProjectFile(projectDir) {
84
+ const p = readJson(path.join(projectDir, TEAM_FILE));
85
+ if (p?.kind !== 'team-project' || typeof p.projectId !== 'string' || typeof p.lead?.name !== 'string') {
86
+ throw new RegisterError(`${path.join(projectDir, TEAM_FILE)} is not a CSPeach team project file`);
87
+ }
88
+ return p;
89
+ }
90
+ /**
91
+ * What SAP accepts as a check variant name. Checked by every caller BEFORE a
92
+ * run, not only at the moment of writing: a name that cannot be written must
93
+ * not be measured with either.
94
+ */
95
+ export const VARIANT_NAME = /^[A-Z0-9_/]{1,40}$/;
96
+ /**
97
+ * The one field of the team project file that is set after `init`: which ATC
98
+ * check variant this team project measures with. Written whole, then renamed,
99
+ * like a record — a sync tool must never pick up half a team project file.
100
+ */
101
+ export function writeProjectVariant(projectDir, variant) {
102
+ if (!VARIANT_NAME.test(variant))
103
+ throw new RegisterError(`"${variant}" is not an ATC check variant name.`);
104
+ const file = path.join(projectDir, TEAM_FILE);
105
+ const next = { ...readProjectFile(projectDir), atcVariant: variant };
106
+ fs.writeFileSync(`${file}.tmp`, `${JSON.stringify(next, null, 2)}\n`);
107
+ fs.renameSync(`${file}.tmp`, file);
108
+ }
109
+ /**
110
+ * One read, one sentence. `existsSync` then `readFileSync` answers only the
111
+ * easy failure: a folder halfway through a sync also gives EISDIR (a folder
112
+ * where the file goes), EBUSY and EPERM (a file the sync tool still holds, or
113
+ * one OneDrive has not fetched). None of those may reach a person as a stack.
114
+ */
115
+ function readTextOrSay(file, missing) {
116
+ try {
117
+ return fs.readFileSync(file, 'utf8');
118
+ }
119
+ catch (e) {
120
+ if (e?.code === 'ENOENT')
121
+ throw new RegisterError(missing);
122
+ throw new RegisterError(`${file} could not be read. It may still be syncing.`);
123
+ }
124
+ }
125
+ export function readBaseline(projectDir) {
126
+ const project = readProjectFile(projectDir);
127
+ const file = path.join(projectDir, project.baseline?.file ?? BASELINE_FILE);
128
+ const text = readTextOrSay(file, `The baseline file is missing: ${file}`);
129
+ if (project.baseline?.sha256 && sha256(text) !== project.baseline.sha256) {
130
+ throw new RegisterError(`${file} does not match the checksum in the team project file. It was changed or only partly copied.`);
131
+ }
132
+ // A folder halfway through a sync is the everyday reason this file is not
133
+ // whole JSON. A person reads a sentence about their own folder here, never a
134
+ // parser's stack trace about a position in a byte stream.
135
+ let b;
136
+ try {
137
+ b = JSON.parse(stripBom(text));
138
+ }
139
+ catch {
140
+ throw new RegisterError(`${file} could not be read. It may still be syncing.`);
141
+ }
142
+ if (b?.kind !== 'baseline' || !Array.isArray(b.objects))
143
+ throw new RegisterError(`${file} is not a baseline file`);
144
+ // Every caller reads `o.type` and `o.name` straight away. One hand-edited or
145
+ // half-copied entry that is not an object would come out as a TypeError with
146
+ // a stack, from whichever command happened to touch it first.
147
+ if (b.objects.some((o) => !o || typeof o.type !== 'string' || typeof o.name !== 'string')) {
148
+ throw new RegisterError(`${file} could not be read. It may still be syncing.`);
149
+ }
150
+ return b.objects;
151
+ }
152
+ export function readLanes(projectDir) {
153
+ const file = path.join(projectDir, LANES_FILE);
154
+ if (!fs.existsSync(file))
155
+ return [];
156
+ const l = readJson(file);
157
+ return Array.isArray(l?.lanes) ? l.lanes : [];
158
+ }
159
+ export function writeLanes(projectDir, projectId, lanes) {
160
+ fs.writeFileSync(path.join(projectDir, LANES_FILE), `${JSON.stringify({ kind: 'lanes', formatVersion: 1, projectId, lanes }, null, 2)}\n`);
161
+ }
162
+ /**
163
+ * A record is a short list of object names. Anything this big is a dump, a log
164
+ * or something that is not a record at all, and reading it would cost a
165
+ * gigabyte of memory before `JSON.parse` could say so.
166
+ */
167
+ const MAX_RECORD_BYTES = 64 * 1024 * 1024;
168
+ /** One record file, or nothing. Counted as unreadable rather than thrown, whatever is wrong with it. */
169
+ function readRecord(file) {
170
+ try {
171
+ if (fs.statSync(file).size > MAX_RECORD_BYTES)
172
+ return { ok: false };
173
+ return { ok: true, value: JSON.parse(stripBom(fs.readFileSync(file, 'utf8'))) };
174
+ }
175
+ catch {
176
+ // A file can be listed before the sync tool has finished writing it, or be a
177
+ // folder, or be held open: skip and count, never fail.
178
+ return { ok: false };
179
+ }
180
+ }
181
+ /** Every record-shaped JSON under a folder: writers/<id>/records, rulings, baseline/measurement-*.json. */
182
+ export function collectRecords(root) {
183
+ const records = [];
184
+ let unreadable = 0;
185
+ let files = 0;
186
+ const take = (file) => {
187
+ files += 1;
188
+ const read = readRecord(file);
189
+ if (read.ok)
190
+ records.push(read.value);
191
+ else
192
+ unreadable += 1;
193
+ };
194
+ const readDir = (dir) => {
195
+ let names = [];
196
+ try {
197
+ names = fs.readdirSync(dir);
198
+ }
199
+ catch {
200
+ return;
201
+ }
202
+ for (const name of names) {
203
+ if (!name.toLowerCase().endsWith('.json'))
204
+ continue;
205
+ take(path.join(dir, name));
206
+ }
207
+ };
208
+ let writers = [];
209
+ try {
210
+ writers = fs.readdirSync(path.join(root, 'writers'));
211
+ }
212
+ catch { /* none handed in yet */ }
213
+ for (const w of writers)
214
+ readDir(path.join(root, 'writers', w, 'records'));
215
+ readDir(path.join(root, 'rulings'));
216
+ try {
217
+ for (const name of fs.readdirSync(path.join(root, 'baseline'))) {
218
+ if (!/^measurement-.*\.json$/i.test(name))
219
+ continue;
220
+ // Measurement zero lands here, written by the lead and then synced: the
221
+ // same hazards as any other record, read the same way.
222
+ take(path.join(root, 'baseline', name));
223
+ }
224
+ }
225
+ catch { /* no baseline folder here (an outbox has none) */ }
226
+ return { records, unreadable, files };
227
+ }
package/dist/repl.js CHANGED
@@ -2482,8 +2482,16 @@ export async function runRepl(opts) {
2482
2482
  // user re-submits the empty pre-fill, just silently re-fill
2483
2483
  // without re-spamming the hint line. Tracked via lastEmptyPrefill.
2484
2484
  if (lastEmptyPrefill !== head) {
2485
- console.log(chalk.dim(`↪ ${head} — type your prompt and press Enter ` +
2486
- chalk.dim(`(or "/cancel" to abandon)`)));
2485
+ // 2026-09-23 (Glenn's first run): "type your prompt" never said WHAT
2486
+ // to type, and our own skills suggest a bare next command
2487
+ // ("/abap-estimate next"), so the user is told to run something and
2488
+ // then asked an unanswerable question. Every skill now carries an
2489
+ // `asks` line with an example (skill-catalog.ts).
2490
+ const asks = SKILL_CATALOG.find((e) => `/${e.name}` === head)?.asks;
2491
+ console.log(chalk.dim(asks
2492
+ ? `↪ ${head} — ${asks}`
2493
+ : `${'↪'} ${head} — type your prompt and press Enter`));
2494
+ console.log(chalk.dim(' Enter to send · /cancel to abandon'));
2487
2495
  lastEmptyPrefill = head;
2488
2496
  }
2489
2497
  // 2026-05-02: bake the pre-fill into the next prompt string
@@ -24,6 +24,7 @@
24
24
  * - sap_delete_object ⇒ restorable when a pre-delete snapshot exists (the
25
25
  * snapshot IS the prior source; the executor recreates it).
26
26
  */
27
+ import { includeSnapshotType } from '../tools/include-snapshot.js';
27
28
  /** The five write tools whose effects can (potentially) be rewound. */
28
29
  const WRITE_TOOLS = new Set([
29
30
  'sap_set_source',
@@ -116,7 +117,19 @@ function extractIdentity(tool, rawArgs) {
116
117
  const args = (rawArgs ?? {});
117
118
  if (IMPLICIT_CLASS_TOOLS.has(tool)) {
118
119
  const object = pickString(args, 'className') ?? pickString(args, 'class_name');
119
- return object ? { object: object.toUpperCase(), objType: 'CLAS' } : null;
120
+ if (!object)
121
+ return null;
122
+ // An include write is rewound from the INCLUDE's own snapshot
123
+ // (type CLAS.<INCLUDE>), never from the class main source — restoring the
124
+ // main source would report success while the include change stayed.
125
+ // No includeType recorded ⇒ unknown target ⇒ not rewindable.
126
+ if (tool === 'sap_set_class_include') {
127
+ const includeType = pickString(args, 'includeType');
128
+ return includeType
129
+ ? { object: object.toUpperCase(), objType: includeSnapshotType(includeType) }
130
+ : null;
131
+ }
132
+ return { object: object.toUpperCase(), objType: 'CLAS' };
120
133
  }
121
134
  const object = pickString(args, 'name') ?? pickString(args, 'object_name');
122
135
  if (!object)
@@ -27,6 +27,7 @@ import { getEffectiveWriteMode } from '../repl/mode-cycle.js';
27
27
  import { effectiveRisk } from '../approvals/risk-floor.js';
28
28
  import { verifySyntax, verifyActive } from '../tools/verify.js';
29
29
  import { renderDiff } from '../repl/diff-display.js';
30
+ import { parseIncludeSnapshotType } from '../tools/include-snapshot.js';
30
31
  /** Compact single-line failure text for receipts / error envelopes. */
31
32
  function shortError(err) {
32
33
  const msg = err instanceof Error ? err.message : String(err);
@@ -55,6 +56,10 @@ export async function restoreSnapshot(c, deps) {
55
56
  if (c.snapshotId === null) {
56
57
  return { ok: false, steps, error: 'no snapshot to restore from' };
57
58
  }
59
+ // A class-include candidate (objType CLAS.<INCLUDE>) restores that include;
60
+ // syntax check / activation / verify then run on the CLASS.
61
+ const include = parseIncludeSnapshotType(c.objType);
62
+ const repoType = include ? 'CLAS' : c.objType;
58
63
  // ── Step 2: Read the snapshot source (the content we will restore) ────────
59
64
  let snapshotSource;
60
65
  try {
@@ -67,12 +72,23 @@ export async function restoreSnapshot(c, deps) {
67
72
  // A 404 means the object was deleted this session; current = null and we
68
73
  // will recreate it. Any OTHER read failure is NOT proof of absence — stop
69
74
  // rather than blindly treat it as a create.
75
+ // Class include candidates (objType CLAS.<INCLUDE>) read and write the
76
+ // include itself; there is no delete-restore for an include, so a 404 stops.
70
77
  let currentSource;
71
78
  try {
72
- currentSource = await deps.adt.getSource(c.objType, c.object);
79
+ if (include) {
80
+ const inc = await deps.adt.classIncludes(c.object, include);
81
+ if (typeof inc !== 'string') {
82
+ return { ok: false, steps, error: 'could not read current include source' };
83
+ }
84
+ currentSource = inc;
85
+ }
86
+ else {
87
+ currentSource = await deps.adt.getSource(c.objType, c.object);
88
+ }
73
89
  }
74
90
  catch (err) {
75
- if (err instanceof SapError && err.httpStatus === 404) {
91
+ if (err instanceof SapError && err.httpStatus === 404 && !include) {
76
92
  currentSource = null;
77
93
  }
78
94
  else {
@@ -87,7 +103,7 @@ export async function restoreSnapshot(c, deps) {
87
103
  const change = {
88
104
  op,
89
105
  object: c.object,
90
- type: c.objType,
106
+ type: repoType,
91
107
  diff: renderDiff(currentSource ?? '', snapshotSource, {
92
108
  label: `${c.objType}/${c.object}`,
93
109
  syntaxHighlight: false,
@@ -143,20 +159,38 @@ export async function restoreSnapshot(c, deps) {
143
159
  return { ok: false, steps, error: `recreate failed: ${created.error ?? `HTTP ${created.httpStatus}`}` };
144
160
  }
145
161
  }
146
- let writeResult;
147
- try {
148
- writeResult = await deps.adt.setSource(c.objType, c.object, snapshotSource, deps.transport);
149
- }
150
- catch (err) {
151
- record('② written ✗');
152
- return { ok: false, steps, error: `write failed: ${shortError(err)}` };
162
+ if (include) {
163
+ // Include restore: write the include back. setClassInclude reads its own
164
+ // write back and returns written:false on a discarded or refused PUT.
165
+ let incResult;
166
+ try {
167
+ incResult = await deps.adt.setClassInclude(c.object, include, snapshotSource, deps.transport);
168
+ }
169
+ catch (err) {
170
+ record('② written ✗');
171
+ return { ok: false, steps, error: `write failed: ${shortError(err)}` };
172
+ }
173
+ if (!incResult.written) {
174
+ record('② written ✗');
175
+ return { ok: false, steps, error: `write failed: ${incResult.error ?? `HTTP ${incResult.httpStatus}`}` };
176
+ }
153
177
  }
154
- // setSource RETURNS (does not throw) on HTTP 4xx — check the return value,
155
- // exactly as sap_set_source does.
156
- if (writeResult.error || (writeResult.writeStatus !== undefined && writeResult.writeStatus >= 400)) {
157
- const detail = writeResult.error ?? `HTTP ${writeResult.writeStatus}`;
158
- record('② written ✗');
159
- return { ok: false, steps, error: `write failed: ${detail}` };
178
+ else {
179
+ let writeResult;
180
+ try {
181
+ writeResult = await deps.adt.setSource(c.objType, c.object, snapshotSource, deps.transport);
182
+ }
183
+ catch (err) {
184
+ record('② written ✗');
185
+ return { ok: false, steps, error: `write failed: ${shortError(err)}` };
186
+ }
187
+ // setSource RETURNS (does not throw) on HTTP 4xx — check the return value,
188
+ // exactly as sap_set_source does.
189
+ if (writeResult.error || (writeResult.writeStatus !== undefined && writeResult.writeStatus >= 400)) {
190
+ const detail = writeResult.error ?? `HTTP ${writeResult.writeStatus}`;
191
+ record('② written ✗');
192
+ return { ok: false, steps, error: `write failed: ${detail}` };
193
+ }
160
194
  }
161
195
  record('② written ✓');
162
196
  // ── Step 7: Syntax check (Rule 10) — errors ⇒ STOP, do NOT activate ───────
@@ -167,7 +201,7 @@ export async function restoreSnapshot(c, deps) {
167
201
  // WRITE HAPPENED; only the verification/activation call failed to run.
168
202
  let syntax;
169
203
  try {
170
- syntax = await verifySyntax(deps.adt, c.object, c.objType);
204
+ syntax = await verifySyntax(deps.adt, c.object, repoType);
171
205
  }
172
206
  catch (err) {
173
207
  record(`③ syntax — check failed to run: ${shortError(err)}`);
@@ -191,7 +225,7 @@ export async function restoreSnapshot(c, deps) {
191
225
  // ── Step 8: Activate + verify (Rule 10) ───────────────────────────────────
192
226
  let activation;
193
227
  try {
194
- activation = await deps.adt.activate(c.objType, c.object);
228
+ activation = await deps.adt.activate(repoType, c.object);
195
229
  }
196
230
  catch (err) {
197
231
  record(`④ activate — call failed: ${shortError(err)}`);
@@ -217,7 +251,7 @@ export async function restoreSnapshot(c, deps) {
217
251
  // past the receipt.
218
252
  let active;
219
253
  try {
220
- active = await verifyActive(deps.adt, c.object, c.objType);
254
+ active = await verifyActive(deps.adt, c.object, repoType);
221
255
  }
222
256
  catch (err) {
223
257
  active = { active: false, verifyError: shortError(err) };
@@ -19,36 +19,42 @@ export const SKILL_CATALOG = [
19
19
  category: 'Read / Analyze',
20
20
  description: 'Impact analysis before changes — traces usage, callers, transports',
21
21
  whenToUse: 'pick this for blast radius before a change (not /abap-explain — explain = teach the code)',
22
+ asks: 'which object? e.g. "ZCL_ORDER_UTILS" or "what breaks if I change ZSD_ORDER_RELEASE"',
22
23
  },
23
24
  {
24
25
  name: 'abap-explain',
25
26
  category: 'Read / Analyze',
26
27
  description: 'Understand an object — plain-language (default) or `technical` orientation (deps, tables, risks)',
27
28
  whenToUse: `plain teaching, or technical=fast orientation (not /abap-impact = blast radius)`,
29
+ asks: 'which object, plain or technical? e.g. "ZFI_DUNNING_SELECT in plain words"',
28
30
  },
29
31
  {
30
32
  name: 'abap-review',
31
33
  category: 'Read / Analyze',
32
34
  description: 'Read-only review across lenses: quality (default), performance, cloud-readiness',
33
35
  whenToUse: `grade source: quality (default) + performance/cloud lenses`,
36
+ asks: 'which object, and which lens? e.g. "ZCL_CREDIT_SVC for performance"',
34
37
  },
35
38
  {
36
39
  name: 'abap-clean-core',
37
40
  category: 'Read / Analyze',
38
41
  description: 'Clean-core decision & defense — recommend the clean-core-correct approach, classify the level (A/B/C/D), produce citable evidence; verifies live when connected. Read-only.',
39
42
  whenToUse: `pick this for "is this clean-core-OK?" decisions + defendable evidence (not /abap-review cloud lens — that grades code)`,
43
+ asks: 'which object or requirement? e.g. "can I add a field to VBAK the clean way"',
40
44
  },
41
45
  {
42
46
  name: 'abap-handover',
43
47
  category: 'Read / Analyze',
44
48
  description: 'Documentation for one object (`object` scope) or a whole package (`package` scope)',
45
49
  whenToUse: `object doc OR full package handover dossier`,
50
+ asks: 'which object or package? e.g. "package ZTEST_LAS, full handover"',
46
51
  },
47
52
  {
48
53
  name: 'abap-dump',
49
54
  category: 'Read / Analyze',
50
55
  description: 'Diagnose ABAP short dumps (ST22) and fix them — root cause, call stack, fix with approval; ask for "triage" for ranked read-only hypotheses',
51
56
  whenToUse: `pick this for any ST22 dump — diagnose + fix by default, say "triage" for read-only ranking (not /abap-incident — that's the full dump-to-transport pipeline)`,
57
+ asks: 'which dump? e.g. "the dump in ZFIN_DOC_JOURNAL this morning"',
52
58
  },
53
59
  // ── Modify / Fix ─────────────────────────────────────────────────────────
54
60
  {
@@ -56,48 +62,56 @@ export const SKILL_CATALOG = [
56
62
  category: 'Modify / Fix',
57
63
  description: 'Run ATC checks against an object and auto-fix findings with safety gates',
58
64
  whenToUse: `pick this for ad-hoc ATC fix on one object (not /abap-upgrade-fix — that's worklist-driven)`,
65
+ asks: 'which object or package? e.g. "ZCL_ORDER_UTILS, fix what is safe"',
59
66
  },
60
67
  {
61
68
  name: 'abap-refactor',
62
69
  category: 'Modify / Fix',
63
70
  description: 'Refactor a Z object in place — FORM→method, SELECT *→specific fields, guard clauses, inline DATA. Same architecture, cleaner code.',
64
71
  whenToUse: `pick this to clean up YOUR Z code, same architecture (not /abap-modernize — that rebuilds as RAP/Fiori)`,
72
+ asks: 'which object and what change? e.g. "ZLEGACY_FI_REPORT — add a net amount column"',
65
73
  },
66
74
  {
67
75
  name: 'abap-enhance',
68
76
  category: 'Modify / Fix',
69
77
  description: 'Extend SAP standard via BAdI implementations, enhancement spots, user exits',
70
78
  whenToUse: `pick this to hook into SAP-STANDARD code via BAdI/exit (not /abap-refactor — that's for YOUR Z code)`,
79
+ asks: 'which SAP object and what behaviour? e.g. "block goods issue when the credit check fails"',
71
80
  },
72
81
  {
73
82
  name: 'abap-extend-model',
74
83
  category: 'Modify / Fix',
75
84
  description: 'Add a field or @UI column to an EXISTING custom CDS view / DDLX metadata extension in place — anchored insertion, snapshot, syntax-check, activate, republish-if-needed. The Fiori Elements column-add.',
76
85
  whenToUse: `pick this to add a field/column to an EXISTING custom CDS/DDLX in place (not /abap-rap — that's a new stack; not /abap-refactor — that's ABAP code, not CDS)`,
86
+ asks: 'which view or BO, and what to add? e.g. "add ShippingPoint to the ZC_Delivery list"',
77
87
  },
78
88
  {
79
89
  name: 'abap-incident',
80
90
  category: 'Modify / Fix',
81
91
  description: 'End-to-end incident pipeline — dump to fix deployed to transport in one skill',
82
92
  whenToUse: `pick this for full ST22-to-transport pipeline with formal report (not /abap-dump — single fix only)`,
93
+ asks: 'the incident. e.g. "dump in ZOPEN_ORDER_REPORT, users blocked"',
83
94
  },
84
95
  {
85
96
  name: 'abap-test',
86
97
  category: 'Modify / Fix',
87
98
  description: 'Generate ABAP Unit test classes for existing code with dependency injection',
88
99
  whenToUse: `pick this to generate + run ABAP Unit tests (not /abap-eml — EML body only)`,
100
+ asks: 'which class? e.g. "unit tests for ZCL_CREDIT_SVC"',
89
101
  },
90
102
  {
91
103
  name: 'abap-eml',
92
104
  category: 'Modify / Fix',
93
105
  description: 'Write and test EML (Entity Manipulation Language) for RAP business objects',
94
106
  whenToUse: `pick this for EML inside a RAP context (not /abap-test — that's the test class)`,
107
+ asks: 'what the EML should do. e.g. "create a draft inspection and activate it"',
95
108
  },
96
109
  {
97
110
  name: 'abap-segw',
98
111
  category: 'Modify / Fix',
99
112
  description: 'Implement SEGW OData DPC_EXT method bodies — reads generated MPC/DPC base classes. SEGW project itself created in transaction SEGW. For S/4 1909+ RAP services use /abap-rap.',
100
113
  whenToUse: `pick this for DPC_EXT method bodies on an EXISTING SEGW project (ECC / older S/4; not /abap-rap — that's S/4 1909+ RAP; SEGW project + MPC/DPC must already exist)`,
114
+ asks: 'which project and operation? e.g. "GET_ENTITYSET for ZORDER_SRV"',
101
115
  },
102
116
  // ── Create / Generate ────────────────────────────────────────────────────
103
117
  {
@@ -105,30 +119,35 @@ export const SKILL_CATALOG = [
105
119
  category: 'Create / Generate',
106
120
  description: 'Generate ONE ABAP class / program / include from a clear scope — asks clarifying questions first per Forge Rule 1. For a full RAP stack use /abap-rap; for DDIC only use /abap-data-model.',
107
121
  whenToUse: `pick this for ONE class/program/include from clear scope (not /abap-rap — that's a full RAP stack; not /abap-data-model — that's DDIC only)`,
122
+ asks: 'what to build, or the scope. e.g. "all changes" after a design, or "a report of open orders by plant"',
108
123
  },
109
124
  {
110
125
  name: 'abap-rap',
111
126
  category: 'Create / Generate',
112
127
  description: 'Scaffold a complete RAP stack on S/4HANA 1909+ — table, CDS interface, projection, BDEF, BP class, SRVD, SRVB, DDLX, DCL. ECC/older S/4 → use /abap-segw.',
113
128
  whenToUse: `pick this for a full RAP stack on S/4 1909+ (10 objects, table → SRVB; not /abap-segw — that's ECC/older S/4 OData; not /abap-generate — that's one object)`,
129
+ asks: 'the business object. e.g. "maintenance requests with draft and an approve action"',
114
130
  },
115
131
  {
116
132
  name: 'abap-fiori-build',
117
133
  category: 'Create / Generate',
118
134
  description: 'Build a working freestyle SAPUI5 app on the laptop from a published OData service — scaffold, pages, dialogs, chart, value help, local preview. Freestyle only, not Fiori Elements.',
119
135
  whenToUse: `pick this for a freestyle UI5 frontend on a published OData service (not /abap-rap — that's the backend; produces local files, not ADT objects)`,
136
+ asks: 'which service and what UI? e.g. "list + object page on ZUI_LAS_INSPECTIONLOG_O4"',
120
137
  },
121
138
  {
122
139
  name: 'abap-data-model',
123
140
  category: 'Create / Generate',
124
141
  description: 'Design and create DDIC objects — domains, data elements, tables, structures',
125
142
  whenToUse: `pick this for DDIC ONLY — domain/data element/table/structure (step after /abap-design; embedded inside /abap-rap step 1 — call standalone when you don't need the rest of the stack)`,
143
+ asks: 'the tables and fields. e.g. "inspection results, header and items"',
126
144
  },
127
145
  {
128
146
  name: 'abap-modernize',
129
147
  category: 'Create / Generate',
130
148
  description: 'Transform classic ABAP to the best modern architecture for the target release (ECC→SEGW, S/4→RAP)',
131
149
  whenToUse: `pick this to rebuild a WRITE/dynpro/FM as Fiori/RAP (not /abap-upgrade-fix — that's syntax patches)`,
150
+ asks: 'which program? e.g. "ZLEGACY_SD_PROCESS onto a modern stack"',
132
151
  },
133
152
  // ── Upgrade / Migrate ────────────────────────────────────────────────────
134
153
  {
@@ -136,42 +155,49 @@ export const SKILL_CATALOG = [
136
155
  category: 'Upgrade / Migrate',
137
156
  description: 'Custom code analysis pipeline for S/4 upgrade — whole-estate inventory + tier classification + ATC findings + report. Multi-session project with state file.',
138
157
  whenToUse: `pick this to discover + classify the whole custom estate (not /abap-migrate — that's per object)`,
158
+ asks: 'which package or namespace? e.g. "Z*" or "package ZTEST_LAS"',
139
159
  },
140
160
  {
141
161
  name: 'abap-upgrade-scan',
142
162
  category: 'Upgrade / Migrate',
143
163
  description: 'Scan custom ABAP code for S/4HANA upgrade findings via ATC readiness variant',
144
164
  whenToUse: 'pick this to baseline ATC for one package + S/4 release (step 1 of 3 — feeds /abap-upgrade-fix)',
165
+ asks: 'the scope. e.g. "package ZTEST_LAS" or "Z* with the S/4 readiness variant"',
145
166
  },
146
167
  {
147
168
  name: 'abap-upgrade-fix',
148
169
  category: 'Upgrade / Migrate',
149
170
  description: 'AI-guided upgrade remediation — fixes ATC findings one object at a time with approval gates',
150
171
  whenToUse: 'pick this to fix the upgrade-scan baseline object-by-object (step 2 of 3 — needs /abap-upgrade-scan first)',
172
+ asks: 'which findings. e.g. "@upgrade-progress" or "the API findings in ZCL_ORDER_UTILS"',
151
173
  },
152
174
  {
153
175
  name: 'abap-upgrade-verify',
154
176
  category: 'Upgrade / Migrate',
155
177
  description: 'Verify remediation results — re-runs ATC with same variant, compares before/after finding counts',
156
178
  whenToUse: 'pick this for the customer sign-off report after fixing (step 3 of 3 — re-runs ATC vs baseline)',
179
+ asks: 'which scan to verify against. e.g. "@upgrade-progress from the first scan"',
157
180
  },
158
181
  {
159
182
  name: 'abap-upgrade-merge',
160
183
  category: 'Upgrade / Migrate',
161
184
  description: 'Reconcile multiple parallel /abap-upgrade-fix progress files into one consolidated upgrade-progress',
162
185
  whenToUse: 'pick this when a multi-dev team ran scoped fix slices in parallel — merges N progress files for /abap-upgrade-verify',
186
+ asks: 'which progress files. e.g. "merge the three @upgrade-progress files here"',
163
187
  },
164
188
  {
165
189
  name: 'abap-cca-merge',
166
190
  category: 'Upgrade / Migrate',
167
191
  description: 'Reconcile multiple parallel /abap-cca assessment files into one consolidated cca-assessment',
168
192
  whenToUse: 'pick this when multiple consultants ran scoped /abap-cca slices — merges N assessments into one team view',
193
+ asks: 'which assessments. e.g. "merge the ZFI and ZSD cca files"',
169
194
  },
170
195
  {
171
196
  name: 'abap-migrate',
172
197
  category: 'Upgrade / Migrate',
173
198
  description: 'ECC to S/4HANA migration assessment — API analysis, clean-core alignment, migration path',
174
199
  whenToUse: `pick this to pick retire/replace/refactor per object (not /abap-cca — cca discovers them)`,
200
+ asks: 'which objects. e.g. "package ZLEGACY, ECC to S/4"',
175
201
  },
176
202
  // ── Ship / Release ───────────────────────────────────────────────────────
177
203
  {
@@ -179,18 +205,21 @@ export const SKILL_CATALOG = [
179
205
  category: 'Ship / Release',
180
206
  description: 'Transport management — create, list, validate, release transport requests',
181
207
  whenToUse: `pick this to CREATE/LIST/RELEASE a TR (inspect-only? use /abap-transport-analysis)`,
208
+ asks: 'what to do. e.g. "create a transport for this session" or "release S4HK903449"',
182
209
  },
183
210
  {
184
211
  name: 'abap-transport-analysis',
185
212
  category: 'Ship / Release',
186
213
  description: 'Pre-release transport analysis — objects, ATC scan, activation status, conflict check',
187
214
  whenToUse: `pick this if you have a TR NUMBER and want a read-only safety scan`,
215
+ asks: 'which transport. e.g. "S4HK903449 before it goes to QA"',
188
216
  },
189
217
  {
190
218
  name: 'abap-preflight',
191
219
  category: 'Ship / Release',
192
220
  description: 'Pre-release checklist — ATC, syntax, inactive objects, transport conflicts',
193
221
  whenToUse: `pick this for a full ship-readiness package on a CHANGE SET (no TR# needed)`,
222
+ asks: 'what is going out. e.g. "the objects in S4HK903449"',
194
223
  },
195
224
  // ── Pre-Coding ───────────────────────────────────────────────────────────
196
225
  {
@@ -198,24 +227,28 @@ export const SKILL_CATALOG = [
198
227
  category: 'Pre-Coding',
199
228
  description: 'Find missing requirements before coding — business questions, technical gaps, assumptions',
200
229
  whenToUse: `pick this for missing business + technical questions on a vague spec (step 1 of 3 — feeds /abap-design)`,
230
+ asks: 'the requirement. e.g. paste the ticket text, or "@spec.pdf"',
201
231
  },
202
232
  {
203
233
  name: 'abap-design',
204
234
  category: 'Pre-Coding',
205
235
  description: 'Solution architecture — object list, dependency sequence, patterns, transport strategy. Plans the build, does not generate code.',
206
236
  whenToUse: `pick this for the object list + dependency sequence + pattern choices (step 2 of 3 — needs /abap-spec-gap done, feeds /abap-estimate or /abap-generate)`,
237
+ asks: 'the requirement, once the gaps are closed. e.g. "the inspection log from the spec above"',
207
238
  },
208
239
  {
209
240
  name: 'abap-estimate',
210
241
  category: 'Pre-Coding',
211
242
  description: 'Effort estimation for ABAP tickets — component breakdown with hour ranges (optimistic / realistic / pessimistic) + risk adjustments',
212
243
  whenToUse: `pick this for hour ranges + risk adjustments on a confirmed object list (step 3 of 3 — needs /abap-design first; high uncertainty without it)`,
244
+ asks: 'what to estimate. e.g. "the objects from the design above" or "all changes in this ticket"',
213
245
  },
214
246
  {
215
247
  name: 'abap-plan',
216
248
  category: 'Pre-Coding',
217
249
  description: 'Multi-session project plan — phase envelope holds state across sessions; create from a goal or spec-gap file, resume one bounded phase per session',
218
250
  whenToUse: `pick this when the work spans multiple sessions or components (not /abap-design — that's one design in one session; plan calls design per phase)`,
251
+ asks: 'the goal, or a plan to resume. e.g. "build the inspection log end to end"',
219
252
  },
220
253
  ];
221
254
  /**
@@ -1,6 +1,6 @@
1
1
  // GENERATED FILE — bundled skills for local mode.
2
2
  // Source: https://manifest.cspeach.dev/v1.json
3
- // Generated: 2026-09-17T21:24:04.334Z
3
+ // Generated: 2026-09-23T17:28:19.737Z
4
4
  // Manifest signature verified at build time.
5
5
  export const BUNDLED_SKILLS = {
6
6
  "abap-atc-fix": {