@cspeach/cli 1.1.51 → 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 +208 -41
- 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 +10 -2
- package/dist/commands/compact.js +6 -1
- package/dist/commands/config-set.js +13 -0
- package/dist/commands/config-show.js +16 -4
- package/dist/commands/export-conversation.js +7 -4
- package/dist/commands/finish-check-guard.js +124 -0
- package/dist/commands/plan-reground.js +27 -4
- package/dist/commands/plan-resume.js +111 -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/models/server-config.js +1 -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/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/turn-stop.js +64 -0
- package/dist/repl.js +144 -22
- 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/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/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';
|
|
@@ -1079,13 +1083,47 @@ export async function runRepl(opts) {
|
|
|
1079
1083
|
: pendingCancelLine(phase, exiting, queuedWorkLabel(pendingDispatchAccessor.get()));
|
|
1080
1084
|
chunkEmitter.emit('chunk', '\n' + formatInfo(line));
|
|
1081
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
|
+
});
|
|
1082
1117
|
/** Auto-dispatch of the next plan phase, unless a post-turn cancel stopped it. */
|
|
1083
1118
|
// Task 13 fix round — `planHop` travels WITH the command into handleSubmit,
|
|
1084
1119
|
// so a refused submit can never leave a stale hop mark behind.
|
|
1085
1120
|
const autoDispatch = (cmd, planHop = false, queued = null) => {
|
|
1086
1121
|
// Round 4 item 9b — the exit is already under way: start nothing new.
|
|
1087
1122
|
// Final review B-I2 — `/exit` typed mid-turn (exitAfterTurn) stops it too.
|
|
1088
|
-
|
|
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);
|
|
1089
1127
|
if (decision.kind === 'skip')
|
|
1090
1128
|
return;
|
|
1091
1129
|
if (decision.kind === 'not-started') {
|
|
@@ -1098,6 +1136,8 @@ export async function runRepl(opts) {
|
|
|
1098
1136
|
const notice = queued ? null : autoRoutedNotice(cmd);
|
|
1099
1137
|
if (notice)
|
|
1100
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();
|
|
1101
1141
|
setImmediate(() => { void handleSubmit(cmd, { planHop, queued }); });
|
|
1102
1142
|
};
|
|
1103
1143
|
// BUG-2b — the actual dispatch. Wrapped by handleSubmit (below) which
|
|
@@ -1108,6 +1148,9 @@ export async function runRepl(opts) {
|
|
|
1108
1148
|
const trimmed = line.trim();
|
|
1109
1149
|
if (!trimmed)
|
|
1110
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;
|
|
1111
1154
|
// Round 6 review I2 — the resumed skill is offered to the FIRST submit
|
|
1112
1155
|
// only (slash commands included), then cleared.
|
|
1113
1156
|
const resumedSkill = resumeKeepsSkill ? session.skill ?? null : null;
|
|
@@ -1116,7 +1159,10 @@ export async function runRepl(opts) {
|
|
|
1116
1159
|
// the Footer has already persisted this submission (flattened), so
|
|
1117
1160
|
// the consecutive-dup guard makes this a no-op instead of the old
|
|
1118
1161
|
// double append.
|
|
1119
|
-
|
|
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);
|
|
1120
1166
|
// Phase A** (2026-05-16) — echo the user's prompt into Body so it
|
|
1121
1167
|
// appears in the conversation history. Plus a dim divider above
|
|
1122
1168
|
// the echo so consecutive turns have a clear visual break (closes
|
|
@@ -1362,7 +1408,7 @@ export async function runRepl(opts) {
|
|
|
1362
1408
|
provider,
|
|
1363
1409
|
userMessage: rr.prompt,
|
|
1364
1410
|
skill: rr.skill,
|
|
1365
|
-
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 },
|
|
1366
1412
|
chunkEmitter,
|
|
1367
1413
|
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
1368
1414
|
autoCompact: true, // Task 19 — top-level REPL turn: auto-compact gate at turn end
|
|
@@ -1371,6 +1417,11 @@ export async function runRepl(opts) {
|
|
|
1371
1417
|
session,
|
|
1372
1418
|
print: (line) => chunkEmitter.emit('chunk', line + '\n'),
|
|
1373
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),
|
|
1374
1425
|
onFinally: () => {
|
|
1375
1426
|
turnStatusEmitter.stop();
|
|
1376
1427
|
currentTurnAbort = null;
|
|
@@ -1393,6 +1444,9 @@ export async function runRepl(opts) {
|
|
|
1393
1444
|
todoEmitter.emit('update', []);
|
|
1394
1445
|
clearAppFlow(session); // Task 7 — /new ends an /abap-app flow
|
|
1395
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();
|
|
1396
1450
|
// 2026-10-22 — and the conversation history: the next prompt sends none of it.
|
|
1397
1451
|
startFreshConversation(session);
|
|
1398
1452
|
idleKeepalive.disarm(); // no idle cache ping for the old history
|
|
@@ -1465,7 +1519,9 @@ export async function runRepl(opts) {
|
|
|
1465
1519
|
// an item in a 2+-item list in the LAST assistant message.
|
|
1466
1520
|
let effectiveInput = trimmed;
|
|
1467
1521
|
{
|
|
1468
|
-
|
|
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));
|
|
1469
1525
|
if (expansion) {
|
|
1470
1526
|
chunkEmitter.emit('chunk', chalk.dim(`${expansion.echo}\n`));
|
|
1471
1527
|
effectiveInput = expansion.text;
|
|
@@ -1490,7 +1546,9 @@ export async function runRepl(opts) {
|
|
|
1490
1546
|
return;
|
|
1491
1547
|
}
|
|
1492
1548
|
effectiveInput = door.input;
|
|
1493
|
-
|
|
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);
|
|
1494
1552
|
// D35 (2026-06-11) — explicit slash naming a skill that isn't in the
|
|
1495
1553
|
// catalog: print the notice ("did you mean" / builtin-with-args) and
|
|
1496
1554
|
// end the turn. Slash input must NEVER fall through to the classifier
|
|
@@ -1611,7 +1669,7 @@ export async function runRepl(opts) {
|
|
|
1611
1669
|
sap_system: selectedAlias,
|
|
1612
1670
|
last_skill: session.last_skill ?? null,
|
|
1613
1671
|
last_object: extractedObject ?? session.last_object ?? null,
|
|
1614
|
-
}, { signal: routingCtl.signal });
|
|
1672
|
+
}, { signal: routingCtl.signal, prose });
|
|
1615
1673
|
}
|
|
1616
1674
|
catch (err) {
|
|
1617
1675
|
if (!routingCtl.signal.aborted) {
|
|
@@ -2056,7 +2114,8 @@ export async function runRepl(opts) {
|
|
|
2056
2114
|
provider,
|
|
2057
2115
|
userMessage: prompt,
|
|
2058
2116
|
skill: skill,
|
|
2059
|
-
|
|
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 },
|
|
2060
2119
|
chunkEmitter,
|
|
2061
2120
|
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
2062
2121
|
explicitSlash: parsed.skill !== undefined, // Task 7 R11 — typed or queued slash command
|
|
@@ -2134,7 +2193,7 @@ export async function runRepl(opts) {
|
|
|
2134
2193
|
// never throw ChatUnavailableError again.
|
|
2135
2194
|
laneSetting = 'skills';
|
|
2136
2195
|
sapStateStore.update({ chatLane: false });
|
|
2137
|
-
pendingDispatchAccessor.set(trimmed, chatFallbackOrigin());
|
|
2196
|
+
pendingDispatchAccessor.set(trimmed, chatFallbackOrigin(prose));
|
|
2138
2197
|
}
|
|
2139
2198
|
else {
|
|
2140
2199
|
// 2026-09-24 — print the turn-error block INTO the conversation (it
|
|
@@ -2144,13 +2203,25 @@ export async function runRepl(opts) {
|
|
|
2144
2203
|
// "type continue" (the partial turn is already saved).
|
|
2145
2204
|
const { renderTurnError } = await import('./agent/turn-error-ux.js');
|
|
2146
2205
|
turnCancelledByUser = currentTurnAbort?.signal.aborted ?? false;
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
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
|
+
}
|
|
2154
2225
|
}
|
|
2155
2226
|
}
|
|
2156
2227
|
finally {
|
|
@@ -2443,21 +2514,23 @@ export async function runRepl(opts) {
|
|
|
2443
2514
|
chunkEmitter.emit('chunk', midTurnSlashHint(kind.command));
|
|
2444
2515
|
return;
|
|
2445
2516
|
}
|
|
2446
|
-
|
|
2517
|
+
// LT-15 Feature 1 (D5) — no scrollback receipt; the App's queue row shows it.
|
|
2518
|
+
if (t.length > 0)
|
|
2447
2519
|
enqueueSteering(t);
|
|
2448
|
-
chunkEmitter.emit('chunk', steeringEchoLine(t));
|
|
2449
|
-
}
|
|
2450
2520
|
return;
|
|
2451
2521
|
}
|
|
2452
2522
|
idleKeepalive.disarm(); // Task 15 — a submit ends the idle period
|
|
2453
2523
|
turnStatusEmitter.reserve();
|
|
2454
2524
|
postTurn = false;
|
|
2455
2525
|
postTurnMayChain = false;
|
|
2526
|
+
// LT-15 review I5 — a dispatch that threw ended by an error.
|
|
2527
|
+
let threw = true;
|
|
2456
2528
|
try {
|
|
2457
2529
|
// Round 4 item 9b — the exit path waits (capped) for this to finish.
|
|
2458
2530
|
const run = dispatchSubmit(line, submitOpts);
|
|
2459
2531
|
dispatchTail = run;
|
|
2460
2532
|
await run;
|
|
2533
|
+
threw = false;
|
|
2461
2534
|
}
|
|
2462
2535
|
finally {
|
|
2463
2536
|
turnStatusEmitter.release();
|
|
@@ -2479,6 +2552,14 @@ export async function runRepl(opts) {
|
|
|
2479
2552
|
// the submit ended in an exit (merge: UI round-3 /exit mid-turn).
|
|
2480
2553
|
if (!done)
|
|
2481
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();
|
|
2482
2563
|
}
|
|
2483
2564
|
};
|
|
2484
2565
|
// Fix round 3 (item 5) — `/exit` / `/quit` typed while a turn runs:
|
|
@@ -2495,6 +2576,9 @@ export async function runRepl(opts) {
|
|
|
2495
2576
|
cancelAnnouncedPhase = null;
|
|
2496
2577
|
pendingCancel.clear();
|
|
2497
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();
|
|
2498
2582
|
},
|
|
2499
2583
|
exit: () => { done = true; if (resolveDone)
|
|
2500
2584
|
resolveDone(); },
|
|
@@ -2532,6 +2616,10 @@ export async function runRepl(opts) {
|
|
|
2532
2616
|
let idleInterruptAt = 0;
|
|
2533
2617
|
const onInterrupt = () => {
|
|
2534
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();
|
|
2535
2623
|
const action = decideInterrupt({
|
|
2536
2624
|
turnActive: currentTurnAbort !== null,
|
|
2537
2625
|
// Review I3 — routing / prep in flight without a controller yet is
|
|
@@ -2545,18 +2633,30 @@ export async function runRepl(opts) {
|
|
|
2545
2633
|
});
|
|
2546
2634
|
if (action === 'abort-turn') {
|
|
2547
2635
|
interruptCount++;
|
|
2548
|
-
|
|
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'));
|
|
2549
2640
|
currentTurnAbort.abort();
|
|
2641
|
+
return 'stopped'; // LT-15 D1 — App puts the waiting text back in the box now
|
|
2550
2642
|
}
|
|
2551
2643
|
else if (action === 'cancel-pending') {
|
|
2552
2644
|
interruptCount++;
|
|
2645
|
+
turnStop.noteCtrlCStop();
|
|
2553
2646
|
announcePendingCancel(false);
|
|
2647
|
+
return 'stopped';
|
|
2554
2648
|
}
|
|
2555
2649
|
else if (action === 'force-exit') {
|
|
2556
2650
|
// Second Ctrl+C during the same turn — give up and exit.
|
|
2557
2651
|
chunkEmitter.emit('chunk', '\n' + formatInfo('Exiting · second Ctrl+C'));
|
|
2558
2652
|
unmount();
|
|
2559
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
|
+
}
|
|
2560
2660
|
printGoodbye(session);
|
|
2561
2661
|
// Final review B M-2 — through gracefulExit (fetch drain, capped).
|
|
2562
2662
|
// Re-review item 3 — mark the session done first, so nothing queued
|
|
@@ -2576,6 +2676,18 @@ export async function runRepl(opts) {
|
|
|
2576
2676
|
resolveDone();
|
|
2577
2677
|
}
|
|
2578
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
|
+
};
|
|
2579
2691
|
// Wave1 live-fix (Bug 2) — bracketed paste for the Ink path. Windows
|
|
2580
2692
|
// Terminal delivers pasted newlines as `\r`, and a `\r`-only chunk
|
|
2581
2693
|
// parses as key.return (ink parse-keypress.js:145-148) → every pasted
|
|
@@ -2640,6 +2752,8 @@ export async function runRepl(opts) {
|
|
|
2640
2752
|
sapAlias: selectedAlias,
|
|
2641
2753
|
onInterrupt,
|
|
2642
2754
|
onExitRequest,
|
|
2755
|
+
onBrake,
|
|
2756
|
+
onCardStop,
|
|
2643
2757
|
}), {
|
|
2644
2758
|
// Bug 2 (wave1 live-fix) — Ink reads from the paste guard, not
|
|
2645
2759
|
// process.stdin directly. The guard satisfies Ink's full stdin
|
|
@@ -2718,6 +2832,13 @@ export async function runRepl(opts) {
|
|
|
2718
2832
|
if (exitTail === 'timeout') {
|
|
2719
2833
|
console.log(formatAttention({ text: 'Post-turn work did not finish', fact: 'the session file may be incomplete' }).trimEnd());
|
|
2720
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
|
+
}
|
|
2721
2842
|
printGoodbye(session);
|
|
2722
2843
|
return 0;
|
|
2723
2844
|
}
|
|
@@ -3404,7 +3525,7 @@ export async function runRepl(opts) {
|
|
|
3404
3525
|
provider,
|
|
3405
3526
|
userMessage: rr.prompt,
|
|
3406
3527
|
skill: rr.skill,
|
|
3407
|
-
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 },
|
|
3408
3529
|
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
3409
3530
|
autoCompact: true, // Task 19 — top-level REPL turn: auto-compact gate at turn end
|
|
3410
3531
|
}),
|
|
@@ -4146,7 +4267,8 @@ export async function runRepl(opts) {
|
|
|
4146
4267
|
provider,
|
|
4147
4268
|
userMessage: prompt,
|
|
4148
4269
|
skill: skill,
|
|
4149
|
-
|
|
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 },
|
|
4150
4272
|
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
4151
4273
|
explicitSlash: parsed.skill !== undefined, // Task 7 R11 — typed or queued slash command
|
|
4152
4274
|
signal: turnAbort.signal,
|
|
@@ -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
|
}
|
|
@@ -45,5 +45,7 @@ export function queuedStepView(origin) {
|
|
|
45
45
|
if (!line)
|
|
46
46
|
return { echo: false, line: null };
|
|
47
47
|
// D11 — the chat-fallback line is dim; painted after shownUpTo flattened it.
|
|
48
|
-
|
|
48
|
+
// LT-4 (1A fix wave, owner ruling): the hand-off arrow line is dim too, kept in scrollback.
|
|
49
|
+
const dim = origin.by === 'chat_fallback' || origin.by === 'hand_off';
|
|
50
|
+
return { echo: false, line: dim ? paint('dim', line) : line };
|
|
49
51
|
}
|
|
@@ -26,8 +26,17 @@ export function isUserPrompt(m) {
|
|
|
26
26
|
* (`<session_context>` from the intent extractor / SAP system info,
|
|
27
27
|
* `<project_context>` from CSPEACH_PROJECT_CONTEXT). The user never typed
|
|
28
28
|
* them, so a prompt preview must never show them (fix round 3, item 2).
|
|
29
|
+
* Stage 1A (M5) — `<skill>`, the playbook block an inline turn appends. The
|
|
30
|
+
* order matters: `<session_context>` first, so the bare `<skill>` word in a
|
|
31
|
+
* `<playbook>` line goes with the block it sits in — `skill` stays last.
|
|
32
|
+
* Shared with the conversation export (commands/export-conversation.ts).
|
|
29
33
|
*/
|
|
30
|
-
const INJECTED_CONTEXT_TAGS = [
|
|
34
|
+
export const INJECTED_CONTEXT_TAGS = [
|
|
35
|
+
'session_context', 'project_context',
|
|
36
|
+
// The /export docx conversation list (main, 2026-10-23): one list for both readers.
|
|
37
|
+
'journey', 'lane_decision', 'system_switch', 'sap_system', 'sap_connection',
|
|
38
|
+
'skill',
|
|
39
|
+
];
|
|
31
40
|
/**
|
|
32
41
|
* One-line preview of what the user typed: injected context blocks removed,
|
|
33
42
|
* `<attached file="x">…</attached>` expansions shown as the original `@x`
|
|
@@ -4,6 +4,7 @@ import { sha512 } from '@noble/hashes/sha512';
|
|
|
4
4
|
import { bytesToHex, concatBytes } from '@noble/hashes/utils';
|
|
5
5
|
import { MANIFEST_PUBLIC_KEYS } from './signing-public-key.js';
|
|
6
6
|
import { canonicalise } from './canonical.js';
|
|
7
|
+
import { fetchWithConnectRetry } from '../util/connect-retry.js';
|
|
7
8
|
// @noble/ed25519 v2 requires an ed25519 sha512 hook.
|
|
8
9
|
ed.etc.sha512Sync = (...m) => sha512(concatBytes(...m));
|
|
9
10
|
/**
|
|
@@ -39,7 +40,7 @@ export class ManifestClient {
|
|
|
39
40
|
async fetchManifest() {
|
|
40
41
|
let r;
|
|
41
42
|
try {
|
|
42
|
-
r = await
|
|
43
|
+
r = await fetchWithConnectRetry(this.manifestUrl, () => ({ signal: AbortSignal.timeout(10_000) }));
|
|
43
44
|
}
|
|
44
45
|
catch (e) {
|
|
45
46
|
throw new ManifestUnavailableError(`manifest fetch failed (${this.manifestUrl}): ${e.message}`);
|
|
@@ -99,7 +100,7 @@ export class ManifestClient {
|
|
|
99
100
|
async fetchSkillBody(name, bodyUrl, opts) {
|
|
100
101
|
let r;
|
|
101
102
|
try {
|
|
102
|
-
r = await
|
|
103
|
+
r = await fetchWithConnectRetry(bodyUrl, () => ({ signal: AbortSignal.timeout(10_000) }));
|
|
103
104
|
}
|
|
104
105
|
catch (e) {
|
|
105
106
|
throw new Error(`skill body fetch failed (${bodyUrl}): ${e.message}`);
|
package/dist/skills/preamble.js
CHANGED
|
@@ -52,8 +52,10 @@ take precedence if a skill's instruction conflicts with them.
|
|
|
52
52
|
revise and re-submit.
|
|
53
53
|
|
|
54
54
|
## Transport discipline
|
|
55
|
-
- All writes go into the
|
|
56
|
-
for a transport number per change.
|
|
55
|
+
- All writes go into the session transport once one is set. Do not ask
|
|
56
|
+
the user for a transport number per change. When a create says a
|
|
57
|
+
transport is needed, ask the user once: a new session transport
|
|
58
|
+
(recommended) or one of the open requests for the package.
|
|
57
59
|
- Never use \$TMP unless the user explicitly requests local-only work.
|
|
58
60
|
|
|
59
61
|
## Escalate, don't fail silently
|
|
@@ -70,6 +72,6 @@ take precedence if a skill's instruction conflicts with them.
|
|
|
70
72
|
|
|
71
73
|
## One skill per turn
|
|
72
74
|
- If the user's request is better served by a different skill, call
|
|
73
|
-
|
|
74
|
-
skill yourself.
|
|
75
|
+
dispatch_skill after the person has picked the command. Do not try to do
|
|
76
|
+
the work of another skill yourself.
|
|
75
77
|
`;
|
|
@@ -14,6 +14,7 @@ import { sha256 } from '@noble/hashes/sha256';
|
|
|
14
14
|
import { bytesToHex } from '@noble/hashes/utils';
|
|
15
15
|
import { ManifestClient, DEFAULT_MANIFEST_URL, SkillIntegrityError, ManifestUnavailableError, } from './manifest-client.js';
|
|
16
16
|
import { fileSkillsCache } from './skills-cache.js';
|
|
17
|
+
import { fetchWithConnectRetry } from '../util/connect-retry.js';
|
|
17
18
|
// Defined next to the client that throws it (Task 8 (a): typed, never string-matched).
|
|
18
19
|
export { SkillIntegrityError };
|
|
19
20
|
export class SkillNotInPlanError extends Error {
|
|
@@ -69,10 +70,10 @@ async function fetchSignedInBody(baseUrl, name, expectedSha, getBearer, doFetch)
|
|
|
69
70
|
throw new SkillSignInError();
|
|
70
71
|
let r;
|
|
71
72
|
try {
|
|
72
|
-
r = await
|
|
73
|
+
r = await fetchWithConnectRetry(`${baseUrl}/v1/skills/${encodeURIComponent(name)}`, () => ({
|
|
73
74
|
headers: { Authorization: `Bearer ${bearer}` },
|
|
74
75
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
75
|
-
});
|
|
76
|
+
}), { fetchImpl: doFetch });
|
|
76
77
|
}
|
|
77
78
|
catch {
|
|
78
79
|
throw new SkillsUnavailableError('could not reach CSPeach');
|
package/dist/tools/approval.js
CHANGED
|
@@ -6,6 +6,14 @@ import { isHeadless, shouldUseInk } from '../renderer/tty.js';
|
|
|
6
6
|
import { effectiveRisk, riskResultFields, typeCode } from '../approvals/risk-floor.js';
|
|
7
7
|
import { objectsWithSyntaxErrors } from './syntax-state.js';
|
|
8
8
|
import { noteWriteTransport } from '../repl/current-transport.js';
|
|
9
|
+
import { STOPPED_BEFORE_START_RESULT } from '../agent/repair-partial.js';
|
|
10
|
+
/**
|
|
11
|
+
* LT-15 D6 (F6) — after a stop (Esc or Ctrl+C set the same turn abort) no new
|
|
12
|
+
* card opens and no approval id is minted. The shape dispatchTool returns for
|
|
13
|
+
* a tool that never started (the tool_use stays paired), with a text that says
|
|
14
|
+
* nothing ran.
|
|
15
|
+
*/
|
|
16
|
+
const stoppedBeforeCard = (ctx) => (ctx.signal?.aborted ? { content: STOPPED_BEFORE_START_RESULT, is_error: true } : null);
|
|
9
17
|
/**
|
|
10
18
|
* Round 8 item 6 — the footer showed "no transport" while the approved card
|
|
11
19
|
* said "transport S4HK903731". An approval granted for an explicit transport
|
|
@@ -29,7 +37,7 @@ import { promptAdvisory } from '../approvals/advisory-prompt.js';
|
|
|
29
37
|
import { markPlanGateApproved } from '../repl/rule8-detector.js';
|
|
30
38
|
import { getEffectiveWriteMode } from '../repl/mode-cycle.js';
|
|
31
39
|
import { getCurrentTransport } from '../repl/current-transport.js';
|
|
32
|
-
import { displayTransport } from '../approvals/op-labels.js';
|
|
40
|
+
import { displayTransport, knownTypeWord } from '../approvals/op-labels.js';
|
|
33
41
|
import { canonicalApprovalObject as bareObjectName } from '../approvals/canonical.js';
|
|
34
42
|
import { projectTransport, SEVERAL_REQUESTS_LABEL, transportForWrite, TRANSPORT_REQUEST } from '../register/project-transport.js';
|
|
35
43
|
import { NOT_ON_THIS_RELEASE, approvalRefusedByProfile } from '../sap/profile-gate.js';
|
|
@@ -223,7 +231,8 @@ registerTool({
|
|
|
223
231
|
type: 'object',
|
|
224
232
|
properties: {
|
|
225
233
|
op: { type: 'string', enum: ['create', 'modify', 'delete', 'activate', 'release', 'run'] },
|
|
226
|
-
|
|
234
|
+
// P-2 (1A fix wave): a group word ('DCLs') is not an object the plan grant can match.
|
|
235
|
+
object: { type: 'string', description: "One real object name per row (comma-join several only in an activate row), never a group word such as 'DCLs'." },
|
|
227
236
|
type: { type: 'string' },
|
|
228
237
|
diff: { type: 'string', description: 'For a CREATE op this MUST be the exact, complete source that will be written — verbatim, character-for-character, NOT a description, summary, or paraphrase. It is shown to the developer in the approval box as the exact thing they are approving, so a one-line summary here means they approve a write they cannot inspect. For a MODIFY op, the real unified diff of the change (again verbatim, not a description).' },
|
|
229
238
|
package: { type: 'string', description: 'Target package (development class) for the object, if known — shown on the approval box' },
|
|
@@ -298,6 +307,10 @@ registerTool({
|
|
|
298
307
|
// no role configured and no session override this is exactly
|
|
299
308
|
// cfg.write_mode — byte-identical behavior.
|
|
300
309
|
const role = cfg.sap[ctx.sapAlias]?.role;
|
|
310
|
+
// LT-15 D6 — stopped during the profile read above: no advisory card, no card.
|
|
311
|
+
const stopA = stoppedBeforeCard(ctx);
|
|
312
|
+
if (stopA)
|
|
313
|
+
return stopA;
|
|
301
314
|
// Advisory-only short-circuit — never mint approval_ids.
|
|
302
315
|
// The developer applies the change manually in ADT; the AI gets an
|
|
303
316
|
// ai_directive telling it not to attempt any SAP write tools.
|
|
@@ -313,11 +326,17 @@ registerTool({
|
|
|
313
326
|
return { content: JSON.stringify(releaseRefusal), is_error: true };
|
|
314
327
|
// Task 8 (B11) — the request that already holds each modified object, before any card.
|
|
315
328
|
await attachOwningTransport(args.changes ?? [], ctx);
|
|
329
|
+
// LT-15 D6 — stopped during the reads above: no card, no nag, no id minted
|
|
330
|
+
// (the plan-grant and auto-approve paths below mint without a card).
|
|
331
|
+
const stopB = stoppedBeforeCard(ctx);
|
|
332
|
+
if (stopB)
|
|
333
|
+
return stopB;
|
|
316
334
|
let sapCfg = cfg.sap[ctx.sapAlias];
|
|
317
335
|
const riskCtx = {
|
|
318
336
|
sapAlias: ctx.sapAlias,
|
|
319
337
|
autoApprovePolicy: sapCfg?.auto_approve ?? 'never',
|
|
320
338
|
syntaxErrorObjects: objectsWithSyntaxErrors(),
|
|
339
|
+
typeWord: knownTypeWord, // LT-10: the syntax-errors reason names the kind of object
|
|
321
340
|
};
|
|
322
341
|
const changes = args.changes;
|
|
323
342
|
const declared = args.risk;
|
|
@@ -421,6 +440,10 @@ registerTool({
|
|
|
421
440
|
sapCfg = cfg.sap[ctx.sapAlias];
|
|
422
441
|
riskCtx.autoApprovePolicy = sapCfg?.auto_approve ?? 'never';
|
|
423
442
|
}
|
|
443
|
+
// LT-15 D6 (review M3) — stopped while the nag was up: no card after it, no id.
|
|
444
|
+
const stopC = stoppedBeforeCard(ctx);
|
|
445
|
+
if (stopC)
|
|
446
|
+
return stopC;
|
|
424
447
|
// 1.1.31 (owner, 2026-10-07): a package or transport always gets a person's
|
|
425
448
|
// yes. Live, auto_approve="low" let the model's own "low" create package
|
|
426
449
|
// ZCC_FIX_PROOF and request S4HK903757 with no card. The plan-grant path
|
|
@@ -478,12 +501,14 @@ registerTool({
|
|
|
478
501
|
// for one task" complaint. Review-each falls back to the per-change
|
|
479
502
|
// gate. Cancel rejects the whole plan.
|
|
480
503
|
//
|
|
481
|
-
// Task 9 (C8): only for more than one VISIBLE change
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
504
|
+
// Task 9 (C8): only for more than one VISIBLE change (an activate whose
|
|
505
|
+
// write is in the list is not a change a person sees). A modify + its
|
|
506
|
+
// activate on one class gets the per-change card only; the activate
|
|
507
|
+
// follows its approved write below. LT-12: the plan gate's LIST also
|
|
508
|
+
// hides a modify that a create of the same object covers (planRows), so
|
|
509
|
+
// it may show fewer rows than this count; this count is unchanged.
|
|
485
510
|
let skipPerChangeGates = false;
|
|
486
|
-
// Raw keys on purpose:
|
|
511
|
+
// Raw keys on purpose: activates collapsed, nothing else.
|
|
487
512
|
const visibleCount = collapseImplicitActivates(changes).length;
|
|
488
513
|
if (visibleCount > 1) {
|
|
489
514
|
const mode = await renderPlanGate(args.summary, changes, planGateTransport, eff, ctx.sapAlias);
|
|
@@ -549,6 +574,11 @@ registerTool({
|
|
|
549
574
|
outcome = { approved: false, reason: 'sibling_write_skipped' };
|
|
550
575
|
}
|
|
551
576
|
else {
|
|
577
|
+
// LT-15 D6 (review M3) — stopped during the mint after the card before:
|
|
578
|
+
// the next card does not open; ids minted so far are dropped unused.
|
|
579
|
+
const stopD = stoppedBeforeCard(ctx);
|
|
580
|
+
if (stopD)
|
|
581
|
+
return stopD;
|
|
552
582
|
outcome = await renderPerChangeApprovalV3(c, eff, displayTransport(c.op, args.transport, sessionTransport, c.owningTransport, project));
|
|
553
583
|
}
|
|
554
584
|
if (outcome.approved) {
|
|
@@ -44,7 +44,7 @@ registerTool({
|
|
|
44
44
|
properties: {
|
|
45
45
|
command: {
|
|
46
46
|
type: 'string',
|
|
47
|
-
description: 'Full slash command including arguments. Must start with `/`. Example: `/abap-rap --from @order-approval-system-design-2924-v1.cspeach.json`.',
|
|
47
|
+
description: 'Full slash command including arguments. Must start with `/`. Example: `/abap-rap --from @order-approval-system-design-2924-v1.cspeach.json`. Never put a transport description in it unless the person typed it.',
|
|
48
48
|
},
|
|
49
49
|
},
|
|
50
50
|
required: ['command'],
|