@cspeach/cli 1.1.18 → 1.1.19

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.
Files changed (224) hide show
  1. package/README.md +3 -4
  2. package/dist/agent/loop.js +143 -25
  3. package/dist/agent/providers/local-provider.js +4 -1
  4. package/dist/agent/repair-partial.js +76 -19
  5. package/dist/agent/tool-dispatch.js +58 -21
  6. package/dist/agent/turn-error-ux.js +77 -26
  7. package/dist/agent/turn-stream.js +25 -3
  8. package/dist/approvals/adt-type.js +27 -0
  9. package/dist/approvals/advisory-prompt.js +48 -0
  10. package/dist/approvals/advisory-render.js +25 -11
  11. package/dist/approvals/approval-prompt.js +74 -24
  12. package/dist/approvals/jwt.js +2 -1
  13. package/dist/approvals/render.js +64 -36
  14. package/dist/approvals/risk-floor.js +53 -2
  15. package/dist/auth/api-key.js +30 -16
  16. package/dist/auth/keychain.js +0 -0
  17. package/dist/auth/me.js +25 -7
  18. package/dist/cli.js +25 -6
  19. package/dist/commands/compact.js +12 -9
  20. package/dist/commands/config-set.js +6 -2
  21. package/dist/commands/config-show.js +7 -5
  22. package/dist/commands/cost.js +41 -34
  23. package/dist/commands/help.js +88 -50
  24. package/dist/commands/login.js +28 -19
  25. package/dist/commands/logout.js +7 -3
  26. package/dist/commands/plan-chain.js +71 -77
  27. package/dist/commands/plan-continue.js +1 -0
  28. package/dist/commands/plan-resume.js +20 -12
  29. package/dist/commands/whoami.js +23 -9
  30. package/dist/config/loader.js +15 -3
  31. package/dist/cost/pricing.js +6 -5
  32. package/dist/doctor/checks/forge-rules.js +13 -3
  33. package/dist/doctor/checks/keychain.js +6 -4
  34. package/dist/doctor/run.js +101 -20
  35. package/dist/index.js +5 -2
  36. package/dist/lib/graceful-exit.js +56 -0
  37. package/dist/lib/piped-prompt.js +65 -0
  38. package/dist/lock-contention.js +2 -2
  39. package/dist/one-shot.js +135 -40
  40. package/dist/projects/answer-blockers.js +6 -1
  41. package/dist/projects/handover-md.js +15 -14
  42. package/dist/projects/plan-run.js +97 -54
  43. package/dist/projects/promote-command.js +11 -2
  44. package/dist/projects/save-command.js +3 -2
  45. package/dist/projects/status.js +2 -1
  46. package/dist/projects/yes-no.js +12 -0
  47. package/dist/renderer/abap-inline.js +18 -14
  48. package/dist/renderer/answer-trim.js +69 -0
  49. package/dist/renderer/banners.js +8 -8
  50. package/dist/renderer/brand-settled.js +19 -0
  51. package/dist/renderer/change-card.js +156 -0
  52. package/dist/renderer/checklist-format.js +130 -0
  53. package/dist/renderer/color-mode.js +45 -0
  54. package/dist/renderer/error-detail.js +104 -0
  55. package/dist/renderer/fence-state.js +46 -0
  56. package/dist/renderer/footer-line.js +96 -0
  57. package/dist/renderer/glyphs.js +25 -0
  58. package/dist/renderer/goodbye.js +38 -0
  59. package/dist/renderer/legacy-palette.js +41 -0
  60. package/dist/renderer/look.js +30 -0
  61. package/dist/renderer/markdown.js +299 -92
  62. package/dist/renderer/notice-log.js +72 -0
  63. package/dist/renderer/notice-shape.js +48 -0
  64. package/dist/renderer/notices.js +40 -8
  65. package/dist/renderer/panel-rows.js +16 -0
  66. package/dist/renderer/pipeline.js +66 -0
  67. package/dist/renderer/progress-chatter.js +23 -17
  68. package/dist/renderer/rendering-mode.js +34 -0
  69. package/dist/renderer/routing-line.js +14 -0
  70. package/dist/renderer/sanitize.js +12 -0
  71. package/dist/renderer/severity.js +2 -7
  72. package/dist/renderer/startup-lines.js +152 -0
  73. package/dist/renderer/status-footer.js +52 -34
  74. package/dist/renderer/steering-echo.js +41 -0
  75. package/dist/renderer/syntax.js +31 -12
  76. package/dist/renderer/tables.js +5 -1
  77. package/dist/renderer/theme.js +44 -0
  78. package/dist/renderer/thinking-heartbeat.js +33 -2
  79. package/dist/renderer/todo-block.js +10 -49
  80. package/dist/renderer/tool-labels.js +238 -0
  81. package/dist/renderer/tool-widget.js +245 -51
  82. package/dist/renderer/trace.js +42 -0
  83. package/dist/renderer/transcript-flow.js +132 -0
  84. package/dist/renderer/tty.js +34 -1
  85. package/dist/renderer/ui-width.js +42 -0
  86. package/dist/renderer/verify-chain.js +4 -2
  87. package/dist/renderer/widget-fallback.js +58 -65
  88. package/dist/repl/bracketed-paste.js +7 -1
  89. package/dist/repl/builtin-commands.js +19 -8
  90. package/dist/repl/credential-handover-gate.js +28 -0
  91. package/dist/repl/early-line-buffer.js +5 -2
  92. package/dist/repl/file-picker.js +54 -12
  93. package/dist/repl/ink-stdin-guard.js +66 -0
  94. package/dist/repl/inquirer-guard.js +59 -16
  95. package/dist/repl/inquirer-theme.js +27 -33
  96. package/dist/repl/paste-marker.js +19 -0
  97. package/dist/repl/plan-turn-end.js +20 -0
  98. package/dist/repl/post-turn-status.js +36 -11
  99. package/dist/repl/reroute-turn.js +21 -0
  100. package/dist/repl/reset-tty-stdin.js +44 -0
  101. package/dist/repl/restore-guard.js +22 -0
  102. package/dist/repl/resume-standalone.js +63 -0
  103. package/dist/repl/rule8-detector.js +11 -3
  104. package/dist/repl/safety-confirm.js +161 -93
  105. package/dist/repl/session-spend-line.js +8 -4
  106. package/dist/repl/themed-prompts.js +19 -0
  107. package/dist/repl/ui-look-command.js +75 -0
  108. package/dist/repl/update-method-preview-hook.js +48 -5
  109. package/dist/repl.js +795 -332
  110. package/dist/rewind/cli.js +3 -2
  111. package/dist/rewind/format.js +14 -9
  112. package/dist/rewind/restore.js +11 -0
  113. package/dist/router/classifier.js +47 -4
  114. package/dist/router/intent-extractor.js +24 -8
  115. package/dist/router/piped-routing.js +23 -0
  116. package/dist/router/resume-routing.js +16 -0
  117. package/dist/router/routing-failure.js +57 -0
  118. package/dist/sap/first-run-choice.js +1 -1
  119. package/dist/sap/onboarding.js +3 -2
  120. package/dist/sap/standalone-onboarding.js +1 -1
  121. package/dist/sap/system-info.js +4 -2
  122. package/dist/sap/unreachable.js +28 -0
  123. package/dist/sap-errors/clean-error-text.js +182 -0
  124. package/dist/session/interrupt-reason.js +39 -0
  125. package/dist/session/recap.js +36 -24
  126. package/dist/session/resume.js +104 -36
  127. package/dist/session/store.js +69 -27
  128. package/dist/session/user-prompt.js +21 -0
  129. package/dist/skill-catalog.js +7 -0
  130. package/dist/skills/bundled-skills.js +76 -69
  131. package/dist/standards/standards-init.js +1 -1
  132. package/dist/test-helpers/answer-any.js +46 -0
  133. package/dist/tools/approval.js +27 -11
  134. package/dist/tools/ask-question.js +47 -5
  135. package/dist/tools/filesystem/file-write.js +19 -2
  136. package/dist/tools/fiori/fe-extend.js +16 -1
  137. package/dist/tools/fiori/fe-scaffold.js +105 -3
  138. package/dist/tools/fiori/html-escapes.js +28 -0
  139. package/dist/tools/fiori/metadata/audit.js +605 -0
  140. package/dist/tools/fiori/metadata/references.js +104 -0
  141. package/dist/tools/fiori/metadata/types.js +8 -0
  142. package/dist/tools/fiori/preview/app-guard.js +215 -0
  143. package/dist/tools/fiori/preview/env.js +193 -0
  144. package/dist/tools/fiori/preview/readiness.js +127 -0
  145. package/dist/tools/fiori/preview/registry.js +412 -0
  146. package/dist/tools/fiori/preview/start.js +469 -0
  147. package/dist/tools/fiori/samples/loader.js +20 -5
  148. package/dist/tools/fiori/scaffold.js +18 -6
  149. package/dist/tools/fiori/smoke/app-driver-page.js +372 -0
  150. package/dist/tools/fiori/smoke/app-driver.js +100 -0
  151. package/dist/tools/fiori/smoke/assertions.js +161 -24
  152. package/dist/tools/fiori/smoke/audit-columns.js +21 -0
  153. package/dist/tools/fiori/smoke/batch.js +273 -0
  154. package/dist/tools/fiori/smoke/draft-safety.js +241 -0
  155. package/dist/tools/fiori/smoke/draft-smoke.js +473 -0
  156. package/dist/tools/fiori/smoke/driver.js +171 -4
  157. package/dist/tools/fiori/smoke/evidence.js +35 -0
  158. package/dist/tools/fiori/smoke/labels-codes.js +207 -0
  159. package/dist/tools/fiori/smoke/nav-error.js +26 -0
  160. package/dist/tools/fiori/smoke/run-smoke.js +155 -12
  161. package/dist/tools/fiori/tools.js +651 -7
  162. package/dist/tools/fiori/ui5-version.js +16 -0
  163. package/dist/tools/index.js +8 -0
  164. package/dist/tools/local-build.js +6 -0
  165. package/dist/tools/sap-read.js +16 -1
  166. package/dist/tools/sap-write.js +122 -10
  167. package/dist/tools/shell/shell_exec.js +30 -12
  168. package/dist/tools/syntax-state.js +67 -0
  169. package/dist/tools/todo.js +23 -18
  170. package/dist/tools/transport-fit.js +462 -0
  171. package/dist/tools/transport-resolution.js +3 -1
  172. package/dist/tools/transport.js +33 -7
  173. package/dist/tools/verify.js +184 -8
  174. package/dist/ui/app.js +284 -146
  175. package/dist/ui/approval-modal.js +41 -15
  176. package/dist/ui/ask-answer-text.js +17 -0
  177. package/dist/ui/body.js +25 -6
  178. package/dist/ui/card-slot.js +98 -0
  179. package/dist/ui/checklist.js +9 -0
  180. package/dist/ui/coaching-picker-classic.js +8 -5
  181. package/dist/ui/command-palette.js +6 -4
  182. package/dist/ui/confirm-request.js +18 -0
  183. package/dist/ui/context-grid.js +2 -1
  184. package/dist/ui/file-palette.js +6 -4
  185. package/dist/ui/files-card-emitter.js +19 -0
  186. package/dist/ui/footer-line.js +16 -0
  187. package/dist/ui/footer.js +105 -104
  188. package/dist/ui/input-wrap.js +122 -0
  189. package/dist/ui/interrupt.js +103 -0
  190. package/dist/ui/key-burst.js +117 -0
  191. package/dist/ui/line-resolution.js +4 -3
  192. package/dist/ui/live-area.js +71 -0
  193. package/dist/ui/login-banner.js +54 -43
  194. package/dist/ui/rewind-panel.js +76 -24
  195. package/dist/ui/role-props.js +47 -0
  196. package/dist/ui/sap-state-store.js +1 -0
  197. package/dist/ui/session-timeline.js +49 -3
  198. package/dist/ui/skill-picker.js +5 -47
  199. package/dist/ui/text-input.js +128 -24
  200. package/dist/ui/todo-emitter.js +19 -0
  201. package/dist/ui/turn-status-emitter.js +28 -2
  202. package/dist/ui/turn-status.js +16 -19
  203. package/dist/ui/use-card-request.js +44 -0
  204. package/dist/ui/widgets/ask-card.js +521 -0
  205. package/dist/ui/widgets/bar-chart.js +11 -5
  206. package/dist/ui/widgets/coaching-picker.js +50 -55
  207. package/dist/ui/widgets/confirm-card.js +193 -0
  208. package/dist/ui/widgets/dep-graph.js +3 -2
  209. package/dist/ui/widgets/diff-viewer.js +6 -5
  210. package/dist/ui/widgets/files-card.js +97 -0
  211. package/dist/ui/widgets/question-card.js +2 -1
  212. package/dist/ui/widgets/reclaim-raw-stdin.js +24 -0
  213. package/dist/ui/widgets/shortcuts-sheet.js +25 -0
  214. package/dist/ui/widgets/stack-frames.js +2 -1
  215. package/dist/upgrade-check.js +39 -8
  216. package/package.json +11 -2
  217. package/test-harness/fe-draft/app.ts +188 -0
  218. package/test-harness/fe-draft/fe-draft.e2e.test.ts +461 -0
  219. package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +51 -0
  220. package/test-harness/fe-draft/routes.ts +224 -0
  221. package/test-harness/fe-draft/routes.unit.test.ts +45 -0
  222. package/test-harness/fe-draft/server.ts +225 -0
  223. package/test-harness/fe-draft/variants.ts +196 -0
  224. package/vitest.smoke-e2e.config.ts +20 -0
@@ -1,41 +1,35 @@
1
1
  /**
2
- * Shared @inquirer/prompts theme. Centralises CSPeach's color discipline
3
- * so any prompt — select, input, confirm, password, checkbox — looks the
4
- * same and stays in line with the surrounding terminal aesthetic.
2
+ * Shared @inquirer/prompts theme (piece 2 §10) — built from the theme roles
3
+ * and the glyph set, so every wizard (system picker, resume picker, first-run,
4
+ * onboarding, standalone profile, standards init, and the classic pickers)
5
+ * looks like the rest of CSPeach on dark AND light terminals and under
6
+ * NO_COLOR. The answered line reads "✓ <message> <answer>".
5
7
  *
6
- * Why this exists: by default @inquirer/prompts renders the chosen
7
- * answer in cyan. CC-style discipline says color should encode meaning
8
- * (success / error / brand) — a cyan answer line on every prompt
9
- * trains the user to treat color as decoration. Override the answer
10
- * style to default white so the only colored thing on a finished
11
- * prompt is the leading green ✔ check.
12
- *
13
- * 2026-05-01 (CC-parity color audit): introduced as part of the cuts
14
- * after screenshot review showed 9 distinct hues on a single screen.
15
- * Goal is to land at ~5: default, dim, brand peach, green outcome,
16
- * red error.
8
+ * `prefix` and `icon` are GETTERS: inquirer merges the theme when a prompt
9
+ * starts, so chalk level (NO_COLOR) and the Unicode/ASCII glyph choice are
10
+ * read at that moment, not at import time. `answer` is plain text: the old
11
+ * chalk.white was invisible on a light terminal.
17
12
  */
18
13
  import chalk from 'chalk';
19
- /** The shared theme. Pass as `theme` to any @inquirer/prompts call. */
14
+ import { paint } from '../renderer/theme.js';
15
+ import { getGlyphs } from '../renderer/glyphs.js';
20
16
  export const inquirerTheme = {
17
+ get prefix() {
18
+ return { idle: paint('sap', '?'), done: paint('ok', getGlyphs().done) };
19
+ },
20
+ get icon() {
21
+ return { cursor: getGlyphs().cursor };
22
+ },
21
23
  style: {
22
- /**
23
- * The selected answer printed back after Enter — by default cyan,
24
- * here default-color. The leading ✔ (provided by inquirer) stays
25
- * green to encode "done", which is enough signal on its own.
26
- */
27
- answer: (s) => chalk.white(s),
28
- /**
29
- * Help / hint text shown next to the prompt question. Default
30
- * inquirer hint is dim — keep it dim, just be explicit so future
31
- * inquirer version bumps don't regress us.
32
- */
33
- help: (s) => chalk.dim(s),
34
- /**
35
- * Highlighted (cursor-on) choice in select / checkbox. Default is
36
- * cyan. Use a subtle inverse instead — the cursor `❯` glyph is
37
- * the primary signal anyway.
38
- */
39
- highlight: (s) => chalk.bold(s),
24
+ answer: (s) => s,
25
+ // Round 6 item 4a — once answered, only the message's first line: a
26
+ // multi-line message (the resume picker's column header) glued the
27
+ // header to the answer on the "✓" line.
28
+ message: (s, status) => chalk.bold(status === 'done' ? (s.split('\n')[0] ?? s) : s),
29
+ help: (s) => paint('dim', s),
30
+ highlight: (s) => paint('sap', chalk.bold(s)),
31
+ description: (s) => paint('dim', s),
32
+ error: (s) => paint('bad', `${getGlyphs().failed} ${s}`),
33
+ defaultAnswer: (s) => paint('dim', `(${s})`),
40
34
  },
41
35
  };
@@ -0,0 +1,19 @@
1
+ // src/repl/paste-marker.ts
2
+ //
3
+ // Piece 2 (spec §8) — "pasted text never triggers the shortcuts sheet". The
4
+ // Ink paste guard (ink-stdin-guard.ts / bracketed-paste.ts) strips the paste
5
+ // markers before Ink parses anything, so a pasted '?' is otherwise identical
6
+ // to a typed one. The decoder marks every paste span it re-emits; App's `?`
7
+ // trigger asks wasPasted() before opening the sheet.
8
+ /** A paste chunk reaches useInput within one drain — 500 ms is ample and far below human retyping. */
9
+ export const PASTE_MARK_TTL_MS = 500;
10
+ let last = null;
11
+ export function markPastedChunk(text, now = Date.now()) {
12
+ last = { text, at: now };
13
+ }
14
+ export function wasPasted(input, now = Date.now()) {
15
+ return last !== null && last.text === input && now - last.at <= PASTE_MARK_TTL_MS;
16
+ }
17
+ export function resetPasteMarkForTest() {
18
+ last = null;
19
+ }
@@ -0,0 +1,20 @@
1
+ import { checklistRenderMode, formatPlanClose, isPlanFinished } from '../renderer/checklist-format.js';
2
+ import { formatTodoChecklistBlock } from '../renderer/todo-block.js';
3
+ import { emitTrace } from '../renderer/trace.js';
4
+ import { todoEmitter, consumeTodoSetThisTurn } from '../ui/todo-emitter.js';
5
+ export function emitPlanTurnEnd(session, write = (s) => { process.stdout.write(s); }) {
6
+ if (!consumeTodoSetThisTurn())
7
+ return;
8
+ const todos = session.todos ?? [];
9
+ if (todos.length === 0)
10
+ return;
11
+ const mode = checklistRenderMode();
12
+ if (mode === 'live') {
13
+ emitTrace(formatPlanClose(todos));
14
+ if (isPlanFinished(todos))
15
+ todoEmitter.emit('close');
16
+ return;
17
+ }
18
+ if (mode === 'turn-end')
19
+ write(formatTodoChecklistBlock(todos));
20
+ }
@@ -24,6 +24,7 @@ import { globalCredits } from '../cost/credits.js';
24
24
  import { printStatusFooter, formatStatusFooter } from '../renderer/status-footer.js';
25
25
  import { shouldUseInk } from '../renderer/tty.js';
26
26
  import { isUserPrompt } from '../session/user-prompt.js';
27
+ import { emitPlanTurnEnd } from './plan-turn-end.js';
27
28
  export function snapshotTurnStart(session) {
28
29
  return {
29
30
  input: session.usage.input_tokens,
@@ -33,22 +34,34 @@ export function snapshotTurnStart(session) {
33
34
  startedAt: Date.now(),
34
35
  };
35
36
  }
36
- export function pushPostTurnStatus(params) {
37
- const { session, selectedAlias, snapshot, chunkEmitter, turnModel } = params;
37
+ /**
38
+ * The per-turn numbers every receipt prints — extracted (piece 2 I3a) so the
39
+ * REPL receipt and the one-shot receipt compute them the same way.
40
+ */
41
+ export function computeTurnFacts(session, snapshot, turnModel) {
38
42
  const userTurns = session.messages.filter(isUserPrompt).length;
39
- const lifetimeTokensAfter = session.usage.input_tokens + session.usage.output_tokens;
40
- const lifetimeTokensBefore = snapshot.input + snapshot.output;
41
- const turnTokens = Math.max(0, lifetimeTokensAfter - lifetimeTokensBefore);
42
- const turnCostTokens = {
43
+ const lifetimeAfter = session.usage.input_tokens + session.usage.output_tokens;
44
+ const turnTokens = Math.max(0, lifetimeAfter - (snapshot.input + snapshot.output));
45
+ const turnCost = computeCost(turnModel ?? session.model, {
43
46
  input: session.usage.input_tokens - snapshot.input,
44
47
  output: session.usage.output_tokens - snapshot.output,
45
48
  cacheRead: session.usage.cache_read_input_tokens - snapshot.cacheRead,
46
49
  cacheCreate: (session.usage.cache_creation_input_tokens ?? 0) - snapshot.cacheCreate,
50
+ });
51
+ return {
52
+ userTurns,
53
+ turnTokens,
54
+ turnCost,
55
+ // F-L2-2: cost/session-cost.ts so the agent loop can push the SAME total mid-stream.
56
+ sessionCost: computeSessionCost(session),
57
+ elapsedMs: Date.now() - snapshot.startedAt,
47
58
  };
48
- const turnCost = computeCost(turnModel ?? session.model, turnCostTokens);
49
- // F-L2-2: extracted to cost/session-cost.ts so the agent loop can push the
50
- // SAME total mid-stream. This call site is unchanged in behaviour.
51
- const sessionCost = computeSessionCost(session);
59
+ }
60
+ export function pushPostTurnStatus(params) {
61
+ const { session, selectedAlias, snapshot, chunkEmitter, turnModel, cancelled } = params;
62
+ // Piece 2 (spec §4) — the plan's turn-end line lands just above the receipt.
63
+ emitPlanTurnEnd(session);
64
+ const { userTurns, turnTokens, turnCost, sessionCost, elapsedMs } = computeTurnFacts(session, snapshot, turnModel);
52
65
  // Task 9 (ux-wave1) — statusline spend wire. Honest semantics note: the
53
66
  // store field is named `dollarSpendToday` (legacy sidebar naming, kept to
54
67
  // avoid rename churn) but the value written is SESSION-scoped — the same
@@ -62,6 +75,10 @@ export function pushPostTurnStatus(params) {
62
75
  // stays as the authoritative end-of-turn settle (the loop's pushes are
63
76
  // gated to REPL turns; this one runs on every path that reaches here).
64
77
  globalStore.update({ dollarSpendToday: sessionCost });
78
+ // Round 6 item 4d — a cancelled turn that used no tokens has nothing to
79
+ // report ("/abap-explain · 0 tokens · <$0.01" read like a charge).
80
+ if (cancelled && turnTokens === 0)
81
+ return;
65
82
  const statusPayload = {
66
83
  sessionId: session.id,
67
84
  sapAlias: selectedAlias,
@@ -69,7 +86,7 @@ export function pushPostTurnStatus(params) {
69
86
  turnCount: userTurns,
70
87
  tokens: turnTokens,
71
88
  cachedTokens: session.usage.cache_read_input_tokens,
72
- elapsedMs: Date.now() - snapshot.startedAt,
89
+ elapsedMs: elapsedMs,
73
90
  costThisTurn: turnCost,
74
91
  costSession: sessionCost,
75
92
  sessionInputTokens: session.usage.input_tokens,
@@ -95,3 +112,11 @@ export function pushPostTurnStatus(params) {
95
112
  printStatusFooter(statusPayload);
96
113
  }
97
114
  }
115
+ /**
116
+ * Round 7 item 8c — seed the footer's live spend from a resumed session's own
117
+ * usage, so it does not read $0.00 until the first turn and then jump to the
118
+ * session total. A new session has no usage and stays at $0.00.
119
+ */
120
+ export function seedFooterSpend(session) {
121
+ globalStore.update({ dollarSpendToday: computeSessionCost(session) });
122
+ }
@@ -0,0 +1,21 @@
1
+ // src/repl/reroute-turn.ts
2
+ //
3
+ // The /reroute turn wrapper (final review, pre-existing gap). Both /reroute
4
+ // handlers in repl.tsx (Ink + classic) run a turn outside the normal turn
5
+ // path, and had no catch: the Ink loop runs `void handleSubmit(line)`, so a
6
+ // provider error during /reroute became an unhandled rejection and crashed the
7
+ // REPL. This wrapper renders the error with the same turn-error block the
8
+ // normal turn paths use, then always runs the caller's turn-end step.
9
+ import { renderTurnError } from '../agent/turn-error-ux.js';
10
+ /** Never rejects because of the turn: a turn error is rendered, not thrown. */
11
+ export async function runRerouteTurn(p) {
12
+ try {
13
+ await p.run();
14
+ }
15
+ catch (err) {
16
+ await renderTurnError({ err, session: p.session, userInterrupted: p.signal?.aborted ?? false, inSession: true, print: p.print });
17
+ }
18
+ finally {
19
+ p.onFinally();
20
+ }
21
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * resetTtyStdin — revive Windows console stdin after an @inquirer prompt.
3
+ *
4
+ * Live fix 2026-09-26: after the `cspeach --resume` session picker (an
5
+ * @inquirer/prompts `select`) closes, Windows console stdin delivers NO data
6
+ * to the Ink pipe even though the stream reports flowing:true / raw:true, so
7
+ * the Ink input box receives no keystrokes. pty experiments showed that
8
+ * `resume()`, `setRawMode(true)+resume()`, a 300 ms wait and
9
+ * pause/setImmediate/resume all fail; ONLY the full cycle
10
+ * `setRawMode(false) → pause() → setRawMode(true) → resume()` restores input.
11
+ *
12
+ * Called in runRepl immediately before `process.stdin.pipe(inkStdin)`, which
13
+ * runs after every pre-Ink inquirer prompt (resume picker, SAP system picker,
14
+ * API-key prompt, first-run choice, onboarding). On a plain start it is
15
+ * harmless: stdin ends raw + flowing and the pipe attaches synchronously
16
+ * right after, then Ink manages raw mode itself.
17
+ *
18
+ * Returns true when the cycle ran. Never throws.
19
+ */
20
+ export function resetTtyStdin(stdin = process.stdin) {
21
+ if (stdin.isTTY !== true || typeof stdin.setRawMode !== 'function')
22
+ return false;
23
+ try {
24
+ stdin.setRawMode(false);
25
+ stdin.pause();
26
+ stdin.setRawMode(true);
27
+ stdin.resume();
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ /**
35
+ * Review I2 (2026-09-27): run the reset cycle, then pipe stdin into the REPL
36
+ * input. Used by BOTH runRepl branches — Ink (`inkStdin`) and classic
37
+ * (`pasteInput`) — because both are reached through the same pre-REPL
38
+ * inquirer prompts. The pipe happens even when the reset is a no-op
39
+ * (non-TTY / no setRawMode).
40
+ */
41
+ export function pipeRevivedStdin(dest, stdin = process.stdin) {
42
+ resetTtyStdin(stdin);
43
+ return stdin.pipe(dest);
44
+ }
@@ -0,0 +1,22 @@
1
+ export function createRestoreGuard(hooks) {
2
+ let active = false;
3
+ let exitAfter = false;
4
+ return {
5
+ get active() { return active; },
6
+ requestExit() { exitAfter = true; },
7
+ async run(restore) {
8
+ active = true;
9
+ try {
10
+ await restore();
11
+ }
12
+ finally {
13
+ active = false;
14
+ hooks.resetDispatchState();
15
+ if (exitAfter) {
16
+ exitAfter = false;
17
+ hooks.exit();
18
+ }
19
+ }
20
+ },
21
+ };
22
+ }
@@ -0,0 +1,63 @@
1
+ import { STANDALONE_ALIAS } from '../tools/index.js';
2
+ import { formatInfo } from '../renderer/notice-shape.js';
3
+ /**
4
+ * Round 6 item 2 + review I3 — standalone or not, for a start that may be a
5
+ * resume. `--no-sap` (or persisted standalone) always wins. A resumed session
6
+ * keeps its system — unless an explicit `--sap-alias` names a DIFFERENT one:
7
+ * then the flag wins and the session moves to it (standalone → SAP, or one
8
+ * SAP system to another). The same alias is a no-op.
9
+ */
10
+ export function resumeStandaloneDecision(p) {
11
+ if (p.requestedStandalone) {
12
+ // Final batch 4 — standalone requested for a resumed SAP session: the
13
+ // session itself moves to standalone (consistently, and it says so).
14
+ return p.resumedSapSystem && p.resumedSapSystem !== STANDALONE_ALIAS
15
+ ? { standalone: true, switchedFrom: p.resumedSapSystem, switchedTo: STANDALONE_ALIAS, standaloneSource: p.standaloneSource ?? '--no-sap' }
16
+ : { standalone: true };
17
+ }
18
+ if (p.resumedSapSystem === STANDALONE_ALIAS) {
19
+ return p.sapAlias
20
+ ? { standalone: false, switchedFrom: STANDALONE_ALIAS, switchedTo: p.sapAlias }
21
+ : { standalone: true };
22
+ }
23
+ if (p.resumedSapSystem && p.sapAlias && p.sapAlias !== p.resumedSapSystem) {
24
+ return { standalone: false, switchedFrom: p.resumedSapSystem, switchedTo: p.sapAlias };
25
+ }
26
+ return { standalone: false };
27
+ }
28
+ /**
29
+ * Apply a switch decided above: the session moves to the selected system, is
30
+ * saved, and one line says so. No switch → nothing happens.
31
+ */
32
+ export async function applySystemSwitch(p) {
33
+ const { decision } = p;
34
+ if (!decision.switchedTo)
35
+ return;
36
+ // Re-review (defence in depth) — never relabel a session whose SAP changes
37
+ // are unfinished: they belong to the system they were made on.
38
+ if ((p.session.toolCalls ?? []).some((c) => !c.completed))
39
+ return;
40
+ const from = decision.switchedFrom === STANDALONE_ALIAS ? 'standalone' : `on ${decision.switchedFrom}`;
41
+ if (decision.switchedTo === STANDALONE_ALIAS) {
42
+ // Final batch 4 — a SAP session resumed with --no-sap.
43
+ if (!decision.standalone)
44
+ return;
45
+ p.session.sap_system = STANDALONE_ALIAS;
46
+ p.session.systemSwitchNote =
47
+ `This session was ${from} until now. It is now standalone (no SAP system connected). `
48
+ + 'Do not read from or write to SAP; work from pasted code and hand changes over for manual implementation.';
49
+ await p.save(p.session);
50
+ p.notify(formatInfo(`This session was ${from} · now standalone (${decision.standaloneSource ?? '--no-sap'})`));
51
+ return;
52
+ }
53
+ if (decision.standalone)
54
+ return;
55
+ p.session.sap_system = p.selectedAlias;
56
+ // Round 6 review I4 — the model is told once, on the next turn.
57
+ p.session.systemSwitchNote =
58
+ `This session was ${decision.switchedFrom === STANDALONE_ALIAS ? 'standalone' : `on ${decision.switchedFrom}`} until now. `
59
+ + `It is now connected to ${p.selectedAlias}. Validate packages, transports and objects against the system; `
60
+ + 'standalone waivers no longer apply.';
61
+ await p.save(p.session);
62
+ p.notify(formatInfo(`This session was ${from} · now on ${p.selectedAlias} (--sap-alias)`));
63
+ }
@@ -76,12 +76,12 @@ export function renderPlanSummary(proposedNext) {
76
76
  const lines = [];
77
77
  lines.push(`Batch in progress (${writeOpsThisTurn.length + 1} write ops this turn):`);
78
78
  writeOpsThisTurn.forEach((op, i) => {
79
- lines.push(` ${i + 1}. ${op.tool} ${summarizeArgs(op.args)}`);
79
+ lines.push(` ${i + 1}. ${op.tool} ${summarizeArgs(op.args, op.tool)}`);
80
80
  });
81
- lines.push(` ${writeOpsThisTurn.length + 1}. ${proposedNext.tool} ${summarizeArgs(proposedNext.args)} ← about to dispatch`);
81
+ lines.push(` ${writeOpsThisTurn.length + 1}. ${proposedNext.tool} ${summarizeArgs(proposedNext.args, proposedNext.tool)} ← about to dispatch`);
82
82
  return lines.join('\n');
83
83
  }
84
- function summarizeArgs(args) {
84
+ function summarizeArgs(args, tool) {
85
85
  // Best-effort one-line summary. Pulls the most useful identifier fields so a
86
86
  // customer reviewing the batch gate sees WHAT each op targets (Forge Rule 8
87
87
  // is unauditable theatre otherwise):
@@ -90,6 +90,14 @@ function summarizeArgs(args) {
90
90
  // - SAP object writes → type+name, class_name->method_name, transport
91
91
  const a = args;
92
92
  const parts = [];
93
+ // L1-6 R1-6: a preview start hands the stored SAP login to a local process.
94
+ // Say so in plain words — the approver must see it, not only the model.
95
+ if (tool === 'fiori_preview' && (a['action'] === undefined || a['action'] === 'start')) {
96
+ const alias = typeof a['alias'] === 'string' && a['alias'] ? a['alias'] : 'the session alias';
97
+ const dir = typeof a['appDir'] === 'string' && a['appDir'] ? `start ${a['appDir']} — ` : 'start — ';
98
+ return (`${dir}the SAP login for ${alias} is passed to the preview process through its environment ` +
99
+ `when the app's ui5 config names a backend (an app with no backend url starts without it)`);
100
+ }
93
101
  // Local-FS writes (the fiori-build path: 13× file_write + 2× shell_exec) —
94
102
  // the path / command is the only meaningful descriptor; surface it FIRST.
95
103
  if (typeof a['path'] === 'string' && a['path']) {