@cspeach/cli 1.1.17 → 1.1.19
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 +3 -4
- package/dist/agent/loop.js +143 -25
- package/dist/agent/providers/local-provider.js +4 -1
- package/dist/agent/repair-partial.js +76 -19
- package/dist/agent/tool-dispatch.js +80 -22
- package/dist/agent/turn-error-ux.js +77 -26
- package/dist/agent/turn-stream.js +25 -3
- package/dist/approvals/adt-type.js +27 -0
- package/dist/approvals/advisory-prompt.js +48 -0
- package/dist/approvals/advisory-render.js +25 -11
- package/dist/approvals/approval-prompt.js +74 -24
- package/dist/approvals/jwt.js +2 -1
- package/dist/approvals/render.js +64 -36
- package/dist/approvals/risk-floor.js +53 -2
- package/dist/auth/api-key.js +30 -16
- package/dist/auth/keychain.js +0 -0
- package/dist/auth/me.js +25 -7
- package/dist/cli.js +25 -6
- package/dist/commands/compact.js +12 -9
- package/dist/commands/config-set.js +6 -2
- package/dist/commands/config-show.js +7 -5
- package/dist/commands/cost.js +41 -34
- package/dist/commands/help.js +88 -50
- package/dist/commands/login.js +28 -19
- package/dist/commands/logout.js +7 -3
- package/dist/commands/plan-chain.js +71 -77
- package/dist/commands/plan-continue.js +1 -0
- package/dist/commands/plan-resume.js +20 -12
- package/dist/commands/whoami.js +23 -9
- package/dist/config/loader.js +15 -3
- package/dist/cost/pricing.js +6 -5
- package/dist/doctor/checks/forge-rules.js +13 -3
- package/dist/doctor/checks/keychain.js +6 -4
- package/dist/doctor/run.js +101 -20
- package/dist/index.js +5 -2
- package/dist/lib/graceful-exit.js +56 -0
- package/dist/lib/piped-prompt.js +65 -0
- package/dist/lock-contention.js +2 -2
- package/dist/one-shot.js +135 -40
- package/dist/projects/answer-blockers.js +6 -1
- package/dist/projects/handover-md.js +15 -14
- package/dist/projects/plan-run.js +97 -54
- package/dist/projects/promote-command.js +11 -2
- package/dist/projects/save-command.js +3 -2
- package/dist/projects/status.js +2 -1
- package/dist/projects/yes-no.js +12 -0
- package/dist/renderer/abap-inline.js +18 -14
- package/dist/renderer/answer-trim.js +69 -0
- package/dist/renderer/banners.js +8 -8
- package/dist/renderer/brand-settled.js +19 -0
- package/dist/renderer/change-card.js +156 -0
- package/dist/renderer/checklist-format.js +130 -0
- package/dist/renderer/color-mode.js +45 -0
- package/dist/renderer/error-detail.js +104 -0
- package/dist/renderer/fence-state.js +46 -0
- package/dist/renderer/footer-line.js +96 -0
- package/dist/renderer/glyphs.js +25 -0
- package/dist/renderer/goodbye.js +38 -0
- package/dist/renderer/legacy-palette.js +41 -0
- package/dist/renderer/look.js +30 -0
- package/dist/renderer/markdown.js +299 -92
- package/dist/renderer/notice-log.js +72 -0
- package/dist/renderer/notice-shape.js +48 -0
- package/dist/renderer/notices.js +40 -8
- package/dist/renderer/panel-rows.js +16 -0
- package/dist/renderer/pipeline.js +66 -0
- package/dist/renderer/progress-chatter.js +23 -17
- package/dist/renderer/rendering-mode.js +34 -0
- package/dist/renderer/routing-line.js +14 -0
- package/dist/renderer/sanitize.js +12 -0
- package/dist/renderer/severity.js +2 -7
- package/dist/renderer/startup-lines.js +152 -0
- package/dist/renderer/status-footer.js +52 -34
- package/dist/renderer/steering-echo.js +41 -0
- package/dist/renderer/syntax.js +31 -12
- package/dist/renderer/tables.js +5 -1
- package/dist/renderer/theme.js +44 -0
- package/dist/renderer/thinking-heartbeat.js +33 -2
- package/dist/renderer/todo-block.js +10 -49
- package/dist/renderer/tool-labels.js +238 -0
- package/dist/renderer/tool-widget.js +245 -51
- package/dist/renderer/trace.js +42 -0
- package/dist/renderer/transcript-flow.js +132 -0
- package/dist/renderer/tty.js +34 -1
- package/dist/renderer/ui-width.js +42 -0
- package/dist/renderer/verify-chain.js +4 -2
- package/dist/renderer/widget-fallback.js +58 -65
- package/dist/repl/bracketed-paste.js +7 -1
- package/dist/repl/builtin-commands.js +19 -8
- package/dist/repl/credential-handover-gate.js +28 -0
- package/dist/repl/early-line-buffer.js +5 -2
- package/dist/repl/file-picker.js +54 -12
- package/dist/repl/ink-stdin-guard.js +66 -0
- package/dist/repl/inquirer-guard.js +59 -16
- package/dist/repl/inquirer-theme.js +27 -33
- package/dist/repl/paste-marker.js +19 -0
- package/dist/repl/plan-turn-end.js +20 -0
- package/dist/repl/post-turn-status.js +36 -11
- package/dist/repl/reroute-turn.js +21 -0
- package/dist/repl/reset-tty-stdin.js +44 -0
- package/dist/repl/restore-guard.js +22 -0
- package/dist/repl/resume-standalone.js +63 -0
- package/dist/repl/rule8-detector.js +11 -3
- package/dist/repl/safety-confirm.js +161 -93
- package/dist/repl/session-spend-line.js +8 -4
- package/dist/repl/themed-prompts.js +19 -0
- package/dist/repl/ui-look-command.js +75 -0
- package/dist/repl/update-method-preview-hook.js +48 -5
- package/dist/repl.js +795 -332
- package/dist/rewind/cli.js +3 -2
- package/dist/rewind/format.js +14 -9
- package/dist/rewind/restore.js +11 -0
- package/dist/router/classifier.js +47 -4
- package/dist/router/intent-extractor.js +24 -8
- package/dist/router/piped-routing.js +23 -0
- package/dist/router/resume-routing.js +16 -0
- package/dist/router/routing-failure.js +57 -0
- package/dist/sap/first-run-choice.js +1 -1
- package/dist/sap/onboarding.js +3 -2
- package/dist/sap/standalone-onboarding.js +1 -1
- package/dist/sap/system-info.js +4 -2
- package/dist/sap/unreachable.js +28 -0
- package/dist/sap-errors/clean-error-text.js +182 -0
- package/dist/session/interrupt-reason.js +39 -0
- package/dist/session/recap.js +36 -24
- package/dist/session/resume.js +104 -36
- package/dist/session/store.js +69 -27
- package/dist/session/user-prompt.js +21 -0
- package/dist/skill-catalog.js +7 -0
- package/dist/skills/bundled-skills.js +76 -69
- package/dist/skills/signing-public-key.js +1 -1
- package/dist/standards/standards-init.js +1 -1
- package/dist/test-helpers/answer-any.js +46 -0
- package/dist/tools/approval.js +27 -11
- package/dist/tools/ask-question.js +47 -5
- package/dist/tools/filesystem/file-write.js +19 -2
- package/dist/tools/fiori/fe-extend.js +16 -1
- package/dist/tools/fiori/fe-scaffold.js +105 -3
- package/dist/tools/fiori/html-escapes.js +28 -0
- package/dist/tools/fiori/metadata/audit.js +605 -0
- package/dist/tools/fiori/metadata/references.js +104 -0
- package/dist/tools/fiori/metadata/types.js +8 -0
- package/dist/tools/fiori/preview/app-guard.js +215 -0
- package/dist/tools/fiori/preview/env.js +193 -0
- package/dist/tools/fiori/preview/readiness.js +127 -0
- package/dist/tools/fiori/preview/registry.js +412 -0
- package/dist/tools/fiori/preview/start.js +469 -0
- package/dist/tools/fiori/samples/loader.js +20 -5
- package/dist/tools/fiori/scaffold.js +18 -6
- package/dist/tools/fiori/smoke/app-driver-page.js +372 -0
- package/dist/tools/fiori/smoke/app-driver.js +100 -0
- package/dist/tools/fiori/smoke/assertions.js +161 -24
- package/dist/tools/fiori/smoke/audit-columns.js +21 -0
- package/dist/tools/fiori/smoke/batch.js +273 -0
- package/dist/tools/fiori/smoke/draft-safety.js +241 -0
- package/dist/tools/fiori/smoke/draft-smoke.js +473 -0
- package/dist/tools/fiori/smoke/driver.js +171 -4
- package/dist/tools/fiori/smoke/evidence.js +35 -0
- package/dist/tools/fiori/smoke/labels-codes.js +207 -0
- package/dist/tools/fiori/smoke/nav-error.js +26 -0
- package/dist/tools/fiori/smoke/run-smoke.js +155 -12
- package/dist/tools/fiori/tools.js +651 -7
- package/dist/tools/fiori/ui5-version.js +16 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/local-build.js +6 -0
- package/dist/tools/sap-read.js +16 -1
- package/dist/tools/sap-write.js +122 -10
- package/dist/tools/shell/shell_exec.js +30 -12
- package/dist/tools/syntax-state.js +67 -0
- package/dist/tools/todo.js +23 -18
- package/dist/tools/transport-fit.js +462 -0
- package/dist/tools/transport-resolution.js +3 -1
- package/dist/tools/transport.js +33 -7
- package/dist/tools/verify.js +184 -8
- package/dist/ui/app.js +284 -146
- package/dist/ui/approval-modal.js +41 -15
- package/dist/ui/ask-answer-text.js +17 -0
- package/dist/ui/body.js +25 -6
- package/dist/ui/card-slot.js +98 -0
- package/dist/ui/checklist.js +9 -0
- package/dist/ui/coaching-picker-classic.js +8 -5
- package/dist/ui/command-palette.js +6 -4
- package/dist/ui/confirm-request.js +18 -0
- package/dist/ui/context-grid.js +2 -1
- package/dist/ui/file-palette.js +6 -4
- package/dist/ui/files-card-emitter.js +19 -0
- package/dist/ui/footer-line.js +16 -0
- package/dist/ui/footer.js +105 -104
- package/dist/ui/input-wrap.js +122 -0
- package/dist/ui/interrupt.js +103 -0
- package/dist/ui/key-burst.js +117 -0
- package/dist/ui/line-resolution.js +4 -3
- package/dist/ui/live-area.js +71 -0
- package/dist/ui/login-banner.js +54 -43
- package/dist/ui/rewind-panel.js +76 -24
- package/dist/ui/role-props.js +47 -0
- package/dist/ui/sap-state-store.js +1 -0
- package/dist/ui/session-timeline.js +49 -3
- package/dist/ui/skill-picker.js +5 -47
- package/dist/ui/text-input.js +128 -24
- package/dist/ui/todo-emitter.js +19 -0
- package/dist/ui/turn-status-emitter.js +28 -2
- package/dist/ui/turn-status.js +16 -19
- package/dist/ui/use-card-request.js +44 -0
- package/dist/ui/widgets/ask-card.js +521 -0
- package/dist/ui/widgets/bar-chart.js +11 -5
- package/dist/ui/widgets/coaching-picker.js +50 -55
- package/dist/ui/widgets/confirm-card.js +193 -0
- package/dist/ui/widgets/dep-graph.js +3 -2
- package/dist/ui/widgets/diff-viewer.js +6 -5
- package/dist/ui/widgets/files-card.js +97 -0
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/reclaim-raw-stdin.js +24 -0
- package/dist/ui/widgets/shortcuts-sheet.js +25 -0
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/upgrade-check.js +39 -8
- package/package.json +11 -2
- package/test-harness/fe-draft/app.ts +188 -0
- package/test-harness/fe-draft/fe-draft.e2e.test.ts +461 -0
- package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +51 -0
- package/test-harness/fe-draft/routes.ts +224 -0
- package/test-harness/fe-draft/routes.unit.test.ts +45 -0
- package/test-harness/fe-draft/server.ts +225 -0
- package/test-harness/fe-draft/variants.ts +196 -0
- package/vitest.smoke-e2e.config.ts +20 -0
package/dist/doctor/run.js
CHANGED
|
@@ -14,8 +14,93 @@ import { checkCredits } from './checks/credits.js';
|
|
|
14
14
|
import { checkForgeRules } from './checks/forge-rules.js';
|
|
15
15
|
import { checkSystemRoles } from './checks/system-roles.js';
|
|
16
16
|
import { checkStandards } from './checks/standards.js';
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
import { paint } from '../renderer/theme.js';
|
|
18
|
+
import { getGlyphs } from '../renderer/glyphs.js';
|
|
19
|
+
import { isNoColor } from '../renderer/color-mode.js';
|
|
20
|
+
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
21
|
+
import { CLI_VERSION } from '../lib/version.js';
|
|
22
|
+
const NAME_WIDTH = 20;
|
|
23
|
+
/** Check messages can carry server text (HTTP bodies, SAP errors): one line, no control characters. */
|
|
24
|
+
const oneLine = (s) => sanitizeControlChars(s).replace(/\s*\r?\n\s*/g, ' ').trim();
|
|
25
|
+
/** Human name of the terminal CSPeach is running in (for the doctor title). */
|
|
26
|
+
export function terminalName(env = process.env, platform = process.platform) {
|
|
27
|
+
if (env['WT_SESSION'])
|
|
28
|
+
return 'Windows Terminal';
|
|
29
|
+
const tp = env['TERM_PROGRAM'];
|
|
30
|
+
if (tp === 'vscode')
|
|
31
|
+
return 'VS Code terminal';
|
|
32
|
+
if (tp === 'iTerm.app')
|
|
33
|
+
return 'iTerm2';
|
|
34
|
+
if (tp === 'Apple_Terminal')
|
|
35
|
+
return 'Terminal.app';
|
|
36
|
+
if (tp)
|
|
37
|
+
return oneLine(tp);
|
|
38
|
+
if (env['TERM'])
|
|
39
|
+
return oneLine(env['TERM']);
|
|
40
|
+
return platform === 'win32' ? 'Windows console' : 'unknown terminal';
|
|
41
|
+
}
|
|
42
|
+
export function colourLevelName(level, noColor = isNoColor()) {
|
|
43
|
+
if (noColor)
|
|
44
|
+
return 'no colour (NO_COLOR)';
|
|
45
|
+
if (level >= 3)
|
|
46
|
+
return 'truecolor';
|
|
47
|
+
if (level === 2)
|
|
48
|
+
return '256 colours';
|
|
49
|
+
if (level === 1)
|
|
50
|
+
return '16 colours';
|
|
51
|
+
return 'no colour';
|
|
52
|
+
}
|
|
53
|
+
/** 1 when any check failed; a skipped check (C.2 permission skip) is not a failure. */
|
|
54
|
+
export function doctorExitCode(checks) {
|
|
55
|
+
return checks.some((c) => !c.ok && !c.skipped) ? 1 : 0;
|
|
56
|
+
}
|
|
57
|
+
export function formatDoctorTitle(meta) {
|
|
58
|
+
return ['', `${chalk.bold('CSPeach doctor')} ${paint('dim', `${meta.version} · ${meta.terminal} · ${meta.colour}`)}`, ''];
|
|
59
|
+
}
|
|
60
|
+
export function formatDoctorRows(checks) {
|
|
61
|
+
const g = getGlyphs();
|
|
62
|
+
const lines = [];
|
|
63
|
+
let passed = 0;
|
|
64
|
+
let warnings = 0;
|
|
65
|
+
let failed = 0;
|
|
66
|
+
for (const c of checks) {
|
|
67
|
+
let glyph;
|
|
68
|
+
if (c.skipped) {
|
|
69
|
+
glyph = paint('warn', g.attention);
|
|
70
|
+
warnings++;
|
|
71
|
+
}
|
|
72
|
+
else if (c.ok) {
|
|
73
|
+
glyph = paint('ok', g.done);
|
|
74
|
+
passed++;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
glyph = paint('bad', g.failed);
|
|
78
|
+
failed++;
|
|
79
|
+
}
|
|
80
|
+
lines.push(`${glyph} ${oneLine(c.name).padEnd(NAME_WIDTH)} ${paint('dim', oneLine(c.message))}`);
|
|
81
|
+
if (c.hint && oneLine(c.hint) !== '')
|
|
82
|
+
lines.push(` ${paint('dim', 'Next:')} ${oneLine(c.hint)}`);
|
|
83
|
+
}
|
|
84
|
+
const summary = `${passed} of ${checks.length} checks passed · ${warnings} warning${warnings === 1 ? '' : 's'} · ${failed} failed`;
|
|
85
|
+
lines.push('', paint('dim', summary), '');
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Re-review 2 item 3 — the forge-rules probe writes to SAP (create, lock,
|
|
90
|
+
* write, snapshot list, delete); any throw there (lock refused, snapshot
|
|
91
|
+
* store unreadable, connection drop) is a failed row with a readable one-line
|
|
92
|
+
* message, never a crashed doctor run.
|
|
93
|
+
*/
|
|
94
|
+
async function safeForgeRules() {
|
|
95
|
+
try {
|
|
96
|
+
return await checkForgeRules();
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
const msg = String(err?.message ?? err).replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
100
|
+
return { name: 'forge-rules', ok: false, message: `Rule 7/10 probe failed: ${msg}` };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export async function collectChecks() {
|
|
19
104
|
const cfg = await loadConfig();
|
|
20
105
|
const checks = [];
|
|
21
106
|
checks.push(await checkProxy());
|
|
@@ -30,23 +115,19 @@ export async function runDoctor() {
|
|
|
30
115
|
checks.push(await checkCert());
|
|
31
116
|
checks.push(await checkSkill());
|
|
32
117
|
checks.push(await checkKeychainFallback());
|
|
33
|
-
checks.push(await
|
|
118
|
+
checks.push(await safeForgeRules());
|
|
34
119
|
checks.push(await checkStandards());
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
console.log(failed === 0
|
|
49
|
-
? chalk.green('\nAll checks passed.\n')
|
|
50
|
-
: chalk.yellow(`\n${failed} check(s) failed.\n`));
|
|
51
|
-
return failed === 0 ? 0 : 1;
|
|
120
|
+
return checks;
|
|
121
|
+
}
|
|
122
|
+
export async function runDoctor(deps = {}) {
|
|
123
|
+
const print = deps.print ?? ((l) => console.log(l));
|
|
124
|
+
const meta = deps.meta ?? { version: CLI_VERSION, terminal: terminalName(), colour: colourLevelName(chalk.level) };
|
|
125
|
+
// Title first: the checks take seconds (network), and the user should see
|
|
126
|
+
// that doctor is running.
|
|
127
|
+
for (const l of formatDoctorTitle(meta))
|
|
128
|
+
print(l);
|
|
129
|
+
const checks = await (deps.collect ?? collectChecks)();
|
|
130
|
+
for (const l of formatDoctorRows(checks))
|
|
131
|
+
print(l);
|
|
132
|
+
return doctorExitCode(checks);
|
|
52
133
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { main } from './cli.js';
|
|
3
|
-
|
|
3
|
+
import { gracefulExit } from './lib/graceful-exit.js';
|
|
4
|
+
// Round 7 item 5 — close fetch's sockets before exiting (Windows libuv
|
|
5
|
+
// assertion on process.exit with live undici handles, nodejs/node#56645).
|
|
6
|
+
main(process.argv).then((code) => gracefulExit(code), (err) => {
|
|
4
7
|
console.error('cspeach: unhandled error:', err);
|
|
5
|
-
|
|
8
|
+
return gracefulExit(1);
|
|
6
9
|
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round 7 item 5 — exit without tearing live network handles down under Node.
|
|
3
|
+
*
|
|
4
|
+
* On Windows, process.exit() while fetch (Node's built-in undici) still holds
|
|
5
|
+
* sockets can abort with the libuv assertion
|
|
6
|
+
* `!(handle->flags & UV_HANDLE_CLOSING), src\win\async.c line 76`
|
|
7
|
+
* (known Node bug: nodejs/node#56645, #64322). The matrix saw it once after a
|
|
8
|
+
* routing cancel + /exit. We close fetch's global dispatcher first and give the
|
|
9
|
+
* loop a short, capped chance to settle, then exit as before.
|
|
10
|
+
*
|
|
11
|
+
* Review r7 M-1 — drain with close() (in-flight requests finish) and only
|
|
12
|
+
* destroy() when that does not finish inside the cap. A fire-and-forget fetch
|
|
13
|
+
* that rejects inside the window must not surface as a crash on the way out,
|
|
14
|
+
* so a no-op unhandledRejection guard is installed for the window only.
|
|
15
|
+
*/
|
|
16
|
+
/** The global symbol Node's bundled undici stores its dispatcher under. */
|
|
17
|
+
export const UNDICI_DISPATCHER = Symbol.for('undici.globalDispatcher.1');
|
|
18
|
+
function capped(p, ms) {
|
|
19
|
+
let timer;
|
|
20
|
+
return Promise.race([
|
|
21
|
+
p.then(() => true, () => true),
|
|
22
|
+
new Promise((r) => { timer = setTimeout(() => r(false), ms); }),
|
|
23
|
+
]).finally(() => { if (timer)
|
|
24
|
+
clearTimeout(timer); });
|
|
25
|
+
}
|
|
26
|
+
function call(fn, self) {
|
|
27
|
+
try {
|
|
28
|
+
return Promise.resolve(fn?.call(self));
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
return Promise.reject(e);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function gracefulExit(code, opts = {}) {
|
|
35
|
+
const exit = opts.exit ?? ((c) => process.exit(c));
|
|
36
|
+
const graceMs = opts.graceMs ?? 250;
|
|
37
|
+
process.exitCode = code;
|
|
38
|
+
const guard = () => { };
|
|
39
|
+
process.on('unhandledRejection', guard);
|
|
40
|
+
try {
|
|
41
|
+
const dispatcher = globalThis[UNDICI_DISPATCHER];
|
|
42
|
+
if (dispatcher) {
|
|
43
|
+
let done = false;
|
|
44
|
+
if (typeof dispatcher.close === 'function')
|
|
45
|
+
done = await capped(call(dispatcher.close, dispatcher), graceMs);
|
|
46
|
+
if (!done && typeof dispatcher.destroy === 'function')
|
|
47
|
+
await capped(call(dispatcher.destroy, dispatcher), graceMs);
|
|
48
|
+
}
|
|
49
|
+
// One more loop turn so socket 'close' callbacks run before the teardown.
|
|
50
|
+
await new Promise((r) => setImmediate(r));
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
process.off('unhandledRejection', guard);
|
|
54
|
+
}
|
|
55
|
+
exit(code);
|
|
56
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// A command head: "/name" followed by a space or the end of the line —
|
|
2
|
+
// "/cost", "/abap-explain ZFOO". A path ("/sap/bc/adt/…", "/usr/sap/…") has
|
|
3
|
+
// a second "/" in its first token and is prose (final review B M-3).
|
|
4
|
+
const SCRIPT_LINE = /(^|\n)[ \t]*\/[A-Za-z][\w-]*(?=[ \t]|\r?\n|$)/;
|
|
5
|
+
export function readPipedPrompt(stdin, opts = {}) {
|
|
6
|
+
if (stdin.isTTY)
|
|
7
|
+
return Promise.resolve({ kind: 'repl' });
|
|
8
|
+
const firstMs = opts.firstMs ?? 250;
|
|
9
|
+
const endMs = opts.endMs ?? 1500;
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
const chunks = [];
|
|
12
|
+
let sawData = false;
|
|
13
|
+
let settled = false;
|
|
14
|
+
let t1;
|
|
15
|
+
let t2;
|
|
16
|
+
const text = () => Buffer.concat(chunks).toString('utf8').replace(/^/, '');
|
|
17
|
+
const done = (r) => {
|
|
18
|
+
if (settled)
|
|
19
|
+
return;
|
|
20
|
+
settled = true;
|
|
21
|
+
if (t1)
|
|
22
|
+
clearTimeout(t1);
|
|
23
|
+
if (t2)
|
|
24
|
+
clearTimeout(t2);
|
|
25
|
+
stdin.off('readable', onReadable);
|
|
26
|
+
stdin.off('end', onEnd);
|
|
27
|
+
resolve(r);
|
|
28
|
+
};
|
|
29
|
+
const giveBack = () => {
|
|
30
|
+
if (settled)
|
|
31
|
+
return;
|
|
32
|
+
if (chunks.length > 0)
|
|
33
|
+
stdin.unshift(Buffer.concat(chunks));
|
|
34
|
+
done({ kind: 'repl' });
|
|
35
|
+
};
|
|
36
|
+
const onEnd = () => {
|
|
37
|
+
const t = text().trim();
|
|
38
|
+
done(t ? { kind: 'prompt', text: t } : { kind: 'repl' });
|
|
39
|
+
};
|
|
40
|
+
const onReadable = () => {
|
|
41
|
+
// EOF with nothing read: leave it unread so the REPL sees the end.
|
|
42
|
+
if (!sawData && stdin.readableLength === 0) {
|
|
43
|
+
done({ kind: 'repl' });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let c;
|
|
47
|
+
while ((c = stdin.read()) !== null)
|
|
48
|
+
chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
|
|
49
|
+
if (SCRIPT_LINE.test(text())) {
|
|
50
|
+
giveBack();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!sawData) {
|
|
54
|
+
sawData = true;
|
|
55
|
+
if (t1)
|
|
56
|
+
clearTimeout(t1);
|
|
57
|
+
t2 = setTimeout(giveBack, endMs);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
stdin.on('readable', onReadable);
|
|
61
|
+
stdin.on('end', onEnd);
|
|
62
|
+
t1 = setTimeout(() => { if (!sawData)
|
|
63
|
+
done({ kind: 'repl' }); }, firstMs);
|
|
64
|
+
});
|
|
65
|
+
}
|
package/dist/lock-contention.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { select } from '@inquirer/prompts';
|
|
2
|
-
import
|
|
2
|
+
import { formatAttention } from './renderer/notice-shape.js';
|
|
3
3
|
export async function handleLockConflict(info) {
|
|
4
|
-
console.log(
|
|
4
|
+
console.log('\n' + formatAttention({ text: `${info.object} is locked`, fact: `by ${info.user} since ${info.lockTime}` }).trimEnd());
|
|
5
5
|
return (await select({
|
|
6
6
|
message: 'How do you want to proceed?',
|
|
7
7
|
choices: [
|
package/dist/one-shot.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadConfig, resolveLocalBuild, resolveStandalone } from './config/loader.js';
|
|
3
|
+
import { applyColorMode } from './renderer/color-mode.js';
|
|
4
|
+
import { formatAttention, formatInfo } from './renderer/notice-shape.js';
|
|
5
|
+
import { formatRoutingLine } from './renderer/routing-line.js';
|
|
3
6
|
import { createOfflineAdtClient } from './sap/offline-adt-client.js';
|
|
4
7
|
import { STANDALONE_ALIAS } from './tools/index.js';
|
|
5
8
|
import { getStore } from './auth/api-key.js';
|
|
6
9
|
import { getConnection } from './sap/connection-manager.js';
|
|
10
|
+
import { unreachableCode, unreachableMessage } from './sap/unreachable.js';
|
|
7
11
|
import { AdtClient } from '@cspeach/sap-client';
|
|
8
12
|
import { makeSessionId, saveSession } from './session/store.js';
|
|
9
13
|
import { newSession } from './session/schema.js';
|
|
@@ -13,13 +17,23 @@ import { createProviderForMode } from './agent/providers/factory.js';
|
|
|
13
17
|
import { assertModeLicensed, CspeachLicenseError } from './agent/providers/license-gate.js';
|
|
14
18
|
import { runTurn } from './agent/loop.js';
|
|
15
19
|
import { classifyPrompt, parseShortcut, rejectUnknownSkill } from './router/classifier.js';
|
|
16
|
-
import {
|
|
20
|
+
import { routingFailureFromResult, routingFailureOneShotLine } from './router/routing-failure.js';
|
|
21
|
+
import { startThinkingHeartbeat, ROUTING_HEARTBEAT_THRESHOLDS } from './renderer/thinking-heartbeat.js';
|
|
22
|
+
import { setRenderingOverride, setHeadlessOverride, setOneShotOverride } from './renderer/tty.js';
|
|
23
|
+
import { resolveRenderingMode } from './renderer/rendering-mode.js';
|
|
24
|
+
import { setLook, setBackground } from './renderer/look.js';
|
|
17
25
|
import { alwaysDeclinePreviewHook } from './repl/update-method-preview-hook.js';
|
|
18
26
|
import { createClassicOutputEmitter } from './renderer/notices.js';
|
|
27
|
+
import { AnswerTrim } from './renderer/answer-trim.js';
|
|
28
|
+
import { writeStdout } from './renderer/transcript-flow.js';
|
|
29
|
+
import { writeAboveSpinner, aboveSpinnerAddsNewline } from './renderer/tool-widget.js';
|
|
19
30
|
import { todoEmitter } from './ui/todo-emitter.js';
|
|
20
31
|
import { applyLocalBuildConfig, applyStandaloneFileTools } from './tools/local-build.js';
|
|
21
32
|
import { getEffectiveWriteMode } from './repl/mode-cycle.js';
|
|
22
33
|
import { initCredits } from './cost/credits-wire.js';
|
|
34
|
+
import { formatTurnReceipt, turnSpendText } from './renderer/footer-line.js';
|
|
35
|
+
import { snapshotTurnStart, computeTurnFacts } from './repl/post-turn-status.js';
|
|
36
|
+
import { globalCredits } from './cost/credits.js';
|
|
23
37
|
// Side-effect imports for tool registration.
|
|
24
38
|
import './tools/sap-read.js';
|
|
25
39
|
import './tools/sap-write.js';
|
|
@@ -52,11 +66,18 @@ import './tools/subagent/monitor_emit.js';
|
|
|
52
66
|
import './tools/subagent/schedule_draft_create.js';
|
|
53
67
|
import './tools/subagent/agent_run.js';
|
|
54
68
|
export async function runOneShot(prompt, opts) {
|
|
69
|
+
applyColorMode(); // NO_COLOR (piece 2 §1.2)
|
|
55
70
|
const cfg = await loadConfig();
|
|
56
71
|
// Standalone (2026-09-16) — run with no SAP system at all: no connection, no
|
|
57
72
|
// AdtClient, SAP tools hidden. `--no-sap` for this run, or `no_sap = true`.
|
|
58
73
|
const standalone = resolveStandalone(cfg, opts?.noSap === true);
|
|
59
|
-
|
|
74
|
+
// One-shot output is unchanged by the classic retirement: `auto` never
|
|
75
|
+
// mounts Ink for a one-shot run. CSPEACH_UI=classic is honoured here too;
|
|
76
|
+
// the stored-value rewrite happens only on an interactive start.
|
|
77
|
+
setOneShotOverride(true);
|
|
78
|
+
setRenderingOverride(resolveRenderingMode(cfg.ui.rendering).mode);
|
|
79
|
+
setLook(cfg.ui.look);
|
|
80
|
+
setBackground(cfg.ui.background);
|
|
60
81
|
// model-governance step 2d — fire-and-forget fetch of the served model-config
|
|
61
82
|
// (managed mode only, never blocks). A turn that starts before it lands uses
|
|
62
83
|
// the built-ins (fail-safe).
|
|
@@ -80,8 +101,10 @@ export async function runOneShot(prompt, opts) {
|
|
|
80
101
|
// write_mode stands on its own.
|
|
81
102
|
const oneShotAlias = standalone ? undefined : (opts?.sapAlias ?? Object.keys(cfg.sap)[0]);
|
|
82
103
|
const oneShotRole = oneShotAlias !== undefined ? cfg.sap[oneShotAlias]?.role : undefined;
|
|
104
|
+
// Fix round 3 (item 7): every startup notice below goes to STDERR — piped
|
|
105
|
+
// stdout stays the answer only (spec §8).
|
|
83
106
|
if (getEffectiveWriteMode(cfg.write_mode, oneShotRole) === 'advisory-only') {
|
|
84
|
-
console.
|
|
107
|
+
console.error(formatAttention({ text: 'Write mode: advisory', fact: 'writes are proposed, not executed' }).trimEnd());
|
|
85
108
|
}
|
|
86
109
|
// local_build toggle — force-enable the flag-gated filesystem tools AND
|
|
87
110
|
// shell_exec BEFORE the (single) turn when persisted-on. Resolve with backward
|
|
@@ -91,11 +114,11 @@ export async function runOneShot(prompt, opts) {
|
|
|
91
114
|
{
|
|
92
115
|
const announce = applyLocalBuildConfig(resolveLocalBuild(cfg), process.cwd());
|
|
93
116
|
if (announce)
|
|
94
|
-
console.
|
|
117
|
+
console.error(chalk.dim(announce));
|
|
95
118
|
// 2026-09-17 — standalone writes files by contract; enable the file tools
|
|
96
119
|
// without the separate local_build step (see repl.tsx for the live case).
|
|
97
120
|
else if (standalone)
|
|
98
|
-
console.
|
|
121
|
+
console.error(chalk.dim(applyStandaloneFileTools(process.cwd())));
|
|
99
122
|
}
|
|
100
123
|
const store = await getStore();
|
|
101
124
|
if (!(await store.get())) {
|
|
@@ -104,9 +127,9 @@ export async function runOneShot(prompt, opts) {
|
|
|
104
127
|
}
|
|
105
128
|
// Credits (2026-09-16) — resolve the display mode before the turn runs, so
|
|
106
129
|
// the per-turn cost JSONL records the ledger charge and nothing downstream
|
|
107
|
-
// can print a dollar to a managed customer
|
|
108
|
-
//
|
|
109
|
-
//
|
|
130
|
+
// can print a dollar to a managed customer — including the one-line stderr
|
|
131
|
+
// receipt after the turn (piece 2 I3a), and the agent loop's cost-log path,
|
|
132
|
+
// which is shared with the REPL. Capped + fail-safe; BYOK/local skip the call.
|
|
110
133
|
await initCredits(cfg, async () => (await store.get()) ?? '');
|
|
111
134
|
const aliases = Object.keys(cfg.sap);
|
|
112
135
|
let alias;
|
|
@@ -117,12 +140,12 @@ export async function runOneShot(prompt, opts) {
|
|
|
117
140
|
// filter + dispatch guard.
|
|
118
141
|
alias = STANDALONE_ALIAS;
|
|
119
142
|
adt = createOfflineAdtClient();
|
|
120
|
-
console.
|
|
143
|
+
console.error(chalk.dim('Standalone mode: no SAP system connected — skills work from pasted code. ' +
|
|
121
144
|
'Connect later with: cspeach config add-sap'));
|
|
122
145
|
// One-shot is headless by design (no prompts), so an unanswered standalone
|
|
123
146
|
// profile can't be filled in here — say how, and carry on with "unknown".
|
|
124
147
|
if (!cfg.standalone) {
|
|
125
|
-
console.
|
|
148
|
+
console.error(chalk.dim('Standalone profile not set — run: cspeach config set standalone.platform ...'));
|
|
126
149
|
}
|
|
127
150
|
}
|
|
128
151
|
else {
|
|
@@ -133,9 +156,19 @@ export async function runOneShot(prompt, opts) {
|
|
|
133
156
|
// C.1 — --sap-alias flag override
|
|
134
157
|
alias = opts?.sapAlias ?? aliases[0];
|
|
135
158
|
if (opts?.sapAlias) {
|
|
136
|
-
console.
|
|
159
|
+
console.error(chalk.dim(`Using --sap-alias ${opts.sapAlias}`));
|
|
160
|
+
}
|
|
161
|
+
let conn;
|
|
162
|
+
try {
|
|
163
|
+
conn = await getConnection(alias, cfg.sap[alias]);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
const code = unreachableCode(err);
|
|
167
|
+
if (!code)
|
|
168
|
+
throw err;
|
|
169
|
+
console.error(unreachableMessage(alias, cfg.sap[alias].host, cfg.sap[alias].port, code));
|
|
170
|
+
return 1;
|
|
137
171
|
}
|
|
138
|
-
const conn = await getConnection(alias, cfg.sap[alias]);
|
|
139
172
|
adt = new AdtClient(conn);
|
|
140
173
|
}
|
|
141
174
|
const session = newSession(makeSessionId(), alias, null, resolveModelRole('session_default', cfg));
|
|
@@ -155,30 +188,51 @@ export async function runOneShot(prompt, opts) {
|
|
|
155
188
|
skill = parsed.skill;
|
|
156
189
|
}
|
|
157
190
|
else {
|
|
158
|
-
// One-shot has no interactive picker — always uses top-1 and
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
191
|
+
// One-shot has no interactive picker — always uses top-1 and names it on
|
|
192
|
+
// a dim stderr line. If < 0.6, add a stderr `!` notice with the top-3
|
|
193
|
+
// candidates so the user can rerun with an explicit /skill.
|
|
194
|
+
// Fix round 3 (item 8) — say we are routing while the classifier runs
|
|
195
|
+
// (stderr; nothing prints when it answers within a second).
|
|
196
|
+
const routingBeat = startThinkingHeartbeat({ thresholds: ROUTING_HEARTBEAT_THRESHOLDS });
|
|
197
|
+
let c;
|
|
198
|
+
try {
|
|
199
|
+
c = await classifyPrompt(body, {
|
|
200
|
+
sap_system: alias,
|
|
201
|
+
last_skill: session.last_skill ?? null,
|
|
202
|
+
last_object: parsed.object ?? session.last_object ?? null,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
// Round 4 item 4 — a missing key here was an uncaught `no_api_key`
|
|
207
|
+
// with a stack (and a libuv assertion on exit). One clean line, exit 1.
|
|
208
|
+
if (err instanceof Error && err.message === 'no_api_key') {
|
|
209
|
+
console.error('No API key configured. Run `cspeach` (interactive) first to set one.');
|
|
210
|
+
return 1;
|
|
211
|
+
}
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
routingBeat.stop();
|
|
216
|
+
}
|
|
166
217
|
if (c.offline) {
|
|
167
218
|
// Remote classify failed twice — local-rules fallback fired. Print
|
|
168
219
|
// to stderr so it doesn't pollute stdout for scripted callers, but
|
|
169
220
|
// is still visible.
|
|
170
|
-
|
|
221
|
+
// Fix 9 (2026-09-26): same true wording as the REPL — a timeout is not "offline".
|
|
222
|
+
console.error(chalk.dim(routingFailureOneShotLine(routingFailureFromResult(c), c.transport_error)));
|
|
171
223
|
}
|
|
172
|
-
|
|
224
|
+
// Piece 2 (spec §8/§9) — routing chatter goes to STDERR so piped stdout
|
|
225
|
+
// stays the answer only. Confidence percentages are dropped everywhere.
|
|
173
226
|
if (c.screenshot)
|
|
174
|
-
|
|
227
|
+
process.stderr.write(formatInfo(`Screenshot: ${c.screenshot}`));
|
|
228
|
+
process.stderr.write(formatRoutingLine({ kind: 'using', skill: c.skill }));
|
|
175
229
|
if (c.confidence < 0.6) {
|
|
176
230
|
const alternates = [c.skill, ...c.alternates].slice(0, 3).map((s) => `/${s}`).join(', ');
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
231
|
+
process.stderr.write(formatAttention({
|
|
232
|
+
text: 'Low confidence',
|
|
233
|
+
fact: `candidates: ${alternates}`,
|
|
234
|
+
next: 'rerun with an explicit /skill to override',
|
|
235
|
+
}));
|
|
182
236
|
}
|
|
183
237
|
skill = c.skill;
|
|
184
238
|
}
|
|
@@ -201,18 +255,59 @@ export async function runOneShot(prompt, opts) {
|
|
|
201
255
|
throw new Error('No API key configured');
|
|
202
256
|
return key;
|
|
203
257
|
});
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
258
|
+
const turnSnapshot = snapshotTurnStart(session);
|
|
259
|
+
// Round 6 item 4f — stdout is the answer, trimmed: no blank lines before
|
|
260
|
+
// it, exactly one newline after it.
|
|
261
|
+
const answerTrim = new AnswerTrim();
|
|
262
|
+
const answerEmitter = createClassicOutputEmitter((s) => {
|
|
263
|
+
const out = answerTrim.feed(s);
|
|
264
|
+
if (!out)
|
|
265
|
+
return;
|
|
266
|
+
// Review M3 — a painting spinner ends the line itself; tell the trim so
|
|
267
|
+
// its held newline does not become a stray blank line.
|
|
268
|
+
const forced = aboveSpinnerAddsNewline(out);
|
|
269
|
+
writeAboveSpinner(out, writeStdout);
|
|
270
|
+
if (forced)
|
|
271
|
+
answerTrim.noteForcedNewline();
|
|
216
272
|
});
|
|
273
|
+
// Owner ruling (r3 M11) — stdout is the answer only, everywhere: tool
|
|
274
|
+
// notices (Rule 9 transport overrides, snapshot lines, the write chain)
|
|
275
|
+
// and tool rows go to STDERR in one-shot.
|
|
276
|
+
const noticeEmitter = createClassicOutputEmitter((s) => { process.stderr.write(s); });
|
|
277
|
+
try {
|
|
278
|
+
await runTurn({
|
|
279
|
+
provider,
|
|
280
|
+
userMessage: body,
|
|
281
|
+
skill,
|
|
282
|
+
// Fix round 7 item 2 — runTurn reads params.chunkEmitter (NOT
|
|
283
|
+
// ctx.chunkEmitter) for the answer stream; without this the trim never
|
|
284
|
+
// ran. Liveness stays off stdout (see loop.ts livenessEmitter).
|
|
285
|
+
chunkEmitter: answerEmitter,
|
|
286
|
+
// B2 fix (2026-06-12): one-shot runs render tool notices ('warn'/'info'
|
|
287
|
+
// — Rule 9 transport overrides, snapshot lines) instead of dropping them;
|
|
288
|
+
// headless transcripts need them for auditability. Owner ruling (r3 M11):
|
|
289
|
+
// they go to STDERR (noticeEmitter) — stdout is the answer only.
|
|
290
|
+
// Task 5 (ux-wave2): todoEmitter wired for the uniform top-level-ctx
|
|
291
|
+
// contract — no listener exists in one-shot, so emits are no-ops, but
|
|
292
|
+
// "top-level ctxs carry it / subagent ctxs omit it" stays the single
|
|
293
|
+
// rule (see ui/todo-emitter.ts).
|
|
294
|
+
ctx: { adt, sapAlias: alias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: alwaysDeclinePreviewHook, chunkEmitter: noticeEmitter, todoEmitter },
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
// Review M2 — even when the turn throws, the answer so far ends with its
|
|
299
|
+
// newline (and any held colour reset) instead of being cut mid-line.
|
|
300
|
+
const answerTail = answerTrim.end();
|
|
301
|
+
if (answerTail)
|
|
302
|
+
writeStdout(answerTail);
|
|
303
|
+
}
|
|
304
|
+
// Piece 2 (spec §8) — the one-line receipt, on STDERR (stdout = the answer).
|
|
305
|
+
const facts = computeTurnFacts(session, turnSnapshot);
|
|
306
|
+
process.stderr.write(formatTurnReceipt({
|
|
307
|
+
skill,
|
|
308
|
+
tokens: facts.turnTokens,
|
|
309
|
+
spend: turnSpendText({ costThisTurn: facts.turnCost, credits: globalCredits.snapshot() }),
|
|
310
|
+
ms: facts.elapsedMs,
|
|
311
|
+
}) + '\n');
|
|
217
312
|
return 0;
|
|
218
313
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFileSync } from 'node:fs';
|
|
2
2
|
import { readProjectFile } from './status.js';
|
|
3
3
|
import { validateEnvelope } from './validate.js';
|
|
4
|
+
import { askYesNo } from './yes-no.js';
|
|
4
5
|
/**
|
|
5
6
|
* The still-open BLOCKER-severity questions of a spec-gap envelope, in item
|
|
6
7
|
* order. Returns [] for any non-spec-gap artefact (only spec-gap has gaps with
|
|
@@ -81,7 +82,11 @@ export async function offerAnswerBlockers(args) {
|
|
|
81
82
|
const blockers = openBlockers(env);
|
|
82
83
|
if (blockers.length === 0)
|
|
83
84
|
return null;
|
|
84
|
-
const reply = await
|
|
85
|
+
const reply = await askYesNo({
|
|
86
|
+
question: `Answer the ${blockers.length} unanswered blocker(s) now?`,
|
|
87
|
+
confirm: args.confirm,
|
|
88
|
+
prompt: args.prompt,
|
|
89
|
+
});
|
|
85
90
|
if (!affirmative(reply))
|
|
86
91
|
return null;
|
|
87
92
|
const answers = new Map();
|
|
@@ -17,32 +17,33 @@ import { basename, dirname, join } from 'node:path';
|
|
|
17
17
|
import chalk from 'chalk';
|
|
18
18
|
import { statusesFromItems, computeNextPhase, isPhaseSatisfied, planCompletionLines, } from './plan-run.js';
|
|
19
19
|
import { projectFilename } from './filename.js';
|
|
20
|
+
import { UNICODE_GLYPHS } from '../renderer/glyphs.js';
|
|
20
21
|
import { readProjectFile } from './status.js';
|
|
21
22
|
/**
|
|
22
|
-
* Plain-text mirror of the tracker's
|
|
23
|
-
* words, no chalk
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* Plain-text mirror of the tracker's audit words (plan-run.ts, piece 2 §4) —
|
|
24
|
+
* same words, no chalk, LITERAL unicode glyphs: this is file content, so it
|
|
25
|
+
* never follows a terminal's ASCII fallback. Empty-audit phases render NO
|
|
26
|
+
* segment, matching the tracker's legacy-envelope behaviour. The `default`
|
|
27
|
+
* arm is deliberate forward-compat: an unknown future PlanAuditState reads
|
|
28
|
+
* "audit not run" instead of crashing the projection.
|
|
27
29
|
*/
|
|
28
30
|
function auditWords(audit) {
|
|
29
31
|
if (!audit)
|
|
30
32
|
return null;
|
|
31
33
|
switch (audit.state) {
|
|
32
|
-
case 'passed': return '
|
|
34
|
+
case 'passed': return '✓ audited';
|
|
33
35
|
case 'warn': {
|
|
34
|
-
// Non-blocking tier — passed WITH notes
|
|
35
|
-
// tracker's warn glyph; count comes from findings.
|
|
36
|
+
// Non-blocking tier — passed WITH notes; count comes from findings.
|
|
36
37
|
const n = audit.findings?.length ?? 0;
|
|
37
|
-
return
|
|
38
|
+
return `! ${n} note${n === 1 ? '' : 's'}`;
|
|
38
39
|
}
|
|
39
|
-
case 'waived': return
|
|
40
|
-
case 'failed': return '
|
|
41
|
-
case 'not_audited_hand_edited': return '
|
|
40
|
+
case 'waived': return `· waived${audit.waiveCategory ? ` (${audit.waiveCategory})` : ''}`;
|
|
41
|
+
case 'failed': return '✗ audit failed';
|
|
42
|
+
case 'not_audited_hand_edited': return '· hand-edited';
|
|
42
43
|
case 'pending':
|
|
43
44
|
case 'infra_failed':
|
|
44
45
|
default:
|
|
45
|
-
return '
|
|
46
|
+
return '! audit not run';
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
@@ -117,7 +118,7 @@ export function renderPlanHandover(envelope) {
|
|
|
117
118
|
// resolves — version-less/newest-version resolution in plan-resume.ts
|
|
118
119
|
// matches the whole family.
|
|
119
120
|
const nextAction = allSatisfied
|
|
120
|
-
? planCompletionLines(content.phases).join(' ')
|
|
121
|
+
? planCompletionLines(content.phases, UNICODE_GLYPHS).join(' ')
|
|
121
122
|
: `/abap-plan --resume @${projectFilename({
|
|
122
123
|
title: envelope.title, artefactType: 'plan', id: envelope.id, version: envelope.version,
|
|
123
124
|
})}`;
|