@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.
Files changed (85) hide show
  1. package/dist/agent/chat-prompt.js +4 -1
  2. package/dist/agent/conversation-prompt.js +30 -13
  3. package/dist/agent/inline-skill-block.js +341 -0
  4. package/dist/agent/loop.js +208 -41
  5. package/dist/agent/provider-shape.js +8 -4
  6. package/dist/agent/providers/ai-hub-provider.js +2 -2
  7. package/dist/agent/providers/azure-provider.js +2 -2
  8. package/dist/agent/providers/byok-provider.js +2 -2
  9. package/dist/agent/providers/local-provider.js +2 -2
  10. package/dist/agent/repair-partial.js +12 -0
  11. package/dist/agent/skill-block-key.js +134 -0
  12. package/dist/agent/steering-queue.js +50 -14
  13. package/dist/agent/system-prompt.js +22 -0
  14. package/dist/agent/tool-dispatch.js +19 -7
  15. package/dist/agent/turn-watchdog.js +1 -1
  16. package/dist/approvals/advisory-render.js +6 -3
  17. package/dist/approvals/approval-prompt.js +9 -4
  18. package/dist/approvals/op-labels.js +14 -0
  19. package/dist/approvals/plan-grant.js +8 -0
  20. package/dist/approvals/render.js +19 -8
  21. package/dist/approvals/risk-floor.js +32 -4
  22. package/dist/approvals/visible-changes.js +50 -0
  23. package/dist/cli.js +10 -2
  24. package/dist/commands/compact.js +6 -1
  25. package/dist/commands/config-set.js +13 -0
  26. package/dist/commands/config-show.js +16 -4
  27. package/dist/commands/export-conversation.js +7 -4
  28. package/dist/commands/finish-check-guard.js +124 -0
  29. package/dist/commands/plan-reground.js +27 -4
  30. package/dist/commands/plan-resume.js +111 -1
  31. package/dist/config/loader.js +5 -1
  32. package/dist/cost/cost-log.js +6 -0
  33. package/dist/cost/system-block.js +6 -0
  34. package/dist/models/server-config.js +1 -1
  35. package/dist/projects/save-command.js +5 -1
  36. package/dist/projects/yes-no.js +5 -1
  37. package/dist/renderer/change-card.js +3 -1
  38. package/dist/renderer/steering-echo.js +53 -8
  39. package/dist/renderer/tool-labels.js +3 -1
  40. package/dist/repl/chat-lane.js +5 -2
  41. package/dist/repl/inline-mode.js +51 -0
  42. package/dist/repl/leftover.js +90 -0
  43. package/dist/repl/reroute-turn.js +7 -1
  44. package/dist/repl/turn-stop.js +64 -0
  45. package/dist/repl.js +144 -22
  46. package/dist/router/classifier.js +1 -1
  47. package/dist/sap/reachability-state.js +7 -0
  48. package/dist/sap/unreachable.js +2 -2
  49. package/dist/session/queued-step.js +3 -1
  50. package/dist/session/user-prompt.js +10 -1
  51. package/dist/skills/manifest-client.js +3 -2
  52. package/dist/skills/preamble.js +6 -4
  53. package/dist/skills/source-authed.js +3 -2
  54. package/dist/tools/approval.js +37 -7
  55. package/dist/tools/dispatch-skill.js +1 -1
  56. package/dist/tools/hand-off.js +49 -7
  57. package/dist/tools/lane/app-lane-resolve.js +9 -0
  58. package/dist/tools/method-only-change.js +241 -0
  59. package/dist/tools/rap/skeleton-resolve.js +4 -0
  60. package/dist/tools/sap-write.js +78 -3
  61. package/dist/tools/snapshot.js +7 -2
  62. package/dist/tools/subagent/agent_run.js +14 -0
  63. package/dist/tools/tool-loading.js +16 -8
  64. package/dist/tools/transport-fit-check.js +70 -0
  65. package/dist/tools/transport.js +17 -8
  66. package/dist/tools/write-pipeline.js +46 -5
  67. package/dist/ui/app.js +112 -13
  68. package/dist/ui/approval-modal.js +11 -6
  69. package/dist/ui/ascii-chars.js +21 -3
  70. package/dist/ui/footer.js +128 -7
  71. package/dist/ui/interrupt.js +35 -1
  72. package/dist/ui/live-area.js +21 -4
  73. package/dist/ui/queue-box-emitter.js +14 -0
  74. package/dist/ui/rewind-panel.js +2 -1
  75. package/dist/ui/session-timeline.js +2 -1
  76. package/dist/ui/text-input.js +31 -2
  77. package/dist/ui/widgets/bar-chart.js +2 -1
  78. package/dist/ui/widgets/confirm-card.js +2 -1
  79. package/dist/ui/widgets/dep-graph.js +2 -1
  80. package/dist/ui/widgets/diff-viewer.js +2 -1
  81. package/dist/ui/widgets/question-card.js +2 -1
  82. package/dist/ui/widgets/shortcuts-sheet.js +2 -2
  83. package/dist/ui/widgets/stack-frames.js +2 -1
  84. package/dist/util/connect-retry.js +65 -0
  85. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
1
  // src/renderer/steering-echo.ts
2
2
  //
3
- // The receipt a mid-turn message leaves in scrollback (spec §7). ONE themed
4
- // helper for both echo paths: App.beginSubmit (ui/app.tsx) and the fallback
5
- // in repl.tsx handleSubmit for callers that bypass beginSubmit.
3
+ // How a mid-turn message is shown (spec §7). LT-15 Feature 1 (D5): typing it
4
+ // leaves no scrollback receipt any more; the App's live queue row shows it
5
+ // while it waits, and the loop prints "↳ read · …" when the model reads it.
6
6
  import { paint } from './theme.js';
7
7
  import { sanitizeControlChars } from './sanitize.js';
8
8
  /** Wave1 fast-follow — only the first line, clipped, with a line count. */
@@ -20,12 +20,57 @@ export function formatSteeringEcho(text) {
20
20
  return out;
21
21
  }
22
22
  /**
23
- * `↳ queued · <first line…>` in the dim role, newline-terminated. The text may
24
- * come from a paste, so control characters are neutralised before it reaches
25
- * scrollback.
23
+ * One waiting or read message as shown: first line, clipped, no control
24
+ * characters, and each tab as one space (a tab is 0 wide to string-width, so
25
+ * Ink's truncate-end would cut too late and the line could wrap).
26
26
  */
27
- export function steeringEchoLine(text) {
28
- return paint('dim', `↳ queued · ${sanitizeControlChars(formatSteeringEcho(text))}`) + '\n';
27
+ function shownSteer(text) {
28
+ return sanitizeControlChars(formatSteeringEcho(text)).replace(/\t/g, ' ');
29
+ }
30
+ /**
31
+ * LT-15 Feature 1 (D5, F7) — the receipt the loop prints when the model reads
32
+ * a waiting message: `↳ read · <first line…>` in the dim role, one line per
33
+ * message (the caller adds the newline). The text may come from a paste, so
34
+ * it is clipped and control characters are neutralised.
35
+ */
36
+ export function steeringReadLine(text) {
37
+ return paint('dim', `↳ read · ${shownSteer(text)}`);
38
+ }
39
+ /** The queue row shows at most this many messages; the rest are counted. */
40
+ export const QUEUE_ROW_MAX_ITEMS = 3;
41
+ /**
42
+ * LT-15 Feature 1 — the live row's lines (plain text, one terminal row each;
43
+ * App paints them dim and truncates to the width). Empty queue → no lines.
44
+ *
45
+ * 4 waiting · use ZPKG
46
+ * · also check the tests… (+1 line)
47
+ * · and the docs
48
+ * + 1 more
49
+ *
50
+ * `maxLines` is the room left in the live area. When the full row does not
51
+ * fit it folds to ONE line, never fewer: `4 waiting · use ZPKG + 3 more`.
52
+ *
53
+ * LT-15 Feature 2 (review I6) — `canEdit` adds `· ↑ to edit` right after the
54
+ * count, only while ↑ would really take the last message back (the App
55
+ * decides). It sits before the messages so a long message never cuts it off.
56
+ * Without it the bytes are exactly the Task 2 ones.
57
+ */
58
+ export function formatQueueRow(items, maxLines = Infinity, opts = {}) {
59
+ if (items.length === 0)
60
+ return [];
61
+ const count = `${items.length} waiting${opts.canEdit ? ' · ↑ to edit' : ''}`;
62
+ const full = Math.min(items.length, QUEUE_ROW_MAX_ITEMS) + (items.length > QUEUE_ROW_MAX_ITEMS ? 1 : 0);
63
+ if (full > Math.max(1, maxLines)) {
64
+ return [`${count} · ${shownSteer(items[0])} + ${items.length - 1} more`];
65
+ }
66
+ const head = `${count} `;
67
+ const pad = ' '.repeat(head.length);
68
+ const shown = items.slice(0, QUEUE_ROW_MAX_ITEMS);
69
+ const lines = shown.map((t, i) => `${i === 0 ? head : pad}· ${shownSteer(t)}`);
70
+ const rest = items.length - shown.length;
71
+ if (rest > 0)
72
+ lines.push(`${pad}+ ${rest} more`);
73
+ return lines;
29
74
  }
30
75
  export function classifyMidTurnLine(text) {
31
76
  const first = text.trim().split(/\s+/)[0] ?? '';
@@ -1,4 +1,5 @@
1
1
  import { spillLabelFor } from '../lib/spill-labels.js';
2
+ import { writtenObjectName } from '../approvals/op-labels.js';
2
3
  const OBJECT_MAX = 60;
3
4
  /**
4
5
  * Every value shown in a label goes through here: ANSI escape sequences and
@@ -108,7 +109,8 @@ const RULES = {
108
109
  },
109
110
  background_run: (a) => ({ verb: 'Started', object: shortRight(commandLine(a)) }),
110
111
  sap_get_source: sapObject('Read'),
111
- sap_set_source: sapObject('Wrote'),
112
+ // LT-11 — an access control, metadata extension or behavior definition names its kind.
113
+ sap_set_source: (a) => ({ verb: 'Wrote', object: shortRight(writtenObjectName(str(a['type']), str(a['name']) ?? '')) }),
112
114
  sap_activate: (a) => ({ verb: 'Activated', object: shortRight(activateObject(a)) }),
113
115
  sap_syntax_check: sapObject('Checked syntax'),
114
116
  sap_atc_run: sapObject('Ran ATC on'),
@@ -113,8 +113,11 @@ export function turnStatusLabel(skill) {
113
113
  }
114
114
  /** D11 — the one dim line shown in place of the echo when the message re-runs through today's router. */
115
115
  export const CHAT_UNAVAILABLE_LINE = "Chat isn't available on this server yet — using skill routing.";
116
- export function chatFallbackOrigin() {
117
- return { by: 'chat_fallback', line: CHAT_UNAVAILABLE_LINE };
116
+ export function chatFallbackOrigin(prose = false) {
117
+ // LT-15 Task 4 fix round 1 — a withdrawn prose replay re-runs as prose.
118
+ return prose
119
+ ? { by: 'chat_fallback', line: CHAT_UNAVAILABLE_LINE, prose: true }
120
+ : { by: 'chat_fallback', line: CHAT_UNAVAILABLE_LINE };
118
121
  }
119
122
  /**
120
123
  * The one place a process lane's tool side effect is applied (D5, §6.1).
@@ -0,0 +1,51 @@
1
+ // cspeach-cli/src/repl/inline-mode.ts
2
+ //
3
+ // Stage 1A, Task 1A.1 — when `assistant.skills = inline` is active.
4
+ //
5
+ // Decided at EVERY submit from the inputs as they are then (rev 4 M4), never
6
+ // cached, never stored in the session file: a resumed session re-decides from
7
+ // today's config and today's proxy. The history is NOT an input (rev 5 S-B):
8
+ // `assistant.skills = system`, `/chat off`, a non-TTY and a non-serving proxy
9
+ // always win. The REPL passes the result to the loop as `ctx.inlineSkills`.
10
+ // Piped stdin and one-shot never get it; reader and agent_run children never
11
+ // carry it (their ctx is built field by field).
12
+ import { servedInlineSkills } from '../models/server-config.js';
13
+ /** True only for `skills = inline` in the chat lane, on a TTY, where the provider can serve it. */
14
+ export function inlineActive({ skills, lane, interactive, served }) {
15
+ return interactive && served && skills === 'inline' && lane === 'chat';
16
+ }
17
+ /**
18
+ * Can this provider serve the inline system block? Managed: only when the
19
+ * proxy's served config says `inline_skills: true` (an old proxy, the kill
20
+ * switch or a stale cache ⇒ false). byok / ai-hub / azure / local build the
21
+ * system block on the laptop ⇒ true. Read at submit time: the served config
22
+ * loads after the config is read at startup.
23
+ */
24
+ export function inlineServed(mode) {
25
+ if (mode === undefined)
26
+ return false;
27
+ return mode === 'managed' ? servedInlineSkills() : true;
28
+ }
29
+ /**
30
+ * The `config show` comment for a stored `inline` that would not be active in
31
+ * an interactive session; null when there is nothing to say.
32
+ */
33
+ export function inlineInactiveReason(i) {
34
+ if (i.skills !== 'inline')
35
+ return null;
36
+ if (i.lane !== 'chat')
37
+ return 'not active: skill routing is on';
38
+ if (!i.served)
39
+ return 'not active: this server does not serve it';
40
+ return null;
41
+ }
42
+ /**
43
+ * Stage 1A, Task 1A.2 (S5) — an interactive chat-lane process: lane `chat` and
44
+ * a TTY, the condition applyLaneTools uses, whatever `assistant.skills` and the
45
+ * proxy say. In such a process the loop neutralises `<skill`, `<playbook` and
46
+ * `<session_context` in what the person sends (ctx.chatLaneProcess). Decided
47
+ * at every turn site like inlineActive.
48
+ */
49
+ export function isChatLaneProcess(lane, interactive) {
50
+ return lane === 'chat' && interactive;
51
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * LT-15 Feature 4 — messages typed while CSPeach worked that are still
3
+ * waiting when a dispatch ends (Ink REPL only; classic, headless and one-shot
4
+ * never queue anything).
5
+ *
6
+ * The runner holds the per-dispatch flags (plan review I4): a Ctrl+C press,
7
+ * a turn error, a scheduled hop. They are set during the dispatch, read ONCE
8
+ * by `settle` at that dispatch's end (handleSubmit's finally, after release,
9
+ * or a restore's end), and reset right there, so a stale flag can never touch
10
+ * the next hop or turn.
11
+ *
12
+ * `settle` acts on leftoverDecision:
13
+ * - run: on the next tick, drain the queue THERE (a message typed
14
+ * between the end and the tick rides along) and submit it once
15
+ * as the person's prose. Nothing when a dispatch is already busy
16
+ * (that turn reads the queue at its round 0, or its own end
17
+ * decides) or the session is ending (I4.1, I4.3).
18
+ * - to-input: ask the App to put the text back into the input box (D1, I5).
19
+ * - wait / report-unsent / none: touch nothing. A queued hop reads the
20
+ * message at its round 0 (D3); the exit reports what is left.
21
+ * Every message ends in exactly one place (I1.1).
22
+ */
23
+ import { drainSteering, drainSteeringText, steeringSnapshot } from '../agent/steering-queue.js';
24
+ import { leftoverDecision, unsentLine } from '../ui/interrupt.js';
25
+ import { formatInfo } from '../renderer/notice-shape.js';
26
+ import { parseShortcut } from '../router/classifier.js';
27
+ export function createLeftoverRunner(deps) {
28
+ let ctrlCStop = false;
29
+ let errorEnd = false;
30
+ let dispatchScheduled = false;
31
+ const reset = () => {
32
+ ctrlCStop = false;
33
+ errorEnd = false;
34
+ dispatchScheduled = false;
35
+ };
36
+ return {
37
+ noteCtrlC() { ctrlCStop = true; },
38
+ noteError() { errorEnd = true; },
39
+ noteDispatchScheduled() { dispatchScheduled = true; },
40
+ reset,
41
+ settle(s) {
42
+ const decision = leftoverDecision({
43
+ done: deps.isDone(),
44
+ exitRequested: s.exitRequested,
45
+ forceExit: s.forceExit === true,
46
+ ctrlCStop,
47
+ braked: s.braked === true,
48
+ errorEnd,
49
+ dispatchScheduled,
50
+ restoreActive: s.restoreActive === true,
51
+ waiting: steeringSnapshot().length,
52
+ });
53
+ reset();
54
+ if (decision === 'run') {
55
+ deps.schedule(() => {
56
+ if (deps.isDone() || deps.isBusy())
57
+ return;
58
+ const text = drainSteeringText();
59
+ if (text !== null)
60
+ deps.submit(text);
61
+ });
62
+ }
63
+ else if (decision === 'to-input') {
64
+ deps.toBox();
65
+ }
66
+ return decision;
67
+ },
68
+ toBoxOrReport() {
69
+ if (steeringSnapshot().length === 0)
70
+ return;
71
+ deps.toBox();
72
+ const left = drainSteering();
73
+ if (left.length > 0)
74
+ deps.print(formatInfo(unsentLine(left.length)));
75
+ },
76
+ };
77
+ }
78
+ /** At exit: drain what still waits and return the one line to print, or null. */
79
+ export function takeUnsentReport() {
80
+ const left = drainSteering();
81
+ return left.length > 0 ? formatInfo(unsentLine(left.length)) : null;
82
+ }
83
+ /**
84
+ * LT-15 F4 — how a submitted line is read. A replayed waiting message is
85
+ * prose: never a slash command or an @-shortcut, so "/sap/bc/adt/x is slow"
86
+ * reaches the model as text. Anything typed at the prompt is read as today.
87
+ */
88
+ export function parseSubmitted(input, prose) {
89
+ return prose ? { prompt: input } : parseShortcut(input);
90
+ }
@@ -13,7 +13,13 @@ export async function runRerouteTurn(p) {
13
13
  await p.run();
14
14
  }
15
15
  catch (err) {
16
- await renderTurnError({ err, session: p.session, userInterrupted: p.signal?.aborted ?? false, inSession: true, print: p.print });
16
+ const stopped = p.signal?.aborted ?? false;
17
+ const line = stopped ? p.stopLine?.() ?? null : null;
18
+ if (line !== null)
19
+ p.print(line);
20
+ else
21
+ await renderTurnError({ err, session: p.session, userInterrupted: stopped, inSession: true, print: p.print });
22
+ p.onError?.();
17
23
  }
18
24
  finally {
19
25
  p.onFinally();
@@ -0,0 +1,64 @@
1
+ /**
2
+ * LT-15 Task 5b — how the current dispatch was stopped (Ink REPL only;
3
+ * classic, headless and one-shot have no Esc brake).
4
+ *
5
+ * Per dispatch (plan review I4): set by the presses, read by autoDispatch
6
+ * (a queued hop / lane plan / switch is not started after ANY stop) and by
7
+ * the leftover runner's settle, then reset in handleSubmit's finally right
8
+ * after that settle, and at a restore's end. Never reset at controller
9
+ * creation, so a stale stop can never touch the next hop or turn.
10
+ *
11
+ * `press` is the Esc brake. It reuses what Ctrl+C already does — the SAME
12
+ * `currentTurnAbort.abort()` (deps.abortTurn) or, in the post-turn window,
13
+ * the SAME pending cancel (deps.cancelPending) — and never touches Ctrl+C's
14
+ * press count (review C1): after an Esc, one Ctrl+C is still Ctrl+C's FIRST
15
+ * press, never the force-exit.
16
+ */
17
+ import { steeringSnapshot } from '../agent/steering-queue.js';
18
+ import { decideEscape, brakeLine, brakeEndLine } from '../ui/interrupt.js';
19
+ export function createTurnStop(deps) {
20
+ let braked = false;
21
+ let ctrlC = false;
22
+ const waiting = deps.waiting ?? (() => steeringSnapshot().length);
23
+ const decide = (ui) => decideEscape({
24
+ ...ui,
25
+ turnActive: deps.turnActive(),
26
+ busy: deps.busy(),
27
+ restoreActive: deps.restoreActive(),
28
+ phase: deps.phase(),
29
+ canChain: deps.canChain(),
30
+ alreadyStopped: braked || ctrlC || deps.exitRequested(),
31
+ });
32
+ const brake = () => {
33
+ braked = true;
34
+ deps.print(brakeLine(waiting() > 0));
35
+ deps.abortTurn();
36
+ };
37
+ return {
38
+ stopFromCard() {
39
+ // The card itself is the overlay that owned the key: decide as if
40
+ // nothing else were open. Only a running turn is stopped.
41
+ if (decide({ overlayOpen: false, bufferCount: 0, fuzzyOpen: false }) !== 'brake')
42
+ return 'none';
43
+ brake();
44
+ return 'brake';
45
+ },
46
+ press(ui) {
47
+ const action = decide(ui);
48
+ if (action === 'brake') {
49
+ brake();
50
+ }
51
+ else if (action === 'cancel-pending') {
52
+ braked = true;
53
+ deps.cancelPending();
54
+ }
55
+ return action;
56
+ },
57
+ noteCtrlCStop() { ctrlC = true; },
58
+ get braked() { return braked; },
59
+ get stopped() { return braked || ctrlC; },
60
+ get brakedOnly() { return braked && !ctrlC; },
61
+ endLine() { return brakeEndLine(waiting() > 0); },
62
+ reset() { braked = false; ctrlC = false; },
63
+ };
64
+ }