@cspeach/cli 1.1.50 → 1.1.52
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 +30 -13
- package/dist/agent/inline-skill-block.js +341 -0
- package/dist/agent/loop.js +314 -44
- 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 +12 -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 +19 -7
- 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 +14 -0
- package/dist/approvals/plan-grant.js +8 -0
- package/dist/approvals/render.js +19 -8
- package/dist/approvals/risk-floor.js +32 -4
- package/dist/approvals/visible-changes.js +50 -0
- package/dist/cli.js +11 -3
- package/dist/commands/compact.js +12 -1
- package/dist/commands/config-set.js +14 -1
- package/dist/commands/config-show.js +16 -4
- package/dist/commands/export-audit.js +26 -3
- package/dist/commands/export-conversation.js +346 -0
- package/dist/commands/finish-check-guard.js +124 -0
- package/dist/commands/help.js +3 -0
- package/dist/commands/plan-boundary.js +5 -0
- package/dist/commands/plan-continue.js +11 -0
- package/dist/commands/plan-reground.js +27 -4
- package/dist/commands/plan-resume.js +111 -1
- package/dist/commands/plan-startup.js +5 -1
- 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 +9 -1
- package/dist/docx/markdown-to-docx.js +14 -1
- package/dist/lane/app-chain.js +24 -1
- package/dist/models/server-config.js +1 -1
- package/dist/projects/document-save.js +65 -8
- package/dist/projects/document-skills.js +32 -1
- package/dist/projects/save-command.js +5 -1
- package/dist/projects/yes-no.js +5 -1
- package/dist/renderer/change-card.js +3 -1
- package/dist/renderer/steering-echo.js +53 -8
- package/dist/renderer/tool-labels.js +3 -1
- package/dist/repl/builtin-commands.js +1 -1
- package/dist/repl/chat-lane.js +5 -2
- 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/slash-completer.js +4 -0
- package/dist/repl/turn-stop.js +64 -0
- package/dist/repl.js +178 -32
- 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/cli-record.js +165 -0
- package/dist/session/fresh-start.js +4 -0
- package/dist/session/queued-step.js +3 -1
- package/dist/session/user-prompt.js +10 -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 +37 -7
- 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/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 +16 -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 +11 -6
- package/dist/ui/ascii-chars.js +21 -3
- package/dist/ui/card-slot.js +18 -1
- 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/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 +1 -1
|
@@ -45,6 +45,7 @@ import { resolveAtTokenAsync, formatProjectFileList, ensureWorkspace } from '../
|
|
|
45
45
|
import { parsePlanContent, phaseWrites } from '../projects/plan-schema.js';
|
|
46
46
|
import { statusesFromItems, computeNextPhase, computeNextPhaseForRun, pendingAuditPhaseIds, renderPlanTracker, renderPlanProgressLine, buildPlanRevision, isPhaseSatisfied, isAuditResolved, planCompletionLines, findServiceBinding, phaseDisplayTitle, } from '../projects/plan-run.js';
|
|
47
47
|
import { extractPlan, NO_PLAN_MANIFEST_MESSAGE } from '../projects/extract-plan.js';
|
|
48
|
+
import { finishCheckGuard } from './finish-check-guard.js';
|
|
48
49
|
import { recordFullNotice } from '../renderer/notice-log.js';
|
|
49
50
|
import { isSapUnreachableNoted } from '../sap/reachability-state.js';
|
|
50
51
|
import { saveProject } from '../projects/save.js';
|
|
@@ -66,6 +67,8 @@ import { isCredentialHandoverApproved } from '../repl/credential-handover-gate.j
|
|
|
66
67
|
import { FIORI_DEPLOY_HANDOVER_SCOPE } from '../tools/fiori/deploy/scope.js';
|
|
67
68
|
import { resolvePlanSources, renderPlanSourcesBlock } from '../projects/plan-sources.js';
|
|
68
69
|
import { regroundPlan } from './plan-reground.js';
|
|
70
|
+
import { getGrantTransport, isPlanGrantInForceFor } from '../approvals/plan-grant.js';
|
|
71
|
+
import { projectTransport, transportForWrite } from '../register/project-transport.js';
|
|
69
72
|
import { PAUSED_LINE, makeSapProbe, pauseUntilSapReachable, promptLockedChoice, resumeCommandFor } from './plan-sap-pause.js';
|
|
70
73
|
/**
|
|
71
74
|
* Parse and STRIP the per-run chaining-mode (`--guarded` / `--step`) and
|
|
@@ -166,6 +169,51 @@ export function buildUiPhaseContext(phases, next) {
|
|
|
166
169
|
}
|
|
167
170
|
return lines;
|
|
168
171
|
}
|
|
172
|
+
const sameTransport = (a, b) => !!a && !!b && a.trim().toUpperCase() === b.trim().toUpperCase();
|
|
173
|
+
/**
|
|
174
|
+
* 2026-10-23 (owner-approved fix, review round 1) — the transport a create in
|
|
175
|
+
* this step would go to, by the write path's own order, so the step prompt can
|
|
176
|
+
* name it. Never the step's own work.transport ahead of that order.
|
|
177
|
+
* - A plan grant of THIS plan is in force: its transport (writes under the
|
|
178
|
+
* grant are held to it; a grant whose new transport is not bound yet names
|
|
179
|
+
* none).
|
|
180
|
+
* - Guarded mode, no grant yet: the grant offered for this step would bind
|
|
181
|
+
* the plan's recorded transport, else the session one; if it is not
|
|
182
|
+
* installed, writes take transportForWrite's order. Named only when the
|
|
183
|
+
* two agree.
|
|
184
|
+
* - Otherwise (step mode): transportForWrite for a create: the joined team
|
|
185
|
+
* project's request, else the session transport (an explicit /transport).
|
|
186
|
+
* Resolve-mode writes (set_source, update_method, delete) never fall back to
|
|
187
|
+
* the session transport, and an object's owning lock still wins over any
|
|
188
|
+
* transport passed. Pure.
|
|
189
|
+
*/
|
|
190
|
+
export function resolveStepWriteTransport(i) {
|
|
191
|
+
if (i.grantInstalled)
|
|
192
|
+
return i.grantTransport?.trim() || undefined;
|
|
193
|
+
const perWrite = transportForWrite({ op: 'create', project: i.project, session: i.session }).request;
|
|
194
|
+
if (i.mode !== 'guarded')
|
|
195
|
+
return perWrite || undefined;
|
|
196
|
+
const planned = recordedPlanTransport(i.phases) ?? i.session ?? undefined;
|
|
197
|
+
return sameTransport(planned, perWrite) ? perWrite : undefined;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The step-prompt line naming the transport. `transport` is passed only when
|
|
201
|
+
* it is the one the write path uses AND the pre-step SAP check listed it as
|
|
202
|
+
* open. Returns [] (prompt unchanged) with no transport, for read-only steps
|
|
203
|
+
* (writes:false), design steps, and UI/CAP steps (local files; a UI deploy
|
|
204
|
+
* names its own).
|
|
205
|
+
*/
|
|
206
|
+
export function buildStepTransportLine(next, transport) {
|
|
207
|
+
if (!transport)
|
|
208
|
+
return [];
|
|
209
|
+
if (!phaseWrites(next))
|
|
210
|
+
return [];
|
|
211
|
+
if (next.delegateTo === 'abap-design')
|
|
212
|
+
return [];
|
|
213
|
+
if (next.layer === 'ui' || next.layer.startsWith('cap.'))
|
|
214
|
+
return [];
|
|
215
|
+
return [`Transport for this step: ${transport} (already settled: use it, do not ask the developer which transport).`];
|
|
216
|
+
}
|
|
169
217
|
/** Compact-example work keys for ui phases — the write-back contract
|
|
170
218
|
* (review C3). Returns an OBJECT fragment because compactExample renders
|
|
171
219
|
* via JSON.stringify: it is spread into the `work` object exactly like the
|
|
@@ -528,17 +576,31 @@ export async function preparePlanResume(args) {
|
|
|
528
576
|
// what this plan built, what a dead run left inactive (a stale lock is
|
|
529
577
|
// waited out, then reported), and whether the transport is still open.
|
|
530
578
|
let sapStateBlock = [];
|
|
579
|
+
// 2026-10-23 — the transport this step's writes would use (write-path
|
|
580
|
+
// order). It is named in the prompt only when the re-check lists it open.
|
|
581
|
+
// A grant counts only while it is in force (this plan, not expired, approvals
|
|
582
|
+
// = plan); otherwise writes take the per-write order, and so does the line.
|
|
583
|
+
const stepWriteTransport = resolveStepWriteTransport({
|
|
584
|
+
phases: content.phases,
|
|
585
|
+
mode: effectiveMode,
|
|
586
|
+
grantInstalled: isPlanGrantInForceFor(envelope.id),
|
|
587
|
+
grantTransport: getGrantTransport(),
|
|
588
|
+
project: projectTransport(args.cwd),
|
|
589
|
+
session: getCurrentTransport(),
|
|
590
|
+
});
|
|
591
|
+
let stepTransportOpen = false;
|
|
531
592
|
if (args.reground) {
|
|
532
593
|
const regroundDeps = args.reground;
|
|
533
594
|
const canAsk = args.interactive === true && !!args.prompt;
|
|
534
595
|
const resumeCommand = resumeCommandFor({ path, mode: flags.mode, auditFlag: flags.audit });
|
|
535
596
|
for (;;) {
|
|
536
|
-
const r = await regroundPlan({ content, statuses, deps: regroundDeps, log: args.log });
|
|
597
|
+
const r = await regroundPlan({ content, statuses, deps: regroundDeps, log: args.log, ...(stepWriteTransport ? { stepTransport: stepWriteTransport } : {}) });
|
|
537
598
|
if (r.ok) {
|
|
538
599
|
// An in-run hop stays quiet unless something needs attention.
|
|
539
600
|
if (!r.quiet || args.boardStyle !== 'line')
|
|
540
601
|
args.log(formatInfo(r.summary).trimEnd());
|
|
541
602
|
sapStateBlock = r.sapStateBlock;
|
|
603
|
+
stepTransportOpen = r.stepTransportOpen === true;
|
|
542
604
|
break;
|
|
543
605
|
}
|
|
544
606
|
if (r.reason === 'unreachable') {
|
|
@@ -562,6 +624,7 @@ export async function preparePlanResume(args) {
|
|
|
562
624
|
continue;
|
|
563
625
|
if (choice === 'continue') {
|
|
564
626
|
sapStateBlock = r.sapStateBlock;
|
|
627
|
+
stepTransportOpen = r.stepTransportOpen === true;
|
|
565
628
|
break;
|
|
566
629
|
}
|
|
567
630
|
args.log(PAUSED_LINE);
|
|
@@ -631,6 +694,10 @@ export async function preparePlanResume(args) {
|
|
|
631
694
|
// '' already ends the prior element). Spread the blank AFTER the block so
|
|
632
695
|
// context keeps single-blank separation on both sides.
|
|
633
696
|
const uiContext = buildUiPhaseContext(content.phases, next);
|
|
697
|
+
// Named only when it is the transport the write uses, it is listed open, and
|
|
698
|
+
// it does not contradict the plan's recorded transport in <sap_state>.
|
|
699
|
+
const recordedTr = recordedPlanTransport(content.phases);
|
|
700
|
+
const stepTransport = buildStepTransportLine(next, stepTransportOpen && (!recordedTr || sameTransport(recordedTr, stepWriteTransport)) ? stepWriteTransport : undefined);
|
|
634
701
|
// 2026-10-04 (plan sources) — the spec-gap items this phase reads, in full,
|
|
635
702
|
// from the file the plan was made from (or the plan's creation snapshot).
|
|
636
703
|
// A phase must never have to hunt the folder for its gap answers.
|
|
@@ -671,6 +738,7 @@ export async function preparePlanResume(args) {
|
|
|
671
738
|
'Only if the plan itself must change structurally (a phase added, removed, or re-sequenced) fall back to the full shape with "content" — CSPeach accepts both. The manifest block is how the result is persisted; a turn that ends without it LOSES the phase.',
|
|
672
739
|
'',
|
|
673
740
|
...(uiContext.length ? [...uiContext, ''] : []),
|
|
741
|
+
...(stepTransport.length ? [...stepTransport, ''] : []),
|
|
674
742
|
PLAN_RESUME_HARNESS_OVERRIDE,
|
|
675
743
|
].join('\n');
|
|
676
744
|
return {
|
|
@@ -935,6 +1003,12 @@ export async function finishPlanResume(args) {
|
|
|
935
1003
|
return null;
|
|
936
1004
|
}
|
|
937
1005
|
}
|
|
1006
|
+
// 2026-10-23 — finish-check guard (inline-waiver findings §6 Option A): a
|
|
1007
|
+
// step claimed "validated" while this turn's LAST rap_finish_check is below
|
|
1008
|
+
// 5 of 5 is saved "blocked" (a waiver only when its notes name every failed
|
|
1009
|
+
// item). Downgrade, never refuse. No check this turn, a 5 of 5, an
|
|
1010
|
+
// unreadable result or any other status → saved exactly as written.
|
|
1011
|
+
applyFinishCheckGuard(extract, args);
|
|
938
1012
|
const nowIso = new Date().toISOString();
|
|
939
1013
|
const statuses = extract.statuses;
|
|
940
1014
|
const executedId = args.prepared.nextPhaseId;
|
|
@@ -1056,6 +1130,42 @@ export async function finishPlanResume(args) {
|
|
|
1056
1130
|
nextTitle: next ? phaseDisplayTitle(next, extract.content.phases) : null,
|
|
1057
1131
|
};
|
|
1058
1132
|
}
|
|
1133
|
+
/**
|
|
1134
|
+
* The finish-check guard's write: when finishCheckGuard rules the executed
|
|
1135
|
+
* step down, set it to 'blocked' in the status map, the item list and the
|
|
1136
|
+
* summary, append the note to its work.notes and log the same line.
|
|
1137
|
+
*/
|
|
1138
|
+
function applyFinishCheckGuard(extract, args) {
|
|
1139
|
+
const executedId = args.prepared.nextPhaseId;
|
|
1140
|
+
const executed = extract.content.phases.find((p) => p.id === executedId);
|
|
1141
|
+
if (!executed)
|
|
1142
|
+
return;
|
|
1143
|
+
const ruling = finishCheckGuard({
|
|
1144
|
+
messages: args.messages,
|
|
1145
|
+
messagesStart: args.messagesStart,
|
|
1146
|
+
status: extract.statuses[executedId],
|
|
1147
|
+
step: phaseDisplayTitle(executed, extract.content.phases),
|
|
1148
|
+
notes: executed.work?.notes,
|
|
1149
|
+
});
|
|
1150
|
+
if (!ruling)
|
|
1151
|
+
return;
|
|
1152
|
+
extract.statuses[executedId] = ruling.status;
|
|
1153
|
+
for (const it of extract.items)
|
|
1154
|
+
if (it.id === executedId)
|
|
1155
|
+
it.status = ruling.status;
|
|
1156
|
+
const prevNotes = executed.work?.notes;
|
|
1157
|
+
executed.work = { ...(executed.work ?? {}), notes: prevNotes ? `${prevNotes}\n${ruling.note}` : ruling.note };
|
|
1158
|
+
const phases = extract.content.phases;
|
|
1159
|
+
const next = computeNextPhase(phases, extract.statuses);
|
|
1160
|
+
extract.content.summary = {
|
|
1161
|
+
...extract.content.summary,
|
|
1162
|
+
validated: phases.filter((p) => isPhaseSatisfied(extract.statuses[p.id], p.audit)).length,
|
|
1163
|
+
blocked: phases.filter((p) => extract.statuses[p.id] === 'blocked').length,
|
|
1164
|
+
next: next?.id ?? null,
|
|
1165
|
+
};
|
|
1166
|
+
args.log('');
|
|
1167
|
+
args.log(ruling.note);
|
|
1168
|
+
}
|
|
1059
1169
|
/**
|
|
1060
1170
|
* Final fix wave, item 5 — save a new plan version beside the plan file. When
|
|
1061
1171
|
* that write fails (a read-only or missing folder), save it in the working
|
|
@@ -20,6 +20,7 @@ import { computeNextPhaseForRun, isPhaseSatisfiedForRun, phaseDisplayTitle, stat
|
|
|
20
20
|
import { collectPlannedObjects, plannedObjectFacts, planGrantFactLines } from '../projects/plan-objects.js';
|
|
21
21
|
import { adoptPlanTransportOnResume, buildResumeCommand, planBuildTransport } from './plan-resume.js';
|
|
22
22
|
import { newestPlanFamilyFiles } from '../session/recap.js';
|
|
23
|
+
import { recordCliCard } from '../session/cli-record.js';
|
|
23
24
|
import { legacyDirsFor } from '../projects/project-folder.js';
|
|
24
25
|
import { getPlanGrant, installPlanGrant, planGrantAllowed } from '../approvals/plan-grant.js';
|
|
25
26
|
import { DEFAULT_PLAN_MODE } from './plan-gate.js';
|
|
@@ -256,6 +257,9 @@ export function classicStartupChooser(prompt, log) {
|
|
|
256
257
|
log('', q.title, ...q.facts.map((f) => ` ${f}`), ...q.choices.map((c, i) => ` ${i + 1}. ${c.label}`));
|
|
257
258
|
const ans = (await prompt(`Choose 1-${q.choices.length} (Enter = ${q.choices[0].label}): `)).trim();
|
|
258
259
|
const n = ans === '' ? 1 : Number(ans);
|
|
259
|
-
|
|
260
|
+
const picked = Number.isInteger(n) && n >= 1 && n <= q.choices.length ? q.choices[n - 1] : null;
|
|
261
|
+
// /export docx conversation: the classic startup choice is recorded like the Ink card.
|
|
262
|
+
recordCliCard({ question: q.title, options: q.choices.map((c) => c.label), ...(q.facts.length ? { facts: [...q.facts] } : {}), answer: picked?.label ?? 'Cancelled', by: 'user' });
|
|
263
|
+
return picked ? picked.value : null;
|
|
260
264
|
};
|
|
261
265
|
}
|
package/dist/config/loader.js
CHANGED
|
@@ -45,7 +45,8 @@ export const DEFAULT_CONFIG = {
|
|
|
45
45
|
// (staged rollout, owner 2026-10-17; users opt in with /chat on, a later
|
|
46
46
|
// release flips this); 'professional' sends no style; 'developer' sends no
|
|
47
47
|
// level (§10 D13).
|
|
48
|
-
|
|
48
|
+
// Stage 1A — 'system' is today's request; 'inline' is opt-in (config set).
|
|
49
|
+
assistant: { lane: 'skills', style: DEFAULT_STYLE, level: DEFAULT_LEVEL, skills: 'system' },
|
|
49
50
|
// Default `approval-gated` — preserves current UX (user approves each write).
|
|
50
51
|
write_mode: 'approval-gated',
|
|
51
52
|
// Default `managed` — lowest-friction entry point; uses the cspeach.dev proxy.
|
|
@@ -321,6 +322,9 @@ async function readConfig() {
|
|
|
321
322
|
merged.assistant.lane = DEFAULT_CONFIG.assistant.lane;
|
|
322
323
|
merged.assistant.style = parseStyle(merged.assistant.style) ?? DEFAULT_CONFIG.assistant.style;
|
|
323
324
|
merged.assistant.level = parseLevel(merged.assistant.level) ?? DEFAULT_CONFIG.assistant.level;
|
|
325
|
+
// Stage 1A — unknown values fall back, like the lane.
|
|
326
|
+
if (merged.assistant.skills !== 'system' && merged.assistant.skills !== 'inline')
|
|
327
|
+
merged.assistant.skills = DEFAULT_CONFIG.assistant.skills;
|
|
324
328
|
if (merged.ui.background !== undefined && merged.ui.background !== 'dark' && merged.ui.background !== 'light') {
|
|
325
329
|
delete merged.ui.background;
|
|
326
330
|
}
|
package/dist/cost/cost-log.js
CHANGED
|
@@ -87,6 +87,12 @@ export function buildEntry(opts) {
|
|
|
87
87
|
entry.skill = opts.skill;
|
|
88
88
|
if (opts.system_block !== undefined)
|
|
89
89
|
entry.system_block = opts.system_block;
|
|
90
|
+
if (opts.system_mode !== undefined)
|
|
91
|
+
entry.system_mode = opts.system_mode;
|
|
92
|
+
if (opts.skill_appended !== undefined && opts.skill_appended.length > 0)
|
|
93
|
+
entry.skill_appended = opts.skill_appended;
|
|
94
|
+
if (opts.tools_surfaced !== undefined && opts.tools_surfaced.length > 0)
|
|
95
|
+
entry.tools_surfaced = opts.tools_surfaced;
|
|
90
96
|
return entry;
|
|
91
97
|
}
|
|
92
98
|
/**
|
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
* Kept in a module WeakMap, never in the session file (no schema change). A
|
|
12
12
|
* resumed session therefore starts at 'first' again.
|
|
13
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* Stage 1A, Task 1A.2 (M2) — the skill part of the key on an inline call: the
|
|
16
|
+
* system block is the one conversation prompt whatever the skill header says,
|
|
17
|
+
* and no delegate is composed into it (the delegate rides in the conversation).
|
|
18
|
+
*/
|
|
19
|
+
export const INLINE_SYSTEM_BLOCK_SKILL = '_inline';
|
|
14
20
|
export function systemBlockKey(p) {
|
|
15
21
|
return `${p.skill}|${p.delegateSkill ?? ''}|${p.style}|${p.level}`;
|
|
16
22
|
}
|
package/dist/docx/doc-meta.js
CHANGED
|
@@ -3,7 +3,15 @@
|
|
|
3
3
|
// The metadata a Word copy carries on its title page. Filled by the caller
|
|
4
4
|
// (the save hook knows the session's SAP alias; the export command knows the
|
|
5
5
|
// file) and, for what the text itself says, by metaFromMarkdown (Task 3).
|
|
6
|
-
const KIND_TITLE = {
|
|
6
|
+
const KIND_TITLE = {
|
|
7
|
+
review: 'Code review', handover: 'Handover', document: 'Document',
|
|
8
|
+
explanation: 'Explanation', impact: 'Impact analysis', preflight: 'Preflight check',
|
|
9
|
+
'transport-analysis': 'Transport analysis', migration: 'Migration assessment',
|
|
10
|
+
'clean-core': 'Clean-core assessment', 'custom-code-analysis': 'Custom code analysis',
|
|
11
|
+
'spec-gap': 'Spec gaps', design: 'Design', estimate: 'Estimate', plan: 'Plan',
|
|
12
|
+
'upgrade-scan': 'Upgrade scan', 'upgrade-report': 'Upgrade report',
|
|
13
|
+
answer: 'CSPeach answer', conversation: 'CSPeach conversation',
|
|
14
|
+
};
|
|
7
15
|
/** Z/Y objects, SAP-style names: ZCL_X, ZFI_DUP_INVOICE_CHECK, Z_REPORT, /NS/ZCL_X. */
|
|
8
16
|
const OBJECT_RE = /(?<![\w/])((?:\/[A-Z0-9]+\/)?[ZY][A-Z0-9_]{2,39})\b/;
|
|
9
17
|
/** Inline Markdown → plain words: `code` → code, [text](url) → text, **x** → x. */
|
|
@@ -63,6 +63,10 @@ function titlePage(meta) {
|
|
|
63
63
|
facts.push(`System: ${meta.system}`);
|
|
64
64
|
if (meta.object)
|
|
65
65
|
facts.push(`Object: ${meta.object}`);
|
|
66
|
+
if (meta.session)
|
|
67
|
+
facts.push(`Session: ${meta.session}`);
|
|
68
|
+
if (meta.person)
|
|
69
|
+
facts.push(`Signed in as: ${meta.person}`);
|
|
66
70
|
return [
|
|
67
71
|
new Paragraph({ text: meta.title, heading: HeadingLevel.TITLE, spacing: { before: 2400 } }),
|
|
68
72
|
...facts.map((f) => new Paragraph({ text: f })),
|
|
@@ -120,18 +124,27 @@ function list(l, ctx, depth) {
|
|
|
120
124
|
const mark = l.ordered
|
|
121
125
|
? { numbering: { reference: NUMBERED, level, instance } }
|
|
122
126
|
: { bullet: { level } };
|
|
127
|
+
// Task-list items (`- [ ]` / `- [x]`) keep their box at the start of the item (2026-10-23).
|
|
128
|
+
const box = item.task ? (item.checked ? '☒ ' : '☐ ') : '';
|
|
123
129
|
let first = true;
|
|
124
130
|
for (const child of item.tokens) {
|
|
125
131
|
if (child.type === 'list') {
|
|
126
132
|
out.push(...list(child, ctx, depth + 1));
|
|
127
133
|
continue;
|
|
128
134
|
}
|
|
135
|
+
if (first && box && (child.type === 'text' || child.type === 'paragraph')) {
|
|
136
|
+
const p = child;
|
|
137
|
+
const kids = 'tokens' in p && p.tokens ? inline(p.tokens) : [new TextRun(p.text ?? p.raw ?? '')];
|
|
138
|
+
out.push(new Paragraph({ ...mark, children: [new TextRun(box), ...kids] }));
|
|
139
|
+
first = false;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
129
142
|
// The first text block of an item carries the marker; later paragraphs are indented plain text.
|
|
130
143
|
out.push(...blockToDocx(child, ctx, depth, first ? mark : { indent: { left: 720 * (level + 1) } }));
|
|
131
144
|
first = false;
|
|
132
145
|
}
|
|
133
146
|
if (first)
|
|
134
|
-
out.push(new Paragraph({ ...mark, text: item.text ?? '' }));
|
|
147
|
+
out.push(new Paragraph({ ...mark, text: box + (item.text ?? '') }));
|
|
135
148
|
}
|
|
136
149
|
return out;
|
|
137
150
|
}
|
package/dist/lane/app-chain.js
CHANGED
|
@@ -30,6 +30,7 @@ import { containSpecPath, specDisplayPath, SpecProtectedError } from './roots.js
|
|
|
30
30
|
import { resolveAtTokenAsync } from '../projects/workspace.js';
|
|
31
31
|
import { parseFromFlag } from '../skills/promotion-dispatch.js';
|
|
32
32
|
import { rebuildStartedAfterPlanSave } from '../journey/rebuild.js';
|
|
33
|
+
import { recordCliCard } from '../session/cli-record.js';
|
|
33
34
|
export const STEP_1 = 'Step 1 of 3: finding the gaps in the spec.';
|
|
34
35
|
export const STEP_2 = 'Step 2 of 3: choosing the build lane.';
|
|
35
36
|
export const STEP_3 = 'Step 3 of 3: planning the build.';
|
|
@@ -239,6 +240,10 @@ function readSavedPlan(savedPath) {
|
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
const OBJECT_LIST_FAILED = 'The object list could not be approved. Each SAP write asks first.';
|
|
243
|
+
/** The plan a "Build this now?" record is about: the plan file name and its version. */
|
|
244
|
+
function planRef(savedPath, version) {
|
|
245
|
+
return `${savedPath.split(/[\\/]/).pop() ?? savedPath} v${version}`;
|
|
246
|
+
}
|
|
242
247
|
/**
|
|
243
248
|
* The facts under "Build this now?": steps, the exact object list, transport,
|
|
244
249
|
* deploy, what stops. `grant: false` (the object-list approval cannot be
|
|
@@ -367,6 +372,9 @@ export async function afterPlanSave(h, savedPath) {
|
|
|
367
372
|
return;
|
|
368
373
|
}
|
|
369
374
|
if (h.headless || !h.queueNext) {
|
|
375
|
+
// /export docx conversation: the plan gate was not shown; say so in the record.
|
|
376
|
+
if (h.headless)
|
|
377
|
+
recordCliCard({ question: START_QUESTION, ref: planRef(savedPath, saved.envelope.version), answer: 'Not asked (no one at the terminal)', by: 'headless' }, h.session);
|
|
370
378
|
h.emit(`Start building with: ${cmd}`);
|
|
371
379
|
end();
|
|
372
380
|
return;
|
|
@@ -389,27 +397,42 @@ export async function afterPlanSave(h, savedPath) {
|
|
|
389
397
|
initial: 0,
|
|
390
398
|
aliases: { y: 'yes', n: 'no' },
|
|
391
399
|
});
|
|
400
|
+
// /export docx conversation (fix round 2, N-1): the record a change ticket
|
|
401
|
+
// needs — when, which plan file and version, the objects and transport shown,
|
|
402
|
+
// the answer, and whether the object-list approval was installed. Only when
|
|
403
|
+
// a card was really shown (an injected confirm, or a card listener).
|
|
404
|
+
const shown = h.confirm !== undefined || (await import('../ui/card-slot.js')).cardSlotEmitter.listenerCount('confirm') > 0;
|
|
405
|
+
const record = (answer) => {
|
|
406
|
+
if (shown)
|
|
407
|
+
recordCliCard({ question: START_QUESTION, ref: planRef(savedPath, saved.envelope.version), options: ['Yes', 'No'], facts: [...facts], answer, by: 'user' }, h.session);
|
|
408
|
+
};
|
|
392
409
|
if (res.value === 'yes' && !res.cancelled) {
|
|
393
410
|
// The plan's recorded transport becomes the session transport when the
|
|
394
411
|
// session has none, so the first step's writes land on (and are covered
|
|
395
412
|
// by) it without the model naming it — the same rule a resume uses.
|
|
396
413
|
adoptPlanTransportOnResume(saved.content.phases);
|
|
397
414
|
const sessionId = h.session.id;
|
|
415
|
+
let installed = true;
|
|
398
416
|
if (grantable && objects.length > 0 && typeof sessionId === 'string' && sessionId) {
|
|
399
417
|
const a = { planId: saved.envelope.id, sessionId, objects, ...(transport ? { transport } : {}) };
|
|
400
418
|
try {
|
|
401
419
|
if (h.grant)
|
|
402
420
|
await h.grant(a);
|
|
403
|
-
else if (!(await installPlanGrant({ ...a, ...(sapAlias ? { sapAlias } : {}) })))
|
|
421
|
+
else if (!(await installPlanGrant({ ...a, ...(sapAlias ? { sapAlias } : {}) }))) {
|
|
422
|
+
installed = false;
|
|
404
423
|
h.emit(OBJECT_LIST_FAILED);
|
|
424
|
+
}
|
|
405
425
|
}
|
|
406
426
|
catch {
|
|
427
|
+
installed = false;
|
|
407
428
|
h.emit(OBJECT_LIST_FAILED);
|
|
408
429
|
}
|
|
409
430
|
}
|
|
431
|
+
record(installed ? 'Yes' : 'Yes (object list not installed; each SAP write asks first)');
|
|
410
432
|
h.queueNext(cmd, APP_ORIGIN);
|
|
411
433
|
}
|
|
412
434
|
else {
|
|
435
|
+
record(res.value === 'no' ? 'No' : 'Cancelled');
|
|
413
436
|
h.emit(`Start it later with: ${cmd}`);
|
|
414
437
|
}
|
|
415
438
|
end();
|
|
@@ -284,7 +284,7 @@ export function managedProxyServesNewShapes() {
|
|
|
284
284
|
* conversation prompt? True only when its served config says
|
|
285
285
|
* `inline_skills: true`; false with no snapshot or an older proxy. Managed
|
|
286
286
|
* snapshot only: BYOK builds its own system prompt and never needs it.
|
|
287
|
-
*
|
|
287
|
+
* Read only by repl/inline-mode.ts `inlineServed` (Stage 1A; pinned by
|
|
288
288
|
* __tests__/served-inline-skills-pin.test.ts). Pure sync read.
|
|
289
289
|
*/
|
|
290
290
|
export function servedInlineSkills() {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// cspeach-cli/src/projects/document-save.ts
|
|
2
2
|
//
|
|
3
|
-
// The save for the
|
|
4
|
-
//
|
|
3
|
+
// The save for the document skills (2026-10-19, owner decision 2026-10-17;
|
|
4
|
+
// widened 2026-10-23, Word copy everywhere — see document-skills.ts).
|
|
5
|
+
// A review, a handover, an explanation, … is prose, not an envelope: it goes to
|
|
5
6
|
// <cwd>/cspeach-out/<date>-<kind>-<OBJECT>.md (the folder the model already
|
|
6
7
|
// writes extra copies to; skipped by the @-picker; exempt from the Rule 8
|
|
7
8
|
// card). Then one question, "Word copy too?", unless `word_copy` decides.
|
|
@@ -13,8 +14,9 @@ import { askYesNo, isYes } from './yes-no.js';
|
|
|
13
14
|
import { metaFromMarkdown } from '../docx/doc-meta.js';
|
|
14
15
|
import { writeDocxNextTo, writeNoClobber } from '../docx/write-docx.js';
|
|
15
16
|
import { safeFileStem } from '../register/xlsx.js';
|
|
16
|
-
import { DOCUMENT_SKILLS } from './document-skills.js';
|
|
17
|
-
|
|
17
|
+
import { DOCUMENT_SKILLS, WORD_AFTER_ARTIFACT_SAVE } from './document-skills.js';
|
|
18
|
+
import { handoverPathFor } from './handover-md.js';
|
|
19
|
+
export { DOCUMENT_SKILLS, WORD_AFTER_ARTIFACT_SAVE };
|
|
18
20
|
export const OUT_DIR = 'cspeach-out';
|
|
19
21
|
export function documentFilename(kind, object, date) {
|
|
20
22
|
const stem = safeFileStem((object ?? 'document').replace(/\//g, ''));
|
|
@@ -44,19 +46,74 @@ export async function offerDocumentSave(p) {
|
|
|
44
46
|
return {};
|
|
45
47
|
}
|
|
46
48
|
p.emit(`Saved: ${path.resolve(mdPath)}`);
|
|
49
|
+
const docxPath = await offerWordCopy({
|
|
50
|
+
// Fix round 1 (m-1): the newer document skills' Word copy leaves out HTML comments
|
|
51
|
+
// (widget markers); review and handover pass their text exactly as before.
|
|
52
|
+
nextTo: mdPath, markdown: skill.yieldsToArtifact ? p.markdown.replace(/<!--[\s\S]*?-->\s*/g, '') : p.markdown, meta, headless: p.headless, wordCopy: p.wordCopy, confirm: p.confirm, emit: p.emit,
|
|
53
|
+
});
|
|
54
|
+
return docxPath ? { mdPath, docxPath } : { mdPath };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The Word step, shared by every save of a document (Word copy everywhere,
|
|
58
|
+
* 2026-10-23). `word_copy` decides: unset → ask "Word copy too?", on →
|
|
59
|
+
* always, off → never. Headless never asks: the copy is written only when
|
|
60
|
+
* `word_copy` is on. Never overwrites, never throws: a failure is one line.
|
|
61
|
+
* Returns the .docx path when one was written.
|
|
62
|
+
*/
|
|
63
|
+
export async function offerWordCopy(p) {
|
|
47
64
|
let wantWord = p.wordCopy === 'always';
|
|
48
65
|
if (p.wordCopy === 'ask' && !p.headless) {
|
|
49
66
|
wantWord = isYes(await askYesNo({ question: 'Word copy too?', confirm: p.confirm }));
|
|
50
67
|
}
|
|
51
68
|
if (!wantWord)
|
|
52
|
-
return
|
|
69
|
+
return undefined;
|
|
53
70
|
try {
|
|
54
|
-
const docxPath = await writeDocxNextTo(
|
|
71
|
+
const docxPath = await writeDocxNextTo(p.nextTo, p.markdown, p.meta);
|
|
55
72
|
p.emit(`Word copy: ${path.resolve(docxPath)}`);
|
|
56
|
-
return
|
|
73
|
+
return docxPath;
|
|
57
74
|
}
|
|
58
75
|
catch (e) {
|
|
59
76
|
p.emit(`Word copy not written. ${e.message}`);
|
|
60
|
-
return
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Artefact blocks (`<!-- cspeach:…-manifest … -->`, legacy `csforge:` too) are for CSPeach, not for the reader. */
|
|
81
|
+
const ARTIFACT_BLOCK_RE = /<!--\s*(?:csforge|cspeach):[\w-]+\s*\n[\s\S]*?-->\s*/g;
|
|
82
|
+
/** The YAML header at the top of a plan's handover Markdown. */
|
|
83
|
+
const FRONT_MATTER_RE = /^---\r?\n[\s\S]*?\r?\n---\r?\n/;
|
|
84
|
+
/** The text a person reads: no artefact blocks; the YAML header only for a plan handover (final-review m-3). */
|
|
85
|
+
export function readerMarkdown(markdown, frontMatter = true) {
|
|
86
|
+
return (frontMatter ? markdown.replace(FRONT_MATTER_RE, '') : markdown).replace(ARTIFACT_BLOCK_RE, '').trim() + '\n';
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The Word step after an artefact save ("Save as project file?" said Yes).
|
|
90
|
+
* The project file is the source of truth and is never touched here; the
|
|
91
|
+
* .docx is a copy for people and is never read back.
|
|
92
|
+
* - a plan: the copy is made from the plan's handover Markdown, next to it
|
|
93
|
+
* (that file is written by the plan save, exactly as before);
|
|
94
|
+
* - the others: from the answer, into cspeach-out/<date>-<kind>-<OBJECT>.docx.
|
|
95
|
+
* Skills not in WORD_AFTER_ARTIFACT_SAVE: nothing.
|
|
96
|
+
*/
|
|
97
|
+
export async function offerWordCopyAfterArtifactSave(p) {
|
|
98
|
+
const kind = WORD_AFTER_ARTIFACT_SAVE[p.saveSkill];
|
|
99
|
+
if (!kind)
|
|
100
|
+
return undefined;
|
|
101
|
+
const handover = kind === 'plan' ? handoverPathFor(p.savedPath) : null;
|
|
102
|
+
let source = p.markdown;
|
|
103
|
+
if (handover) {
|
|
104
|
+
try {
|
|
105
|
+
source = await fs.promises.readFile(handover, 'utf8');
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return undefined; // the plan save already said the handover was not written
|
|
109
|
+
}
|
|
61
110
|
}
|
|
111
|
+
const markdown = readerMarkdown(source, handover !== null);
|
|
112
|
+
if (markdown.trim().length === 0)
|
|
113
|
+
return undefined;
|
|
114
|
+
const meta = metaFromMarkdown(markdown, { date: p.today, kind, ...(p.sapSystem ? { system: p.sapSystem } : {}) });
|
|
115
|
+
const nextTo = handover ?? path.join(p.cwd, OUT_DIR, documentFilename(kind, meta.object, p.today));
|
|
116
|
+
return offerWordCopy({
|
|
117
|
+
nextTo, markdown, meta, headless: p.headless, wordCopy: p.wordCopy, confirm: p.confirm, emit: p.emit,
|
|
118
|
+
});
|
|
62
119
|
}
|
|
@@ -1,9 +1,40 @@
|
|
|
1
1
|
// cspeach-cli/src/projects/document-skills.ts
|
|
2
2
|
//
|
|
3
|
-
// The
|
|
3
|
+
// The document skills, on their own so the agent loop can check a skill
|
|
4
4
|
// name without loading document-save.ts (and the docx package behind it) on
|
|
5
5
|
// every turn. Type-only import: nothing heavy loads from here.
|
|
6
|
+
//
|
|
7
|
+
// Word copy everywhere (2026-10-23, owner: "add this word feature everywhere"):
|
|
8
|
+
// - DOCUMENT_SKILLS: skills whose answer is a document a person keeps. After
|
|
9
|
+
// the turn: "Save this <noun> to a file?" (cspeach-out/<date>-<kind>-<OBJECT>.md),
|
|
10
|
+
// then the Word step. Review and handover were the first two (2026-10-19);
|
|
11
|
+
// the others join with `yieldsToArtifact`: when their answer carries a
|
|
12
|
+
// recognizable artefact block (a plan, a clean-core triage, …) the artefact
|
|
13
|
+
// save runs instead, exactly as before.
|
|
14
|
+
// - WORD_AFTER_ARTIFACT_SAVE: artefact saves (a .cspeach.json project file)
|
|
15
|
+
// whose answer is also a document. After "Save as project file?" says Yes,
|
|
16
|
+
// the same Word step runs. The project file and its save do not change.
|
|
6
17
|
export const DOCUMENT_SKILLS = {
|
|
7
18
|
'abap-review': { kind: 'review', noun: 'review' },
|
|
8
19
|
'abap-handover': { kind: 'handover', noun: 'handover' },
|
|
20
|
+
'abap-explain': { kind: 'explanation', noun: 'explanation', yieldsToArtifact: true },
|
|
21
|
+
'abap-impact': { kind: 'impact', noun: 'impact analysis', yieldsToArtifact: true },
|
|
22
|
+
'abap-preflight': { kind: 'preflight', noun: 'preflight check', yieldsToArtifact: true },
|
|
23
|
+
'abap-transport-analysis': { kind: 'transport-analysis', noun: 'transport analysis', yieldsToArtifact: true },
|
|
24
|
+
'abap-migrate': { kind: 'migration', noun: 'migration assessment', yieldsToArtifact: true },
|
|
25
|
+
'abap-clean-core': { kind: 'clean-core', noun: 'clean-core assessment', yieldsToArtifact: true },
|
|
26
|
+
'abap-cca': { kind: 'custom-code-analysis', noun: 'custom code analysis', yieldsToArtifact: true },
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Artefact saves that also get the Word step (skill name as saved → kind).
|
|
30
|
+
* A plan's Word copy is made from its handover Markdown (written next to the
|
|
31
|
+
* plan file); the others from the answer the person read.
|
|
32
|
+
*/
|
|
33
|
+
export const WORD_AFTER_ARTIFACT_SAVE = {
|
|
34
|
+
'abap-spec-gap': 'spec-gap',
|
|
35
|
+
'abap-design': 'design',
|
|
36
|
+
'abap-estimate': 'estimate',
|
|
37
|
+
'abap-plan': 'plan',
|
|
38
|
+
'abap-upgrade-scan': 'upgrade-scan',
|
|
39
|
+
'abap-upgrade-verify': 'upgrade-report',
|
|
9
40
|
};
|
|
@@ -398,7 +398,11 @@ export async function runSaveCommand(args) {
|
|
|
398
398
|
// maybeOfferSave never does headless — so the blanket 'y' never reaches it.)
|
|
399
399
|
// Inside a journey a triage is saved without the question: the journey needs the rows.
|
|
400
400
|
const journey = spec.artefactType === 'clean-core-decision' ? args.journey : undefined;
|
|
401
|
-
const answer = journey ? 'y' : await askYesNo({
|
|
401
|
+
const answer = journey ? 'y' : await askYesNo({
|
|
402
|
+
question: 'Save as project file?', confirm: args.confirm, prompt: args.prompt,
|
|
403
|
+
// F-A (live check 2026-10-26) — a plan is only built from its saved file.
|
|
404
|
+
...(spec.artefactType === 'plan' ? { defaultYes: true } : {}),
|
|
405
|
+
});
|
|
402
406
|
if (!isYes(answer)) {
|
|
403
407
|
args.onNotSaved?.('declined');
|
|
404
408
|
return null;
|
package/dist/projects/yes-no.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export async function askYesNo(a) {
|
|
2
2
|
if (a.confirm)
|
|
3
|
-
return a.confirm(a.question, { default: 'n' });
|
|
3
|
+
return a.confirm(a.question, { default: a.defaultYes ? 'y' : 'n' });
|
|
4
4
|
if (!a.prompt)
|
|
5
5
|
return '';
|
|
6
|
+
if (a.defaultYes) {
|
|
7
|
+
const reply = (await a.prompt(`${a.question} [Y/n]: `)).trim().toLowerCase();
|
|
8
|
+
return reply === '' ? 'y' : reply;
|
|
9
|
+
}
|
|
6
10
|
return (await a.prompt(`${a.question} [y/N]: `)).trim().toLowerCase();
|
|
7
11
|
}
|
|
8
12
|
/** Today's decision rule at all six sites. */
|
|
@@ -14,7 +14,9 @@ const DIFF_LINES = 6;
|
|
|
14
14
|
/** Myers diff is O(N·D) and synchronous: past this budget we show no diff
|
|
15
15
|
* at all (object + footer only), never a partial one. */
|
|
16
16
|
const DIFF_TIMEOUT_MS = 200;
|
|
17
|
-
|
|
17
|
+
// LT-7: a last line that only gains a newline (because lines follow it) is
|
|
18
|
+
// the same line, never a -/+ pair.
|
|
19
|
+
const defaultDiffer = (a, b) => diffLines(a, b, { timeout: DIFF_TIMEOUT_MS, ignoreNewlineAtEof: true });
|
|
18
20
|
// eslint-disable-next-line no-control-regex
|
|
19
21
|
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
20
22
|
const visibleLen = (s) => s.replace(ANSI, '').length;
|