@cspeach/cli 1.1.18 → 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 +58 -21
- 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/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
|
@@ -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',
|
package/dist/tools/sap-read.js
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
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';
|
|
25
27
|
// -----------------------------------------------------------------------
|
|
26
28
|
// 1. sap_get_source
|
|
27
29
|
// -----------------------------------------------------------------------
|
|
@@ -455,7 +457,20 @@ registerTool({
|
|
|
455
457
|
},
|
|
456
458
|
handler: async (args, ctx) => {
|
|
457
459
|
// syntaxCheck(type, name) — type first
|
|
458
|
-
|
|
460
|
+
// Fails closed (fix round 7, item 1): the model must never read a
|
|
461
|
+
// syntax error as clean — see normaliseSyntaxResult.
|
|
462
|
+
let raw;
|
|
463
|
+
try {
|
|
464
|
+
raw = await checkSyntax(ctx.adt, args.type, args.name); // A-I2: notProcessed retry
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
// Re-review note — a check that did not run is "not clean": never leave
|
|
468
|
+
// an older clean result in the syntax state.
|
|
469
|
+
recordSyntaxCheck(args.type, args.name, true);
|
|
470
|
+
throw err;
|
|
471
|
+
}
|
|
472
|
+
const result = normaliseSyntaxResult(raw);
|
|
473
|
+
recordSyntaxCheck(args.type, args.name, result.hasErrors); // round 7 item 8e
|
|
459
474
|
return { content: JSON.stringify(result, null, 2) };
|
|
460
475
|
},
|
|
461
476
|
});
|
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);
|
|
@@ -45,7 +45,10 @@ import * as path from 'node:path';
|
|
|
45
45
|
import { registerTool } from '../index.js';
|
|
46
46
|
import { resolveSafePath, PathOutsideRootError } from '../_filesystem-shared.js';
|
|
47
47
|
import { loadSafelist, buildSafeEnv, resolveExecutable, validateArgv, rejectPathSeparator, DEFAULT_SAFELIST, KILL_GRACE_MS, } from '../_command-shared.js';
|
|
48
|
+
import { killProcessTree } from '../fiori/preview/registry.js';
|
|
48
49
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
50
|
+
/** After the kill's grace: how long to wait for the pipes before returning anyway. */
|
|
51
|
+
const ORPHAN_WAIT_MS = 3_000;
|
|
49
52
|
const MAX_TIMEOUT_MS = 300_000;
|
|
50
53
|
const MAX_OUTPUT_BYTES = 1_000_000; // per stream
|
|
51
54
|
export async function shellExecHandler(args, ctx) {
|
|
@@ -171,18 +174,38 @@ export async function shellExecHandler(args, ctx) {
|
|
|
171
174
|
stderrTrunc = true;
|
|
172
175
|
}
|
|
173
176
|
});
|
|
177
|
+
const timedOutResult = () => {
|
|
178
|
+
const tail = (stdoutBuf ? `stdout (partial):\n${stdoutBuf}\n${stdoutTrunc ? '[truncated]\n' : ''}` : '') +
|
|
179
|
+
(stderrBuf ? `stderr (partial):\n${stderrBuf}\n${stderrTrunc ? '[truncated]\n' : ''}` : '');
|
|
180
|
+
return { content: `error: command timed out after ${timeoutMs} ms — child killed.\n${tail}`, is_error: true };
|
|
181
|
+
};
|
|
174
182
|
const timer = setTimeout(() => {
|
|
175
183
|
timedOut = true;
|
|
176
|
-
|
|
177
|
-
|
|
184
|
+
if (process.platform === 'win32') {
|
|
185
|
+
// Dry run 2026-09-25: `npm start` = npm.cmd → cmd.exe → node. Killing the
|
|
186
|
+
// direct child left the server alive holding our pipes; 'close' never
|
|
187
|
+
// fired and a 45 s call returned after 21 minutes. Kill the whole tree.
|
|
188
|
+
void killProcessTree(child.pid, child);
|
|
178
189
|
}
|
|
179
|
-
|
|
180
|
-
setTimeout(() => {
|
|
190
|
+
else {
|
|
181
191
|
try {
|
|
182
|
-
child.kill('
|
|
192
|
+
child.kill('SIGTERM');
|
|
183
193
|
}
|
|
184
194
|
catch { /* ignore */ }
|
|
185
|
-
|
|
195
|
+
setTimeout(() => {
|
|
196
|
+
try {
|
|
197
|
+
child.kill('SIGKILL');
|
|
198
|
+
}
|
|
199
|
+
catch { /* ignore */ }
|
|
200
|
+
}, KILL_GRACE_MS).unref();
|
|
201
|
+
}
|
|
202
|
+
// Never wait on an orphan: if the pipes are still open after the kill
|
|
203
|
+
// had its grace, stop reading and return the timeout result anyway.
|
|
204
|
+
setTimeout(() => {
|
|
205
|
+
child.stdout?.destroy();
|
|
206
|
+
child.stderr?.destroy();
|
|
207
|
+
settle(timedOutResult());
|
|
208
|
+
}, KILL_GRACE_MS + ORPHAN_WAIT_MS).unref();
|
|
186
209
|
}, timeoutMs);
|
|
187
210
|
timer.unref();
|
|
188
211
|
child.on('error', (err) => {
|
|
@@ -192,12 +215,7 @@ export async function shellExecHandler(args, ctx) {
|
|
|
192
215
|
child.on('close', (code, signal) => {
|
|
193
216
|
clearTimeout(timer);
|
|
194
217
|
if (timedOut) {
|
|
195
|
-
|
|
196
|
-
(stderrBuf ? `stderr (partial):\n${stderrBuf}\n${stderrTrunc ? '[truncated]\n' : ''}` : '');
|
|
197
|
-
settle({
|
|
198
|
-
content: `error: command timed out after ${timeoutMs} ms — child killed.\n${tail}`,
|
|
199
|
-
is_error: true,
|
|
200
|
-
});
|
|
218
|
+
settle(timedOutResult());
|
|
201
219
|
return;
|
|
202
220
|
}
|
|
203
221
|
const stdoutSection = `stdout:\n${stdoutBuf}${stdoutTrunc ? '\n[truncated — exceeded 1 MB cap]' : ''}`;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round 7 item 8e — which objects' LAST syntax check found errors (this CLI
|
|
3
|
+
* process). Written by every syntax check the CLI runs (the Rule 10 write
|
|
4
|
+
* gate and the sap_syntax_check tool); read by the approval risk floor (such
|
|
5
|
+
* an object never auto-approves) and by sap_activate (an automatically minted
|
|
6
|
+
* approval never activates it). A clean check clears the object.
|
|
7
|
+
*
|
|
8
|
+
* Review r7 M-3 — keyed by object TYPE and name: a PROG and a CLAS with the
|
|
9
|
+
* same name never share the flag. `PROG/P` and `PROG` are the same type.
|
|
10
|
+
*
|
|
11
|
+
* Re-review 2 item 2 — a name index of flagged objects. When either side's
|
|
12
|
+
* type is not a known ADT code (a recorded "ABAP CLASS", a queried "some
|
|
13
|
+
* thing"), the object is matched by name alone: over-match, never miss.
|
|
14
|
+
*/
|
|
15
|
+
import { normaliseAdtType } from '../approvals/adt-type.js';
|
|
16
|
+
const UNKNOWN = '?';
|
|
17
|
+
/** name → the type codes (or UNKNOWN) flagged under that name. */
|
|
18
|
+
const byName = new Map();
|
|
19
|
+
const nameOf = (name) => name.trim().toUpperCase();
|
|
20
|
+
const codeOf = (type) => normaliseAdtType(type) ?? UNKNOWN;
|
|
21
|
+
export function recordSyntaxCheck(type, name, hasErrors) {
|
|
22
|
+
const n = nameOf(name);
|
|
23
|
+
const c = codeOf(type);
|
|
24
|
+
const set = byName.get(n) ?? new Set();
|
|
25
|
+
if (hasErrors)
|
|
26
|
+
set.add(c);
|
|
27
|
+
else {
|
|
28
|
+
set.delete(c);
|
|
29
|
+
// Final review A M-3 — a clean check under a KNOWN code also clears an
|
|
30
|
+
// entry recorded under an unknown type word for the same name.
|
|
31
|
+
if (c !== UNKNOWN)
|
|
32
|
+
set.delete(UNKNOWN);
|
|
33
|
+
}
|
|
34
|
+
if (set.size > 0)
|
|
35
|
+
byName.set(n, set);
|
|
36
|
+
else
|
|
37
|
+
byName.delete(n);
|
|
38
|
+
}
|
|
39
|
+
export function hasRecordedSyntaxErrors(type, name) {
|
|
40
|
+
const set = byName.get(nameOf(name));
|
|
41
|
+
if (!set || set.size === 0)
|
|
42
|
+
return false;
|
|
43
|
+
const c = codeOf(type);
|
|
44
|
+
return c === UNKNOWN || set.has(c) || set.has(UNKNOWN);
|
|
45
|
+
}
|
|
46
|
+
/** Final review A M-1 — any flagged entry with this name, whatever its type. */
|
|
47
|
+
export function hasRecordedSyntaxErrorsByName(name) {
|
|
48
|
+
return (byName.get(nameOf(name))?.size ?? 0) > 0;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Final review A M-2 — a successful activation verified against the
|
|
52
|
+
* inactive-objects list proves the current source compiles: clear the name.
|
|
53
|
+
*/
|
|
54
|
+
export function clearSyntaxErrorsAfterActivation(name) {
|
|
55
|
+
byName.delete(nameOf(name));
|
|
56
|
+
}
|
|
57
|
+
/** `TYPE:NAME` keys (TYPE is an ADT code, or `?` when the type word was unknown). */
|
|
58
|
+
export function objectsWithSyntaxErrors() {
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const [n, set] of byName)
|
|
61
|
+
for (const c of set)
|
|
62
|
+
out.push(`${c}:${n}`);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
export function resetSyntaxStateForTest() {
|
|
66
|
+
byName.clear();
|
|
67
|
+
}
|
package/dist/tools/todo.js
CHANGED
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Semantics: FULL-LIST REPLACE on every call — never a delta. 1..20 items.
|
|
9
9
|
*
|
|
10
|
-
* Transcript render (CC-style
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* Transcript render (piece 2, spec §4 — was CC-style 2026-07): WHERE the
|
|
11
|
+
* checklist is drawn depends on the render mode (renderer/checklist-format.ts
|
|
12
|
+
* checklistRenderMode). Ink draws a LIVE checklist (ui/checklist.tsx) that
|
|
13
|
+
* redraws in place, with one close line at turn end; classic interactive
|
|
14
|
+
* prints the block once at turn end (repl/plan-turn-end.ts); only headless /
|
|
15
|
+
* one-shot emit the per-set scrollback block (renderer/todo-block.ts) via
|
|
16
|
+
* ctx.chunkEmitter, so CI logs keep the progression. The raw ⏺/⎿ todo_set
|
|
17
|
+
* rows are widget-suppressed (renderer/tool-widget.ts) in every mode.
|
|
18
18
|
*
|
|
19
19
|
* Session-write route (chosen with evidence, 2026-07-05): the tool mutates
|
|
20
20
|
* `ctx.session.todos` directly. ToolContext.session IS the live
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
*/
|
|
46
46
|
import { registerTool } from './index.js';
|
|
47
47
|
import { formatTodoChecklistBlock } from '../renderer/todo-block.js';
|
|
48
|
+
import { checklistRenderMode } from '../renderer/checklist-format.js';
|
|
49
|
+
import { markTodoSetThisTurn } from '../ui/todo-emitter.js';
|
|
48
50
|
/** Hard cap on list length — past this the list stops being a glanceable plan. */
|
|
49
51
|
export const TODO_MAX_ITEMS = 20;
|
|
50
52
|
const VALID_STATUSES = new Set(['pending', 'in_progress', 'completed']);
|
|
@@ -85,16 +87,19 @@ export async function todoSetHandler(args, ctx) {
|
|
|
85
87
|
// copy so a consumer mutating the payload can never corrupt the
|
|
86
88
|
// persisted session state.
|
|
87
89
|
ctx.todoEmitter?.emit('update', todos.map((t) => ({ ...t })));
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
// (
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
// Piece 2 (spec §4) — where the checklist is drawn depends on the mode.
|
|
91
|
+
// Ink: the live checklist (ui/checklist.tsx) redraws in place — no
|
|
92
|
+
// scrollback reprint per update; one close line lands at turn end
|
|
93
|
+
// (repl/plan-turn-end.ts). Classic interactive: the block prints once, at
|
|
94
|
+
// turn end. Headless / one-shot: the block still prints on EVERY update so
|
|
95
|
+
// CI logs keep the progression. The raw ⏺/⎿ todo_set rows stay
|
|
96
|
+
// widget-suppressed (renderer/tool-widget.ts). Subagent ctxs omit both
|
|
97
|
+
// emitters, so a child todo_set never reaches the parent's transcript.
|
|
98
|
+
if (checklistRenderMode() === 'per-update') {
|
|
99
|
+
ctx.chunkEmitter?.emit('chunk', formatTodoChecklistBlock(todos));
|
|
100
|
+
}
|
|
101
|
+
if (ctx.todoEmitter)
|
|
102
|
+
markTodoSetThisTurn();
|
|
98
103
|
const result = {
|
|
99
104
|
count: todos.length,
|
|
100
105
|
// LAST-wins, matching the app strip and Ctrl+T panel (both `.at(-1)`).
|