@cspeach/cli 1.1.50 → 1.1.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/chat-prompt.js +4 -1
- package/dist/agent/conversation-prompt.js +30 -13
- package/dist/agent/inline-skill-block.js +341 -0
- package/dist/agent/loop.js +314 -44
- package/dist/agent/provider-shape.js +8 -4
- package/dist/agent/providers/ai-hub-provider.js +2 -2
- package/dist/agent/providers/azure-provider.js +2 -2
- package/dist/agent/providers/byok-provider.js +2 -2
- package/dist/agent/providers/local-provider.js +2 -2
- package/dist/agent/repair-partial.js +12 -0
- package/dist/agent/skill-block-key.js +134 -0
- package/dist/agent/steering-queue.js +50 -14
- package/dist/agent/system-prompt.js +22 -0
- package/dist/agent/tool-dispatch.js +19 -7
- package/dist/agent/turn-watchdog.js +1 -1
- package/dist/approvals/advisory-render.js +6 -3
- package/dist/approvals/approval-prompt.js +9 -4
- package/dist/approvals/op-labels.js +14 -0
- package/dist/approvals/plan-grant.js +8 -0
- package/dist/approvals/render.js +19 -8
- package/dist/approvals/risk-floor.js +32 -4
- package/dist/approvals/visible-changes.js +50 -0
- package/dist/cli.js +11 -3
- package/dist/commands/compact.js +12 -1
- package/dist/commands/config-set.js +14 -1
- package/dist/commands/config-show.js +16 -4
- package/dist/commands/export-audit.js +26 -3
- package/dist/commands/export-conversation.js +346 -0
- package/dist/commands/finish-check-guard.js +124 -0
- package/dist/commands/help.js +3 -0
- package/dist/commands/plan-boundary.js +5 -0
- package/dist/commands/plan-continue.js +11 -0
- package/dist/commands/plan-reground.js +27 -4
- package/dist/commands/plan-resume.js +111 -1
- package/dist/commands/plan-startup.js +5 -1
- package/dist/config/loader.js +5 -1
- package/dist/cost/cost-log.js +6 -0
- package/dist/cost/system-block.js +6 -0
- package/dist/docx/doc-meta.js +9 -1
- package/dist/docx/markdown-to-docx.js +14 -1
- package/dist/lane/app-chain.js +24 -1
- package/dist/models/server-config.js +1 -1
- package/dist/projects/document-save.js +65 -8
- package/dist/projects/document-skills.js +32 -1
- package/dist/projects/save-command.js +5 -1
- package/dist/projects/yes-no.js +5 -1
- package/dist/renderer/change-card.js +3 -1
- package/dist/renderer/steering-echo.js +53 -8
- package/dist/renderer/tool-labels.js +3 -1
- package/dist/repl/builtin-commands.js +1 -1
- package/dist/repl/chat-lane.js +5 -2
- package/dist/repl/inline-mode.js +51 -0
- package/dist/repl/leftover.js +90 -0
- package/dist/repl/reroute-turn.js +7 -1
- package/dist/repl/slash-completer.js +4 -0
- package/dist/repl/turn-stop.js +64 -0
- package/dist/repl.js +178 -32
- package/dist/router/classifier.js +1 -1
- package/dist/sap/reachability-state.js +7 -0
- package/dist/sap/unreachable.js +2 -2
- package/dist/session/cli-record.js +165 -0
- package/dist/session/fresh-start.js +4 -0
- package/dist/session/queued-step.js +3 -1
- package/dist/session/user-prompt.js +10 -1
- package/dist/skills/manifest-client.js +3 -2
- package/dist/skills/preamble.js +6 -4
- package/dist/skills/source-authed.js +3 -2
- package/dist/tools/approval.js +37 -7
- package/dist/tools/dispatch-skill.js +1 -1
- package/dist/tools/hand-off.js +49 -7
- package/dist/tools/lane/app-lane-resolve.js +9 -0
- package/dist/tools/method-only-change.js +241 -0
- package/dist/tools/rap/skeleton-resolve.js +4 -0
- package/dist/tools/sap-write.js +78 -3
- package/dist/tools/snapshot.js +7 -2
- package/dist/tools/subagent/agent_run.js +14 -0
- package/dist/tools/tool-loading.js +16 -8
- package/dist/tools/transport-fit-check.js +70 -0
- package/dist/tools/transport.js +17 -8
- package/dist/tools/write-pipeline.js +46 -5
- package/dist/ui/app.js +112 -13
- package/dist/ui/approval-modal.js +11 -6
- package/dist/ui/ascii-chars.js +21 -3
- package/dist/ui/card-slot.js +18 -1
- package/dist/ui/footer.js +128 -7
- package/dist/ui/interrupt.js +35 -1
- package/dist/ui/live-area.js +21 -4
- package/dist/ui/queue-box-emitter.js +14 -0
- package/dist/ui/rewind-panel.js +2 -1
- package/dist/ui/session-timeline.js +2 -1
- package/dist/ui/text-input.js +31 -2
- package/dist/ui/widgets/bar-chart.js +2 -1
- package/dist/ui/widgets/confirm-card.js +2 -1
- package/dist/ui/widgets/dep-graph.js +2 -1
- package/dist/ui/widgets/diff-viewer.js +2 -1
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/shortcuts-sheet.js +2 -2
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/util/connect-retry.js +65 -0
- package/package.json +1 -1
package/dist/repl.js
CHANGED
|
@@ -30,6 +30,7 @@ import { createDispatchSlot, queuedStepView } from './session/queued-step.js';
|
|
|
30
30
|
import { continueSkillFor } from './repl/continue-skill.js';
|
|
31
31
|
import { applyResumedSkill } from './router/resume-routing.js';
|
|
32
32
|
import { ACK_PHRASES, wantsContinuation, isConversationalAck, decideLane, turnStatusLabel, applyLaneTools, chatFallbackOrigin } from './repl/chat-lane.js';
|
|
33
|
+
import { inlineActive, inlineServed, isChatLaneProcess } from './repl/inline-mode.js';
|
|
33
34
|
import { CHAT_SKILL } from './agent/chat-prompt.js';
|
|
34
35
|
import { setAssistantStyle } from './agent/style-state.js';
|
|
35
36
|
import { setAssistantLevel } from './agent/level-state.js';
|
|
@@ -45,7 +46,7 @@ import { startSkillsCacheWarmup } from './skills/source-authed.js';
|
|
|
45
46
|
import { formatLoginBanner, signInFragment } from './ui/login-banner.js';
|
|
46
47
|
import { printStartupBanner, printKeytarFallbackBanner, keychainFallbackNotice } from './renderer/banners.js';
|
|
47
48
|
import { paint } from './renderer/theme.js';
|
|
48
|
-
import {
|
|
49
|
+
import { classifyMidTurnLine, midTurnSlashHint } from './renderer/steering-echo.js';
|
|
49
50
|
import { runAddSapWizard } from './sap/onboarding.js';
|
|
50
51
|
import { getConnection } from './sap/connection-manager.js';
|
|
51
52
|
import { unreachableCode, unreachableMessage } from './sap/unreachable.js';
|
|
@@ -154,6 +155,9 @@ import { getEffectiveWriteMode, runModeCommand } from './repl/mode-cycle.js';
|
|
|
154
155
|
import { maybeExpandNumberSelection, lastAssistantText } from './repl/numbered-menu.js';
|
|
155
156
|
import { attachEarlyLineBuffer, noopEarlyLineBuffer } from './repl/early-line-buffer.js';
|
|
156
157
|
import { createRestoreGuard } from './repl/restore-guard.js';
|
|
158
|
+
import { createLeftoverRunner, takeUnsentReport, parseSubmitted } from './repl/leftover.js';
|
|
159
|
+
import { createTurnStop } from './repl/turn-stop.js';
|
|
160
|
+
import { requestQueueToBox } from './ui/queue-box-emitter.js';
|
|
157
161
|
import { gracefulExit } from './lib/graceful-exit.js';
|
|
158
162
|
import { endSapSessionAfterTurn } from './sap/session-end.js';
|
|
159
163
|
import { snapshotTurnStart, pushPostTurnStatus, seedFooterSpend } from './repl/post-turn-status.js';
|
|
@@ -208,6 +212,17 @@ import './tools/subagent/schedule_draft_create.js';
|
|
|
208
212
|
import './tools/subagent/agent_run.js';
|
|
209
213
|
import './tools/subagent/read_agent.js';
|
|
210
214
|
import { folderTag, currentFolderKey } from './session/project-match.js';
|
|
215
|
+
import { bindCliCardSession } from './session/cli-record.js';
|
|
216
|
+
/** /export docx conversation (2026-10-23) — the origin of a turn CSPeach started itself. */
|
|
217
|
+
async function cliTurnOrigin(planResume, queuedCmd) {
|
|
218
|
+
if (planResume) {
|
|
219
|
+
const { planStepOriginLine } = await import('./commands/plan-continue.js');
|
|
220
|
+
return { userMessageOrigin: { kind: 'plan-step', line: planStepOriginLine(planResume) } };
|
|
221
|
+
}
|
|
222
|
+
if (queuedCmd)
|
|
223
|
+
return { userMessageOrigin: { kind: 'queued', line: `CSPeach ran the next step: ${queuedCmd.trim()}` } };
|
|
224
|
+
return {};
|
|
225
|
+
}
|
|
211
226
|
const HISTORY_SIZE = 500;
|
|
212
227
|
// Persistence (read + dedup-guarded append) lives in ./repl/history.ts —
|
|
213
228
|
// the single writer shared with the Ink Footer (Task 8, ux-wave1).
|
|
@@ -793,6 +808,8 @@ export async function runRepl(opts) {
|
|
|
793
808
|
// served config), marks it 'predicted', saves, then stays pinned. Every
|
|
794
809
|
// resume entry (picker, --resume <id>) lands here via session/resume.ts.
|
|
795
810
|
const session = opts?.resumedSession ?? newSession(makeSessionId(), selectedAlias, null, resolveModelRole('session_default', cfg), folderTag(process.cwd()));
|
|
811
|
+
// /export docx conversation — the plan startup, lane and journey cards are recorded on this session.
|
|
812
|
+
bindCliCardSession(session);
|
|
796
813
|
// Round 6 item 4c — a resumed session keeps its skill for the first plain prompt.
|
|
797
814
|
let resumeKeepsSkill = Boolean(opts?.resumedSession?.skill);
|
|
798
815
|
// Round 7 item 8c — the footer shows a resumed session's spend at once.
|
|
@@ -1066,13 +1083,47 @@ export async function runRepl(opts) {
|
|
|
1066
1083
|
: pendingCancelLine(phase, exiting, queuedWorkLabel(pendingDispatchAccessor.get()));
|
|
1067
1084
|
chunkEmitter.emit('chunk', '\n' + formatInfo(line));
|
|
1068
1085
|
};
|
|
1086
|
+
// LT-15 Feature 4 — messages still waiting when a dispatch ends: run them
|
|
1087
|
+
// as the next turn (normal end), put them back in the box (Ctrl+C, an
|
|
1088
|
+
// error), let a queued hop read them first, or report them at exit.
|
|
1089
|
+
// Flags are per dispatch: set below, read + reset by settle() in
|
|
1090
|
+
// handleSubmit's finally and at a restore's end (plan review I4).
|
|
1091
|
+
const leftover = createLeftoverRunner({
|
|
1092
|
+
isBusy: () => turnStatusEmitter.isBusy(),
|
|
1093
|
+
isDone: () => done,
|
|
1094
|
+
// F4 — prose: never parsed as a slash command or @-shortcut; it goes
|
|
1095
|
+
// through routing like a fresh prompt (review M5), as the person's turn.
|
|
1096
|
+
submit: (text) => { void handleSubmit(text, { prose: true }); },
|
|
1097
|
+
toBox: () => { requestQueueToBox(); },
|
|
1098
|
+
print: (line) => chunkEmitter.emit('chunk', '\n' + line),
|
|
1099
|
+
schedule: (fn) => { setImmediate(fn); },
|
|
1100
|
+
});
|
|
1101
|
+
// LT-15 Task 5b — how this dispatch was stopped (Esc brake or Ctrl+C).
|
|
1102
|
+
// Per dispatch: read by autoDispatch (nothing queued starts after a stop)
|
|
1103
|
+
// and by leftover.settle, reset right after that settle and at a
|
|
1104
|
+
// restore's end (plan review I4). The brake reuses Ctrl+C's own abort and
|
|
1105
|
+
// pending cancel and never touches interruptCount (review C1).
|
|
1106
|
+
const turnStop = createTurnStop({
|
|
1107
|
+
turnActive: () => currentTurnAbort !== null,
|
|
1108
|
+
busy: () => turnStatusEmitter.isBusy(),
|
|
1109
|
+
restoreActive: () => restoreGuard.active,
|
|
1110
|
+
phase: () => cancelPhase(),
|
|
1111
|
+
canChain: () => canChainNow(),
|
|
1112
|
+
exitRequested: () => exitAfterTurn,
|
|
1113
|
+
abortTurn: () => { currentTurnAbort?.abort(); },
|
|
1114
|
+
cancelPending: () => { announcePendingCancel(false); },
|
|
1115
|
+
print: (line) => chunkEmitter.emit('chunk', '\n' + formatInfo(line)),
|
|
1116
|
+
});
|
|
1069
1117
|
/** Auto-dispatch of the next plan phase, unless a post-turn cancel stopped it. */
|
|
1070
1118
|
// Task 13 fix round — `planHop` travels WITH the command into handleSubmit,
|
|
1071
1119
|
// so a refused submit can never leave a stale hop mark behind.
|
|
1072
1120
|
const autoDispatch = (cmd, planHop = false, queued = null) => {
|
|
1073
1121
|
// Round 4 item 9b — the exit is already under way: start nothing new.
|
|
1074
1122
|
// Final review B-I2 — `/exit` typed mid-turn (exitAfterTurn) stops it too.
|
|
1075
|
-
|
|
1123
|
+
// LT-15 Task 5b (D4, 5a re-review) — nothing queued starts after a stop
|
|
1124
|
+
// of this dispatch (Esc or Ctrl+C); the D11 chat fallback still re-runs
|
|
1125
|
+
// the person's own message (review M6).
|
|
1126
|
+
const decision = autoDispatchDecision({ done, exitRequested: exitAfterTurn, cancelPending: pendingCancel.pending, stopped: turnStop.stopped, chatFallback: queued?.by === 'chat_fallback' }, cmd);
|
|
1076
1127
|
if (decision.kind === 'skip')
|
|
1077
1128
|
return;
|
|
1078
1129
|
if (decision.kind === 'not-started') {
|
|
@@ -1085,6 +1136,8 @@ export async function runRepl(opts) {
|
|
|
1085
1136
|
const notice = queued ? null : autoRoutedNotice(cmd);
|
|
1086
1137
|
if (notice)
|
|
1087
1138
|
chunkEmitter.emit('chunk', formatInfo(notice));
|
|
1139
|
+
// LT-15 D3 — the hop goes first; a waiting message is read at its round 0.
|
|
1140
|
+
leftover.noteDispatchScheduled();
|
|
1088
1141
|
setImmediate(() => { void handleSubmit(cmd, { planHop, queued }); });
|
|
1089
1142
|
};
|
|
1090
1143
|
// BUG-2b — the actual dispatch. Wrapped by handleSubmit (below) which
|
|
@@ -1095,6 +1148,9 @@ export async function runRepl(opts) {
|
|
|
1095
1148
|
const trimmed = line.trim();
|
|
1096
1149
|
if (!trimmed)
|
|
1097
1150
|
return;
|
|
1151
|
+
// LT-15 F4 — a replayed waiting message is prose, also when the D11
|
|
1152
|
+
// chat fallback re-runs it (the flag rides on the fallback origin).
|
|
1153
|
+
const prose = submitOpts?.prose === true || submitOpts?.queued?.prose === true;
|
|
1098
1154
|
// Round 6 review I2 — the resumed skill is offered to the FIRST submit
|
|
1099
1155
|
// only (slash commands included), then cleared.
|
|
1100
1156
|
const resumedSkill = resumeKeepsSkill ? session.skill ?? null : null;
|
|
@@ -1103,7 +1159,10 @@ export async function runRepl(opts) {
|
|
|
1103
1159
|
// the Footer has already persisted this submission (flattened), so
|
|
1104
1160
|
// the consecutive-dup guard makes this a no-op instead of the old
|
|
1105
1161
|
// double append.
|
|
1106
|
-
|
|
1162
|
+
// LT-15 F4 — a replayed waiting message is already in history (the
|
|
1163
|
+
// Footer stored each one when it was typed).
|
|
1164
|
+
if (!prose)
|
|
1165
|
+
appendHistoryEntry(trimmed);
|
|
1107
1166
|
// Phase A** (2026-05-16) — echo the user's prompt into Body so it
|
|
1108
1167
|
// appears in the conversation history. Plus a dim divider above
|
|
1109
1168
|
// the echo so consecutive turns have a clear visual break (closes
|
|
@@ -1157,16 +1216,18 @@ export async function runRepl(opts) {
|
|
|
1157
1216
|
// session audit record (who did what, where, under which approval, at
|
|
1158
1217
|
// what cost) to a markdown file in cwd and print the path.
|
|
1159
1218
|
if (trimmed === '/export' || trimmed.startsWith('/export ')) {
|
|
1160
|
-
const { parseExportArgs, runExportAuditCommand } = await import('./commands/export-audit.js');
|
|
1161
|
-
const { sub, filename } = parseExportArgs(trimmed.slice('/export'.length));
|
|
1219
|
+
const { parseExportArgs, runExportAuditCommand, EXPORT_REPL_USAGE } = await import('./commands/export-audit.js');
|
|
1220
|
+
const { sub, filename, docxArgs } = parseExportArgs(trimmed.slice('/export'.length));
|
|
1162
1221
|
// Word export (2026-10-19): /export docx <file>.md, same body as `cspeach export`.
|
|
1222
|
+
// 2026-10-23: no file → the last answer; `conversation` → the whole conversation.
|
|
1223
|
+
// The Word package loads only here: /export audit never needs it.
|
|
1163
1224
|
if (sub === 'docx') {
|
|
1164
|
-
const {
|
|
1165
|
-
await
|
|
1225
|
+
const { runExportDocxRepl } = await import('./commands/export-conversation.js');
|
|
1226
|
+
await runExportDocxRepl({ args: docxArgs ?? '', session, ...(signedInAs ? { person: signedInAs } : {}), emit: (l) => chunkEmitter.emit('chunk', l + '\n') });
|
|
1166
1227
|
return;
|
|
1167
1228
|
}
|
|
1168
1229
|
if (sub !== 'audit') {
|
|
1169
|
-
chunkEmitter.emit('chunk', chalk.dim(
|
|
1230
|
+
chunkEmitter.emit('chunk', chalk.dim(`\n${EXPORT_REPL_USAGE}\n`));
|
|
1170
1231
|
return;
|
|
1171
1232
|
}
|
|
1172
1233
|
await runExportAuditCommand({
|
|
@@ -1347,7 +1408,7 @@ export async function runRepl(opts) {
|
|
|
1347
1408
|
provider,
|
|
1348
1409
|
userMessage: rr.prompt,
|
|
1349
1410
|
skill: rr.skill,
|
|
1350
|
-
ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: inkPreview, chunkEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor },
|
|
1411
|
+
ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: inkPreview, chunkEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor, inlineSkills: inlineActive({ skills: cfg.assistant.skills, lane: laneSetting, interactive: !!process.stdin.isTTY, served: inlineServed(cfg.llm?.mode) }), chatLaneProcess: isChatLaneProcess(laneSetting, !!process.stdin.isTTY), turnBy: lastTurnBy },
|
|
1351
1412
|
chunkEmitter,
|
|
1352
1413
|
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
1353
1414
|
autoCompact: true, // Task 19 — top-level REPL turn: auto-compact gate at turn end
|
|
@@ -1356,6 +1417,11 @@ export async function runRepl(opts) {
|
|
|
1356
1417
|
session,
|
|
1357
1418
|
print: (line) => chunkEmitter.emit('chunk', line + '\n'),
|
|
1358
1419
|
signal: rerouteAbort.signal,
|
|
1420
|
+
// LT-15 review I5 — an error end puts waiting text back in the box.
|
|
1421
|
+
onError: () => { if (!rerouteAbort.signal.aborted)
|
|
1422
|
+
leftover.noteError(); },
|
|
1423
|
+
// LT-15 Task 5b — an Esc brake prints one dim line, not the block.
|
|
1424
|
+
stopLine: () => (turnStop.brakedOnly ? formatInfo(turnStop.endLine()).trimEnd() : null),
|
|
1359
1425
|
onFinally: () => {
|
|
1360
1426
|
turnStatusEmitter.stop();
|
|
1361
1427
|
currentTurnAbort = null;
|
|
@@ -1378,6 +1444,9 @@ export async function runRepl(opts) {
|
|
|
1378
1444
|
todoEmitter.emit('update', []);
|
|
1379
1445
|
clearAppFlow(session); // Task 7 — /new ends an /abap-app flow
|
|
1380
1446
|
clearJourneyFlow(session); // Task 3 — and a clean-core journey
|
|
1447
|
+
// LT-15 Task 4 — a message still waiting belongs to the old
|
|
1448
|
+
// conversation: back into the box (or reported), never carried over.
|
|
1449
|
+
leftover.toBoxOrReport();
|
|
1381
1450
|
// 2026-10-22 — and the conversation history: the next prompt sends none of it.
|
|
1382
1451
|
startFreshConversation(session);
|
|
1383
1452
|
idleKeepalive.disarm(); // no idle cache ping for the old history
|
|
@@ -1450,7 +1519,9 @@ export async function runRepl(opts) {
|
|
|
1450
1519
|
// an item in a 2+-item list in the LAST assistant message.
|
|
1451
1520
|
let effectiveInput = trimmed;
|
|
1452
1521
|
{
|
|
1453
|
-
|
|
1522
|
+
// LT-15 Task 4 fix round 1 — never for a replayed waiting message: a
|
|
1523
|
+
// "2" typed mid-turn did not answer the menu of the turn that just ended.
|
|
1524
|
+
const expansion = prose ? null : maybeExpandNumberSelection(trimmed, lastAssistantText(session.messages));
|
|
1454
1525
|
if (expansion) {
|
|
1455
1526
|
chunkEmitter.emit('chunk', chalk.dim(`${expansion.echo}\n`));
|
|
1456
1527
|
effectiveInput = expansion.text;
|
|
@@ -1475,7 +1546,9 @@ export async function runRepl(opts) {
|
|
|
1475
1546
|
return;
|
|
1476
1547
|
}
|
|
1477
1548
|
effectiveInput = door.input;
|
|
1478
|
-
|
|
1549
|
+
// LT-15 F4 — a replayed waiting message is prose: never a slash command
|
|
1550
|
+
// or an @-shortcut (`/sap/bc/adt/x is slow` reaches the model as text).
|
|
1551
|
+
const parsed = parseSubmitted(effectiveInput, prose);
|
|
1479
1552
|
// D35 (2026-06-11) — explicit slash naming a skill that isn't in the
|
|
1480
1553
|
// catalog: print the notice ("did you mean" / builtin-with-args) and
|
|
1481
1554
|
// end the turn. Slash input must NEVER fall through to the classifier
|
|
@@ -1596,7 +1669,7 @@ export async function runRepl(opts) {
|
|
|
1596
1669
|
sap_system: selectedAlias,
|
|
1597
1670
|
last_skill: session.last_skill ?? null,
|
|
1598
1671
|
last_object: extractedObject ?? session.last_object ?? null,
|
|
1599
|
-
}, { signal: routingCtl.signal });
|
|
1672
|
+
}, { signal: routingCtl.signal, prose });
|
|
1600
1673
|
}
|
|
1601
1674
|
catch (err) {
|
|
1602
1675
|
if (!routingCtl.signal.aborted) {
|
|
@@ -2041,7 +2114,8 @@ export async function runRepl(opts) {
|
|
|
2041
2114
|
provider,
|
|
2042
2115
|
userMessage: prompt,
|
|
2043
2116
|
skill: skill,
|
|
2044
|
-
|
|
2117
|
+
// Stage 1A — inline is decided here, at every submit, from today's inputs (never cached).
|
|
2118
|
+
ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: inkPreview, chunkEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor, inlineSkills: inlineActive({ skills: cfg.assistant.skills, lane: laneSetting, interactive: !!process.stdin.isTTY, served: inlineServed(cfg.llm?.mode) }), chatLaneProcess: isChatLaneProcess(laneSetting, !!process.stdin.isTTY), turnBy: lastTurnBy },
|
|
2045
2119
|
chunkEmitter,
|
|
2046
2120
|
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
2047
2121
|
explicitSlash: parsed.skill !== undefined, // Task 7 R11 — typed or queued slash command
|
|
@@ -2051,6 +2125,8 @@ export async function runRepl(opts) {
|
|
|
2051
2125
|
// turns (their post-turn persist indexes messages from turn start).
|
|
2052
2126
|
autoCompact: planResume === null && planContinue === null,
|
|
2053
2127
|
delegateSkill: planPhaseDelegate,
|
|
2128
|
+
// /export docx conversation — a plan step or a queued step is CSPeach's words, not the person's.
|
|
2129
|
+
...(await cliTurnOrigin(planResume, submitOpts?.queued ? line : null)),
|
|
2054
2130
|
// A4 — undefined on every non-plan turn (and when tiering is off).
|
|
2055
2131
|
modelOverride: planModelOverride,
|
|
2056
2132
|
// Task 9 — a tier-down, never the session role (proxy must not enforce it).
|
|
@@ -2117,7 +2193,7 @@ export async function runRepl(opts) {
|
|
|
2117
2193
|
// never throw ChatUnavailableError again.
|
|
2118
2194
|
laneSetting = 'skills';
|
|
2119
2195
|
sapStateStore.update({ chatLane: false });
|
|
2120
|
-
pendingDispatchAccessor.set(trimmed, chatFallbackOrigin());
|
|
2196
|
+
pendingDispatchAccessor.set(trimmed, chatFallbackOrigin(prose));
|
|
2121
2197
|
}
|
|
2122
2198
|
else {
|
|
2123
2199
|
// 2026-09-24 — print the turn-error block INTO the conversation (it
|
|
@@ -2127,13 +2203,25 @@ export async function runRepl(opts) {
|
|
|
2127
2203
|
// "type continue" (the partial turn is already saved).
|
|
2128
2204
|
const { renderTurnError } = await import('./agent/turn-error-ux.js');
|
|
2129
2205
|
turnCancelledByUser = currentTurnAbort?.signal.aborted ?? false;
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2206
|
+
// LT-15 review I5 — an error end (not a stop): waiting text goes back
|
|
2207
|
+
// into the box, nothing runs by itself.
|
|
2208
|
+
if (!turnCancelledByUser)
|
|
2209
|
+
leftover.noteError();
|
|
2210
|
+
if (turnCancelledByUser && turnStop.brakedOnly) {
|
|
2211
|
+
// LT-15 Task 5b — an Esc brake: one dim line in place of the
|
|
2212
|
+
// "type continue" block. The loop already saved the turn as
|
|
2213
|
+
// interrupted, and a journey stays paused.
|
|
2214
|
+
chunkEmitter.emit('chunk', '\n' + formatInfo(turnStop.endLine()));
|
|
2215
|
+
}
|
|
2216
|
+
else {
|
|
2217
|
+
await renderTurnError({
|
|
2218
|
+
err,
|
|
2219
|
+
session,
|
|
2220
|
+
userInterrupted: turnCancelledByUser,
|
|
2221
|
+
inSession: true,
|
|
2222
|
+
print: (line) => chunkEmitter.emit('chunk', line + '\n'),
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2137
2225
|
}
|
|
2138
2226
|
}
|
|
2139
2227
|
finally {
|
|
@@ -2426,21 +2514,23 @@ export async function runRepl(opts) {
|
|
|
2426
2514
|
chunkEmitter.emit('chunk', midTurnSlashHint(kind.command));
|
|
2427
2515
|
return;
|
|
2428
2516
|
}
|
|
2429
|
-
|
|
2517
|
+
// LT-15 Feature 1 (D5) — no scrollback receipt; the App's queue row shows it.
|
|
2518
|
+
if (t.length > 0)
|
|
2430
2519
|
enqueueSteering(t);
|
|
2431
|
-
chunkEmitter.emit('chunk', steeringEchoLine(t));
|
|
2432
|
-
}
|
|
2433
2520
|
return;
|
|
2434
2521
|
}
|
|
2435
2522
|
idleKeepalive.disarm(); // Task 15 — a submit ends the idle period
|
|
2436
2523
|
turnStatusEmitter.reserve();
|
|
2437
2524
|
postTurn = false;
|
|
2438
2525
|
postTurnMayChain = false;
|
|
2526
|
+
// LT-15 review I5 — a dispatch that threw ended by an error.
|
|
2527
|
+
let threw = true;
|
|
2439
2528
|
try {
|
|
2440
2529
|
// Round 4 item 9b — the exit path waits (capped) for this to finish.
|
|
2441
2530
|
const run = dispatchSubmit(line, submitOpts);
|
|
2442
2531
|
dispatchTail = run;
|
|
2443
2532
|
await run;
|
|
2533
|
+
threw = false;
|
|
2444
2534
|
}
|
|
2445
2535
|
finally {
|
|
2446
2536
|
turnStatusEmitter.release();
|
|
@@ -2462,6 +2552,14 @@ export async function runRepl(opts) {
|
|
|
2462
2552
|
// the submit ended in an exit (merge: UI round-3 /exit mid-turn).
|
|
2463
2553
|
if (!done)
|
|
2464
2554
|
idleKeepalive.armAfterTurn(session);
|
|
2555
|
+
// LT-15 Feature 4 — after release(): messages still waiting run as the
|
|
2556
|
+
// next turn (on the next tick, drained there), go back into the box,
|
|
2557
|
+
// wait for a queued hop, or are left for the exit report.
|
|
2558
|
+
if (threw)
|
|
2559
|
+
leftover.noteError();
|
|
2560
|
+
leftover.settle({ exitRequested: exitAfterTurn, braked: turnStop.braked });
|
|
2561
|
+
// LT-15 Task 5b (I4) — the stop belongs to this dispatch only.
|
|
2562
|
+
turnStop.reset();
|
|
2465
2563
|
}
|
|
2466
2564
|
};
|
|
2467
2565
|
// Fix round 3 (item 5) — `/exit` / `/quit` typed while a turn runs:
|
|
@@ -2478,6 +2576,9 @@ export async function runRepl(opts) {
|
|
|
2478
2576
|
cancelAnnouncedPhase = null;
|
|
2479
2577
|
pendingCancel.clear();
|
|
2480
2578
|
interruptCount = 0;
|
|
2579
|
+
// LT-15 Feature 4 — a restore's end decides like a normal turn end.
|
|
2580
|
+
leftover.settle({ exitRequested: exitAfterTurn, braked: turnStop.braked });
|
|
2581
|
+
turnStop.reset();
|
|
2481
2582
|
},
|
|
2482
2583
|
exit: () => { done = true; if (resolveDone)
|
|
2483
2584
|
resolveDone(); },
|
|
@@ -2515,6 +2616,10 @@ export async function runRepl(opts) {
|
|
|
2515
2616
|
let idleInterruptAt = 0;
|
|
2516
2617
|
const onInterrupt = () => {
|
|
2517
2618
|
const now = Date.now();
|
|
2619
|
+
// LT-15 D1 — Ctrl+C during a dispatch: waiting messages go back into the
|
|
2620
|
+
// box at this dispatch's end; nothing runs by itself.
|
|
2621
|
+
if (turnStatusEmitter.isBusy())
|
|
2622
|
+
leftover.noteCtrlC();
|
|
2518
2623
|
const action = decideInterrupt({
|
|
2519
2624
|
turnActive: currentTurnAbort !== null,
|
|
2520
2625
|
// Review I3 — routing / prep in flight without a controller yet is
|
|
@@ -2528,18 +2633,30 @@ export async function runRepl(opts) {
|
|
|
2528
2633
|
});
|
|
2529
2634
|
if (action === 'abort-turn') {
|
|
2530
2635
|
interruptCount++;
|
|
2531
|
-
|
|
2636
|
+
turnStop.noteCtrlCStop(); // LT-15 Task 5b — nothing queued starts after it
|
|
2637
|
+
// After an Esc the stop line is already on screen: not repeated (C1).
|
|
2638
|
+
if (!turnStop.braked)
|
|
2639
|
+
chunkEmitter.emit('chunk', '\n' + formatInfo('Stopping the turn · saving the work done so far'));
|
|
2532
2640
|
currentTurnAbort.abort();
|
|
2641
|
+
return 'stopped'; // LT-15 D1 — App puts the waiting text back in the box now
|
|
2533
2642
|
}
|
|
2534
2643
|
else if (action === 'cancel-pending') {
|
|
2535
2644
|
interruptCount++;
|
|
2645
|
+
turnStop.noteCtrlCStop();
|
|
2536
2646
|
announcePendingCancel(false);
|
|
2647
|
+
return 'stopped';
|
|
2537
2648
|
}
|
|
2538
2649
|
else if (action === 'force-exit') {
|
|
2539
2650
|
// Second Ctrl+C during the same turn — give up and exit.
|
|
2540
2651
|
chunkEmitter.emit('chunk', '\n' + formatInfo('Exiting · second Ctrl+C'));
|
|
2541
2652
|
unmount();
|
|
2542
2653
|
process.stdout.write('\x1b[?25h'); // restore cursor
|
|
2654
|
+
// LT-15 Feature 4 — say how many waiting messages were never sent.
|
|
2655
|
+
{
|
|
2656
|
+
const unsent = takeUnsentReport();
|
|
2657
|
+
if (unsent)
|
|
2658
|
+
console.log(unsent.trimEnd());
|
|
2659
|
+
}
|
|
2543
2660
|
printGoodbye(session);
|
|
2544
2661
|
// Final review B M-2 — through gracefulExit (fetch drain, capped).
|
|
2545
2662
|
// Re-review item 3 — mark the session done first, so nothing queued
|
|
@@ -2559,6 +2676,18 @@ export async function runRepl(opts) {
|
|
|
2559
2676
|
resolveDone();
|
|
2560
2677
|
}
|
|
2561
2678
|
};
|
|
2679
|
+
// LT-15 Task 5b — Esc while working (the App calls this only while busy
|
|
2680
|
+
// and when no card, held draft or Ctrl+R search owns the Esc). The brake
|
|
2681
|
+
// decides with decideEscape and reuses Ctrl+C's own stop.
|
|
2682
|
+
const onBrake = (ui) => {
|
|
2683
|
+
turnStop.press(ui);
|
|
2684
|
+
};
|
|
2685
|
+
// LT-15 Revision 2 — Esc / [c] on the approval card (or Esc / Cancel on
|
|
2686
|
+
// the plan gate card) is the same safe stop as the Esc brake. The card
|
|
2687
|
+
// still answers cancel-turn, so nothing from it is written or minted.
|
|
2688
|
+
const onCardStop = () => {
|
|
2689
|
+
turnStop.stopFromCard();
|
|
2690
|
+
};
|
|
2562
2691
|
// Wave1 live-fix (Bug 2) — bracketed paste for the Ink path. Windows
|
|
2563
2692
|
// Terminal delivers pasted newlines as `\r`, and a `\r`-only chunk
|
|
2564
2693
|
// parses as key.return (ink parse-keypress.js:145-148) → every pasted
|
|
@@ -2623,6 +2752,8 @@ export async function runRepl(opts) {
|
|
|
2623
2752
|
sapAlias: selectedAlias,
|
|
2624
2753
|
onInterrupt,
|
|
2625
2754
|
onExitRequest,
|
|
2755
|
+
onBrake,
|
|
2756
|
+
onCardStop,
|
|
2626
2757
|
}), {
|
|
2627
2758
|
// Bug 2 (wave1 live-fix) — Ink reads from the paste guard, not
|
|
2628
2759
|
// process.stdin directly. The guard satisfies Ink's full stdin
|
|
@@ -2701,6 +2832,13 @@ export async function runRepl(opts) {
|
|
|
2701
2832
|
if (exitTail === 'timeout') {
|
|
2702
2833
|
console.log(formatAttention({ text: 'Post-turn work did not finish', fact: 'the session file may be incomplete' }).trimEnd());
|
|
2703
2834
|
}
|
|
2835
|
+
// LT-15 Feature 4 — `/exit` (or the idle exit) with messages still waiting:
|
|
2836
|
+
// they are not sent; say how many.
|
|
2837
|
+
{
|
|
2838
|
+
const unsent = takeUnsentReport();
|
|
2839
|
+
if (unsent)
|
|
2840
|
+
console.log(unsent.trimEnd());
|
|
2841
|
+
}
|
|
2704
2842
|
printGoodbye(session);
|
|
2705
2843
|
return 0;
|
|
2706
2844
|
}
|
|
@@ -3026,6 +3164,8 @@ export async function runRepl(opts) {
|
|
|
3026
3164
|
// Task 13 — classic in-run hop flag: set by the loop-top consumer from planHops.take().
|
|
3027
3165
|
let classicPlanHop = false;
|
|
3028
3166
|
let classicLineBy = 'person';
|
|
3167
|
+
/** The command CSPeach queued for this line (null when the person typed it). */
|
|
3168
|
+
let classicQueuedCmd = null;
|
|
3029
3169
|
while (true) {
|
|
3030
3170
|
// Re-attach the bare-Esc detector every iteration. Inquirer prompts
|
|
3031
3171
|
// (slash-picker, approval, ask_question, safety-confirm) remove
|
|
@@ -3042,6 +3182,7 @@ export async function runRepl(opts) {
|
|
|
3042
3182
|
const classicQueued = pendingDispatchAccessor.take();
|
|
3043
3183
|
// Task 3 — who started this line: the CSPeach source that queued it, else the person.
|
|
3044
3184
|
classicLineBy = classicQueued?.origin?.by ?? 'person';
|
|
3185
|
+
classicQueuedCmd = classicQueued?.origin ? classicQueued.cmd : null;
|
|
3045
3186
|
if (classicQueued) {
|
|
3046
3187
|
const dispatchedCmd = classicQueued.cmd;
|
|
3047
3188
|
// No context clear here: beginPlanPhaseContext (plan-boundary.ts,
|
|
@@ -3197,16 +3338,18 @@ export async function runRepl(opts) {
|
|
|
3197
3338
|
// Task 4 (ux-wave3) — classic-mode /export audit [filename]. Mirrors the
|
|
3198
3339
|
// Ink handler: build the signable audit record, write it to cwd, print path.
|
|
3199
3340
|
if (trimmed === '/export' || trimmed.startsWith('/export ')) {
|
|
3200
|
-
const { parseExportArgs, runExportAuditCommand } = await import('./commands/export-audit.js');
|
|
3201
|
-
const { sub, filename } = parseExportArgs(trimmed.slice('/export'.length));
|
|
3341
|
+
const { parseExportArgs, runExportAuditCommand, EXPORT_REPL_USAGE } = await import('./commands/export-audit.js');
|
|
3342
|
+
const { sub, filename, docxArgs } = parseExportArgs(trimmed.slice('/export'.length));
|
|
3202
3343
|
// Word export (2026-10-19): /export docx <file>.md, same body as `cspeach export`.
|
|
3344
|
+
// 2026-10-23: no file → the last answer; `conversation` → the whole conversation.
|
|
3345
|
+
// The Word package loads only here: /export audit never needs it.
|
|
3203
3346
|
if (sub === 'docx') {
|
|
3204
|
-
const {
|
|
3205
|
-
await
|
|
3347
|
+
const { runExportDocxRepl } = await import('./commands/export-conversation.js');
|
|
3348
|
+
await runExportDocxRepl({ args: docxArgs ?? '', session, ...(signedInAs ? { person: signedInAs } : {}), emit: (l) => console.log(l) });
|
|
3206
3349
|
continue;
|
|
3207
3350
|
}
|
|
3208
3351
|
if (sub !== 'audit') {
|
|
3209
|
-
console.log(chalk.dim(
|
|
3352
|
+
console.log(chalk.dim(EXPORT_REPL_USAGE));
|
|
3210
3353
|
continue;
|
|
3211
3354
|
}
|
|
3212
3355
|
await runExportAuditCommand({
|
|
@@ -3382,7 +3525,7 @@ export async function runRepl(opts) {
|
|
|
3382
3525
|
provider,
|
|
3383
3526
|
userMessage: rr.prompt,
|
|
3384
3527
|
skill: rr.skill,
|
|
3385
|
-
ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: replPreviewHook, chunkEmitter: classicOutputEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor },
|
|
3528
|
+
ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: replPreviewHook, chunkEmitter: classicOutputEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor, inlineSkills: inlineActive({ skills: cfg.assistant.skills, lane: laneSetting, interactive: !pipedStdin, served: inlineServed(cfg.llm?.mode) }), chatLaneProcess: isChatLaneProcess(laneSetting, !pipedStdin), turnBy: lastTurnBy },
|
|
3386
3529
|
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
3387
3530
|
autoCompact: true, // Task 19 — top-level REPL turn: auto-compact gate at turn end
|
|
3388
3531
|
}),
|
|
@@ -4124,7 +4267,8 @@ export async function runRepl(opts) {
|
|
|
4124
4267
|
provider,
|
|
4125
4268
|
userMessage: prompt,
|
|
4126
4269
|
skill: skill,
|
|
4127
|
-
|
|
4270
|
+
// Stage 1A — inline is decided here, at every submit, from today's inputs (never cached).
|
|
4271
|
+
ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: replPreviewHook, chunkEmitter: classicOutputEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor, inlineSkills: inlineActive({ skills: cfg.assistant.skills, lane: laneSetting, interactive: !pipedStdin, served: inlineServed(cfg.llm?.mode) }), chatLaneProcess: isChatLaneProcess(laneSetting, !pipedStdin), turnBy: lastTurnBy },
|
|
4128
4272
|
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
4129
4273
|
explicitSlash: parsed.skill !== undefined, // Task 7 R11 — typed or queued slash command
|
|
4130
4274
|
signal: turnAbort.signal,
|
|
@@ -4133,6 +4277,8 @@ export async function runRepl(opts) {
|
|
|
4133
4277
|
// turns (their post-turn persist indexes messages from turn start).
|
|
4134
4278
|
autoCompact: planResume === null && planContinue === null,
|
|
4135
4279
|
delegateSkill: planPhaseDelegate,
|
|
4280
|
+
// /export docx conversation — a plan step or a queued step is CSPeach's words, not the person's.
|
|
4281
|
+
...(await cliTurnOrigin(planResume, classicQueuedCmd)),
|
|
4136
4282
|
// A4 — undefined on every non-plan turn (and when tiering is off).
|
|
4137
4283
|
modelOverride: planModelOverride,
|
|
4138
4284
|
// Task 9 — a tier-down, never the session role (proxy must not enforce it).
|
|
@@ -157,7 +157,7 @@ export async function classifyPrompt(prompt, context, opts = {}) {
|
|
|
157
157
|
// before classification; this guard is the backstop that turns any
|
|
158
158
|
// future regression into a loud error instead of a silent misroute.
|
|
159
159
|
// Fires BEFORE config/key loading so no network call is ever made.
|
|
160
|
-
if (prompt.trimStart().startsWith('/')) {
|
|
160
|
+
if (!opts.prose && prompt.trimStart().startsWith('/')) {
|
|
161
161
|
throw new Error('slash command reached the classifier — explicit /commands must be routed before classification (CLI bug, please report)');
|
|
162
162
|
}
|
|
163
163
|
const cfg = await loadConfig();
|
|
@@ -7,6 +7,13 @@ let unreachable = null;
|
|
|
7
7
|
const CODE_RE = /\b(ETIMEDOUT|ECONNREFUSED|ENOTFOUND|EHOSTUNREACH|ENETUNREACH|ECONNRESET|EAI_AGAIN)\b/;
|
|
8
8
|
export const SAP_UNREACHABLE_DIRECTIVE = 'SAP is not reachable from this machine right now. Stop here: do not retry SAP tools and do not write the plan manifest. '
|
|
9
9
|
+ 'End the turn with one short sentence. CSPeach pauses the build and runs this step again when the network is back.';
|
|
10
|
+
/**
|
|
11
|
+
* LT-14 (1A fix wave) — outside a plan step, the note that rides on a SAP
|
|
12
|
+
* tool error when SAP did not answer. Before, the model got the bare error
|
|
13
|
+
* and invented network advice for the person. No pause, no flag.
|
|
14
|
+
*/
|
|
15
|
+
export const SAP_UNREACHABLE_NOTE = 'SAP did not answer from this machine. Do not guess why or give the person network advice. '
|
|
16
|
+
+ "After three such failures in a row, stop and ask; offer 'SAP is reachable again; try once more' and 'Stop for now; I'll pick up later'.";
|
|
10
17
|
export function noteSapUnreachable(code) {
|
|
11
18
|
unreachable = code;
|
|
12
19
|
}
|
package/dist/sap/unreachable.js
CHANGED
|
@@ -26,6 +26,6 @@ export function unreachableMessage(alias, host, port, code) {
|
|
|
26
26
|
return (
|
|
27
27
|
// 2026-10-04: never assume a VPN — many users sit on the company network.
|
|
28
28
|
`Can't reach SAP system ${alias} (${host}:${port}) from this machine — ${code}. ` +
|
|
29
|
-
'Check network access to the SAP system
|
|
30
|
-
'
|
|
29
|
+
'Check network access to the SAP system. ' +
|
|
30
|
+
'For work that does not need SAP, run with --no-sap.');
|
|
31
31
|
}
|