@cspeach/cli 0.8.0 → 1.0.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 +330 -36
- package/dist/agent/providers/license-gate.js +44 -0
- package/dist/agent/skill-checkpoint.js +1 -1
- package/dist/agent/steering-queue.js +27 -0
- package/dist/agent/tool-dispatch.js +15 -0
- package/dist/approvals/canonical.js +91 -0
- package/dist/approvals/jwt.js +84 -7
- package/dist/approvals/render.js +38 -0
- package/dist/auth/org-anthropic-key.js +25 -0
- package/dist/classifier/client.js +24 -5
- package/dist/commands/config-set.js +95 -0
- package/dist/commands/login.js +31 -14
- package/dist/commands/plan-model-tier.js +83 -0
- package/dist/commands/plan-resume.js +435 -0
- package/dist/config/loader.js +102 -3
- package/dist/cost/pricing.js +14 -5
- 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/zcspeach.js +19 -4
- package/dist/one-shot.js +52 -4
- package/dist/projects/answer-blockers.js +137 -0
- package/dist/projects/email-template.js +2 -0
- package/dist/projects/extract-cca.js +108 -16
- package/dist/projects/extract-modernize.js +1 -1
- package/dist/projects/extract-plan.js +178 -0
- package/dist/projects/extract-spec-gap.js +34 -7
- package/dist/projects/extract-test-coverage.js +1 -1
- package/dist/projects/extract-upgrade.js +113 -22
- package/dist/projects/index.js +7 -1
- 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 +254 -0
- package/dist/projects/plan-schema.js +210 -0
- package/dist/projects/promote-command.js +26 -2
- package/dist/projects/promote.js +128 -0
- package/dist/projects/save-command.js +263 -20
- package/dist/projects/status.js +22 -0
- package/dist/projects/validate.js +2 -0
- 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/syntax.js +16 -1
- package/dist/renderer/thinking-heartbeat.js +13 -1
- package/dist/renderer/tool-widget.js +18 -4
- package/dist/renderer/tty.js +43 -4
- package/dist/renderer/verify-chain.js +77 -0
- package/dist/repl/at-picker.js +93 -21
- package/dist/repl/builtin-commands.js +37 -0
- package/dist/repl/early-line-buffer.js +68 -0
- package/dist/repl/inquirer-guard.js +70 -5
- package/dist/repl/numbered-menu.js +131 -0
- package/dist/repl/post-turn-status.js +2 -2
- 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-picker.js +25 -19
- package/dist/repl.js +470 -22
- 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/pending.js +27 -0
- package/dist/skill-catalog.js +54 -43
- package/dist/skills/bundled-skills.js +279 -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 +64 -21
- package/dist/tools/ask-question.js +96 -4
- package/dist/tools/capability/tool.js +74 -0
- package/dist/tools/dispatch-skill.js +22 -1
- package/dist/tools/extend-model/anchored-insert.js +810 -0
- package/dist/tools/extend-model/tool.js +188 -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-scaffold.js +45 -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/scaffold.js +39 -0
- package/dist/tools/fiori/tools.js +356 -0
- package/dist/tools/fiori/types.js +1 -0
- package/dist/tools/local-build.js +76 -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 +53 -9
- package/dist/tools/sap-write.js +530 -21
- package/dist/tools/shell/shell_exec.js +41 -6
- package/dist/tools/snapshot.js +37 -14
- package/dist/tools/subagent/background_run.js +17 -1
- 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 +84 -11
- package/dist/ui/body.js +13 -0
- package/dist/ui/command-palette.js +46 -10
- package/dist/ui/file-palette.js +44 -0
- package/dist/ui/footer.js +28 -11
- package/dist/ui/line-resolution.js +92 -0
- package/dist/ui/session-timeline.js +1 -0
- package/dist/ui/text-input.js +150 -0
- package/dist/ui/turn-status-emitter.js +52 -0
- package/dist/ui/turn-status.js +59 -0
- package/dist/ui/widgets/ask-question-modal.js +30 -2
- package/package.json +23 -4
- 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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import { extractSpecGap } from './extract-spec-gap.js';
|
|
2
4
|
import { extractDesign } from './extract-design.js';
|
|
3
5
|
import { extractEstimate } from './extract-estimate.js';
|
|
@@ -5,10 +7,15 @@ import { extractUpgradeScan, extractUpgradeProgress, extractUpgradeReport } from
|
|
|
5
7
|
import { extractCcaAssessment } from './extract-cca.js';
|
|
6
8
|
import { extractModernizeResult } from './extract-modernize.js';
|
|
7
9
|
import { extractTestCoverage } from './extract-test-coverage.js';
|
|
10
|
+
import { extractPlan } from './extract-plan.js';
|
|
8
11
|
import { buildEnvelope } from './build.js';
|
|
12
|
+
import { buildPlanRevision } from './plan-run.js';
|
|
13
|
+
import { parsePlanContent } from './plan-schema.js';
|
|
14
|
+
import { validateEnvelope } from './validate.js';
|
|
15
|
+
import { readProjectFile } from './status.js';
|
|
9
16
|
import { saveProject } from './save.js';
|
|
10
17
|
import { renderEmailTemplate } from './email-template.js';
|
|
11
|
-
import { ensureWorkspace } from './workspace.js';
|
|
18
|
+
import { ensureWorkspace, resolveWorkspacePath } from './workspace.js';
|
|
12
19
|
const SKILL_REGISTRY = {
|
|
13
20
|
'abap-spec-gap': {
|
|
14
21
|
artefactType: 'spec-gap',
|
|
@@ -29,16 +36,19 @@ const SKILL_REGISTRY = {
|
|
|
29
36
|
artefactType: 'upgrade-scan',
|
|
30
37
|
extract: (md) => extractUpgradeScan(md),
|
|
31
38
|
itemNoun: 'findings',
|
|
39
|
+
manifestMarker: 'csforge:upgrade-manifest',
|
|
32
40
|
},
|
|
33
41
|
'abap-upgrade-fix': {
|
|
34
42
|
artefactType: 'upgrade-progress',
|
|
35
43
|
extract: (md) => extractUpgradeProgress(md),
|
|
36
44
|
itemNoun: 'fixes',
|
|
45
|
+
manifestMarker: 'csforge:upgrade-manifest',
|
|
37
46
|
},
|
|
38
47
|
'abap-upgrade-verify': {
|
|
39
48
|
artefactType: 'upgrade-report',
|
|
40
49
|
extract: (md) => extractUpgradeReport(md),
|
|
41
50
|
itemNoun: 'regressions',
|
|
51
|
+
manifestMarker: 'csforge:upgrade-manifest',
|
|
42
52
|
},
|
|
43
53
|
'abap-upgrade-merge': {
|
|
44
54
|
// Merge produces a NEW upgrade-progress (consolidated) — same shape
|
|
@@ -46,11 +56,13 @@ const SKILL_REGISTRY = {
|
|
|
46
56
|
artefactType: 'upgrade-progress',
|
|
47
57
|
extract: (md) => extractUpgradeProgress(md),
|
|
48
58
|
itemNoun: 'fixes',
|
|
59
|
+
manifestMarker: 'csforge:upgrade-manifest',
|
|
49
60
|
},
|
|
50
61
|
'abap-cca': {
|
|
51
62
|
artefactType: 'cca-assessment',
|
|
52
63
|
extract: (md) => extractCcaAssessment(md),
|
|
53
64
|
itemNoun: 'classifications',
|
|
65
|
+
manifestMarker: 'csforge:cca-manifest',
|
|
54
66
|
},
|
|
55
67
|
'abap-cca-merge': {
|
|
56
68
|
// Merge produces a NEW cca-assessment (consolidated) — same shape as
|
|
@@ -58,49 +70,277 @@ const SKILL_REGISTRY = {
|
|
|
58
70
|
artefactType: 'cca-assessment',
|
|
59
71
|
extract: (md) => extractCcaAssessment(md),
|
|
60
72
|
itemNoun: 'classifications',
|
|
73
|
+
manifestMarker: 'csforge:cca-manifest',
|
|
61
74
|
},
|
|
62
75
|
'abap-modernize': {
|
|
63
76
|
artefactType: 'modernize-result',
|
|
64
77
|
extract: (md) => extractModernizeResult(md),
|
|
65
78
|
itemNoun: 'modernizations',
|
|
79
|
+
manifestMarker: 'csforge:modernize-manifest',
|
|
66
80
|
},
|
|
67
81
|
'abap-test': {
|
|
68
82
|
artefactType: 'test-coverage',
|
|
69
83
|
extract: (md) => extractTestCoverage(md),
|
|
70
84
|
itemNoun: 'tests',
|
|
85
|
+
manifestMarker: 'csforge:test-coverage-manifest',
|
|
86
|
+
},
|
|
87
|
+
'abap-plan': {
|
|
88
|
+
// 2026-06-06 B3: Mode 1 (create) save. Mode 2 (--resume) turns
|
|
89
|
+
// suppress this hook (RunTurnParams.suppressSaveHook) and persist a
|
|
90
|
+
// REVISION of the existing envelope via commands/plan-resume.ts.
|
|
91
|
+
// C1 (2026-06-11, D30): this generic path ALSO builds a revision when a
|
|
92
|
+
// prior envelope with the same title exists in the workspace — a steered
|
|
93
|
+
// "save the project file" must never fork a new identity off an existing
|
|
94
|
+
// version chain (the 49dd duplicate). The dispatch in runSaveCommand
|
|
95
|
+
// special-cases artefactType 'plan'; this `extract` is the no-prior
|
|
96
|
+
// fallback shape only.
|
|
97
|
+
artefactType: 'plan',
|
|
98
|
+
extract: (md) => extractPlan(md),
|
|
99
|
+
itemNoun: 'phases',
|
|
100
|
+
manifestMarker: 'csforge:plan-manifest',
|
|
71
101
|
},
|
|
72
102
|
};
|
|
103
|
+
/**
|
|
104
|
+
* C1 (2026-06-11, defect D22) — map an inline artifact-manifest block in the
|
|
105
|
+
* assistant output to the SKILL_REGISTRY key that knows how to extract it.
|
|
106
|
+
* Used by the post-turn save hook (agent/loop.ts:maybeOfferSave) as the
|
|
107
|
+
* fallback trigger when the routed-skill LABEL would not fire: a misrouted
|
|
108
|
+
* turn that still emitted a recognizable manifest must not silently lose the
|
|
109
|
+
* artifact. Returns null when no recognizable manifest is present (prose-only
|
|
110
|
+
* artefacts — spec-gap / design / estimate — have no inline marker and stay
|
|
111
|
+
* label-triggered).
|
|
112
|
+
*
|
|
113
|
+
* The upgrade marker is shared by three artefact shapes; the `artefact:` line
|
|
114
|
+
* inside the block disambiguates which extractor (registry skill) applies.
|
|
115
|
+
*/
|
|
116
|
+
export function detectArtifactSkill(markdown) {
|
|
117
|
+
if (/<!--\s*csforge:plan-manifest\b/.test(markdown))
|
|
118
|
+
return 'abap-plan';
|
|
119
|
+
if (/<!--\s*csforge:cca-manifest\b/.test(markdown))
|
|
120
|
+
return 'abap-cca';
|
|
121
|
+
if (/<!--\s*csforge:modernize-manifest\b/.test(markdown))
|
|
122
|
+
return 'abap-modernize';
|
|
123
|
+
if (/<!--\s*csforge:test-coverage-manifest\b/.test(markdown))
|
|
124
|
+
return 'abap-test';
|
|
125
|
+
const up = /<!--\s*csforge:upgrade-manifest\s*\n([\s\S]*?)\n\s*-->/.exec(markdown);
|
|
126
|
+
if (up) {
|
|
127
|
+
const art = /(?:^|\n)\s*artefact:\s*(\S+)/.exec(up[1] ?? '');
|
|
128
|
+
switch (art?.[1]) {
|
|
129
|
+
case 'upgrade-scan': return 'abap-upgrade-scan';
|
|
130
|
+
case 'upgrade-progress': return 'abap-upgrade-fix';
|
|
131
|
+
case 'upgrade-report': return 'abap-upgrade-verify';
|
|
132
|
+
default: return null; // marker present, artefact unknown — extractor would reject it anyway
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
// Same block grammar as extract-plan.ts:MANIFEST_RE (last block wins).
|
|
138
|
+
const PLAN_MANIFEST_RE = /<!--\s*csforge:plan-manifest\s*\n([\s\S]*?)\n\s*-->/g;
|
|
139
|
+
/**
|
|
140
|
+
* Cheap title peek into the LAST plan-manifest block — needed BEFORE the full
|
|
141
|
+
* extract because compact manifests can only be expanded against the prior
|
|
142
|
+
* envelope, and the prior envelope is found by title. Returns null on any
|
|
143
|
+
* malformation (the full extract then reports the precise error).
|
|
144
|
+
*/
|
|
145
|
+
export function peekPlanManifestTitle(markdown) {
|
|
146
|
+
const re = new RegExp(PLAN_MANIFEST_RE.source, PLAN_MANIFEST_RE.flags);
|
|
147
|
+
let m;
|
|
148
|
+
let last = null;
|
|
149
|
+
while ((m = re.exec(markdown)) !== null)
|
|
150
|
+
last = m;
|
|
151
|
+
if (!last)
|
|
152
|
+
return null;
|
|
153
|
+
try {
|
|
154
|
+
const parsed = JSON.parse(last[1]);
|
|
155
|
+
return typeof parsed.title === 'string' && parsed.title.trim().length > 0
|
|
156
|
+
? parsed.title.trim()
|
|
157
|
+
: null;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* C1 (2026-06-11, defect D30 — the 49dd duplicate) — find the existing plan
|
|
165
|
+
* envelope a generic save should REVISE instead of forking a new identity.
|
|
166
|
+
*
|
|
167
|
+
* Match key: artefactType 'plan' + same title (trimmed, case-insensitive —
|
|
168
|
+
* the title is model-emitted and may drift in casing). Among matches the
|
|
169
|
+
* HIGHEST version wins (lastEditedAt breaks ties); when a past duplicate
|
|
170
|
+
* already split the family into two ids, this picks the longer (real) chain,
|
|
171
|
+
* so subsequent saves heal onto it instead of extending the fork.
|
|
172
|
+
*
|
|
173
|
+
* Non-recursive scan of `dir` (the workspace folder where saves land);
|
|
174
|
+
* unreadable/invalid files are skipped — resolution is best-effort, never
|
|
175
|
+
* fatal to the save.
|
|
176
|
+
*/
|
|
177
|
+
export function resolvePriorPlanEnvelope(title, dir) {
|
|
178
|
+
if (!title)
|
|
179
|
+
return null;
|
|
180
|
+
const want = title.trim().toLowerCase();
|
|
181
|
+
let names;
|
|
182
|
+
try {
|
|
183
|
+
names = readdirSync(dir);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
let best = null;
|
|
189
|
+
for (const name of names) {
|
|
190
|
+
if (!name.endsWith('.cspeach.json'))
|
|
191
|
+
continue;
|
|
192
|
+
const full = join(dir, name);
|
|
193
|
+
let env;
|
|
194
|
+
try {
|
|
195
|
+
env = readProjectFile(full);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (env.artefactType !== 'plan')
|
|
201
|
+
continue;
|
|
202
|
+
if (env.title.trim().toLowerCase() !== want)
|
|
203
|
+
continue;
|
|
204
|
+
if (!best
|
|
205
|
+
|| env.version > best.envelope.version
|
|
206
|
+
|| (env.version === best.envelope.version && env.lastEditedAt > best.envelope.lastEditedAt)) {
|
|
207
|
+
best = { path: full, envelope: env };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return best;
|
|
211
|
+
}
|
|
73
212
|
export async function runSaveCommand(args) {
|
|
74
213
|
const spec = SKILL_REGISTRY[args.skillName];
|
|
75
214
|
if (!spec)
|
|
76
215
|
return null;
|
|
216
|
+
// C1 (D30): a plan save must continue an existing version chain when one is
|
|
217
|
+
// resolvable. The prior is found by manifest title BEFORE extraction, both
|
|
218
|
+
// to decide new-vs-revision and because a COMPACT manifest (statuses +
|
|
219
|
+
// changed map) can only be expanded against the prior content. The scan is
|
|
220
|
+
// strictly READ-ONLY (resolveWorkspacePath, not ensureWorkspace): nothing
|
|
221
|
+
// before the user's confirm may mkdir the workspace or touch .gitignore as
|
|
222
|
+
// a side effect — a missing workspace dir simply means no prior
|
|
223
|
+
// (resolvePriorPlanEnvelope treats an unreadable dir as "no matches").
|
|
224
|
+
let prior = null;
|
|
225
|
+
let priorContent;
|
|
226
|
+
if (spec.artefactType === 'plan') {
|
|
227
|
+
const peekedTitle = peekPlanManifestTitle(args.skillOutput);
|
|
228
|
+
if (peekedTitle) {
|
|
229
|
+
let scanDir;
|
|
230
|
+
try {
|
|
231
|
+
scanDir = await resolveWorkspacePath();
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
scanDir = args.cwd;
|
|
235
|
+
}
|
|
236
|
+
prior = resolvePriorPlanEnvelope(peekedTitle, scanDir);
|
|
237
|
+
if (prior) {
|
|
238
|
+
const pc = parsePlanContent(prior.envelope.content);
|
|
239
|
+
if (pc.ok) {
|
|
240
|
+
priorContent = pc.content;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
// Prior exists but its content no longer parses — revising it would
|
|
244
|
+
// propagate corruption. Fall back to a fresh envelope and say so.
|
|
245
|
+
args.log(`[save] existing plan '${prior.envelope.title}' has invalid content (${pc.errors[0] ?? 'schema error'}) — saving a NEW envelope instead of a revision.`);
|
|
246
|
+
prior = null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
77
251
|
let extract;
|
|
252
|
+
let planExtract = null;
|
|
78
253
|
try {
|
|
79
|
-
|
|
254
|
+
if (spec.artefactType === 'plan') {
|
|
255
|
+
planExtract = extractPlan(args.skillOutput, priorContent);
|
|
256
|
+
extract = planExtract;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
extract = spec.extract(args.skillOutput);
|
|
260
|
+
}
|
|
80
261
|
}
|
|
81
|
-
catch {
|
|
262
|
+
catch (e) {
|
|
263
|
+
// C1 residual D22 — the label IS in the registry but WRONG for the output
|
|
264
|
+
// (e.g. a plan manifest under an 'abap-design' label): the label's
|
|
265
|
+
// extractor throws, and bailing here would silently lose an artifact that
|
|
266
|
+
// carries a complete, recognizable manifest. If the manifest resolves to
|
|
267
|
+
// a DIFFERENT registry skill, retry the save under that skill. Skipped
|
|
268
|
+
// when both skills share the same manifest-marker family (abap-cca-merge
|
|
269
|
+
// ↔ abap-cca, abap-upgrade-merge ↔ abap-upgrade-fix): the family
|
|
270
|
+
// extractor already ran and failed, and re-dispatching would only launder
|
|
271
|
+
// merge provenance onto the non-merge skill — keep the label's failure.
|
|
272
|
+
const detected = detectArtifactSkill(args.skillOutput);
|
|
273
|
+
if (detected && detected !== args.skillName) {
|
|
274
|
+
const detectedSpec = SKILL_REGISTRY[detected];
|
|
275
|
+
const sameFamily = detectedSpec?.manifestMarker !== undefined
|
|
276
|
+
&& detectedSpec.manifestMarker === spec.manifestMarker;
|
|
277
|
+
if (detectedSpec && !sameFamily) {
|
|
278
|
+
args.log(`[save] output is a ${detected} artifact, not ${args.skillName} — saving as ${detected}`);
|
|
279
|
+
// Recursion is bounded: if the retry's extract fails too, the
|
|
280
|
+
// re-detected skill equals args.skillName and we fall through to the
|
|
281
|
+
// loud-failure path below instead of recursing again.
|
|
282
|
+
return runSaveCommand({ ...args, skillName: detected });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (spec.manifestMarker && args.skillOutput.includes(spec.manifestMarker)) {
|
|
286
|
+
args.log('');
|
|
287
|
+
args.log(`[save] ${args.skillName}: a ${spec.manifestMarker} block IS present but failed to parse — artefact NOT saved.`);
|
|
288
|
+
args.log(`[save] ${e instanceof Error ? e.message : String(e)}`);
|
|
289
|
+
args.log(`[save] The output above is otherwise complete — re-run the skill or report this as a manifest bug.`);
|
|
290
|
+
}
|
|
82
291
|
return null;
|
|
83
292
|
}
|
|
293
|
+
// C1 review fix — name the revision target BEFORE the confirm so the user
|
|
294
|
+
// knows what a 'y' will do. Log line only, deliberately NOT a second
|
|
295
|
+
// question: the B5 headless contract (below) allows at most one prompt.
|
|
296
|
+
if (planExtract && prior) {
|
|
297
|
+
const priorFilename = prior.path.split(/[\\\/]/).pop();
|
|
298
|
+
args.log(`[save] this will save as v${prior.envelope.version + 1} of existing plan '${prior.envelope.title}' (${priorFilename})`);
|
|
299
|
+
}
|
|
300
|
+
// CONTRACT (B5): runSaveCommand may ask the user at most ONE question —
|
|
301
|
+
// this save confirm. The headless path in loop.ts (maybeOfferSave) installs
|
|
302
|
+
// a prompt that blanket-answers 'y' to WHATEVER it is asked; any second
|
|
303
|
+
// question added here would silently receive 'y' too. If you need another
|
|
304
|
+
// prompt, give maybeOfferSave's headless prompt a question-aware answer
|
|
305
|
+
// first.
|
|
84
306
|
const answer = (await args.prompt('Save as project file? [y/N]: ')).trim().toLowerCase();
|
|
85
307
|
if (answer !== 'y' && answer !== 'yes')
|
|
86
308
|
return null;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
309
|
+
let env;
|
|
310
|
+
if (planExtract && prior) {
|
|
311
|
+
// C1 (D30) — the 49dd duplicate: the generic save used to mint a fresh
|
|
312
|
+
// envelope id even when the same plan already lived in the workspace,
|
|
313
|
+
// forking the version chain. With a resolvable prior, the save IS a
|
|
314
|
+
// revision: same id, version+1, history appended (buildPlanRevision —
|
|
315
|
+
// the exact path /abap-plan --resume persistence uses).
|
|
316
|
+
env = buildPlanRevision(prior.envelope, planExtract, args.author, new Date().toISOString());
|
|
317
|
+
// JSON round-trip simulates disk serialization for the validator.
|
|
318
|
+
const check = validateEnvelope(JSON.parse(JSON.stringify(env)));
|
|
319
|
+
if (!check.ok) {
|
|
320
|
+
args.log(`[save] plan revision failed validation (${check.error.message}) — artefact NOT saved.`);
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
env = buildEnvelope({
|
|
326
|
+
artefactType: spec.artefactType,
|
|
327
|
+
extract,
|
|
328
|
+
author: args.author,
|
|
329
|
+
source: {
|
|
330
|
+
skill: args.skillName,
|
|
331
|
+
skillVersion: args.skillVersion,
|
|
332
|
+
input: args.skillInput,
|
|
333
|
+
tokensUsed: args.tokensUsed,
|
|
334
|
+
model: args.model,
|
|
335
|
+
},
|
|
336
|
+
promotedFrom: args.promotedFrom ?? null,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
// Saves land in the workspace folder, not cwd — created only NOW, after
|
|
340
|
+
// the user confirmed (declined saves and conversational turns leave no
|
|
341
|
+
// mkdir/.gitignore side effects). Falls back to args.cwd if the workspace
|
|
342
|
+
// can't be created (e.g. permission error on the home directory) — saving
|
|
343
|
+
// SOMEWHERE is better than losing the artefact.
|
|
104
344
|
let outDir;
|
|
105
345
|
try {
|
|
106
346
|
outDir = await ensureWorkspace();
|
|
@@ -110,6 +350,9 @@ export async function runSaveCommand(args) {
|
|
|
110
350
|
}
|
|
111
351
|
const path = await saveProject(env, { cwd: outDir });
|
|
112
352
|
args.log(`Saved: ${path}`);
|
|
353
|
+
if (planExtract && prior) {
|
|
354
|
+
args.log(`(revision v${env.version} of the existing plan — version chain preserved, no new id)`);
|
|
355
|
+
}
|
|
113
356
|
args.log(`Share at: https://cspeach.dev/edit`);
|
|
114
357
|
args.log('');
|
|
115
358
|
args.log(renderEmailTemplate({
|
package/dist/projects/status.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { validateEnvelope } from './validate.js';
|
|
3
3
|
import { canonicalSha256 } from './canonicalize.js';
|
|
4
|
+
import { statusesFromItems, computeNextPhase, renderPlanTracker } from './plan-run.js';
|
|
4
5
|
export function readProjectFile(path) {
|
|
5
6
|
const raw = readFileSync(path, 'utf8');
|
|
6
7
|
let parsed;
|
|
@@ -22,6 +23,7 @@ const STATUS_BY_TYPE = {
|
|
|
22
23
|
'estimate': ['confirmed', 'disputed', 'deferred'],
|
|
23
24
|
'cca': ['keep', 'fix', 'retire', 'redesign'],
|
|
24
25
|
'incident': ['pending', 'done'],
|
|
26
|
+
'plan': ['validated', 'validated-with-waiver', 'todo', 'designing', 'building', 'verifying', 'blocked'],
|
|
25
27
|
};
|
|
26
28
|
const PRIMARY_BY_TYPE = {
|
|
27
29
|
'spec-gap': 'answered',
|
|
@@ -29,6 +31,7 @@ const PRIMARY_BY_TYPE = {
|
|
|
29
31
|
'estimate': 'confirmed',
|
|
30
32
|
'cca': 'keep',
|
|
31
33
|
'incident': 'done',
|
|
34
|
+
'plan': 'validated',
|
|
32
35
|
};
|
|
33
36
|
const PROMOTE_HINT_BY_TYPE = {
|
|
34
37
|
'spec-gap': '/abap-design --from',
|
|
@@ -36,6 +39,7 @@ const PROMOTE_HINT_BY_TYPE = {
|
|
|
36
39
|
'estimate': null,
|
|
37
40
|
'cca': null,
|
|
38
41
|
'incident': null,
|
|
42
|
+
'plan': '/abap-plan --resume',
|
|
39
43
|
};
|
|
40
44
|
export function renderStatus(env) {
|
|
41
45
|
const items = env.interaction.items;
|
|
@@ -85,6 +89,22 @@ export function renderStatus(env) {
|
|
|
85
89
|
lines.push(` ... ${stillOpen.length - 10} more`);
|
|
86
90
|
lines.push('');
|
|
87
91
|
}
|
|
92
|
+
// B3 (2026-06-06): plan envelopes get the phase tracker board — the same
|
|
93
|
+
// renderer the --resume flow prints before/after each phase. Header off:
|
|
94
|
+
// title + version were already printed above.
|
|
95
|
+
if (env.artefactType === 'plan') {
|
|
96
|
+
const content = env.content;
|
|
97
|
+
const statuses = statusesFromItems(items, content.phases);
|
|
98
|
+
lines.push(renderPlanTracker({
|
|
99
|
+
title: env.title,
|
|
100
|
+
version: env.version,
|
|
101
|
+
content,
|
|
102
|
+
statuses,
|
|
103
|
+
currentId: computeNextPhase(content.phases, statuses)?.id ?? null,
|
|
104
|
+
includeHeader: false,
|
|
105
|
+
}));
|
|
106
|
+
lines.push('');
|
|
107
|
+
}
|
|
88
108
|
// Promote hint fires whenever a "next phase" exists for this artefactType.
|
|
89
109
|
// Unlike the v1 implementation we no longer gate on `primaryCount > 0` —
|
|
90
110
|
// a design with zero `keep` items (all `redesign` / `defer`) is still a
|
|
@@ -100,6 +120,8 @@ export function renderStatus(env) {
|
|
|
100
120
|
function displayStatus(s) {
|
|
101
121
|
if (s === 'n-a')
|
|
102
122
|
return 'n/a';
|
|
123
|
+
if (s === 'validated-with-waiver')
|
|
124
|
+
return 'validated* (waived)';
|
|
103
125
|
return s;
|
|
104
126
|
}
|
|
105
127
|
function formatDate(iso) {
|
|
@@ -5,6 +5,7 @@ const KNOWN_ARTEFACT_TYPES = [
|
|
|
5
5
|
'upgrade-scan', 'upgrade-progress', 'upgrade-report',
|
|
6
6
|
'cca-assessment',
|
|
7
7
|
'modernize-result', 'test-coverage',
|
|
8
|
+
'plan',
|
|
8
9
|
];
|
|
9
10
|
const STATUS_BY_TYPE = {
|
|
10
11
|
'spec-gap': ['open', 'answered', 'deferred', 'n-a'],
|
|
@@ -18,6 +19,7 @@ const STATUS_BY_TYPE = {
|
|
|
18
19
|
'cca-assessment': ['open', 'reviewed', 'classified', 'archived'],
|
|
19
20
|
'modernize-result': ['applied', 'skipped', 'failed', 'pending'],
|
|
20
21
|
'test-coverage': ['generated', 'skipped', 'failed', 'pending'],
|
|
22
|
+
'plan': ['todo', 'designing', 'building', 'verifying', 'validated', 'validated-with-waiver', 'blocked'],
|
|
21
23
|
};
|
|
22
24
|
const REQUIRED_TOP_LEVEL = [
|
|
23
25
|
'schemaVersion', 'artefactType', 'id', 'title',
|