@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
package/dist/repl/at-picker.js
CHANGED
|
@@ -48,7 +48,7 @@ export function listWorkspaceFileChoices(opts) {
|
|
|
48
48
|
const seen = new Set();
|
|
49
49
|
// Workspace half (takes precedence on name collisions).
|
|
50
50
|
try {
|
|
51
|
-
const ws = opts?.workspace ?? ensureWorkspaceSync();
|
|
51
|
+
const ws = opts?.workspace ?? ensureWorkspaceSync(opts?.cwd);
|
|
52
52
|
for (const f of listProjectFilesSync(ws)) {
|
|
53
53
|
if (seen.has(f.filename))
|
|
54
54
|
continue;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { enterJourney } from '../journey/chain.js';
|
|
2
|
+
import { openStatusPage } from '../util/open-in-browser.js';
|
|
3
|
+
export async function runJourneyEntry(trigger, a) {
|
|
4
|
+
const hooks = {
|
|
5
|
+
session: a.session,
|
|
6
|
+
cwd: a.cwd,
|
|
7
|
+
headless: a.headless,
|
|
8
|
+
emit: a.emit,
|
|
9
|
+
...(a.queueNext && !a.headless ? { queueNext: a.queueNext } : {}),
|
|
10
|
+
...(!a.standalone && a.adt ? { adt: a.adt } : {}),
|
|
11
|
+
sapAlias: a.sapAlias,
|
|
12
|
+
...(a.standalone ? { noSap: true } : {}),
|
|
13
|
+
signal: a.signal,
|
|
14
|
+
// Task 6 — the status page opens in the browser after Yes on its card.
|
|
15
|
+
openFile: (file) => openStatusPage(file),
|
|
16
|
+
};
|
|
17
|
+
try {
|
|
18
|
+
await enterJourney(trigger, hooks);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
if (process.env['CSPEACH_DEBUG'] === '1')
|
|
22
|
+
throw err;
|
|
23
|
+
a.emit('The clean-core steps stopped here. Say carry on to continue.');
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/repl.js
CHANGED
|
@@ -10,7 +10,7 @@ import { select, password as promptPassword } from './repl/themed-prompts.js';
|
|
|
10
10
|
import { loadConfig, resolveLocalBuild, resolveStandalone, DEFAULT_CONFIG } from './config/loader.js';
|
|
11
11
|
import { getLook, setLook, setBackground } from './renderer/look.js';
|
|
12
12
|
import { applyColorMode } from './renderer/color-mode.js';
|
|
13
|
-
import { startEarlyBrand, aliasSourceNote, panelsHintLine, panelsResumeLine, panelsStartTail, panelsInterruptedNote, formatPromptEcho } from './renderer/startup-lines.js';
|
|
13
|
+
import { startEarlyBrand, aliasSourceNote, panelsHintLine, panelsResumeLine, panelsStartTail, panelsInterruptedNote, lastSessionHereLine, formatPromptEcho } from './renderer/startup-lines.js';
|
|
14
14
|
import { formatAttention, formatFailure, formatInfo } from './renderer/notice-shape.js';
|
|
15
15
|
import { formatPanel } from './renderer/panel-rows.js';
|
|
16
16
|
import { sanitizeControlChars } from './renderer/sanitize.js';
|
|
@@ -21,6 +21,7 @@ import { createOfflineAdtClient } from './sap/offline-adt-client.js';
|
|
|
21
21
|
import { STANDALONE_ALIAS } from './tools/index.js';
|
|
22
22
|
import { resumeStandaloneDecision, applySystemSwitch } from './repl/resume-standalone.js';
|
|
23
23
|
import { clearAppFlow } from './lane/app-flow.js';
|
|
24
|
+
import { clearJourneyFlow, isJourneyHop, journeyHopLine } from './journey/flow.js';
|
|
24
25
|
import { applyResumedSkill, isShortFollowUp } from './router/resume-routing.js';
|
|
25
26
|
import { readableInterruptReason, isUserStopReason } from './session/interrupt-reason.js';
|
|
26
27
|
import { getStore } from './auth/api-key.js';
|
|
@@ -79,8 +80,8 @@ import { registerReadline, withInquirer } from './repl/inquirer-guard.js';
|
|
|
79
80
|
import { pipeRevivedStdin } from './repl/reset-tty-stdin.js';
|
|
80
81
|
import { logsDir } from './config/paths.js';
|
|
81
82
|
import { noticeLogPathFor, pruneNoticeLogs, setNoticeLogPath } from './renderer/notice-log.js';
|
|
82
|
-
import {
|
|
83
|
-
import {
|
|
83
|
+
import { maybeOfferAbapforgeMigration } from './projects/migration.js';
|
|
84
|
+
import { shouldOfferAbapforgeMigration } from './projects/workspace-retire.js';
|
|
84
85
|
import { slashCompleter } from './repl/slash-completer.js';
|
|
85
86
|
import { openSlashPicker, isExactSlashMatch } from './repl/slash-picker.js';
|
|
86
87
|
import { openAtPicker, pendingAtTokenPrefix } from './repl/at-picker.js';
|
|
@@ -170,12 +171,14 @@ import './tools/project/convention_get.js';
|
|
|
170
171
|
import './tools/project/cca_merge.js';
|
|
171
172
|
import './tools/project/upgrade_merge_progress.js';
|
|
172
173
|
import './tools/project/team_findings.js';
|
|
174
|
+
import './tools/project/team_estate.js';
|
|
173
175
|
import './tools/subagent/_background-shared.js';
|
|
174
176
|
import './tools/subagent/background_run.js';
|
|
175
177
|
import './tools/subagent/monitor_emit.js';
|
|
176
178
|
import './tools/subagent/schedule_draft_create.js';
|
|
177
179
|
import './tools/subagent/agent_run.js';
|
|
178
180
|
import './tools/subagent/read_agent.js';
|
|
181
|
+
import { folderTag, currentFolderKey } from './session/project-match.js';
|
|
179
182
|
const HISTORY_SIZE = 500;
|
|
180
183
|
// Persistence (read + dedup-guarded append) lives in ./repl/history.ts —
|
|
181
184
|
// the single writer shared with the Ink Footer (Task 8, ux-wave1).
|
|
@@ -390,22 +393,52 @@ export async function runRepl(opts) {
|
|
|
390
393
|
const earlyBrand = getLook() === 'panels'
|
|
391
394
|
? startEarlyBrand({ version: CLI_VERSION, tty: process.stdout.isTTY === true, write: (s) => { process.stdout.write(s); } })
|
|
392
395
|
: undefined;
|
|
396
|
+
// 2026-10-08 (Task 12) — once, tag sessions saved before folders existed so
|
|
397
|
+
// the lines below and the pickers see them. Both looks; capped, never throws.
|
|
398
|
+
try {
|
|
399
|
+
const { backfillSessionFolders } = await import('./session/backfill.js');
|
|
400
|
+
await backfillSessionFolders().catch(() => { });
|
|
401
|
+
}
|
|
402
|
+
catch { /* best effort */ }
|
|
393
403
|
// v0.6 — surface interrupted sessions at startup so the developer doesn't
|
|
394
404
|
// have to remember the session id from the crash UX. Like `git stash list`
|
|
395
405
|
// for resilience: if you have any unfinished work, you see it immediately.
|
|
396
406
|
// Skip when --resume is already in flight (the user is resuming right now).
|
|
407
|
+
// 2026-10-08 (sessions/folders) — both lines are scoped to THIS folder; the
|
|
408
|
+
// dim "Last session here" line points at `cspeach --continue`.
|
|
397
409
|
if (!opts?.resumedSession) {
|
|
398
410
|
try {
|
|
399
|
-
const { countInterruptedSessions } = await import('./session/store.js');
|
|
400
|
-
const
|
|
411
|
+
const { countInterruptedSessions, latestSessionHere } = await import('./session/store.js');
|
|
412
|
+
const here = currentFolderKey(process.cwd());
|
|
413
|
+
const n = await countInterruptedSessions(here);
|
|
401
414
|
if (n > 0) {
|
|
402
415
|
if (getLook() === 'panels')
|
|
403
416
|
startupNotices.push(panelsInterruptedNote(n) + '\n');
|
|
404
417
|
else
|
|
405
|
-
console.log(formatAttention({ text: `You have ${n} interrupted session${n === 1 ? '' : 's'}`, next: 'cspeach --resume (
|
|
418
|
+
console.log(formatAttention({ text: `You have ${n} interrupted session${n === 1 ? '' : 's'}`, next: 'cspeach --resume (this folder\'s sessions)' }));
|
|
419
|
+
}
|
|
420
|
+
const last = await latestSessionHere(here);
|
|
421
|
+
if (last) {
|
|
422
|
+
if (getLook() === 'panels')
|
|
423
|
+
startupNotices.push(lastSessionHereLine(last) + '\n');
|
|
424
|
+
else
|
|
425
|
+
console.log(lastSessionHereLine(last));
|
|
406
426
|
}
|
|
407
427
|
}
|
|
408
428
|
catch { /* startup banner is best-effort */ }
|
|
429
|
+
// Task 11 — files left in old save locations: point to `cspeach files
|
|
430
|
+
// sort`, at most once per folder per day.
|
|
431
|
+
try {
|
|
432
|
+
const { legacyHintLine, claimLegacyHintToday } = await import('./projects/workspace.js');
|
|
433
|
+
const hint = legacyHintLine(process.cwd());
|
|
434
|
+
if (hint && claimLegacyHintToday(process.cwd())) {
|
|
435
|
+
if (getLook() === 'panels')
|
|
436
|
+
startupNotices.push(formatAttention({ text: hint }));
|
|
437
|
+
else
|
|
438
|
+
console.log(chalk.dim(hint));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch { /* the hint is best-effort */ }
|
|
409
442
|
}
|
|
410
443
|
// Phase 2.1 of Track B — when CSPEACH_PROJECT_CONTEXT=on the dogfooder
|
|
411
444
|
// is on the experimental enrichment path. Surface that visibly so they
|
|
@@ -726,7 +759,7 @@ export async function runRepl(opts) {
|
|
|
726
759
|
// through the same role resolution new sessions use (after the awaited
|
|
727
760
|
// served config), marks it 'predicted', saves, then stays pinned. Every
|
|
728
761
|
// resume entry (picker, --resume <id>) lands here via session/resume.ts.
|
|
729
|
-
const session = opts?.resumedSession ?? newSession(makeSessionId(), selectedAlias, null, resolveModelRole('session_default', cfg));
|
|
762
|
+
const session = opts?.resumedSession ?? newSession(makeSessionId(), selectedAlias, null, resolveModelRole('session_default', cfg), folderTag(process.cwd()));
|
|
730
763
|
// Round 6 item 4c — a resumed session keeps its skill for the first plain prompt.
|
|
731
764
|
let resumeKeepsSkill = Boolean(opts?.resumedSession?.skill);
|
|
732
765
|
// Round 7 item 8c — the footer shows a resumed session's spend at once.
|
|
@@ -974,7 +1007,8 @@ export async function runRepl(opts) {
|
|
|
974
1007
|
return;
|
|
975
1008
|
}
|
|
976
1009
|
// Task 13 — no echo for a plan hop: the chain already said "Next: <title>".
|
|
977
|
-
|
|
1010
|
+
// Journey hops (Task 3) — the journey already said its step line.
|
|
1011
|
+
const notice = isJourneyHop(session, cmd) ? null : autoRoutedNotice(cmd);
|
|
978
1012
|
if (notice)
|
|
979
1013
|
chunkEmitter.emit('chunk', formatInfo(notice));
|
|
980
1014
|
setImmediate(() => { void handleSubmit(cmd, { planHop }); });
|
|
@@ -1003,7 +1037,10 @@ export async function runRepl(opts) {
|
|
|
1003
1037
|
// new turn's content touched with no separator and looked like
|
|
1004
1038
|
// garbled overlap).
|
|
1005
1039
|
if (trimmed !== '/exit' && trimmed !== '/quit') {
|
|
1006
|
-
|
|
1040
|
+
// Rehearsal fix 1 — a journey hop was queued by CSPeach, not typed: one
|
|
1041
|
+
// plain journey line, never "You › <the command>".
|
|
1042
|
+
const hopLine = journeyHopLine(session, trimmed);
|
|
1043
|
+
chunkEmitter.emit('chunk', hopLine ? `\n${hopLine}\n` : formatPromptEcho(trimmed, process.stdout.columns || 80));
|
|
1007
1044
|
}
|
|
1008
1045
|
if (trimmed === '/exit' || trimmed === '/quit') {
|
|
1009
1046
|
done = true;
|
|
@@ -1213,6 +1250,7 @@ export async function runRepl(opts) {
|
|
|
1213
1250
|
session.todos = [];
|
|
1214
1251
|
todoEmitter.emit('update', []);
|
|
1215
1252
|
clearAppFlow(session); // Task 7 — /new ends an /abap-app flow
|
|
1253
|
+
clearJourneyFlow(session); // Task 3 — and a clean-core journey
|
|
1216
1254
|
chunkEmitter.emit('chunk', '\n' + formatInfo('Fresh start · the next prompt picks a skill again'));
|
|
1217
1255
|
return;
|
|
1218
1256
|
}
|
|
@@ -1271,6 +1309,41 @@ export async function runRepl(opts) {
|
|
|
1271
1309
|
if (rejectUnknownSkill(parsed, (m) => chunkEmitter.emit('chunk', chalk.yellow(`\n${m}\n`)))) {
|
|
1272
1310
|
return;
|
|
1273
1311
|
}
|
|
1312
|
+
// Guided clean-core journey (Task 3) — "start clean core on ZFI_AR",
|
|
1313
|
+
// "/abap-clean-core start ZFI_AR", "carry on" in a project folder.
|
|
1314
|
+
// Detected here, before the classifier; any other line is untouched.
|
|
1315
|
+
{
|
|
1316
|
+
const { detectJourney } = await import('./journey/trigger.js');
|
|
1317
|
+
const journey = detectJourney(effectiveInput, { cwd: process.cwd(), session });
|
|
1318
|
+
if (journey) {
|
|
1319
|
+
const { runJourneyEntry } = await import('./repl/journey-entry.js');
|
|
1320
|
+
const journeyAbort = new AbortController();
|
|
1321
|
+
currentTurnAbort = journeyAbort;
|
|
1322
|
+
interruptCount = 0;
|
|
1323
|
+
if (pendingCancel.applyTo(journeyAbort))
|
|
1324
|
+
interruptCount = 1;
|
|
1325
|
+
turnStatusEmitter.start('clean core');
|
|
1326
|
+
try {
|
|
1327
|
+
await runJourneyEntry(journey, {
|
|
1328
|
+
session, cwd: process.cwd(), adt, sapAlias: selectedAlias, standalone, headless: isHeadless(),
|
|
1329
|
+
emit: (line) => chunkEmitter.emit('chunk', line + '\n'),
|
|
1330
|
+
queueNext: (cmd) => pendingDispatchAccessor.set(cmd),
|
|
1331
|
+
signal: journeyAbort.signal,
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
finally {
|
|
1335
|
+
turnStatusEmitter.stop();
|
|
1336
|
+
currentTurnAbort = null;
|
|
1337
|
+
postTurn = true;
|
|
1338
|
+
}
|
|
1339
|
+
const queued = pendingDispatchAccessor.get();
|
|
1340
|
+
if (queued) {
|
|
1341
|
+
pendingDispatchAccessor.set(null);
|
|
1342
|
+
autoDispatch(queued);
|
|
1343
|
+
}
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1274
1347
|
// Record the prose body (post-shortcut stripping) for /reroute. Using
|
|
1275
1348
|
// `parsed.prompt` rather than `trimmed` means:
|
|
1276
1349
|
// - plain prose: `parsed.prompt === trimmed`, stored as-is
|
|
@@ -2355,6 +2428,17 @@ export async function runRepl(opts) {
|
|
|
2355
2428
|
// sits in this folder. Best-effort: never blocks the prompt.
|
|
2356
2429
|
if (!isHeadless()) {
|
|
2357
2430
|
void (async () => {
|
|
2431
|
+
// 2026-10-08 — one-time offer to retire the old `workspace_folder`
|
|
2432
|
+
// setting, before the plan offer so the two never overlap.
|
|
2433
|
+
try {
|
|
2434
|
+
const { maybeOfferWorkspaceRetire } = await import('./projects/workspace-retire.js');
|
|
2435
|
+
const { inkAwareConfirm } = await import('./agent/loop.js');
|
|
2436
|
+
await maybeOfferWorkspaceRetire({
|
|
2437
|
+
confirm: async (q) => (await inkAwareConfirm(q, { default: 'y' })) === 'y',
|
|
2438
|
+
log: (line) => chunkEmitter.emit('chunk', line + '\n'),
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
catch { /* the offer is best-effort */ }
|
|
2358
2442
|
try {
|
|
2359
2443
|
const { offerContinueAtStartup, inkStartupChooser } = await import('./commands/plan-startup.js');
|
|
2360
2444
|
const { resolveWorkspacePath } = await import('./projects/workspace.js');
|
|
@@ -2363,6 +2447,7 @@ export async function runRepl(opts) {
|
|
|
2363
2447
|
return;
|
|
2364
2448
|
await offerContinueAtStartup({
|
|
2365
2449
|
workspaceDir: ws,
|
|
2450
|
+
cwd: process.cwd(),
|
|
2366
2451
|
sessionId: session.id,
|
|
2367
2452
|
sapAlias: session.sap_system ?? undefined,
|
|
2368
2453
|
choose: inkStartupChooser(),
|
|
@@ -2663,40 +2748,40 @@ export async function runRepl(opts) {
|
|
|
2663
2748
|
registerReadline(rl, (v) => {
|
|
2664
2749
|
suppressRlClose = v;
|
|
2665
2750
|
}, { input: pasteInput });
|
|
2666
|
-
// 2026-
|
|
2667
|
-
//
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
],
|
|
2680
|
-
})),
|
|
2681
|
-
log: (line) => console.log(chalk.dim(line)),
|
|
2682
|
-
});
|
|
2751
|
+
// 2026-10-08: one-time offer to retire the old `workspace_folder` setting
|
|
2752
|
+
// (files now save in the folder you start in). Best-effort, never blocks.
|
|
2753
|
+
if (!isHeadless()) {
|
|
2754
|
+
try {
|
|
2755
|
+
const { maybeOfferWorkspaceRetire } = await import('./projects/workspace-retire.js');
|
|
2756
|
+
const { inkAwareConfirm } = await import('./agent/loop.js');
|
|
2757
|
+
await maybeOfferWorkspaceRetire({
|
|
2758
|
+
confirm: async (q) => (await inkAwareConfirm(q, { default: 'y' })) === 'y',
|
|
2759
|
+
log: (line) => console.log(chalk.dim(line)),
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
catch { /* best-effort */ }
|
|
2763
|
+
}
|
|
2683
2764
|
// 2026-06-15: one-time output-namespace migration. The customer-output
|
|
2684
2765
|
// root was renamed from the dead-brand `.abapforge/` to `.cspeach/`.
|
|
2685
2766
|
// Workspaces that still have a top-level `.abapforge/` get offered a
|
|
2686
2767
|
// one-time merge-move into `.cspeach/`. The dual-read resolver keeps old
|
|
2687
2768
|
// projects working if the user declines; "never" suppresses future offers.
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2769
|
+
// 2026-10-08: solo folders only — a team folder must not merge into the
|
|
2770
|
+
// shared team .cspeach, and a home start must not target the CLI home.
|
|
2771
|
+
if (shouldOfferAbapforgeMigration(process.cwd())) {
|
|
2772
|
+
await maybeOfferAbapforgeMigration(process.cwd(), {
|
|
2773
|
+
prompt: async ({ legacyDir, newDir }) => withInquirer(() => select({
|
|
2774
|
+
message: `Found legacy CSPeach outputs in ${legacyDir}. The output folder is now '.cspeach/'. Move them?`,
|
|
2775
|
+
default: 'move',
|
|
2776
|
+
choices: [
|
|
2777
|
+
{ value: 'move', name: `Move everything into ${newDir} (recommended)` },
|
|
2778
|
+
{ value: 'skip', name: `Not now — ask me again next time` },
|
|
2779
|
+
{ value: 'never', name: `Leave it and never ask again` },
|
|
2780
|
+
],
|
|
2781
|
+
})),
|
|
2782
|
+
log: (line) => console.log(chalk.dim(line)),
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2700
2785
|
// 2026-10-04 (spec §3.5) — "Continue '<plan>'?" (classic: numbered
|
|
2701
2786
|
// choices, Enter = the first). Continue queues the resume for the loop.
|
|
2702
2787
|
if (!isHeadless()) {
|
|
@@ -2708,6 +2793,7 @@ export async function runRepl(opts) {
|
|
|
2708
2793
|
if (ws) {
|
|
2709
2794
|
await offerContinueAtStartup({
|
|
2710
2795
|
workspaceDir: ws,
|
|
2796
|
+
cwd: process.cwd(),
|
|
2711
2797
|
sessionId: session.id,
|
|
2712
2798
|
sapAlias: session.sap_system ?? undefined,
|
|
2713
2799
|
choose: classicStartupChooser(inkAwarePrompt, (...l) => l.forEach((x) => console.log(x))),
|
|
@@ -2743,9 +2829,13 @@ export async function runRepl(opts) {
|
|
|
2743
2829
|
// destroys the session context.
|
|
2744
2830
|
// Task 13 — no echo for a plan hop: the chain already said "Next: <title>".
|
|
2745
2831
|
{
|
|
2746
|
-
const n = autoRoutedNotice(dispatchedCmd);
|
|
2832
|
+
const n = isJourneyHop(session, dispatchedCmd) ? null : autoRoutedNotice(dispatchedCmd);
|
|
2747
2833
|
if (n)
|
|
2748
2834
|
console.log(formatInfo(n).trimEnd());
|
|
2835
|
+
// Rehearsal fix 1 — the journey hop says what CSPeach is doing (the Ink mirror).
|
|
2836
|
+
const hop = journeyHopLine(session, dispatchedCmd);
|
|
2837
|
+
if (hop)
|
|
2838
|
+
console.log(hop);
|
|
2749
2839
|
classicPlanHop = planHops.take(dispatchedCmd);
|
|
2750
2840
|
}
|
|
2751
2841
|
line = dispatchedCmd;
|
|
@@ -3041,6 +3131,7 @@ export async function runRepl(opts) {
|
|
|
3041
3131
|
session.awaitingSkillAnswer = false;
|
|
3042
3132
|
session.todos = []; // P2 — /new clears the plan (spec §4)
|
|
3043
3133
|
clearAppFlow(session); // Task 7 — /new ends an /abap-app flow
|
|
3134
|
+
clearJourneyFlow(session); // Task 3 — and a clean-core journey
|
|
3044
3135
|
console.log(formatInfo('Fresh start · the next prompt picks a skill again').trimEnd());
|
|
3045
3136
|
continue;
|
|
3046
3137
|
}
|
|
@@ -3223,6 +3314,33 @@ export async function runRepl(opts) {
|
|
|
3223
3314
|
if (rejectUnknownSkill(parsed, (m) => console.log(chalk.yellow(`\n${m}\n`)))) {
|
|
3224
3315
|
continue;
|
|
3225
3316
|
}
|
|
3317
|
+
// Guided clean-core journey (Task 3) — classic mirror of the Ink entry.
|
|
3318
|
+
// Cards become one typed line here (no card listener); a queued step is
|
|
3319
|
+
// picked up by the loop-top dispatch consumer.
|
|
3320
|
+
{
|
|
3321
|
+
const { detectJourney } = await import('./journey/trigger.js');
|
|
3322
|
+
const journey = detectJourney(resolvedTrimmed, { cwd: process.cwd(), session });
|
|
3323
|
+
if (journey) {
|
|
3324
|
+
const { runJourneyEntry } = await import('./repl/journey-entry.js');
|
|
3325
|
+
const journeyAbort = new AbortController();
|
|
3326
|
+
const onJourneySigint = () => { journeyAbort.abort(); };
|
|
3327
|
+
turnInProgress = true;
|
|
3328
|
+
rl.on('SIGINT', onJourneySigint);
|
|
3329
|
+
try {
|
|
3330
|
+
await runJourneyEntry(journey, {
|
|
3331
|
+
session, cwd: process.cwd(), adt, sapAlias: selectedAlias, standalone, headless: isHeadless(),
|
|
3332
|
+
emit: (line) => console.log(line),
|
|
3333
|
+
queueNext: (cmd) => pendingDispatchAccessor.set(cmd),
|
|
3334
|
+
signal: journeyAbort.signal,
|
|
3335
|
+
});
|
|
3336
|
+
}
|
|
3337
|
+
finally {
|
|
3338
|
+
rl.removeListener('SIGINT', onJourneySigint);
|
|
3339
|
+
turnInProgress = false;
|
|
3340
|
+
}
|
|
3341
|
+
continue;
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3226
3344
|
// Record the prose body (post-shortcut stripping) for /reroute. Using
|
|
3227
3345
|
// `parsed.prompt` rather than `trimmed` means:
|
|
3228
3346
|
// - plain prose: `parsed.prompt === trimmed`, stored as-is
|