@cspeach/cli 1.1.18 → 1.1.20
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/anthropic-provider.js +30 -10
- package/dist/agent/cache-keepalive.js +162 -0
- package/dist/agent/cold-prune.js +116 -0
- package/dist/agent/loop.js +804 -159
- package/dist/agent/provider-shape.js +263 -0
- package/dist/agent/providers/ai-hub-provider.js +17 -2
- package/dist/agent/providers/byok-provider.js +33 -3
- package/dist/agent/providers/local-provider.js +12 -2
- package/dist/agent/repair-partial.js +125 -7
- package/dist/agent/summarise-via-provider.js +6 -1
- package/dist/agent/system-prompt.js +38 -0
- package/dist/agent/tool-dispatch.js +66 -21
- package/dist/agent/tool-loading-pin.js +100 -0
- 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 +36 -6
- package/dist/commands/auto-compact.js +33 -16
- package/dist/commands/compact.js +49 -11
- package/dist/commands/config-set.js +16 -3
- package/dist/commands/config-show.js +18 -5
- package/dist/commands/cost.js +54 -35
- 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-audit.js +1 -0
- 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 +70 -5
- package/dist/cost/cost-log.js +62 -2
- package/dist/cost/pricing.js +11 -6
- 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/lib/spill-labels.js +13 -0
- package/dist/lock-contention.js +2 -2
- package/dist/models/resolve.js +93 -2
- package/dist/models/server-config.js +158 -3
- package/dist/one-shot.js +150 -45
- package/dist/projects/answer-blockers.js +6 -1
- package/dist/projects/handover-md.js +15 -14
- package/dist/projects/image-attachments.js +15 -2
- 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 +100 -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 +154 -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 +269 -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 +55 -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/current-transport.js +13 -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 +43 -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 +863 -339
- 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/repin-model.js +18 -0
- package/dist/session/resume.js +104 -36
- package/dist/session/store.js +84 -28
- 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/preamble.js +75 -0
- package/dist/skills/source-manifest.js +11 -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-read.js +11 -1
- 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/result-spill.js +238 -0
- package/dist/tools/sap-read.js +74 -15
- package/dist/tools/sap-write.js +122 -10
- package/dist/tools/shell/shell_exec.js +39 -12
- package/dist/tools/subagent/adt-serial.js +33 -0
- package/dist/tools/subagent/agent_run.js +2 -0
- package/dist/tools/subagent/read_agent.js +178 -0
- package/dist/tools/subagent/reader-prompt.js +48 -0
- package/dist/tools/syntax-state.js +67 -0
- package/dist/tools/todo.js +26 -19
- package/dist/tools/tool-loading.js +255 -0
- package/dist/tools/tool-output-read.js +117 -0
- package/dist/tools/transport-fit.js +462 -0
- package/dist/tools/transport-resolution.js +3 -1
- package/dist/tools/transport.js +39 -8
- 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 +2 -1
- 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 +65 -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 +12 -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
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
// The proxy serves `GET {proxy_url}/v1/model-config` (Task 3):
|
|
5
5
|
// { roles: { session_default, audit, compact, router },
|
|
6
6
|
// pricing: Record<model, { inputPer1M, outputPer1M, cacheReadPer1M, cacheCreatePer1M? }>,
|
|
7
|
+
// effort: { session_default?, audit?, compact? }, (Task 4; {} = none)
|
|
8
|
+
// tool_loading: 'deferred' | 'static', (Task 4)
|
|
9
|
+
// auto_compact_tokens: number | null, (Task 4; null = CLI default)
|
|
7
10
|
// updated_at }
|
|
8
11
|
// top-level keys snake_case, pricing entries camelCase, Cache-Control 60s, no auth.
|
|
9
12
|
//
|
|
@@ -24,6 +27,11 @@
|
|
|
24
27
|
import fs from 'node:fs/promises';
|
|
25
28
|
import path from 'node:path';
|
|
26
29
|
import { cspeachRoot } from '../config/paths.js';
|
|
30
|
+
export const EFFORT_VALUES = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
31
|
+
const EFFORT_ROLE_KEYS = ['session_default', 'audit', 'compact'];
|
|
32
|
+
export function isEffort(v) {
|
|
33
|
+
return typeof v === 'string' && EFFORT_VALUES.includes(v);
|
|
34
|
+
}
|
|
27
35
|
// ── Synchronous module snapshot ───────────────────────────────────────────────
|
|
28
36
|
let snapshot = null;
|
|
29
37
|
/** The current served-config snapshot, or null (⇒ built-ins win). Pure sync read. */
|
|
@@ -60,6 +68,27 @@ function coercePricing(raw) {
|
|
|
60
68
|
? raw
|
|
61
69
|
: {};
|
|
62
70
|
}
|
|
71
|
+
/** Keep only valid effort levels for the known effort roles. */
|
|
72
|
+
function coerceEffort(raw) {
|
|
73
|
+
const out = {};
|
|
74
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
75
|
+
return out;
|
|
76
|
+
const r = raw;
|
|
77
|
+
for (const k of EFFORT_ROLE_KEYS) {
|
|
78
|
+
if (isEffort(r[k]))
|
|
79
|
+
out[k] = r[k];
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
function coerceToolLoading(raw) {
|
|
84
|
+
return raw === 'deferred' || raw === 'static' ? raw : undefined;
|
|
85
|
+
}
|
|
86
|
+
/** Same bounds as the proxy column CHECK (migration 021). */
|
|
87
|
+
function coerceAutoCompactTokens(raw) {
|
|
88
|
+
return typeof raw === 'number' && Number.isInteger(raw) && raw >= 10_000 && raw <= 10_000_000
|
|
89
|
+
? raw
|
|
90
|
+
: undefined;
|
|
91
|
+
}
|
|
63
92
|
/**
|
|
64
93
|
* Map a served (or cached) document to a ServerModelConfig, or null when the
|
|
65
94
|
* shape is unusable. `fetchedAt`, when provided, is preserved (disk-cache read);
|
|
@@ -73,9 +102,14 @@ function coerceDocument(body, fetchedAt) {
|
|
|
73
102
|
const roles = coerceRoles(b.roles);
|
|
74
103
|
if (Object.keys(roles).length === 0)
|
|
75
104
|
return null;
|
|
105
|
+
const toolLoading = coerceToolLoading(b.tool_loading);
|
|
106
|
+
const autoCompactTokens = coerceAutoCompactTokens(b.auto_compact_tokens);
|
|
76
107
|
return {
|
|
77
108
|
roles,
|
|
78
109
|
pricing: coercePricing(b.pricing),
|
|
110
|
+
effort: coerceEffort(b.effort),
|
|
111
|
+
...(toolLoading ? { tool_loading: toolLoading } : {}),
|
|
112
|
+
...(autoCompactTokens !== undefined ? { auto_compact_tokens: autoCompactTokens } : {}),
|
|
79
113
|
fetchedAt: typeof fetchedAt === 'string' && fetchedAt.length > 0 ? fetchedAt : new Date().toISOString(),
|
|
80
114
|
};
|
|
81
115
|
}
|
|
@@ -112,9 +146,11 @@ async function readFreshDiskCache() {
|
|
|
112
146
|
// ── Public entrypoint ─────────────────────────────────────────────────────────
|
|
113
147
|
/**
|
|
114
148
|
* Fetch the served model-config and update the module snapshot. Managed mode
|
|
115
|
-
* ONLY.
|
|
149
|
+
* ONLY. NEVER throws. Startup awaits it with `timeoutMs: 1500` (Task 3) so the
|
|
150
|
+
* session model is pinned from the served config before call 0; a slower
|
|
151
|
+
* fetch aborts and falls back to a fresh disk cache. Default cap: 5 s.
|
|
116
152
|
*/
|
|
117
|
-
export async function loadServerModelConfig(cfg) {
|
|
153
|
+
export async function loadServerModelConfig(cfg, opts) {
|
|
118
154
|
// Non-managed modes bypass the proxy for inference and must never fetch.
|
|
119
155
|
if (cfg.llm?.mode !== 'managed')
|
|
120
156
|
return;
|
|
@@ -125,7 +161,7 @@ export async function loadServerModelConfig(cfg) {
|
|
|
125
161
|
// rejection that crashes startup. Keep it caught like every other failure.
|
|
126
162
|
const url = `${cfg.proxy_url.replace(/\/$/, '')}/v1/model-config`;
|
|
127
163
|
const controller = new AbortController();
|
|
128
|
-
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
164
|
+
const timer = setTimeout(() => controller.abort(), opts?.timeoutMs ?? FETCH_TIMEOUT_MS);
|
|
129
165
|
timer.unref?.();
|
|
130
166
|
let body;
|
|
131
167
|
try {
|
|
@@ -153,3 +189,122 @@ export async function loadServerModelConfig(cfg) {
|
|
|
153
189
|
snapshot = cached;
|
|
154
190
|
}
|
|
155
191
|
}
|
|
192
|
+
let customerSnapshot = null;
|
|
193
|
+
/** The current customer snapshot, or null (⇒ layer skipped). Pure sync read. */
|
|
194
|
+
export function getCustomerModelConfig() {
|
|
195
|
+
return customerSnapshot;
|
|
196
|
+
}
|
|
197
|
+
/** Test-only setter for the customer snapshot. */
|
|
198
|
+
export function __setCustomerModelConfigForTests(c) {
|
|
199
|
+
customerSnapshot = c;
|
|
200
|
+
}
|
|
201
|
+
function coerceCustomer(body) {
|
|
202
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
203
|
+
return null;
|
|
204
|
+
const b = body;
|
|
205
|
+
const model = typeof b.model === 'string' && b.model.trim().length > 0 ? b.model.trim() : null;
|
|
206
|
+
return {
|
|
207
|
+
model,
|
|
208
|
+
effort: isEffort(b.effort) ? b.effort : null,
|
|
209
|
+
enforced: b.enforced === true,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Fetch the caller's customer model-config and update the customer snapshot.
|
|
214
|
+
* NEVER throws; on any failure the snapshot is null.
|
|
215
|
+
*/
|
|
216
|
+
export async function loadCustomerModelConfig(cfg, getBearer, opts) {
|
|
217
|
+
customerSnapshot = null;
|
|
218
|
+
const controller = new AbortController();
|
|
219
|
+
const timer = setTimeout(() => controller.abort(), opts?.timeoutMs ?? FETCH_TIMEOUT_MS);
|
|
220
|
+
timer.unref?.();
|
|
221
|
+
try {
|
|
222
|
+
// The keychain read shares the cap: a hanging getBearer loses the race to
|
|
223
|
+
// the same timer that aborts the fetch (review M1).
|
|
224
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
225
|
+
controller.signal.addEventListener('abort', () => reject(new Error('timeout')), { once: true });
|
|
226
|
+
});
|
|
227
|
+
aborted.catch(() => { });
|
|
228
|
+
const key = await Promise.race([getBearer(), aborted]);
|
|
229
|
+
if (!key)
|
|
230
|
+
return;
|
|
231
|
+
const url = `${cfg.proxy_url.replace(/\/$/, '')}/v1/me/model-config`;
|
|
232
|
+
const r = await fetch(url, {
|
|
233
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
234
|
+
signal: controller.signal,
|
|
235
|
+
});
|
|
236
|
+
if (!r.ok)
|
|
237
|
+
return;
|
|
238
|
+
customerSnapshot = coerceCustomer(await r.json());
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
customerSnapshot = null;
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
clearTimeout(timer);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// ── Tool-loading kill switch (Task 16, ruling F13) ───────────────────────────
|
|
248
|
+
//
|
|
249
|
+
// Managed mode reads `tool_loading` from the served snapshot above. BYOK never
|
|
250
|
+
// loads that snapshot (its roles must not steer a BYOK session), so it reads
|
|
251
|
+
// ONLY `tool_loading` from the same public endpoint — and only when signed in
|
|
252
|
+
// (a CSPeach bearer exists). No bearer, any failure, or an invalid value ⇒
|
|
253
|
+
// undefined, which resolveToolLoading turns into 'static' for BYOK. No disk
|
|
254
|
+
// cache. NEVER throws. Startup runs it alongside the other loads under the
|
|
255
|
+
// same 1.5 s cap.
|
|
256
|
+
let byokToolLoading;
|
|
257
|
+
/** Test-only setter for the BYOK tool-loading value. */
|
|
258
|
+
export function __setByokToolLoadingForTests(v) {
|
|
259
|
+
byokToolLoading = v;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The served tool-loading mode: the managed snapshot's `tool_loading`, else
|
|
263
|
+
* the BYOK fetch result, else undefined. Pure sync read.
|
|
264
|
+
*/
|
|
265
|
+
export function servedToolLoading() {
|
|
266
|
+
return snapshot?.tool_loading ?? byokToolLoading;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Task 21 fix round (controller ruling) — the managed proxy's capability
|
|
270
|
+
* signal: its served config carries `tool_loading` (migration 021 and later).
|
|
271
|
+
* An older proxy serves no such field and adds none of the new betas, so the
|
|
272
|
+
* loop sends the new wire shapes (deferred tools, tool search, tool_addition,
|
|
273
|
+
* `thinking.block_binding`) on the managed path only when this is true.
|
|
274
|
+
* Managed snapshot only; BYOK never reads it. Pure sync read.
|
|
275
|
+
*/
|
|
276
|
+
export function managedProxyServesNewShapes() {
|
|
277
|
+
return snapshot?.tool_loading !== undefined;
|
|
278
|
+
}
|
|
279
|
+
/** BYOK only: fetch `/v1/model-config` and keep its `tool_loading`. */
|
|
280
|
+
export async function fetchToolLoadingMode(cfg, getBearer, opts) {
|
|
281
|
+
byokToolLoading = undefined;
|
|
282
|
+
if (cfg.llm?.mode !== 'byok')
|
|
283
|
+
return undefined;
|
|
284
|
+
const controller = new AbortController();
|
|
285
|
+
const timer = setTimeout(() => controller.abort(), opts?.timeoutMs ?? FETCH_TIMEOUT_MS);
|
|
286
|
+
timer.unref?.();
|
|
287
|
+
try {
|
|
288
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
289
|
+
controller.signal.addEventListener('abort', () => reject(new Error('timeout')), { once: true });
|
|
290
|
+
});
|
|
291
|
+
aborted.catch(() => { });
|
|
292
|
+
const key = await Promise.race([getBearer(), aborted]);
|
|
293
|
+
if (!key)
|
|
294
|
+
return undefined;
|
|
295
|
+
const url = `${cfg.proxy_url.replace(/\/$/, '')}/v1/model-config`;
|
|
296
|
+
const r = await fetch(url, { signal: controller.signal });
|
|
297
|
+
if (!r.ok)
|
|
298
|
+
return undefined;
|
|
299
|
+
const body = (await r.json());
|
|
300
|
+
byokToolLoading = coerceToolLoading(body && typeof body === 'object' ? body.tool_loading : undefined);
|
|
301
|
+
return byokToolLoading;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
byokToolLoading = undefined;
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
clearTimeout(timer);
|
|
309
|
+
}
|
|
310
|
+
}
|
package/dist/one-shot.js
CHANGED
|
@@ -1,25 +1,39 @@
|
|
|
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';
|
|
10
14
|
import { resolveModelRole } from './models/resolve.js';
|
|
11
|
-
import { loadServerModelConfig } from './models/server-config.js';
|
|
15
|
+
import { loadServerModelConfig, loadCustomerModelConfig, fetchToolLoadingMode } from './models/server-config.js';
|
|
12
16
|
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';
|
|
@@ -30,6 +44,7 @@ import './tools/ask-question.js';
|
|
|
30
44
|
import './tools/dispatch-skill.js';
|
|
31
45
|
import './tools/todo.js';
|
|
32
46
|
import './tools/filesystem/file-read.js';
|
|
47
|
+
import './tools/tool-output-read.js';
|
|
33
48
|
import './tools/filesystem/read-document.js';
|
|
34
49
|
import './tools/filesystem/file-edit.js';
|
|
35
50
|
import './tools/filesystem/file-write.js';
|
|
@@ -51,16 +66,32 @@ import './tools/subagent/background_run.js';
|
|
|
51
66
|
import './tools/subagent/monitor_emit.js';
|
|
52
67
|
import './tools/subagent/schedule_draft_create.js';
|
|
53
68
|
import './tools/subagent/agent_run.js';
|
|
69
|
+
import './tools/subagent/read_agent.js';
|
|
54
70
|
export async function runOneShot(prompt, opts) {
|
|
71
|
+
applyColorMode(); // NO_COLOR (piece 2 §1.2)
|
|
55
72
|
const cfg = await loadConfig();
|
|
56
73
|
// Standalone (2026-09-16) — run with no SAP system at all: no connection, no
|
|
57
74
|
// AdtClient, SAP tools hidden. `--no-sap` for this run, or `no_sap = true`.
|
|
58
75
|
const standalone = resolveStandalone(cfg, opts?.noSap === true);
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
// One-shot output is unchanged by the classic retirement: `auto` never
|
|
77
|
+
// mounts Ink for a one-shot run. CSPEACH_UI=classic is honoured here too;
|
|
78
|
+
// the stored-value rewrite happens only on an interactive start.
|
|
79
|
+
setOneShotOverride(true);
|
|
80
|
+
setRenderingOverride(resolveRenderingMode(cfg.ui.rendering).mode);
|
|
81
|
+
setLook(cfg.ui.look);
|
|
82
|
+
setBackground(cfg.ui.background);
|
|
83
|
+
// model-governance step 2d — fetch the served model-config (managed mode
|
|
84
|
+
// only). Task 3: awaited, but startup waits at most 1.5 s (then a fresh disk
|
|
85
|
+
// cache, else the built-ins), so the session model pinned at newSession
|
|
86
|
+
// below comes from the served config. Never throws.
|
|
87
|
+
// Task 12 — the customer layer (/v1/me/model-config, every mode with a
|
|
88
|
+
// key) loads in parallel under the SAME 1.5 s cap. Fails open.
|
|
89
|
+
await Promise.all([
|
|
90
|
+
loadServerModelConfig(cfg, { timeoutMs: 1500 }),
|
|
91
|
+
loadCustomerModelConfig(cfg, async () => (await (await getStore()).get()) ?? '', { timeoutMs: 1500 }),
|
|
92
|
+
// Task 16 (F13) — BYOK reads only the served tool_loading (signed in only).
|
|
93
|
+
fetchToolLoadingMode(cfg, async () => (await (await getStore()).get()) ?? '', { timeoutMs: 1500 }),
|
|
94
|
+
]);
|
|
64
95
|
// B5 (2026-06-11) — one-shot is headless by design: a single turn, no
|
|
65
96
|
// REPL, often piped (CI / battery). Any stdin prompt would hang forever
|
|
66
97
|
// (defect D1). The headless answer policy (ask_question auto-answers,
|
|
@@ -80,8 +111,10 @@ export async function runOneShot(prompt, opts) {
|
|
|
80
111
|
// write_mode stands on its own.
|
|
81
112
|
const oneShotAlias = standalone ? undefined : (opts?.sapAlias ?? Object.keys(cfg.sap)[0]);
|
|
82
113
|
const oneShotRole = oneShotAlias !== undefined ? cfg.sap[oneShotAlias]?.role : undefined;
|
|
114
|
+
// Fix round 3 (item 7): every startup notice below goes to STDERR — piped
|
|
115
|
+
// stdout stays the answer only (spec §8).
|
|
83
116
|
if (getEffectiveWriteMode(cfg.write_mode, oneShotRole) === 'advisory-only') {
|
|
84
|
-
console.
|
|
117
|
+
console.error(formatAttention({ text: 'Write mode: advisory', fact: 'writes are proposed, not executed' }).trimEnd());
|
|
85
118
|
}
|
|
86
119
|
// local_build toggle — force-enable the flag-gated filesystem tools AND
|
|
87
120
|
// shell_exec BEFORE the (single) turn when persisted-on. Resolve with backward
|
|
@@ -91,11 +124,11 @@ export async function runOneShot(prompt, opts) {
|
|
|
91
124
|
{
|
|
92
125
|
const announce = applyLocalBuildConfig(resolveLocalBuild(cfg), process.cwd());
|
|
93
126
|
if (announce)
|
|
94
|
-
console.
|
|
127
|
+
console.error(chalk.dim(announce));
|
|
95
128
|
// 2026-09-17 — standalone writes files by contract; enable the file tools
|
|
96
129
|
// without the separate local_build step (see repl.tsx for the live case).
|
|
97
130
|
else if (standalone)
|
|
98
|
-
console.
|
|
131
|
+
console.error(chalk.dim(applyStandaloneFileTools(process.cwd())));
|
|
99
132
|
}
|
|
100
133
|
const store = await getStore();
|
|
101
134
|
if (!(await store.get())) {
|
|
@@ -104,9 +137,9 @@ export async function runOneShot(prompt, opts) {
|
|
|
104
137
|
}
|
|
105
138
|
// Credits (2026-09-16) — resolve the display mode before the turn runs, so
|
|
106
139
|
// the per-turn cost JSONL records the ledger charge and nothing downstream
|
|
107
|
-
// can print a dollar to a managed customer
|
|
108
|
-
//
|
|
109
|
-
//
|
|
140
|
+
// can print a dollar to a managed customer — including the one-line stderr
|
|
141
|
+
// receipt after the turn (piece 2 I3a), and the agent loop's cost-log path,
|
|
142
|
+
// which is shared with the REPL. Capped + fail-safe; BYOK/local skip the call.
|
|
110
143
|
await initCredits(cfg, async () => (await store.get()) ?? '');
|
|
111
144
|
const aliases = Object.keys(cfg.sap);
|
|
112
145
|
let alias;
|
|
@@ -117,12 +150,12 @@ export async function runOneShot(prompt, opts) {
|
|
|
117
150
|
// filter + dispatch guard.
|
|
118
151
|
alias = STANDALONE_ALIAS;
|
|
119
152
|
adt = createOfflineAdtClient();
|
|
120
|
-
console.
|
|
153
|
+
console.error(chalk.dim('Standalone mode: no SAP system connected — skills work from pasted code. ' +
|
|
121
154
|
'Connect later with: cspeach config add-sap'));
|
|
122
155
|
// One-shot is headless by design (no prompts), so an unanswered standalone
|
|
123
156
|
// profile can't be filled in here — say how, and carry on with "unknown".
|
|
124
157
|
if (!cfg.standalone) {
|
|
125
|
-
console.
|
|
158
|
+
console.error(chalk.dim('Standalone profile not set — run: cspeach config set standalone.platform ...'));
|
|
126
159
|
}
|
|
127
160
|
}
|
|
128
161
|
else {
|
|
@@ -133,9 +166,19 @@ export async function runOneShot(prompt, opts) {
|
|
|
133
166
|
// C.1 — --sap-alias flag override
|
|
134
167
|
alias = opts?.sapAlias ?? aliases[0];
|
|
135
168
|
if (opts?.sapAlias) {
|
|
136
|
-
console.
|
|
169
|
+
console.error(chalk.dim(`Using --sap-alias ${opts.sapAlias}`));
|
|
170
|
+
}
|
|
171
|
+
let conn;
|
|
172
|
+
try {
|
|
173
|
+
conn = await getConnection(alias, cfg.sap[alias]);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
const code = unreachableCode(err);
|
|
177
|
+
if (!code)
|
|
178
|
+
throw err;
|
|
179
|
+
console.error(unreachableMessage(alias, cfg.sap[alias].host, cfg.sap[alias].port, code));
|
|
180
|
+
return 1;
|
|
137
181
|
}
|
|
138
|
-
const conn = await getConnection(alias, cfg.sap[alias]);
|
|
139
182
|
adt = new AdtClient(conn);
|
|
140
183
|
}
|
|
141
184
|
const session = newSession(makeSessionId(), alias, null, resolveModelRole('session_default', cfg));
|
|
@@ -155,30 +198,51 @@ export async function runOneShot(prompt, opts) {
|
|
|
155
198
|
skill = parsed.skill;
|
|
156
199
|
}
|
|
157
200
|
else {
|
|
158
|
-
// One-shot has no interactive picker — always uses top-1 and
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
201
|
+
// One-shot has no interactive picker — always uses top-1 and names it on
|
|
202
|
+
// a dim stderr line. If < 0.6, add a stderr `!` notice with the top-3
|
|
203
|
+
// candidates so the user can rerun with an explicit /skill.
|
|
204
|
+
// Fix round 3 (item 8) — say we are routing while the classifier runs
|
|
205
|
+
// (stderr; nothing prints when it answers within a second).
|
|
206
|
+
const routingBeat = startThinkingHeartbeat({ thresholds: ROUTING_HEARTBEAT_THRESHOLDS });
|
|
207
|
+
let c;
|
|
208
|
+
try {
|
|
209
|
+
c = await classifyPrompt(body, {
|
|
210
|
+
sap_system: alias,
|
|
211
|
+
last_skill: session.last_skill ?? null,
|
|
212
|
+
last_object: parsed.object ?? session.last_object ?? null,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
// Round 4 item 4 — a missing key here was an uncaught `no_api_key`
|
|
217
|
+
// with a stack (and a libuv assertion on exit). One clean line, exit 1.
|
|
218
|
+
if (err instanceof Error && err.message === 'no_api_key') {
|
|
219
|
+
console.error('No API key configured. Run `cspeach` (interactive) first to set one.');
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
routingBeat.stop();
|
|
226
|
+
}
|
|
166
227
|
if (c.offline) {
|
|
167
228
|
// Remote classify failed twice — local-rules fallback fired. Print
|
|
168
229
|
// to stderr so it doesn't pollute stdout for scripted callers, but
|
|
169
230
|
// is still visible.
|
|
170
|
-
|
|
231
|
+
// Fix 9 (2026-09-26): same true wording as the REPL — a timeout is not "offline".
|
|
232
|
+
console.error(chalk.dim(routingFailureOneShotLine(routingFailureFromResult(c), c.transport_error)));
|
|
171
233
|
}
|
|
172
|
-
|
|
234
|
+
// Piece 2 (spec §8/§9) — routing chatter goes to STDERR so piped stdout
|
|
235
|
+
// stays the answer only. Confidence percentages are dropped everywhere.
|
|
173
236
|
if (c.screenshot)
|
|
174
|
-
|
|
237
|
+
process.stderr.write(formatInfo(`Screenshot: ${c.screenshot}`));
|
|
238
|
+
process.stderr.write(formatRoutingLine({ kind: 'using', skill: c.skill }));
|
|
175
239
|
if (c.confidence < 0.6) {
|
|
176
240
|
const alternates = [c.skill, ...c.alternates].slice(0, 3).map((s) => `/${s}`).join(', ');
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
241
|
+
process.stderr.write(formatAttention({
|
|
242
|
+
text: 'Low confidence',
|
|
243
|
+
fact: `candidates: ${alternates}`,
|
|
244
|
+
next: 'rerun with an explicit /skill to override',
|
|
245
|
+
}));
|
|
182
246
|
}
|
|
183
247
|
skill = c.skill;
|
|
184
248
|
}
|
|
@@ -201,18 +265,59 @@ export async function runOneShot(prompt, opts) {
|
|
|
201
265
|
throw new Error('No API key configured');
|
|
202
266
|
return key;
|
|
203
267
|
});
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
268
|
+
const turnSnapshot = snapshotTurnStart(session);
|
|
269
|
+
// Round 6 item 4f — stdout is the answer, trimmed: no blank lines before
|
|
270
|
+
// it, exactly one newline after it.
|
|
271
|
+
const answerTrim = new AnswerTrim();
|
|
272
|
+
const answerEmitter = createClassicOutputEmitter((s) => {
|
|
273
|
+
const out = answerTrim.feed(s);
|
|
274
|
+
if (!out)
|
|
275
|
+
return;
|
|
276
|
+
// Review M3 — a painting spinner ends the line itself; tell the trim so
|
|
277
|
+
// its held newline does not become a stray blank line.
|
|
278
|
+
const forced = aboveSpinnerAddsNewline(out);
|
|
279
|
+
writeAboveSpinner(out, writeStdout);
|
|
280
|
+
if (forced)
|
|
281
|
+
answerTrim.noteForcedNewline();
|
|
216
282
|
});
|
|
283
|
+
// Owner ruling (r3 M11) — stdout is the answer only, everywhere: tool
|
|
284
|
+
// notices (Rule 9 transport overrides, snapshot lines, the write chain)
|
|
285
|
+
// and tool rows go to STDERR in one-shot.
|
|
286
|
+
const noticeEmitter = createClassicOutputEmitter((s) => { process.stderr.write(s); });
|
|
287
|
+
try {
|
|
288
|
+
await runTurn({
|
|
289
|
+
provider,
|
|
290
|
+
userMessage: body,
|
|
291
|
+
skill,
|
|
292
|
+
// Fix round 7 item 2 — runTurn reads params.chunkEmitter (NOT
|
|
293
|
+
// ctx.chunkEmitter) for the answer stream; without this the trim never
|
|
294
|
+
// ran. Liveness stays off stdout (see loop.ts livenessEmitter).
|
|
295
|
+
chunkEmitter: answerEmitter,
|
|
296
|
+
// B2 fix (2026-06-12): one-shot runs render tool notices ('warn'/'info'
|
|
297
|
+
// — Rule 9 transport overrides, snapshot lines) instead of dropping them;
|
|
298
|
+
// headless transcripts need them for auditability. Owner ruling (r3 M11):
|
|
299
|
+
// they go to STDERR (noticeEmitter) — stdout is the answer only.
|
|
300
|
+
// Task 5 (ux-wave2): todoEmitter wired for the uniform top-level-ctx
|
|
301
|
+
// contract — no listener exists in one-shot, so emits are no-ops, but
|
|
302
|
+
// "top-level ctxs carry it / subagent ctxs omit it" stays the single
|
|
303
|
+
// rule (see ui/todo-emitter.ts).
|
|
304
|
+
ctx: { adt, sapAlias: alias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: alwaysDeclinePreviewHook, chunkEmitter: noticeEmitter, todoEmitter },
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
// Review M2 — even when the turn throws, the answer so far ends with its
|
|
309
|
+
// newline (and any held colour reset) instead of being cut mid-line.
|
|
310
|
+
const answerTail = answerTrim.end();
|
|
311
|
+
if (answerTail)
|
|
312
|
+
writeStdout(answerTail);
|
|
313
|
+
}
|
|
314
|
+
// Piece 2 (spec §8) — the one-line receipt, on STDERR (stdout = the answer).
|
|
315
|
+
const facts = computeTurnFacts(session, turnSnapshot);
|
|
316
|
+
process.stderr.write(formatTurnReceipt({
|
|
317
|
+
skill,
|
|
318
|
+
tokens: facts.turnTokens,
|
|
319
|
+
spend: turnSpendText({ costThisTurn: facts.turnCost, credits: globalCredits.snapshot() }),
|
|
320
|
+
ms: facts.elapsedMs,
|
|
321
|
+
}) + '\n');
|
|
217
322
|
return 0;
|
|
218
323
|
}
|
|
@@ -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
|
})}`;
|
|
@@ -135,6 +135,14 @@ export function buildUserContent(text, images) {
|
|
|
135
135
|
* hits the 32 MB request cap (and stops paying for stale screenshots).
|
|
136
136
|
*/
|
|
137
137
|
export const MAX_HISTORY_IMAGES = 10;
|
|
138
|
+
/**
|
|
139
|
+
* Task 7 — stub images in batches, not one per turn. Each stub edits an
|
|
140
|
+
* earlier user message, and Opus 5.5 binds every later thinking block to the
|
|
141
|
+
* exact prefix that produced it, so each trim invalidates them (and the prompt
|
|
142
|
+
* cache). Once over `max`, the live count drops to `max - IMAGE_TRIM_BATCH + 1`
|
|
143
|
+
* (6 at max 10), so the next trim is IMAGE_TRIM_BATCH images away.
|
|
144
|
+
*/
|
|
145
|
+
export const IMAGE_TRIM_BATCH = 5;
|
|
138
146
|
/** Mutates `messages`; returns how many image blocks were removed. */
|
|
139
147
|
export function trimHistoryImages(messages, max = MAX_HISTORY_IMAGES) {
|
|
140
148
|
const slots = [];
|
|
@@ -146,10 +154,15 @@ export function trimHistoryImages(messages, max = MAX_HISTORY_IMAGES) {
|
|
|
146
154
|
slots.push({ blocks: m.content, i });
|
|
147
155
|
});
|
|
148
156
|
}
|
|
149
|
-
|
|
157
|
+
if (slots.length <= max)
|
|
158
|
+
return 0;
|
|
159
|
+
// Ruling F11: stub slots.length − (max − batch + 1) ⇒ 11 images at max 10
|
|
160
|
+
// stub the oldest 5. Clamped for a tiny `max` (never keep fewer than 1).
|
|
161
|
+
const keep = Math.max(1, max - IMAGE_TRIM_BATCH + 1);
|
|
162
|
+
const excess = slots.length - keep;
|
|
150
163
|
for (let k = 0; k < excess; k++) {
|
|
151
164
|
const { blocks, i } = slots[k];
|
|
152
165
|
blocks[i] = { type: 'text', text: '[image removed from history]' };
|
|
153
166
|
}
|
|
154
|
-
return
|
|
167
|
+
return excess;
|
|
155
168
|
}
|