@cspeach/cli 0.9.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.
- package/README.md +1 -1
- package/dist/agent/intent-system-prompt.js +1 -1
- package/dist/agent/loop.js +228 -26
- package/dist/agent/providers/license-gate.js +44 -0
- package/dist/agent/skill-checkpoint.js +1 -1
- package/dist/agent/tool-dispatch.js +15 -0
- package/dist/approvals/canonical.js +91 -0
- package/dist/approvals/jwt.js +39 -2
- package/dist/approvals/op-labels.js +124 -0
- package/dist/approvals/render.js +42 -36
- package/dist/auth/org-anthropic-key.js +25 -0
- package/dist/classifier/client.js +18 -3
- package/dist/cli.js +15 -0
- package/dist/commands/compact.js +28 -2
- package/dist/commands/config-set.js +284 -0
- package/dist/commands/config-show.js +20 -0
- package/dist/commands/export-audit.js +43 -0
- package/dist/commands/help.js +5 -0
- package/dist/commands/login.js +31 -14
- package/dist/commands/plan-audit-evidence.js +266 -0
- package/dist/commands/plan-audit.js +692 -0
- package/dist/commands/plan-chain.js +671 -0
- package/dist/commands/plan-continue.js +179 -0
- package/dist/commands/plan-gate.js +154 -0
- package/dist/commands/plan-model-tier.js +83 -0
- package/dist/commands/plan-resume.js +728 -46
- package/dist/config/loader.js +223 -5
- package/dist/config/model-defaults.js +14 -0
- package/dist/cost/pricing.js +27 -1
- package/dist/doctor/checks/_http-probe.js +1 -0
- package/dist/doctor/checks/cert.js +14 -3
- package/dist/doctor/checks/sap.js +30 -8
- package/dist/doctor/checks/system-roles.js +41 -0
- package/dist/doctor/checks/zcspeach.js +19 -4
- package/dist/doctor/run.js +2 -0
- package/dist/models/resolve.js +61 -0
- package/dist/models/server-config.js +155 -0
- package/dist/one-shot.js +76 -6
- package/dist/projects/answer-blockers.js +137 -0
- package/dist/projects/extract-cca.js +111 -17
- package/dist/projects/extract-modernize.js +4 -2
- package/dist/projects/extract-plan.js +184 -37
- package/dist/projects/extract-spec-gap.js +34 -7
- package/dist/projects/extract-test-coverage.js +4 -2
- package/dist/projects/extract-upgrade.js +116 -23
- package/dist/projects/handover-md.js +195 -0
- package/dist/projects/index.js +5 -2
- package/dist/projects/merge-cca.js +292 -0
- package/dist/projects/merge-upgrade.js +173 -0
- package/dist/projects/migration.js +103 -1
- package/dist/projects/output-paths.js +27 -0
- package/dist/projects/plan-run.js +285 -27
- package/dist/projects/plan-schema.js +136 -3
- package/dist/projects/promote-command.js +25 -2
- package/dist/projects/promote.js +128 -0
- package/dist/projects/run-lease.js +157 -0
- package/dist/projects/save-command.js +259 -21
- package/dist/projects/status.js +3 -1
- package/dist/projects/validate.js +1 -1
- package/dist/projects/workspace.js +164 -20
- package/dist/renderer/notices.js +64 -0
- package/dist/renderer/progress-chatter.js +8 -0
- package/dist/renderer/status-footer.js +22 -12
- package/dist/renderer/thinking-heartbeat.js +64 -8
- package/dist/renderer/todo-block.js +51 -0
- package/dist/renderer/tool-widget.js +55 -4
- package/dist/renderer/tty.js +43 -4
- package/dist/renderer/verify-chain.js +77 -0
- package/dist/repl/at-picker.js +60 -7
- package/dist/repl/bracketed-paste.js +28 -19
- package/dist/repl/builtin-commands.js +42 -0
- package/dist/repl/current-transport.js +10 -0
- package/dist/repl/early-line-buffer.js +68 -0
- package/dist/repl/history.js +86 -0
- package/dist/repl/ink-stdin-guard.js +64 -0
- package/dist/repl/inquirer-guard.js +70 -5
- package/dist/repl/mode-ceiling.js +16 -0
- package/dist/repl/mode-cycle.js +104 -0
- package/dist/repl/numbered-menu.js +131 -0
- package/dist/repl/post-turn-status.js +26 -6
- package/dist/repl/rule8-detector.js +17 -2
- package/dist/repl/safety-confirm.js +111 -2
- package/dist/repl/safety-mode-state.js +19 -3
- package/dist/repl/slash-completer.js +5 -0
- package/dist/repl/slash-picker.js +10 -15
- package/dist/repl.js +1232 -95
- package/dist/rewind/candidates.js +194 -0
- package/dist/rewind/cli.js +137 -0
- package/dist/rewind/format.js +27 -0
- package/dist/rewind/restore.js +245 -0
- package/dist/router/classifier.js +150 -6
- package/dist/sap/capability-matrix.js +20 -0
- package/dist/sap/capability-matrix.json +11236 -0
- package/dist/sap/capability.js +146 -0
- package/dist/sap/connection-manager.js +19 -1
- package/dist/sap/onboarding.js +42 -4
- package/dist/session/audit-export.js +459 -0
- package/dist/session/context-report.js +163 -0
- package/dist/session/pending.js +27 -0
- package/dist/session/recap.js +160 -0
- package/dist/skill-catalog.js +51 -40
- package/dist/skills/bundled-skills.js +272 -1
- package/dist/skills/promotion-dispatch.js +23 -0
- package/dist/tools/_command-shared.js +36 -12
- package/dist/tools/_filesystem-shared.js +139 -4
- package/dist/tools/_flag.js +25 -0
- package/dist/tools/approval.js +177 -26
- package/dist/tools/ask-question.js +400 -7
- package/dist/tools/capability/tool.js +74 -0
- package/dist/tools/dispatch-skill.js +22 -1
- package/dist/tools/extend-model/anchored-insert.js +1414 -0
- package/dist/tools/extend-model/tool.js +340 -0
- package/dist/tools/filesystem/extract-document.js +57 -0
- package/dist/tools/filesystem/file-edit.js +12 -2
- package/dist/tools/filesystem/file-read.js +2 -2
- package/dist/tools/filesystem/file-write.js +11 -2
- package/dist/tools/filesystem/glob.js +11 -0
- package/dist/tools/filesystem/grep.js +10 -0
- package/dist/tools/filesystem/read-document.js +107 -0
- package/dist/tools/fiori/apply.js +50 -0
- package/dist/tools/fiori/bin.js +3 -0
- package/dist/tools/fiori/catalog/index.js +27 -0
- package/dist/tools/fiori/catalog/value-help.js +230 -0
- package/dist/tools/fiori/catalog/viz-chart.js +177 -0
- package/dist/tools/fiori/cli.js +71 -0
- package/dist/tools/fiori/deploy-config.js +73 -0
- package/dist/tools/fiori/fe-extend.js +76 -0
- package/dist/tools/fiori/fe-scaffold.js +71 -0
- package/dist/tools/fiori/floorplan-map.js +19 -0
- package/dist/tools/fiori/i18n.js +39 -0
- package/dist/tools/fiori/manifest.js +70 -0
- package/dist/tools/fiori/render.js +77 -0
- package/dist/tools/fiori/samples/data/index.json +13602 -0
- package/dist/tools/fiori/samples/data/sources.generated.js +808 -0
- package/dist/tools/fiori/samples/loader.js +248 -0
- package/dist/tools/fiori/samples/search.js +63 -0
- package/dist/tools/fiori/samples/types.js +2 -0
- package/dist/tools/fiori/scaffold.js +39 -0
- package/dist/tools/fiori/smoke/assertions.js +74 -0
- package/dist/tools/fiori/smoke/browser.js +52 -0
- package/dist/tools/fiori/smoke/driver.js +89 -0
- package/dist/tools/fiori/smoke/freestyle-spec.js +317 -0
- package/dist/tools/fiori/smoke/run-smoke.js +149 -0
- package/dist/tools/fiori/tools.js +681 -0
- package/dist/tools/fiori/types.js +1 -0
- package/dist/tools/local-build.js +86 -0
- package/dist/tools/local-files.js +31 -0
- package/dist/tools/project/_merge-shared.js +68 -0
- package/dist/tools/project/cca_merge.js +164 -0
- package/dist/tools/project/playbook_get.js +1 -1
- package/dist/tools/project/upgrade_merge_progress.js +206 -0
- package/dist/tools/sap-read.js +132 -20
- package/dist/tools/sap-write.js +550 -21
- package/dist/tools/shell/shell_exec.js +41 -6
- package/dist/tools/snapshot.js +63 -14
- package/dist/tools/subagent/agent_run.js +27 -3
- package/dist/tools/subagent/background_run.js +17 -1
- package/dist/tools/todo.js +144 -0
- package/dist/tools/transport-resolution.js +86 -0
- package/dist/tools/transport.js +224 -5
- package/dist/tools/write-mode.js +4 -0
- package/dist/ui/app.js +378 -21
- package/dist/ui/approval-modal.js +49 -16
- package/dist/ui/ask-question-emitter.js +14 -0
- package/dist/ui/body.js +13 -0
- package/dist/ui/context-grid.js +108 -0
- package/dist/ui/footer.js +120 -27
- package/dist/ui/header.js +7 -0
- package/dist/ui/line-resolution.js +35 -8
- package/dist/ui/rewind-emitter.js +10 -0
- package/dist/ui/rewind-panel.js +81 -0
- package/dist/ui/sap-state-store.js +1 -0
- package/dist/ui/session-timeline.js +1 -0
- package/dist/ui/status-line.js +43 -0
- package/dist/ui/text-input.js +214 -0
- package/dist/ui/todo-emitter.js +25 -0
- package/dist/ui/todo-panel.js +64 -0
- package/dist/ui/turn-status-emitter.js +50 -4
- package/dist/ui/turn-status.js +18 -3
- package/dist/ui/widgets/ask-form.js +242 -0
- package/dist/ui/widgets/ask-question-modal.js +21 -8
- package/package.json +22 -3
- package/bench/README.md +0 -78
- package/bench/prompts/abap-document-cds.md +0 -44
- package/bench/prompts/abap-explain-bdef-handler.md +0 -57
- package/bench/prompts/abap-test-method.md +0 -42
- package/bench/results/abap-document-cds/claude-haiku-4-5.md +0 -189
- package/bench/results/abap-document-cds/claude-opus-4-7.md +0 -120
- package/bench/results/abap-document-cds/claude-sonnet-4-6.md +0 -151
- package/bench/results/abap-explain-bdef-handler/claude-haiku-4-5.md +0 -112
- package/bench/results/abap-explain-bdef-handler/claude-opus-4-7.md +0 -101
- package/bench/results/abap-explain-bdef-handler/claude-sonnet-4-6.md +0 -101
- package/bench/results/abap-test-method/claude-haiku-4-5.md +0 -186
- package/bench/results/abap-test-method/claude-opus-4-7.md +0 -193
- package/bench/results/abap-test-method/claude-sonnet-4-6.md +0 -234
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
* The prompt is interactive (via the supplied async prompt callback);
|
|
25
25
|
* one-shot mode skips it entirely (caller passes a no-op).
|
|
26
26
|
*/
|
|
27
|
-
import { existsSync, mkdirSync, readdirSync, renameSync } from 'node:fs';
|
|
27
|
+
import { existsSync, mkdirSync, readdirSync, renameSync, rmdirSync, statSync } from 'node:fs';
|
|
28
28
|
import { join } from 'node:path';
|
|
29
29
|
import os from 'node:os';
|
|
30
30
|
import { loadConfig, saveConfig } from '../config/loader.js';
|
|
31
31
|
import { defaultWorkspaceFor } from './workspace.js';
|
|
32
|
+
import { OUTPUT_ROOT, LEGACY_ROOT } from './output-paths.js';
|
|
32
33
|
/**
|
|
33
34
|
* Run the migration prompt if and only if it hasn't run before AND
|
|
34
35
|
* there's actually something to migrate. No-op otherwise. Always sets
|
|
@@ -109,3 +110,104 @@ export async function maybePromptWorkspaceMigration(deps) {
|
|
|
109
110
|
}
|
|
110
111
|
await saveConfig({ ...cfg, migration_prompted: true });
|
|
111
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Recursively move every entry under `srcDir` into `dstDir`, MERGING into
|
|
115
|
+
* existing directories rather than clobbering them. Files that already exist
|
|
116
|
+
* at the destination are left untouched (the legacy copy is dropped — the
|
|
117
|
+
* dual-read already prefers the new root, so an existing new file wins).
|
|
118
|
+
* After the walk, the source directory is removed if empty.
|
|
119
|
+
*/
|
|
120
|
+
function mergeMoveTree(srcDir, dstDir, log) {
|
|
121
|
+
mkdirSync(dstDir, { recursive: true });
|
|
122
|
+
for (const name of readdirSync(srcDir)) {
|
|
123
|
+
const src = join(srcDir, name);
|
|
124
|
+
const dst = join(dstDir, name);
|
|
125
|
+
const isDir = statSync(src).isDirectory();
|
|
126
|
+
if (isDir) {
|
|
127
|
+
mergeMoveTree(src, dst, log);
|
|
128
|
+
}
|
|
129
|
+
else if (existsSync(dst)) {
|
|
130
|
+
log(`(keeping existing ${name} — already migrated)`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
renameSync(src, dst);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Remove the source dir if the walk emptied it. A merge that kept some
|
|
137
|
+
// existing-destination files may leave it non-empty — rmdir throws then,
|
|
138
|
+
// which we swallow (the leftover is harmless and visible to the user).
|
|
139
|
+
try {
|
|
140
|
+
rmdirSync(srcDir);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
/* non-empty after a partial merge — leave it for the user to inspect */
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* One-time offer to migrate a workspace's legacy `.abapforge/` output root
|
|
148
|
+
* into the branded `.cspeach/` root.
|
|
149
|
+
*
|
|
150
|
+
* Fires only when a top-level `.abapforge/` directory exists in the given
|
|
151
|
+
* workspace root AND the user has not previously chosen "never". Safe to call
|
|
152
|
+
* every startup: a successful move removes the folder (so it stops naturally),
|
|
153
|
+
* and the dual-read resolver keeps old projects working in the interim if the
|
|
154
|
+
* user declines.
|
|
155
|
+
*
|
|
156
|
+
* Idempotent and non-destructive: the move MERGES into any existing
|
|
157
|
+
* `.cspeach/<domain>` rather than overwriting it.
|
|
158
|
+
*/
|
|
159
|
+
export async function maybeOfferAbapforgeMigration(workspaceRoot, deps) {
|
|
160
|
+
const legacyDir = join(workspaceRoot, LEGACY_ROOT);
|
|
161
|
+
if (!existsSync(legacyDir))
|
|
162
|
+
return;
|
|
163
|
+
const cfg = await loadConfig();
|
|
164
|
+
if (cfg.abapforge_migration_declined === true)
|
|
165
|
+
return;
|
|
166
|
+
const newDir = join(workspaceRoot, OUTPUT_ROOT);
|
|
167
|
+
let choice;
|
|
168
|
+
try {
|
|
169
|
+
choice = await deps.prompt({ workspaceRoot, legacyDir, newDir });
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// User aborted the prompt (Ctrl+C) — leave everything as-is so the
|
|
173
|
+
// offer can re-fire next session. Never silently commit to a default.
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (choice === 'never') {
|
|
177
|
+
await saveConfig({ ...cfg, abapforge_migration_declined: true });
|
|
178
|
+
deps.log(`Leaving ${legacyDir} alone. (Will not ask again.)`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (choice === 'skip') {
|
|
182
|
+
deps.log(`Leaving ${legacyDir} alone for now.`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// choice === 'move'
|
|
186
|
+
try {
|
|
187
|
+
for (const child of readdirSync(legacyDir)) {
|
|
188
|
+
const src = join(legacyDir, child);
|
|
189
|
+
const dst = join(newDir, child);
|
|
190
|
+
if (statSync(src).isDirectory()) {
|
|
191
|
+
mergeMoveTree(src, dst, deps.log);
|
|
192
|
+
}
|
|
193
|
+
else if (existsSync(dst)) {
|
|
194
|
+
deps.log(`(keeping existing ${child} — already migrated)`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
mkdirSync(newDir, { recursive: true });
|
|
198
|
+
renameSync(src, dst);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Drop the now-(hopefully)-empty legacy root.
|
|
202
|
+
try {
|
|
203
|
+
rmdirSync(legacyDir);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
/* a partial merge left files behind — keep the folder for inspection */
|
|
207
|
+
}
|
|
208
|
+
deps.log(`Moved legacy outputs from ${legacyDir} → ${newDir}`);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
deps.log(`Migration failed: ${err.message}. Old folder left untouched.`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/** Branded output root for customer working files (was .abapforge). */
|
|
4
|
+
export const OUTPUT_ROOT = '.cspeach';
|
|
5
|
+
/** Pre-2026-06 root — still READ for back-compat, never written anew. */
|
|
6
|
+
export const LEGACY_ROOT = '.abapforge';
|
|
7
|
+
/** Build `<projectRoot>/.cspeach/<domain>` (domain = cca | upgrades | modernize | tests). */
|
|
8
|
+
export function domainDir(projectRoot, domain) {
|
|
9
|
+
return join(projectRoot, OUTPUT_ROOT, domain);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a project-relative detail path to an absolute path, preferring the
|
|
13
|
+
* new root and falling back to the legacy `.abapforge/` location when the new
|
|
14
|
+
* one is absent. Returns the NEW absolute path when neither exists (the caller
|
|
15
|
+
* is about to create it). `relPath` may be expressed against either root — we
|
|
16
|
+
* normalise the leading segment before probing.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveExistingDomainPath(projectRoot, relPath) {
|
|
19
|
+
const tail = relPath.replace(/^\.(cspeach|abapforge)[/\\]/, '');
|
|
20
|
+
const asNew = join(projectRoot, OUTPUT_ROOT, tail);
|
|
21
|
+
if (existsSync(asNew))
|
|
22
|
+
return asNew;
|
|
23
|
+
const asLegacy = join(projectRoot, LEGACY_ROOT, tail);
|
|
24
|
+
if (existsSync(asLegacy))
|
|
25
|
+
return asLegacy;
|
|
26
|
+
return asNew;
|
|
27
|
+
}
|
|
@@ -1,5 +1,53 @@
|
|
|
1
|
+
// cspeach-cli/src/projects/plan-run.ts
|
|
2
|
+
//
|
|
3
|
+
// Pure plan-execution helpers for /abap-plan --resume (B3, 2026-06-06).
|
|
4
|
+
// No I/O here — file reads, prompts, and the runTurn call live in
|
|
5
|
+
// commands/plan-resume.ts. Keeping these pure makes the next-phase
|
|
6
|
+
// picker, the tracker renderer, and the revision builder unit-testable
|
|
7
|
+
// without touching disk.
|
|
8
|
+
import chalk from 'chalk';
|
|
1
9
|
/** Statuses a dead session may have left behind — resumable, picked before fresh `todo`s. */
|
|
2
10
|
const IN_PROGRESS = ['designing', 'building', 'verifying'];
|
|
11
|
+
/**
|
|
12
|
+
* Auditor verdict resolution (2026-07-03 agentic-flow design). A phase's
|
|
13
|
+
* audit is RESOLVED when the independent auditor passed it or the user
|
|
14
|
+
* explicitly waived the findings. An ABSENT audit is resolved too — legacy
|
|
15
|
+
* envelopes (phases validated before the auditor existed) must remain
|
|
16
|
+
* satisfied, or every pre-audit plan would deadlock on resume.
|
|
17
|
+
*/
|
|
18
|
+
export function isAuditResolved(audit) {
|
|
19
|
+
if (!audit)
|
|
20
|
+
return true; // legacy envelope — pre-auditor phases stay satisfied
|
|
21
|
+
// 'warn' (confidence-tiered redesign, 2026-07-08) is the NON-BLOCKING tier:
|
|
22
|
+
// the phase passed with notes, so — exactly like a pass or an explicit waiver
|
|
23
|
+
// — it RESOLVES the audit and unlocks dependants. The notes ride along on
|
|
24
|
+
// audit.findings and are surfaced by the tracker glyph + the chain render.
|
|
25
|
+
//
|
|
26
|
+
// 'infra_failed' (audit-infra-continue, 2026-07-09) is RESOLVED too, for a
|
|
27
|
+
// different reason: the audit could not RUN (the model call returned no
|
|
28
|
+
// output after its in-turn retries). An audit that can't run is not a finding
|
|
29
|
+
// against the work — and the phase already passed its own exit gate
|
|
30
|
+
// (activation + ATC) — so walling verified work on an infra outage is wrong.
|
|
31
|
+
// It resolves like a warn: dependants unlock, the chain continues, and the
|
|
32
|
+
// "not verified" fact is surfaced (loud chain note + the ⚠ audit-not-run
|
|
33
|
+
// glyph) and recorded (state stays infra_failed; NOT laundered to waived).
|
|
34
|
+
return audit.state === 'passed' || audit.state === 'waived'
|
|
35
|
+
|| audit.state === 'warn' || audit.state === 'infra_failed';
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* C1 (2026-06-11, D30 waiver) — a phase counts as DONE for DAG purposes when
|
|
39
|
+
* it is 'validated' OR 'validated-with-waiver' (exit gate not met, but the
|
|
40
|
+
* user explicitly waived it). Single predicate so eligibility, summary
|
|
41
|
+
* counting, and the "plan complete" checks can never drift apart.
|
|
42
|
+
*
|
|
43
|
+
* 2026-07-03 (agentic-flow): additionally, when the phase carries an auditor
|
|
44
|
+
* verdict, it must be resolved (passed/waived) — a validated-but-audit-failed
|
|
45
|
+
* phase does NOT unlock dependants. The param is optional so pre-audit call
|
|
46
|
+
* sites keep their exact behaviour.
|
|
47
|
+
*/
|
|
48
|
+
export function isPhaseSatisfied(s, audit) {
|
|
49
|
+
return (s === 'validated' || s === 'validated-with-waiver') && isAuditResolved(audit);
|
|
50
|
+
}
|
|
3
51
|
/**
|
|
4
52
|
* Phase status lives in interaction.items (store convention). Build the
|
|
5
53
|
* id→status map; a phase with no matching item defaults to 'todo' rather
|
|
@@ -21,13 +69,16 @@ export function statusesFromItems(items, phases) {
|
|
|
21
69
|
* re-entered from the top, gates and grounding included.
|
|
22
70
|
*/
|
|
23
71
|
export function computeNextPhase(phases, statuses) {
|
|
72
|
+
// Audit verdicts live on the phase objects, not the status map — look each
|
|
73
|
+
// dependency up so a validated-but-audit-failed phase gates its dependants.
|
|
74
|
+
const byId = new Map(phases.map((p) => [p.id, p]));
|
|
24
75
|
for (const p of phases) {
|
|
25
76
|
const s = statuses[p.id] ?? 'todo';
|
|
26
|
-
if (s
|
|
77
|
+
if (isPhaseSatisfied(s, p.audit) || s === 'blocked')
|
|
27
78
|
continue;
|
|
28
79
|
if (s !== 'todo' && !IN_PROGRESS.includes(s))
|
|
29
80
|
continue;
|
|
30
|
-
if (p.entryCriteria.every((dep) => statuses[dep]
|
|
81
|
+
if (p.entryCriteria.every((dep) => isPhaseSatisfied(statuses[dep], byId.get(dep)?.audit)))
|
|
31
82
|
return p;
|
|
32
83
|
}
|
|
33
84
|
return null;
|
|
@@ -42,41 +93,240 @@ export function computeNextPhase(phases, statuses) {
|
|
|
42
93
|
* ▶ c1.orchestration abap-generate ← THIS SESSION
|
|
43
94
|
* ○ c1.integration abap-generate (needs c1.orchestration)
|
|
44
95
|
*/
|
|
96
|
+
// Human phase labels — a phase is named for the KIND of work it does, so a
|
|
97
|
+
// non-ABAP reader can follow the plan. The exit-gate / produced objects carry
|
|
98
|
+
// the specifics on the detail line.
|
|
99
|
+
const LAYER_LABELS = {
|
|
100
|
+
'types': 'Types',
|
|
101
|
+
'persistence': 'Tables & structures',
|
|
102
|
+
'data-model': 'CDS views',
|
|
103
|
+
'behavior': 'Behaviour & validations',
|
|
104
|
+
'service': 'Service binding',
|
|
105
|
+
'orchestration': 'Jobs & reports',
|
|
106
|
+
'integration': 'Integration',
|
|
107
|
+
'ui': 'UI',
|
|
108
|
+
};
|
|
109
|
+
const DELEGATE_LABELS = {
|
|
110
|
+
'abap-design': 'Design',
|
|
111
|
+
'abap-test': 'Tests',
|
|
112
|
+
'abap-eml': 'Behaviour logic',
|
|
113
|
+
'abap-segw': 'OData (SEGW)',
|
|
114
|
+
};
|
|
115
|
+
function phaseTitle(p) {
|
|
116
|
+
// C2 (2026-06-11, D29): an explicit seed-time title wins — the seed prose
|
|
117
|
+
// instructs short human names ("Design", "Tables", "Service"). The label
|
|
118
|
+
// maps remain the fallback for envelopes seeded before titles existed.
|
|
119
|
+
const explicit = p.title?.trim();
|
|
120
|
+
if (explicit)
|
|
121
|
+
return explicit;
|
|
122
|
+
return DELEGATE_LABELS[p.delegateTo] ?? LAYER_LABELS[p.layer] ?? tidyId(p.id);
|
|
123
|
+
}
|
|
124
|
+
function tidyId(id) {
|
|
125
|
+
const tail = id.split('.').pop() ?? id;
|
|
126
|
+
return tail.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
127
|
+
}
|
|
128
|
+
// A short plain-language hint from the exit gate: the first clause, clamped.
|
|
129
|
+
function shortGate(gate, n = 60) {
|
|
130
|
+
const first = String(gate ?? '').split(/[.;]/)[0].trim();
|
|
131
|
+
const s = first.length >= 10 ? first : String(gate ?? '').trim();
|
|
132
|
+
return s.length > n ? s.slice(0, n - 1).trimEnd() + '…' : s;
|
|
133
|
+
}
|
|
134
|
+
// The detail line: what a done phase produced, else what the phase is about.
|
|
135
|
+
function phaseDetail(p, s) {
|
|
136
|
+
if (isPhaseSatisfied(s)) {
|
|
137
|
+
const objs = (p.work?.generated ?? []).join(', ');
|
|
138
|
+
return objs || shortGate(p.exitGate);
|
|
139
|
+
}
|
|
140
|
+
return shortGate(p.exitGate);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Auditor-verdict glyph rendered after the phase status (2026-07-03,
|
|
144
|
+
* agentic-flow). Empty string when the phase carries no audit — legacy
|
|
145
|
+
* envelopes must render byte-identical to the pre-audit tracker.
|
|
146
|
+
*/
|
|
147
|
+
function auditGlyph(audit) {
|
|
148
|
+
if (!audit)
|
|
149
|
+
return '';
|
|
150
|
+
switch (audit.state) {
|
|
151
|
+
case 'passed': return ` ${chalk.green('✔ audited')}`;
|
|
152
|
+
case 'warn': {
|
|
153
|
+
// Non-blocking tier: validated-WITH-NOTES. Distinct from a clean pass and
|
|
154
|
+
// from the "not run" states — it ran, passed, and carries N notes.
|
|
155
|
+
const n = audit.findings?.length ?? 0;
|
|
156
|
+
return ` ${chalk.yellow(`⚠ ${n} note${n === 1 ? '' : 's'}`)}`;
|
|
157
|
+
}
|
|
158
|
+
case 'waived': return ` ${chalk.yellow(`✎ waived${audit.waiveCategory ? ` (${audit.waiveCategory})` : ''}`)}`;
|
|
159
|
+
case 'failed': return ` ${chalk.red('✖ audit failed')}`;
|
|
160
|
+
case 'not_audited_hand_edited': return ` ${chalk.yellow('✋ hand-edited')}`;
|
|
161
|
+
case 'pending':
|
|
162
|
+
case 'infra_failed':
|
|
163
|
+
default:
|
|
164
|
+
// `default` is deliberate forward-compat, not a missed case: an unknown
|
|
165
|
+
// future PlanAuditState renders "⚠ audit not run" instead of crashing.
|
|
166
|
+
return ` ${chalk.yellow('⚠ audit not run')}`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Plan-progress board — a Claude-Code-style checkbox task list. Each phase is
|
|
171
|
+
* named for the work it does (not its mechanical id), so the reader can follow
|
|
172
|
+
* along; ☒ = done (dim), ☐ = to do, the active phase is bold with a `◀ now`
|
|
173
|
+
* marker, blocked is red. Flows through chunkEmitter (Ink) and console.log
|
|
174
|
+
* (classic) alike — chalk degrades to plain text on a non-colour stream.
|
|
175
|
+
*/
|
|
45
176
|
export function renderPlanTracker(a) {
|
|
177
|
+
const PEACH = '#F5A623';
|
|
46
178
|
const lines = [];
|
|
47
179
|
const total = a.content.phases.length;
|
|
48
|
-
|
|
180
|
+
// Waived phases count as done in the header tally — the DAG treats them as
|
|
181
|
+
// satisfied; the per-line "✓* (waived)" marker carries the distinction.
|
|
182
|
+
// Audit-aware (2026-07-03): a validated-but-audit-failed phase must NOT
|
|
183
|
+
// count as done, or the header would say "N/N done" while the completion
|
|
184
|
+
// check refuses to complete — the tally uses the same predicate as the DAG.
|
|
185
|
+
const validated = a.content.phases.filter((p) => isPhaseSatisfied(a.statuses[p.id], p.audit)).length;
|
|
49
186
|
if (a.includeHeader !== false) {
|
|
50
|
-
lines.push(`─
|
|
187
|
+
lines.push(chalk.dim(`─ ${a.title} (v${a.version}) · ${validated}/${total} done ─`));
|
|
188
|
+
lines.push('');
|
|
51
189
|
}
|
|
52
|
-
const idWidth = Math.max(...a.content.phases.map((p) => p.id.length));
|
|
53
|
-
const skillWidth = Math.max(...a.content.phases.map((p) => p.delegateTo.length));
|
|
54
190
|
for (const p of a.content.phases) {
|
|
55
191
|
const s = a.statuses[p.id] ?? 'todo';
|
|
56
192
|
const isCurrent = a.currentId != null && p.id === a.currentId;
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
193
|
+
const title = phaseTitle(p);
|
|
194
|
+
const detail = phaseDetail(p, s);
|
|
195
|
+
const dash = detail ? ` — ${detail}` : '';
|
|
196
|
+
// Auditor verdict after the status — '' on legacy phases (no audit field),
|
|
197
|
+
// so pre-audit envelopes render byte-identical.
|
|
198
|
+
const audit = auditGlyph(p.audit);
|
|
199
|
+
if (s === 'validated') {
|
|
200
|
+
lines.push(chalk.dim(` ☒ ${title}${dash}`) + audit);
|
|
201
|
+
}
|
|
202
|
+
else if (s === 'validated-with-waiver') {
|
|
203
|
+
// C1 (D30 waiver) — done-but-waived: the exit gate was never met; the
|
|
204
|
+
// user accepted it anyway. Distinct marker so a tracker reader can tell
|
|
205
|
+
// a verified phase from a waived one at a glance.
|
|
206
|
+
lines.push(chalk.dim(` ☒ ${title}${dash}`) + ` ${chalk.yellow('✓* (waived)')}` + audit);
|
|
207
|
+
}
|
|
208
|
+
else if (s === 'blocked') {
|
|
209
|
+
lines.push(` ${chalk.red('☐')} ${chalk.red(title)} ${chalk.red('— blocked')}${detail ? chalk.dim(` · ${detail}`) : ''}${audit}`);
|
|
210
|
+
}
|
|
211
|
+
else if (isCurrent) {
|
|
212
|
+
lines.push(` ${chalk.hex(PEACH)('☐')} ${chalk.bold(title)}${dash}${audit} ${chalk.hex(PEACH).bold('◀ now')}`);
|
|
213
|
+
}
|
|
214
|
+
else if (IN_PROGRESS.includes(s)) {
|
|
215
|
+
lines.push(` ${chalk.hex(PEACH)('☐')} ${chalk.bold(title)}${dash}${audit} ${chalk.dim(`(${s})`)}`);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
lines.push(` ☐ ${title}${chalk.dim(dash)}${audit}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Off-switch footer — honesty: state plainly that this run's phases are not
|
|
222
|
+
// being audited. Rendered only when the caller threads auditsOff (in-memory).
|
|
223
|
+
if (a.auditsOff) {
|
|
224
|
+
lines.push('');
|
|
225
|
+
lines.push(chalk.dim(' audits: off'));
|
|
63
226
|
}
|
|
64
227
|
return lines.join('\n');
|
|
65
228
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
229
|
+
/* ── plan-complete chaining (C2, 2026-06-11, D24) ───────────────────────── */
|
|
230
|
+
/**
|
|
231
|
+
* SRVB-shaped object name. Two naming families seen live + documented in
|
|
232
|
+
* .claude/rules/rap-patterns.md:
|
|
233
|
+
* - ADT default OData suffix: ZUI_PM_MAINTREQ_O4 / ..._O2
|
|
234
|
+
* - rap-patterns convention: ZSalesOrder_UI_V4 / _UI_V2 / _API_V4 / _API_V2
|
|
235
|
+
*/
|
|
236
|
+
const SRVB_NAME_RE = /^[zy]\w*(?:_o[24]|_(?:ui|api)_v[24])$/i;
|
|
237
|
+
/**
|
|
238
|
+
* The exact service-binding name a completed plan produced, for the
|
|
239
|
+
* /abap-fiori-build hand-off. Resolution ladder (newest phase wins at
|
|
240
|
+
* each rung — a structurally-revised plan may carry several):
|
|
241
|
+
* 1. explicit `work.binding` (the seed/resume prose instructs the service
|
|
242
|
+
* phase to record it)
|
|
243
|
+
* 2. an SRVB-shaped name in a service-layer phase's `work.generated`
|
|
244
|
+
* 3. an SRVB-shaped name anywhere in `work.generated` (binding built
|
|
245
|
+
* outside a service-layer phase)
|
|
246
|
+
* Returns null when nothing matches — the caller still mentions
|
|
247
|
+
* /abap-fiori-build generically.
|
|
248
|
+
*/
|
|
249
|
+
export function findServiceBinding(phases) {
|
|
250
|
+
for (let i = phases.length - 1; i >= 0; i--) {
|
|
251
|
+
const explicit = phases[i].work?.binding?.trim();
|
|
252
|
+
if (explicit)
|
|
253
|
+
return explicit;
|
|
254
|
+
}
|
|
255
|
+
const scan = (ps) => {
|
|
256
|
+
for (let i = ps.length - 1; i >= 0; i--) {
|
|
257
|
+
const hit = (ps[i].work?.generated ?? []).find((n) => SRVB_NAME_RE.test(n.trim()));
|
|
258
|
+
if (hit)
|
|
259
|
+
return hit.trim();
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
};
|
|
263
|
+
return scan(phases.filter((p) => p.layer === 'service')) ?? scan(phases);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* The lines printed when a plan has no next phase because EVERY phase is
|
|
267
|
+
* satisfied. Single construction site for both call points (preparePlanResume
|
|
268
|
+
* re-opening a finished plan, finishPlanResume completing the last phase) so
|
|
269
|
+
* the D24 fiori chain can never drift between them.
|
|
270
|
+
*
|
|
271
|
+
* A UI-less backend stack (service layer present, no ui phase) chains to
|
|
272
|
+
* /abap-fiori-build — with the EXACT binding name when resolvable. A plan
|
|
273
|
+
* that already built its UI, or never built a service, gets no chain line.
|
|
274
|
+
*/
|
|
275
|
+
export function planCompletionLines(phases) {
|
|
276
|
+
const lines = ['Plan complete — every phase validated.'];
|
|
277
|
+
// Confidence-tiered redesign (2026-07-08): a completed plan that carried
|
|
278
|
+
// non-blocking warn verdicts surfaces them in the FINAL summary — a warned
|
|
279
|
+
// phase shows its notes, so an "all validated" plan never hides that a phase
|
|
280
|
+
// passed WITH reservations. (Empty when no phase warned ⇒ byte-identical to
|
|
281
|
+
// the pre-warn completion message.)
|
|
282
|
+
for (const p of phases) {
|
|
283
|
+
if (p.audit?.state !== 'warn')
|
|
284
|
+
continue;
|
|
285
|
+
const notes = p.audit.findings ?? [];
|
|
286
|
+
lines.push(notes.length > 0
|
|
287
|
+
? `⚠ ${p.id} passed with ${notes.length} note${notes.length === 1 ? '' : 's'}: ${notes.join('; ')}.`
|
|
288
|
+
: `⚠ ${p.id} passed with notes.`);
|
|
73
289
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
290
|
+
// §10 Q2 (audit-confidence redesign): a waived phase surfaces its light
|
|
291
|
+
// category + reason in the FINAL summary — an "all validated" plan never
|
|
292
|
+
// hides that a gate was a human override, and the category (false-positive
|
|
293
|
+
// vs accepted-risk) is right there for the reader. Legacy waivers without a
|
|
294
|
+
// category degrade to a bare `waived:` line (back-compat).
|
|
295
|
+
for (const p of phases) {
|
|
296
|
+
if (p.audit?.state !== 'waived')
|
|
297
|
+
continue;
|
|
298
|
+
const cat = p.audit.waiveCategory ? ` (${p.audit.waiveCategory})` : '';
|
|
299
|
+
const reason = p.audit.waivedReason ? `: ${p.audit.waivedReason}` : '';
|
|
300
|
+
lines.push(`✎ ${p.id} waived${cat}${reason}.`);
|
|
301
|
+
}
|
|
302
|
+
// audit-infra-continue (2026-07-09): a phase whose audit could NOT RUN
|
|
303
|
+
// surfaces in the FINAL summary as a DISTINCT "unverified" line — an "all
|
|
304
|
+
// validated" plan must never hide that a phase shipped without an independent
|
|
305
|
+
// audit. It is framed as an infrastructure failure (not a code finding) and
|
|
306
|
+
// NOT as a waiver (the user never asserted acceptance). Empty when no phase
|
|
307
|
+
// hit infra ⇒ byte-identical to the pre-fix completion message.
|
|
308
|
+
for (const p of phases) {
|
|
309
|
+
if (p.audit?.state !== 'infra_failed')
|
|
310
|
+
continue;
|
|
311
|
+
lines.push(`⚠ ${p.id} was NOT audit-verified (audit infrastructure failed — its own exit gate, activation + ATC, passed).`);
|
|
312
|
+
}
|
|
313
|
+
const hasUi = phases.some((p) => p.layer === 'ui' || p.delegateTo === 'abap-fiori-build');
|
|
314
|
+
const hasService = phases.some((p) => p.layer === 'service');
|
|
315
|
+
if (!hasUi && hasService) {
|
|
316
|
+
const binding = findServiceBinding(phases);
|
|
317
|
+
lines.push(binding
|
|
318
|
+
? `Backend complete. Next: /abap-fiori-build — point it at ${binding}.`
|
|
319
|
+
: 'Backend complete. Next: /abap-fiori-build — point it at the published service binding.');
|
|
320
|
+
}
|
|
321
|
+
// Last-written-wins: the LAST ui phase carrying a deployedUrl, matching
|
|
322
|
+
// buildUiPhaseContext's reverse-scan resolution (both consume ui work that
|
|
323
|
+
// ui.deploy records — direction must agree between the two sites).
|
|
324
|
+
const deployed = [...phases].reverse().find((p) => p.layer === 'ui' && p.work?.deployedUrl);
|
|
325
|
+
if (deployed?.work?.deployedUrl) {
|
|
326
|
+
lines.push(`App deployed: ${deployed.work.deployedUrl}`);
|
|
327
|
+
}
|
|
328
|
+
lines.push('Consider /abap-preflight on the produced transport(s) before release.');
|
|
329
|
+
return lines;
|
|
80
330
|
}
|
|
81
331
|
/**
|
|
82
332
|
* Build the version-N+1 envelope from the original plan envelope and the
|
|
@@ -90,7 +340,14 @@ function annotationFor(p, s, isCurrent, statuses) {
|
|
|
90
340
|
* - item comments / answers added via the editor survive the revision —
|
|
91
341
|
* only the status comes from the new extract
|
|
92
342
|
*/
|
|
93
|
-
export function buildPlanRevision(original, extract, author, nowIso
|
|
343
|
+
export function buildPlanRevision(original, extract, author, nowIso,
|
|
344
|
+
/**
|
|
345
|
+
* Task 6 (2026-07-03): optional history-entry summary. Audit-verdict
|
|
346
|
+
* revisions carry no phase-status diff, so the default "resume run — no
|
|
347
|
+
* phase status change" would hide WHY the version exists; callers like
|
|
348
|
+
* applyAuditResult pass e.g. "audit c1.design: failed (2 findings)".
|
|
349
|
+
*/
|
|
350
|
+
summaryOverride) {
|
|
94
351
|
if (original.artefactType !== 'plan') {
|
|
95
352
|
throw new Error(`buildPlanRevision expects a plan envelope, got ${original.artefactType}`);
|
|
96
353
|
}
|
|
@@ -107,7 +364,8 @@ export function buildPlanRevision(original, extract, author, nowIso) {
|
|
|
107
364
|
if (before !== after)
|
|
108
365
|
diffs.push(`${p.id}: ${before} → ${after}`);
|
|
109
366
|
}
|
|
110
|
-
const summary =
|
|
367
|
+
const summary = summaryOverride
|
|
368
|
+
?? (diffs.length > 0 ? diffs.join('; ') : 'resume run — no phase status change');
|
|
111
369
|
return {
|
|
112
370
|
...original,
|
|
113
371
|
lastEditedAt: nowIso,
|