@cspeach/cli 1.0.0 → 1.1.1

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 +71 -78
  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
@@ -0,0 +1,160 @@
1
+ // cspeach-cli/src/session/recap.ts
2
+ //
3
+ // UX Wave 2 / Task 6 — session recap: three lines that re-orient a developer
4
+ // who just resumed (or typed /recap) without scrolling back through the
5
+ // transcript.
6
+ //
7
+ // ↩ last session: /abap-generate · 4 turns · 3h ago · "add a validation…"
8
+ // ▶ tasks: 2 done / 3 open — next: write the BDEF validation
9
+ // ▷ plan Maintenance Requests: phase 3/7 validated — next: /abap-plan --resume @…
10
+ //
11
+ // Line 1 always prints (unless the session has zero user turns → recap is
12
+ // empty and callers print nothing). Line 2 only when the session's todo list
13
+ // has non-completed entries. Line 3 only when the NEWEST plan family in the
14
+ // workspace still has unsatisfied phases (audit-aware, same predicates the
15
+ // plan DAG uses).
16
+ //
17
+ // PURE except the plan-envelope reads: no writes, no prompts, never throws —
18
+ // a bad/missing workspace or a corrupt envelope silently drops line 3. The
19
+ // workspace dir is injectable (opts.workspaceDir) so call sites can pass the
20
+ // config-resolved path (resolveWorkspacePath) and tests can point at a temp
21
+ // dir; the fallback is the project-rooted default for `cwd`.
22
+ import { readdirSync, statSync } from 'node:fs';
23
+ import { basename, join } from 'node:path';
24
+ import { formatTimeAgo } from './time-ago.js';
25
+ import { readProjectFile } from '../projects/status.js';
26
+ import { parsePlanContent } from '../projects/plan-schema.js';
27
+ import { statusesFromItems, isPhaseSatisfied } from '../projects/plan-run.js';
28
+ import { defaultWorkspaceFor } from '../projects/workspace.js';
29
+ /** Wave-1 truncation rule (see /reroute): ≤60 chars total, '...' suffix. */
30
+ function truncatePrompt(s) {
31
+ return s.length > 60 ? s.slice(0, 57) + '...' : s;
32
+ }
33
+ /**
34
+ * Build the recap lines for a session. Returns [] for an empty session
35
+ * (zero user turns) — callers print nothing in that case.
36
+ */
37
+ export function buildRecap(session, cwd, opts) {
38
+ const lines = [];
39
+ // ── line 1: session orientation ─────────────────────────────────────────
40
+ // "User turns" = plain-string user messages (tool_result rounds are arrays)
41
+ // — same counting rule as the resume picker (session/store.ts).
42
+ const userMessages = session.messages.filter((m) => m.role === 'user' && typeof m.content === 'string');
43
+ const turns = userMessages.length;
44
+ if (turns === 0)
45
+ return [];
46
+ const skillLabel = session.skill ? `/${session.skill}` : 'no skill';
47
+ // Last user prompt: prefer the persisted lastUserPrompt (user-typed prose;
48
+ // null after slash-only commands), fall back to the last string user
49
+ // message with the first-turn <session_context> wrapper stripped (same
50
+ // rule as the picker's promptSnippet).
51
+ let prompt = session.lastUserPrompt;
52
+ if (!prompt) {
53
+ const last = userMessages[userMessages.length - 1].content;
54
+ prompt = last.replace(/<session_context>[\s\S]*?<\/session_context>\s*/g, '').trim() || null;
55
+ }
56
+ const quoted = prompt ? `"${truncatePrompt(prompt)}"` : '(no prompt)';
57
+ const ago = formatTimeAgo(session.last_turn_at, opts?.now);
58
+ lines.push(`↩ last session: ${skillLabel} · ${turns} turn${turns === 1 ? '' : 's'} · ${ago} · ${quoted}`);
59
+ // ── line 2: todo list (Task 4's todo_set) — only when open work exists ──
60
+ const todos = session.todos ?? [];
61
+ const open = todos.filter((t) => t.status !== 'completed');
62
+ if (open.length > 0) {
63
+ const done = todos.length - open.length;
64
+ const next = todos.find((t) => t.status === 'in_progress') ?? todos.find((t) => t.status === 'pending');
65
+ lines.push(`▶ tasks: ${done} done / ${open.length} open — next: ${next.text}`);
66
+ }
67
+ // ── line 3: newest plan family with unsatisfied phases ──────────────────
68
+ // Never throws: any fs/parse problem silently drops the line.
69
+ try {
70
+ const planLine = buildPlanLine(opts?.workspaceDir ?? defaultWorkspaceFor(cwd));
71
+ if (planLine)
72
+ lines.push(planLine);
73
+ }
74
+ catch { /* recap must never throw */ }
75
+ return lines;
76
+ }
77
+ /**
78
+ * Scan `workspaceDir` for the newest plan family and, when it still has
79
+ * unsatisfied phases, produce the plan recap line. Returns null when there
80
+ * is no plan, the newest plan family is fully satisfied, or its envelope is
81
+ * invalid (silent skip by design).
82
+ *
83
+ * "Newest family": envelope files group into families by filename base
84
+ * (`<base>-v<N>[-<sub>].cspeach.json` — same convention as
85
+ * pickNewestPlanVersion / findNewestVersion in commands/plan-resume.ts);
86
+ * within a family the highest version wins; families rank by their newest
87
+ * file's mtime. Non-plan envelopes (design, spec-gap, …) are skipped so the
88
+ * newest PLAN family is found even when a newer non-plan artefact exists.
89
+ */
90
+ function buildPlanLine(workspaceDir) {
91
+ let names;
92
+ try {
93
+ names = readdirSync(workspaceDir);
94
+ }
95
+ catch {
96
+ return null; // workspace missing — nothing to recap
97
+ }
98
+ const re = /^(.+)-v(\d+)(?:-(\d+))?\.cspeach\.json$/;
99
+ const cands = [];
100
+ for (const name of names) {
101
+ if (!name.endsWith('.cspeach.json'))
102
+ continue;
103
+ const path = join(workspaceDir, name);
104
+ let mtimeMs;
105
+ try {
106
+ mtimeMs = statSync(path).mtimeMs;
107
+ }
108
+ catch {
109
+ continue;
110
+ }
111
+ const m = re.exec(name);
112
+ if (m) {
113
+ cands.push({ path, family: m[1], version: Number(m[2]), sub: m[3] ? Number(m[3]) : 0, mtimeMs });
114
+ }
115
+ else {
116
+ // Hand-renamed / version-less envelope: its own single-file family.
117
+ cands.push({ path, family: name.slice(0, -'.cspeach.json'.length), version: 0, sub: 0, mtimeMs });
118
+ }
119
+ }
120
+ if (cands.length === 0)
121
+ return null;
122
+ // Newest version per family (version, then collision sub, then mtime).
123
+ const byFamily = new Map();
124
+ for (const c of cands) {
125
+ const cur = byFamily.get(c.family);
126
+ if (!cur
127
+ || c.version > cur.version
128
+ || (c.version === cur.version && (c.sub > cur.sub || (c.sub === cur.sub && c.mtimeMs > cur.mtimeMs)))) {
129
+ byFamily.set(c.family, c);
130
+ }
131
+ }
132
+ // Families newest-first by their representative file's mtime; the first
133
+ // family whose envelope IS a plan is "the newest plan family".
134
+ const familiesNewestFirst = [...byFamily.values()].sort((a, b) => b.mtimeMs - a.mtimeMs);
135
+ for (const cand of familiesNewestFirst) {
136
+ let env;
137
+ try {
138
+ env = readProjectFile(cand.path);
139
+ }
140
+ catch {
141
+ continue; // unreadable/invalid file — can't tell it's a plan; skip family
142
+ }
143
+ if (env.artefactType !== 'plan')
144
+ continue;
145
+ // This is the newest plan family. Invalid content → silent skip (no
146
+ // falling through to an older plan family — stale info is worse than none).
147
+ const pc = parsePlanContent(env.content);
148
+ if (!pc.ok)
149
+ return null;
150
+ const statuses = statusesFromItems(env.interaction.items, pc.content.phases);
151
+ const total = pc.content.phases.length;
152
+ // Audit-aware satisfaction — same predicate as the plan DAG (a
153
+ // validated-but-audit-failed phase is NOT done).
154
+ const validated = pc.content.phases.filter((p) => isPhaseSatisfied(statuses[p.id], p.audit)).length;
155
+ if (total === 0 || validated >= total)
156
+ return null; // fully satisfied — no line
157
+ return `▷ plan ${env.title}: phase ${validated}/${total} validated — next: /abap-plan --resume @${basename(cand.path)}`;
158
+ }
159
+ return null;
160
+ }
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Static skill catalog for CSPeach CLI v0.2.
3
3
  *
4
- * STALENESS RISK (spec §3.3): This constant is hardcoded against the 32 skills
5
- * in .claude/skills/ as of 2026-06-28 (abap-dump-analyze + abap-dump-triage
4
+ * STALENESS RISK (spec §3.3): This constant is hardcoded against the 33 skills
5
+ * in .claude/skills/ as of 2026-07-03 (abap-dump-analyze + abap-dump-triage
6
6
  * merged into abap-dump 2026-06-12; abap-extend-model added 2026-06-25;
7
7
  * abap-radar/abap-document/abap-performance/abap-cloud-check merged into
8
8
  * abap-explain/abap-handover/abap-review 2026-06-28 per
9
- * plan 2026-06-28-skill-portfolio-merge). It will drift if skills are added or
9
+ * plan 2026-06-28-skill-portfolio-merge; abap-clean-core added 2026-06-29). It will drift if skills are added or
10
10
  * removed. At Plan 4b kickoff decision the build-time generation option
11
11
  * (scripts/generate-skill-catalog.ts reads SKILL.md frontmatter) or runtime
12
12
  * fetch (/v1/skills endpoint) will be chosen to replace this file. Until then,
@@ -32,6 +32,12 @@ export const SKILL_CATALOG = [
32
32
  description: 'Read-only review across lenses: quality (default), performance, cloud-readiness',
33
33
  whenToUse: `grade source: quality (default) + performance/cloud lenses`,
34
34
  },
35
+ {
36
+ name: 'abap-clean-core',
37
+ category: 'Read / Analyze',
38
+ description: 'Clean-core decision & defense — recommend the clean-core-correct approach, classify the level (A/B/C/D), produce citable evidence; verifies live when connected. Read-only.',
39
+ whenToUse: `pick this for "is this clean-core-OK?" decisions + defendable evidence (not /abap-review cloud lens — that grades code)`,
40
+ },
35
41
  {
36
42
  name: 'abap-handover',
37
43
  category: 'Read / Analyze',