@cspeach/cli 1.1.52 → 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/conversation-prompt.js +1 -0
- package/dist/agent/loop.js +101 -13
- package/dist/agent/maybe-build-project-context.js +11 -2
- package/dist/agent/repair-partial.js +9 -0
- package/dist/agent/tool-dispatch.js +6 -2
- package/dist/approvals/op-labels.js +6 -3
- package/dist/approvals/render.js +3 -2
- package/dist/commands/plan-chain.js +2 -2
- package/dist/commands/plan-resume.js +2 -2
- package/dist/commands/plan-startup.js +3 -3
- 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/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 +46 -18
- package/dist/renderer/change-card.js +14 -0
- package/dist/renderer/html-entities.js +25 -0
- package/dist/renderer/machine-blocks.js +54 -7
- package/dist/renderer/tool-labels.js +2 -0
- package/dist/renderer/tool-widget.js +20 -5
- package/dist/repl/chat-lane.js +10 -1
- package/dist/repl.js +11 -2
- package/dist/session/fresh-start.js +4 -0
- package/dist/session/user-prompt.js +2 -0
- package/dist/tools/approval.js +12 -1
- package/dist/tools/project/spec_answers_record.js +190 -0
- package/dist/tools/tool-loading.js +2 -0
- package/dist/ui/approval-modal.js +2 -1
- package/dist/ui/widgets/ask-card.js +171 -29
- package/package.json +2 -2
|
@@ -53,6 +53,92 @@ export async function offerDocumentSave(p) {
|
|
|
53
53
|
});
|
|
54
54
|
return docxPath ? { mdPath, docxPath } : { mdPath };
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* The header the CLI writes above a saved change options document (never the
|
|
58
|
+
* model: the document itself never names the person). Parts that are not
|
|
59
|
+
* known are left out: no system in standalone, no person in a one-shot run.
|
|
60
|
+
*/
|
|
61
|
+
export function changeOptionsHeader(p) {
|
|
62
|
+
const parts = [`Date: ${p.today}`];
|
|
63
|
+
if (p.sapSystem)
|
|
64
|
+
parts.push(`System: ${p.sapSystem}`);
|
|
65
|
+
if (p.sessionId)
|
|
66
|
+
parts.push(`Session: ${p.sessionId}`);
|
|
67
|
+
if (p.operator)
|
|
68
|
+
parts.push(`Prepared for: ${p.operator}`);
|
|
69
|
+
return `> **Saved by CSPeach** · ${parts.join(' · ')}\n\n`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Change options (2026-10-23): the marked document(s) from refactor / impact.
|
|
73
|
+
* One "Save this change options document?" for the turn (headless saves
|
|
74
|
+
* without asking). Each document goes to
|
|
75
|
+
* cspeach-out/<date>-change-options-<OBJECT>.md, never overwritten (a second
|
|
76
|
+
* one is -2), with the CLI header on top. Then the one shared Word step for
|
|
77
|
+
* each file; in ask mode "Word copy too?" is asked once and covers all.
|
|
78
|
+
* The Word copy is made from the document alone; its title page carries the
|
|
79
|
+
* facts. Never throws: a failure is one line through `emit`.
|
|
80
|
+
*/
|
|
81
|
+
export async function offerChangeOptionsSave(p) {
|
|
82
|
+
const docs = p.docs.filter((d) => d.markdown.trim().length > 0);
|
|
83
|
+
if (docs.length === 0)
|
|
84
|
+
return {};
|
|
85
|
+
if (!p.headless) {
|
|
86
|
+
// Defaults to Yes (live check 2026-10-26, same as F-A for plans): Enter
|
|
87
|
+
// must not lose the document.
|
|
88
|
+
const a = await askYesNo({ question: 'Save this change options document?', confirm: p.confirm, defaultYes: true });
|
|
89
|
+
if (!isYes(a))
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
p.emit(chalk.dim('headless: saving the change options document without asking'));
|
|
94
|
+
}
|
|
95
|
+
const header = changeOptionsHeader(p);
|
|
96
|
+
const mdPaths = [];
|
|
97
|
+
const docxPaths = [];
|
|
98
|
+
let wordCopy = p.wordCopy;
|
|
99
|
+
for (const doc of docs) {
|
|
100
|
+
let mdPath;
|
|
101
|
+
try {
|
|
102
|
+
const target = path.join(p.cwd, OUT_DIR, documentFilename('change-options', doc.object, p.today));
|
|
103
|
+
await fs.promises.mkdir(path.dirname(target), { recursive: true });
|
|
104
|
+
mdPath = await writeNoClobber(target, `${header}${doc.markdown}\n`);
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
p.emit(`Not saved. ${e.message}`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
p.emit(`Saved: ${path.resolve(mdPath)}`);
|
|
111
|
+
mdPaths.push(mdPath);
|
|
112
|
+
// The Word copy reads plain \n line endings (Task 5 review: CRLF answers).
|
|
113
|
+
const markdown = doc.markdown.replace(/\r\n?/g, '\n');
|
|
114
|
+
const fromText = metaFromMarkdown(markdown, { date: p.today, kind: 'change-options' });
|
|
115
|
+
const meta = {
|
|
116
|
+
title: fromText.title,
|
|
117
|
+
date: p.today,
|
|
118
|
+
...(p.sapSystem ? { system: p.sapSystem } : {}),
|
|
119
|
+
...(doc.object ? { object: doc.object } : {}),
|
|
120
|
+
kind: 'change-options',
|
|
121
|
+
...(p.operator ? { preparedFor: p.operator } : {}),
|
|
122
|
+
};
|
|
123
|
+
let answer;
|
|
124
|
+
const docxPath = await offerWordCopy({
|
|
125
|
+
nextTo: mdPath, markdown, meta, headless: p.headless, wordCopy, emit: p.emit,
|
|
126
|
+
confirm: async (q, o) => { const a = await p.confirm(q, o); answer = isYes(a); return a; },
|
|
127
|
+
});
|
|
128
|
+
// One "Word copy too?" for the turn: its answer decides the next files.
|
|
129
|
+
if (answer !== undefined)
|
|
130
|
+
wordCopy = answer ? 'always' : 'never';
|
|
131
|
+
if (docxPath)
|
|
132
|
+
docxPaths.push(docxPath);
|
|
133
|
+
}
|
|
134
|
+
if (mdPaths.length === 0)
|
|
135
|
+
return {};
|
|
136
|
+
return {
|
|
137
|
+
mdPath: mdPaths[mdPaths.length - 1],
|
|
138
|
+
mdPaths,
|
|
139
|
+
...(docxPaths.length ? { docxPath: docxPaths[docxPaths.length - 1], docxPaths } : {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
56
142
|
/**
|
|
57
143
|
* The Word step, shared by every save of a document (Word copy everywhere,
|
|
58
144
|
* 2026-10-23). `word_copy` decides: unset → ask "Word copy too?", on →
|
|
@@ -93,6 +93,10 @@ function parseDependencySequence(section, nameToId) {
|
|
|
93
93
|
}
|
|
94
94
|
return out;
|
|
95
95
|
}
|
|
96
|
+
/** Task 17 (F-N) — the design's title heading is the sign of the document. */
|
|
97
|
+
export function hasDesignSign(markdown) {
|
|
98
|
+
return matchTitleHeading(markdown, TITLE_SPEC) !== null;
|
|
99
|
+
}
|
|
96
100
|
export function extractDesign(markdown) {
|
|
97
101
|
const titleMatch = matchTitleHeading(markdown, TITLE_SPEC);
|
|
98
102
|
if (!titleMatch) {
|
|
@@ -60,6 +60,10 @@ function parseComponentsTable(section) {
|
|
|
60
60
|
}
|
|
61
61
|
return rows;
|
|
62
62
|
}
|
|
63
|
+
/** Task 17 (F-N) — the estimate's title heading is the sign of the document. */
|
|
64
|
+
export function hasEstimateSign(markdown) {
|
|
65
|
+
return matchTitleHeading(markdown, TITLE_SPEC) !== null;
|
|
66
|
+
}
|
|
63
67
|
export function extractEstimate(markdown) {
|
|
64
68
|
const titleMatch = matchTitleHeading(markdown, TITLE_SPEC);
|
|
65
69
|
if (!titleMatch) {
|
|
@@ -126,6 +126,14 @@ export function looksLikeSpecGap(markdown) {
|
|
|
126
126
|
}
|
|
127
127
|
return false;
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Task 17 (F-N) — any sign of a spec-gap document: its title heading or its
|
|
131
|
+
* body (looksLikeSpecGap). A recap turn carries neither, so the save step
|
|
132
|
+
* stays quiet on it.
|
|
133
|
+
*/
|
|
134
|
+
export function hasSpecGapSign(markdown) {
|
|
135
|
+
return matchTitleHeading(markdown, TITLE_SPEC) !== null || looksLikeSpecGap(markdown);
|
|
136
|
+
}
|
|
129
137
|
function parseGapsFromSection(sectionBody, nextId) {
|
|
130
138
|
const out = [];
|
|
131
139
|
const gapRe = new RegExp(GAP_LINE_RE.source); // per-line, no /m — exec on each line
|
package/dist/projects/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { validateEnvelope } from './validate.js';
|
|
|
5
5
|
export { canonicalSha256 } from './canonicalize.js';
|
|
6
6
|
export { titleSlug, shortId, projectFilename } from './filename.js';
|
|
7
7
|
export { runSaveCommand, detectArtifactSkill, peekPlanManifestTitle, resolvePriorPlanEnvelope } from './save-command.js';
|
|
8
|
-
export { offerAnswerBlockers, openBlockers, applyBlockerAnswers } from './answer-blockers.js';
|
|
8
|
+
export { offerAnswerBlockers, openBlockers, applyBlockerAnswers, saveBlockerAnswers } from './answer-blockers.js';
|
|
9
9
|
export { mergeUpgradeProgress, renderUpgradeMergeManifest } from './merge-upgrade.js';
|
|
10
10
|
export { extractDesign } from './extract-design.js';
|
|
11
11
|
export { extractEstimate } from './extract-estimate.js';
|
|
@@ -73,3 +73,27 @@ export function plannedObjectFacts(objs, max = 12) {
|
|
|
73
73
|
export function planGrantFactLines() {
|
|
74
74
|
return ['Writes to these objects need no further card.', 'Anything else stops and asks.'];
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* True when the plan lists objects and every one is in a local package
|
|
78
|
+
* (`$TMP`, `$…`): nothing on the list goes into a transport.
|
|
79
|
+
*/
|
|
80
|
+
export function localOnlyObjects(objs) {
|
|
81
|
+
return objs.length > 0 && objs.every((o) => typeof o.package === 'string' && o.package.startsWith('$'));
|
|
82
|
+
}
|
|
83
|
+
/** The card's transport line. Local-only objects with no transport say so plainly. */
|
|
84
|
+
export function planTransportFact(transport, objs) {
|
|
85
|
+
if (transport)
|
|
86
|
+
return `Transport: ${transport}`;
|
|
87
|
+
if (localOnlyObjects(objs))
|
|
88
|
+
return 'Transport: none (local objects, not transported)';
|
|
89
|
+
return 'Transport: a new one for this build';
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Will the object-list grant cover these writes? It mirrors grantCovers
|
|
93
|
+
* (approvals/plan-grant.ts): with no bound transport no write is covered,
|
|
94
|
+
* and a local-only plan never creates one, so each write asks. Card text
|
|
95
|
+
* only; the grant itself is minted exactly as before.
|
|
96
|
+
*/
|
|
97
|
+
export function planGrantCoversWrites(transport, objs) {
|
|
98
|
+
return transport !== undefined || !localOnlyObjects(objs);
|
|
99
|
+
}
|
|
@@ -373,7 +373,11 @@ export function findServiceBinding(phases) {
|
|
|
373
373
|
export function planCompletionLines(phases,
|
|
374
374
|
// Terminal callers follow the terminal's glyph set; file writers (the
|
|
375
375
|
// handover markdown) pass UNICODE_GLYPHS so file content never varies.
|
|
376
|
-
g = getGlyphs()
|
|
376
|
+
g = getGlyphs(),
|
|
377
|
+
// 1.1.53 item 11: the run's transport (terminal callers pass
|
|
378
|
+
// planBuildTransport). The handover file passes none, so its content
|
|
379
|
+
// depends only on the transport recorded in the plan file.
|
|
380
|
+
transport) {
|
|
377
381
|
const lines = ['Plan complete — every phase validated.'];
|
|
378
382
|
// Confidence-tiered redesign (2026-07-08): a completed plan that carried
|
|
379
383
|
// non-blocking warn verdicts surfaces them in the FINAL summary — a warned
|
|
@@ -436,7 +440,13 @@ g = getGlyphs()) {
|
|
|
436
440
|
? `${g.info} Deploy skipped by the developer: ${sanitizeControlChars(optOut)}`
|
|
437
441
|
: `${g.attention} The app was not deployed to SAP — it runs only in local preview. Deploy it with /abap-fiori-build or record why not.`);
|
|
438
442
|
}
|
|
439
|
-
|
|
443
|
+
// A run that produced no transport (all objects local, e.g. $TMP) has
|
|
444
|
+
// nothing to preflight before release, so the hint is left out.
|
|
445
|
+
const producedTransport = !!transport?.trim()
|
|
446
|
+
|| phases.some((p) => typeof p.work?.transport === 'string' && p.work.transport.trim() !== '');
|
|
447
|
+
if (producedTransport) {
|
|
448
|
+
lines.push('Consider /abap-preflight on the produced transport(s) before release.');
|
|
449
|
+
}
|
|
440
450
|
return lines;
|
|
441
451
|
}
|
|
442
452
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readdirSync } from 'node:fs';
|
|
2
2
|
import { basename, join } from 'node:path';
|
|
3
|
-
import { extractSpecGap } from './extract-spec-gap.js';
|
|
4
|
-
import { extractDesign } from './extract-design.js';
|
|
5
|
-
import { extractEstimate } from './extract-estimate.js';
|
|
3
|
+
import { extractSpecGap, hasSpecGapSign } from './extract-spec-gap.js';
|
|
4
|
+
import { extractDesign, hasDesignSign } from './extract-design.js';
|
|
5
|
+
import { extractEstimate, hasEstimateSign } from './extract-estimate.js';
|
|
6
6
|
import { extractUpgradeScan, extractUpgradeProgress, extractUpgradeReport } from './extract-upgrade.js';
|
|
7
7
|
import { extractCcaAssessment } from './extract-cca.js';
|
|
8
8
|
import { extractModernizeResult } from './extract-modernize.js';
|
|
@@ -29,16 +29,22 @@ const SKILL_REGISTRY = {
|
|
|
29
29
|
artefactType: 'spec-gap',
|
|
30
30
|
extract: (md) => extractSpecGap(md),
|
|
31
31
|
itemNoun: 'questions',
|
|
32
|
+
hasDocumentSign: hasSpecGapSign,
|
|
33
|
+
documentNoun: 'a spec-gap document',
|
|
32
34
|
},
|
|
33
35
|
'abap-design': {
|
|
34
36
|
artefactType: 'design',
|
|
35
37
|
extract: (md) => extractDesign(md),
|
|
36
38
|
itemNoun: 'objects',
|
|
39
|
+
hasDocumentSign: hasDesignSign,
|
|
40
|
+
documentNoun: 'a design document',
|
|
37
41
|
},
|
|
38
42
|
'abap-estimate': {
|
|
39
43
|
artefactType: 'estimate',
|
|
40
44
|
extract: (md) => extractEstimate(md),
|
|
41
45
|
itemNoun: 'components',
|
|
46
|
+
hasDocumentSign: hasEstimateSign,
|
|
47
|
+
documentNoun: 'an estimate document',
|
|
42
48
|
},
|
|
43
49
|
'abap-upgrade-scan': {
|
|
44
50
|
artefactType: 'upgrade-scan',
|
|
@@ -231,6 +237,18 @@ export function resolvePriorPlanEnvelope(title, dir, opts = {}) {
|
|
|
231
237
|
opts.log?.(`[save] More than one plan is titled '${pick.envelope.title}'; revising the newest: ${basename(pick.path)}`);
|
|
232
238
|
return pick;
|
|
233
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Task 17 (F-N) — the person's message asks us to save the document:
|
|
242
|
+
* "save this/it/that", "save the spec-gap/design/estimate/document",
|
|
243
|
+
* "can you save", "please save". Ordinary wording about saving in the app
|
|
244
|
+
* ("validation on save", "the user clicks Save", "saving changes") is not.
|
|
245
|
+
*/
|
|
246
|
+
const ASKED_TO_SAVE_RE = new RegExp([
|
|
247
|
+
String.raw `\b(?:can|could|would|will)\s+you\s+(?:please\s+)?save\b`,
|
|
248
|
+
String.raw `\bplease\s+save\b`,
|
|
249
|
+
String.raw `\bsave\s+(?:this|it|that|them)\b`,
|
|
250
|
+
String.raw `\bsave\s+(?:the|my|our)\s+(?:spec[\s-]*gaps?|design|estimate|document|doc|result|analysis|file|project\s+file)\b`,
|
|
251
|
+
].join('|'), 'i');
|
|
234
252
|
export async function runSaveCommand(args) {
|
|
235
253
|
const spec = SKILL_REGISTRY[args.skillName];
|
|
236
254
|
if (!spec)
|
|
@@ -323,22 +341,31 @@ export async function runSaveCommand(args) {
|
|
|
323
341
|
return runSaveCommand({ ...args, skillName: detected });
|
|
324
342
|
}
|
|
325
343
|
}
|
|
326
|
-
if (spec.manifestMarker
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
344
|
+
if (spec.manifestMarker) {
|
|
345
|
+
// Task 17 fix round 1 (F-N): the manifest skills send every turn here.
|
|
346
|
+
// A turn with no manifest block is a conversational turn, not a failed
|
|
347
|
+
// save: say nothing (the B5 contract on manifestMarker above). A block
|
|
348
|
+
// that is present but fails to parse keeps its loud lines.
|
|
349
|
+
if (args.skillOutput.includes(spec.manifestMarker)) {
|
|
350
|
+
args.log('');
|
|
351
|
+
args.log(`[save] ${args.skillName}: a ${spec.manifestMarker} block IS present but failed to parse — artefact NOT saved.`);
|
|
352
|
+
args.log(`[save] ${e instanceof Error ? e.message : String(e)}`);
|
|
353
|
+
args.log(`[save] The output above is otherwise complete — re-run the skill or report this as a manifest bug.`);
|
|
354
|
+
}
|
|
331
355
|
}
|
|
332
|
-
else {
|
|
333
|
-
// 2026-
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
356
|
+
else if (spec.hasDocumentSign && spec.documentNoun) {
|
|
357
|
+
// Task 17 (F-N, 2026-10-26) — the save step runs on every turn of the
|
|
358
|
+
// session skill. A turn with no sign of the document (a recap after a
|
|
359
|
+
// choice card) is not a failed save: say nothing, unless the person
|
|
360
|
+
// asked to save. A text that does look like the document gets one
|
|
361
|
+
// plain line.
|
|
362
|
+
if (spec.hasDocumentSign(args.skillOutput) || ASKED_TO_SAVE_RE.test(args.skillInput ?? '')) {
|
|
363
|
+
args.log('');
|
|
364
|
+
args.log(`We couldn't save this as ${spec.documentNoun}. Ask us to write it again in the usual layout.`);
|
|
365
|
+
}
|
|
341
366
|
}
|
|
367
|
+
// (2026-09-17's "[save] … could not read the output" lines are gone: every
|
|
368
|
+
// registry skill now has a manifest marker or a document sign above.)
|
|
342
369
|
args.onNotSaved?.('unreadable');
|
|
343
370
|
return null;
|
|
344
371
|
}
|
|
@@ -401,7 +428,8 @@ export async function runSaveCommand(args) {
|
|
|
401
428
|
const answer = journey ? 'y' : await askYesNo({
|
|
402
429
|
question: 'Save as project file?', confirm: args.confirm, prompt: args.prompt,
|
|
403
430
|
// F-A (live check 2026-10-26) — a plan is only built from its saved file.
|
|
404
|
-
|
|
431
|
+
// Task 20 (owner 2026-10-26) — the spec-gap document defaults to Yes too.
|
|
432
|
+
...(spec.artefactType === 'plan' || spec.artefactType === 'spec-gap' ? { defaultYes: true } : {}),
|
|
405
433
|
});
|
|
406
434
|
if (!isYes(answer)) {
|
|
407
435
|
args.onNotSaved?.('declined');
|
|
@@ -103,6 +103,20 @@ export function diffBody(card, differ = defaultDiffer) {
|
|
|
103
103
|
for (const l of part.value.replace(/\r?\n$/, '').split(/\r?\n/))
|
|
104
104
|
changed.push(`${part.added ? '+' : '-'} ${clean(l)}`);
|
|
105
105
|
}
|
|
106
|
+
// 1.1.53 (live F-E): the first write after a create replaces SAP's template,
|
|
107
|
+
// whose removed lines are only comments. Myers puts those first and hides the
|
|
108
|
+
// new code, so in that case the added code leads, then added comment/blank
|
|
109
|
+
// lines, then the removals. Any removal that carries code keeps Myers order.
|
|
110
|
+
const isCommentOrBlank = (l) => {
|
|
111
|
+
const code = l.slice(2);
|
|
112
|
+
return code.trim() === '' || code.startsWith('*') || /^\s*"/.test(code);
|
|
113
|
+
};
|
|
114
|
+
const removed = changed.filter((l) => l.startsWith('-'));
|
|
115
|
+
const added = changed.filter((l) => l.startsWith('+'));
|
|
116
|
+
const addedCode = added.filter((l) => !isCommentOrBlank(l));
|
|
117
|
+
if (removed.length > 0 && removed.every(isCommentOrBlank) && addedCode.length > 0) {
|
|
118
|
+
changed.splice(0, changed.length, ...addedCode, ...added.filter(isCommentOrBlank), ...removed);
|
|
119
|
+
}
|
|
106
120
|
return { lines: changed.slice(0, DIFF_LINES), hidden: Math.max(0, changed.length - DIFF_LINES) };
|
|
107
121
|
}
|
|
108
122
|
export function buildChangeCard(input, width, box = getBoxChars()) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1.1.53 item 5 — the model sometimes writes `<` / `>` / `&` in the
|
|
3
|
+
* text and diffs it puts on an approval card. Cards show the real characters.
|
|
4
|
+
* One pass only: `&lt;` becomes `<`, never `<`.
|
|
5
|
+
*
|
|
6
|
+
* Display only. Never apply this to anything written to SAP or saved.
|
|
7
|
+
*/
|
|
8
|
+
const ENTITY = /&(lt|gt|amp);/g;
|
|
9
|
+
const CHAR = { lt: '<', gt: '>', amp: '&' };
|
|
10
|
+
export function decodeDisplayEntities(s) {
|
|
11
|
+
return s.replace(ENTITY, (_m, name) => CHAR[name]);
|
|
12
|
+
}
|
|
13
|
+
/** A raw field symbol or raw markup: `<` followed by a letter, `_`, `/` or `!`. */
|
|
14
|
+
const RAW_TAG = /<[A-Za-z_/!]/;
|
|
15
|
+
/**
|
|
16
|
+
* The same decode for a whole source or diff block, except when the block
|
|
17
|
+
* holds a raw field symbol or raw markup (`<fs>`, `<b>`, `</p>`, `<!--`).
|
|
18
|
+
* A model that escapes field symbols escapes them throughout; real source
|
|
19
|
+
* that holds `<` in a literal almost always has a raw `<fs>` or markup
|
|
20
|
+
* too, so it stays as written. Operators and comparisons (`->`, `=>`, `>=`,
|
|
21
|
+
* `<=`, `<>`, ` < `) do not count. Display only, like decodeDisplayEntities.
|
|
22
|
+
*/
|
|
23
|
+
export function decodeDisplayBlock(s) {
|
|
24
|
+
return RAW_TAG.test(s) ? s : decodeDisplayEntities(s);
|
|
25
|
+
}
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
// at the START of a line. It ends at a `-->` on the opener's own line, or
|
|
17
17
|
// else at a `-->` that starts a later line (one newline straight after it is
|
|
18
18
|
// dropped too). An inline mention mid-line is prose and stays.
|
|
19
|
+
// 1.1.53: a line that starts with a closing marker (`<!-- /cspeach:…` or
|
|
20
|
+
// `<!-- /csforge:…`) is a block like an opener. A one-line change options
|
|
21
|
+
// marker (`<!-- csforge:change-options OBJECT=X -->`, `<!-- /csforge:change-options -->`)
|
|
22
|
+
// is removed wherever it sits on a line; the text around it stays. Other
|
|
23
|
+
// mid-line mentions stay prose, and so does a mid-line marker whose `-->` is
|
|
24
|
+
// not on the same line.
|
|
19
25
|
// A chunk boundary can fall anywhere: a line that could still become an
|
|
20
26
|
// opener is held until it can be decided; inside a block a tail that could
|
|
21
27
|
// still become the closing `-->` is held the same way.
|
|
@@ -24,8 +30,10 @@ export function initialMachineBlockState() {
|
|
|
24
30
|
}
|
|
25
31
|
// A leading `\n` marks a line start (the probe prepends one when the text
|
|
26
32
|
// begins at a line start) — no `^`, which would match a mid-line chunk start.
|
|
27
|
-
const OPENER_RE = /\n[ \t]*<!--[ \t]
|
|
28
|
-
const PREFIXES = ['cspeach:', 'csforge:'];
|
|
33
|
+
const OPENER_RE = /\n[ \t]*<!--[ \t]*\/?(?:cspeach|csforge):/;
|
|
34
|
+
const PREFIXES = ['cspeach:', 'csforge:', '/cspeach:', '/csforge:'];
|
|
35
|
+
const INLINE_MARKER_RE = /<!--[ \t]*\/?(?:cspeach|csforge):change-options\b[^\n]*?-->/g;
|
|
36
|
+
const INLINE_PREFIXES = ['cspeach:change-options', 'csforge:change-options'];
|
|
29
37
|
const CLOSER_RE = /\n[ \t]*-->/;
|
|
30
38
|
const PARTIAL_CLOSER_RE = /^\n[ \t]*-{0,2}$/;
|
|
31
39
|
/** Could `frag` (one incomplete line) still grow into a block opener? */
|
|
@@ -38,6 +46,27 @@ function couldBeOpener(frag) {
|
|
|
38
46
|
const r = s.slice(4).replace(/^[ \t]*/, '');
|
|
39
47
|
return PREFIXES.some((p) => p.startsWith(r));
|
|
40
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Could `frag` (text from a mid-line `<`, no newline yet) still grow into a
|
|
51
|
+
* one-line change options marker? A full prefix stays held until its `-->`
|
|
52
|
+
* or the end of the line decides it.
|
|
53
|
+
*/
|
|
54
|
+
function couldBeInlineMarker(frag) {
|
|
55
|
+
if (frag.includes('-->') || frag.includes('\n'))
|
|
56
|
+
return false;
|
|
57
|
+
if (frag.length <= 4)
|
|
58
|
+
return '<!--'.startsWith(frag);
|
|
59
|
+
if (!frag.startsWith('<!--'))
|
|
60
|
+
return false;
|
|
61
|
+
let r = frag.slice(4).replace(/^[ \t]*/, '');
|
|
62
|
+
if (r.startsWith('/'))
|
|
63
|
+
r = r.slice(1);
|
|
64
|
+
return INLINE_PREFIXES.some((p) => p.startsWith(r) || r.startsWith(p));
|
|
65
|
+
}
|
|
66
|
+
/** Remove every complete one-line change options marker. */
|
|
67
|
+
function stripInlineMarkers(text) {
|
|
68
|
+
return text.replace(INLINE_MARKER_RE, '');
|
|
69
|
+
}
|
|
41
70
|
export function filterMachineBlocks(chunk, prev) {
|
|
42
71
|
const st = { ...prev };
|
|
43
72
|
let work = st.held + chunk;
|
|
@@ -103,7 +132,7 @@ export function filterMachineBlocks(chunk, prev) {
|
|
|
103
132
|
if (m) {
|
|
104
133
|
const offset = st.lineStart ? 1 : 0;
|
|
105
134
|
const lineBegin = m.index + 1 - offset; // index in `work`, just after the newline
|
|
106
|
-
output += work.slice(0, lineBegin);
|
|
135
|
+
output += stripInlineMarkers(work.slice(0, lineBegin));
|
|
107
136
|
work = work.slice(m.index + m[0].length - offset);
|
|
108
137
|
st.inBlock = true;
|
|
109
138
|
st.firstLine = true;
|
|
@@ -113,14 +142,32 @@ export function filterMachineBlocks(chunk, prev) {
|
|
|
113
142
|
const lastNl = work.lastIndexOf('\n');
|
|
114
143
|
const fragStart = lastNl >= 0 ? lastNl + 1 : st.lineStart ? 0 : -1;
|
|
115
144
|
if (fragStart >= 0 && fragStart < work.length && couldBeOpener(work.slice(fragStart))) {
|
|
116
|
-
output += work.slice(0, fragStart);
|
|
145
|
+
output += stripInlineMarkers(work.slice(0, fragStart));
|
|
117
146
|
st.held = work.slice(fragStart);
|
|
118
147
|
st.lineStart = true;
|
|
119
148
|
}
|
|
120
149
|
else {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
150
|
+
// Remove complete one-line markers; hold a mid-line tail that could
|
|
151
|
+
// still become one until its `-->` or the newline decides it.
|
|
152
|
+
const shown = stripInlineMarkers(work);
|
|
153
|
+
const tailStart = shown.lastIndexOf('\n') + 1;
|
|
154
|
+
let holdAt = -1;
|
|
155
|
+
for (let i = shown.indexOf('<', tailStart); i >= 0; i = shown.indexOf('<', i + 1)) {
|
|
156
|
+
if (couldBeInlineMarker(shown.slice(i))) {
|
|
157
|
+
holdAt = i;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (holdAt >= 0) {
|
|
162
|
+
output += shown.slice(0, holdAt);
|
|
163
|
+
st.held = shown.slice(holdAt);
|
|
164
|
+
st.lineStart = holdAt === 0 ? st.lineStart : shown[holdAt - 1] === '\n';
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
output += shown;
|
|
168
|
+
if (shown.length > 0)
|
|
169
|
+
st.lineStart = shown.endsWith('\n');
|
|
170
|
+
}
|
|
124
171
|
}
|
|
125
172
|
break;
|
|
126
173
|
}
|
|
@@ -168,6 +168,7 @@ const RULES = {
|
|
|
168
168
|
team_findings: (a) => ({ verb: 'Read team findings', object: str(a['lane']) ?? str(a['packages']) ?? 'all objects' }),
|
|
169
169
|
rules_propose: () => ({ verb: 'Check a proposed decision rule', object: 'nothing is saved' }),
|
|
170
170
|
team_estate: (a) => ({ verb: 'Read estate inventory', object: str(a['packages']) ?? 'all packages' }),
|
|
171
|
+
spec_answers_record: (a) => ({ verb: 'Recorded answers in', object: shortLeft(str(a['path']) ?? '') }),
|
|
171
172
|
team_lane_move: (a) => ({ verb: 'Move packages to another person', object: shortRight(Array.isArray(a['packages']) ? a['packages'].map(String).join(', ') : 'packages') }),
|
|
172
173
|
sap_badi_list: (a) => ({ verb: 'Searched BAdIs', object: shortRight(`"${str(a['query']) ?? ''}"`) }),
|
|
173
174
|
sap_badi_read: (a) => ({ verb: 'Read BAdI', object: shortRight([str(a['kind']), str(a['name'])].filter(Boolean).join(' ')) }),
|
|
@@ -289,6 +290,7 @@ const PRESENT = {
|
|
|
289
290
|
'Audited metadata': 'Auditing metadata', 'Extended model': 'Extending model',
|
|
290
291
|
'Checked capability': 'Checking capability',
|
|
291
292
|
'Read texts of': 'Reading texts of', 'Read team findings': 'Reading team findings', 'Check a proposed decision rule': 'Checking a proposed decision rule', 'Read estate inventory': 'Reading estate inventory', 'Move packages to another person': 'Moving packages to another person', 'Wrote texts': 'Writing texts',
|
|
293
|
+
'Recorded answers in': 'Recording answers in',
|
|
292
294
|
};
|
|
293
295
|
export function toolActivityLabel(name, args) {
|
|
294
296
|
const l = toolLabel(name, args);
|
|
@@ -6,6 +6,16 @@ import { toolLabel, toolActivityLabel, foldOutput, clean } from './tool-labels.j
|
|
|
6
6
|
import { getEffectiveBodyWidth } from './tty.js';
|
|
7
7
|
import { flowFor, stdoutFlow, writeStdout, canAnimateInPlace } from './transcript-flow.js';
|
|
8
8
|
import { humanErrorDetail } from './error-detail.js';
|
|
9
|
+
import { STOPPED_BEFORE_START_RESULT } from '../agent/repair-partial.js';
|
|
10
|
+
/**
|
|
11
|
+
* 1.1.53 item 10 — a call stopped before it started (Esc / Ctrl+C) changed
|
|
12
|
+
* nothing: the row is a neutral dim stop, not a red failure. The model still
|
|
13
|
+
* receives STOPPED_BEFORE_START_RESULT unchanged; this is display only.
|
|
14
|
+
*/
|
|
15
|
+
const STOPPED_BEFORE_START_LINE = 'Stopped before it started. Nothing changed in SAP.';
|
|
16
|
+
function stoppedBeforeStart(isError, content) {
|
|
17
|
+
return isError && content === STOPPED_BEFORE_START_RESULT;
|
|
18
|
+
}
|
|
9
19
|
export { canAnimateInPlace };
|
|
10
20
|
/**
|
|
11
21
|
* Tool-dispatch renderer — CC-style two-line format with peach spinner.
|
|
@@ -316,7 +326,8 @@ export function renderToolCallBottom(params) {
|
|
|
316
326
|
write(panelsRow({ name: name ?? '', args: args ?? {}, isError, resultSummary, resultContent, durationMs }));
|
|
317
327
|
return;
|
|
318
328
|
}
|
|
319
|
-
const
|
|
329
|
+
const stopped = stoppedBeforeStart(isError, resultContent);
|
|
330
|
+
const icon = stopped ? chalk.dim('·') : isError ? chalk.red('✗') : chalk.green('✓');
|
|
320
331
|
// Label: plain (not bold — the top line keeps visual primacy) name +
|
|
321
332
|
// dim arg summary, mirroring the top-line shape so orphaned rows still
|
|
322
333
|
// read as "which tool, on what object".
|
|
@@ -326,9 +337,9 @@ export function renderToolCallBottom(params) {
|
|
|
326
337
|
const wrapped = summary ? chalk.gray('(') + chalk.gray(summary) + chalk.gray(')') : '';
|
|
327
338
|
label = ` ${name}${wrapped}`;
|
|
328
339
|
}
|
|
329
|
-
const detailRaw = resultSummary ? truncate(resultSummary, RESULT_SUMMARY_MAX) : '';
|
|
340
|
+
const detailRaw = stopped ? STOPPED_BEFORE_START_LINE : resultSummary ? truncate(resultSummary, RESULT_SUMMARY_MAX) : '';
|
|
330
341
|
const detail = detailRaw
|
|
331
|
-
? (isError ? chalk.red(detailRaw) : chalk.gray(detailRaw))
|
|
342
|
+
? (stopped ? chalk.dim(detailRaw) : isError ? chalk.red(detailRaw) : chalk.gray(detailRaw))
|
|
332
343
|
: '';
|
|
333
344
|
const timing = chalk.dim(formatDuration(durationMs));
|
|
334
345
|
const sep = chalk.dim('·');
|
|
@@ -424,7 +435,8 @@ function panelsRow(p) {
|
|
|
424
435
|
// Round 8 item 2 — a cancelled / rejected approval is not a success: no
|
|
425
436
|
// green tick, and the row says what happened.
|
|
426
437
|
const declined = !p.isError && p.name === 'request_approval' ? approvalDeclined(p.resultContent) : undefined;
|
|
427
|
-
const
|
|
438
|
+
const stopped = stoppedBeforeStart(p.isError, p.resultContent);
|
|
439
|
+
const icon = stopped ? paint('dim', '·') : p.isError ? paint('bad', '✗') : declined ? paint('warn', '·') : paint('ok', '✓');
|
|
428
440
|
const timing = p.durationMs >= 1000 ? formatDuration(p.durationMs) : '';
|
|
429
441
|
// 1.1.31: a setting said yes, not a person — the row says so.
|
|
430
442
|
const auto = !p.isError && !declined && p.name === 'request_approval' && approvalAutoApproved(p.resultContent);
|
|
@@ -440,7 +452,10 @@ function panelsRow(p) {
|
|
|
440
452
|
// Live fix 2026-09-26: a JSON tool result (e.g. sap_activate's
|
|
441
453
|
// activation_results) is turned into readable lines, never shown raw.
|
|
442
454
|
const errorSource = p.resultContent?.trim().startsWith('{') ? p.resultContent : p.resultSummary;
|
|
443
|
-
if (
|
|
455
|
+
if (stopped) {
|
|
456
|
+
out.push(' ' + paint('dim', truncate(STOPPED_BEFORE_START_LINE, width - 4)));
|
|
457
|
+
}
|
|
458
|
+
else if (p.isError && errorSource) {
|
|
444
459
|
// Raw error text can carry ANSI codes, \r redraws and tabs: clean() each
|
|
445
460
|
// line (the one sanitiser, shared with the labels) before truncate + paint.
|
|
446
461
|
for (const d of humanErrorDetail(errorSource).map(clean).filter(Boolean).slice(0, ERROR_DETAIL_LINES)) {
|
package/dist/repl/chat-lane.js
CHANGED
|
@@ -85,9 +85,18 @@ const THANKS_PHRASES = new Set(['thanks', 'thank you', 'thank u', 'thx', 'tx', '
|
|
|
85
85
|
function isThanksOnly(trimmed) {
|
|
86
86
|
return THANKS_PHRASES.has(stripOkPrefix(trimmed).toLowerCase().replace(/[.!?,]+$/, ''));
|
|
87
87
|
}
|
|
88
|
+
/** Change options (1.1.53): the reply names a level (1-4) AND the document's
|
|
89
|
+
* object (case-insensitive). */
|
|
90
|
+
export function isLevelDecisionFor(trimmed, last) {
|
|
91
|
+
return /\blevel\s*[1-4]\b/i.test(trimmed) && trimmed.toUpperCase().includes(last.object.toUpperCase());
|
|
92
|
+
}
|
|
88
93
|
/** Today's continuation predicate (repl.tsx Ink + classic paths), as one
|
|
89
|
-
* function: the skill that keeps the turn, or null.
|
|
94
|
+
* function: the skill that keeps the turn, or null. A level decision for the
|
|
95
|
+
* last change options object goes back to the skill that showed it (only the
|
|
96
|
+
* skills-lane call sites pass lastChangeOptions). */
|
|
90
97
|
export function wantsContinuation(i) {
|
|
98
|
+
if (i.lastChangeOptions && isLevelDecisionFor(i.trimmed, i.lastChangeOptions))
|
|
99
|
+
return i.lastChangeOptions.skill;
|
|
91
100
|
if (!i.sessionSkill)
|
|
92
101
|
return null;
|
|
93
102
|
return (i.awaitingSkillAnswer || (i.resumedSkill !== null && isShortFollowUp(i.trimmed)) || isAckContinuation(i.trimmed, i.sessionSkill)) ? i.sessionSkill : null;
|
package/dist/repl.js
CHANGED
|
@@ -205,6 +205,7 @@ import './tools/project/team_findings.js';
|
|
|
205
205
|
import './tools/project/team_estate.js';
|
|
206
206
|
import './tools/project/rules_propose.js';
|
|
207
207
|
import './tools/project/team_lane_move.js';
|
|
208
|
+
import './tools/project/spec_answers_record.js';
|
|
208
209
|
import './tools/subagent/_background-shared.js';
|
|
209
210
|
import './tools/subagent/background_run.js';
|
|
210
211
|
import './tools/subagent/monitor_emit.js';
|
|
@@ -1409,6 +1410,7 @@ export async function runRepl(opts) {
|
|
|
1409
1410
|
userMessage: rr.prompt,
|
|
1410
1411
|
skill: rr.skill,
|
|
1411
1412
|
ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: inkPreview, chunkEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor, inlineSkills: inlineActive({ skills: cfg.assistant.skills, lane: laneSetting, interactive: !!process.stdin.isTTY, served: inlineServed(cfg.llm?.mode) }), chatLaneProcess: isChatLaneProcess(laneSetting, !!process.stdin.isTTY), turnBy: lastTurnBy },
|
|
1413
|
+
...(signedInAs ? { operator: signedInAs } : {}), // change options: Prepared for
|
|
1412
1414
|
chunkEmitter,
|
|
1413
1415
|
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
1414
1416
|
autoCompact: true, // Task 19 — top-level REPL turn: auto-compact gate at turn end
|
|
@@ -1638,7 +1640,7 @@ export async function runRepl(opts) {
|
|
|
1638
1640
|
// Round 6 item 4c / review I2 — after a resume, a short follow-up
|
|
1639
1641
|
// ("continue", a number) stays in the session's skill; anything else
|
|
1640
1642
|
// is classified (applyResumedSkill below).
|
|
1641
|
-
const continuedSkill = wantsContinuation({ trimmed, sessionSkill: session.skill, awaitingSkillAnswer: session.awaitingSkillAnswer, resumedSkill });
|
|
1643
|
+
const continuedSkill = wantsContinuation({ trimmed, sessionSkill: session.skill, awaitingSkillAnswer: session.awaitingSkillAnswer, resumedSkill, lastChangeOptions: session.lastChangeOptions ?? null });
|
|
1642
1644
|
if (continuedSkill) {
|
|
1643
1645
|
chunkEmitter.emit('chunk', formatRoutingLine({ kind: 'continuing', skill: continuedSkill }));
|
|
1644
1646
|
skill = continuedSkill;
|
|
@@ -2116,6 +2118,7 @@ export async function runRepl(opts) {
|
|
|
2116
2118
|
skill: skill,
|
|
2117
2119
|
// Stage 1A — inline is decided here, at every submit, from today's inputs (never cached).
|
|
2118
2120
|
ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: inkPreview, chunkEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor, inlineSkills: inlineActive({ skills: cfg.assistant.skills, lane: laneSetting, interactive: !!process.stdin.isTTY, served: inlineServed(cfg.llm?.mode) }), chatLaneProcess: isChatLaneProcess(laneSetting, !!process.stdin.isTTY), turnBy: lastTurnBy },
|
|
2121
|
+
...(signedInAs ? { operator: signedInAs } : {}), // change options: Prepared for
|
|
2119
2122
|
chunkEmitter,
|
|
2120
2123
|
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
2121
2124
|
explicitSlash: parsed.skill !== undefined, // Task 7 R11 — typed or queued slash command
|
|
@@ -2347,6 +2350,8 @@ export async function runRepl(opts) {
|
|
|
2347
2350
|
tokensUsed: 0,
|
|
2348
2351
|
model: session.model,
|
|
2349
2352
|
emit: (line) => chunkEmitter.emit('chunk', line + '\n'),
|
|
2353
|
+
...(signedInAs ? { operator: signedInAs } : {}),
|
|
2354
|
+
sessionId: session.id,
|
|
2350
2355
|
});
|
|
2351
2356
|
}
|
|
2352
2357
|
else if (r.kind === 'none' && !(await import('./sap/reachability-state.js')).isSapUnreachableNoted()) {
|
|
@@ -3526,6 +3531,7 @@ export async function runRepl(opts) {
|
|
|
3526
3531
|
userMessage: rr.prompt,
|
|
3527
3532
|
skill: rr.skill,
|
|
3528
3533
|
ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: replPreviewHook, chunkEmitter: classicOutputEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor, inlineSkills: inlineActive({ skills: cfg.assistant.skills, lane: laneSetting, interactive: !pipedStdin, served: inlineServed(cfg.llm?.mode) }), chatLaneProcess: isChatLaneProcess(laneSetting, !pipedStdin), turnBy: lastTurnBy },
|
|
3534
|
+
...(signedInAs ? { operator: signedInAs } : {}), // change options: Prepared for
|
|
3529
3535
|
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
3530
3536
|
autoCompact: true, // Task 19 — top-level REPL turn: auto-compact gate at turn end
|
|
3531
3537
|
}),
|
|
@@ -3833,7 +3839,7 @@ export async function runRepl(opts) {
|
|
|
3833
3839
|
continue;
|
|
3834
3840
|
}
|
|
3835
3841
|
// Round 6 item 4c / review I2 — see the Ink path.
|
|
3836
|
-
const continuedSkill = wantsContinuation({ trimmed, sessionSkill: session.skill, awaitingSkillAnswer: session.awaitingSkillAnswer, resumedSkill });
|
|
3842
|
+
const continuedSkill = wantsContinuation({ trimmed, sessionSkill: session.skill, awaitingSkillAnswer: session.awaitingSkillAnswer, resumedSkill, lastChangeOptions: session.lastChangeOptions ?? null });
|
|
3837
3843
|
if (continuedSkill) {
|
|
3838
3844
|
console.log(formatRoutingLine({ kind: 'continuing', skill: continuedSkill }).trimEnd());
|
|
3839
3845
|
skill = continuedSkill;
|
|
@@ -4269,6 +4275,7 @@ export async function runRepl(opts) {
|
|
|
4269
4275
|
skill: skill,
|
|
4270
4276
|
// Stage 1A — inline is decided here, at every submit, from today's inputs (never cached).
|
|
4271
4277
|
ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: replPreviewHook, chunkEmitter: classicOutputEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor, inlineSkills: inlineActive({ skills: cfg.assistant.skills, lane: laneSetting, interactive: !pipedStdin, served: inlineServed(cfg.llm?.mode) }), chatLaneProcess: isChatLaneProcess(laneSetting, !pipedStdin), turnBy: lastTurnBy },
|
|
4278
|
+
...(signedInAs ? { operator: signedInAs } : {}), // change options: Prepared for
|
|
4272
4279
|
pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
|
|
4273
4280
|
explicitSlash: parsed.skill !== undefined, // Task 7 R11 — typed or queued slash command
|
|
4274
4281
|
signal: turnAbort.signal,
|
|
@@ -4462,6 +4469,8 @@ export async function runRepl(opts) {
|
|
|
4462
4469
|
tokensUsed: 0,
|
|
4463
4470
|
model: session.model,
|
|
4464
4471
|
emit: (line) => console.log(line),
|
|
4472
|
+
...(signedInAs ? { operator: signedInAs } : {}),
|
|
4473
|
+
sessionId: session.id,
|
|
4465
4474
|
});
|
|
4466
4475
|
}
|
|
4467
4476
|
else if (r.kind === 'none' && !(await import('./sap/reachability-state.js')).isSapUnreachableNoted()) {
|