@cspeach/cli 1.1.30 → 1.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cspeach/project.json +4 -0
- package/README.md +11 -1
- package/dist/agent/loop.js +121 -8
- package/dist/cli.js +36 -14
- package/dist/commands/config-set.js +1 -1
- package/dist/commands/plan-continue.js +1 -1
- package/dist/commands/plan-gate.js +8 -1
- package/dist/commands/plan-resume.js +24 -18
- package/dist/commands/plan-startup.js +49 -6
- package/dist/commands/team-discover.js +195 -0
- package/dist/commands/team-plan.js +585 -0
- package/dist/commands/team.js +375 -41
- package/dist/journey/chain.js +728 -0
- package/dist/journey/clean-core.js +213 -0
- package/dist/journey/flow.js +166 -0
- package/dist/journey/run.js +435 -0
- package/dist/journey/session-deps.js +77 -0
- package/dist/journey/trigger.js +153 -0
- package/dist/lane/app-chain.js +9 -7
- package/dist/lane/chain-token.js +9 -5
- package/dist/lane/roots.js +97 -41
- package/dist/one-shot.js +20 -1
- package/dist/projects/clean-core-confirm.js +156 -0
- package/dist/projects/detail-path.js +9 -4
- package/dist/projects/extract-clean-core.js +3 -0
- package/dist/projects/migration.js +4 -105
- package/dist/projects/plan-lane.js +67 -17
- package/dist/projects/plan-sources.js +13 -7
- package/dist/projects/project-folder.js +131 -0
- package/dist/projects/promote-command.js +39 -0
- package/dist/projects/save-command.js +83 -12
- package/dist/projects/saved-lines.js +17 -3
- package/dist/projects/sort-command.js +499 -0
- package/dist/projects/sort-core.js +248 -0
- package/dist/projects/workspace-retire.js +48 -0
- package/dist/projects/workspace.js +182 -121
- package/dist/register/discover.js +901 -0
- package/dist/register/findings.js +5 -2
- package/dist/register/offer.js +100 -8
- package/dist/register/picture.js +6 -2
- package/dist/register/plan-import.js +325 -0
- package/dist/register/records.js +25 -0
- package/dist/register/status-file/estate-parts.js +168 -0
- package/dist/register/status-file/plan-parts.js +68 -0
- package/dist/register/status-file/progress-chart.js +110 -0
- package/dist/register/status-file/register-map.js +11 -3
- package/dist/register/status-file/register-model.js +40 -6
- package/dist/register/status-file/register-parts.js +57 -11
- package/dist/register/status-file/register-status-file.js +24 -6
- package/dist/register/status-file/team-css.js +1 -1
- package/dist/register/status-file-sync.js +3 -0
- package/dist/register/status-html.js +3 -1
- package/dist/register/store.js +40 -2
- package/dist/register/workbooks.js +27 -7
- package/dist/register/xlsx.js +13 -3
- package/dist/register/xref.js +117 -0
- package/dist/renderer/startup-lines.js +11 -0
- package/dist/renderer/tool-labels.js +2 -1
- package/dist/renderer/tool-widget.js +15 -1
- package/dist/repl/at-completer.js +6 -2
- package/dist/repl/at-picker.js +1 -1
- package/dist/repl/journey-entry.js +25 -0
- package/dist/repl.js +157 -39
- package/dist/session/backfill.js +342 -0
- package/dist/session/project-match.js +63 -0
- package/dist/session/resume.js +72 -24
- package/dist/session/schema.js +3 -1
- package/dist/session/store.js +96 -51
- package/dist/skill-catalog.js +2 -2
- package/dist/skills/bundled-skills.js +1 -1
- package/dist/tools/_filesystem-shared.js +174 -12
- package/dist/tools/_team-work-path.js +133 -0
- package/dist/tools/approval.js +7 -1
- package/dist/tools/filesystem/extract-xlsx.js +27 -3
- package/dist/tools/filesystem/file-edit.js +12 -4
- package/dist/tools/filesystem/file-read.js +8 -4
- package/dist/tools/filesystem/file-write.js +12 -4
- package/dist/tools/filesystem/glob.js +33 -8
- package/dist/tools/filesystem/grep.js +9 -4
- package/dist/tools/filesystem/read-document.js +6 -3
- package/dist/tools/project/cca_merge.js +8 -2
- package/dist/tools/project/team_estate.js +91 -0
- package/dist/tools/project/team_findings.js +77 -4
- package/dist/tools/project/upgrade_merge_progress.js +8 -2
- package/dist/tools/sap-read.js +21 -4
- package/dist/tools/sap-write.js +200 -10
- package/dist/tools/subagent/read_agent.js +3 -0
- package/dist/tools/tool-loading.js +1 -1
- package/dist/tools/transport-fit-check.js +117 -0
- package/dist/tools/transport-fit.js +15 -7
- package/dist/tools/transport.js +57 -2
- package/dist/ui/app.js +2 -2
- package/dist/ui/approval-modal.js +74 -6
- package/dist/ui/widgets/confirm-card.js +68 -5
- package/dist/util/open-in-browser.js +143 -0
- package/package.json +120 -120
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guided clean-core journey, Task 3 — the harness-owned chain (the /abap-app
|
|
3
|
+
* shape: lane/app-chain.ts).
|
|
4
|
+
*
|
|
5
|
+
* enterJourney a start sentence / "carry on" (REPL, before the classifier)
|
|
6
|
+
* drive derive the stage from the folder, ask the step card,
|
|
7
|
+
* run the code stage, derive again … until a model step
|
|
8
|
+
* (queued through the pending-dispatch slot), a stop, or done
|
|
9
|
+
* afterJourneyTurn the queued model turn ended (runTurn's post-turn hook):
|
|
10
|
+
* derive again and go on
|
|
11
|
+
* gateJourneyForTurn any other turn ends the chain with one line
|
|
12
|
+
*
|
|
13
|
+
* Every queued command is built by code (run.ts triageCommand / refactorCommand).
|
|
14
|
+
* Headless runs nothing and queues nothing: runJourneyStage says the next step.
|
|
15
|
+
*/
|
|
16
|
+
import { estateSummaryOf } from '@cspeach/register-core';
|
|
17
|
+
import { DEFAULT_ATC_VARIANT, runTeamCommand, whoAmI } from '../commands/team.js';
|
|
18
|
+
import { activeProject, readEstate, readProjectFile, VARIANT_NAME } from '../register/store.js';
|
|
19
|
+
import { findingsByObject } from '../register/findings.js';
|
|
20
|
+
import { shown, shownUpTo } from '../register/atc-file.js';
|
|
21
|
+
import { cspeachWritesThisSession } from '../register/offer.js';
|
|
22
|
+
import { cardSlotEmitter } from '../ui/card-slot.js';
|
|
23
|
+
import { claimedSinceCheck, decidePage, fixFailedCheck, readJourneyFacts, stageOf, stepOf, STEPS, toDecide, toFix } from './clean-core.js';
|
|
24
|
+
import { addJourneyLeftOut, addJourneySkips, clearJourneyFlow, forgetNotNow, getJourneyFlow, journeySkips, pauseJourneyFlow, setJourneyPage, setJourneyQueued, startJourneyFlow, } from './flow.js';
|
|
25
|
+
import { notRecordedLine, passedOverLine, refactorCommand, runJourneyStage, shownReason } from './run.js';
|
|
26
|
+
import { cleanKeepReason, FACT_ROWS, KEEP_QUESTION } from '../projects/clean-core-confirm.js';
|
|
27
|
+
import { sessionTeamDeps } from './session-deps.js';
|
|
28
|
+
/* ── words ──────────────────────────────────────────────────────────────── */
|
|
29
|
+
export const PAUSED_LINE = (step) => step ? `Paused at step ${step}. Say carry on to continue.` : 'Paused. Say carry on to continue.';
|
|
30
|
+
export const LOOK_TITLE = 'Look at the system?';
|
|
31
|
+
export const START_TITLE = 'Start the project here?';
|
|
32
|
+
export const NOTHING_READ_LINE = 'Nothing was read from the system.';
|
|
33
|
+
export const NOT_STARTED_LINE = 'No project was started.';
|
|
34
|
+
/** A discovery this many days old is used as it is; an older one asks again. */
|
|
35
|
+
export const FRESH_DAYS = 7;
|
|
36
|
+
const plural = (count, one, many) => `${count.toLocaleString('en-US')} ${count === 1 ? one : many}`;
|
|
37
|
+
/** About 25 objects a minute (run.ts, live measurement 2026-10-07). */
|
|
38
|
+
const aboutMinutes = (objects) => {
|
|
39
|
+
const m = Math.max(1, Math.ceil(objects / 25));
|
|
40
|
+
return m === 1 ? 'about a minute' : `about ${m} minutes`;
|
|
41
|
+
};
|
|
42
|
+
const footnote = (stage) => `Step ${stepOf(stage)} of 6`;
|
|
43
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
44
|
+
/** "09:12 on 7 Oct", local time. */
|
|
45
|
+
export function shownTime(iso) {
|
|
46
|
+
const d = new Date(iso);
|
|
47
|
+
if (!Number.isFinite(d.getTime()))
|
|
48
|
+
return 'an earlier time';
|
|
49
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
50
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
51
|
+
return `${hh}:${mm} on ${d.getDate()} ${MONTHS[d.getMonth()]}`;
|
|
52
|
+
}
|
|
53
|
+
const CANCELLED = { value: null, cancelled: true };
|
|
54
|
+
/**
|
|
55
|
+
* The classic REPL has no card listener: each journey card becomes one typed
|
|
56
|
+
* line ("Fix X now? Yes (y) / Pick another (p) / Later (l)"), its facts printed
|
|
57
|
+
* above it. Empty or unknown → put away.
|
|
58
|
+
*/
|
|
59
|
+
export function lineConfirm(prompt, emit) {
|
|
60
|
+
return async (req) => {
|
|
61
|
+
for (const f of req.facts ?? [])
|
|
62
|
+
emit(` ${f}`);
|
|
63
|
+
const keys = Object.entries(req.aliases ?? {});
|
|
64
|
+
const choices = req.choices.map((c) => {
|
|
65
|
+
const k = keys.find(([, v]) => v === c.value)?.[0];
|
|
66
|
+
return k ? `${c.label} (${k})` : c.label;
|
|
67
|
+
}).join(' / ');
|
|
68
|
+
const raw = (await prompt(`${req.title} ${choices}`)).trim().toLowerCase();
|
|
69
|
+
if (!raw)
|
|
70
|
+
return CANCELLED;
|
|
71
|
+
const alias = req.aliases?.[raw];
|
|
72
|
+
if (alias)
|
|
73
|
+
return { value: alias };
|
|
74
|
+
const hit = req.choices.find((c) => c.label.toLowerCase() === raw || c.value === raw);
|
|
75
|
+
return hit ? { value: hit.value } : CANCELLED;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The journey's card: the Ink card slot when it has a listener, else a typed
|
|
80
|
+
* line (classic REPL). Also the row cards of the triage save (Task 4).
|
|
81
|
+
*/
|
|
82
|
+
export function journeyCardConfirm(emit) {
|
|
83
|
+
return async (req) => {
|
|
84
|
+
if (cardSlotEmitter.listenerCount('confirm') > 0) {
|
|
85
|
+
const { askConfirm } = await import('../ui/confirm-request.js');
|
|
86
|
+
return askConfirm(req);
|
|
87
|
+
}
|
|
88
|
+
const { inkAwarePrompt } = await import('../agent/loop.js');
|
|
89
|
+
return lineConfirm(inkAwarePrompt, emit)(req);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The journey's free-text question ("Why keep it?"): the Ink prompt when it
|
|
94
|
+
* has a listener, else a typed line (classic). Esc on the Ink prompt puts the
|
|
95
|
+
* question away, as Esc on a card does — it throws, so the rows stop and the
|
|
96
|
+
* journey pauses, instead of the question coming back.
|
|
97
|
+
*/
|
|
98
|
+
export function journeyAsk() {
|
|
99
|
+
return async (question) => {
|
|
100
|
+
const { askQuestionEmitter } = await import('../ui/ask-question-emitter.js');
|
|
101
|
+
if (askQuestionEmitter.listenerCount('request') > 0) {
|
|
102
|
+
const res = await askQuestionEmitter.request({ id: 'journey-ask', question, kind: 'text', choices: [] });
|
|
103
|
+
if (res.cancelled || res.answer === null)
|
|
104
|
+
throw new Error('put away');
|
|
105
|
+
return res.answer;
|
|
106
|
+
}
|
|
107
|
+
const { inkAwarePrompt } = await import('../agent/loop.js');
|
|
108
|
+
return inkAwarePrompt(question);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const defaultConfirm = (h) => journeyCardConfirm(h.emit);
|
|
112
|
+
async function teamFor(h) {
|
|
113
|
+
const base = {
|
|
114
|
+
...(h.adt ? { adt: h.adt } : {}),
|
|
115
|
+
...(h.sapAlias ? { sapAlias: h.sapAlias } : {}),
|
|
116
|
+
...(h.noSap ? { noSap: true } : {}),
|
|
117
|
+
...(h.identity ? { identity: h.identity } : {}),
|
|
118
|
+
...(h.now ? { now: h.now } : {}),
|
|
119
|
+
...(h.rand ? { rand: h.rand } : {}),
|
|
120
|
+
...(h.machine ? { machine: h.machine } : {}),
|
|
121
|
+
};
|
|
122
|
+
return h.headless
|
|
123
|
+
? sessionTeamDeps({ ...base, headless: true })
|
|
124
|
+
: sessionTeamDeps({ ...base, headless: false, signal: h.signal ?? new AbortController().signal, confirm: h.confirm ?? defaultConfirm(h) });
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The line for a chain that cannot go on at `stage`: before a project exists
|
|
128
|
+
* there is nothing to carry on, so steps 1 and 2 say what did not happen.
|
|
129
|
+
*/
|
|
130
|
+
function stopAt(h, stage) {
|
|
131
|
+
if (stage === 'INVENTORY')
|
|
132
|
+
end(h, NOTHING_READ_LINE);
|
|
133
|
+
else if (stage === 'START')
|
|
134
|
+
end(h, NOT_STARTED_LINE);
|
|
135
|
+
else
|
|
136
|
+
pause(h, stepOf(stage));
|
|
137
|
+
}
|
|
138
|
+
/** The session's tool calls (the refactor's writes are read from them); none when the session keeps none. */
|
|
139
|
+
function toolCallsOf(session) {
|
|
140
|
+
const calls = session.toolCalls;
|
|
141
|
+
return Array.isArray(calls) ? calls : [];
|
|
142
|
+
}
|
|
143
|
+
/** End the chain: optional last line, the guarded run off. */
|
|
144
|
+
function end(h, line) {
|
|
145
|
+
clearJourneyFlow(h.session);
|
|
146
|
+
if (line)
|
|
147
|
+
h.emit(line);
|
|
148
|
+
}
|
|
149
|
+
/** Pause the chain: the pause line, and "carry on" (even bare) picks it up. */
|
|
150
|
+
function pause(h, step) {
|
|
151
|
+
pauseJourneyFlow(h.session);
|
|
152
|
+
h.emit(PAUSED_LINE(step));
|
|
153
|
+
}
|
|
154
|
+
/* ── entry ──────────────────────────────────────────────────────────────── */
|
|
155
|
+
/** A start sentence, a refused pattern, or "carry on" — from the REPL, before the classifier. */
|
|
156
|
+
export async function enterJourney(trigger, h) {
|
|
157
|
+
if (trigger.kind === 'refuse') {
|
|
158
|
+
h.emit(trigger.line);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
startJourneyFlow(h.session, trigger.kind === 'start' ? trigger.packages : []);
|
|
162
|
+
forgetNotNow(h.session);
|
|
163
|
+
try {
|
|
164
|
+
await drive(h);
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
end(h);
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/* ── the step cards ─────────────────────────────────────────────────────── */
|
|
172
|
+
async function yesNo(confirm, req) {
|
|
173
|
+
const res = await confirm({
|
|
174
|
+
...req, tag: 'QUESTION', choices: [{ value: 'yes', label: 'Yes' }, { value: 'no', label: 'No' }], initial: 0, aliases: { y: 'yes', n: 'no' },
|
|
175
|
+
});
|
|
176
|
+
return !res.cancelled && res.value === 'yes';
|
|
177
|
+
}
|
|
178
|
+
/** The folder's inventory: when it was taken, on which system; null when there is none. */
|
|
179
|
+
function inventoryOf(cwd) {
|
|
180
|
+
const found = readEstate(cwd);
|
|
181
|
+
const s = found ? estateSummaryOf(found.estate) : null;
|
|
182
|
+
if (!s)
|
|
183
|
+
return null;
|
|
184
|
+
return {
|
|
185
|
+
takenAt: s.takenAt,
|
|
186
|
+
...(s.system?.sid ? { sid: s.system.sid } : {}),
|
|
187
|
+
packages: new Map(s.packages.map((p) => [p.name.toUpperCase(), p.codeObjects])),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const PACKAGE = /^(\/[A-Z0-9_]{1,10}\/)?[A-Z0-9_]{1,30}$/;
|
|
191
|
+
/** The named packages and their code objects, when all are names the system has; else null (the runner says why). */
|
|
192
|
+
function startScope(cwd, packages) {
|
|
193
|
+
const names = [...new Set(packages.map((p) => p.trim().toUpperCase()).filter(Boolean))];
|
|
194
|
+
if (!names.length || names.some((p) => !PACKAGE.test(p)))
|
|
195
|
+
return null;
|
|
196
|
+
const inv = inventoryOf(cwd);
|
|
197
|
+
if (!inv || names.some((p) => !inv.packages.has(p)))
|
|
198
|
+
return null;
|
|
199
|
+
return { names, objects: names.reduce((n, p) => n + (inv.packages.get(p) ?? 0), 0) };
|
|
200
|
+
}
|
|
201
|
+
/** The card shows this much of a recorded reason, wrapped over up to FACT_ROWS rows. */
|
|
202
|
+
export const CARD_REASON = 300;
|
|
203
|
+
const LEVEL = /^[A-D]$/;
|
|
204
|
+
/**
|
|
205
|
+
* Every fix or rebuild is recorded with target A when none was given
|
|
206
|
+
* (register/records.ts, `cspeach team decide`); register-core keeps what it is
|
|
207
|
+
* handed, so a recorded A cannot be told from a default. Only another level
|
|
208
|
+
* was set on purpose, and only that one is said.
|
|
209
|
+
*/
|
|
210
|
+
const DEFAULT_TARGET = 'A';
|
|
211
|
+
/**
|
|
212
|
+
* The facts of the fix card, from the register only (no SAP call): the open
|
|
213
|
+
* findings, a failed check, why (the decision's reason as written, cleaned
|
|
214
|
+
* for the screen, at most 300 characters, wrapped on the card), the target level when one was set
|
|
215
|
+
* on purpose, the approval promise, and where this object stands in the list.
|
|
216
|
+
*/
|
|
217
|
+
export function fixFacts(o, at, of, open = []) {
|
|
218
|
+
const reason = shownReason(o.decision?.reason, CARD_REASON);
|
|
219
|
+
const set = o.decision?.targetLevel;
|
|
220
|
+
const target = set && LEVEL.test(set) && set !== DEFAULT_TARGET ? set : undefined;
|
|
221
|
+
const now = o.decision?.level && LEVEL.test(o.decision.level) ? o.decision.level : undefined;
|
|
222
|
+
return [
|
|
223
|
+
o.openFindings === null ? 'Not checked yet.' : `${plural(o.openFindings, 'open finding', 'open findings')}.`,
|
|
224
|
+
...(fixFailedCheck(o) ? ['Its last fix failed the check.', ...open] : []),
|
|
225
|
+
...(reason ? [`Why: ${reason}`] : []),
|
|
226
|
+
...(target ? [`Target: level ${target}${now && now !== target ? `, now ${now}` : ''}.`] : []),
|
|
227
|
+
'CSPeach asks before it writes to SAP.',
|
|
228
|
+
`${at} of ${plural(of, 'object marked fix', 'objects marked fix')}.`,
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
/** "Still open:" shows at most this many lines on a card. */
|
|
232
|
+
const STILL_OPEN_ROWS = 3;
|
|
233
|
+
/** A fact wraps onto at most this many rows on a card that also lists what is still open. */
|
|
234
|
+
export const FACT_ROWS_BESIDE_OPEN = 3;
|
|
235
|
+
/**
|
|
236
|
+
* What the newest measurement still finds in an object, in plain words, at
|
|
237
|
+
* most three lines: "Still open: Usage of not released application API
|
|
238
|
+
* (line 13)". Read from the folder's measurement records (findings.ts, the
|
|
239
|
+
* project's variant only); none when they cannot be read. The titles were
|
|
240
|
+
* written on another laptop, so they are cleaned for the screen.
|
|
241
|
+
*/
|
|
242
|
+
export function stillOpenLines(key) {
|
|
243
|
+
let latest = [];
|
|
244
|
+
try {
|
|
245
|
+
const joined = activeProject();
|
|
246
|
+
if (!joined)
|
|
247
|
+
return [];
|
|
248
|
+
latest = findingsByObject(joined.projectDir, joined.outboxDir, { lines: true }).get(key)?.latest ?? [];
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
const said = (r) => {
|
|
254
|
+
const what = shownUpTo(r.message || r.rule, 120).replace(/[\s.]+$/, '');
|
|
255
|
+
const where = r.count > 1 ? ` (${r.count} places${r.line ? `, first at line ${r.line}` : ''})` : r.line ? ` (line ${r.line})` : '';
|
|
256
|
+
return `Still open: ${what}${where}`;
|
|
257
|
+
};
|
|
258
|
+
if (latest.length <= STILL_OPEN_ROWS)
|
|
259
|
+
return latest.map(said);
|
|
260
|
+
// Findings, not rules: the same count the card's title gives.
|
|
261
|
+
const more = latest.slice(STILL_OPEN_ROWS - 1).reduce((sum, r) => sum + r.count, 0);
|
|
262
|
+
return [...latest.slice(0, STILL_OPEN_ROWS - 1).map(said), `And ${plural(more, 'other finding', 'other findings')}.`];
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Step 4: "Fix <NAME> now?" [Yes] [Pick another] [Later], smallest first
|
|
266
|
+
* (toFix's order); a fix that failed the check asks "<NAME>'s fix failed the
|
|
267
|
+
* check — try again?". Pick another goes round the list; Later puts the
|
|
268
|
+
* object off for the session. Yes hands the runner a skip list that leaves
|
|
269
|
+
* only the chosen object first. Objects whose names are not ABAP names never
|
|
270
|
+
* reach this card: the chain passes them over first.
|
|
271
|
+
*/
|
|
272
|
+
export async function fixCard(confirm, objects, skips, openOf = () => []) {
|
|
273
|
+
const list = objects.filter((o) => refactorCommand({ type: o.type, name: o.name }) !== null);
|
|
274
|
+
if (!list.length)
|
|
275
|
+
return { kind: 'none' };
|
|
276
|
+
const many = list.length > 1;
|
|
277
|
+
for (let i = 0; i < list.length * 20; i++) {
|
|
278
|
+
const o = list[i % list.length];
|
|
279
|
+
const name = o.name.toUpperCase();
|
|
280
|
+
const open = fixFailedCheck(o) ? openOf(o) : [];
|
|
281
|
+
const res = await confirm({
|
|
282
|
+
id: 'journey-fix',
|
|
283
|
+
tag: 'QUESTION',
|
|
284
|
+
title: fixFailedCheck(o) ? `${name}'s fix failed the check — try again?` : `Fix ${name} now?`,
|
|
285
|
+
facts: fixFacts(o, (i % list.length) + 1, list.length, open),
|
|
286
|
+
choices: [{ value: 'yes', label: 'Yes' }, ...(many ? [{ value: 'another', label: 'Pick another' }] : []), { value: 'later', label: 'Later' }],
|
|
287
|
+
initial: 0,
|
|
288
|
+
aliases: { y: 'yes', ...(many ? { p: 'another' } : {}), l: 'later' },
|
|
289
|
+
footnote: footnote('FIX'),
|
|
290
|
+
// With what is still open on the card, the reason gets three rows, so the card stays short.
|
|
291
|
+
wrapFacts: open.length ? FACT_ROWS_BESIDE_OPEN : FACT_ROWS,
|
|
292
|
+
});
|
|
293
|
+
if (res.cancelled || res.value === null)
|
|
294
|
+
return { kind: 'cancel' };
|
|
295
|
+
if (res.value === 'another')
|
|
296
|
+
continue;
|
|
297
|
+
if (res.value === 'later')
|
|
298
|
+
return { kind: 'later', object: o };
|
|
299
|
+
const others = objects.filter((x) => x.key !== o.key).map((x) => x.key);
|
|
300
|
+
return { kind: 'yes', object: o, skips: { ...skips, fix: new Set([...(skips.fix ?? []), ...others]) } };
|
|
301
|
+
}
|
|
302
|
+
return { kind: 'cancel' };
|
|
303
|
+
}
|
|
304
|
+
/* ── the chain ──────────────────────────────────────────────────────────── */
|
|
305
|
+
/** Where the chain is: the stage, and a print of what the session put off (no progress → stop). */
|
|
306
|
+
const fingerprint = (stage, s) => `${stage}|${s.decide?.size ?? 0}|${s.fix?.size ?? 0}|${s.prove ? 1 : 0}|${s.report ? 1 : 0}`;
|
|
307
|
+
async function drive(h) {
|
|
308
|
+
const flow = getJourneyFlow(h.session);
|
|
309
|
+
if (!flow)
|
|
310
|
+
return;
|
|
311
|
+
const team = await teamFor(h);
|
|
312
|
+
const confirm = team.confirm;
|
|
313
|
+
const now = () => (h.now ? h.now() : new Date());
|
|
314
|
+
let ranInventory = false;
|
|
315
|
+
let saidInventory = false;
|
|
316
|
+
let previous = '';
|
|
317
|
+
for (let round = 0; round < 24; round++) {
|
|
318
|
+
const me = whoAmI(team.deps);
|
|
319
|
+
const skips = journeySkips(h.session);
|
|
320
|
+
const facts = readJourneyFacts(h.cwd, me, skips);
|
|
321
|
+
const stage = stageOf(facts);
|
|
322
|
+
// Ctrl-C: what was measured is kept; "carry on" picks up from the folder.
|
|
323
|
+
if (h.signal?.aborted) {
|
|
324
|
+
stopAt(h, stage);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const print = fingerprint(stage, skips);
|
|
328
|
+
// A code stage that changed nothing: never go round again.
|
|
329
|
+
if (print === previous) {
|
|
330
|
+
stopAt(h, stage);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
previous = print;
|
|
334
|
+
let runAs = stage;
|
|
335
|
+
let runSkips = skips;
|
|
336
|
+
if (!h.headless) {
|
|
337
|
+
// Step 1 — "Look at the system?": no inventory yet, or one older than a week.
|
|
338
|
+
const inv = stage === 'START' && !ranInventory ? inventoryOf(h.cwd) : null;
|
|
339
|
+
const stale = !!inv && now().getTime() - Date.parse(inv.takenAt) > FRESH_DAYS * 86_400_000;
|
|
340
|
+
if (stage === 'INVENTORY' || stale) {
|
|
341
|
+
const yes = await yesNo(confirm, {
|
|
342
|
+
id: 'journey-look',
|
|
343
|
+
title: LOOK_TITLE,
|
|
344
|
+
facts: [
|
|
345
|
+
...(stale ? [`The last inventory is from ${shownTime(inv.takenAt)}.`] : []),
|
|
346
|
+
'Reads packages, objects, usage sources and the caller index.',
|
|
347
|
+
'About 2 minutes.',
|
|
348
|
+
'Writes nothing to SAP.',
|
|
349
|
+
'Saves the inventory in this folder.',
|
|
350
|
+
],
|
|
351
|
+
footnote: footnote('INVENTORY'),
|
|
352
|
+
});
|
|
353
|
+
if (!yes && !stale) {
|
|
354
|
+
end(h, NOTHING_READ_LINE);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (yes)
|
|
358
|
+
runAs = 'INVENTORY';
|
|
359
|
+
else {
|
|
360
|
+
h.emit(`Using the inventory from ${shownTime(inv.takenAt)}.`);
|
|
361
|
+
saidInventory = true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
else if (inv && !saidInventory) {
|
|
365
|
+
h.emit(`I took an inventory of ${inv.sid ?? h.sapAlias ?? 'the system'} at ${shownTime(inv.takenAt)}.`);
|
|
366
|
+
saidInventory = true;
|
|
367
|
+
}
|
|
368
|
+
// Step 2 — "Start the project here?", when the named packages are ones the system has.
|
|
369
|
+
if (runAs === 'START') {
|
|
370
|
+
const scope = startScope(h.cwd, flow.packages);
|
|
371
|
+
if (scope) {
|
|
372
|
+
const n = scope.objects;
|
|
373
|
+
const yes = await yesNo(confirm, {
|
|
374
|
+
id: 'journey-start',
|
|
375
|
+
title: START_TITLE,
|
|
376
|
+
facts: [
|
|
377
|
+
`${plural(n, 'object', 'objects')} in ${scope.names.join(', ')} in the inventory.`,
|
|
378
|
+
"The start checks this against SAP's own count.",
|
|
379
|
+
`First check: ${aboutMinutes(n)}.`,
|
|
380
|
+
'Saved in this folder.',
|
|
381
|
+
// Recording fix 5: the name every record is written under, seen before anything is written.
|
|
382
|
+
`You: ${shown(me)}.`,
|
|
383
|
+
`You own all ${n.toLocaleString('en-US')}.`,
|
|
384
|
+
'Writes nothing to SAP.',
|
|
385
|
+
],
|
|
386
|
+
footnote: footnote('START'),
|
|
387
|
+
});
|
|
388
|
+
if (!yes) {
|
|
389
|
+
end(h, NOT_STARTED_LINE);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Step 4 — "Fix <NAME> now?".
|
|
395
|
+
if (runAs === 'FIX' && facts.register) {
|
|
396
|
+
// A name that is not an ABAP name never reaches a card or a command: one line, passed over.
|
|
397
|
+
const bad = toFix(facts.register, me, skips).filter((o) => refactorCommand({ type: o.type, name: o.name }) === null);
|
|
398
|
+
if (bad.length) {
|
|
399
|
+
for (const o of bad)
|
|
400
|
+
h.emit(passedOverLine(o.name));
|
|
401
|
+
addJourneySkips(h.session, { fix: new Set(bad.map((o) => o.key)) });
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const pick = await fixCard(confirm, toFix(facts.register, me, skips), skips, (o) => stillOpenLines(o.key));
|
|
405
|
+
if (pick.kind === 'cancel') {
|
|
406
|
+
pause(h, stepOf('FIX'));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (pick.kind === 'later') {
|
|
410
|
+
addJourneySkips(h.session, { fix: new Set([pick.object.key]) });
|
|
411
|
+
h.emit(`${pick.object.name.toUpperCase()}: left for later.`);
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (pick.kind === 'yes')
|
|
415
|
+
runSkips = pick.skips;
|
|
416
|
+
}
|
|
417
|
+
// Step 3, from the second page on — "Next 40? [Yes] [Later]".
|
|
418
|
+
if (runAs === 'DECIDE' && facts.register && (getJourneyFlow(h.session)?.pages ?? 0) > 0) {
|
|
419
|
+
const left = toDecide(facts.register, me, skips);
|
|
420
|
+
const next = decidePage(facts.register, me, skips).length;
|
|
421
|
+
const res = await confirm({
|
|
422
|
+
id: 'journey-next-page',
|
|
423
|
+
tag: 'QUESTION',
|
|
424
|
+
title: `Next ${next.toLocaleString('en-US')}?`,
|
|
425
|
+
facts: [`${plural(left.length, 'object', 'objects')} left to decide.`],
|
|
426
|
+
choices: [{ value: 'yes', label: 'Yes' }, { value: 'later', label: 'Later' }],
|
|
427
|
+
initial: 0,
|
|
428
|
+
aliases: { y: 'yes', l: 'later' },
|
|
429
|
+
footnote: footnote('DECIDE'),
|
|
430
|
+
});
|
|
431
|
+
if (res.cancelled || res.value === null) {
|
|
432
|
+
pause(h, stepOf('DECIDE'));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (res.value === 'later') {
|
|
436
|
+
// Left out, not skipped: "carry on" brings them back to deciding.
|
|
437
|
+
addJourneyLeftOut(h.session, left.map((o) => o.key));
|
|
438
|
+
h.emit('Left for later.');
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const r = await runJourneyStage(runAs, {
|
|
444
|
+
cwd: h.cwd, team, headless: h.headless, skips: runSkips, packages: flow.packages,
|
|
445
|
+
...(h.openFile ? { openFile: h.openFile } : {}),
|
|
446
|
+
});
|
|
447
|
+
if (runAs === 'INVENTORY')
|
|
448
|
+
ranInventory = true;
|
|
449
|
+
for (const line of r.lines)
|
|
450
|
+
h.emit(line);
|
|
451
|
+
addJourneySkips(h.session, r.skip);
|
|
452
|
+
if (r.next.kind === 'continue') {
|
|
453
|
+
if (runAs !== stage)
|
|
454
|
+
previous = ''; // a fresh inventory is progress
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (r.next.kind === 'queue') {
|
|
458
|
+
const command = r.next.command;
|
|
459
|
+
if (!h.queueNext) {
|
|
460
|
+
end(h, `Next: ${command}`);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const skill = /^\/(\S+)/.exec(command)[1];
|
|
464
|
+
if (runAs === 'DECIDE') {
|
|
465
|
+
setJourneyQueued(h.session, { command, skill, stage: 'DECIDE', left: facts.register ? toDecide(facts.register, me, skips).length : 0 });
|
|
466
|
+
// The page the triage is shown (team_findings reads it): at most 40, in the queued area.
|
|
467
|
+
setJourneyPage(h.session, facts.register ? decidePage(facts.register, me, skips).map((o) => o.key) : []);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
const key = r.next.key;
|
|
471
|
+
setJourneyQueued(h.session, { command, skill, stage: 'FIX', ...(key ? { key } : {}), callsAt: toolCallsOf(h.session).length, variant: projectVariant(h.cwd) });
|
|
472
|
+
}
|
|
473
|
+
h.queueNext(command);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
end(h); // stop or done: the runner said its last words
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
pause(h);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* runTurn's post-turn hook. A turn of the queued step's skill that moved
|
|
483
|
+
* nothing and ends on a question waits for the reply; one that moved nothing
|
|
484
|
+
* otherwise pauses the chain; anything that moved goes on.
|
|
485
|
+
*/
|
|
486
|
+
export async function afterJourneyTurn(h, t) {
|
|
487
|
+
const q = getJourneyFlow(h.session)?.queued;
|
|
488
|
+
if (!q || t.skill !== q.skill || t.awaitingAnswer)
|
|
489
|
+
return;
|
|
490
|
+
if (h.headless) {
|
|
491
|
+
end(h);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const team = await teamFor(h);
|
|
495
|
+
const me = whoAmI(team.deps);
|
|
496
|
+
const skips = journeySkips(h.session);
|
|
497
|
+
const facts = readJourneyFacts(h.cwd, me, skips);
|
|
498
|
+
const stage = stageOf(facts);
|
|
499
|
+
let moved = stage !== q.stage;
|
|
500
|
+
if (!moved && facts.register && q.stage === 'DECIDE')
|
|
501
|
+
moved = toDecide(facts.register, me, skips).length < (q.left ?? Number.MAX_SAFE_INTEGER);
|
|
502
|
+
if (!moved && facts.register && q.stage === 'FIX')
|
|
503
|
+
moved = !toFix(facts.register, me).some((o) => o.key === q.key);
|
|
504
|
+
if (!moved) {
|
|
505
|
+
if (/\?\s*$/.test(t.assistantText.trim()))
|
|
506
|
+
return;
|
|
507
|
+
if (q.stage !== 'FIX' || !q.key) {
|
|
508
|
+
pause(h, stepOf(q.stage));
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
// Recording fix 3b: a retry of a fix that failed the check ended with
|
|
512
|
+
// nothing recorded. "Left for later" alone hid what was still open: one
|
|
513
|
+
// card says it and asks what to do.
|
|
514
|
+
const o = facts.register?.objects.find((x) => x.key === q.key);
|
|
515
|
+
if (o && fixFailedCheck(o) && !claimedSinceCheck(o)) {
|
|
516
|
+
setJourneyQueued(h.session, undefined);
|
|
517
|
+
const goOn = await retryCard(h, team, o);
|
|
518
|
+
if (!goOn)
|
|
519
|
+
return;
|
|
520
|
+
try {
|
|
521
|
+
await drive(h);
|
|
522
|
+
}
|
|
523
|
+
catch (e) {
|
|
524
|
+
end(h);
|
|
525
|
+
throw e;
|
|
526
|
+
}
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
// The refactor ended with no fix recorded (the approval card declined, or
|
|
530
|
+
// nothing written): never the same object again this session — one line,
|
|
531
|
+
// then the next fix or the next step.
|
|
532
|
+
const name = q.key.slice(q.key.indexOf('|') + 1).toUpperCase();
|
|
533
|
+
const written = cspeachWritesThisSession(toolCallsOf(h.session).slice(q.callsAt ?? 0)).has(name);
|
|
534
|
+
addJourneySkips(h.session, { fix: new Set([q.key]) });
|
|
535
|
+
h.emit(written ? `${name}: no fix was recorded — left for later.` : `${name} was not changed — left for later.`);
|
|
536
|
+
}
|
|
537
|
+
setJourneyQueued(h.session, undefined);
|
|
538
|
+
try {
|
|
539
|
+
await drive(h);
|
|
540
|
+
}
|
|
541
|
+
catch (e) {
|
|
542
|
+
end(h);
|
|
543
|
+
throw e;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* After a retry that recorded nothing: "<NAME> still has <n> open findings."
|
|
548
|
+
* [Try another fix] queues the refactor of the same object again; [Keep as is]
|
|
549
|
+
* asks "Why keep it?" and records an accepted exception at its level with that
|
|
550
|
+
* reason — the record the row card's Keep as is writes (team decide <T> <N>
|
|
551
|
+
* exception --level <L> --reason "…"); [Later] puts it off for this session.
|
|
552
|
+
* Returns true when the chain goes on from the folder (drive), false when it
|
|
553
|
+
* stopped or a command was queued.
|
|
554
|
+
*/
|
|
555
|
+
async function retryCard(h, team, o) {
|
|
556
|
+
const name = o.name.toUpperCase();
|
|
557
|
+
const type = o.type.toUpperCase();
|
|
558
|
+
const level = o.decision?.level && LEVEL.test(o.decision.level) ? o.decision.level : undefined;
|
|
559
|
+
const n = o.openFindings ?? 0;
|
|
560
|
+
const ask = h.ask ?? journeyAsk();
|
|
561
|
+
for (let tries = 0; tries < 5; tries++) {
|
|
562
|
+
const res = await team.confirm({
|
|
563
|
+
id: 'journey-retry',
|
|
564
|
+
tag: 'QUESTION',
|
|
565
|
+
title: `${name} still has ${plural(n, 'open finding', 'open findings')}.`,
|
|
566
|
+
facts: [...stillOpenLines(o.key), 'CSPeach asks before it writes to SAP.'],
|
|
567
|
+
choices: [
|
|
568
|
+
{ value: 'again', label: 'Try another fix' },
|
|
569
|
+
...(level ? [{ value: 'keep', label: 'Keep as is' }] : []),
|
|
570
|
+
{ value: 'later', label: 'Later' },
|
|
571
|
+
],
|
|
572
|
+
initial: 0,
|
|
573
|
+
aliases: { t: 'again', ...(level ? { k: 'keep' } : {}), l: 'later' },
|
|
574
|
+
footnote: footnote('FIX'),
|
|
575
|
+
wrapFacts: FACT_ROWS_BESIDE_OPEN,
|
|
576
|
+
});
|
|
577
|
+
if (res.cancelled || res.value === null) {
|
|
578
|
+
pause(h, stepOf('FIX'));
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
if (res.value === 'later') {
|
|
582
|
+
addJourneySkips(h.session, { fix: new Set([o.key]) });
|
|
583
|
+
h.emit(`${name}: left for later.`);
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
if (res.value === 'again') {
|
|
587
|
+
const command = refactorCommand({ type: o.type, name: o.name, ...(o.decision?.reason ? { reason: o.decision.reason } : {}) });
|
|
588
|
+
if (!command) {
|
|
589
|
+
addJourneySkips(h.session, { fix: new Set([o.key]) });
|
|
590
|
+
h.emit(passedOverLine(o.name));
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
if (!h.queueNext) {
|
|
594
|
+
end(h, `Next: ${command}`);
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
h.emit(`Step ${stepOf('FIX')} of ${STEPS} — fix ${name} again.`);
|
|
598
|
+
setJourneyQueued(h.session, { command, skill: 'abap-refactor', stage: 'FIX', key: o.key, callsAt: toolCallsOf(h.session).length, variant: projectVariant(h.cwd) });
|
|
599
|
+
h.queueNext(command);
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
if (res.value === 'keep' && level) {
|
|
603
|
+
let said;
|
|
604
|
+
try {
|
|
605
|
+
said = await ask(KEEP_QUESTION);
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
pause(h, stepOf('FIX'));
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
const reason = cleanKeepReason(said);
|
|
612
|
+
if (!reason) {
|
|
613
|
+
h.emit(`A keep needs a reason. Nothing was recorded for ${type} ${name} yet.`);
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
team.said.length = 0;
|
|
617
|
+
const code = await runTeamCommand(['decide', type, name, 'exception', '--level', level, '--reason', reason], team.deps).catch(() => 1);
|
|
618
|
+
if (code !== 0) {
|
|
619
|
+
const why = team.said.find((l) => l.startsWith('✗'))?.replace(/^✗\s*/, '') ?? '';
|
|
620
|
+
h.emit(notRecordedLine(new Error(why || 'the record could not be written to this folder')));
|
|
621
|
+
addJourneySkips(h.session, { fix: new Set([o.key]) });
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
h.emit(`✓ Recorded: ${shown(type)} ${shown(name)} — kept at ${level}, reason recorded.`);
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
addJourneySkips(h.session, { fix: new Set([o.key]) });
|
|
629
|
+
h.emit(`${name}: left for later.`);
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* The queued step's turn was interrupted (Ctrl-C, a dropped stream): the
|
|
634
|
+
* journey pauses, so "carry on" picks it up on the first try. Returns the
|
|
635
|
+
* line to say, or undefined when no journey step was running.
|
|
636
|
+
*/
|
|
637
|
+
export function pauseJourneyForInterruptedTurn(session) {
|
|
638
|
+
const q = getJourneyFlow(session)?.queued;
|
|
639
|
+
if (!q)
|
|
640
|
+
return undefined;
|
|
641
|
+
pauseJourneyFlow(session);
|
|
642
|
+
return PAUSED_LINE(stepOf(q.stage));
|
|
643
|
+
}
|
|
644
|
+
/* ── the queued triage turn's prompt ────────────────────────────────────── */
|
|
645
|
+
/**
|
|
646
|
+
* Final fix wave, ruling 3 — the "no commands" note in the turn's own prompt
|
|
647
|
+
* (the skill treats tool strings as data, so the team_findings note alone is
|
|
648
|
+
* not enough; it stays as belt-and-braces).
|
|
649
|
+
*/
|
|
650
|
+
export const JOURNEY_TURN_BLOCK = [
|
|
651
|
+
'<journey>',
|
|
652
|
+
"This triage is one step of CSPeach's guided clean-core run.",
|
|
653
|
+
'Do not suggest any cspeach command, and do not use the words team or lane.',
|
|
654
|
+
'After your table, CSPeach asks the person about each row, row by row.',
|
|
655
|
+
'End with the table and the manifest. No closing instructions.',
|
|
656
|
+
'</journey>',
|
|
657
|
+
].join('\n');
|
|
658
|
+
/**
|
|
659
|
+
* The check variant the journey proves a fix with (step 5): the project's own,
|
|
660
|
+
* else the one a project is started with. Never anything that is not a
|
|
661
|
+
* variant name — it goes into the model's prompt.
|
|
662
|
+
*/
|
|
663
|
+
export function projectVariant(cwd) {
|
|
664
|
+
let v = '';
|
|
665
|
+
try {
|
|
666
|
+
v = (readProjectFile(cwd).atcVariant ?? '').trim().toUpperCase();
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
v = '';
|
|
670
|
+
}
|
|
671
|
+
return VARIANT_NAME.test(v) ? v : DEFAULT_ATC_VARIANT;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Recording fix 3 (2026-10-07) — the queued fix turn checked with ATC's
|
|
675
|
+
* DEFAULT variant and said done, while the journey's proof uses the project's
|
|
676
|
+
* variant and found the fix had failed. The turn's own prompt now names that
|
|
677
|
+
* variant and when the fix is done.
|
|
678
|
+
*/
|
|
679
|
+
export function journeyFixBlock(variant) {
|
|
680
|
+
const v = VARIANT_NAME.test(variant) ? variant : DEFAULT_ATC_VARIANT;
|
|
681
|
+
return [
|
|
682
|
+
'<journey>',
|
|
683
|
+
"This fix is one step of CSPeach's guided clean-core run.",
|
|
684
|
+
`The project's check variant is ${v}.`,
|
|
685
|
+
`After you activate the object, run ATC on it with the variant ${v}, not the default one.`,
|
|
686
|
+
`The fix is done only when ${v} shows no findings for the object.`,
|
|
687
|
+
'If findings remain, fix them and check again. For example: do not type parameters with fields of views SAP has not released; use released data elements and classes.',
|
|
688
|
+
'Do at most 2 more rounds. Then say plainly what is still open.',
|
|
689
|
+
'Do not suggest any cspeach command, and do not use the words team or lane.',
|
|
690
|
+
'</journey>',
|
|
691
|
+
].join('\n');
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* The block for the queued DECIDE triage turn, and for the queued FIX
|
|
695
|
+
* refactor turn; '' on every other turn (the prompt stays as it was).
|
|
696
|
+
*/
|
|
697
|
+
export function journeyTurnBlock(session, skill) {
|
|
698
|
+
const q = getJourneyFlow(session)?.queued;
|
|
699
|
+
if (q?.stage === 'DECIDE' && skill === 'abap-clean-core')
|
|
700
|
+
return JOURNEY_TURN_BLOCK;
|
|
701
|
+
if (q?.stage === 'FIX' && skill === 'abap-refactor')
|
|
702
|
+
return journeyFixBlock(q.variant ?? DEFAULT_ATC_VARIANT);
|
|
703
|
+
return '';
|
|
704
|
+
}
|
|
705
|
+
/* ── the gate ───────────────────────────────────────────────────────────── */
|
|
706
|
+
const norm = (s) => s.trim().replace(/\s+/g, ' ');
|
|
707
|
+
/**
|
|
708
|
+
* At the top of every top-level turn. The journey survives only its own step:
|
|
709
|
+
* the exact command it queued, or a plain reply that stays in that step's
|
|
710
|
+
* skill. Anything else — another slash command, a reply routed elsewhere —
|
|
711
|
+
* ends the chain: `line` is the one thing to say (interactive only).
|
|
712
|
+
*/
|
|
713
|
+
export function gateJourneyForTurn(session, t) {
|
|
714
|
+
const flow = getJourneyFlow(session);
|
|
715
|
+
if (!flow)
|
|
716
|
+
return { ended: false };
|
|
717
|
+
const q = flow.queued;
|
|
718
|
+
if (q) {
|
|
719
|
+
if (t.explicitSlash && norm(`/${t.skill} ${t.userMessage}`) === norm(q.command))
|
|
720
|
+
return { ended: false };
|
|
721
|
+
if (!t.explicitSlash && t.skill === q.skill && t.previousSkill === q.skill)
|
|
722
|
+
return { ended: false };
|
|
723
|
+
}
|
|
724
|
+
// The stray message is pushed after this gate: count it as seen, so "carry
|
|
725
|
+
// on" right after its answer resumes, as the pause line says.
|
|
726
|
+
pauseJourneyFlow(session, 1);
|
|
727
|
+
return { ended: true, line: PAUSED_LINE(q ? stepOf(q.stage) : undefined) };
|
|
728
|
+
}
|