@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,179 @@
1
+ // src/commands/plan-continue.ts
2
+ // C1 mid-phase-continue (2026-07-14) — re-attach a router-dispatched bare
3
+ // "continue" turn to the post-phase chain (audit + next-phase offer) via the
4
+ // persisted session.plan_in_flight marker. Design + seam analysis:
5
+ // .superpowers/sdd/midphase-continue-report.md §4 (approved). ALL continuation
6
+ // logic lives here so the repl.tsx wiring stays thin and mirror-safe; the
7
+ // existing repl seam pins (finishPlanResume ×4, runPostPhaseChain ×2) hold
8
+ // because this module — not repl.tsx — calls those functions.
9
+ import { basename } from 'node:path';
10
+ import { existsSync } from 'node:fs';
11
+ import { leasePathFor, acquireRunLease } from '../projects/run-lease.js';
12
+ import { readProjectFile } from '../projects/status.js';
13
+ import { parsePlanContent, phaseWrites } from '../projects/plan-schema.js';
14
+ import { statusesFromItems } from '../projects/plan-run.js';
15
+ import { findNewestVersion } from './plan-resume.js';
16
+ import { collectTurnAssistantText } from '../agent/turn-assistant-text.js';
17
+ import { finishPlanResume } from './plan-resume.js';
18
+ import { runPostPhaseChain } from './plan-chain.js';
19
+ /**
20
+ * The same block shape loop.ts's manifest-on-interrupt hook scans for.
21
+ * E2 dual-read: accept both the legacy `csforge:` and current `cspeach:` prefixes.
22
+ */
23
+ const MANIFEST_BLOCK_RE = /<!--\s*(?:csforge|cspeach):plan-manifest\s*\n([\s\S]*?)\n\s*-->/g;
24
+ /**
25
+ * Build the marker from the prepared resume the phase turn is about to run.
26
+ * `family` mirrors leasePathFor's family-base convention (version suffix
27
+ * stripped) — display/diagnostic identity, the lease itself re-derives it.
28
+ */
29
+ export function buildPlanInFlight(prepared, attemptStartedAt) {
30
+ const family = basename(leasePathFor(prepared.path)).replace(/\.cspeach\.lock$/, '');
31
+ return {
32
+ path: prepared.path,
33
+ family,
34
+ phaseId: prepared.nextPhaseId,
35
+ attemptStartedAt,
36
+ ...(prepared.mode ? { mode: prepared.mode } : {}),
37
+ ...(prepared.effectiveMode ? { effectiveMode: prepared.effectiveMode } : {}),
38
+ ...(prepared.auditFlag ? { auditFlag: prepared.auditFlag } : {}),
39
+ ...(prepared.auditMode ? { auditMode: prepared.auditMode } : {}),
40
+ };
41
+ }
42
+ /**
43
+ * Foreign-manifest guard. Every plan manifest (full AND compact) carries a
44
+ * `title`; a differing title means the turn produced a NEW plan, not this
45
+ * continuation — the caller must NOT attach it to the in-flight phase (it
46
+ * runs the generic save instead so the artifact isn't lost).
47
+ * - 'none' — no manifest block: phase still in flight, nothing to persist.
48
+ * - 'match' — attach: same title, titleless compact manifest, or unparseable
49
+ * JSON (finishPlanResume owns loud extract failures).
50
+ * - 'foreign' — a different plan's manifest.
51
+ * The LAST block wins, matching collectTurnAssistantText/extractPlan reading
52
+ * the final manifest of a turn.
53
+ */
54
+ export function classifyContinuationText(text, envelopeTitle) {
55
+ const blocks = [...text.matchAll(MANIFEST_BLOCK_RE)];
56
+ if (blocks.length === 0)
57
+ return 'none';
58
+ const raw = blocks[blocks.length - 1][1];
59
+ try {
60
+ const parsed = JSON.parse(raw);
61
+ if (typeof parsed.title === 'string' && parsed.title.trim().length > 0
62
+ && envelopeTitle.trim().length > 0 && parsed.title.trim() !== envelopeTitle.trim()) {
63
+ return 'foreign';
64
+ }
65
+ }
66
+ catch {
67
+ // Unparseable → 'match': hand it to finishPlanResume, whose extract
68
+ // failure path reports PHASE RESULT NOT PERSISTED loudly.
69
+ }
70
+ return 'match';
71
+ }
72
+ /**
73
+ * Resolve the in-flight marker to a live plan + phase and re-acquire the
74
+ * family lease (it was released when the interrupted turn ended). Order
75
+ * matters: file + phase are validated BEFORE the lease so a stale marker
76
+ * never leaves a fresh lock behind. Every null return means "run a normal
77
+ * turn"; `stale` additionally tells the caller to clear the marker.
78
+ */
79
+ export function preparePlanContinuation(args) {
80
+ const { marker } = args;
81
+ let path = marker.path;
82
+ if (!existsSync(path))
83
+ return { prepared: null, stale: true };
84
+ let envelope;
85
+ try {
86
+ envelope = readProjectFile(path);
87
+ }
88
+ catch {
89
+ return { prepared: null, stale: true };
90
+ }
91
+ // The marker may predate later phase-save rotations — same newest-version
92
+ // redirect preparePlanResume performs after its @token resolution.
93
+ const newer = findNewestVersion(path, envelope);
94
+ if (newer)
95
+ ({ path, envelope } = newer);
96
+ const parsed = parsePlanContent(envelope.content);
97
+ if (!parsed.ok)
98
+ return { prepared: null, stale: true };
99
+ const phase = parsed.content.phases.find((p) => p.id === marker.phaseId);
100
+ if (!phase)
101
+ return { prepared: null, stale: true };
102
+ // Lease LAST: a stale marker (missing file / unknown phase) must never leave
103
+ // a fresh lock behind. A LIVE other-session holder is contention, NOT stale —
104
+ // the marker stays so the user can finish there or take over via --resume.
105
+ const lease = acquireRunLease(path, args.sessionId, { log: (l) => args.log(l) });
106
+ if (!lease.acquired) {
107
+ const holder = lease.holder;
108
+ args.log(`[plan] in-flight phase ${marker.phaseId} not re-attached — another session holds the run lease`
109
+ + (holder ? ` (session ${holder.sessionId}, pid ${holder.pid})` : '')
110
+ + '. Finish there or /abap-plan --resume here to take over.');
111
+ return { prepared: null, stale: false };
112
+ }
113
+ const prepared = {
114
+ path,
115
+ envelope,
116
+ statuses: statusesFromItems(envelope.interaction.items, parsed.content.phases),
117
+ nextPhaseId: marker.phaseId,
118
+ nextDelegateTo: phase.delegateTo,
119
+ nextPhaseWrites: phaseWrites(phase),
120
+ llmPrompt: '',
121
+ ...(marker.mode ? { mode: marker.mode } : {}),
122
+ ...(marker.effectiveMode ? { effectiveMode: marker.effectiveMode } : {}),
123
+ ...(marker.auditFlag ? { auditFlag: marker.auditFlag } : {}),
124
+ ...(marker.auditMode ? { auditMode: marker.auditMode } : {}),
125
+ };
126
+ return { prepared };
127
+ }
128
+ /**
129
+ * The continuation counterpart of the repl's finishPlanResume call sites:
130
+ * classify the turn text first (foreign-manifest guard), then reuse
131
+ * finishPlanResume verbatim — pending-audit injection, revision build,
132
+ * validation, handover projection all behave exactly as on a --resume turn.
133
+ * attemptStartedAt comes from the MARKER (the original phase turn), so the
134
+ * audit evidence window spans the interrupt.
135
+ */
136
+ export async function finishPlanContinuation(args) {
137
+ const text = args.textOverride ?? collectTurnAssistantText(args.messages, args.messagesStart);
138
+ const cls = classifyContinuationText(text, args.prepared.envelope.title);
139
+ if (cls === 'none')
140
+ return { kind: 'none' };
141
+ if (cls === 'foreign') {
142
+ args.log(`[plan] this turn's manifest belongs to a different plan — NOT attached to in-flight phase ${args.marker.phaseId}.`);
143
+ return { kind: 'foreign' };
144
+ }
145
+ const outcome = await finishPlanResume({
146
+ prepared: args.prepared,
147
+ messages: args.messages,
148
+ messagesStart: args.messagesStart,
149
+ sessionId: args.sessionId,
150
+ attemptStartedAt: args.marker.attemptStartedAt,
151
+ auditMode: args.prepared.auditMode,
152
+ log: args.log,
153
+ ...(args.textOverride !== undefined ? { textOverride: args.textOverride } : {}),
154
+ });
155
+ return outcome ? { kind: 'saved', outcome } : { kind: 'not-persisted' };
156
+ }
157
+ /**
158
+ * The continuation counterpart of the repl's runPostPhaseChain call sites —
159
+ * executedPhaseId comes from the marker, run semantics (mode/audit flags)
160
+ * from the prepared object the marker rebuilt. Kept HERE so repl.tsx's
161
+ * runPostPhaseChain call-site count (structural seam pin ×2) is unchanged.
162
+ */
163
+ export async function runContinuationChain(args) {
164
+ await runPostPhaseChain({
165
+ savedPath: args.outcome.savedPath,
166
+ executedPhaseId: args.marker.phaseId,
167
+ sessionId: args.sessionId,
168
+ cwd: args.cwd,
169
+ mode: args.prepared.effectiveMode ?? 'step',
170
+ modeFlag: args.prepared.mode,
171
+ auditMode: args.prepared.auditMode,
172
+ auditFlag: args.prepared.auditFlag,
173
+ attemptStartedAt: args.marker.attemptStartedAt,
174
+ subagentDispatches: args.subagentDispatches,
175
+ prompt: args.prompt,
176
+ queueDispatch: args.queueDispatch,
177
+ log: args.log,
178
+ });
179
+ }
@@ -0,0 +1,154 @@
1
+ // cspeach-cli/src/commands/plan-gate.ts
2
+ //
3
+ // Guarded phase-chaining gate (2026-07-03 agentic-flow design) — the PURE
4
+ // decision of what happens between two plan phases. No fs, no config, no
5
+ // I/O: the effective mode (config default, overridden by per-run
6
+ // --guarded/--step flags — see parsePlanResumeFlags in plan-resume.ts)
7
+ // arrives as an argument, and the caller (Task 6 wiring in the REPL's
8
+ // offerNextPhase path) renders the returned decision.
9
+ //
10
+ // Semantics (binding, from the design doc):
11
+ // step ⇒ always `prompt` — today's behaviour, one consent per phase.
12
+ // guarded ⇒ `stop` when the PREVIOUS phase's audit is unresolved
13
+ // ('audit-infra' for infra_failed, else 'audit-failed');
14
+ // `prompt` with 'write-phase' when the NEXT phase writes to SAP
15
+ // (phaseWrites is fail-safe: undeclared ⇒ writes). This
16
+ // prompt IS the stop — consent to RUN the phase; it does
17
+ // NOT pre-approve the writes inside it;
18
+ // `auto` otherwise (declared non-write phase, clean prior audit).
19
+ //
20
+ // Precedence: the audit stop wins over the write-phase prompt when both
21
+ // apply — a hard stop must never be downgraded to a consent prompt.
22
+ // Legacy envelopes (phases with no audit field) never stop the chain:
23
+ // isAuditResolved(undefined) === true.
24
+ import { phaseWrites } from '../projects/plan-schema.js';
25
+ import { isAuditResolved } from '../projects/plan-run.js';
26
+ /* ── guarded-run auto_approve override (Task 6, 2026-07-03) ────────────────
27
+ * Module-level flag consulted by tools/approval.ts BEFORE its auto-approve
28
+ * branch. The REPL sets it at guarded-chain start (right before the phase's
29
+ * runTurn) and clears it in a finally — so during a guarded auto-chained
30
+ * phase, every request_approval falls through to the interactive prompt even
31
+ * when the user's config says auto_approve. Rationale: auto_approve was
32
+ * consented for hand-driven turns; an auto-dispatched chain must not inherit
33
+ * that consent. When the flag is off, approval.ts behaviour is byte-identical
34
+ * (skill-mode compatibility). */
35
+ let guardedRunActive = false;
36
+ export function setGuardedRunActive(active) {
37
+ guardedRunActive = active;
38
+ }
39
+ export function isGuardedRunActive() {
40
+ return guardedRunActive;
41
+ }
42
+ /* ── plan-phase read-only enforcement (Task 7, 2026-07-03) ─────────────────
43
+ * Module-level flag consulted by tools/subagent/agent_run.ts. The REPL sets
44
+ * it around EVERY plan-resume phase turn (guarded AND step — unlike the
45
+ * guarded flag above) and clears it in the same finally — so any subagent
46
+ * dispatched from inside a plan phase is FORCED onto the read-only tool
47
+ * filter regardless of the model's `read_only` arg. Governance rule: inside
48
+ * plan phases, subagents can read, never write — the model cannot opt out.
49
+ * When the flag is off, agent_run's opt-in read_only behaviour is
50
+ * byte-identical (skill-mode compatibility). */
51
+ let planPhaseActive = false;
52
+ export function setPlanPhaseActive(active) {
53
+ planPhaseActive = active;
54
+ }
55
+ export function isPlanPhaseActive() {
56
+ return planPhaseActive;
57
+ }
58
+ /* Task 7 — per-phase subagent dispatch counter. agent_run increments it on
59
+ * every dispatch made while a plan phase is active; the REPL reads it
60
+ * (read+reset) in the runTurn finally that clears the plan-phase flag and
61
+ * threads the value to runPostPhaseChain, so the phase-end report can name
62
+ * how many subagents the phase used (fix-wave: consuming it in the chain
63
+ * alone leaked counts when the chain was skipped on error paths — the
64
+ * chain's read+reset is now only a fallback for turn-less callers). Lives
65
+ * HERE (not in agent_run.ts) so plan-chain never has to import the tool
66
+ * module — plan-gate is already the shared, pure, I/O-free home for
67
+ * plan-phase module state. */
68
+ let planPhaseSubagentDispatches = 0;
69
+ export function notePlanPhaseSubagentDispatch() {
70
+ planPhaseSubagentDispatches += 1;
71
+ }
72
+ export function getAndResetPlanPhaseSubagentDispatches() {
73
+ const n = planPhaseSubagentDispatches;
74
+ planPhaseSubagentDispatches = 0;
75
+ return n;
76
+ }
77
+ /* ── Task 10 — deviation backstop state (2026-07-03) ───────────────────────
78
+ * currentPhaseWrites: the DECLARED `writes` field of the plan phase executing
79
+ * THIS turn (null = outside phases). Set by the repl wiring alongside
80
+ * setPlanPhaseActive at both call sites, cleared to null in the same finally.
81
+ * tools/approval.ts consults it BEFORE rendering anything: guarded run +
82
+ * writes === false + a request_approval arriving anyway = a plan deviation —
83
+ * the write is DENIED without prompting (an unattended chain must never hang
84
+ * on an interactive approval), and the deviation flag below is raised.
85
+ * Fail-safe by construction: only an EXPLICIT false ever denies. */
86
+ let currentPhaseWrites = null;
87
+ export function setCurrentPhaseWrites(w) {
88
+ currentPhaseWrites = w;
89
+ }
90
+ export function getCurrentPhaseWrites() {
91
+ return currentPhaseWrites;
92
+ }
93
+ /* Task 10 — the "deviation occurred" flag, same read+reset pattern as the
94
+ * subagent counter above: approval.ts raises it (with the detail string) when
95
+ * it denies an undeclared write; plan-chain.ts reads+resets it at post-phase-
96
+ * chain start and turns it into blocked + lastStop:'deviation' + STOPPED
97
+ * banner. Lives HERE so plan-chain never imports the tool module. */
98
+ /**
99
+ * The one detail string a deviation carries — shared so approval.ts (the
100
+ * raiser), plan-chain.ts (the persister) and the tests can never drift.
101
+ */
102
+ export const PLAN_DEVIATION_DETAIL = 'phase declared writes:false but requested a write approval — chain stopped';
103
+ let planDeviationDetail = null;
104
+ export function notePlanDeviation(detail) {
105
+ planDeviationDetail = detail;
106
+ }
107
+ export function getAndResetPlanDeviation() {
108
+ const d = planDeviationDetail;
109
+ planDeviationDetail = null;
110
+ return d;
111
+ }
112
+ /**
113
+ * Banner shown for every non-auto guarded outcome. The resume hint uses the
114
+ * literal `@<plan>` placeholder on purpose — this module is pure and doesn't
115
+ * know the saved path; the caller may substitute the real basename.
116
+ */
117
+ const banner = (reason, next) => `\n■ STOPPED — ${reason}\n Resume with: /abap-plan --resume @<plan> (next phase: ${next})\n`;
118
+ export function decideNextPhaseGate(args) {
119
+ if (args.mode === 'step') {
120
+ // Step mode is today's behaviour verbatim — a plain consent prompt with
121
+ // no guarded decorations (no banner, no stopReason).
122
+ return { action: 'prompt' };
123
+ }
124
+ // Guarded. Hard stop first: an unresolved prior audit must never be
125
+ // downgraded to a runnable prompt by the write-phase branch below.
126
+ if (!isAuditResolved(args.prevPhaseAudit)) {
127
+ if (args.prevPhaseAudit?.state === 'infra_failed') {
128
+ return {
129
+ action: 'stop',
130
+ stopReason: 'audit-infra',
131
+ banner: banner('auditor infrastructure failure — verdict unknown (not a code finding)', args.nextPhase.id),
132
+ };
133
+ }
134
+ return {
135
+ action: 'stop',
136
+ stopReason: 'audit-failed',
137
+ banner: banner('prior phase audit is unresolved — fix the findings or explicitly waive them', args.nextPhase.id),
138
+ };
139
+ }
140
+ if (phaseWrites(args.nextPhase)) {
141
+ // The prompt IS the stop: consent to run the write phase. Writes inside
142
+ // the phase still go through their own approval flow — this never
143
+ // pre-approves them.
144
+ return {
145
+ action: 'prompt',
146
+ stopReason: 'write-phase',
147
+ // Task 3 review carry-over: the banner's resume hint already names the
148
+ // phase (`next phase: <id>`), so the reason string stays generic —
149
+ // naming it twice read as a rendering bug.
150
+ banner: banner('next phase writes to SAP — consent required to run it', args.nextPhase.id),
151
+ };
152
+ }
153
+ return { action: 'auto' };
154
+ }