@cspeach/cli 1.0.0 → 1.1.0

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 (100) hide show
  1. package/dist/agent/loop.js +22 -9
  2. package/dist/approvals/op-labels.js +124 -0
  3. package/dist/approvals/render.js +42 -36
  4. package/dist/cli.js +15 -0
  5. package/dist/commands/compact.js +28 -2
  6. package/dist/commands/config-set.js +189 -0
  7. package/dist/commands/config-show.js +20 -0
  8. package/dist/commands/export-audit.js +43 -0
  9. package/dist/commands/help.js +5 -0
  10. package/dist/commands/plan-audit-evidence.js +266 -0
  11. package/dist/commands/plan-audit.js +692 -0
  12. package/dist/commands/plan-chain.js +671 -0
  13. package/dist/commands/plan-continue.js +179 -0
  14. package/dist/commands/plan-gate.js +154 -0
  15. package/dist/commands/plan-resume.js +588 -33
  16. package/dist/config/loader.js +128 -4
  17. package/dist/config/model-defaults.js +14 -0
  18. package/dist/cost/pricing.js +27 -1
  19. package/dist/doctor/checks/system-roles.js +41 -0
  20. package/dist/doctor/run.js +2 -0
  21. package/dist/models/resolve.js +61 -0
  22. package/dist/models/server-config.js +155 -0
  23. package/dist/one-shot.js +25 -3
  24. package/dist/projects/extract-cca.js +3 -1
  25. package/dist/projects/extract-modernize.js +3 -1
  26. package/dist/projects/extract-plan.js +60 -6
  27. package/dist/projects/extract-test-coverage.js +3 -1
  28. package/dist/projects/extract-upgrade.js +3 -1
  29. package/dist/projects/handover-md.js +195 -0
  30. package/dist/projects/index.js +1 -1
  31. package/dist/projects/plan-run.js +137 -13
  32. package/dist/projects/plan-schema.js +73 -0
  33. package/dist/projects/run-lease.js +157 -0
  34. package/dist/projects/save-command.js +26 -15
  35. package/dist/renderer/status-footer.js +22 -12
  36. package/dist/renderer/thinking-heartbeat.js +64 -8
  37. package/dist/renderer/todo-block.js +51 -0
  38. package/dist/renderer/tool-widget.js +37 -0
  39. package/dist/repl/bracketed-paste.js +28 -19
  40. package/dist/repl/builtin-commands.js +5 -0
  41. package/dist/repl/current-transport.js +10 -0
  42. package/dist/repl/history.js +86 -0
  43. package/dist/repl/ink-stdin-guard.js +64 -0
  44. package/dist/repl/mode-ceiling.js +16 -0
  45. package/dist/repl/mode-cycle.js +104 -0
  46. package/dist/repl/post-turn-status.js +24 -4
  47. package/dist/repl/slash-completer.js +5 -0
  48. package/dist/repl.js +954 -83
  49. package/dist/rewind/candidates.js +194 -0
  50. package/dist/rewind/cli.js +137 -0
  51. package/dist/rewind/format.js +27 -0
  52. package/dist/rewind/restore.js +245 -0
  53. package/dist/session/audit-export.js +459 -0
  54. package/dist/session/context-report.js +163 -0
  55. package/dist/session/recap.js +160 -0
  56. package/dist/skill-catalog.js +9 -3
  57. package/dist/skills/bundled-skills.js +59 -66
  58. package/dist/tools/approval.js +115 -7
  59. package/dist/tools/ask-question.js +304 -3
  60. package/dist/tools/extend-model/anchored-insert.js +604 -0
  61. package/dist/tools/extend-model/tool.js +162 -10
  62. package/dist/tools/fiori/fe-extend.js +76 -0
  63. package/dist/tools/fiori/fe-scaffold.js +29 -3
  64. package/dist/tools/fiori/floorplan-map.js +19 -0
  65. package/dist/tools/fiori/samples/data/index.json +13602 -0
  66. package/dist/tools/fiori/samples/data/sources.generated.js +808 -0
  67. package/dist/tools/fiori/samples/loader.js +248 -0
  68. package/dist/tools/fiori/samples/search.js +63 -0
  69. package/dist/tools/fiori/samples/types.js +2 -0
  70. package/dist/tools/fiori/smoke/assertions.js +74 -0
  71. package/dist/tools/fiori/smoke/browser.js +52 -0
  72. package/dist/tools/fiori/smoke/driver.js +89 -0
  73. package/dist/tools/fiori/smoke/freestyle-spec.js +317 -0
  74. package/dist/tools/fiori/smoke/run-smoke.js +149 -0
  75. package/dist/tools/fiori/tools.js +328 -3
  76. package/dist/tools/local-build.js +11 -1
  77. package/dist/tools/sap-read.js +79 -11
  78. package/dist/tools/sap-write.js +24 -4
  79. package/dist/tools/snapshot.js +27 -1
  80. package/dist/tools/subagent/agent_run.js +27 -3
  81. package/dist/tools/todo.js +144 -0
  82. package/dist/ui/app.js +372 -19
  83. package/dist/ui/approval-modal.js +49 -16
  84. package/dist/ui/ask-question-emitter.js +14 -0
  85. package/dist/ui/context-grid.js +108 -0
  86. package/dist/ui/footer.js +109 -30
  87. package/dist/ui/header.js +7 -0
  88. package/dist/ui/line-resolution.js +18 -2
  89. package/dist/ui/rewind-emitter.js +10 -0
  90. package/dist/ui/rewind-panel.js +81 -0
  91. package/dist/ui/sap-state-store.js +1 -0
  92. package/dist/ui/status-line.js +43 -0
  93. package/dist/ui/text-input.js +72 -8
  94. package/dist/ui/todo-emitter.js +25 -0
  95. package/dist/ui/todo-panel.js +64 -0
  96. package/dist/ui/turn-status-emitter.js +50 -4
  97. package/dist/ui/turn-status.js +18 -3
  98. package/dist/ui/widgets/ask-form.js +242 -0
  99. package/dist/ui/widgets/ask-question-modal.js +17 -7
  100. package/package.json +4 -1
@@ -28,6 +28,7 @@
28
28
  */
29
29
  import { EventEmitter } from 'node:events';
30
30
  import { registerTool } from '../index.js';
31
+ import { isPlanPhaseActive, notePlanPhaseSubagentDispatch, } from '../../commands/plan-gate.js';
31
32
  import { runTurn } from '../../agent/loop.js';
32
33
  import { collectTurnAssistantText } from '../../agent/turn-assistant-text.js';
33
34
  export const MAX_AGENT_DEPTH = 3;
@@ -98,12 +99,20 @@ export async function agentRunHandler(args, ctx) {
98
99
  // not parent's start time. Avoids surprising file timestamps.
99
100
  started_at: new Date().toISOString(),
100
101
  last_turn_at: new Date().toISOString(),
102
+ // Task 5 (ux-wave2): a fresh subagent has no task list — without this
103
+ // the spread above would leak the PARENT's todos into the child's
104
+ // session file.
105
+ todos: undefined,
101
106
  };
102
107
  // Build an isolated child ctx. Crucial differences vs parent ctx:
103
108
  // - session: the fresh child session above (independent usage counters)
104
109
  // - previewHook: undefined — subagent must NOT auto-approve writes
105
110
  // - pendingDispatch / currentTransport: cleared — subagent doesn't
106
111
  // mutate parent REPL state
112
+ // - todoEmitter: omitted (Task 5) — todo_set emits via
113
+ // ctx.todoEmitter?.emit, so a child's todo_set updates only
114
+ // childSession.todos and never repaints the PARENT's Ctrl+T panel /
115
+ // turn-status strip. Same seam as chunkEmitter suppression.
107
116
  // chunkEmitter is passed via the RunTurn params (not the ctx) as a
108
117
  // null-sink EventEmitter — see I2 fix below.
109
118
  const childCtx = {
@@ -113,7 +122,8 @@ export async function agentRunHandler(args, ctx) {
113
122
  cwd: ctx.cwd,
114
123
  provider: ctx.provider,
115
124
  skillSource: ctx.skillSource,
116
- // previewHook + pendingDispatch + currentTransport intentionally omitted.
125
+ // previewHook + pendingDispatch + currentTransport + todoEmitter
126
+ // intentionally omitted.
117
127
  };
118
128
  // I2 fix: a null-sink EventEmitter suppresses all subagent output. Every
119
129
  // stdout-write site in loop.ts is guarded by `if (params.chunkEmitter)`,
@@ -128,9 +138,23 @@ export async function agentRunHandler(args, ctx) {
128
138
  // mutating tool from the subagent's tool list. Saves tokens on the
129
139
  // sub-agent invocation (smaller tools array = less input cost) AND
130
140
  // hard-prevents accidental writes from the subagent.
131
- const toolFilter = args.read_only
141
+ //
142
+ // Task 7 (agentic-flow, 2026-07-03) — inside a plan phase the filter is
143
+ // FORCED regardless of args.read_only: subagents can read, never write,
144
+ // and the model cannot opt out (read_only:false is ignored). Outside plan
145
+ // phases the opt-in behaviour above is byte-identical to before.
146
+ const planPhase = isPlanPhaseActive();
147
+ const readOnly = planPhase || Boolean(args.read_only);
148
+ const toolFilter = readOnly
132
149
  ? (t) => !t.isMutating
133
150
  : undefined;
151
+ if (planPhase)
152
+ notePlanPhaseSubagentDispatch();
153
+ // Task 7 — per-dispatch announce on the PARENT's emitter (childCtx
154
+ // deliberately omits chunkEmitter, so this is the one line the user sees
155
+ // for each dispatch). 'info' renders dim in both the Ink and classic REPLs.
156
+ const taskPreview = args.task.length > 60 ? `${args.task.slice(0, 60)}…` : args.task;
157
+ ctx.chunkEmitter?.emit('info', `↳ subagent: ${args.skill}${readOnly ? ' — read-only' : ''} — ${taskPreview}`);
134
158
  try {
135
159
  await runTurn({
136
160
  provider: ctx.provider,
@@ -189,7 +213,7 @@ registerTool({
189
213
  },
190
214
  read_only: {
191
215
  type: 'boolean',
192
- description: 'When true, the subagent only sees read-only tools (no sap_set_source, no file-write, no shell_exec, etc.). Use this for research / analysis subagents that should not make changes — e.g. spawning a subagent to read code, run greps, or summarise findings. Smaller tools array = lower per-call cost; also a hard guard against accidental writes.',
216
+ description: 'When true, the subagent only sees read-only tools (no sap_set_source, no file-write, no shell_exec, etc.). Use this for research / analysis subagents that should not make changes — e.g. spawning a subagent to read code, run greps, or summarise findings. Smaller tools array = lower per-call cost; also a hard guard against accidental writes. NOTE: during plan phases the read-only filter is ALWAYS enforced regardless of this argument.',
193
217
  },
194
218
  },
195
219
  required: ['skill', 'task'],
@@ -0,0 +1,144 @@
1
+ /**
2
+ * todo_set — UX Wave 2 / Task 4. CC's TodoWrite adapted for CSPeach: the
3
+ * model maintains a visible task list on long multi-step jobs (RAP stacks,
4
+ * upgrade batches, plan phases). Task 5 renders it (Ctrl+T panel +
5
+ * turn-status strip) via todoEmitter; /compact appends it to the summary
6
+ * block so the plan survives compaction (commands/compact.ts).
7
+ *
8
+ * Semantics: FULL-LIST REPLACE on every call — never a delta. 1..20 items.
9
+ *
10
+ * Transcript render (CC-style, 2026-07): on every successful set the
11
+ * handler also emits the FULL checklist as a scrollback block via
12
+ * ctx.chunkEmitter (renderer/todo-block.ts). The raw ⏺/⎿ todo_set rows are
13
+ * widget-suppressed (renderer/tool-widget.ts) so the block is the sole
14
+ * visible representation — the transcript now shows the whole plan
15
+ * advancing, not a raw `⏺ todo_set({"todos":[…])` JSON line. This is the
16
+ * Claude-Code parity the owner asked for; the turn-status strip (single ▶
17
+ * row) and Ctrl+T panel stay exactly as they were.
18
+ *
19
+ * Session-write route (chosen with evidence, 2026-07-05): the tool mutates
20
+ * `ctx.session.todos` directly. ToolContext.session IS the live
21
+ * SessionState object (tools/index.ts) — the same instance repl.tsx /
22
+ * one-shot.ts construct the ctx from — and agent/loop.ts already calls
23
+ * saveSession(session) end-of-turn (and session/pending.ts saves per
24
+ * mutating-tool WAL entry), so no extra save call is needed here. The
25
+ * emitter-plus-repl-side-subscriber alternative was rejected: it would
26
+ * add a second writer for state the tool already owns, and headless /
27
+ * one-shot paths (no subscriber mounted) would silently lose persistence.
28
+ *
29
+ * Validation discipline: structured error results (is_error: true), never
30
+ * throws — caps at 20 items, rejects empty lists, empty text, unknown
31
+ * status. MULTIPLE in_progress items are a WARNING in the result, not an
32
+ * error: blocking the model mid-job over a presentation nit would stall
33
+ * long workflows, and the list is still coherent and renderable — the
34
+ * warning text steers the model to fix it on its next call instead.
35
+ *
36
+ * isMutating: false — writes CLI-side session state only, never SAP.
37
+ * Headless: identical behaviour (session write + emitter absent/no-op;
38
+ * no prompt involved).
39
+ *
40
+ * Emitter seam (Task 5, 2026-07-05): the panel channel arrives via
41
+ * `ctx.todoEmitter` — NOT a module-level import — mirroring how
42
+ * chunkEmitter is ctx-carried. agent_run's child ctx omits it, so a
43
+ * subagent's todo_set updates only the CHILD session and can never
44
+ * clobber the parent's Ctrl+T panel.
45
+ */
46
+ import { registerTool } from './index.js';
47
+ import { formatTodoChecklistBlock } from '../renderer/todo-block.js';
48
+ /** Hard cap on list length — past this the list stops being a glanceable plan. */
49
+ export const TODO_MAX_ITEMS = 20;
50
+ const VALID_STATUSES = new Set(['pending', 'in_progress', 'completed']);
51
+ function errorResult(message) {
52
+ return { content: `error: ${message}`, is_error: true };
53
+ }
54
+ export async function todoSetHandler(args, ctx) {
55
+ const raw = args?.todos;
56
+ if (!Array.isArray(raw)) {
57
+ return errorResult("todos must be an array of { text, status } items (status: 'pending' | 'in_progress' | 'completed').");
58
+ }
59
+ if (raw.length === 0) {
60
+ return errorResult('todos must contain at least 1 item — this is a full-list replace, never call it with an empty list.');
61
+ }
62
+ if (raw.length > TODO_MAX_ITEMS) {
63
+ return errorResult(`todos exceeds the ${TODO_MAX_ITEMS}-item cap (got ${raw.length}). Collapse completed work or group related steps into one item.`);
64
+ }
65
+ // Validate + normalise BEFORE touching session state — a rejected call
66
+ // must leave the previous list fully intact.
67
+ const todos = [];
68
+ for (let i = 0; i < raw.length; i++) {
69
+ const item = raw[i];
70
+ const text = typeof item?.text === 'string' ? item.text.trim() : '';
71
+ if (!text) {
72
+ return errorResult(`todos[${i}].text must be a non-empty string.`);
73
+ }
74
+ if (typeof item?.status !== 'string' || !VALID_STATUSES.has(item.status)) {
75
+ return errorResult(`todos[${i}].status must be one of 'pending' | 'in_progress' | 'completed' (got ${JSON.stringify(item?.status ?? null)}).`);
76
+ }
77
+ todos.push({ text, status: item.status });
78
+ }
79
+ const inProgress = todos.filter((t) => t.status === 'in_progress');
80
+ // Full-list replace on the LIVE session object — persisted by the loop's
81
+ // existing end-of-turn saveSession (see module doc for the evidence).
82
+ ctx.session.todos = todos;
83
+ // UI fan-out (Task 5) — via the ctx-carried emitter (absent on subagent
84
+ // ctxs → suppressed; absent listener → harmless no-op). Emit a defensive
85
+ // copy so a consumer mutating the payload can never corrupt the
86
+ // persisted session state.
87
+ ctx.todoEmitter?.emit('update', todos.map((t) => ({ ...t })));
88
+ // Transcript render (CC-style, 2026-07): drop the FULL checklist into
89
+ // scrollback on every successful set. The raw ⏺/⎿ todo_set rows are
90
+ // widget-suppressed (renderer/tool-widget.ts), so this block is the sole
91
+ // visible representation — mirroring ask_question. ctx.chunkEmitter routes
92
+ // to Ink Static scrollback AND the classic 'chunk'→stdout path
93
+ // (renderer/notices.ts). Subagent ctxs deliberately omit chunkEmitter
94
+ // (tools/subagent/agent_run.ts), so a child turn's todo_set can never
95
+ // print its checklist into the PARENT transcript — same seam as the
96
+ // todoEmitter suppression above.
97
+ ctx.chunkEmitter?.emit('chunk', formatTodoChecklistBlock(todos));
98
+ const result = {
99
+ count: todos.length,
100
+ in_progress: inProgress[0]?.text ?? null,
101
+ };
102
+ if (inProgress.length > 1) {
103
+ result.warning =
104
+ `${inProgress.length} items are in_progress — exactly one item should be in_progress at a time. ` +
105
+ 'Mark the others pending or completed on your next todo_set call.';
106
+ }
107
+ return { content: JSON.stringify(result) };
108
+ }
109
+ registerTool({
110
+ name: 'todo_set',
111
+ // Model-behavior lever — this exact framing is binding (task-4 brief).
112
+ description: 'Maintain your task list for multi-step work. Call with the FULL updated list whenever you ' +
113
+ 'complete a step, start a new one, or discover new work. Exactly one item should be ' +
114
+ 'in_progress at a time. The user sees this list — keep items short and outcome-shaped.',
115
+ isMutating: false,
116
+ category: 'session',
117
+ input_schema: {
118
+ type: 'object',
119
+ properties: {
120
+ todos: {
121
+ type: 'array',
122
+ minItems: 1,
123
+ maxItems: TODO_MAX_ITEMS,
124
+ description: 'The full task list — replaces the previous list entirely.',
125
+ items: {
126
+ type: 'object',
127
+ properties: {
128
+ text: {
129
+ type: 'string',
130
+ description: 'Short, outcome-shaped item, e.g. "Activate ZCL_ORDER_API".',
131
+ },
132
+ status: {
133
+ type: 'string',
134
+ enum: ['pending', 'in_progress', 'completed'],
135
+ },
136
+ },
137
+ required: ['text', 'status'],
138
+ },
139
+ },
140
+ },
141
+ required: ['todos'],
142
+ },
143
+ handler: (args, ctx) => todoSetHandler(args, ctx),
144
+ });