@cspeach/cli 1.1.51 → 1.1.53
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/dist/agent/chat-prompt.js +4 -1
- package/dist/agent/conversation-prompt.js +31 -13
- package/dist/agent/inline-skill-block.js +341 -0
- package/dist/agent/loop.js +304 -49
- package/dist/agent/maybe-build-project-context.js +11 -2
- package/dist/agent/provider-shape.js +8 -4
- package/dist/agent/providers/ai-hub-provider.js +2 -2
- package/dist/agent/providers/azure-provider.js +2 -2
- package/dist/agent/providers/byok-provider.js +2 -2
- package/dist/agent/providers/local-provider.js +2 -2
- package/dist/agent/repair-partial.js +21 -0
- package/dist/agent/skill-block-key.js +134 -0
- package/dist/agent/steering-queue.js +50 -14
- package/dist/agent/system-prompt.js +22 -0
- package/dist/agent/tool-dispatch.js +25 -9
- package/dist/agent/turn-watchdog.js +1 -1
- package/dist/approvals/advisory-render.js +6 -3
- package/dist/approvals/approval-prompt.js +9 -4
- package/dist/approvals/op-labels.js +20 -3
- package/dist/approvals/plan-grant.js +8 -0
- package/dist/approvals/render.js +22 -10
- package/dist/approvals/risk-floor.js +32 -4
- package/dist/approvals/visible-changes.js +50 -0
- package/dist/cli.js +10 -2
- package/dist/commands/compact.js +6 -1
- package/dist/commands/config-set.js +13 -0
- package/dist/commands/config-show.js +16 -4
- package/dist/commands/export-conversation.js +7 -4
- package/dist/commands/finish-check-guard.js +124 -0
- package/dist/commands/plan-chain.js +2 -2
- package/dist/commands/plan-reground.js +27 -4
- package/dist/commands/plan-resume.js +113 -3
- package/dist/commands/plan-startup.js +3 -3
- package/dist/config/loader.js +5 -1
- package/dist/cost/cost-log.js +6 -0
- package/dist/cost/system-block.js +6 -0
- package/dist/docx/doc-meta.js +1 -0
- package/dist/docx/markdown-to-docx.js +2 -0
- package/dist/lane/app-chain.js +13 -6
- package/dist/models/server-config.js +1 -1
- package/dist/one-shot.js +1 -0
- package/dist/project-context/conventions.js +8 -1
- package/dist/project-context/index-files.js +97 -34
- package/dist/project-context/index.js +1 -1
- package/dist/project-context/render.js +38 -4
- package/dist/projects/answer-blockers.js +107 -16
- package/dist/projects/blocker-answers-note.js +89 -0
- package/dist/projects/change-options.js +70 -0
- package/dist/projects/document-save.js +86 -0
- package/dist/projects/extract-design.js +4 -0
- package/dist/projects/extract-estimate.js +4 -0
- package/dist/projects/extract-spec-gap.js +8 -0
- package/dist/projects/index.js +1 -1
- package/dist/projects/plan-objects.js +24 -0
- package/dist/projects/plan-run.js +12 -2
- package/dist/projects/save-command.js +50 -18
- package/dist/projects/yes-no.js +5 -1
- package/dist/renderer/change-card.js +17 -1
- package/dist/renderer/html-entities.js +25 -0
- package/dist/renderer/machine-blocks.js +54 -7
- package/dist/renderer/steering-echo.js +53 -8
- package/dist/renderer/tool-labels.js +5 -1
- package/dist/renderer/tool-widget.js +20 -5
- package/dist/repl/chat-lane.js +15 -3
- package/dist/repl/inline-mode.js +51 -0
- package/dist/repl/leftover.js +90 -0
- package/dist/repl/reroute-turn.js +7 -1
- package/dist/repl/turn-stop.js +64 -0
- package/dist/repl.js +155 -24
- package/dist/router/classifier.js +1 -1
- package/dist/sap/reachability-state.js +7 -0
- package/dist/sap/unreachable.js +2 -2
- package/dist/session/fresh-start.js +4 -0
- package/dist/session/queued-step.js +3 -1
- package/dist/session/user-prompt.js +12 -1
- package/dist/skills/manifest-client.js +3 -2
- package/dist/skills/preamble.js +6 -4
- package/dist/skills/source-authed.js +3 -2
- package/dist/tools/approval.js +49 -8
- package/dist/tools/dispatch-skill.js +1 -1
- package/dist/tools/hand-off.js +49 -7
- package/dist/tools/lane/app-lane-resolve.js +9 -0
- package/dist/tools/method-only-change.js +241 -0
- package/dist/tools/project/spec_answers_record.js +190 -0
- package/dist/tools/rap/skeleton-resolve.js +4 -0
- package/dist/tools/sap-write.js +78 -3
- package/dist/tools/snapshot.js +7 -2
- package/dist/tools/subagent/agent_run.js +14 -0
- package/dist/tools/tool-loading.js +18 -8
- package/dist/tools/transport-fit-check.js +70 -0
- package/dist/tools/transport.js +17 -8
- package/dist/tools/write-pipeline.js +46 -5
- package/dist/ui/app.js +112 -13
- package/dist/ui/approval-modal.js +13 -7
- package/dist/ui/ascii-chars.js +21 -3
- package/dist/ui/footer.js +128 -7
- package/dist/ui/interrupt.js +35 -1
- package/dist/ui/live-area.js +21 -4
- package/dist/ui/queue-box-emitter.js +14 -0
- package/dist/ui/rewind-panel.js +2 -1
- package/dist/ui/session-timeline.js +2 -1
- package/dist/ui/text-input.js +31 -2
- package/dist/ui/widgets/ask-card.js +171 -29
- package/dist/ui/widgets/bar-chart.js +2 -1
- package/dist/ui/widgets/confirm-card.js +2 -1
- package/dist/ui/widgets/dep-graph.js +2 -1
- package/dist/ui/widgets/diff-viewer.js +2 -1
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/shortcuts-sheet.js +2 -2
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/util/connect-retry.js +65 -0
- package/package.json +2 -2
|
@@ -103,6 +103,14 @@ export function getPlanGrant() { return active; }
|
|
|
103
103
|
export function isPlanGrantInScope() {
|
|
104
104
|
return active !== null && approvalsMode === 'plan' && inScope(active);
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* 2026-10-23 — the grant of THIS plan is in force: installed, not expired, and
|
|
108
|
+
* approvals = plan. The same test as isPlanGrantInScope without "a step of the
|
|
109
|
+
* plan is running": the step prompt is built just before its step starts.
|
|
110
|
+
*/
|
|
111
|
+
export function isPlanGrantInForceFor(planId) {
|
|
112
|
+
return active !== null && active.planId === planId && approvalsMode === 'plan' && isLive(active);
|
|
113
|
+
}
|
|
106
114
|
/**
|
|
107
115
|
* The claims a grant-minted id carries for one covered change: the listed
|
|
108
116
|
* type, the listed row's package, and the bound transport.
|
package/dist/approvals/render.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { select, input } from '@inquirer/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import {
|
|
3
|
+
import { riskReasonTexts, shownRiskReasons } from './risk-floor.js';
|
|
4
4
|
import { verbLine, viewAffordance, toSourceLines, affectsLine, releaseLines, behaviourText, behaviourLines } from './op-labels.js';
|
|
5
5
|
import { uiWidth } from '../renderer/ui-width.js';
|
|
6
6
|
import { shouldUseInk } from '../renderer/tty.js';
|
|
@@ -12,8 +12,9 @@ import { emitTrace } from '../renderer/trace.js';
|
|
|
12
12
|
import { getGlyphs } from '../renderer/glyphs.js';
|
|
13
13
|
import { paint } from '../renderer/theme.js';
|
|
14
14
|
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
15
|
-
import {
|
|
15
|
+
import { planRows, looksLikeUnifiedDiff } from './visible-changes.js';
|
|
16
16
|
import { withSpinnerSuspended } from '../renderer/tool-widget.js';
|
|
17
|
+
import { decodeDisplayEntities, decodeDisplayBlock } from '../renderer/html-entities.js';
|
|
17
18
|
// Piece 2 §1.1 — the brand accent comes from the theme role, not a legacy hex.
|
|
18
19
|
const peach = (s) => paint('brand', s);
|
|
19
20
|
/**
|
|
@@ -75,11 +76,21 @@ function colorizeDiff(diff) {
|
|
|
75
76
|
* compressed. This is what fixes the "what 2 changes?" complaint when the
|
|
76
77
|
* model declares modify+activate of the same object.
|
|
77
78
|
*/
|
|
78
|
-
// collapseImplicitActivates
|
|
79
|
+
// collapseImplicitActivates and planRows (LT-12) live in ./visible-changes.ts (Task 9: request_approval uses the first for its gate count).
|
|
80
|
+
/**
|
|
81
|
+
* LT-12 fix round 1 — the apply choice. One shown row (a create + modify of
|
|
82
|
+
* one object, the gate still opens on the raw count) reads "Apply this
|
|
83
|
+
* change", never "Apply all 1 change".
|
|
84
|
+
*/
|
|
85
|
+
function applyAllLabel(visibleCount, noun) {
|
|
86
|
+
return visibleCount === 1 ? 'Apply this change' : `Apply all ${visibleCount} ${noun}`;
|
|
87
|
+
}
|
|
79
88
|
export async function renderPlanGate(summary, changes, transport, effectiveRisk, system) {
|
|
80
|
-
|
|
89
|
+
// LT-12: one row per new object (display only; the changes behind the card are untouched).
|
|
90
|
+
const visibleChanges = planRows(changes);
|
|
81
91
|
const visibleCount = visibleChanges.length;
|
|
82
92
|
const noun = visibleCount === 1 ? 'change' : 'changes';
|
|
93
|
+
const applyLabel = applyAllLabel(visibleCount, noun);
|
|
83
94
|
// Ink path (piece 2 §5): the plan gate is an APPROVAL confirm card in the
|
|
84
95
|
// card slot. Same three values, same order, cursor on Apply all; anything
|
|
85
96
|
// else (Esc, no listener) is 'cancel'. Digits pick on an APPROVAL card
|
|
@@ -102,7 +113,7 @@ export async function renderPlanGate(summary, changes, transport, effectiveRisk,
|
|
|
102
113
|
];
|
|
103
114
|
const shownReasons = shownRiskReasons(effectiveRisk.raisedBy);
|
|
104
115
|
if (shownReasons.length > 0) {
|
|
105
|
-
facts.push(`Risk raised to ${effectiveRisk.level}: ${
|
|
116
|
+
facts.push(`Risk raised to ${effectiveRisk.level}: ${riskReasonTexts(effectiveRisk).join('; ')}`);
|
|
106
117
|
}
|
|
107
118
|
// Part 2b D7 — a step that names the process it runs in (a BAdI
|
|
108
119
|
// implementation) carries its "Affects:" row right under it. The card's
|
|
@@ -158,7 +169,7 @@ export async function renderPlanGate(summary, changes, transport, effectiveRisk,
|
|
|
158
169
|
title: system ? `Apply this plan on ${system}?` : 'Apply this plan?',
|
|
159
170
|
facts,
|
|
160
171
|
choices: [
|
|
161
|
-
{ value: 'apply_all', label:
|
|
172
|
+
{ value: 'apply_all', label: applyLabel },
|
|
162
173
|
{ value: 'review', label: 'Review each change one by one' },
|
|
163
174
|
{ value: 'cancel', label: 'Cancel' },
|
|
164
175
|
],
|
|
@@ -178,6 +189,7 @@ export async function renderPlanGate(summary, changes, transport, effectiveRisk,
|
|
|
178
189
|
async function classicPlanGate(summary, visibleChanges, transport, effectiveRisk) {
|
|
179
190
|
const visibleCount = visibleChanges.length;
|
|
180
191
|
const noun = visibleCount === 1 ? 'change' : 'changes';
|
|
192
|
+
const applyLabel = applyAllLabel(visibleCount, noun);
|
|
181
193
|
console.log('');
|
|
182
194
|
console.log(`${peach('Plan')} ${summary}`);
|
|
183
195
|
const meta = [`${visibleCount} ${noun}`, riskTag(effectiveRisk.level)];
|
|
@@ -186,7 +198,7 @@ async function classicPlanGate(summary, visibleChanges, transport, effectiveRisk
|
|
|
186
198
|
console.log(` ${chalk.dim(meta.join(' · '))}`);
|
|
187
199
|
const shownReasons = shownRiskReasons(effectiveRisk.raisedBy);
|
|
188
200
|
if (shownReasons.length > 0) {
|
|
189
|
-
console.log(` ${chalk.dim(`(risk raised: ${
|
|
201
|
+
console.log(` ${chalk.dim(`(risk raised: ${riskReasonTexts(effectiveRisk).join('; ')})`)}`);
|
|
190
202
|
}
|
|
191
203
|
visibleChanges.forEach((c, i) => {
|
|
192
204
|
console.log(` ${chalk.dim(`${i + 1}.`)} ${verbLine(c)}`);
|
|
@@ -207,7 +219,7 @@ async function classicPlanGate(summary, visibleChanges, transport, effectiveRisk
|
|
|
207
219
|
default: 'apply_all',
|
|
208
220
|
theme: inquirerTheme,
|
|
209
221
|
choices: [
|
|
210
|
-
{ value: 'apply_all', name:
|
|
222
|
+
{ value: 'apply_all', name: applyLabel },
|
|
211
223
|
{ value: 'review', name: 'Review each change individually' },
|
|
212
224
|
{ value: 'cancel', name: 'Cancel' },
|
|
213
225
|
],
|
|
@@ -283,11 +295,11 @@ export async function renderPerChangeApproval(change, effectiveRisk, transport)
|
|
|
283
295
|
// proper diff content.
|
|
284
296
|
if (change.diff) {
|
|
285
297
|
if (looksLikeUnifiedDiff(change.diff)) {
|
|
286
|
-
console.log('\n' + colorizeDiff(change.diff) + '\n');
|
|
298
|
+
console.log('\n' + colorizeDiff(decodeDisplayBlock(change.diff)) + '\n');
|
|
287
299
|
}
|
|
288
300
|
else {
|
|
289
301
|
console.log('\n' + chalk.yellow(`${getGlyphs().attention} Model did not provide a unified diff — only a description:`));
|
|
290
|
-
console.log(chalk.dim(change.diff) + '\n');
|
|
302
|
+
console.log(chalk.dim(decodeDisplayEntities(change.diff)) + '\n');
|
|
291
303
|
}
|
|
292
304
|
}
|
|
293
305
|
else {
|
|
@@ -100,8 +100,13 @@ export function signatureChange(diff) {
|
|
|
100
100
|
return false;
|
|
101
101
|
let state = 'unknown';
|
|
102
102
|
for (const raw of diff.split('\n')) {
|
|
103
|
-
|
|
103
|
+
// LT-10 (1A fix wave) — a file header starts another file (a diff can
|
|
104
|
+
// cover several includes): a declaration still open in the file before
|
|
105
|
+
// it never carries over.
|
|
106
|
+
if (raw.startsWith('+++') || raw.startsWith('---')) {
|
|
107
|
+
state = 'unknown';
|
|
104
108
|
continue;
|
|
109
|
+
}
|
|
105
110
|
if (raw.startsWith('@@')) {
|
|
106
111
|
// Task 7 (R2) / fix round 1 — a header naming the block
|
|
107
112
|
// (`@@ METHOD to_internal. @@`) hints that the hunk starts in its body.
|
|
@@ -157,6 +162,7 @@ export function effectiveRisk(declared, changes, ctx) {
|
|
|
157
162
|
// a body-only change read "may alter a method signature" when activation
|
|
158
163
|
// was the true reason.
|
|
159
164
|
const applied = [];
|
|
165
|
+
let syntaxObjectWord;
|
|
160
166
|
const raise = (r, label, always = false) => {
|
|
161
167
|
applied.push({ r, label, always });
|
|
162
168
|
};
|
|
@@ -176,6 +182,16 @@ export function effectiveRisk(declared, changes, ctx) {
|
|
|
176
182
|
// Final review A M-4 — the reason is always shown, even when the floor
|
|
177
183
|
// was already high (policy never, or declared high).
|
|
178
184
|
raise('high', 'syntax_errors', true);
|
|
185
|
+
// LT-10 — "this class" / "this program" from the RECORDED keys' types
|
|
186
|
+
// for the hit names (the match is by name, so the row's type may not be
|
|
187
|
+
// what was checked). Only when they all agree on one known word.
|
|
188
|
+
const hitNames = new Set(changes.filter(hit).map((c) => c.object.trim().split(/\s+/)[0].toUpperCase()));
|
|
189
|
+
const words = new Set(ctx.syntaxErrorObjects
|
|
190
|
+
.filter((k) => hitNames.has(k.slice(k.lastIndexOf(':') + 1).toUpperCase()))
|
|
191
|
+
.map((k) => ctx.typeWord?.(k.slice(0, k.lastIndexOf(':')))));
|
|
192
|
+
const [only] = [...words];
|
|
193
|
+
if (words.size === 1 && only)
|
|
194
|
+
syntaxObjectWord = only;
|
|
179
195
|
}
|
|
180
196
|
}
|
|
181
197
|
// Part 2b D7 — a BAdI implementation runs inside a standard SAP process:
|
|
@@ -231,7 +247,7 @@ export function effectiveRisk(declared, changes, ctx) {
|
|
|
231
247
|
raisedBy.splice(act, 1);
|
|
232
248
|
raisedBy.splice(sig, 0, 'activation');
|
|
233
249
|
}
|
|
234
|
-
return { level: floor, raisedBy };
|
|
250
|
+
return syntaxObjectWord ? { level: floor, raisedBy, syntaxObjectWord } : { level: floor, raisedBy };
|
|
235
251
|
}
|
|
236
252
|
/** Plain words for each risk reason code — the codes stay in audit data only. */
|
|
237
253
|
const RISK_REASON_TEXT = {
|
|
@@ -252,15 +268,17 @@ const RISK_REASON_TEXT = {
|
|
|
252
268
|
* Task 7 (R2) — one sentence on every request_approval result (and once in
|
|
253
269
|
* the journey fix block): the risk line is CSPeach's reading of the diff, so
|
|
254
270
|
* the model relays it rather than telling the person it is wrong.
|
|
271
|
+
* LT-6 (1A fix wave): the card already shows it, so the model neither repeats
|
|
272
|
+
* nor explains it unless asked (live: "rated medium only because…").
|
|
255
273
|
*/
|
|
256
|
-
export const RISK_NOTE = "The risk line is CSPeach's reading of the diff
|
|
274
|
+
export const RISK_NOTE = "The risk line is CSPeach's reading of the diff. Do not repeat, explain or dispute it unless the person asks.";
|
|
257
275
|
/**
|
|
258
276
|
* Task 7 (R2) — the risk fields of every request_approval result:
|
|
259
277
|
* effective_risk and raised_by as before (older readers), plus risk { level,
|
|
260
278
|
* reasons } in plain words and the note.
|
|
261
279
|
*/
|
|
262
280
|
export function riskResultFields(eff) {
|
|
263
|
-
const reasons = eff.raisedBy.map(
|
|
281
|
+
const reasons = eff.raisedBy.map((c) => reasonTextFor(c, eff)); // round 8 item 4: plain words, never codes
|
|
264
282
|
return { effective_risk: eff.level, raised_by: reasons, risk: { level: eff.level, reasons: [...reasons] }, note: RISK_NOTE };
|
|
265
283
|
}
|
|
266
284
|
/** Round 8 item 4 — every code the floor can raise (each has plain words above). */
|
|
@@ -279,3 +297,13 @@ const NOT_A_RISK_REASON = new Set(['non_prod_override']);
|
|
|
279
297
|
export function shownRiskReasons(raisedBy) {
|
|
280
298
|
return raisedBy.filter((c) => !NOT_A_RISK_REASON.has(c));
|
|
281
299
|
}
|
|
300
|
+
/** LT-10 — one reason in plain words; the syntax-errors one names the kind of object when known. */
|
|
301
|
+
function reasonTextFor(code, eff) {
|
|
302
|
+
if (code === 'syntax_errors' && eff.syntaxObjectWord)
|
|
303
|
+
return `the last syntax check of this ${eff.syntaxObjectWord} found errors`;
|
|
304
|
+
return riskReasonText(code);
|
|
305
|
+
}
|
|
306
|
+
/** LT-10 — the shown reasons of a result, in plain words (every approval surface). */
|
|
307
|
+
export function riskReasonTexts(eff) {
|
|
308
|
+
return shownRiskReasons(eff.raisedBy).map((c) => reasonTextFor(c, eff));
|
|
309
|
+
}
|
|
@@ -21,6 +21,56 @@ export function collapseImplicitActivates(changes) {
|
|
|
21
21
|
return !writeKeys.has(`${c.type}:${c.object}`);
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* LT-12 (1A fix wave) — the rows a plan list shows. On top of the collapse
|
|
26
|
+
* above, a `modify` of an object that also has a `create` in the list is
|
|
27
|
+
* hidden: the create already covers its source write and activation
|
|
28
|
+
* (approvals/jwt.ts OP_COVERAGE), so "Create X" + "Change X" read as two
|
|
29
|
+
* changes for one new object. Fix round 1: only a modify with no diff of its
|
|
30
|
+
* own, or the same diff as the create, is hidden; a modify carrying other
|
|
31
|
+
* content stays, so the person sees it before "Apply all". When the create
|
|
32
|
+
* row has no behaviour (or affects) and a hidden modify has one, the row
|
|
33
|
+
* shows the modify's — a display copy, never the caller's object. Display
|
|
34
|
+
* only: the `changes[]` behind the card (approval ids, grants) and the gate
|
|
35
|
+
* count (collapseImplicitActivates) are untouched.
|
|
36
|
+
*/
|
|
37
|
+
export function planRows(changes) {
|
|
38
|
+
const rows = collapseImplicitActivates(changes);
|
|
39
|
+
const createOf = new Map();
|
|
40
|
+
for (const c of rows)
|
|
41
|
+
if (c.op === 'create' && !createOf.has(`${c.type}:${c.object}`))
|
|
42
|
+
createOf.set(`${c.type}:${c.object}`, c);
|
|
43
|
+
const hidden = new Set();
|
|
44
|
+
const hiddenOf = new Map();
|
|
45
|
+
for (const c of rows) {
|
|
46
|
+
if (c.op !== 'modify')
|
|
47
|
+
continue;
|
|
48
|
+
const key = `${c.type}:${c.object}`;
|
|
49
|
+
const cr = createOf.get(key);
|
|
50
|
+
if (cr && (!c.diff || c.diff === cr.diff)) {
|
|
51
|
+
hidden.add(c);
|
|
52
|
+
if (!hiddenOf.has(key))
|
|
53
|
+
hiddenOf.set(key, c);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const c of rows) {
|
|
58
|
+
if (hidden.has(c))
|
|
59
|
+
continue;
|
|
60
|
+
const m = c.op === 'create' ? hiddenOf.get(`${c.type}:${c.object}`) : undefined;
|
|
61
|
+
if (!m) {
|
|
62
|
+
out.push(c);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const row = { ...c };
|
|
66
|
+
if (row.behaviour === undefined && m.behaviour !== undefined)
|
|
67
|
+
row.behaviour = m.behaviour;
|
|
68
|
+
if (row.affects === undefined && m.affects !== undefined)
|
|
69
|
+
row.affects = m.affects;
|
|
70
|
+
out.push(row);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
24
74
|
/**
|
|
25
75
|
* Heuristic: a real unified diff has at least one line starting with `+`,
|
|
26
76
|
* `-`, or `@@`. A summary or change description won't. Used to gate
|
package/dist/cli.js
CHANGED
|
@@ -157,12 +157,20 @@ export async function main(argv) {
|
|
|
157
157
|
// Task 12 — the full dump ends with "# model set by your account admin"
|
|
158
158
|
// when /v1/me/model-config says the admin's model is enforced. Capped at
|
|
159
159
|
// 1.5 s and fail-open: offline / signed out ⇒ the output is unchanged.
|
|
160
|
-
const [{ loadConfig }, { getStore }, { loadCustomerModelConfig }] = await Promise.all([
|
|
160
|
+
const [{ loadConfig }, { getStore }, { loadCustomerModelConfig, loadServerModelConfig }] = await Promise.all([
|
|
161
161
|
import('./config/loader.js'),
|
|
162
162
|
import('./auth/api-key.js'),
|
|
163
163
|
import('./models/server-config.js'),
|
|
164
164
|
]);
|
|
165
|
-
|
|
165
|
+
const showCfg = await loadConfig();
|
|
166
|
+
await Promise.all([
|
|
167
|
+
loadCustomerModelConfig(showCfg, async () => (await (await getStore()).get()) ?? '', { timeoutMs: 1500 }).catch(() => { }),
|
|
168
|
+
// Stage 1A — only a stored `assistant.skills = inline` needs the served
|
|
169
|
+
// flag (the "not active" comment). Managed only, same cap, fail-open.
|
|
170
|
+
showCfg.assistant?.skills === 'inline'
|
|
171
|
+
? loadServerModelConfig(showCfg, { timeoutMs: 1500 }).catch(() => { })
|
|
172
|
+
: Promise.resolve(),
|
|
173
|
+
]);
|
|
166
174
|
}
|
|
167
175
|
await runConfigShow(args.slice(2));
|
|
168
176
|
return 0;
|
package/dist/commands/compact.js
CHANGED
|
@@ -33,6 +33,7 @@ import { paint } from '../renderer/theme.js';
|
|
|
33
33
|
import { getGlyphs } from '../renderer/glyphs.js';
|
|
34
34
|
import { saveSession } from '../session/store.js';
|
|
35
35
|
import { isUserPrompt } from '../session/user-prompt.js';
|
|
36
|
+
import { stripSkillBlocksInMessage } from '../agent/inline-skill-block.js';
|
|
36
37
|
import { rebuildCliMarksAfterCompact, COMPACT_LINE } from '../session/cli-record.js';
|
|
37
38
|
import { getActiveDeployments } from '../models/model-identity.js';
|
|
38
39
|
/** Keep this many recent user-prompt anchored turns verbatim. */
|
|
@@ -131,7 +132,11 @@ export function buildSummarisationPrompt() {
|
|
|
131
132
|
*/
|
|
132
133
|
export function serialiseForSummariser(messages) {
|
|
133
134
|
const out = [];
|
|
134
|
-
for (const
|
|
135
|
+
for (const original of messages) {
|
|
136
|
+
// Stage 1A (S-A, M-3) — a playbook body never reaches the summariser: a
|
|
137
|
+
// hash-passing <skill> block in user text becomes [skill block: <name>].
|
|
138
|
+
// A message without one is the same object (byte-identical output).
|
|
139
|
+
const msg = stripSkillBlocksInMessage(original);
|
|
135
140
|
if (typeof msg.content === 'string') {
|
|
136
141
|
out.push({ role: msg.role, content: msg.content });
|
|
137
142
|
}
|
|
@@ -19,6 +19,7 @@ const MUTABLE_KEYS = new Set([
|
|
|
19
19
|
'assistant.lane',
|
|
20
20
|
'assistant.style',
|
|
21
21
|
'assistant.level',
|
|
22
|
+
'assistant.skills',
|
|
22
23
|
'local_build',
|
|
23
24
|
'local_files', // deprecated alias for local_build (2026-06-13) — still accepted
|
|
24
25
|
'show_cogs',
|
|
@@ -83,6 +84,7 @@ const PLAN_MODE_VALUES = ['step', 'guarded'];
|
|
|
83
84
|
const PLAN_AUDIT_VALUES = ['on', 'off'];
|
|
84
85
|
const USAGE_REPORTING_VALUES = ['on', 'off'];
|
|
85
86
|
const ASSISTANT_LANE_VALUES = ['chat', 'skills'];
|
|
87
|
+
const ASSISTANT_SKILLS_VALUES = ['system', 'inline'];
|
|
86
88
|
const APPROVALS_VALUES = ['plan', 'per-write'];
|
|
87
89
|
/**
|
|
88
90
|
* Thrown on validation failure. The outer CLI dispatcher converts to
|
|
@@ -547,6 +549,15 @@ export async function runConfigSet(args) {
|
|
|
547
549
|
cfg.assistant.lane = lane;
|
|
548
550
|
break;
|
|
549
551
|
}
|
|
552
|
+
// Stage 1A — explicit, like the lane. markExplicit below keeps the table.
|
|
553
|
+
case 'assistant.skills': {
|
|
554
|
+
const skills = value.trim().toLowerCase();
|
|
555
|
+
if (skills !== 'system' && skills !== 'inline') {
|
|
556
|
+
err(`assistant.skills must be one of: ${ASSISTANT_SKILLS_VALUES.join(', ')}`);
|
|
557
|
+
}
|
|
558
|
+
cfg.assistant.skills = skills;
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
550
561
|
case 'assistant.style': {
|
|
551
562
|
const style = parseStyle(value);
|
|
552
563
|
if (!style)
|
|
@@ -800,6 +811,8 @@ export function validateValue(key, value) {
|
|
|
800
811
|
return value === 'true' || value === 'false';
|
|
801
812
|
case 'assistant.lane':
|
|
802
813
|
return ASSISTANT_LANE_VALUES.includes(value.trim().toLowerCase());
|
|
814
|
+
case 'assistant.skills':
|
|
815
|
+
return ASSISTANT_SKILLS_VALUES.includes(value.trim().toLowerCase());
|
|
803
816
|
case 'assistant.style':
|
|
804
817
|
return parseStyle(value) !== null;
|
|
805
818
|
case 'assistant.level':
|
|
@@ -3,6 +3,7 @@ import { loadConfig } from '../config/loader.js';
|
|
|
3
3
|
import { paint } from '../renderer/theme.js';
|
|
4
4
|
import { formatAttention } from '../renderer/notice-shape.js';
|
|
5
5
|
import { getCustomerModelConfig } from '../models/server-config.js';
|
|
6
|
+
import { inlineInactiveReason, inlineServed } from '../repl/inline-mode.js';
|
|
6
7
|
/**
|
|
7
8
|
* Dotted paths of keys that must never appear in plaintext config output.
|
|
8
9
|
* SAP passwords live in the keychain and should never land in config.toml,
|
|
@@ -74,7 +75,11 @@ function printLlmMissingHints(llm) {
|
|
|
74
75
|
console.log(`ai_hub_router_model = ${paint('dim', '<not set — skill routing uses ai_hub_model_alias>')}`);
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
+
/**
|
|
79
|
+
* `notes` maps a dotted key to a trailing comment (Stage 1A: a stored
|
|
80
|
+
* `assistant.skills = inline` that would not be active).
|
|
81
|
+
*/
|
|
82
|
+
function printConfigAsToml(cfg, prefix = '', notes = {}) {
|
|
78
83
|
// Emit scalars before nested objects — otherwise a top-level scalar that
|
|
79
84
|
// follows a nested table (e.g. `write_mode` declared after `ui` in the
|
|
80
85
|
// config shape) would render underneath `[ui]` in TOML, which reparses as
|
|
@@ -91,7 +96,8 @@ function printConfigAsToml(cfg, prefix = '') {
|
|
|
91
96
|
console.log(`${k} = [REDACTED]`);
|
|
92
97
|
}
|
|
93
98
|
else {
|
|
94
|
-
|
|
99
|
+
const note = notes[fullKey];
|
|
100
|
+
console.log(`${k} = ${formatTomlValue(v)}` + (note ? chalk.gray(` # ${note}`) : ''));
|
|
95
101
|
// Surface an explanatory hint for advisory-only mode so users understand
|
|
96
102
|
// the implication (writes proposed, never executed) without having to
|
|
97
103
|
// read the docs.
|
|
@@ -104,7 +110,7 @@ function printConfigAsToml(cfg, prefix = '') {
|
|
|
104
110
|
const fullKey = prefix ? `${prefix}.${k}` : k;
|
|
105
111
|
console.log('');
|
|
106
112
|
console.log(chalk.bold(`[${fullKey}]`));
|
|
107
|
-
printConfigAsToml(v, fullKey);
|
|
113
|
+
printConfigAsToml(v, fullKey, notes);
|
|
108
114
|
// Mode-aware extras for the `[llm]` section — print required keys that
|
|
109
115
|
// are empty as `<not set>` so a user who ran `cspeach config set llm.mode
|
|
110
116
|
// ai-hub` without also setting the base URL can see what is still
|
|
@@ -165,7 +171,13 @@ export async function runConfigShow(args) {
|
|
|
165
171
|
// Full config dump
|
|
166
172
|
console.log('');
|
|
167
173
|
console.log(chalk.gray('# ~/.cspeach/config.toml'));
|
|
168
|
-
|
|
174
|
+
// Stage 1A — say when a stored `inline` would not be active in an
|
|
175
|
+
// interactive session (the served flag is loaded by cli.ts).
|
|
176
|
+
// Read defensively: a config without an [assistant] table means the defaults.
|
|
177
|
+
const inlineNote = cfg.assistant?.skills === 'inline'
|
|
178
|
+
? inlineInactiveReason({ skills: 'inline', lane: cfg.assistant.lane, served: inlineServed(cfg.llm?.mode) })
|
|
179
|
+
: null;
|
|
180
|
+
printConfigAsToml(cfg, '', inlineNote ? { 'assistant.skills': inlineNote } : {});
|
|
169
181
|
console.log('');
|
|
170
182
|
// Task 12 — one trailing line when the account admin's model is enforced
|
|
171
183
|
// by the proxy (/v1/me/model-config). No snapshot ⇒ output unchanged.
|
|
@@ -20,7 +20,7 @@ import path from 'node:path';
|
|
|
20
20
|
import { metaFromMarkdown } from '../docx/doc-meta.js';
|
|
21
21
|
import { writeDocxNextTo, writeNoClobber } from '../docx/write-docx.js';
|
|
22
22
|
import { normalizeAskInput } from '../tools/ask-question.js';
|
|
23
|
-
import { isUserPrompt, userPromptText } from '../session/user-prompt.js';
|
|
23
|
+
import { INJECTED_CONTEXT_TAGS, isUserPrompt, userPromptText } from '../session/user-prompt.js';
|
|
24
24
|
import { resolveCliMarks, startsTurn } from '../session/cli-record.js';
|
|
25
25
|
import { docxFileFromReplArgs, runExportDocxCommand } from './export-docx.js';
|
|
26
26
|
// The usage line lives in the light export-audit module (final-review I-2); re-exported here.
|
|
@@ -30,11 +30,14 @@ const OUT_DIR = 'cspeach-out';
|
|
|
30
30
|
function readerText(text) {
|
|
31
31
|
return text.replace(/<!--[\s\S]*?-->/g, '').replace(/\n{3,}/g, '\n\n').trim();
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Context blocks the CLI adds to a prompt (the developer never typed them) are
|
|
35
|
+
* dropped with the shared INJECTED_CONTEXT_TAGS list (session/user-prompt.ts),
|
|
36
|
+
* which carries Stage 1A's inline `<skill>` playbook blocks too.
|
|
37
|
+
*/
|
|
35
38
|
function typedText(text) {
|
|
36
39
|
let out = text;
|
|
37
|
-
for (const tag of
|
|
40
|
+
for (const tag of INJECTED_CONTEXT_TAGS) {
|
|
38
41
|
out = out.replace(new RegExp(`<${tag}(?:\\s[^>]*)?>[\\s\\S]*?</${tag}>`, 'g'), '');
|
|
39
42
|
}
|
|
40
43
|
out = out.replace(/<attached file="([^"]*)">[\s\S]*?<\/attached>/g, (_m, f) => `@${f}`);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// cspeach-cli/src/commands/finish-check-guard.ts
|
|
2
|
+
//
|
|
3
|
+
// Finish-check guard (2026-10-23, inline-waiver findings §6 Option A).
|
|
4
|
+
// A plan step may not be saved "validated" while that turn's LAST
|
|
5
|
+
// rap_finish_check said fewer than 5 of 5 passed. The write-back
|
|
6
|
+
// downgrades it to "blocked" (never refuses: a refusal would cost another
|
|
7
|
+
// model turn). "validated-with-waiver" is kept only when the step's notes
|
|
8
|
+
// name every failed item id.
|
|
9
|
+
//
|
|
10
|
+
// The guard acts only when it reads the check confidently. No check this
|
|
11
|
+
// turn, an unreadable last check, a 5 of 5, or any other status → null
|
|
12
|
+
// (today's behaviour).
|
|
13
|
+
const TOOL_NAME = 'rap_finish_check';
|
|
14
|
+
// The real handler's shape (tools/rap/finish-check.ts formatFinish):
|
|
15
|
+
// Finish check: <n> of 5 passed
|
|
16
|
+
// F<k> <title> — passed | needs work: <problems>
|
|
17
|
+
const HEADER_RE = /^Finish check: (\d+) of (\d+) passed$/;
|
|
18
|
+
const ITEM_RE = /^(F[1-5]) .*? — (passed|needs work:.*)$/;
|
|
19
|
+
const ITEM_IDS = ['F1', 'F2', 'F3', 'F4', 'F5'];
|
|
20
|
+
function resultText(content) {
|
|
21
|
+
if (typeof content === 'string')
|
|
22
|
+
return content;
|
|
23
|
+
if (!Array.isArray(content))
|
|
24
|
+
return null;
|
|
25
|
+
const parts = [];
|
|
26
|
+
for (const b of content) {
|
|
27
|
+
if (b?.type === 'text' && typeof b.text === 'string')
|
|
28
|
+
parts.push(b.text);
|
|
29
|
+
}
|
|
30
|
+
return parts.length > 0 ? parts.join('\n') : null;
|
|
31
|
+
}
|
|
32
|
+
/** The text of this turn's LAST rap_finish_check tool result, or null when there is none. */
|
|
33
|
+
export function lastFinishCheckText(messages, messagesStart) {
|
|
34
|
+
const checkIds = new Set();
|
|
35
|
+
let last = null;
|
|
36
|
+
for (let i = Math.max(0, messagesStart); i < messages.length; i++) {
|
|
37
|
+
const m = messages[i];
|
|
38
|
+
if (!Array.isArray(m.content))
|
|
39
|
+
continue;
|
|
40
|
+
for (const b of m.content) {
|
|
41
|
+
if (!b || typeof b !== 'object')
|
|
42
|
+
continue;
|
|
43
|
+
if (m.role === 'assistant' && b.type === 'tool_use' && b.name === TOOL_NAME && typeof b.id === 'string') {
|
|
44
|
+
checkIds.add(b.id);
|
|
45
|
+
}
|
|
46
|
+
else if (m.role === 'user' && b.type === 'tool_result' && typeof b.tool_use_id === 'string' && checkIds.has(b.tool_use_id)) {
|
|
47
|
+
// An unreadable last result still counts as the last one: it makes the guard stand down.
|
|
48
|
+
last = resultText(b.content) ?? '';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return last;
|
|
53
|
+
}
|
|
54
|
+
/** Parses a rap_finish_check result; null unless every line agrees with the handler's shape. */
|
|
55
|
+
export function parseFinishCheck(text) {
|
|
56
|
+
const lines = text.split(/\r?\n/).map((l) => l.trimEnd());
|
|
57
|
+
const head = HEADER_RE.exec(lines[0] ?? '');
|
|
58
|
+
if (!head)
|
|
59
|
+
return null;
|
|
60
|
+
const passed = Number(head[1]);
|
|
61
|
+
const total = Number(head[2]);
|
|
62
|
+
if (total !== ITEM_IDS.length || passed > total)
|
|
63
|
+
return null;
|
|
64
|
+
const seen = [];
|
|
65
|
+
const failed = [];
|
|
66
|
+
for (const line of lines.slice(1)) {
|
|
67
|
+
const m = ITEM_RE.exec(line);
|
|
68
|
+
if (!m)
|
|
69
|
+
continue;
|
|
70
|
+
seen.push(m[1]);
|
|
71
|
+
if (m[2] !== 'passed')
|
|
72
|
+
failed.push(m[1]);
|
|
73
|
+
}
|
|
74
|
+
if (seen.join(',') !== ITEM_IDS.join(','))
|
|
75
|
+
return null;
|
|
76
|
+
if (failed.length !== total - passed)
|
|
77
|
+
return null;
|
|
78
|
+
return { passed, total, failed };
|
|
79
|
+
}
|
|
80
|
+
const namesId = (notes, id) => new RegExp(`\\b${id}\\b`).test(notes);
|
|
81
|
+
/**
|
|
82
|
+
* The guard's note: one place for its wording. It goes into the step's
|
|
83
|
+
* work.notes and is shown as the run line.
|
|
84
|
+
* <step>: finish check 3 of 5, F1, F4 failed. Saved as blocked.
|
|
85
|
+
* <step>: finish check 3 of 5, F1, F4 failed. The notes do not waive F4. Saved as blocked.
|
|
86
|
+
*/
|
|
87
|
+
const GUARD_NOTE_TAIL = 'Saved as blocked.';
|
|
88
|
+
const GUARD_NOTE_HEAD = 'finish check';
|
|
89
|
+
const GUARD_NOTE_RE = new RegExp(`: ${GUARD_NOTE_HEAD} \\d+ of \\d+, .* ${GUARD_NOTE_TAIL.replace(/\./g, '\\.')}$`);
|
|
90
|
+
export function formatFinishGuardNote(a) {
|
|
91
|
+
const waiver = a.notWaived && a.notWaived.length > 0 ? ` The notes do not waive ${a.notWaived.join(', ')}.` : '';
|
|
92
|
+
return `${a.step}: ${GUARD_NOTE_HEAD} ${a.score.passed} of ${a.score.total}, ${a.score.failed.join(', ')} failed.${waiver} ${GUARD_NOTE_TAIL}`;
|
|
93
|
+
}
|
|
94
|
+
/** True for a line the guard wrote itself (it never counts as a waiver). */
|
|
95
|
+
export function isFinishGuardNoteLine(line) {
|
|
96
|
+
return GUARD_NOTE_RE.test(line.trim());
|
|
97
|
+
}
|
|
98
|
+
/** The notes without the guard's own lines: only what the model or developer wrote. */
|
|
99
|
+
function notesWithoutGuardLines(notes) {
|
|
100
|
+
return notes.split(/\r?\n/).filter((l) => !isFinishGuardNoteLine(l)).join('\n');
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The guard's ruling for the executed step: `{ status: 'blocked', note }` when
|
|
104
|
+
* the claimed status must be downgraded, otherwise null (save as written).
|
|
105
|
+
* A waiver counts from any turn's notes, but never from the guard's own lines.
|
|
106
|
+
*/
|
|
107
|
+
export function finishCheckGuard(args) {
|
|
108
|
+
if (args.status !== 'validated' && args.status !== 'validated-with-waiver')
|
|
109
|
+
return null;
|
|
110
|
+
const text = lastFinishCheckText(args.messages, args.messagesStart);
|
|
111
|
+
if (text === null)
|
|
112
|
+
return null;
|
|
113
|
+
const score = parseFinishCheck(text);
|
|
114
|
+
if (!score || score.failed.length === 0)
|
|
115
|
+
return null;
|
|
116
|
+
let notWaived;
|
|
117
|
+
if (args.status === 'validated-with-waiver') {
|
|
118
|
+
const notes = notesWithoutGuardLines(args.notes ?? '');
|
|
119
|
+
notWaived = score.failed.filter((id) => !namesId(notes, id));
|
|
120
|
+
if (notWaived.length === 0)
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return { status: 'blocked', note: formatFinishGuardNote({ step: args.step, score, notWaived }) };
|
|
124
|
+
}
|
|
@@ -34,7 +34,7 @@ import { collectPlannedObjects, plannedObjectFacts } from '../projects/plan-obje
|
|
|
34
34
|
import { isDeployPhase } from '../projects/plan-lane.js';
|
|
35
35
|
import { askDeployCard } from './deploy-card.js';
|
|
36
36
|
import { decideNextPhaseGate, getAndResetPlanPhaseSubagentDispatches, getAndResetPlanDeviation, } from './plan-gate.js';
|
|
37
|
-
import { applyAuditResult, applyPhaseResolution, buildResumeCommand, offerNextPhaseAutoRun, } from './plan-resume.js';
|
|
37
|
+
import { applyAuditResult, applyPhaseResolution, buildResumeCommand, offerNextPhaseAutoRun, planBuildTransport, } from './plan-resume.js';
|
|
38
38
|
import { formatFailure, formatInfo } from '../renderer/notice-shape.js';
|
|
39
39
|
import { clean } from '../renderer/tool-labels.js';
|
|
40
40
|
import { runBatchAudit, pendingAuditPhaseIds, selectPlanCardAction, } from './plan-batch-audit.js';
|
|
@@ -251,7 +251,7 @@ async function postPhaseChain(args) {
|
|
|
251
251
|
const built = after.content.phases.every((q) => isPhaseSatisfiedForRun(st[q.id], q.audit));
|
|
252
252
|
const nextAfter = computeNextPhaseForRun(after.content.phases, st);
|
|
253
253
|
if (built && !nextAfter) {
|
|
254
|
-
args.log('', ...planCompletionLines(after.content.phases));
|
|
254
|
+
args.log('', ...planCompletionLines(after.content.phases, undefined, planBuildTransport(after.content.phases)));
|
|
255
255
|
// Step 2 (C5): a rebuild whose plan this is now reads "built, not yet checked".
|
|
256
256
|
await afterPlanComplete(cur);
|
|
257
257
|
args.log(`Plan file: ${savedLocation(cur, args.cwd)}`);
|
|
@@ -99,10 +99,28 @@ export async function regroundPlan(a) {
|
|
|
99
99
|
stillLocked.push(o.name);
|
|
100
100
|
}
|
|
101
101
|
let transportClosed = false;
|
|
102
|
+
let listed = null;
|
|
103
|
+
const isListed = (t, tr) => t.list.some((x) => x.trim().toUpperCase() === tr.trim().toUpperCase());
|
|
102
104
|
if (transport) {
|
|
103
105
|
const t = await a.deps.openTransports();
|
|
104
|
-
|
|
106
|
+
listed = t;
|
|
107
|
+
transportClosed = t.complete && !isListed(t, transport);
|
|
105
108
|
}
|
|
109
|
+
// The step transport counts as open only when SAP lists it. Any failure
|
|
110
|
+
// here keeps the old text and a "not open" verdict.
|
|
111
|
+
const step = a.stepTransport?.trim();
|
|
112
|
+
let stepOpen = false;
|
|
113
|
+
if (step) {
|
|
114
|
+
try {
|
|
115
|
+
listed = listed ?? await a.deps.openTransports();
|
|
116
|
+
stepOpen = isListed(listed, step);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
recordFullNotice('plan', `Transport check before a plan step failed: ${errText(err)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const openStepTransport = !transport && stepOpen ? step : null;
|
|
123
|
+
const verdict = step ? { stepTransportOpen: stepOpen } : {};
|
|
106
124
|
// An object left inactive gets the inactive instruction only, never the
|
|
107
125
|
// "do not rewrite" one as well.
|
|
108
126
|
const inactiveNames = new Set(inactive.map((o) => o.name));
|
|
@@ -124,7 +142,9 @@ export async function regroundPlan(a) {
|
|
|
124
142
|
? (transportClosed
|
|
125
143
|
? `- The plan's transport ${transport} is no longer open. Do not write to it. Creating a new transport needs the developer's approval (it is not on the approved list).`
|
|
126
144
|
: `- Transport ${transport} is still open: use it.`)
|
|
127
|
-
:
|
|
145
|
+
: openStepTransport
|
|
146
|
+
? `- Transport ${openStepTransport} is open: use it.`
|
|
147
|
+
: '- No transport is recorded yet: the first writing step creates one.',
|
|
128
148
|
'</sap_state>',
|
|
129
149
|
];
|
|
130
150
|
if (stillLocked.length > 0) {
|
|
@@ -133,12 +153,14 @@ export async function regroundPlan(a) {
|
|
|
133
153
|
ok: false, reason: 'locked',
|
|
134
154
|
lines: [`${stillLocked.join(', ')} ${one ? 'is' : 'are'} still locked by another session. SAP frees ${one ? 'it' : 'them'} when that session ends or times out, usually within a few minutes.`],
|
|
135
155
|
sapStateBlock: block,
|
|
156
|
+
...verdict,
|
|
136
157
|
};
|
|
137
158
|
}
|
|
138
159
|
const n = inactive.length;
|
|
139
160
|
const summary = `Checked SAP before continuing: ${n} object${n === 1 ? '' : 's'} left inactive`
|
|
140
|
-
+ (transport ? `, transport ${transport} ${transportClosed ? 'no longer open' : 'still open'}` : '')
|
|
141
|
-
|
|
161
|
+
+ (transport ? `, transport ${transport} ${transportClosed ? 'no longer open' : 'still open'}` : '')
|
|
162
|
+
+ (openStepTransport ? `, transport ${openStepTransport} open` : '') + '.';
|
|
163
|
+
return { ok: true, summary, quiet: n === 0 && !transportClosed, sapStateBlock: block, ...verdict };
|
|
142
164
|
}
|
|
143
165
|
catch (err) {
|
|
144
166
|
const code = unreachableOf(err);
|
|
@@ -158,6 +180,7 @@ export async function regroundPlan(a) {
|
|
|
158
180
|
built.length > 0 ? `- Already built by earlier steps of this plan: ${built.join(', ')}.` : '- No earlier step of this plan has built SAP objects yet.',
|
|
159
181
|
'</sap_state>',
|
|
160
182
|
],
|
|
183
|
+
...(a.stepTransport?.trim() ? { stepTransportOpen: false } : {}),
|
|
161
184
|
};
|
|
162
185
|
}
|
|
163
186
|
}
|