@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
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// cspeach-cli/src/tools/fiori/ui5-version.ts
|
|
2
|
+
//
|
|
3
|
+
// THE one pinned UI5 version: the default both scaffolds write into
|
|
4
|
+
// manifest.json (minUI5Version) and the version the offline smoke harness
|
|
5
|
+
// serves (test-harness/fe-draft/routes.ts imports it as its "newest" pin).
|
|
6
|
+
//
|
|
7
|
+
// Why an exact, hosted patch and never an `x.y.0` guess: fiori-tools-proxy
|
|
8
|
+
// fetches `https://ui5.sap.com/<minUI5Version>/resources/...` verbatim, and SAP
|
|
9
|
+
// removes old patch releases from that CDN. The old default `1.120.0` was gone
|
|
10
|
+
// on 2026-09-21 (sap/m/library.js → 404): every freshly scaffolded app booted
|
|
11
|
+
// to a blank page. When SAP retires this patch too, bump the patch — the
|
|
12
|
+
// smoke-e2e run fails with a message pointing here.
|
|
13
|
+
//
|
|
14
|
+
// Why 1.120: the S/4HANA 2023-era LTS; it parses the hyphenated
|
|
15
|
+
// `data-sap-ui-on-init` bootstrap attribute the writers emit (1.71 does not).
|
|
16
|
+
export const PINNED_UI5_VERSION = '1.120.51';
|
package/dist/tools/index.js
CHANGED
|
@@ -20,6 +20,14 @@ export function listTools() {
|
|
|
20
20
|
// always returned, preserving today's listTools() semantics exactly.
|
|
21
21
|
return Array.from(registry.values()).filter((t) => !t.flagGated || isToolFlagOn(t.name));
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Every registered tool, flag-gated ones included (no visibility filter).
|
|
25
|
+
* For coverage checks (e.g. every tool has a transcript label); callers that
|
|
26
|
+
* send tools to the model must keep using listTools().
|
|
27
|
+
*/
|
|
28
|
+
export function listAllTools() {
|
|
29
|
+
return Array.from(registry.values());
|
|
30
|
+
}
|
|
23
31
|
/**
|
|
24
32
|
* Reserved alias for standalone mode — CSPeach running with no SAP system.
|
|
25
33
|
* Set at the single ctx-construction site in repl.tsx / one-shot.ts alongside
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* shell: shell_exec
|
|
13
13
|
* fiori: fiori_scaffold, fiori_apply, fiori_list, fiori_deploy_config,
|
|
14
14
|
* fiori_scaffold_fe, fiori_fe_extend, fiori_render_smoke,
|
|
15
|
+
* fiori_preview (L1-6 — the preview server every browser check
|
|
16
|
+
* depends on), fiori_metadata_audit, fiori_draft_smoke
|
|
17
|
+
* (L1-5b — a SAP write, approval-gated like every write),
|
|
15
18
|
* fiori_sample_search, fiori_sample_get
|
|
16
19
|
* extend: extend_model_insert (anchored CDS/DDLX edit — abap-extend-model)
|
|
17
20
|
* subagent: agent_run (Task 7, 2026-07-03 — customers get subagent
|
|
@@ -63,6 +66,9 @@ export const LOCAL_BUILD_TOOLS = [
|
|
|
63
66
|
'fiori_scaffold_fe',
|
|
64
67
|
'fiori_fe_extend',
|
|
65
68
|
'fiori_render_smoke',
|
|
69
|
+
'fiori_preview',
|
|
70
|
+
'fiori_metadata_audit',
|
|
71
|
+
'fiori_draft_smoke',
|
|
66
72
|
'fiori_sample_search',
|
|
67
73
|
'fiori_sample_get',
|
|
68
74
|
'extend_model_insert',
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task 17 (model control, 2026-09-27, spec D12) — large tool results go to a
|
|
3
|
+
* file; the history keeps a preview and a path.
|
|
4
|
+
*
|
|
5
|
+
* Claude Code's "output too large, saved to file" pattern. Above
|
|
6
|
+
* SPILL_THRESHOLD_CHARS the full text is written to
|
|
7
|
+
* ~/.cspeach/streams/<sessionId>/tool-<toolUseId>.txt and the tool_result
|
|
8
|
+
* carries the first 60 lines (plus the last 20 for source, SQL, shell and file windows),
|
|
9
|
+
* then one pointer line naming tool_output_read.
|
|
10
|
+
*
|
|
11
|
+
* Sizing happens ONCE, when the tool_result is created (the handler's return
|
|
12
|
+
* value) — never afterwards, so it is not a history edit and the cached
|
|
13
|
+
* prefix / preserved thinking stay append-only.
|
|
14
|
+
*
|
|
15
|
+
* The spill file holds exactly the text the tool would have returned — no
|
|
16
|
+
* more (no secrets beyond what the tool already produced), and it lives in the
|
|
17
|
+
* per-session area under ~/.cspeach, never in the user's project.
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from 'node:fs';
|
|
20
|
+
import * as path from 'node:path';
|
|
21
|
+
import * as crypto from 'node:crypto';
|
|
22
|
+
import { streamsRoot } from '../agent/turn-stream.js';
|
|
23
|
+
import { rememberSpillLabel } from '../lib/spill-labels.js';
|
|
24
|
+
export const SPILL_THRESHOLD_CHARS = 8_000;
|
|
25
|
+
export const PREVIEW_HEAD_LINES = 60;
|
|
26
|
+
export const PREVIEW_TAIL_LINES = 20;
|
|
27
|
+
/** One preview line never carries more than this (a minified JSON line must not defeat the preview). */
|
|
28
|
+
export const PREVIEW_LINE_MAX_CHARS = 500;
|
|
29
|
+
/** Character budgets for the head and tail blocks of the preview. */
|
|
30
|
+
export const PREVIEW_HEAD_BUDGET_CHARS = 6_000;
|
|
31
|
+
export const PREVIEW_TAIL_BUDGET_CHARS = 2_000;
|
|
32
|
+
/**
|
|
33
|
+
* Kinds whose preview also keeps the last lines (the end matters: totals,
|
|
34
|
+
* exit code, ENDCLASS; for file_read the window's last line). Only search
|
|
35
|
+
* hits and reports are head-only.
|
|
36
|
+
*/
|
|
37
|
+
const TAIL_KINDS = new Set(['source', 'sql', 'shell', 'file']);
|
|
38
|
+
/** Session ids / tool_use ids become file names: keep them path-inert. */
|
|
39
|
+
function safeSegment(s) {
|
|
40
|
+
const cleaned = s.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
41
|
+
return cleaned.length > 0 ? cleaned : '_';
|
|
42
|
+
}
|
|
43
|
+
/** The directory that holds one session's spill files. */
|
|
44
|
+
export function spillDirFor(sessionId) {
|
|
45
|
+
return path.join(streamsRoot(), safeSegment(sessionId));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Session id as the tools see it, or undefined when the ctx carries none
|
|
49
|
+
* (some tests and non-loop call paths). Without a session there is no
|
|
50
|
+
* private spill dir, so nothing is spilled and tool_output_read refuses —
|
|
51
|
+
* never a shared fallback directory (fix round 1, minor 3).
|
|
52
|
+
*/
|
|
53
|
+
export function sessionIdOf(ctx) {
|
|
54
|
+
const id = ctx.session?.id;
|
|
55
|
+
return typeof id === 'string' && id.length > 0 ? id : undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Spill file names: `tool-<id>.txt`, or `tool-<id>.n.txt` for text that is
|
|
59
|
+
* already line-numbered (file_read windows keep the FILE's line numbers, so
|
|
60
|
+
* tool_output_read shows those lines as they are instead of numbering again —
|
|
61
|
+
* one numbering space). tool_output_read accepts only these shapes.
|
|
62
|
+
*/
|
|
63
|
+
export const SPILL_FILE_RE = /^tool-[A-Za-z0-9_-]+(\.n)?\.txt$/;
|
|
64
|
+
export const NUMBERED_SPILL_RE = /\.n\.txt$/;
|
|
65
|
+
/** Spill files older than this are deleted at REPL start (like the notice logs). */
|
|
66
|
+
export const SPILL_MAX_AGE_DAYS = 14;
|
|
67
|
+
function clipLine(line) {
|
|
68
|
+
if (line.length <= PREVIEW_LINE_MAX_CHARS)
|
|
69
|
+
return line;
|
|
70
|
+
return `${line.slice(0, PREVIEW_LINE_MAX_CHARS)}… [+${line.length - PREVIEW_LINE_MAX_CHARS} chars]`;
|
|
71
|
+
}
|
|
72
|
+
export function pointerLine(lines, chars, filePath) {
|
|
73
|
+
return `[${lines} lines / ${chars} chars — full output saved to ${filePath}; read slices with tool_output_read(path, offset, limit)]`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build the preview text for `text` (already known to be large). Pure. Line
|
|
77
|
+
* numbers in the omitted marker are the spill file's own (1-based, =
|
|
78
|
+
* tool_output_read offset + 1); for an already-numbered spill the marker names
|
|
79
|
+
* the offset instead, since the visible numbers belong to the source file.
|
|
80
|
+
*/
|
|
81
|
+
export function buildPreview(text, kind, filePath, opts = {}) {
|
|
82
|
+
const lines = text.split('\n');
|
|
83
|
+
const total = lines.length;
|
|
84
|
+
const head = [];
|
|
85
|
+
let used = 0;
|
|
86
|
+
for (let i = 0; i < total && head.length < PREVIEW_HEAD_LINES; i++) {
|
|
87
|
+
const l = clipLine(lines[i]);
|
|
88
|
+
if (head.length > 0 && used + l.length + 1 > PREVIEW_HEAD_BUDGET_CHARS)
|
|
89
|
+
break;
|
|
90
|
+
head.push(l);
|
|
91
|
+
used += l.length + 1;
|
|
92
|
+
}
|
|
93
|
+
const tail = [];
|
|
94
|
+
if (TAIL_KINDS.has(kind)) {
|
|
95
|
+
let tailUsed = 0;
|
|
96
|
+
for (let i = total - 1; i >= head.length && tail.length < PREVIEW_TAIL_LINES; i--) {
|
|
97
|
+
const l = clipLine(lines[i]);
|
|
98
|
+
if (tail.length > 0 && tailUsed + l.length + 1 > PREVIEW_TAIL_BUDGET_CHARS)
|
|
99
|
+
break;
|
|
100
|
+
tail.unshift(l);
|
|
101
|
+
tailUsed += l.length + 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const out = [...head];
|
|
105
|
+
const omittedFrom = head.length + 1; // 1-based first omitted line
|
|
106
|
+
const omittedTo = total - tail.length; // 1-based last omitted line
|
|
107
|
+
if (omittedTo >= omittedFrom) {
|
|
108
|
+
out.push(opts.numbered
|
|
109
|
+
? `[… ${omittedTo - omittedFrom + 1} lines omitted — tool_output_read offset=${omittedFrom - 1} …]`
|
|
110
|
+
: `[… lines ${omittedFrom}–${omittedTo} omitted …]`);
|
|
111
|
+
}
|
|
112
|
+
out.push(...tail);
|
|
113
|
+
out.push(pointerLine(total, text.length, filePath));
|
|
114
|
+
return out.join('\n');
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Size a tool result at ingestion. At or below the threshold → unchanged.
|
|
118
|
+
* Above it → the full text goes to the session spill file and a preview comes
|
|
119
|
+
* back. A write failure fails OPEN (full text returned, nothing lost).
|
|
120
|
+
*
|
|
121
|
+
* `header` (sap_get_source's version line) counts toward the threshold and
|
|
122
|
+
* leads the preview, but is NOT written to the file: the file is the body
|
|
123
|
+
* alone, so tool_output_read line k is body line k (fix round 1, I1), and the
|
|
124
|
+
* pointer's counts describe the body.
|
|
125
|
+
*/
|
|
126
|
+
export function spillIfLarge(i) {
|
|
127
|
+
const whole = i.header !== undefined ? `${i.header}\n${i.text}` : i.text;
|
|
128
|
+
if (whole.length <= (i.threshold ?? SPILL_THRESHOLD_CHARS))
|
|
129
|
+
return { spilled: false, text: whole };
|
|
130
|
+
const dir = spillDirFor(i.sessionId);
|
|
131
|
+
const id = safeSegment(i.toolUseId && i.toolUseId.length > 0 ? i.toolUseId : crypto.randomUUID());
|
|
132
|
+
const filePath = path.join(dir, `tool-${id}${i.numbered ? '.n' : ''}.txt`);
|
|
133
|
+
try {
|
|
134
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
135
|
+
fs.writeFileSync(filePath, i.text, { encoding: 'utf-8', mode: 0o600 });
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return { spilled: false, text: whole };
|
|
139
|
+
}
|
|
140
|
+
const preview = buildPreview(i.text, i.kind, filePath, { numbered: i.numbered });
|
|
141
|
+
return { spilled: true, text: i.header !== undefined ? `${i.header}\n${preview}` : preview, path: filePath };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Convenience for handlers: size `result.content` with the ctx's session and
|
|
145
|
+
* tool_use id. With a header, `result.content` is the body and the returned
|
|
146
|
+
* content is `header\n…`. No session id → never spilled.
|
|
147
|
+
*/
|
|
148
|
+
export function spillToolResult(result, ctx, kind, opts = {}) {
|
|
149
|
+
const sessionId = sessionIdOf(ctx);
|
|
150
|
+
const whole = opts.header !== undefined ? `${opts.header}\n${result.content}` : result.content;
|
|
151
|
+
if (sessionId === undefined)
|
|
152
|
+
return whole === result.content ? result : { ...result, content: whole };
|
|
153
|
+
const { label, ...spillOpts } = opts;
|
|
154
|
+
const s = spillIfLarge({ text: result.content, sessionId, toolUseId: ctx.toolUseId, kind, ...spillOpts });
|
|
155
|
+
if (s.spilled && s.path && label)
|
|
156
|
+
rememberSpillLabel(s.path, label);
|
|
157
|
+
return s.text === result.content ? result : { ...result, content: s.text };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Delete spill files (tool-*.txt in every session dir under the spill root)
|
|
161
|
+
* last modified more than SPILL_MAX_AGE_DAYS ago, then any session dir left
|
|
162
|
+
* empty. Other files (the turn mirrors) are left alone; symlinks and
|
|
163
|
+
* junctions (root, dirs, files) are never followed. Best effort: never
|
|
164
|
+
* throws. Returns how many files were deleted.
|
|
165
|
+
*/
|
|
166
|
+
export function pruneSpillFiles(now = Date.now()) {
|
|
167
|
+
const cutoff = now - SPILL_MAX_AGE_DAYS * 24 * 60 * 60 * 1000;
|
|
168
|
+
let removed = 0;
|
|
169
|
+
let dirs;
|
|
170
|
+
try {
|
|
171
|
+
// Final review tools-I1 — never follow links: a symlinked / junctioned
|
|
172
|
+
// root, session dir or file is skipped (lstat), so the prune can never
|
|
173
|
+
// delete outside the CLI's own area.
|
|
174
|
+
if (!fs.lstatSync(streamsRoot()).isDirectory())
|
|
175
|
+
return 0;
|
|
176
|
+
dirs = fs.readdirSync(streamsRoot());
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
for (const d of dirs) {
|
|
182
|
+
const dir = path.join(streamsRoot(), d);
|
|
183
|
+
let names;
|
|
184
|
+
try {
|
|
185
|
+
const st = fs.lstatSync(dir);
|
|
186
|
+
if (st.isSymbolicLink() || !st.isDirectory())
|
|
187
|
+
continue;
|
|
188
|
+
names = fs.readdirSync(dir);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
for (const name of names) {
|
|
194
|
+
if (!SPILL_FILE_RE.test(name))
|
|
195
|
+
continue;
|
|
196
|
+
const p = path.join(dir, name);
|
|
197
|
+
try {
|
|
198
|
+
const st = fs.lstatSync(p);
|
|
199
|
+
if (st.isSymbolicLink() || !st.isFile())
|
|
200
|
+
continue;
|
|
201
|
+
if (st.mtimeMs < cutoff) {
|
|
202
|
+
fs.unlinkSync(p);
|
|
203
|
+
removed++;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
/* best effort */
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
if (fs.readdirSync(dir).length === 0)
|
|
212
|
+
fs.rmdirSync(dir);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
/* best effort */
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return removed;
|
|
219
|
+
}
|
|
220
|
+
/** True when `child` lies strictly inside `dir`. */
|
|
221
|
+
export function isInside(dir, child) {
|
|
222
|
+
const rel = path.relative(dir, child);
|
|
223
|
+
return rel.length > 0 && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
224
|
+
}
|
|
225
|
+
/** `p` and, when it exists, its realpath (8.3 names, /var → /private/var). */
|
|
226
|
+
export function withRealpath(p) {
|
|
227
|
+
try {
|
|
228
|
+
const real = fs.realpathSync.native(p);
|
|
229
|
+
return real === p ? [p] : [p, real];
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return [p];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** True when `absPath` (already a realpath) lies under the spill root (any session). */
|
|
236
|
+
export function isUnderSpillRoot(absPath) {
|
|
237
|
+
return withRealpath(streamsRoot()).some((root) => isInside(root, absPath));
|
|
238
|
+
}
|
package/dist/tools/sap-read.js
CHANGED
|
@@ -22,6 +22,50 @@
|
|
|
22
22
|
* badiList(query, enhancementSpot?, maxResults?)
|
|
23
23
|
*/
|
|
24
24
|
import { registerTool } from './index.js';
|
|
25
|
+
import { normaliseSyntaxResult, checkSyntax } from './verify.js';
|
|
26
|
+
import { recordSyntaxCheck } from './syntax-state.js';
|
|
27
|
+
import { spillToolResult } from './result-spill.js';
|
|
28
|
+
// -----------------------------------------------------------------------
|
|
29
|
+
// Task 17 (spec D12, ruling F18) — model-facing formats.
|
|
30
|
+
// -----------------------------------------------------------------------
|
|
31
|
+
/**
|
|
32
|
+
* sap_get_source's one-line header: `[version: active]`,
|
|
33
|
+
* `[version: inactive | <warning>]` (any version may carry the split-state
|
|
34
|
+
* warning), or `[version: unknown]`. Plan-audit evidence reads the first
|
|
35
|
+
* 200 chars of the result, so the version leads it.
|
|
36
|
+
*/
|
|
37
|
+
export function formatSourceHeader(version, warning) {
|
|
38
|
+
return warning ? `[version: ${version} | ${warning.replace(/\s*\n\s*/g, ' ')}]` : `[version: ${version}]`;
|
|
39
|
+
}
|
|
40
|
+
/** Inverse of formatSourceHeader: the version (and warning) back from a result. */
|
|
41
|
+
export function parseSourceHeader(text) {
|
|
42
|
+
const first = text.split('\n', 1)[0] ?? '';
|
|
43
|
+
const m = /^\[version: ([a-z]+)(?: \| (.*))?\]$/.exec(first);
|
|
44
|
+
if (!m)
|
|
45
|
+
return { version: 'unknown' };
|
|
46
|
+
return m[2] !== undefined ? { version: m[1], warning: m[2] } : { version: m[1] };
|
|
47
|
+
}
|
|
48
|
+
/** One TSV cell: tabs / newlines inside a value cannot break the grid. */
|
|
49
|
+
const tsvCell = (v) => (v === null || v === undefined ? '' : String(v)).replace(/[\t\r\n]+/g, ' ');
|
|
50
|
+
export function formatSqlTsv(result) {
|
|
51
|
+
const cols = result.columns ?? [];
|
|
52
|
+
const rows = result.rows ?? [];
|
|
53
|
+
const lines = [cols.map(tsvCell).join('\t'), ...rows.map((r) => cols.map((c) => tsvCell(r[c])).join('\t'))];
|
|
54
|
+
const n = rows.length;
|
|
55
|
+
const total = typeof result.totalRows === 'number' && result.totalRows > n ? ` of ${result.totalRows}` : '';
|
|
56
|
+
lines.push(`(${n} ${n === 1 ? 'row' : 'rows'}${total})`);
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
/** Search hits, one per line: NAME TYPE PACKAGE DESCRIPTION URI ("-" for an empty field). */
|
|
60
|
+
export function formatSearchHits(hits) {
|
|
61
|
+
if (hits.length === 0)
|
|
62
|
+
return '(no hits)';
|
|
63
|
+
const f = (v) => {
|
|
64
|
+
const t = typeof v === 'string' ? v.replace(/\s+/g, ' ').trim() : '';
|
|
65
|
+
return t.length > 0 ? t : '-';
|
|
66
|
+
};
|
|
67
|
+
return hits.map((h) => [h.name, h.type, h.packageName ?? h.package, h.description, h.uri].map(f).join(' ')).join('\n');
|
|
68
|
+
}
|
|
25
69
|
// -----------------------------------------------------------------------
|
|
26
70
|
// 1. sap_get_source
|
|
27
71
|
// -----------------------------------------------------------------------
|
|
@@ -34,7 +78,8 @@ registerTool({
|
|
|
34
78
|
+ 'the response is the active source AND a warning is emitted so the '
|
|
35
79
|
+ 'caller knows the inactive edits are not visible. For a freshly '
|
|
36
80
|
+ 'created object with NO active version yet, the inactive (working) '
|
|
37
|
-
+ 'source is returned with version="inactive" and an honest note.'
|
|
81
|
+
+ 'source is returned with version="inactive" and an honest note. '
|
|
82
|
+
+ 'The first line is a [version: …] header, not part of the source.',
|
|
38
83
|
isMutating: false,
|
|
39
84
|
requiresSap: true,
|
|
40
85
|
input_schema: {
|
|
@@ -115,13 +160,13 @@ registerTool({
|
|
|
115
160
|
// probe failed — leave resolvedVersion as 'unknown'
|
|
116
161
|
}
|
|
117
162
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
};
|
|
163
|
+
// Fix round 1 (I1): the header leads the result and the preview, but a
|
|
164
|
+
// spill file holds the source alone — tool_output_read line k is source
|
|
165
|
+
// line k, matching syntax-check / ATC line numbers.
|
|
166
|
+
return spillToolResult({ content: String(source) }, ctx, 'source', {
|
|
167
|
+
header: formatSourceHeader(resolvedVersion, splitStateWarning),
|
|
168
|
+
label: String(args.name).toUpperCase(),
|
|
169
|
+
});
|
|
125
170
|
},
|
|
126
171
|
});
|
|
127
172
|
// -----------------------------------------------------------------------
|
|
@@ -153,10 +198,11 @@ registerTool({
|
|
|
153
198
|
// `version` right after name/type puts it inside the excerpt window.
|
|
154
199
|
const versionMatch = /\badtcore:version="([^"]*)"/.exec(result.rawXml ?? '');
|
|
155
200
|
const version = versionMatch?.[1] || 'unknown';
|
|
156
|
-
// name/type/version lead the serialized JSON
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
const { name, type, ...rest } = result;
|
|
201
|
+
// name/type/version lead the serialized JSON, so `version` sits inside
|
|
202
|
+
// the excerpt window. Task 17: rawXml is parsed above and never sent to
|
|
203
|
+
// the model (it was the bulk of the result).
|
|
204
|
+
const { name, type, rawXml, ...rest } = result;
|
|
205
|
+
void rawXml;
|
|
160
206
|
return { content: JSON.stringify({ name, type, version, ...rest }, null, 2) };
|
|
161
207
|
},
|
|
162
208
|
});
|
|
@@ -185,7 +231,7 @@ registerTool({
|
|
|
185
231
|
},
|
|
186
232
|
handler: async (args, ctx) => {
|
|
187
233
|
const results = await ctx.adt.searchObject(args.query, args.type, args.maxResults ?? 100);
|
|
188
|
-
return { content:
|
|
234
|
+
return spillToolResult({ content: formatSearchHits(results) }, ctx, 'search', { label: `search "${args.query}"` });
|
|
189
235
|
},
|
|
190
236
|
});
|
|
191
237
|
// -----------------------------------------------------------------------
|
|
@@ -229,7 +275,7 @@ registerTool({
|
|
|
229
275
|
},
|
|
230
276
|
handler: async (args, ctx) => {
|
|
231
277
|
const result = await ctx.adt.sqlQuery(args.query, args.maxRows ?? 100);
|
|
232
|
-
return { content:
|
|
278
|
+
return spillToolResult({ content: formatSqlTsv(result) }, ctx, 'sql', { label: 'query result' });
|
|
233
279
|
},
|
|
234
280
|
});
|
|
235
281
|
// -----------------------------------------------------------------------
|
|
@@ -455,7 +501,20 @@ registerTool({
|
|
|
455
501
|
},
|
|
456
502
|
handler: async (args, ctx) => {
|
|
457
503
|
// syntaxCheck(type, name) — type first
|
|
458
|
-
|
|
504
|
+
// Fails closed (fix round 7, item 1): the model must never read a
|
|
505
|
+
// syntax error as clean — see normaliseSyntaxResult.
|
|
506
|
+
let raw;
|
|
507
|
+
try {
|
|
508
|
+
raw = await checkSyntax(ctx.adt, args.type, args.name); // A-I2: notProcessed retry
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
// Re-review note — a check that did not run is "not clean": never leave
|
|
512
|
+
// an older clean result in the syntax state.
|
|
513
|
+
recordSyntaxCheck(args.type, args.name, true);
|
|
514
|
+
throw err;
|
|
515
|
+
}
|
|
516
|
+
const result = normaliseSyntaxResult(raw);
|
|
517
|
+
recordSyntaxCheck(args.type, args.name, result.hasErrors); // round 7 item 8e
|
|
459
518
|
return { content: JSON.stringify(result, null, 2) };
|
|
460
519
|
},
|
|
461
520
|
});
|
package/dist/tools/sap-write.js
CHANGED
|
@@ -22,12 +22,17 @@
|
|
|
22
22
|
* auditLog aliased from safety/audit-log.ts#log via sap-client/src/index.ts.
|
|
23
23
|
*/
|
|
24
24
|
import crypto from 'node:crypto';
|
|
25
|
+
import { readFileSync } from 'node:fs';
|
|
25
26
|
import { registerTool } from './index.js';
|
|
26
27
|
import { verifyAndSpendApprovalId } from '../approvals/jwt.js';
|
|
27
28
|
import { autoSnapshot, autoSnapshotClassInclude } from './snapshot.js';
|
|
28
29
|
import { includeSnapshotType } from './include-snapshot.js';
|
|
29
30
|
import { buildWriteChainLine } from '../renderer/verify-chain.js';
|
|
31
|
+
import { buildChangeCard } from '../renderer/change-card.js';
|
|
32
|
+
import { getLook } from '../renderer/look.js';
|
|
33
|
+
import { getEffectiveBodyWidth } from '../renderer/tty.js';
|
|
30
34
|
import { verifySyntax, verifyActive } from './verify.js';
|
|
35
|
+
import { hasRecordedSyntaxErrorsByName, clearSyntaxErrorsAfterActivation } from './syntax-state.js';
|
|
31
36
|
import { appendPending, finalizeToolCall } from '../session/pending.js';
|
|
32
37
|
import { auditLog } from '@cspeach/sap-client';
|
|
33
38
|
import { ERR } from '../errors/codes.js';
|
|
@@ -79,9 +84,70 @@ function snapshotChainState(snap) {
|
|
|
79
84
|
}
|
|
80
85
|
return { snapshot: 'best_effort_failed' };
|
|
81
86
|
}
|
|
82
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Review r7 I-3 — sap-client 0.1.15's setSource/updateMethod run their own
|
|
89
|
+
* (correct) syntax check and, on errors, return a 2xx writeStatus with
|
|
90
|
+
* `syntaxResult.hasErrors` and `error: 'Syntax check found errors —
|
|
91
|
+
* activation skipped'`. The source IS written (an inactive version exists):
|
|
92
|
+
* that is written-with-errors — the normal Rule 10 gate below reports it —
|
|
93
|
+
* never write_failed.
|
|
94
|
+
*/
|
|
95
|
+
function writtenWithSyntaxErrors(r) {
|
|
96
|
+
return r.writeStatus !== undefined && r.writeStatus >= 200 && r.writeStatus < 300 && r.syntaxResult?.hasErrors === true;
|
|
97
|
+
}
|
|
98
|
+
async function verifyWriteActivation(ctx, r, name, type) {
|
|
99
|
+
const a = r.activationResult;
|
|
100
|
+
if (!a)
|
|
101
|
+
return undefined;
|
|
102
|
+
const errors = (a.messages ?? []).filter((m) => m.severity === 'error').map((m) => m.shortText ?? '').filter(Boolean).slice(0, 5);
|
|
103
|
+
if (!a.success)
|
|
104
|
+
return { success: false, verified: false, errors };
|
|
105
|
+
const v = await verifyActive(ctx.adt, name, type);
|
|
106
|
+
if (v.verifyError !== undefined)
|
|
107
|
+
return { success: true, verified: false, errors, verifyError: v.verifyError };
|
|
108
|
+
return v.active ? { success: true, verified: true, errors } : { success: true, verified: false, errors, stillInactive: true };
|
|
109
|
+
}
|
|
110
|
+
/** Round 8 item 5 + A-I1 — the change-frame state from the verified activation. */
|
|
111
|
+
function chainActivation(w) {
|
|
112
|
+
if (!w)
|
|
113
|
+
return undefined;
|
|
114
|
+
if (!w.success)
|
|
115
|
+
return { state: 'failed', detail: w.errors[0] };
|
|
116
|
+
if (w.verified)
|
|
117
|
+
return { state: 'active' };
|
|
118
|
+
return w.stillInactive ? { state: 'still_inactive' } : { state: 'unverified' };
|
|
119
|
+
}
|
|
120
|
+
function activatedByWrite(w) {
|
|
121
|
+
if (!w)
|
|
122
|
+
return {};
|
|
123
|
+
return { activated_by_write: { success: w.success, verified: w.verified, errors: w.errors } };
|
|
124
|
+
}
|
|
125
|
+
/** A-I1 minor — after a verified activation on write, say so instead of "activation_allowed". */
|
|
126
|
+
function nextStepAfterWrite(syntaxOk, w) {
|
|
127
|
+
if (!syntaxOk)
|
|
128
|
+
return 'activation_blocked_until_syntax_errors_fixed';
|
|
129
|
+
if (w?.success && w.verified)
|
|
130
|
+
return 'already activated by the write — verify with sap_inactive_objects before reporting it active';
|
|
131
|
+
return 'activation_allowed';
|
|
132
|
+
}
|
|
133
|
+
/** Compact ①②③④ pipeline summary — derived from the real gate results.
|
|
134
|
+
* Panels look: a change card (same honest chain in its footer). */
|
|
83
135
|
function emitWriteChain(ctx, input) {
|
|
84
|
-
|
|
136
|
+
const text = getLook() === 'panels' && input.card
|
|
137
|
+
? buildChangeCard(input, getEffectiveBodyWidth())
|
|
138
|
+
: buildWriteChainLine(input);
|
|
139
|
+
ctx.chunkEmitter?.emit('chunk', text + '\n');
|
|
140
|
+
}
|
|
141
|
+
/** Old source for the change card: ONLY from a snapshot actually taken. */
|
|
142
|
+
function snapshotSource(snap) {
|
|
143
|
+
if (!snap?.taken || !snap.entry?.filePath)
|
|
144
|
+
return undefined;
|
|
145
|
+
try {
|
|
146
|
+
return readFileSync(snap.entry.filePath, 'utf8');
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
85
151
|
}
|
|
86
152
|
registerTool({
|
|
87
153
|
name: 'sap_set_source',
|
|
@@ -165,6 +231,7 @@ registerTool({
|
|
|
165
231
|
what_will_happen: 'Overwrites full object source; snapshot is taken first.',
|
|
166
232
|
rollback_available: true,
|
|
167
233
|
transport: resolved.transport ?? ctx.currentTransport?.get() ?? '',
|
|
234
|
+
args: { ...args },
|
|
168
235
|
});
|
|
169
236
|
if (!r.confirmed) {
|
|
170
237
|
return {
|
|
@@ -201,7 +268,7 @@ registerTool({
|
|
|
201
268
|
is_error: true,
|
|
202
269
|
};
|
|
203
270
|
}
|
|
204
|
-
if (writeResult.error || (writeResult.writeStatus !== undefined && writeResult.writeStatus >= 400)) {
|
|
271
|
+
if ((writeResult.error && !writtenWithSyntaxErrors(writeResult)) || (writeResult.writeStatus !== undefined && writeResult.writeStatus >= 400)) {
|
|
205
272
|
const detail = writeResult.error ?? `Write returned HTTP ${writeResult.writeStatus} but did not throw`;
|
|
206
273
|
await finalizeToolCall(ctx.session, toolUseId, detail, true, Date.now() - startedAt);
|
|
207
274
|
return {
|
|
@@ -218,11 +285,22 @@ registerTool({
|
|
|
218
285
|
// ── Gate 4: Verify — syntax check (Rule 10) ──────────────────────────────
|
|
219
286
|
const syntax = await verifySyntax(ctx.adt, args.name, args.type);
|
|
220
287
|
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ written: true, syntax }), false, Date.now() - startedAt);
|
|
288
|
+
const writeActivation = await verifyWriteActivation(ctx, writeResult, args.name, args.type);
|
|
221
289
|
// Tour slide-7 chain — derived from the gates that actually ran.
|
|
290
|
+
// Old source is read (once) only for the panels card; legacy reads nothing extra.
|
|
291
|
+
const oldSource = getLook() === 'panels' ? snapshotSource(snapOutcome) : undefined;
|
|
222
292
|
emitWriteChain(ctx, {
|
|
223
293
|
...snapshotChainState(snapOutcome),
|
|
224
294
|
written: true,
|
|
225
295
|
syntax: { ok: syntax.ok, errors: syntax.errors },
|
|
296
|
+
activation: chainActivation(writeActivation),
|
|
297
|
+
card: {
|
|
298
|
+
object: args.name,
|
|
299
|
+
what: 'source',
|
|
300
|
+
transport: resolved.transport,
|
|
301
|
+
oldSource,
|
|
302
|
+
newSource: oldSource !== undefined ? args.source : undefined,
|
|
303
|
+
},
|
|
226
304
|
});
|
|
227
305
|
return {
|
|
228
306
|
content: JSON.stringify({
|
|
@@ -230,10 +308,9 @@ registerTool({
|
|
|
230
308
|
syntax_ok: syntax.ok,
|
|
231
309
|
errors: syntax.errors,
|
|
232
310
|
warnings: syntax.warnings,
|
|
311
|
+
...activatedByWrite(writeActivation),
|
|
233
312
|
...(resolved.owning ? { transport_used: resolved.owning, transport_note: resolved.note } : {}),
|
|
234
|
-
next_step: syntax.ok
|
|
235
|
-
? 'activation_allowed'
|
|
236
|
-
: 'activation_blocked_until_syntax_errors_fixed',
|
|
313
|
+
next_step: nextStepAfterWrite(syntax.ok, writeActivation),
|
|
237
314
|
}),
|
|
238
315
|
};
|
|
239
316
|
},
|
|
@@ -395,7 +472,7 @@ registerTool({
|
|
|
395
472
|
is_error: true,
|
|
396
473
|
};
|
|
397
474
|
}
|
|
398
|
-
if (updateResult.error || (updateResult.writeStatus !== undefined && updateResult.writeStatus >= 400)) {
|
|
475
|
+
if ((updateResult.error && !writtenWithSyntaxErrors(updateResult)) || (updateResult.writeStatus !== undefined && updateResult.writeStatus >= 400)) {
|
|
399
476
|
const detail = updateResult.error ?? `Update returned HTTP ${updateResult.writeStatus} but did not throw`;
|
|
400
477
|
await finalizeToolCall(ctx.session, toolUseId, detail, true, Date.now() - startedAt);
|
|
401
478
|
return {
|
|
@@ -412,11 +489,14 @@ registerTool({
|
|
|
412
489
|
// ── Gate 4: Verify — syntax check (Rule 10) ──────────────────────────────
|
|
413
490
|
const syntax = await verifySyntax(ctx.adt, args.class_name, 'CLAS');
|
|
414
491
|
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ updated: true, syntax }), false, Date.now() - startedAt);
|
|
492
|
+
const writeActivation = await verifyWriteActivation(ctx, updateResult, args.class_name, 'CLAS');
|
|
415
493
|
// Tour slide-7 chain — derived from the gates that actually ran.
|
|
416
494
|
emitWriteChain(ctx, {
|
|
417
495
|
...snapshotChainState(snapOutcome),
|
|
418
496
|
written: true,
|
|
419
497
|
syntax: { ok: syntax.ok, errors: syntax.errors },
|
|
498
|
+
activation: chainActivation(writeActivation),
|
|
499
|
+
card: { object: args.class_name, what: `method ${args.method_name}`, transport: resolved.transport },
|
|
420
500
|
});
|
|
421
501
|
return {
|
|
422
502
|
content: JSON.stringify({
|
|
@@ -424,10 +504,9 @@ registerTool({
|
|
|
424
504
|
syntax_ok: syntax.ok,
|
|
425
505
|
errors: syntax.errors,
|
|
426
506
|
warnings: syntax.warnings,
|
|
507
|
+
...activatedByWrite(writeActivation),
|
|
427
508
|
...(resolved.owning ? { transport_used: resolved.owning, transport_note: resolved.note } : {}),
|
|
428
|
-
next_step: syntax.ok
|
|
429
|
-
? 'activation_allowed'
|
|
430
|
-
: 'activation_blocked_until_syntax_errors_fixed',
|
|
509
|
+
next_step: nextStepAfterWrite(syntax.ok, writeActivation),
|
|
431
510
|
}),
|
|
432
511
|
};
|
|
433
512
|
},
|
|
@@ -496,6 +575,7 @@ registerTool({
|
|
|
496
575
|
what_will_happen: 'Deletes object from SAP; snapshot is taken first; recovery via sap_snapshot_read + sap_set_source (LLM composes the rollback).',
|
|
497
576
|
rollback_available: true,
|
|
498
577
|
transport: resolved.transport ?? ctx.currentTransport?.get() ?? '',
|
|
578
|
+
args: { ...args },
|
|
499
579
|
});
|
|
500
580
|
if (!r.confirmed) {
|
|
501
581
|
return {
|
|
@@ -785,6 +865,7 @@ registerTool({
|
|
|
785
865
|
...snapshotChainState(snapOutcome),
|
|
786
866
|
written: true,
|
|
787
867
|
syntax: 'not_checked',
|
|
868
|
+
card: { object: args.className, what: `include ${args.includeType}`, transport: resolved.transport },
|
|
788
869
|
});
|
|
789
870
|
return {
|
|
790
871
|
content: JSON.stringify({
|
|
@@ -1107,6 +1188,33 @@ registerTool({
|
|
|
1107
1188
|
is_error: true,
|
|
1108
1189
|
};
|
|
1109
1190
|
}
|
|
1191
|
+
// Round 7 item 8e — an approval auto_approve minted (no human saw it)
|
|
1192
|
+
// never activates an object whose last syntax check found errors. Any
|
|
1193
|
+
// approval minted after the errors went through the card (risk floor).
|
|
1194
|
+
// Final review A M-1 — match by NAME, like the risk floor: a type word can
|
|
1195
|
+
// map to a different code (DDLS recorded, activated as VIEW). Over-block,
|
|
1196
|
+
// never under-block; an auto approval can always be asked for again.
|
|
1197
|
+
const broken = args.objects
|
|
1198
|
+
.filter((o) => hasRecordedSyntaxErrorsByName(o.name)).map((o) => o.name.toUpperCase());
|
|
1199
|
+
if (verify.payload.auto) {
|
|
1200
|
+
if (broken.length > 0) {
|
|
1201
|
+
ctx.chunkEmitter?.emit('warn', `Activation blocked · syntax errors in ${broken.join(', ')}`);
|
|
1202
|
+
return {
|
|
1203
|
+
content: JSON.stringify({
|
|
1204
|
+
error: 'activation_blocked_syntax_errors',
|
|
1205
|
+
objects: broken,
|
|
1206
|
+
detail: 'The last syntax check of these objects found errors and this activation was auto-approved before that. '
|
|
1207
|
+
+ 'Fix the errors (sap_syntax_check shows them) or ask for approval again.',
|
|
1208
|
+
}),
|
|
1209
|
+
is_error: true,
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
// Review r7 M-6 — a human-approved activation stays allowed (a mass
|
|
1214
|
+
// activation can clear cross-object errors), but it is never silent.
|
|
1215
|
+
for (const n of broken) {
|
|
1216
|
+
ctx.chunkEmitter?.emit('warn', `Activating ${n} · its last syntax check found errors · SAP will refuse it unless they are resolved`);
|
|
1217
|
+
}
|
|
1110
1218
|
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
1111
1219
|
const startedAt = Date.now();
|
|
1112
1220
|
// ── WAL: record pending entry ────────────────────────────────────────────
|
|
@@ -1149,6 +1257,10 @@ registerTool({
|
|
|
1149
1257
|
else if (!v.active) {
|
|
1150
1258
|
stillInactive.push(`${obj.type}/${obj.name}`);
|
|
1151
1259
|
}
|
|
1260
|
+
else if (activationResults.find((r) => r.name === obj.name && r.type === obj.type)?.success) {
|
|
1261
|
+
// Final review A M-2 — activated AND verified: the source compiles.
|
|
1262
|
+
clearSyntaxErrorsAfterActivation(obj.name);
|
|
1263
|
+
}
|
|
1152
1264
|
}
|
|
1153
1265
|
const overallSuccess = !anyError && stillInactive.length === 0 && unverifiable.length === 0;
|
|
1154
1266
|
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ overallSuccess, activationResults, stillInactive, unverifiable }), anyError, Date.now() - startedAt);
|