@cspeach/cli 1.1.30 → 1.1.32
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/README.md +11 -1
- package/dist/agent/loop.js +132 -9
- package/dist/cli.js +49 -18
- package/dist/commands/config-set.js +1 -1
- package/dist/commands/help.js +16 -1
- package/dist/commands/plan-chain.js +3 -0
- package/dist/commands/plan-continue.js +1 -1
- package/dist/commands/plan-gate.js +17 -1
- package/dist/commands/plan-resume.js +28 -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-rules.js +187 -0
- package/dist/commands/team.js +971 -95
- package/dist/journey/chain.js +978 -0
- package/dist/journey/clean-core.js +235 -0
- package/dist/journey/flow.js +216 -0
- package/dist/journey/rebuild.js +464 -0
- package/dist/journey/run.js +628 -0
- package/dist/journey/session-deps.js +81 -0
- package/dist/journey/trigger.js +153 -0
- package/dist/lane/app-chain.js +21 -9
- package/dist/lane/chain-token.js +9 -5
- package/dist/lane/roots.js +97 -41
- package/dist/one-shot.js +28 -2
- package/dist/projects/clean-core-confirm.js +192 -3
- 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 +100 -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/baseline.js +39 -2
- package/dist/register/decision-rules.js +35 -0
- package/dist/register/discover.js +931 -0
- package/dist/register/export-files.js +58 -0
- package/dist/register/findings.js +5 -2
- package/dist/register/measure.js +191 -9
- package/dist/register/offer.js +100 -8
- package/dist/register/picture.js +6 -2
- package/dist/register/plan-import.js +328 -0
- package/dist/register/records.js +40 -3
- package/dist/register/rule-proposal.js +4 -0
- package/dist/register/rules-edit.js +257 -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 +106 -90
- package/dist/register/status-file/report-model.js +569 -0
- package/dist/register/status-file/team-css.js +1 -1
- package/dist/register/status-file-sync.js +4 -0
- package/dist/register/status-html.js +27 -6
- package/dist/register/store.js +44 -5
- package/dist/register/variant.js +65 -0
- package/dist/register/workbooks.js +28 -42
- package/dist/register/write-whole.js +30 -2
- 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 +3 -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/builtin-commands.js +4 -0
- package/dist/repl/journey-entry.js +27 -0
- package/dist/repl/plan-hop.js +2 -9
- package/dist/repl/rules-menu.js +177 -0
- package/dist/repl/slash-completer.js +1 -0
- package/dist/repl/slash-picker.js +5 -4
- package/dist/repl.js +236 -59
- package/dist/router/classifier.js +8 -4
- package/dist/router/router-core.js +43 -7
- package/dist/router/routing-decision.js +1 -1
- package/dist/runs/bundled-runs.js +100 -0
- package/dist/runs/front-door.js +19 -0
- package/dist/runs/manifest-runs.js +50 -0
- package/dist/runs/run-command.js +24 -0
- package/dist/runs/runs.js +27 -0
- package/dist/runs/shadow.js +100 -0
- package/dist/runs/types.js +1 -0
- package/dist/session/backfill.js +342 -0
- package/dist/session/project-match.js +63 -0
- package/dist/session/queued-step.js +45 -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 +53 -53
- 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/dispatch-skill.js +8 -4
- 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/lane/app-lane-resolve.js +1 -1
- package/dist/tools/project/cca_merge.js +8 -2
- package/dist/tools/project/rules_propose.js +59 -0
- package/dist/tools/project/team_estate.js +91 -0
- package/dist/tools/project/team_findings.js +89 -5
- 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/upgrade-check.js +49 -4
- package/dist/util/open-in-browser.js +143 -0
- package/package.json +3 -3
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,8 @@ 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 } from './journey/flow.js';
|
|
25
|
+
import { createDispatchSlot, queuedStepView } from './session/queued-step.js';
|
|
24
26
|
import { applyResumedSkill, isShortFollowUp } from './router/resume-routing.js';
|
|
25
27
|
import { readableInterruptReason, isUserStopReason } from './session/interrupt-reason.js';
|
|
26
28
|
import { getStore } from './auth/api-key.js';
|
|
@@ -79,10 +81,12 @@ import { registerReadline, withInquirer } from './repl/inquirer-guard.js';
|
|
|
79
81
|
import { pipeRevivedStdin } from './repl/reset-tty-stdin.js';
|
|
80
82
|
import { logsDir } from './config/paths.js';
|
|
81
83
|
import { noticeLogPathFor, pruneNoticeLogs, setNoticeLogPath } from './renderer/notice-log.js';
|
|
82
|
-
import {
|
|
83
|
-
import {
|
|
84
|
+
import { maybeOfferAbapforgeMigration } from './projects/migration.js';
|
|
85
|
+
import { shouldOfferAbapforgeMigration } from './projects/workspace-retire.js';
|
|
84
86
|
import { slashCompleter } from './repl/slash-completer.js';
|
|
85
87
|
import { openSlashPicker, isExactSlashMatch } from './repl/slash-picker.js';
|
|
88
|
+
import { detectRunStart } from './runs/front-door.js';
|
|
89
|
+
import { shadowStart, guardRunLabel } from './runs/shadow.js';
|
|
86
90
|
import { openAtPicker, pendingAtTokenPrefix } from './repl/at-picker.js';
|
|
87
91
|
import { atCompleter } from './repl/at-completer.js';
|
|
88
92
|
import { appendHistoryEntry, readHistoryEntries } from './repl/history.js';
|
|
@@ -170,12 +174,15 @@ import './tools/project/convention_get.js';
|
|
|
170
174
|
import './tools/project/cca_merge.js';
|
|
171
175
|
import './tools/project/upgrade_merge_progress.js';
|
|
172
176
|
import './tools/project/team_findings.js';
|
|
177
|
+
import './tools/project/team_estate.js';
|
|
178
|
+
import './tools/project/rules_propose.js';
|
|
173
179
|
import './tools/subagent/_background-shared.js';
|
|
174
180
|
import './tools/subagent/background_run.js';
|
|
175
181
|
import './tools/subagent/monitor_emit.js';
|
|
176
182
|
import './tools/subagent/schedule_draft_create.js';
|
|
177
183
|
import './tools/subagent/agent_run.js';
|
|
178
184
|
import './tools/subagent/read_agent.js';
|
|
185
|
+
import { folderTag, currentFolderKey } from './session/project-match.js';
|
|
179
186
|
const HISTORY_SIZE = 500;
|
|
180
187
|
// Persistence (read + dedup-guarded append) lives in ./repl/history.ts —
|
|
181
188
|
// the single writer shared with the Ink Footer (Task 8, ux-wave1).
|
|
@@ -390,22 +397,52 @@ export async function runRepl(opts) {
|
|
|
390
397
|
const earlyBrand = getLook() === 'panels'
|
|
391
398
|
? startEarlyBrand({ version: CLI_VERSION, tty: process.stdout.isTTY === true, write: (s) => { process.stdout.write(s); } })
|
|
392
399
|
: undefined;
|
|
400
|
+
// 2026-10-08 (Task 12) — once, tag sessions saved before folders existed so
|
|
401
|
+
// the lines below and the pickers see them. Both looks; capped, never throws.
|
|
402
|
+
try {
|
|
403
|
+
const { backfillSessionFolders } = await import('./session/backfill.js');
|
|
404
|
+
await backfillSessionFolders().catch(() => { });
|
|
405
|
+
}
|
|
406
|
+
catch { /* best effort */ }
|
|
393
407
|
// v0.6 — surface interrupted sessions at startup so the developer doesn't
|
|
394
408
|
// have to remember the session id from the crash UX. Like `git stash list`
|
|
395
409
|
// for resilience: if you have any unfinished work, you see it immediately.
|
|
396
410
|
// Skip when --resume is already in flight (the user is resuming right now).
|
|
411
|
+
// 2026-10-08 (sessions/folders) — both lines are scoped to THIS folder; the
|
|
412
|
+
// dim "Last session here" line points at `cspeach --continue`.
|
|
397
413
|
if (!opts?.resumedSession) {
|
|
398
414
|
try {
|
|
399
|
-
const { countInterruptedSessions } = await import('./session/store.js');
|
|
400
|
-
const
|
|
415
|
+
const { countInterruptedSessions, latestSessionHere } = await import('./session/store.js');
|
|
416
|
+
const here = currentFolderKey(process.cwd());
|
|
417
|
+
const n = await countInterruptedSessions(here);
|
|
401
418
|
if (n > 0) {
|
|
402
419
|
if (getLook() === 'panels')
|
|
403
420
|
startupNotices.push(panelsInterruptedNote(n) + '\n');
|
|
404
421
|
else
|
|
405
|
-
console.log(formatAttention({ text: `You have ${n} interrupted session${n === 1 ? '' : 's'}`, next: 'cspeach --resume (
|
|
422
|
+
console.log(formatAttention({ text: `You have ${n} interrupted session${n === 1 ? '' : 's'}`, next: 'cspeach --resume (this folder\'s sessions)' }));
|
|
423
|
+
}
|
|
424
|
+
const last = await latestSessionHere(here);
|
|
425
|
+
if (last) {
|
|
426
|
+
if (getLook() === 'panels')
|
|
427
|
+
startupNotices.push(lastSessionHereLine(last) + '\n');
|
|
428
|
+
else
|
|
429
|
+
console.log(lastSessionHereLine(last));
|
|
406
430
|
}
|
|
407
431
|
}
|
|
408
432
|
catch { /* startup banner is best-effort */ }
|
|
433
|
+
// Task 11 — files left in old save locations: point to `cspeach files
|
|
434
|
+
// sort`, at most once per folder per day.
|
|
435
|
+
try {
|
|
436
|
+
const { legacyHintLine, claimLegacyHintToday } = await import('./projects/workspace.js');
|
|
437
|
+
const hint = legacyHintLine(process.cwd());
|
|
438
|
+
if (hint && claimLegacyHintToday(process.cwd())) {
|
|
439
|
+
if (getLook() === 'panels')
|
|
440
|
+
startupNotices.push(formatAttention({ text: hint }));
|
|
441
|
+
else
|
|
442
|
+
console.log(chalk.dim(hint));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
catch { /* the hint is best-effort */ }
|
|
409
446
|
}
|
|
410
447
|
// Phase 2.1 of Track B — when CSPEACH_PROJECT_CONTEXT=on the dogfooder
|
|
411
448
|
// is on the experimental enrichment path. Surface that visibly so they
|
|
@@ -726,7 +763,7 @@ export async function runRepl(opts) {
|
|
|
726
763
|
// through the same role resolution new sessions use (after the awaited
|
|
727
764
|
// served config), marks it 'predicted', saves, then stays pinned. Every
|
|
728
765
|
// resume entry (picker, --resume <id>) lands here via session/resume.ts.
|
|
729
|
-
const session = opts?.resumedSession ?? newSession(makeSessionId(), selectedAlias, null, resolveModelRole('session_default', cfg));
|
|
766
|
+
const session = opts?.resumedSession ?? newSession(makeSessionId(), selectedAlias, null, resolveModelRole('session_default', cfg), folderTag(process.cwd()));
|
|
730
767
|
// Round 6 item 4c — a resumed session keeps its skill for the first plain prompt.
|
|
731
768
|
let resumeKeepsSkill = Boolean(opts?.resumedSession?.skill);
|
|
732
769
|
// Round 7 item 8c — the footer shows a resumed session's spend at once.
|
|
@@ -860,11 +897,11 @@ export async function runRepl(opts) {
|
|
|
860
897
|
// scope so both the Ink and readline branches share the same instance.
|
|
861
898
|
// Cleared at the start of every consume cycle so a stale value never
|
|
862
899
|
// re-fires.
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
900
|
+
// Audit M-2 — the slot also carries WHO queued the command: a CSPeach source
|
|
901
|
+
// (journey, rebuild hand-off, app chain, plan chain) sets its origin, and both
|
|
902
|
+
// branches render it through queuedStepView — never as "You › <the command>".
|
|
903
|
+
// Consumers take() the command and its origin together.
|
|
904
|
+
const pendingDispatchAccessor = createDispatchSlot();
|
|
868
905
|
// Task 13 fix round — the plan chain queues through planHops.queue, which
|
|
869
906
|
// marks the command as an in-run hop (one progress line, no guarded
|
|
870
907
|
// notice). Anything else in the slot (the /abap-app start, a skill switch)
|
|
@@ -962,7 +999,7 @@ export async function runRepl(opts) {
|
|
|
962
999
|
/** Auto-dispatch of the next plan phase, unless a post-turn cancel stopped it. */
|
|
963
1000
|
// Task 13 fix round — `planHop` travels WITH the command into handleSubmit,
|
|
964
1001
|
// so a refused submit can never leave a stale hop mark behind.
|
|
965
|
-
const autoDispatch = (cmd, planHop = false) => {
|
|
1002
|
+
const autoDispatch = (cmd, planHop = false, queued = null) => {
|
|
966
1003
|
// Round 4 item 9b — the exit is already under way: start nothing new.
|
|
967
1004
|
// Final review B-I2 — `/exit` typed mid-turn (exitAfterTurn) stops it too.
|
|
968
1005
|
const decision = autoDispatchDecision({ done, exitRequested: exitAfterTurn, cancelPending: pendingCancel.pending }, cmd);
|
|
@@ -974,10 +1011,11 @@ export async function runRepl(opts) {
|
|
|
974
1011
|
return;
|
|
975
1012
|
}
|
|
976
1013
|
// Task 13 — no echo for a plan hop: the chain already said "Next: <title>".
|
|
977
|
-
|
|
1014
|
+
// Audit M-2 — no "Auto-routed:" for any command CSPeach queued itself.
|
|
1015
|
+
const notice = queued ? null : autoRoutedNotice(cmd);
|
|
978
1016
|
if (notice)
|
|
979
1017
|
chunkEmitter.emit('chunk', formatInfo(notice));
|
|
980
|
-
setImmediate(() => { void handleSubmit(cmd, { planHop }); });
|
|
1018
|
+
setImmediate(() => { void handleSubmit(cmd, { planHop, queued }); });
|
|
981
1019
|
};
|
|
982
1020
|
// BUG-2b — the actual dispatch. Wrapped by handleSubmit (below) which
|
|
983
1021
|
// owns the single-flight reserve()/release() lifecycle. All re-entrant
|
|
@@ -1003,7 +1041,14 @@ export async function runRepl(opts) {
|
|
|
1003
1041
|
// new turn's content touched with no separator and looked like
|
|
1004
1042
|
// garbled overlap).
|
|
1005
1043
|
if (trimmed !== '/exit' && trimmed !== '/quit') {
|
|
1006
|
-
|
|
1044
|
+
// Rehearsal fix 1 + audit M-2 — a command CSPeach queued (journey,
|
|
1045
|
+
// rebuild hand-off, app chain, plan chain) is its own step on screen:
|
|
1046
|
+
// its one line, never "You › <the command>".
|
|
1047
|
+
const view = queuedStepView(submitOpts?.queued);
|
|
1048
|
+
if (view.echo)
|
|
1049
|
+
chunkEmitter.emit('chunk', formatPromptEcho(trimmed, process.stdout.columns || 80));
|
|
1050
|
+
else if (view.line)
|
|
1051
|
+
chunkEmitter.emit('chunk', `\n${view.line}\n`);
|
|
1007
1052
|
}
|
|
1008
1053
|
if (trimmed === '/exit' || trimmed === '/quit') {
|
|
1009
1054
|
done = true;
|
|
@@ -1213,6 +1258,7 @@ export async function runRepl(opts) {
|
|
|
1213
1258
|
session.todos = [];
|
|
1214
1259
|
todoEmitter.emit('update', []);
|
|
1215
1260
|
clearAppFlow(session); // Task 7 — /new ends an /abap-app flow
|
|
1261
|
+
clearJourneyFlow(session); // Task 3 — and a clean-core journey
|
|
1216
1262
|
chunkEmitter.emit('chunk', '\n' + formatInfo('Fresh start · the next prompt picks a skill again'));
|
|
1217
1263
|
return;
|
|
1218
1264
|
}
|
|
@@ -1237,6 +1283,19 @@ export async function runRepl(opts) {
|
|
|
1237
1283
|
rewindEmitter.emit('open');
|
|
1238
1284
|
return;
|
|
1239
1285
|
}
|
|
1286
|
+
// /rules — the team project's decision rules (step 2, Task 10, D7): table, menu, card before every save.
|
|
1287
|
+
if (trimmed === '/rules' || trimmed.startsWith('/rules ')) {
|
|
1288
|
+
const { runRulesSlash } = await import('./repl/rules-menu.js');
|
|
1289
|
+
const rulesAbort = new AbortController();
|
|
1290
|
+
currentTurnAbort = rulesAbort;
|
|
1291
|
+
try {
|
|
1292
|
+
await runRulesSlash(trimmed, { emit: (line) => chunkEmitter.emit('chunk', line + '\n'), headless: isHeadless(), signal: rulesAbort.signal });
|
|
1293
|
+
}
|
|
1294
|
+
finally {
|
|
1295
|
+
currentTurnAbort = null;
|
|
1296
|
+
}
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1240
1299
|
// /safety-mode on|off — structured Forge Rule confirmation cards.
|
|
1241
1300
|
// UX-5 #1 (2026-06-12): handleSafetyModeCommand existed (unit-tested)
|
|
1242
1301
|
// but no REPL path ever routed to it — the toggle was unreachable at
|
|
@@ -1262,6 +1321,25 @@ export async function runRepl(opts) {
|
|
|
1262
1321
|
effectiveInput = expansion.text;
|
|
1263
1322
|
}
|
|
1264
1323
|
}
|
|
1324
|
+
// Guided runs — the one front door for Ink, classic and one-shot:
|
|
1325
|
+
// the /run verb (rewritten to its alias), then today's trigger. The
|
|
1326
|
+
// trigger still decides (spec §4 step 2); the dispatch below is unchanged.
|
|
1327
|
+
const door = await detectRunStart(effectiveInput, {
|
|
1328
|
+
cwd: process.cwd(), session, resume: true,
|
|
1329
|
+
// Shadow (spec §4 step 2): the trigger decided; in managed mode ask our
|
|
1330
|
+
// router the same text, with the shadow-only text, and count whether it
|
|
1331
|
+
// agrees. Not awaited; counts only; nothing on screen.
|
|
1332
|
+
onFreeTextStart: (text) => void shadowStart(text, 'run-clean-core', {
|
|
1333
|
+
classify: (p, c) => classifyPrompt(p, c, { shadowRuns: true }),
|
|
1334
|
+
context: { sap_system: selectedAlias, last_skill: session.last_skill ?? null, last_object: session.last_object ?? null },
|
|
1335
|
+
llmMode: cfg.llm?.mode,
|
|
1336
|
+
}),
|
|
1337
|
+
});
|
|
1338
|
+
if (door.kind === 'line') {
|
|
1339
|
+
chunkEmitter.emit('chunk', door.line + '\n');
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
effectiveInput = door.input;
|
|
1265
1343
|
const parsed = parseShortcut(effectiveInput);
|
|
1266
1344
|
// D35 (2026-06-11) — explicit slash naming a skill that isn't in the
|
|
1267
1345
|
// catalog: print the notice ("did you mean" / builtin-with-args) and
|
|
@@ -1271,6 +1349,35 @@ export async function runRepl(opts) {
|
|
|
1271
1349
|
if (rejectUnknownSkill(parsed, (m) => chunkEmitter.emit('chunk', chalk.yellow(`\n${m}\n`)))) {
|
|
1272
1350
|
return;
|
|
1273
1351
|
}
|
|
1352
|
+
// Guided clean-core journey (Task 3) — "start clean core on ZFI_AR",
|
|
1353
|
+
// "/abap-clean-core start ZFI_AR", "/run clean-core ZFI_AR", "carry on"
|
|
1354
|
+
// in a project folder. Detected above, before the classifier.
|
|
1355
|
+
if (door.kind === 'run') {
|
|
1356
|
+
const { runJourneyEntry } = await import('./repl/journey-entry.js');
|
|
1357
|
+
const journeyAbort = new AbortController();
|
|
1358
|
+
currentTurnAbort = journeyAbort;
|
|
1359
|
+
interruptCount = 0;
|
|
1360
|
+
if (pendingCancel.applyTo(journeyAbort))
|
|
1361
|
+
interruptCount = 1;
|
|
1362
|
+
turnStatusEmitter.start('clean core');
|
|
1363
|
+
try {
|
|
1364
|
+
await runJourneyEntry(door.trigger, {
|
|
1365
|
+
session, cwd: process.cwd(), adt, sapAlias: selectedAlias, standalone, headless: isHeadless(),
|
|
1366
|
+
emit: (line) => chunkEmitter.emit('chunk', line + '\n'),
|
|
1367
|
+
queueNext: (cmd, origin) => pendingDispatchAccessor.set(cmd, origin),
|
|
1368
|
+
signal: journeyAbort.signal,
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
finally {
|
|
1372
|
+
turnStatusEmitter.stop();
|
|
1373
|
+
currentTurnAbort = null;
|
|
1374
|
+
postTurn = true;
|
|
1375
|
+
}
|
|
1376
|
+
const queued = pendingDispatchAccessor.take();
|
|
1377
|
+
if (queued)
|
|
1378
|
+
autoDispatch(queued.cmd, false, queued.origin);
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1274
1381
|
// Record the prose body (post-shortcut stripping) for /reroute. Using
|
|
1275
1382
|
// `parsed.prompt` rather than `trimmed` means:
|
|
1276
1383
|
// - plain prose: `parsed.prompt === trimmed`, stored as-is
|
|
@@ -1366,6 +1473,8 @@ export async function runRepl(opts) {
|
|
|
1366
1473
|
}
|
|
1367
1474
|
return;
|
|
1368
1475
|
}
|
|
1476
|
+
// Defensive (spec §4 step 2): a run label never dispatches in this release.
|
|
1477
|
+
c = guardRunLabel(c);
|
|
1369
1478
|
if (c.screenshot) {
|
|
1370
1479
|
chunkEmitter.emit('chunk', formatInfo(`Screenshot: ${c.screenshot}`));
|
|
1371
1480
|
}
|
|
@@ -1595,10 +1704,9 @@ export async function runRepl(opts) {
|
|
|
1595
1704
|
const { clearPlanGrant } = await import('./approvals/plan-grant.js');
|
|
1596
1705
|
clearPlanGrant();
|
|
1597
1706
|
}
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
autoDispatch(
|
|
1601
|
-
}
|
|
1707
|
+
const reaudit = pendingDispatchAccessor.take();
|
|
1708
|
+
if (reaudit)
|
|
1709
|
+
autoDispatch(reaudit.cmd, planHops.take(reaudit.cmd), reaudit.origin);
|
|
1602
1710
|
return;
|
|
1603
1711
|
}
|
|
1604
1712
|
// 2026-10-04 (continuous run) — a phase boundary is save + COMPACT:
|
|
@@ -2088,9 +2196,8 @@ export async function runRepl(opts) {
|
|
|
2088
2196
|
// next-phase offer above queued the plan-resume it built itself.
|
|
2089
2197
|
// Consume the slot and re-enter handleSubmit with that command —
|
|
2090
2198
|
// no user keypress. The user's consent was the picker pick / y answer.
|
|
2091
|
-
const dispatched = pendingDispatchAccessor.
|
|
2199
|
+
const dispatched = pendingDispatchAccessor.take();
|
|
2092
2200
|
if (dispatched) {
|
|
2093
|
-
pendingDispatchAccessor.set(null);
|
|
2094
2201
|
// No context clear here: beginPlanPhaseContext (plan-boundary.ts,
|
|
2095
2202
|
// called in the plan-resume prepare branch of handleSubmit) is the
|
|
2096
2203
|
// single owner of the phase-boundary compaction — it runs for typed AND dispatched resumes,
|
|
@@ -2100,7 +2207,7 @@ export async function runRepl(opts) {
|
|
|
2100
2207
|
// resolves cleanly before the next one starts. Recursion via
|
|
2101
2208
|
// setImmediate keeps the call stack flat across long chains.
|
|
2102
2209
|
// (autoDispatch skips it when a post-turn Ctrl+C / /exit asked to stop.)
|
|
2103
|
-
autoDispatch(dispatched, planHops.take(dispatched));
|
|
2210
|
+
autoDispatch(dispatched.cmd, planHops.take(dispatched.cmd), dispatched.origin);
|
|
2104
2211
|
}
|
|
2105
2212
|
};
|
|
2106
2213
|
// BUG-2b (2026-07-05 live smoke) — single-flight dispatch guard.
|
|
@@ -2355,6 +2462,17 @@ export async function runRepl(opts) {
|
|
|
2355
2462
|
// sits in this folder. Best-effort: never blocks the prompt.
|
|
2356
2463
|
if (!isHeadless()) {
|
|
2357
2464
|
void (async () => {
|
|
2465
|
+
// 2026-10-08 — one-time offer to retire the old `workspace_folder`
|
|
2466
|
+
// setting, before the plan offer so the two never overlap.
|
|
2467
|
+
try {
|
|
2468
|
+
const { maybeOfferWorkspaceRetire } = await import('./projects/workspace-retire.js');
|
|
2469
|
+
const { inkAwareConfirm } = await import('./agent/loop.js');
|
|
2470
|
+
await maybeOfferWorkspaceRetire({
|
|
2471
|
+
confirm: async (q) => (await inkAwareConfirm(q, { default: 'y' })) === 'y',
|
|
2472
|
+
log: (line) => chunkEmitter.emit('chunk', line + '\n'),
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
catch { /* the offer is best-effort */ }
|
|
2358
2476
|
try {
|
|
2359
2477
|
const { offerContinueAtStartup, inkStartupChooser } = await import('./commands/plan-startup.js');
|
|
2360
2478
|
const { resolveWorkspacePath } = await import('./projects/workspace.js');
|
|
@@ -2363,6 +2481,7 @@ export async function runRepl(opts) {
|
|
|
2363
2481
|
return;
|
|
2364
2482
|
await offerContinueAtStartup({
|
|
2365
2483
|
workspaceDir: ws,
|
|
2484
|
+
cwd: process.cwd(),
|
|
2366
2485
|
sessionId: session.id,
|
|
2367
2486
|
sapAlias: session.sap_system ?? undefined,
|
|
2368
2487
|
choose: inkStartupChooser(),
|
|
@@ -2663,40 +2782,40 @@ export async function runRepl(opts) {
|
|
|
2663
2782
|
registerReadline(rl, (v) => {
|
|
2664
2783
|
suppressRlClose = v;
|
|
2665
2784
|
}, { 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
|
-
});
|
|
2785
|
+
// 2026-10-08: one-time offer to retire the old `workspace_folder` setting
|
|
2786
|
+
// (files now save in the folder you start in). Best-effort, never blocks.
|
|
2787
|
+
if (!isHeadless()) {
|
|
2788
|
+
try {
|
|
2789
|
+
const { maybeOfferWorkspaceRetire } = await import('./projects/workspace-retire.js');
|
|
2790
|
+
const { inkAwareConfirm } = await import('./agent/loop.js');
|
|
2791
|
+
await maybeOfferWorkspaceRetire({
|
|
2792
|
+
confirm: async (q) => (await inkAwareConfirm(q, { default: 'y' })) === 'y',
|
|
2793
|
+
log: (line) => console.log(chalk.dim(line)),
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
catch { /* best-effort */ }
|
|
2797
|
+
}
|
|
2683
2798
|
// 2026-06-15: one-time output-namespace migration. The customer-output
|
|
2684
2799
|
// root was renamed from the dead-brand `.abapforge/` to `.cspeach/`.
|
|
2685
2800
|
// Workspaces that still have a top-level `.abapforge/` get offered a
|
|
2686
2801
|
// one-time merge-move into `.cspeach/`. The dual-read resolver keeps old
|
|
2687
2802
|
// projects working if the user declines; "never" suppresses future offers.
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2803
|
+
// 2026-10-08: solo folders only — a team folder must not merge into the
|
|
2804
|
+
// shared team .cspeach, and a home start must not target the CLI home.
|
|
2805
|
+
if (shouldOfferAbapforgeMigration(process.cwd())) {
|
|
2806
|
+
await maybeOfferAbapforgeMigration(process.cwd(), {
|
|
2807
|
+
prompt: async ({ legacyDir, newDir }) => withInquirer(() => select({
|
|
2808
|
+
message: `Found legacy CSPeach outputs in ${legacyDir}. The output folder is now '.cspeach/'. Move them?`,
|
|
2809
|
+
default: 'move',
|
|
2810
|
+
choices: [
|
|
2811
|
+
{ value: 'move', name: `Move everything into ${newDir} (recommended)` },
|
|
2812
|
+
{ value: 'skip', name: `Not now — ask me again next time` },
|
|
2813
|
+
{ value: 'never', name: `Leave it and never ask again` },
|
|
2814
|
+
],
|
|
2815
|
+
})),
|
|
2816
|
+
log: (line) => console.log(chalk.dim(line)),
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2700
2819
|
// 2026-10-04 (spec §3.5) — "Continue '<plan>'?" (classic: numbered
|
|
2701
2820
|
// choices, Enter = the first). Continue queues the resume for the loop.
|
|
2702
2821
|
if (!isHeadless()) {
|
|
@@ -2708,6 +2827,7 @@ export async function runRepl(opts) {
|
|
|
2708
2827
|
if (ws) {
|
|
2709
2828
|
await offerContinueAtStartup({
|
|
2710
2829
|
workspaceDir: ws,
|
|
2830
|
+
cwd: process.cwd(),
|
|
2711
2831
|
sessionId: session.id,
|
|
2712
2832
|
sapAlias: session.sap_system ?? undefined,
|
|
2713
2833
|
choose: classicStartupChooser(inkAwarePrompt, (...l) => l.forEach((x) => console.log(x))),
|
|
@@ -2733,19 +2853,26 @@ export async function runRepl(opts) {
|
|
|
2733
2853
|
// consent was the picker pick, no Enter required. Cleared
|
|
2734
2854
|
// immediately so a stale value never re-fires.
|
|
2735
2855
|
classicPlanHop = false; // a typed line is never an in-run hop
|
|
2736
|
-
const
|
|
2737
|
-
if (
|
|
2738
|
-
|
|
2856
|
+
const classicQueued = pendingDispatchAccessor.take();
|
|
2857
|
+
if (classicQueued) {
|
|
2858
|
+
const dispatchedCmd = classicQueued.cmd;
|
|
2739
2859
|
// No context clear here: beginPlanPhaseContext (plan-boundary.ts,
|
|
2740
2860
|
// called in the plan-resume prepare branch below) is the single owner
|
|
2741
2861
|
// of the phase-boundary compaction — it runs for typed AND dispatched resumes, and
|
|
2742
2862
|
// only AFTER preparePlanResume succeeds, so a failed prepare never
|
|
2743
2863
|
// destroys the session context.
|
|
2744
2864
|
// Task 13 — no echo for a plan hop: the chain already said "Next: <title>".
|
|
2865
|
+
// Audit M-2 — the Ink mirror: a command CSPeach queued is its own step
|
|
2866
|
+
// (its line, or nothing when its source already said it), never "Auto-routed:".
|
|
2745
2867
|
{
|
|
2746
|
-
const
|
|
2747
|
-
if (
|
|
2748
|
-
|
|
2868
|
+
const view = queuedStepView(classicQueued.origin);
|
|
2869
|
+
if (view.echo) {
|
|
2870
|
+
const n = autoRoutedNotice(dispatchedCmd);
|
|
2871
|
+
if (n)
|
|
2872
|
+
console.log(formatInfo(n).trimEnd());
|
|
2873
|
+
}
|
|
2874
|
+
else if (view.line)
|
|
2875
|
+
console.log(view.line);
|
|
2749
2876
|
classicPlanHop = planHops.take(dispatchedCmd);
|
|
2750
2877
|
}
|
|
2751
2878
|
line = dispatchedCmd;
|
|
@@ -3041,6 +3168,7 @@ export async function runRepl(opts) {
|
|
|
3041
3168
|
session.awaitingSkillAnswer = false;
|
|
3042
3169
|
session.todos = []; // P2 — /new clears the plan (spec §4)
|
|
3043
3170
|
clearAppFlow(session); // Task 7 — /new ends an /abap-app flow
|
|
3171
|
+
clearJourneyFlow(session); // Task 3 — and a clean-core journey
|
|
3044
3172
|
console.log(formatInfo('Fresh start · the next prompt picks a skill again').trimEnd());
|
|
3045
3173
|
continue;
|
|
3046
3174
|
}
|
|
@@ -3051,6 +3179,12 @@ export async function runRepl(opts) {
|
|
|
3051
3179
|
console.log(chalk.cyan(msg));
|
|
3052
3180
|
continue;
|
|
3053
3181
|
}
|
|
3182
|
+
// /rules — the team project's decision rules (step 2, Task 10, D7); cards are typed lines here.
|
|
3183
|
+
if (trimmed === '/rules' || trimmed.startsWith('/rules ')) {
|
|
3184
|
+
const { runRulesSlash } = await import('./repl/rules-menu.js');
|
|
3185
|
+
await runRulesSlash(trimmed, { emit: (line) => console.log(line), headless: isHeadless(), signal: new AbortController().signal });
|
|
3186
|
+
continue;
|
|
3187
|
+
}
|
|
3054
3188
|
// Task 3 (ux-wave3) — classic-mode /rewind: numbered inquirer list + confirm
|
|
3055
3189
|
// + gated restore. Reuses the identical candidate collector and executor as
|
|
3056
3190
|
// the Ink panel (executeRestore); only the surface differs.
|
|
@@ -3171,7 +3305,8 @@ export async function runRepl(opts) {
|
|
|
3171
3305
|
// Pressing Enter twice used to clear, but users expected Enter
|
|
3172
3306
|
// to be a no-op when there's nothing to submit, so the
|
|
3173
3307
|
// double-Enter cancel was confusing.
|
|
3174
|
-
|
|
3308
|
+
// A bare /run falls through to its usage line below, like /help (M-1).
|
|
3309
|
+
if (resolvedTrimmed.startsWith('/') && !/^\/run$/i.test(resolvedTrimmed)) {
|
|
3175
3310
|
const splitIdx = resolvedTrimmed.search(/\s/);
|
|
3176
3311
|
const bodyAfter = splitIdx === -1 ? '' : resolvedTrimmed.slice(splitIdx).trim();
|
|
3177
3312
|
if (bodyAfter.length === 0) {
|
|
@@ -3214,6 +3349,23 @@ export async function runRepl(opts) {
|
|
|
3214
3349
|
resolvedTrimmed = expansion.text;
|
|
3215
3350
|
}
|
|
3216
3351
|
}
|
|
3352
|
+
// Guided runs — the same front door as the Ink path: the /run verb
|
|
3353
|
+
// (rewritten to its alias), then today's trigger. The trigger still
|
|
3354
|
+
// decides (spec §4 step 2); the dispatch below is unchanged.
|
|
3355
|
+
const door = await detectRunStart(resolvedTrimmed, {
|
|
3356
|
+
cwd: process.cwd(), session, resume: true,
|
|
3357
|
+
// Shadow (spec §4 step 2): same as the Ink path. Not awaited; counts only.
|
|
3358
|
+
onFreeTextStart: (text) => void shadowStart(text, 'run-clean-core', {
|
|
3359
|
+
classify: (p, c) => classifyPrompt(p, c, { shadowRuns: true }),
|
|
3360
|
+
context: { sap_system: selectedAlias, last_skill: session.last_skill ?? null, last_object: session.last_object ?? null },
|
|
3361
|
+
llmMode: cfg.llm?.mode,
|
|
3362
|
+
}),
|
|
3363
|
+
});
|
|
3364
|
+
if (door.kind === 'line') {
|
|
3365
|
+
console.log(door.line);
|
|
3366
|
+
continue;
|
|
3367
|
+
}
|
|
3368
|
+
resolvedTrimmed = door.input;
|
|
3217
3369
|
const parsed = parseShortcut(resolvedTrimmed);
|
|
3218
3370
|
// D35 (2026-06-11) — explicit slash naming a skill that isn't in the
|
|
3219
3371
|
// catalog: print the notice ("did you mean" / builtin-with-args) and
|
|
@@ -3223,6 +3375,29 @@ export async function runRepl(opts) {
|
|
|
3223
3375
|
if (rejectUnknownSkill(parsed, (m) => console.log(chalk.yellow(`\n${m}\n`)))) {
|
|
3224
3376
|
continue;
|
|
3225
3377
|
}
|
|
3378
|
+
// Guided clean-core journey (Task 3) — classic mirror of the Ink entry.
|
|
3379
|
+
// Cards become one typed line here (no card listener); a queued step is
|
|
3380
|
+
// picked up by the loop-top dispatch consumer.
|
|
3381
|
+
if (door.kind === 'run') {
|
|
3382
|
+
const { runJourneyEntry } = await import('./repl/journey-entry.js');
|
|
3383
|
+
const journeyAbort = new AbortController();
|
|
3384
|
+
const onJourneySigint = () => { journeyAbort.abort(); };
|
|
3385
|
+
turnInProgress = true;
|
|
3386
|
+
rl.on('SIGINT', onJourneySigint);
|
|
3387
|
+
try {
|
|
3388
|
+
await runJourneyEntry(door.trigger, {
|
|
3389
|
+
session, cwd: process.cwd(), adt, sapAlias: selectedAlias, standalone, headless: isHeadless(),
|
|
3390
|
+
emit: (line) => console.log(line),
|
|
3391
|
+
queueNext: (cmd, origin) => pendingDispatchAccessor.set(cmd, origin),
|
|
3392
|
+
signal: journeyAbort.signal,
|
|
3393
|
+
});
|
|
3394
|
+
}
|
|
3395
|
+
finally {
|
|
3396
|
+
rl.removeListener('SIGINT', onJourneySigint);
|
|
3397
|
+
turnInProgress = false;
|
|
3398
|
+
}
|
|
3399
|
+
continue;
|
|
3400
|
+
}
|
|
3226
3401
|
// Record the prose body (post-shortcut stripping) for /reroute. Using
|
|
3227
3402
|
// `parsed.prompt` rather than `trimmed` means:
|
|
3228
3403
|
// - plain prose: `parsed.prompt === trimmed`, stored as-is
|
|
@@ -3282,6 +3457,8 @@ export async function runRepl(opts) {
|
|
|
3282
3457
|
throw err;
|
|
3283
3458
|
c = fallback;
|
|
3284
3459
|
}
|
|
3460
|
+
// Defensive (spec §4 step 2): a run label never dispatches in this release.
|
|
3461
|
+
c = guardRunLabel(c);
|
|
3285
3462
|
if (c.screenshot)
|
|
3286
3463
|
console.log(formatInfo(`Screenshot: ${c.screenshot}`).trimEnd());
|
|
3287
3464
|
// Fix 10 (2026-09-26): routing failed mid-skill → stay in the
|
|
@@ -94,7 +94,7 @@ function transportKindOf(err) {
|
|
|
94
94
|
return 'timeout';
|
|
95
95
|
return 'network';
|
|
96
96
|
}
|
|
97
|
-
async function tryFetchOnce(proxyUrl, key, prompt, context, images = [], signal) {
|
|
97
|
+
async function tryFetchOnce(proxyUrl, key, prompt, context, images = [], signal, shadowRuns = false) {
|
|
98
98
|
// Review I2 — a cancel that landed before this call (e.g. while prompt
|
|
99
99
|
// images were read) never fires the 'abort' event: honour it here.
|
|
100
100
|
if (signal?.aborted)
|
|
@@ -111,7 +111,10 @@ async function tryFetchOnce(proxyUrl, key, prompt, context, images = [], signal)
|
|
|
111
111
|
'Authorization': `Bearer ${key}`,
|
|
112
112
|
'Content-Type': 'application/json',
|
|
113
113
|
},
|
|
114
|
-
|
|
114
|
+
// Guided runs (spec §4 step 2): a shadow call asks the proxy for the
|
|
115
|
+
// shadow-only router text; a live call's body is unchanged.
|
|
116
|
+
body: JSON.stringify(shadowRuns ? { prompt, context, shadow: 'runs' }
|
|
117
|
+
: images.length > 0 ? { prompt, context, images } : { prompt, context }),
|
|
115
118
|
signal: ctrl.signal,
|
|
116
119
|
});
|
|
117
120
|
}
|
|
@@ -188,7 +191,8 @@ export async function classifyPrompt(prompt, context, opts = {}) {
|
|
|
188
191
|
throw new Error('no_api_key');
|
|
189
192
|
if (opts.signal?.aborted)
|
|
190
193
|
throw routingCancelled();
|
|
191
|
-
|
|
194
|
+
// A shadow call never sends images (spec §4 step 2): text only.
|
|
195
|
+
const images = opts.shadowRuns ? [] : await promptImages(prompt, opts.cwd ?? process.cwd());
|
|
192
196
|
if (opts.signal?.aborted)
|
|
193
197
|
throw routingCancelled();
|
|
194
198
|
let r;
|
|
@@ -201,7 +205,7 @@ export async function classifyPrompt(prompt, context, opts = {}) {
|
|
|
201
205
|
// they bubble through as before.
|
|
202
206
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
203
207
|
try {
|
|
204
|
-
r = await tryFetchOnce(cfg.proxy_url, key, prompt, context, images, opts.signal);
|
|
208
|
+
r = await tryFetchOnce(cfg.proxy_url, key, prompt, context, images, opts.signal, opts.shadowRuns === true);
|
|
205
209
|
break;
|
|
206
210
|
}
|
|
207
211
|
catch (err) {
|