@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
package/dist/cost/pricing.js
CHANGED
|
@@ -11,16 +11,25 @@
|
|
|
11
11
|
* BOTH this file AND the proxy's version. Verify on the public pricing page:
|
|
12
12
|
* https://www.anthropic.com/pricing
|
|
13
13
|
*
|
|
14
|
-
* Last verified 2026-
|
|
14
|
+
* Last verified 2026-06-07. Same as proxy.
|
|
15
|
+
*
|
|
16
|
+
* 2026-06-07 SYNC: e294cff switched the default model to claude-opus-4-8 AND
|
|
17
|
+
* applied the 3x Opus correction, but only to the proxy copy — this CLI copy
|
|
18
|
+
* was left stale, so every opus-4-8 turn was an unknown model (footer $0) and
|
|
19
|
+
* opus-4-5/4-6/4-7 still carried the legacy $15/$75 (~3x overstated). Brought
|
|
20
|
+
* back in line with cspeach-proxy/src/anthropic/pricing.ts below.
|
|
15
21
|
*
|
|
16
22
|
* Unknown models → returns null + records the model name to a process-level
|
|
17
23
|
* Set so we surface them as an obvious warning rather than silently zeroing.
|
|
18
24
|
*/
|
|
19
25
|
const PRICING = {
|
|
20
|
-
// Claude Opus 4.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
'claude-opus-4-
|
|
26
|
+
// Claude Opus 4.5+ — input 5, output 25, cache_read 0.50, cache_write 6.25 (1.25x input).
|
|
27
|
+
// 2026-06-07 correction: 4.5/4.6/4.7 were carried at the LEGACY $15/$75; actual
|
|
28
|
+
// since Opus 4.5 is $5/$25. Only claude-opus-4 (4.0/4.1) genuinely was $15/$75.
|
|
29
|
+
'claude-opus-4-8': { inputPer1M: 5.00, outputPer1M: 25.00, cacheReadPer1M: 0.50, cacheCreatePer1M: 6.25 },
|
|
30
|
+
'claude-opus-4-7': { inputPer1M: 5.00, outputPer1M: 25.00, cacheReadPer1M: 0.50, cacheCreatePer1M: 6.25 },
|
|
31
|
+
'claude-opus-4-6': { inputPer1M: 5.00, outputPer1M: 25.00, cacheReadPer1M: 0.50, cacheCreatePer1M: 6.25 },
|
|
32
|
+
'claude-opus-4-5': { inputPer1M: 5.00, outputPer1M: 25.00, cacheReadPer1M: 0.50, cacheCreatePer1M: 6.25 },
|
|
24
33
|
'claude-opus-4': { inputPer1M: 15.00, outputPer1M: 75.00, cacheReadPer1M: 1.50, cacheCreatePer1M: 18.75 },
|
|
25
34
|
// Claude Sonnet 4.x family — input 3, output 15, cache_read 0.30, cache_write 3.75
|
|
26
35
|
'claude-sonnet-4-6': { inputPer1M: 3.00, outputPer1M: 15.00, cacheReadPer1M: 0.30, cacheCreatePer1M: 3.75 },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import tls from 'node:tls';
|
|
2
|
-
import { loadConfig } from '../../config/loader.js';
|
|
2
|
+
import { loadConfig, resolveSapTls } from '../../config/loader.js';
|
|
3
3
|
export async function checkCert() {
|
|
4
4
|
const cfg = await loadConfig();
|
|
5
5
|
const systems = Object.entries(cfg.sap).filter(([_, s]) => s.useSsl);
|
|
@@ -7,14 +7,25 @@ export async function checkCert() {
|
|
|
7
7
|
return { name: 'cert', ok: true, message: 'n/a (no TLS SAP systems configured)' };
|
|
8
8
|
const results = [];
|
|
9
9
|
for (const [alias, sys] of systems) {
|
|
10
|
+
const tlsMode = resolveSapTls(sys);
|
|
10
11
|
const info = await new Promise((resolve) => {
|
|
12
|
+
// Probe with rejectUnauthorized:false so we can always read the cert and
|
|
13
|
+
// report the OS-trust chain status, then describe the configured posture
|
|
14
|
+
// separately (verify / CA-trusted / INSECURE).
|
|
11
15
|
const socket = tls.connect({ host: sys.host, port: sys.port, rejectUnauthorized: false, timeout: 5000 }, () => {
|
|
12
16
|
const cert = socket.getPeerCertificate();
|
|
13
17
|
const authorized = socket.authorized;
|
|
14
18
|
socket.end();
|
|
15
19
|
const chainValid = authorized ? 'yes' : 'no';
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
// What the configured client mode means for acceptance:
|
|
21
|
+
// verify → accepts only if the OS/Node trust chain validates
|
|
22
|
+
// CA-trusted → accepts via the configured CA (verification ON)
|
|
23
|
+
// INSECURE → accepts anything (verification OFF — dev only)
|
|
24
|
+
const clientAccepted = tlsMode.mode === 'insecure' ? 'yes (INSECURE — verification disabled, dev only)'
|
|
25
|
+
: tlsMode.mode === 'ca-trusted' ? 'yes (CA-trusted — verification ON via configured CA)'
|
|
26
|
+
: (authorized ? 'yes' : 'no (verify ON — configure ca_cert_path to trust an internal CA)');
|
|
27
|
+
resolve(`${alias}: mode: ${tlsMode.mode} / chain valid: ${chainValid} / ` +
|
|
28
|
+
`client accepted: ${clientAccepted} / subject: ${cert.subject?.CN ?? '?'}`);
|
|
18
29
|
});
|
|
19
30
|
socket.on('error', (err) => resolve(`${alias}: ${err.message}`));
|
|
20
31
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { loadConfig, resolveSapTls } from '../../config/loader.js';
|
|
2
3
|
import { probeGet } from './_http-probe.js';
|
|
3
4
|
export async function checkSap() {
|
|
4
5
|
const cfg = await loadConfig();
|
|
@@ -9,23 +10,44 @@ export async function checkSap() {
|
|
|
9
10
|
let anyFailed = false;
|
|
10
11
|
for (const [alias, sys] of systems) {
|
|
11
12
|
const url = `${sys.useSsl ? 'https' : 'http'}://${sys.host}:${sys.port}/sap/bc/adt/discovery`;
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
13
|
+
// TLS posture (2026-06-14): verification ON by default. resolveSapTls maps
|
|
14
|
+
// the per-alias config (ca_cert_path / insecure_skip_tls_verify, legacy
|
|
15
|
+
// sslVerify:false fallback). Node's built-in fetch() doesn't expose a
|
|
16
|
+
// per-request TLS-verify toggle, so we use node's https directly — see
|
|
17
|
+
// checks/_http-probe.ts.
|
|
18
|
+
const tls = resolveSapTls(sys);
|
|
19
|
+
// Tag the connectivity result with the cert mode so doctor surfaces it.
|
|
20
|
+
const modeTag = tls.mode === 'verify' ? 'verify'
|
|
21
|
+
: tls.mode === 'ca-trusted' ? 'CA-trusted'
|
|
22
|
+
: 'INSECURE';
|
|
23
|
+
let ca;
|
|
24
|
+
if (tls.mode === 'ca-trusted' && tls.caCertPath) {
|
|
25
|
+
try {
|
|
26
|
+
ca = await readFile(tls.caCertPath);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
// Fail closed, like the connection path: a configured-but-unreadable CA
|
|
30
|
+
// is an error, not a reason to drop verification.
|
|
31
|
+
results.push(`${alias}: CA unreadable at ${tls.caCertPath} (${e.message})`);
|
|
32
|
+
anyFailed = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
15
36
|
const r = await probeGet(url, {
|
|
16
|
-
rejectUnauthorized:
|
|
37
|
+
rejectUnauthorized: tls.mode !== 'insecure',
|
|
38
|
+
ca,
|
|
17
39
|
timeoutMs: 8000,
|
|
18
40
|
});
|
|
19
41
|
if (r.error) {
|
|
20
|
-
results.push(`${alias}: ${r.error}`);
|
|
42
|
+
results.push(`${alias}: ${r.error} [${modeTag}]`);
|
|
21
43
|
anyFailed = true;
|
|
22
44
|
}
|
|
23
45
|
else if (r.status === 200 || r.status === 401) {
|
|
24
46
|
// 200 = anonymous discovery allowed; 401 = reachable + auth-required (expected, fine)
|
|
25
|
-
results.push(`${alias}: reachable`);
|
|
47
|
+
results.push(`${alias}: reachable [${modeTag}]`);
|
|
26
48
|
}
|
|
27
49
|
else {
|
|
28
|
-
results.push(`${alias}: HTTP ${r.status}`);
|
|
50
|
+
results.push(`${alias}: HTTP ${r.status} [${modeTag}]`);
|
|
29
51
|
anyFailed = true;
|
|
30
52
|
}
|
|
31
53
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import keytar from 'keytar';
|
|
2
|
-
import {
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { loadConfig, resolveSapTls } from '../../config/loader.js';
|
|
3
4
|
import { probeGet } from './_http-probe.js';
|
|
4
5
|
const SAP_SERVICE = 'cspeach-sap';
|
|
5
6
|
export async function checkZcspeach() {
|
|
@@ -29,10 +30,24 @@ export async function checkZcspeach() {
|
|
|
29
30
|
results.push(`${alias}: skipped (no password in keychain)`);
|
|
30
31
|
continue;
|
|
31
32
|
}
|
|
32
|
-
//
|
|
33
|
-
//
|
|
33
|
+
// TLS posture (2026-06-14): verification ON by default; resolveSapTls maps
|
|
34
|
+
// the per-alias config (ca_cert_path / insecure_skip_tls_verify, legacy
|
|
35
|
+
// sslVerify:false fallback). See checks/_http-probe.ts.
|
|
36
|
+
const tls = resolveSapTls(sys);
|
|
37
|
+
let ca;
|
|
38
|
+
if (tls.mode === 'ca-trusted' && tls.caCertPath) {
|
|
39
|
+
try {
|
|
40
|
+
ca = await readFile(tls.caCertPath);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
results.push(`${alias}: CA unreadable at ${tls.caCertPath} (${e.message})`);
|
|
44
|
+
anyFailed = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
34
48
|
const r = await probeGet(url, {
|
|
35
|
-
rejectUnauthorized:
|
|
49
|
+
rejectUnauthorized: tls.mode !== 'insecure',
|
|
50
|
+
ca,
|
|
36
51
|
auth: `${sys.username}:${pwd}`,
|
|
37
52
|
accept: 'application/json',
|
|
38
53
|
timeoutMs: 5000,
|
package/dist/one-shot.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { loadConfig } from './config/loader.js';
|
|
2
|
+
import { loadConfig, resolveLocalBuild } from './config/loader.js';
|
|
3
3
|
import { getStore } from './auth/api-key.js';
|
|
4
4
|
import { getConnection } from './sap/connection-manager.js';
|
|
5
5
|
import { AdtClient } from '@cspeach/sap-client';
|
|
6
6
|
import { makeSessionId, saveSession } from './session/store.js';
|
|
7
7
|
import { newSession } from './session/schema.js';
|
|
8
8
|
import { createProviderForMode } from './agent/providers/factory.js';
|
|
9
|
+
import { assertModeLicensed, CspeachLicenseError } from './agent/providers/license-gate.js';
|
|
9
10
|
import { runTurn } from './agent/loop.js';
|
|
10
|
-
import { classifyPrompt, parseShortcut } from './router/classifier.js';
|
|
11
|
-
import { setRenderingOverride } from './renderer/tty.js';
|
|
11
|
+
import { classifyPrompt, parseShortcut, rejectUnknownSkill } from './router/classifier.js';
|
|
12
|
+
import { setRenderingOverride, setHeadlessOverride } from './renderer/tty.js';
|
|
12
13
|
import { alwaysDeclinePreviewHook } from './repl/update-method-preview-hook.js';
|
|
14
|
+
import { createClassicOutputEmitter } from './renderer/notices.js';
|
|
15
|
+
import { applyLocalBuildConfig } from './tools/local-build.js';
|
|
13
16
|
// Side-effect imports for tool registration.
|
|
14
17
|
import './tools/sap-read.js';
|
|
15
18
|
import './tools/sap-write.js';
|
|
@@ -19,16 +22,22 @@ import './tools/snapshot.js';
|
|
|
19
22
|
import './tools/ask-question.js';
|
|
20
23
|
import './tools/dispatch-skill.js';
|
|
21
24
|
import './tools/filesystem/file-read.js';
|
|
25
|
+
import './tools/filesystem/read-document.js';
|
|
22
26
|
import './tools/filesystem/file-edit.js';
|
|
23
27
|
import './tools/filesystem/file-write.js';
|
|
24
28
|
import './tools/filesystem/glob.js';
|
|
25
29
|
import './tools/filesystem/grep.js';
|
|
26
30
|
import './tools/shell/shell_exec.js';
|
|
31
|
+
import './tools/fiori/tools.js';
|
|
32
|
+
import './tools/extend-model/tool.js';
|
|
33
|
+
import './tools/capability/tool.js';
|
|
27
34
|
import './tools/web/web_fetch.js';
|
|
28
35
|
import './tools/web/web_search.js';
|
|
29
36
|
import './tools/project/project_context_get.js';
|
|
30
37
|
import './tools/project/playbook_get.js';
|
|
31
38
|
import './tools/project/convention_get.js';
|
|
39
|
+
import './tools/project/cca_merge.js';
|
|
40
|
+
import './tools/project/upgrade_merge_progress.js';
|
|
32
41
|
import './tools/subagent/_background-shared.js';
|
|
33
42
|
import './tools/subagent/background_run.js';
|
|
34
43
|
import './tools/subagent/monitor_emit.js';
|
|
@@ -37,12 +46,27 @@ import './tools/subagent/agent_run.js';
|
|
|
37
46
|
export async function runOneShot(prompt, opts) {
|
|
38
47
|
const cfg = await loadConfig();
|
|
39
48
|
setRenderingOverride(cfg.ui.rendering);
|
|
49
|
+
// B5 (2026-06-11) — one-shot is headless by design: a single turn, no
|
|
50
|
+
// REPL, often piped (CI / battery). Any stdin prompt would hang forever
|
|
51
|
+
// (defect D1). The headless answer policy (ask_question auto-answers,
|
|
52
|
+
// save hook defaults YES, approvals fail fast) keys off this flag.
|
|
53
|
+
setHeadlessOverride(true);
|
|
40
54
|
// Task C.10 — advisory-only is the only mode the one-shot announces, because
|
|
41
55
|
// non-interactive output should stay minimal. The user needs to know that
|
|
42
56
|
// writes will NOT be executed before a skill that normally writes runs.
|
|
43
57
|
if (cfg.write_mode === 'advisory-only') {
|
|
44
58
|
console.log(chalk.yellow('⚠ Advisory mode: writes will be proposed, not executed.'));
|
|
45
59
|
}
|
|
60
|
+
// local_build toggle — force-enable the flag-gated filesystem tools AND
|
|
61
|
+
// shell_exec BEFORE the (single) turn when persisted-on. Resolve with backward
|
|
62
|
+
// compat (legacy `local_files = on` still applies). Announce so headless
|
|
63
|
+
// transcripts record that local file access + command execution were live
|
|
64
|
+
// (trust-visibility / auditability).
|
|
65
|
+
{
|
|
66
|
+
const announce = applyLocalBuildConfig(resolveLocalBuild(cfg), process.cwd());
|
|
67
|
+
if (announce)
|
|
68
|
+
console.log(chalk.dim(announce));
|
|
69
|
+
}
|
|
46
70
|
const store = await getStore();
|
|
47
71
|
if (!(await store.get())) {
|
|
48
72
|
console.error('No API key configured. Run `cspeach` (interactive) first to set one.');
|
|
@@ -66,6 +90,14 @@ export async function runOneShot(prompt, opts) {
|
|
|
66
90
|
const body = parsed.prompt;
|
|
67
91
|
let skill;
|
|
68
92
|
if (parsed.skill) {
|
|
93
|
+
// D35 (2026-06-11) — explicit slash naming a skill that isn't in the
|
|
94
|
+
// catalog. One-shot has no picker, so fail fast with the notice ("did
|
|
95
|
+
// you mean" / builtin-with-args) instead of dispatching to a
|
|
96
|
+
// nonexistent skill (and NEVER falling through to the classifier with
|
|
97
|
+
// the slash embedded). Shared gate: rejectUnknownSkill in classifier.ts.
|
|
98
|
+
if (rejectUnknownSkill(parsed, (m) => console.error(chalk.red(m)))) {
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
69
101
|
skill = parsed.skill;
|
|
70
102
|
}
|
|
71
103
|
else {
|
|
@@ -94,6 +126,19 @@ export async function runOneShot(prompt, opts) {
|
|
|
94
126
|
}
|
|
95
127
|
skill = c.skill;
|
|
96
128
|
}
|
|
129
|
+
// License gate: non-managed modes (byok/local/ai-hub) bypass our proxy for
|
|
130
|
+
// inference, so without this anyone who never logged in could use the bundled
|
|
131
|
+
// skills untracked. Require a valid CSPeach login. (See license-gate.ts.)
|
|
132
|
+
try {
|
|
133
|
+
await assertModeLicensed(cfg.llm.mode, async () => (await store.get()) ?? '');
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
if (e instanceof CspeachLicenseError) {
|
|
137
|
+
console.error(chalk.yellow(`\n${e.message}\n`));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
throw e;
|
|
141
|
+
}
|
|
97
142
|
const provider = createProviderForMode(cfg, async () => {
|
|
98
143
|
const key = await store.get();
|
|
99
144
|
if (!key)
|
|
@@ -104,7 +149,10 @@ export async function runOneShot(prompt, opts) {
|
|
|
104
149
|
provider,
|
|
105
150
|
userMessage: body,
|
|
106
151
|
skill,
|
|
107
|
-
|
|
152
|
+
// B2 fix (2026-06-12): one-shot runs render tool notices ('warn'/'info'
|
|
153
|
+
// — Rule 9 transport overrides, snapshot lines) to stdout instead of
|
|
154
|
+
// dropping them. Headless transcripts need these for auditability.
|
|
155
|
+
ctx: { adt, sapAlias: alias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: alwaysDeclinePreviewHook, chunkEmitter: createClassicOutputEmitter() },
|
|
108
156
|
});
|
|
109
157
|
return 0;
|
|
110
158
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { readProjectFile } from './status.js';
|
|
3
|
+
import { validateEnvelope } from './validate.js';
|
|
4
|
+
/**
|
|
5
|
+
* The still-open BLOCKER-severity questions of a spec-gap envelope, in item
|
|
6
|
+
* order. Returns [] for any non-spec-gap artefact (only spec-gap has gaps with
|
|
7
|
+
* a severity + an `answered` status). Carries any candidate `options` so the
|
|
8
|
+
* answer UI can offer choices instead of a bare text box.
|
|
9
|
+
*/
|
|
10
|
+
export function openBlockers(env) {
|
|
11
|
+
if (env.artefactType !== 'spec-gap')
|
|
12
|
+
return [];
|
|
13
|
+
const gapById = new Map();
|
|
14
|
+
for (const s of env.content.sections) {
|
|
15
|
+
for (const g of s.gaps)
|
|
16
|
+
gapById.set(g.id, g);
|
|
17
|
+
}
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const it of env.interaction.items) {
|
|
20
|
+
if (it.status !== 'open')
|
|
21
|
+
continue;
|
|
22
|
+
const g = gapById.get(it.id);
|
|
23
|
+
if (g?.severity !== 'BLOCKER')
|
|
24
|
+
continue;
|
|
25
|
+
const ob = { id: it.id, text: g.text };
|
|
26
|
+
if (g.options && g.options.length > 0) {
|
|
27
|
+
ob.options = g.options;
|
|
28
|
+
if (g.optionsMultiSelect)
|
|
29
|
+
ob.multiSelect = true;
|
|
30
|
+
}
|
|
31
|
+
out.push(ob);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Return a copy of `env` with each item in `answers` marked answered. Keys with
|
|
37
|
+
* a blank value are ignored (treated as skipped). version + history are left
|
|
38
|
+
* untouched (invariant version === history.length holds); lastEditedAt/By move
|
|
39
|
+
* to the answerer to reflect who filled them.
|
|
40
|
+
*/
|
|
41
|
+
export function applyBlockerAnswers(env, answers, answerer, nowIso) {
|
|
42
|
+
const items = env.interaction.items.map((it) => {
|
|
43
|
+
const a = answers.get(it.id);
|
|
44
|
+
if (a === undefined || a.trim().length === 0)
|
|
45
|
+
return it;
|
|
46
|
+
return {
|
|
47
|
+
...it,
|
|
48
|
+
status: 'answered',
|
|
49
|
+
answer: a,
|
|
50
|
+
answeredBy: answerer,
|
|
51
|
+
answeredAt: nowIso,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
...env,
|
|
56
|
+
interaction: { items },
|
|
57
|
+
lastEditedAt: nowIso,
|
|
58
|
+
lastEditedBy: answerer,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function affirmative(reply) {
|
|
62
|
+
const v = reply.trim().toLowerCase();
|
|
63
|
+
return v === 'y' || v === 'yes';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Offer to answer the open blockers of the just-saved spec-gap file inline.
|
|
67
|
+
* No-op (no prompt) when the file isn't a spec-gap or has no open blockers, so
|
|
68
|
+
* it is always safe to call after any save. In headless runs the prompt returns
|
|
69
|
+
* '' → declined → nothing happens.
|
|
70
|
+
*
|
|
71
|
+
* Returns the count recorded, or null when nothing was offered/written.
|
|
72
|
+
*/
|
|
73
|
+
export async function offerAnswerBlockers(args) {
|
|
74
|
+
let env;
|
|
75
|
+
try {
|
|
76
|
+
env = readProjectFile(args.path);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const blockers = openBlockers(env);
|
|
82
|
+
if (blockers.length === 0)
|
|
83
|
+
return null;
|
|
84
|
+
const reply = await args.prompt(`Answer the ${blockers.length} unanswered blocker(s) now? [y/N]: `);
|
|
85
|
+
if (!affirmative(reply))
|
|
86
|
+
return null;
|
|
87
|
+
const answers = new Map();
|
|
88
|
+
for (let i = 0; i < blockers.length; i++) {
|
|
89
|
+
const b = blockers[i];
|
|
90
|
+
args.log('');
|
|
91
|
+
args.log(`Blocker ${i + 1}/${blockers.length}: ${b.text}`);
|
|
92
|
+
// Choices first, where the question has them and a picker is available.
|
|
93
|
+
// 'write' falls through to the text box; 'skip' leaves the blocker open.
|
|
94
|
+
let askText = true;
|
|
95
|
+
if (b.options && b.options.length > 0 && args.choose) {
|
|
96
|
+
const c = await args.choose({
|
|
97
|
+
question: b.text,
|
|
98
|
+
options: b.options,
|
|
99
|
+
multiSelect: !!b.multiSelect,
|
|
100
|
+
index: i,
|
|
101
|
+
total: blockers.length,
|
|
102
|
+
});
|
|
103
|
+
if (c.kind === 'picked') {
|
|
104
|
+
if (c.answer.trim().length > 0)
|
|
105
|
+
answers.set(b.id, c.answer.trim());
|
|
106
|
+
askText = false;
|
|
107
|
+
}
|
|
108
|
+
else if (c.kind === 'skip') {
|
|
109
|
+
askText = false;
|
|
110
|
+
}
|
|
111
|
+
// c.kind === 'write' → askText stays true
|
|
112
|
+
}
|
|
113
|
+
if (askText) {
|
|
114
|
+
const a = (await args.prompt(' Your answer (Enter to skip): ')).trim();
|
|
115
|
+
if (a.length > 0)
|
|
116
|
+
answers.set(b.id, a);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (answers.size === 0) {
|
|
120
|
+
args.log('');
|
|
121
|
+
args.log('No answers entered — all blockers left open for the viewer.');
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const nowIso = (args.now ?? (() => new Date().toISOString()))();
|
|
125
|
+
const revised = applyBlockerAnswers(env, answers, args.identity, nowIso);
|
|
126
|
+
const check = validateEnvelope(JSON.parse(JSON.stringify(revised)));
|
|
127
|
+
if (!check.ok) {
|
|
128
|
+
args.log(`[answer] revision invalid (${check.error.message}) — answers NOT saved.`);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
writeFileSync(args.path, JSON.stringify(revised, null, 2), 'utf8');
|
|
132
|
+
const stillOpen = blockers.length - answers.size;
|
|
133
|
+
args.log('');
|
|
134
|
+
args.log(`Recorded ${answers.size} answer(s) in the file`
|
|
135
|
+
+ (stillOpen > 0 ? ` — ${stillOpen} blocker(s) still open for the viewer.` : '.'));
|
|
136
|
+
return answers.size;
|
|
137
|
+
}
|
|
@@ -11,6 +11,7 @@ const SUBJECT_BY_TYPE = {
|
|
|
11
11
|
'cca-assessment': (t) => `Custom code assessment — ${t}`,
|
|
12
12
|
'modernize-result': (t) => `Modernization result — ${t}`,
|
|
13
13
|
'test-coverage': (t) => `Test coverage report — ${t}`,
|
|
14
|
+
'plan': (t) => `Project plan — ${t}`,
|
|
14
15
|
};
|
|
15
16
|
const BODY_BY_TYPE = {
|
|
16
17
|
'spec-gap': (a) => `Attached is the gap analysis for the ${a.title}.\n${a.itemCount} ${a.itemNoun} across business / data / authorisation / edge cases.`,
|
|
@@ -24,6 +25,7 @@ const BODY_BY_TYPE = {
|
|
|
24
25
|
'cca-assessment': (a) => `Attached is the custom code assessment for ${a.title}.\n${a.itemCount} ${a.itemNoun} classified — review keep / fix / retire / redesign per object.`,
|
|
25
26
|
'modernize-result': (a) => `Attached is the modernization result for ${a.title}.\n${a.itemCount} ${a.itemNoun} attempted — review applied / skipped / failed per object before sign-off.`,
|
|
26
27
|
'test-coverage': (a) => `Attached is the test coverage report for ${a.title}.\n${a.itemCount} test ${a.itemNoun} generated — review the targets + method counts before merging.`,
|
|
28
|
+
'plan': (a) => `Attached is the project plan for ${a.title}.\n${a.itemCount} ${a.itemNoun} ordered bottom-up — each phase runs in its own session via /abap-plan --resume.`,
|
|
27
29
|
};
|
|
28
30
|
export function renderEmailTemplate(a) {
|
|
29
31
|
const subject = SUBJECT_BY_TYPE[a.artefactType](a.title);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* <!-- csforge:cca-manifest
|
|
10
10
|
* artefact: cca-assessment
|
|
11
11
|
* title: ZFI / ZSD — S/4HANA 2023 estate
|
|
12
|
-
* detail_path: .
|
|
12
|
+
* detail_path: .cspeach/cca/projects/zfi-zsd-2026-04/project.json
|
|
13
13
|
* project_id: zfi-zsd-2026-04
|
|
14
14
|
* scope: ZFI* + ZSD*
|
|
15
15
|
* atc_variant: S4HANA_READINESS_2023
|
|
@@ -78,6 +78,13 @@ function intOr(kv, key, fallback) {
|
|
|
78
78
|
const n = Number.parseInt(v, 10);
|
|
79
79
|
return Number.isFinite(n) ? n : fallback;
|
|
80
80
|
}
|
|
81
|
+
function optionalInt(kv, key) {
|
|
82
|
+
const v = kv[key];
|
|
83
|
+
if (v === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
const n = Number.parseInt(v, 10);
|
|
86
|
+
return Number.isFinite(n) ? n : undefined;
|
|
87
|
+
}
|
|
81
88
|
const CLASSIFICATIONS = ['keep', 'fix', 'retire', 'redesign'];
|
|
82
89
|
const CONFIDENCES = ['high', 'medium', 'low'];
|
|
83
90
|
function asClassification(raw) {
|
|
@@ -92,6 +99,36 @@ function asConfidence(raw) {
|
|
|
92
99
|
? v
|
|
93
100
|
: 'medium';
|
|
94
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Parse the optional 6th table cell on a merged row:
|
|
104
|
+
* `conflict=<label>:<cls>;<label>:<cls>`
|
|
105
|
+
* (rendered by merge-cca.ts:renderCcaMergeManifest). Returns undefined
|
|
106
|
+
* for absent / unrecognisable cells; invalid classification values are
|
|
107
|
+
* skipped pair-wise so one typo doesn't drop the whole record.
|
|
108
|
+
*/
|
|
109
|
+
function parseConflictCell(raw) {
|
|
110
|
+
if (!raw)
|
|
111
|
+
return undefined;
|
|
112
|
+
const m = /^conflict=(.+)$/.exec(raw.trim());
|
|
113
|
+
if (!m)
|
|
114
|
+
return undefined;
|
|
115
|
+
const values = {};
|
|
116
|
+
for (const pair of m[1].split(';')) {
|
|
117
|
+
const idx = pair.lastIndexOf(':');
|
|
118
|
+
if (idx === -1)
|
|
119
|
+
continue;
|
|
120
|
+
const label = pair.slice(0, idx).trim();
|
|
121
|
+
const cls = pair.slice(idx + 1).trim().toLowerCase();
|
|
122
|
+
if (label.length === 0)
|
|
123
|
+
continue;
|
|
124
|
+
if (!CLASSIFICATIONS.includes(cls))
|
|
125
|
+
continue;
|
|
126
|
+
values[label] = cls;
|
|
127
|
+
}
|
|
128
|
+
if (Object.keys(values).length === 0)
|
|
129
|
+
return undefined;
|
|
130
|
+
return { values, rule: 'most-restrictive' };
|
|
131
|
+
}
|
|
95
132
|
export function extractCcaAssessment(markdown) {
|
|
96
133
|
const { kv, tables } = parseManifestBlock(markdown);
|
|
97
134
|
if (kv.artefact !== 'cca-assessment') {
|
|
@@ -103,27 +140,82 @@ export function extractCcaAssessment(markdown) {
|
|
|
103
140
|
if (!kv.project_id) {
|
|
104
141
|
throw new Error("cca-assessment manifest missing required 'project_id'");
|
|
105
142
|
}
|
|
106
|
-
const classifications = (tables.classifications ?? []).map((row, i) =>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
143
|
+
const classifications = (tables.classifications ?? []).map((row, i) => {
|
|
144
|
+
const conflict = parseConflictCell(row[5]);
|
|
145
|
+
return {
|
|
146
|
+
id: row[0] ?? `item-${String(i + 1).padStart(3, '0')}`,
|
|
147
|
+
objectName: row[1] ?? '',
|
|
148
|
+
objectType: row[2] ?? '',
|
|
149
|
+
classification: asClassification(row[3] ?? 'keep'),
|
|
150
|
+
confidence: asConfidence(row[4] ?? 'medium'),
|
|
151
|
+
...(conflict ? { conflict } : {}),
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
const summary = {
|
|
155
|
+
totalObjects: intOr(kv, 'total_objects', classifications.length),
|
|
156
|
+
keep: intOr(kv, 'keep', 0),
|
|
157
|
+
fix: intOr(kv, 'fix', 0),
|
|
158
|
+
retire: intOr(kv, 'retire', 0),
|
|
159
|
+
redesign: intOr(kv, 'redesign', 0),
|
|
160
|
+
uncategorized: intOr(kv, 'uncategorized', 0),
|
|
161
|
+
};
|
|
162
|
+
// ── D6/D7 manifest integrity (2026-06-12) ───────────────────────────────
|
|
163
|
+
// Battery S4 caught manifests whose counts contradicted their own rows
|
|
164
|
+
// (`retire: 0` next to 7 retire rows) and tables silently truncated
|
|
165
|
+
// (27/104 rows, no marker). These manifests are CHAIN INPUT for
|
|
166
|
+
// /abap-cca-merge — a quiet save corrupts the consolidated estate
|
|
167
|
+
// numbers, so a contradiction fails the extract LOUDLY instead (same
|
|
168
|
+
// philosophy as extract-plan's loud-failure path in save-command.ts).
|
|
169
|
+
const truncated = kv.classifications_truncated === 'true';
|
|
170
|
+
const shown = optionalInt(kv, 'classifications_shown');
|
|
171
|
+
const totalCls = optionalInt(kv, 'classifications_total');
|
|
172
|
+
const rowTally = {
|
|
173
|
+
keep: 0, fix: 0, retire: 0, redesign: 0,
|
|
174
|
+
};
|
|
175
|
+
for (const c of classifications)
|
|
176
|
+
rowTally[c.classification]++;
|
|
177
|
+
if (truncated) {
|
|
178
|
+
if (shown !== undefined && shown !== classifications.length) {
|
|
179
|
+
throw new Error(`cca-assessment manifest contradiction: classifications_shown is ${shown} but the table has ${classifications.length} rows.`);
|
|
180
|
+
}
|
|
181
|
+
for (const cls of ['keep', 'fix', 'retire', 'redesign']) {
|
|
182
|
+
if (rowTally[cls] > summary[cls]) {
|
|
183
|
+
throw new Error(`cca-assessment manifest contradiction: ${rowTally[cls]} '${cls}' rows in the table but the summary says ${cls}: ${summary[cls]}. `
|
|
184
|
+
+ `Counts must cover every row even when the table is truncated.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (totalCls !== undefined && totalCls < classifications.length) {
|
|
188
|
+
throw new Error(`cca-assessment manifest contradiction: classifications_total (${totalCls}) is smaller than the ${classifications.length} rows shown.`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
for (const cls of ['keep', 'fix', 'retire', 'redesign']) {
|
|
193
|
+
if (rowTally[cls] !== summary[cls]) {
|
|
194
|
+
throw new Error(`cca-assessment manifest contradiction: summary says ${cls}: ${summary[cls]} but the table has ${rowTally[cls]} '${cls}' rows. `
|
|
195
|
+
+ `Counts must equal rows — if the table was capped, mark it with classifications_truncated: true + classifications_shown + classifications_total.`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// total_objects must equal the class-count sum in BOTH branches — the
|
|
200
|
+
// summary counts cover the full estate even when the table is capped.
|
|
201
|
+
const expectedTotal = summary.keep + summary.fix + summary.retire + summary.redesign + summary.uncategorized;
|
|
202
|
+
if (summary.totalObjects !== expectedTotal) {
|
|
203
|
+
throw new Error(`cca-assessment manifest contradiction: total_objects is ${summary.totalObjects} but keep+fix+retire+redesign+uncategorized = ${expectedTotal}.`);
|
|
204
|
+
}
|
|
113
205
|
const content = {
|
|
114
206
|
detailPath: kv.detail_path,
|
|
115
207
|
projectId: kv.project_id,
|
|
116
208
|
scope: kv.scope ?? '',
|
|
117
209
|
atcVariant: kv.atc_variant ?? '',
|
|
118
|
-
summary
|
|
119
|
-
totalObjects: intOr(kv, 'total_objects', classifications.length),
|
|
120
|
-
keep: intOr(kv, 'keep', 0),
|
|
121
|
-
fix: intOr(kv, 'fix', 0),
|
|
122
|
-
retire: intOr(kv, 'retire', 0),
|
|
123
|
-
redesign: intOr(kv, 'redesign', 0),
|
|
124
|
-
uncategorized: intOr(kv, 'uncategorized', 0),
|
|
125
|
-
},
|
|
210
|
+
summary,
|
|
126
211
|
classifications,
|
|
212
|
+
...(truncated
|
|
213
|
+
? {
|
|
214
|
+
classificationsTruncated: true,
|
|
215
|
+
...(shown !== undefined ? { classificationsShown: shown } : {}),
|
|
216
|
+
...(totalCls !== undefined ? { classificationsTotal: totalCls } : {}),
|
|
217
|
+
}
|
|
218
|
+
: {}),
|
|
127
219
|
};
|
|
128
220
|
// One InteractionItem per classification entry, default status 'open'
|
|
129
221
|
// (auto-classified by the AI, awaiting human review).
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* <!-- csforge:modernize-manifest
|
|
12
12
|
* artefact: modernize-result
|
|
13
13
|
* title: ZDPR_PAYMENTS — modernization
|
|
14
|
-
* detail_path: .
|
|
14
|
+
* detail_path: .cspeach/modernize/zdpr-payments/project.json
|
|
15
15
|
* baseline_path: <project-relative path>
|
|
16
16
|
* baseline_sha256: <hex>
|
|
17
17
|
* transport: <id or ->
|