@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
|
@@ -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
|
|
@@ -473,7 +521,7 @@ export async function preparePlanResume(args) {
|
|
|
473
521
|
// C2 (D24): a finished UI-less backend stack chains to /abap-fiori-build
|
|
474
522
|
// with the exact binding name — the marketed idea→app story must not
|
|
475
523
|
// silently end at the service binding.
|
|
476
|
-
args.log(...planCompletionLines(content.phases));
|
|
524
|
+
args.log(...planCompletionLines(content.phases, undefined, planBuildTransport(content.phases)));
|
|
477
525
|
}
|
|
478
526
|
else {
|
|
479
527
|
const blocked = content.phases.filter((p) => statuses[p.id] === 'blocked').map((p) => titleOfId(p.id));
|
|
@@ -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;
|
|
@@ -1028,7 +1102,7 @@ export async function finishPlanResume(args) {
|
|
|
1028
1102
|
args.log(renderPlanProgressLine({ content: { ...extract.content, phases: provisionalPhases }, statuses, currentId: null }));
|
|
1029
1103
|
if (!next && provisionalPhases.every((p) => isPhaseSatisfied(statuses[p.id], p.audit))) {
|
|
1030
1104
|
// C2 (D24): same chain as preparePlanResume — single source in plan-run.ts.
|
|
1031
|
-
args.log(...planCompletionLines(provisionalPhases));
|
|
1105
|
+
args.log(...planCompletionLines(provisionalPhases, undefined, planBuildTransport(provisionalPhases)));
|
|
1032
1106
|
// Step 2 (C5): the live run finished — a rebuild whose plan this is reads built.
|
|
1033
1107
|
// Not at the re-opened complete plan above (preparePlanResume).
|
|
1034
1108
|
await afterPlanComplete(args.prepared.path);
|
|
@@ -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
|
|
@@ -17,7 +17,7 @@ import { basename, dirname } from 'node:path';
|
|
|
17
17
|
import { readProjectFile } from '../projects/status.js';
|
|
18
18
|
import { parsePlanContent } from '../projects/plan-schema.js';
|
|
19
19
|
import { computeNextPhaseForRun, isPhaseSatisfiedForRun, phaseDisplayTitle, statusesFromItems, } from '../projects/plan-run.js';
|
|
20
|
-
import { collectPlannedObjects, plannedObjectFacts, planGrantFactLines } from '../projects/plan-objects.js';
|
|
20
|
+
import { collectPlannedObjects, plannedObjectFacts, planGrantCoversWrites, planGrantFactLines, planTransportFact, } from '../projects/plan-objects.js';
|
|
21
21
|
import { adoptPlanTransportOnResume, buildResumeCommand, planBuildTransport } from './plan-resume.js';
|
|
22
22
|
import { newestPlanFamilyFiles } from '../session/recap.js';
|
|
23
23
|
import { recordCliCard } from '../session/cli-record.js';
|
|
@@ -100,8 +100,8 @@ function objectListFacts(objects, transport, grantable) {
|
|
|
100
100
|
return [
|
|
101
101
|
`It will create or change these ${objects.length} SAP object${objects.length === 1 ? '' : 's'}:`,
|
|
102
102
|
...plannedObjectFacts(objects).map((r) => ` ${r}`),
|
|
103
|
-
|
|
104
|
-
...(grantable ? planGrantFactLines() : ['CSPeach asks before each write.']),
|
|
103
|
+
planTransportFact(transport, objects),
|
|
104
|
+
...(grantable && planGrantCoversWrites(transport, objects) ? planGrantFactLines() : ['CSPeach asks before each write.']),
|
|
105
105
|
];
|
|
106
106
|
}
|
|
107
107
|
const OBJECT_LIST_FAILED = 'The object list could not be approved. Each SAP write asks first.';
|
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
|
@@ -11,6 +11,7 @@ const KIND_TITLE = {
|
|
|
11
11
|
'spec-gap': 'Spec gaps', design: 'Design', estimate: 'Estimate', plan: 'Plan',
|
|
12
12
|
'upgrade-scan': 'Upgrade scan', 'upgrade-report': 'Upgrade report',
|
|
13
13
|
answer: 'CSPeach answer', conversation: 'CSPeach conversation',
|
|
14
|
+
'change-options': 'Change options',
|
|
14
15
|
};
|
|
15
16
|
/** Z/Y objects, SAP-style names: ZCL_X, ZFI_DUP_INVOICE_CHECK, Z_REPORT, /NS/ZCL_X. */
|
|
16
17
|
const OBJECT_RE = /(?<![\w/])((?:\/[A-Z0-9]+\/)?[ZY][A-Z0-9_]{2,39})\b/;
|
|
@@ -67,6 +67,8 @@ function titlePage(meta) {
|
|
|
67
67
|
facts.push(`Session: ${meta.session}`);
|
|
68
68
|
if (meta.person)
|
|
69
69
|
facts.push(`Signed in as: ${meta.person}`);
|
|
70
|
+
if (meta.preparedFor)
|
|
71
|
+
facts.push(`Prepared for: ${meta.preparedFor}`);
|
|
70
72
|
return [
|
|
71
73
|
new Paragraph({ text: meta.title, heading: HeadingLevel.TITLE, spacing: { before: 2400 } }),
|
|
72
74
|
...facts.map((f) => new Paragraph({ text: f })),
|
package/dist/lane/app-chain.js
CHANGED
|
@@ -18,7 +18,7 @@ import { readProjectFile } from '../projects/status.js';
|
|
|
18
18
|
import { looksLikeSpecGap } from '../projects/extract-spec-gap.js';
|
|
19
19
|
import { parsePlanContent } from '../projects/plan-schema.js';
|
|
20
20
|
import { adoptPlanTransportOnResume, buildResumeCommand, planBuildTransport } from '../commands/plan-resume.js';
|
|
21
|
-
import { collectPlannedObjects, plannedObjectFacts, planGrantFactLines } from '../projects/plan-objects.js';
|
|
21
|
+
import { collectPlannedObjects, plannedObjectFacts, planGrantCoversWrites, planGrantFactLines, planTransportFact, } from '../projects/plan-objects.js';
|
|
22
22
|
import { phaseDisplayTitle } from '../projects/plan-run.js';
|
|
23
23
|
import { isDeployPhase } from '../projects/plan-lane.js';
|
|
24
24
|
import { installPlanGrant, planGrantAllowed } from '../approvals/plan-grant.js';
|
|
@@ -255,12 +255,15 @@ export function buildStartCardFacts(content, opts = {}) {
|
|
|
255
255
|
// The plan's recorded transport, else the session's current one: the grant
|
|
256
256
|
// is minted with it bound, so the card names it.
|
|
257
257
|
const transport = planBuildTransport(content.phases);
|
|
258
|
+
// A local-only plan with no transport: the grant covers no write (it needs
|
|
259
|
+
// a bound transport), so the card says each write asks.
|
|
260
|
+
const covers = opts.grant !== false && planGrantCoversWrites(transport, objects);
|
|
258
261
|
const facts = [`Steps: ${content.phases.map((p) => phaseDisplayTitle(p, content.phases)).join(' · ')}`];
|
|
259
262
|
if (objects.length > 0) {
|
|
260
263
|
facts.push(`It will create or change these ${objects.length} SAP object${objects.length === 1 ? '' : 's'}:`);
|
|
261
264
|
facts.push(...plannedObjectFacts(objects).map((r) => ` ${r}`));
|
|
262
|
-
facts.push(
|
|
263
|
-
if (
|
|
265
|
+
facts.push(planTransportFact(transport, objects));
|
|
266
|
+
if (!covers)
|
|
264
267
|
facts.push('CSPeach asks before each write.');
|
|
265
268
|
else
|
|
266
269
|
facts.push(...planGrantFactLines());
|
|
@@ -273,7 +276,7 @@ export function buildStartCardFacts(content, opts = {}) {
|
|
|
273
276
|
}
|
|
274
277
|
// The closing fact follows what was granted: with no object-list approval
|
|
275
278
|
// every SAP write asks, so it never promises "only a write not on the list".
|
|
276
|
-
facts.push(
|
|
279
|
+
facts.push(covers && objects.length > 0
|
|
277
280
|
? 'It runs every step and stops only for your answers, a write not on the list, a failed check, or the deploy.'
|
|
278
281
|
: 'It runs every step and stops for your answers, each SAP write, a failed check, or the deploy.');
|
|
279
282
|
return { facts, objects, ...(transport ? { transport } : {}) };
|
|
@@ -314,8 +317,12 @@ export async function afterSpecGapSave(h, savedPath) {
|
|
|
314
317
|
h.queueNext(cmd, APP_ORIGIN);
|
|
315
318
|
h.emit(STEP_2);
|
|
316
319
|
}
|
|
317
|
-
/**
|
|
318
|
-
|
|
320
|
+
/**
|
|
321
|
+
* The result was written but could not be read as a spec-gap: the flow waits at step 1.
|
|
322
|
+
* Task 17 fix round 1: it follows the save step's plain "We couldn't save this…"
|
|
323
|
+
* line, so it says the same thing in the same voice, with no phrase to type.
|
|
324
|
+
*/
|
|
325
|
+
export const SPEC_GAP_NOT_SAVED_LINE = (specToken) => `The /abap-app run is paused at step 1 until the spec-gap result is saved. When we write it again in the usual layout, the run goes on. To start over, run /abap-app ${shownToken(specToken, 'spec file')}.`;
|
|
319
326
|
/** The user said No to the save: the run ends here. */
|
|
320
327
|
export const SPEC_GAP_DECLINED_LINE = (specToken) => `The /abap-app run stopped at step 1 because the spec-gap result was not saved. To start again, run /abap-app ${shownToken(specToken, 'spec file')}.`;
|
|
321
328
|
/**
|
|
@@ -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() {
|
package/dist/one-shot.js
CHANGED
|
@@ -82,6 +82,7 @@ import './tools/project/upgrade_merge_progress.js';
|
|
|
82
82
|
import './tools/project/team_findings.js';
|
|
83
83
|
import './tools/project/team_estate.js';
|
|
84
84
|
import './tools/project/team_lane_move.js';
|
|
85
|
+
import './tools/project/spec_answers_record.js';
|
|
85
86
|
import './tools/subagent/_background-shared.js';
|
|
86
87
|
import './tools/subagent/background_run.js';
|
|
87
88
|
import './tools/subagent/monitor_emit.js';
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { promises as fs } from 'node:fs';
|
|
27
27
|
import * as path from 'node:path';
|
|
28
|
+
import { MAX_ENTRIES_VISITED } from './index-files.js';
|
|
28
29
|
/**
|
|
29
30
|
* Default conventions used when no source has anything to say. Picked
|
|
30
31
|
* to match the most common conventions in modern SAP / web projects:
|
|
@@ -268,8 +269,11 @@ async function inferStyleFromExistingFiles(cwd) {
|
|
|
268
269
|
* node_modules, .git, dist, build, .cspeach output dirs. Prefers files
|
|
269
270
|
* likely to reflect human-authored style (ts, js, abap, cds, xml).
|
|
270
271
|
*/
|
|
271
|
-
async function findSampleFiles(cwd, limit) {
|
|
272
|
+
export async function findSampleFiles(cwd, limit, maxEntries = MAX_ENTRIES_VISITED) {
|
|
272
273
|
const out = [];
|
|
274
|
+
// 1.1.53 item 12 — the same entries budget as the index walk: a folder of
|
|
275
|
+
// ~283,000 empty folders held no sample file and was walked to the end.
|
|
276
|
+
let entriesLeft = maxEntries;
|
|
273
277
|
const TEXT_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.abap', '.cds', '.xml', '.yaml', '.yml'];
|
|
274
278
|
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.cspeach', '.abapforge']);
|
|
275
279
|
async function walk(d) {
|
|
@@ -285,6 +289,9 @@ async function findSampleFiles(cwd, limit) {
|
|
|
285
289
|
for (const ent of entries) {
|
|
286
290
|
if (out.length >= limit)
|
|
287
291
|
return;
|
|
292
|
+
if (entriesLeft <= 0)
|
|
293
|
+
return;
|
|
294
|
+
entriesLeft--;
|
|
288
295
|
if (ent.isFile()) {
|
|
289
296
|
if (TEXT_EXTENSIONS.some((ext) => ent.name.endsWith(ext))) {
|
|
290
297
|
out.push(path.join(d, ent.name));
|
|
@@ -35,6 +35,15 @@ import * as path from 'node:path';
|
|
|
35
35
|
const MAX_FILES_TRAVERSED = 10_000;
|
|
36
36
|
/** Cap on paths shipped in FileIndex. Selected by relevance. */
|
|
37
37
|
const MAX_PATHS_IN_INDEX = 500;
|
|
38
|
+
/**
|
|
39
|
+
* 1.1.53 item 12 — budget on entries (files AND folders) the walk looks at.
|
|
40
|
+
* %TEMP% held ~283,000 empty folders: the file cap never fired and the walk
|
|
41
|
+
* opened every one (~10 s). Spent ⇒ the walk stops and the index is cut.
|
|
42
|
+
* Measured there: 20,000 took ~2.4 s, 5,000 ~0.95 s (with two root reads).
|
|
43
|
+
* Controller ruling: 12,000, so a real project (an abapGit export of
|
|
44
|
+
* thousands of files) keeps its full picture up to the 10,000-file cap.
|
|
45
|
+
*/
|
|
46
|
+
export const MAX_ENTRIES_VISITED = 12_000;
|
|
38
47
|
const ALWAYS_SKIPPED_DIRS = new Set([
|
|
39
48
|
'node_modules',
|
|
40
49
|
'.git',
|
|
@@ -71,14 +80,32 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
71
80
|
* IO errors during the walk are silently skipped (the offending dir/file
|
|
72
81
|
* just doesn't contribute to the count). Never throws.
|
|
73
82
|
*/
|
|
74
|
-
export async function indexFiles(cwd) {
|
|
83
|
+
export async function indexFiles(cwd, options = {}) {
|
|
84
|
+
const maxEntries = options.maxEntries ?? MAX_ENTRIES_VISITED;
|
|
85
|
+
let entriesLeft = maxEntries;
|
|
86
|
+
let walkCut = false;
|
|
75
87
|
const ignoreMatchers = await loadIgnoreMatchers(cwd);
|
|
76
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Files collected with their stat info — used to rank for the path list.
|
|
90
|
+
* `order` is the file's place in a plain depth-first walk (the readdir
|
|
91
|
+
* index at each level), so the output keeps today's order although the
|
|
92
|
+
* walk now visits each level's files before its folders.
|
|
93
|
+
*/
|
|
77
94
|
const collected = [];
|
|
78
|
-
const byExtension = {};
|
|
79
95
|
let totalFiles = 0;
|
|
80
96
|
let truncated = false;
|
|
81
|
-
|
|
97
|
+
let rootEntries = null;
|
|
98
|
+
/** One entry of the budget; false (and the walk marked cut) once it is spent. */
|
|
99
|
+
function spend() {
|
|
100
|
+
if (entriesLeft <= 0) {
|
|
101
|
+
walkCut = true;
|
|
102
|
+
truncated = true;
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
entriesLeft--;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
async function walk(dir, relPrefix, order) {
|
|
82
109
|
if (truncated)
|
|
83
110
|
return;
|
|
84
111
|
let entries;
|
|
@@ -88,45 +115,68 @@ export async function indexFiles(cwd) {
|
|
|
88
115
|
catch {
|
|
89
116
|
return;
|
|
90
117
|
}
|
|
91
|
-
for (
|
|
118
|
+
// The root listing is kept for the top-level list (one read, not two).
|
|
119
|
+
if (relPrefix === '')
|
|
120
|
+
rootEntries = entries;
|
|
121
|
+
// 1.1.53 item 12 (fix round 3) — this level's files first, then its
|
|
122
|
+
// folders: one big early folder can no longer spend the entries budget
|
|
123
|
+
// before a spec or plan file at this level is seen.
|
|
124
|
+
for (let i = 0; i < entries.length; i++) {
|
|
125
|
+
const ent = entries[i];
|
|
126
|
+
if (ent.isDirectory())
|
|
127
|
+
continue;
|
|
92
128
|
if (truncated)
|
|
93
129
|
return;
|
|
130
|
+
if (!spend())
|
|
131
|
+
return;
|
|
132
|
+
if (!ent.isFile())
|
|
133
|
+
continue;
|
|
94
134
|
const relPath = relPrefix ? `${relPrefix}/${ent.name}` : ent.name;
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await walk(path.join(dir, ent.name), relPath);
|
|
135
|
+
if (matchesIgnore(ignoreMatchers, relPath, /* isDirectory */ false))
|
|
136
|
+
continue;
|
|
137
|
+
if (totalFiles >= MAX_FILES_TRAVERSED) {
|
|
138
|
+
truncated = true;
|
|
139
|
+
return;
|
|
101
140
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
totalFiles++;
|
|
110
|
-
const ext = extOf(ent.name);
|
|
111
|
-
byExtension[ext] = (byExtension[ext] ?? 0) + 1;
|
|
112
|
-
// Read mtime cheaply; on error use 0 so the file sorts late.
|
|
113
|
-
let mtime = 0;
|
|
114
|
-
try {
|
|
115
|
-
const s = await fs.stat(path.join(dir, ent.name));
|
|
116
|
-
mtime = s.mtimeMs;
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
// ignore
|
|
120
|
-
}
|
|
121
|
-
collected.push({ path: relPath, ext, mtime });
|
|
141
|
+
totalFiles++;
|
|
142
|
+
// Read mtime cheaply; on error use 0 so the file sorts late.
|
|
143
|
+
let mtime = 0;
|
|
144
|
+
try {
|
|
145
|
+
const s = await fs.stat(path.join(dir, ent.name));
|
|
146
|
+
mtime = s.mtimeMs;
|
|
122
147
|
}
|
|
148
|
+
catch {
|
|
149
|
+
// ignore
|
|
150
|
+
}
|
|
151
|
+
collected.push({ path: relPath, ext: extOf(ent.name), mtime, order: [...order, i] });
|
|
152
|
+
}
|
|
153
|
+
for (let i = 0; i < entries.length; i++) {
|
|
154
|
+
const ent = entries[i];
|
|
155
|
+
if (!ent.isDirectory())
|
|
156
|
+
continue;
|
|
157
|
+
if (truncated)
|
|
158
|
+
return;
|
|
159
|
+
if (!spend())
|
|
160
|
+
return;
|
|
161
|
+
if (ALWAYS_SKIPPED_DIRS.has(ent.name))
|
|
162
|
+
continue;
|
|
163
|
+
const relPath = relPrefix ? `${relPrefix}/${ent.name}` : ent.name;
|
|
164
|
+
if (matchesIgnore(ignoreMatchers, relPath, /* isDirectory */ true))
|
|
165
|
+
continue;
|
|
166
|
+
await walk(path.join(dir, ent.name), relPath, [...order, i]);
|
|
123
167
|
}
|
|
124
168
|
}
|
|
125
|
-
await walk(cwd, '');
|
|
169
|
+
await walk(cwd, '', []);
|
|
170
|
+
// Back to the plain depth-first order, so the counts and the ranking below
|
|
171
|
+
// see the files in exactly today's order.
|
|
172
|
+
collected.sort((a, b) => compareOrder(a.order, b.order));
|
|
173
|
+
const byExtension = {};
|
|
174
|
+
for (const f of collected)
|
|
175
|
+
byExtension[f.ext] = (byExtension[f.ext] ?? 0) + 1;
|
|
126
176
|
// Top-level dirs that survived the walk (i.e. weren't skipped).
|
|
127
177
|
let topLevelDirs = [];
|
|
128
178
|
try {
|
|
129
|
-
const topLevel = await fs.readdir(cwd, { withFileTypes: true });
|
|
179
|
+
const topLevel = rootEntries ?? await fs.readdir(cwd, { withFileTypes: true });
|
|
130
180
|
topLevelDirs = topLevel
|
|
131
181
|
.filter((e) => e.isDirectory() && !ALWAYS_SKIPPED_DIRS.has(e.name) && !matchesIgnore(ignoreMatchers, e.name, true))
|
|
132
182
|
.map((e) => e.name)
|
|
@@ -144,13 +194,26 @@ export async function indexFiles(cwd) {
|
|
|
144
194
|
return b.mtime - a.mtime;
|
|
145
195
|
});
|
|
146
196
|
const paths = collected.slice(0, MAX_PATHS_IN_INDEX).map((f) => f.path);
|
|
147
|
-
|
|
197
|
+
const index = {
|
|
148
198
|
totalFiles,
|
|
149
199
|
byExtension,
|
|
150
200
|
topLevelDirs,
|
|
151
201
|
paths,
|
|
152
202
|
truncated,
|
|
153
203
|
};
|
|
204
|
+
// Only a cut walk carries the field, so every other index is the same object as before.
|
|
205
|
+
if (walkCut)
|
|
206
|
+
index.walkCut = true;
|
|
207
|
+
return index;
|
|
208
|
+
}
|
|
209
|
+
/** Lexicographic order of two readdir-index paths (depth-first order). */
|
|
210
|
+
function compareOrder(a, b) {
|
|
211
|
+
const n = Math.min(a.length, b.length);
|
|
212
|
+
for (let i = 0; i < n; i++) {
|
|
213
|
+
if (a[i] !== b[i])
|
|
214
|
+
return a[i] - b[i];
|
|
215
|
+
}
|
|
216
|
+
return a.length - b.length;
|
|
154
217
|
}
|
|
155
218
|
async function loadIgnoreMatchers(cwd) {
|
|
156
219
|
const patterns = [];
|
|
@@ -29,7 +29,7 @@ import { extractCap } from './domain/cap.js';
|
|
|
29
29
|
import { extractFiori } from './domain/fiori.js';
|
|
30
30
|
import { getGitState } from './git.js';
|
|
31
31
|
import { indexFiles } from './index-files.js';
|
|
32
|
-
export { renderProjectContext } from './render.js';
|
|
32
|
+
export { renderProjectContext, renderProjectContextCapped } from './render.js';
|
|
33
33
|
/**
|
|
34
34
|
* Compute the full project context for a directory. Best-effort: returns
|
|
35
35
|
* null on hard failure (e.g. cwd doesn't exist), otherwise an envelope
|