@cspeach/cli 1.1.27 → 1.1.28
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/loop.js +33 -4
- package/dist/agent/provider-shape.js +9 -2
- package/dist/agent/providers/ai-hub-provider.js +2 -2
- package/dist/agent/providers/byok-provider.js +2 -2
- package/dist/agent/providers/local-provider.js +4 -4
- package/dist/agent/system-prompt.js +34 -7
- package/dist/agent/tool-dispatch.js +20 -1
- package/dist/approvals/grant-claims.js +70 -0
- package/dist/approvals/jwt.js +21 -1
- package/dist/approvals/plan-grant.js +297 -0
- package/dist/commands/config-set.js +12 -0
- package/dist/commands/deploy-card.js +10 -5
- package/dist/commands/export-audit.js +1 -1
- package/dist/commands/plan-audit-evidence.js +47 -1
- package/dist/commands/plan-audit.js +23 -8
- package/dist/commands/plan-batch-audit.js +349 -0
- package/dist/commands/plan-boundary.js +88 -0
- package/dist/commands/plan-chain.js +409 -566
- package/dist/commands/plan-continue.js +21 -7
- package/dist/commands/plan-gate.js +28 -49
- package/dist/commands/plan-reground.js +174 -0
- package/dist/commands/plan-resume.js +227 -103
- package/dist/commands/plan-sap-pause.js +105 -0
- package/dist/commands/plan-startup.js +218 -0
- package/dist/config/loader.js +9 -0
- package/dist/doctor/checks/forge-rules.js +67 -85
- package/dist/doctor/run.js +4 -4
- package/dist/lane/app-chain.js +88 -9
- package/dist/one-shot.js +1 -0
- package/dist/projects/extract-plan.js +34 -1
- package/dist/projects/handover-md.js +1 -1
- package/dist/projects/plan-objects.js +75 -0
- package/dist/projects/plan-run.js +74 -18
- package/dist/projects/plan-schema.js +6 -0
- package/dist/projects/plan-sources.js +199 -0
- package/dist/projects/promote-command.js +1 -1
- package/dist/projects/save-command.js +11 -13
- package/dist/projects/saved-lines.js +36 -0
- package/dist/renderer/checklist-format.js +5 -5
- package/dist/renderer/machine-blocks.js +141 -0
- package/dist/renderer/tool-labels.js +3 -0
- package/dist/repl/plan-hop.js +22 -0
- package/dist/repl/rule8-detector.js +16 -1
- package/dist/repl.js +320 -55
- package/dist/router/router-core.js +2 -2
- package/dist/sap/reachability-state.js +25 -0
- package/dist/session/audit-export.js +10 -16
- package/dist/session/recap.js +16 -26
- package/dist/skills/bundled-skills.js +1 -1
- package/dist/tools/_filesystem-shared.js +37 -0
- package/dist/tools/approval.js +48 -2
- package/dist/tools/dispatch-skill.js +3 -2
- package/dist/tools/filesystem/file-read.js +5 -3
- package/dist/tools/filesystem/glob.js +27 -4
- package/dist/tools/fiori/tools.js +6 -0
- package/dist/tools/lane/app-lane-resolve.js +1 -1
- package/dist/tools/sap-read.js +37 -3
- package/dist/tools/sap-write.js +65 -0
- package/dist/tools/text-elements/pool.js +250 -0
- package/dist/tools/text-elements/tools.js +433 -0
- package/dist/tools/tool-loading.js +3 -0
- package/dist/tools/transport.js +26 -5
- package/dist/tools/write-mode.js +4 -3
- package/package.json +120 -121
- package/dist/projects/email-template.js +0 -56
|
@@ -11,17 +11,33 @@ import { existsSync } from 'node:fs';
|
|
|
11
11
|
import { leasePathFor, acquireRunLease } from '../projects/run-lease.js';
|
|
12
12
|
import { readProjectFile } from '../projects/status.js';
|
|
13
13
|
import { parsePlanContent, phaseWrites } from '../projects/plan-schema.js';
|
|
14
|
-
import { statusesFromItems } from '../projects/plan-run.js';
|
|
14
|
+
import { statusesFromItems, phaseDisplayTitle } from '../projects/plan-run.js';
|
|
15
15
|
import { findNewestVersion } from './plan-resume.js';
|
|
16
16
|
import { collectTurnAssistantText } from '../agent/turn-assistant-text.js';
|
|
17
17
|
import { finishPlanResume } from './plan-resume.js';
|
|
18
18
|
import { runPostPhaseChain } from './plan-chain.js';
|
|
19
|
+
import { DEFAULT_PLAN_MODE } from './plan-gate.js';
|
|
19
20
|
import { loadPlanLaneForRun } from '../projects/plan-lane.js';
|
|
20
21
|
/**
|
|
21
22
|
* The same block shape loop.ts's manifest-on-interrupt hook scans for.
|
|
22
23
|
* E2 dual-read: accept both the legacy `csforge:` and current `cspeach:` prefixes.
|
|
23
24
|
*/
|
|
24
25
|
const MANIFEST_BLOCK_RE = /<!--\s*(?:csforge|cspeach):plan-manifest\s*\n([\s\S]*?)\n\s*-->/g;
|
|
26
|
+
/** Task 13 — a step's display title from a plan envelope; a plain fallback, never the id. */
|
|
27
|
+
function stepTitle(envelope, phaseId) {
|
|
28
|
+
const pc = parsePlanContent(envelope.content);
|
|
29
|
+
const p = pc.ok ? pc.content.phases.find((q) => q.id === phaseId) : undefined;
|
|
30
|
+
return p && pc.ok ? phaseDisplayTitle(p, pc.content.phases) : 'the interrupted step';
|
|
31
|
+
}
|
|
32
|
+
/** The one line shown when a bare "continue" re-attaches to an interrupted step. */
|
|
33
|
+
export function continuationNotice(prepared) {
|
|
34
|
+
return `Continuing ${stepTitle(prepared.envelope, prepared.nextPhaseId)} where it stopped`;
|
|
35
|
+
}
|
|
36
|
+
/** The dim line after a continuation turn that saved nothing: the step is not finished. */
|
|
37
|
+
export function stillInFlightLine(envelope, phaseId) {
|
|
38
|
+
const t = stepTitle(envelope, phaseId);
|
|
39
|
+
return `${t.charAt(0).toUpperCase()}${t.slice(1)} is not finished yet. Nothing was saved for it this turn.`;
|
|
40
|
+
}
|
|
25
41
|
/**
|
|
26
42
|
* Build the marker from the prepared resume the phase turn is about to run.
|
|
27
43
|
* `family` mirrors leasePathFor's family-base convention (version suffix
|
|
@@ -113,10 +129,8 @@ export async function preparePlanContinuation(args) {
|
|
|
113
129
|
// the marker stays so the user can finish there or take over via --resume.
|
|
114
130
|
const lease = acquireRunLease(path, args.sessionId, { log: (l) => args.log(l) });
|
|
115
131
|
if (!lease.acquired) {
|
|
116
|
-
const
|
|
117
|
-
args.log(
|
|
118
|
-
+ (holder ? ` (session ${holder.sessionId}, pid ${holder.pid})` : '')
|
|
119
|
-
+ '. Finish there or /abap-plan --resume here to take over.');
|
|
132
|
+
const title = phaseDisplayTitle(phase, parsed.content.phases);
|
|
133
|
+
args.log(`${title} was not picked up — another CSPeach window is running this plan. Finish there, or type /abap-plan --resume here to take over.`);
|
|
120
134
|
return { prepared: null, stale: false };
|
|
121
135
|
}
|
|
122
136
|
const prepared = {
|
|
@@ -149,7 +163,7 @@ export async function finishPlanContinuation(args) {
|
|
|
149
163
|
if (cls === 'none')
|
|
150
164
|
return { kind: 'none' };
|
|
151
165
|
if (cls === 'foreign') {
|
|
152
|
-
args.log(`
|
|
166
|
+
args.log(`This turn's result belongs to a different plan — it was not added to ${stepTitle(args.prepared.envelope, args.marker.phaseId)}.`);
|
|
153
167
|
return { kind: 'foreign' };
|
|
154
168
|
}
|
|
155
169
|
const outcome = await finishPlanResume({
|
|
@@ -177,7 +191,7 @@ export async function runContinuationChain(args) {
|
|
|
177
191
|
sessionId: args.sessionId,
|
|
178
192
|
cwd: args.cwd,
|
|
179
193
|
...(args.sapAlias ? { sapAlias: args.sapAlias } : {}),
|
|
180
|
-
mode: args.prepared.effectiveMode ??
|
|
194
|
+
mode: args.prepared.effectiveMode ?? DEFAULT_PLAN_MODE,
|
|
181
195
|
modeFlag: args.prepared.mode,
|
|
182
196
|
auditMode: args.prepared.auditMode,
|
|
183
197
|
auditFlag: args.prepared.auditFlag,
|
|
@@ -7,22 +7,17 @@
|
|
|
7
7
|
// arrives as an argument, and the caller (Task 6 wiring in the REPL's
|
|
8
8
|
// offerNextPhase path) renders the returned decision.
|
|
9
9
|
//
|
|
10
|
-
// Semantics (
|
|
11
|
-
// step ⇒ always `prompt` —
|
|
12
|
-
// guarded ⇒ `stop`
|
|
13
|
-
//
|
|
14
|
-
// `
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// apply — a hard stop must never be downgraded to a consent prompt.
|
|
22
|
-
// Legacy envelopes (phases with no audit field) never stop the chain:
|
|
23
|
-
// isAuditResolved(undefined) === true.
|
|
24
|
-
import { phaseWrites } from '../projects/plan-schema.js';
|
|
25
|
-
import { isAuditResolved } from '../projects/plan-run.js';
|
|
10
|
+
// Semantics (2026-10-04, continuous run — spec §3.1/§3.3):
|
|
11
|
+
// step ⇒ always `prompt` — one consent per step (unchanged).
|
|
12
|
+
// guarded ⇒ `stop` when the PREVIOUS step's audit is unresolved for the run
|
|
13
|
+
// (a deferred 'pending' counts as fine; 'failed' / hand-edited do not);
|
|
14
|
+
// `auto` otherwise — write steps included. Writes are approved by the
|
|
15
|
+
// plan's object-list grant or by their own cards, never by a
|
|
16
|
+
// step-level prompt.
|
|
17
|
+
// Guarded is the DEFAULT (DEFAULT_PLAN_MODE): one approval, then a continuous run.
|
|
18
|
+
import { isAuditResolvedForRun } from '../projects/plan-run.js';
|
|
19
|
+
/** 2026-10-04 — an absent plan_mode / flag means the continuous run. */
|
|
20
|
+
export const DEFAULT_PLAN_MODE = 'guarded';
|
|
26
21
|
/* ── guarded-run auto_approve override (Task 6, 2026-07-03) ────────────────
|
|
27
22
|
* Module-level flag consulted by tools/approval.ts BEFORE its auto-approve
|
|
28
23
|
* branch. The REPL sets it at guarded-chain start (right before the phase's
|
|
@@ -49,12 +44,21 @@ export function isGuardedRunActive() {
|
|
|
49
44
|
* When the flag is off, agent_run's opt-in read_only behaviour is
|
|
50
45
|
* byte-identical (skill-mode compatibility). */
|
|
51
46
|
let planPhaseActive = false;
|
|
52
|
-
|
|
47
|
+
/* 2026-10-04 (continuous run) — the id of the plan whose step is running. The
|
|
48
|
+
* plan object-list grant covers requests only for THIS plan; a step started
|
|
49
|
+
* without an id (null) is never covered. Cleared with the flag. */
|
|
50
|
+
let activePlanId = null;
|
|
51
|
+
export function setPlanPhaseActive(active, planId) {
|
|
53
52
|
planPhaseActive = active;
|
|
53
|
+
activePlanId = active && typeof planId === 'string' && planId ? planId : null;
|
|
54
54
|
}
|
|
55
55
|
export function isPlanPhaseActive() {
|
|
56
56
|
return planPhaseActive;
|
|
57
57
|
}
|
|
58
|
+
/** The plan whose step is running, or null (no step, or started without an id). */
|
|
59
|
+
export function getActivePlanId() {
|
|
60
|
+
return planPhaseActive ? activePlanId : null;
|
|
61
|
+
}
|
|
58
62
|
/* Task 7 — per-phase subagent dispatch counter. agent_run increments it on
|
|
59
63
|
* every dispatch made while a plan phase is active; the REPL reads it
|
|
60
64
|
* (read+reset) in the runTurn finally that clears the plan-phase flag and
|
|
@@ -110,44 +114,19 @@ export function getAndResetPlanDeviation() {
|
|
|
110
114
|
return d;
|
|
111
115
|
}
|
|
112
116
|
/**
|
|
113
|
-
* Banner shown
|
|
114
|
-
*
|
|
115
|
-
*
|
|
117
|
+
* Banner shown when a guarded run stops. The resume hint uses the literal
|
|
118
|
+
* `@<plan>` placeholder on purpose — this module is pure and doesn't know the
|
|
119
|
+
* saved path; the caller substitutes the real basename. Titles only, never ids.
|
|
116
120
|
*/
|
|
117
|
-
const banner = (reason,
|
|
121
|
+
const banner = (reason, nextTitle) => `\n■ STOPPED — ${reason}\n To continue: open CSPeach in this folder and choose Continue, or type /abap-plan --resume @<plan> (next: ${nextTitle})\n`;
|
|
118
122
|
export function decideNextPhaseGate(args) {
|
|
119
|
-
if (args.mode === 'step')
|
|
120
|
-
// Step mode is today's behaviour verbatim — a plain consent prompt with
|
|
121
|
-
// no guarded decorations (no banner, no stopReason).
|
|
123
|
+
if (args.mode === 'step')
|
|
122
124
|
return { action: 'prompt' };
|
|
123
|
-
|
|
124
|
-
// Guarded. Hard stop first: an unresolved prior audit must never be
|
|
125
|
-
// downgraded to a runnable prompt by the write-phase branch below.
|
|
126
|
-
if (!isAuditResolved(args.prevPhaseAudit)) {
|
|
127
|
-
if (args.prevPhaseAudit?.state === 'infra_failed') {
|
|
128
|
-
return {
|
|
129
|
-
action: 'stop',
|
|
130
|
-
stopReason: 'audit-infra',
|
|
131
|
-
banner: banner('auditor infrastructure failure — verdict unknown (not a code finding)', args.nextPhase.id),
|
|
132
|
-
};
|
|
133
|
-
}
|
|
125
|
+
if (!isAuditResolvedForRun(args.prevPhaseAudit)) {
|
|
134
126
|
return {
|
|
135
127
|
action: 'stop',
|
|
136
128
|
stopReason: 'audit-failed',
|
|
137
|
-
banner: banner('
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
if (phaseWrites(args.nextPhase)) {
|
|
141
|
-
// The prompt IS the stop: consent to run the write phase. Writes inside
|
|
142
|
-
// the phase still go through their own approval flow — this never
|
|
143
|
-
// pre-approves them.
|
|
144
|
-
return {
|
|
145
|
-
action: 'prompt',
|
|
146
|
-
stopReason: 'write-phase',
|
|
147
|
-
// Task 3 review carry-over: the banner's resume hint already names the
|
|
148
|
-
// phase (`next phase: <id>`), so the reason string stays generic —
|
|
149
|
-
// naming it twice read as a rendering bug.
|
|
150
|
-
banner: banner('next phase writes to SAP — consent required to run it', args.nextPhase.id),
|
|
129
|
+
banner: banner('an earlier step failed its independent check — fix it or waive it', args.nextPhase.title ?? 'the next step'),
|
|
151
130
|
};
|
|
152
131
|
}
|
|
153
132
|
return { action: 'auto' };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { isPhaseSatisfiedForRun } from '../projects/plan-run.js';
|
|
2
|
+
import { OBJECT_TYPE_LABELS, collectPlannedObjects, normalizeObjectName, normalizeObjectType } from '../projects/plan-objects.js';
|
|
3
|
+
import { recordedPlanTransport } from './plan-resume.js';
|
|
4
|
+
import { SAP_PAUSE_LINE } from './plan-sap-pause.js';
|
|
5
|
+
import { unreachableCode } from '../sap/unreachable.js';
|
|
6
|
+
import { unreachableCodeInText } from '../sap/reachability-state.js';
|
|
7
|
+
import { recordFullNotice } from '../renderer/notice-log.js';
|
|
8
|
+
const LOCK_WAIT_MS = 10_000;
|
|
9
|
+
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
10
|
+
/** Network code when SAP did not answer at all (an HTTP error means it answered). */
|
|
11
|
+
function unreachableOf(err) {
|
|
12
|
+
return unreachableCode(err) ?? unreachableCodeInText(String(err));
|
|
13
|
+
}
|
|
14
|
+
function errText(err) {
|
|
15
|
+
return err instanceof Error ? err.message : String(err);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Lock, then unlock at once. 'free' = the lock was taken (and released);
|
|
19
|
+
* 'held' = SAP refused the lock; 'unknown' = the probe itself failed (not a
|
|
20
|
+
* network failure) — never blocks the step. Unreachable SAP is rethrown.
|
|
21
|
+
*/
|
|
22
|
+
async function probeLock(deps, type, name) {
|
|
23
|
+
let l;
|
|
24
|
+
try {
|
|
25
|
+
l = await deps.lock(type, name);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (unreachableOf(err))
|
|
29
|
+
throw err;
|
|
30
|
+
recordFullNotice('plan', `Lock check before a plan step failed for ${type} ${name}: ${errText(err)}`);
|
|
31
|
+
return 'unknown';
|
|
32
|
+
}
|
|
33
|
+
if (l.status !== 'locked')
|
|
34
|
+
return 'held';
|
|
35
|
+
// Nothing runs between the lock and this unlock, so what was locked is
|
|
36
|
+
// always released (an unlock error is logged; the object still counts free).
|
|
37
|
+
try {
|
|
38
|
+
await deps.unlock(type, name, l.lockHandle);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
recordFullNotice('plan', `Unlock after a lock check failed for ${type} ${name}: ${errText(err)}`);
|
|
42
|
+
if (unreachableOf(err))
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
return 'free';
|
|
46
|
+
}
|
|
47
|
+
export async function regroundPlan(a) {
|
|
48
|
+
const tries = Math.max(1, a.lockWaitTries ?? 6);
|
|
49
|
+
const sleep = a.deps.sleep ?? defaultSleep;
|
|
50
|
+
// What finished steps of this plan built (their recorded object names).
|
|
51
|
+
const built = [];
|
|
52
|
+
for (const p of a.content.phases) {
|
|
53
|
+
if (!isPhaseSatisfiedForRun(a.statuses[p.id], p.audit))
|
|
54
|
+
continue;
|
|
55
|
+
for (const g of p.work?.generated ?? []) {
|
|
56
|
+
const n = normalizeObjectName(String(g ?? ''));
|
|
57
|
+
if (n && !built.includes(n))
|
|
58
|
+
built.push(n);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Which objects belong to this plan: the planned list (type + name) and the
|
|
62
|
+
// built names. A planned name only matches its planned type(s).
|
|
63
|
+
const plannedTypes = new Map();
|
|
64
|
+
for (const o of collectPlannedObjects(a.content)) {
|
|
65
|
+
const s = plannedTypes.get(o.name) ?? new Set();
|
|
66
|
+
s.add(o.type);
|
|
67
|
+
plannedTypes.set(o.name, s);
|
|
68
|
+
}
|
|
69
|
+
const ofThisPlan = (type, name) => {
|
|
70
|
+
if (!Object.hasOwn(OBJECT_TYPE_LABELS, type))
|
|
71
|
+
return false;
|
|
72
|
+
const types = plannedTypes.get(name);
|
|
73
|
+
if (types)
|
|
74
|
+
return types.has(type);
|
|
75
|
+
return built.includes(name);
|
|
76
|
+
};
|
|
77
|
+
const transport = recordedPlanTransport(a.content.phases);
|
|
78
|
+
try {
|
|
79
|
+
const inactive = [];
|
|
80
|
+
for (const raw of await a.deps.inactiveObjects()) {
|
|
81
|
+
const type = normalizeObjectType(raw.type);
|
|
82
|
+
const name = normalizeObjectName(raw.name);
|
|
83
|
+
if (!ofThisPlan(type, name))
|
|
84
|
+
continue;
|
|
85
|
+
if (inactive.some((o) => o.type === type && o.name === name))
|
|
86
|
+
continue;
|
|
87
|
+
inactive.push({ type, name });
|
|
88
|
+
}
|
|
89
|
+
const stillLocked = [];
|
|
90
|
+
for (const o of inactive) {
|
|
91
|
+
let state = await probeLock(a.deps, o.type, o.name);
|
|
92
|
+
for (let i = 1; i < tries && state === 'held'; i++) {
|
|
93
|
+
if (i === 1)
|
|
94
|
+
a.log?.(`${o.name} is locked by another session. Waiting up to a minute for SAP to free it.`);
|
|
95
|
+
await sleep(LOCK_WAIT_MS);
|
|
96
|
+
state = await probeLock(a.deps, o.type, o.name);
|
|
97
|
+
}
|
|
98
|
+
if (state === 'held')
|
|
99
|
+
stillLocked.push(o.name);
|
|
100
|
+
}
|
|
101
|
+
let transportClosed = false;
|
|
102
|
+
if (transport) {
|
|
103
|
+
const t = await a.deps.openTransports();
|
|
104
|
+
transportClosed = t.complete && !t.list.some((x) => x.trim().toUpperCase() === transport.trim().toUpperCase());
|
|
105
|
+
}
|
|
106
|
+
// An object left inactive gets the inactive instruction only, never the
|
|
107
|
+
// "do not rewrite" one as well.
|
|
108
|
+
const inactiveNames = new Set(inactive.map((o) => o.name));
|
|
109
|
+
const builtActive = built.filter((n) => !inactiveNames.has(n));
|
|
110
|
+
const block = [
|
|
111
|
+
'<sap_state>',
|
|
112
|
+
'Checked live in SAP before this step (authoritative — never repeat finished writes):',
|
|
113
|
+
...(builtActive.length > 0
|
|
114
|
+
? [`- Already built by earlier steps of this plan: ${builtActive.join(', ')}. Do not create or rewrite these; read them if this step needs them.`]
|
|
115
|
+
: built.length > 0 ? [] : ['- No earlier step of this plan has built SAP objects yet.']),
|
|
116
|
+
inactive.length > 0
|
|
117
|
+
? `- Left inactive by an interrupted run: ${inactive.map((o) => o.name).join(', ')}. Run the syntax check and activate them before anything else; rewrite one only if its check fails.`
|
|
118
|
+
: '- Nothing of this plan is left inactive.',
|
|
119
|
+
...(stillLocked.length > 0
|
|
120
|
+
? [`- Still locked by another session: ${stillLocked.join(', ')}. The developer chose to go on. If a write or activation meets the lock, stop and report it; do not retry in a loop.`]
|
|
121
|
+
: []),
|
|
122
|
+
'- If an object of THIS step already exists and is active, verify it (read it, check it) instead of writing it again.',
|
|
123
|
+
transport
|
|
124
|
+
? (transportClosed
|
|
125
|
+
? `- 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
|
+
: `- Transport ${transport} is still open: use it.`)
|
|
127
|
+
: '- No transport is recorded yet: the first writing step creates one.',
|
|
128
|
+
'</sap_state>',
|
|
129
|
+
];
|
|
130
|
+
if (stillLocked.length > 0) {
|
|
131
|
+
const one = stillLocked.length === 1;
|
|
132
|
+
return {
|
|
133
|
+
ok: false, reason: 'locked',
|
|
134
|
+
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
|
+
sapStateBlock: block,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const n = inactive.length;
|
|
139
|
+
const summary = `Checked SAP before continuing: ${n} object${n === 1 ? '' : 's'} left inactive`
|
|
140
|
+
+ (transport ? `, transport ${transport} ${transportClosed ? 'no longer open' : 'still open'}` : '') + '.';
|
|
141
|
+
return { ok: true, summary, quiet: n === 0 && !transportClosed, sapStateBlock: block };
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
const code = unreachableOf(err);
|
|
145
|
+
if (code) {
|
|
146
|
+
recordFullNotice('plan', `SAP unreachable before a plan step: ${code}`);
|
|
147
|
+
return { ok: false, reason: 'unreachable', lines: [SAP_PAUSE_LINE] };
|
|
148
|
+
}
|
|
149
|
+
// Any other SAP error never blocks: the step grounds itself (Mode 2 step 4).
|
|
150
|
+
recordFullNotice('plan', `SAP check before a plan step failed: ${errText(err)}`);
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
summary: 'Could not check SAP before continuing; the step checks it itself.',
|
|
154
|
+
quiet: false,
|
|
155
|
+
sapStateBlock: [
|
|
156
|
+
'<sap_state>',
|
|
157
|
+
'CSPeach could not read the SAP state before this step. Ground on the live system first (Mode 2 step 4) and never repeat a write an earlier step finished.',
|
|
158
|
+
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
|
+
'</sap_state>',
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export function makeRegroundDeps(adt) {
|
|
165
|
+
return {
|
|
166
|
+
inactiveObjects: () => adt.inactiveObjects(),
|
|
167
|
+
lock: (t, n) => adt.lock(t, n),
|
|
168
|
+
unlock: (t, n, h) => adt.unlock(t, n, h),
|
|
169
|
+
openTransports: async () => {
|
|
170
|
+
const r = await adt.transportList(undefined, 'modifiable');
|
|
171
|
+
return { list: r.transports.map((x) => x.transportNumber), complete: !r.note };
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|