@cspeach/cli 1.1.55 → 1.1.56

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.
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { basename, dirname } from 'node:path';
2
+ import { basename, dirname, relative, resolve as resolvePath, sep } from 'node:path';
3
3
  import { dispatchTool } from './tool-dispatch.js';
4
4
  import { listTools, getTool, isStandalone, toolsForContext, toAnthropicTools } from '../tools/index.js';
5
5
  import { toolsForSeat } from '../tools/seat-gate.js';
@@ -54,7 +54,7 @@ import { planLaneBlockForTurn } from '../projects/plan-lane.js';
54
54
  import { DOCUMENT_SKILLS, WORD_AFTER_ARTIFACT_SAVE } from '../projects/document-skills.js';
55
55
  import { metaFromMarkdown } from '../docx/doc-meta.js';
56
56
  import { bindCliCardSession, documentOffersFor, markCliMessage, CONTINUE_LINE, WATCHDOG_LINE } from '../session/cli-record.js';
57
- import { clearAppFlow, getAppFlow, setGapConfirmed, startAppFlow } from '../lane/app-flow.js';
57
+ import { clearAppFlow, getAppFlow, setGapConfirmed, setSpecGap, startAppFlow } from '../lane/app-flow.js';
58
58
  import { getLatestSkeletonDecision } from '../rap/skeleton-session.js';
59
59
  import { afterRebuildTurn } from '../journey/rebuild.js';
60
60
  import { afterJourneyTurn, gateJourneyForTurn, journeyAsk, journeyCardConfirm, journeyStoppedOnError, journeyTurnBlock, pauseJourneyForInterruptedTurn } from '../journey/chain.js';
@@ -64,6 +64,7 @@ import { shownToken } from '../lane/card.js';
64
64
  import { parseFromFlag, coerceDocFromFlagToAttachment } from '../skills/promotion-dispatch.js';
65
65
  import { input } from '@inquirer/prompts';
66
66
  import { withInquirer } from '../repl/inquirer-guard.js';
67
+ import { takeGateGoAhead } from '../repl/open-questions-gate.js';
67
68
  import { expandTextFileAttachments } from '../projects/workspace.js';
68
69
  import { expandImageAttachments, buildUserContent, trimHistoryImages } from '../projects/image-attachments.js';
69
70
  import { expandImageChips } from '../ui/clipboard-image.js';
@@ -451,6 +452,51 @@ async function isSharedSpecSave(cwd, saveSkill, savedPath) {
451
452
  return false;
452
453
  }
453
454
  }
455
+ /**
456
+ * F14 — the spec text the lane step quotes from: the Markdown copy next to the
457
+ * shared spec-gap (`<slug>.md`, else the one .md there), cwd-relative. The
458
+ * lane tool holds the model to the flow's spec, and the spec-gap file is never
459
+ * that spec. Undefined when there is none.
460
+ */
461
+ async function sharedSpecText(cwd, gapAbs) {
462
+ const { readdir } = await import('node:fs/promises');
463
+ const dir = dirname(gapAbs);
464
+ const mds = (await readdir(dir).catch(() => [])).filter((f) => f.toLowerCase().endsWith('.md'));
465
+ const pick = mds.find((f) => f === `${basename(dir)}.md`) ?? (mds.length === 1 ? mds[0] : undefined);
466
+ return pick === undefined ? undefined : relative(cwd, resolvePath(dir, pick)).split(sep).join('/');
467
+ }
468
+ /**
469
+ * F14 (2026-10-30 live retest) — `/abap-app --from @specs/<slug>/<file>` in a
470
+ * team build folder with no flow for that file: start the flow at the lane
471
+ * step, so the lane tool queues `/abap-plan --from @specs/<slug>/<file>`.
472
+ * A flow already at the lane step for the same real file is kept as it is.
473
+ * True when the source is a shared spec here. Never throws: a folder it
474
+ * cannot read starts nothing.
475
+ */
476
+ async function startSharedSpecFlow(session, cwd, token, source, yes) {
477
+ try {
478
+ const { buildFolderAt } = await import('../commands/team-spec.js');
479
+ const here = buildFolderAt(cwd);
480
+ if (!here)
481
+ return false;
482
+ const abs = resolvePath(cwd, source);
483
+ const { isSharedSpecPath } = await import('../projects/spec-share.js');
484
+ if (!isSharedSpecPath(here.dir, abs))
485
+ return false;
486
+ const flow = getAppFlow(session);
487
+ if (flow?.expect === 'lane' && flow.specGapPath && await sameRealFile(flow.specGapPath, abs))
488
+ return true;
489
+ startAppFlow(session, await sharedSpecText(cwd, abs) ?? token);
490
+ setSpecGap(session, abs);
491
+ if (yes !== undefined && await sameRealFile(yes, abs))
492
+ setGapConfirmed(session, abs);
493
+ return true;
494
+ }
495
+ catch {
496
+ // no flow: the lane step says no plan was queued, as before
497
+ return false;
498
+ }
499
+ }
454
500
  async function offerWordAfterArtifactSave(p, saveSkill, savedPath, confirm) {
455
501
  try {
456
502
  const { offerWordCopyAfterArtifactSave } = await import('../projects/document-save.js');
@@ -648,13 +694,12 @@ export async function maybeOfferSave(p) {
648
694
  // Team build (Task 9): a spec in the shared specs/ folder is never
649
695
  // rewritten — each answer is a record of its own, and the spec is read
650
696
  // with the records applied. Found by the folder (R10), never activeProject().
651
- const { sharedSpecTargetFor, isSharedSpecPath, composeSpec, readAnswers, writeAnswerRecord, specAuthorFor } = await import('../projects/spec-share.js');
652
- const { readProjectFile } = await import('../projects/status.js');
697
+ const { sharedSpecTargetFor, isSharedSpecPath, readComposedSpec, writeAnswerRecord, specAuthorFor } = await import('../projects/spec-share.js');
653
698
  const shared = sharedSpecTargetFor(p.cwd ?? process.cwd(), 'spec-gap');
654
699
  const records = shared && isSharedSpecPath(shared.teamDir, savedPath)
655
700
  ? {
656
701
  // R11: only the answers recorded for this envelope.
657
- readEnvelope: (path) => composeSpec(readProjectFile(path), readAnswers(dirname(path)).filter((a) => a.file === basename(path))),
702
+ readEnvelope: readComposedSpec,
658
703
  recordAnswer: (a) => {
659
704
  const specDir = dirname(savedPath);
660
705
  writeAnswerRecord(specDir, {
@@ -1020,6 +1065,13 @@ export async function runTurn(params) {
1020
1065
  // (plan step) skips it when it promotes the same real file.
1021
1066
  const flowNow = !params.ctx.subagent ? getAppFlow(session) : undefined;
1022
1067
  const confirmedGap = flowNow?.gapConfirmed;
1068
+ // F12 (2026-10-30 live retest) — the open-questions gate's go-ahead on a
1069
+ // shared spec was the yes: this turn's /abap-app promote of that same
1070
+ // real file asks nothing more. It counts for the plan step too.
1071
+ const gateYes = !params.ctx.subagent && params.skill === 'abap-app' ? takeGateGoAhead(session) : undefined;
1072
+ // F14 (2026-10-30 live retest) — the yes given this turn, kept for a flow
1073
+ // this promote starts (a shared spec built with no flow yet).
1074
+ let yesThisTurn;
1023
1075
  const result = await runPromoteCommand({
1024
1076
  sourcePath: fromPath,
1025
1077
  targetSkill: params.skill,
@@ -1029,9 +1081,22 @@ export async function runTurn(params) {
1029
1081
  // Final residual 4 — the same folder gateAppFlowForTurn resolves @X against.
1030
1082
  cwd: params.ctx.cwd,
1031
1083
  ...(flowNow?.expect === 'lane' && params.skill === 'abap-app'
1032
- ? { onOpenWarningsConfirmed: (p) => setGapConfirmed(session, p) } : {}),
1084
+ ? { onOpenWarningsConfirmed: (p) => setGapConfirmed(session, p) }
1085
+ : !params.ctx.subagent && params.skill === 'abap-app'
1086
+ ? { onOpenWarningsConfirmed: (p) => { yesThisTurn = p; } } : {}),
1033
1087
  ...(flowNow?.expect === 'plan' && params.skill === 'abap-plan' && confirmedGap
1034
1088
  ? { skipOpenWarnings: (p) => sameRealFile(p, confirmedGap) } : {}),
1089
+ ...(gateYes
1090
+ ? {
1091
+ skipOpenWarnings: async (p) => {
1092
+ const same = await gateYes(p);
1093
+ if (same && getAppFlow(session)?.expect === 'lane')
1094
+ setGapConfirmed(session, p);
1095
+ if (same)
1096
+ yesThisTurn = p;
1097
+ return same;
1098
+ },
1099
+ } : {}),
1035
1100
  });
1036
1101
  if (!result) {
1037
1102
  // User declined, source invalid, or unsupported target — skip the turn.
@@ -1040,6 +1105,12 @@ export async function runTurn(params) {
1040
1105
  }
1041
1106
  promotedFromForSave = result.promotedFrom;
1042
1107
  promotedFromPathForSave = result.sourcePath;
1108
+ // F14 (2026-10-30 live retest) — a build started from a shared spec
1109
+ // (start screen, gate, F9c hand-off or typed) had no app flow, so the
1110
+ // lane step queued no plan. Start one here, at the lane step, as after a
1111
+ // chained spec-gap save. Team build folders only; the gate above has run.
1112
+ const sharedSpec = !params.ctx.subagent && params.skill === 'abap-app' && result.promotedFrom.artefactType === 'spec-gap'
1113
+ && await startSharedSpecFlow(session, params.ctx.cwd ?? process.cwd(), fromPath, result.sourcePath, yesThisTurn);
1043
1114
  // 2026-05-08: thread the original @<filename> token into the LLM
1044
1115
  // prompt so skills that need to dispatch a follow-up command (e.g.
1045
1116
  // dispatch_skill calling /abap-rap --from @<same-file>) can use the
@@ -1049,7 +1120,9 @@ export async function runTurn(params) {
1049
1120
  // "foo-v1"), producing "no file matching" auto-routes. Use basename
1050
1121
  // so absolute / relative paths normalise to the workspace @<token>
1051
1122
  // form the picker resolves.
1052
- const sourceToken = `@${basename(fromPath)}`;
1123
+ // F14: a shared spec keeps the path as typed (`specs/<slug>/<file>`); its
1124
+ // bare name resolves against the seat's work folder and fails.
1125
+ const sourceToken = sharedSpec ? `@${/\s/.test(fromPath) ? `"${fromPath}"` : fromPath}` : `@${basename(fromPath)}`;
1053
1126
  userMessageForLLM = `Source file: ${sourceToken}\n${result.extendedSkillInput}\n\n---\n\n${rest}`;
1054
1127
  userMessageForSave = rest;
1055
1128
  }
@@ -9,7 +9,9 @@ export async function readySpecFor(cwd, request) {
9
9
  const said = request.toLowerCase();
10
10
  const isNamed = (s) => said.includes(s.title.toLowerCase()) || said.includes(s.slug.replace(/-/g, ' '));
11
11
  const namedAll = all.filter(isNamed);
12
- const ready = all.filter((s) => s.status === 'ready for build');
12
+ // F13 (2026-10-30 live retest): a spec being built stays buildable, so a
13
+ // build stopped after the gate, or a closed window, can be continued.
14
+ const ready = all.filter((s) => s.status === 'ready for build' || s.status === 'being built');
13
15
  const named = ready.filter(isNamed);
14
16
  // S1 — the request names a spec, and it is not one that is ready.
15
17
  if (named.length === 0 && namedAll.length === 1) {
@@ -22,7 +22,6 @@
22
22
  // continuous run) calls resolvePlanSources + renderPlanSourcesBlock.
23
23
  import { promises as fs } from 'node:fs';
24
24
  import path from 'node:path';
25
- import { readProjectFile } from './status.js';
26
25
  import { buildPromotedFromSnapshot } from './promote.js';
27
26
  import { assertRealPathContained } from '../tools/_filesystem-shared.js';
28
27
  import { laneRootFor, workspaceFolderFor } from '../lane/roots.js';
@@ -92,7 +91,9 @@ async function readLiveSpecGap(cwd, content, promotedFrom, laneRecord, planPath)
92
91
  continue;
93
92
  let env;
94
93
  try {
95
- env = readProjectFile(abs);
94
+ // F12 — a shared spec's answer records reach the phase too.
95
+ const { readComposedSpec } = await import('./spec-share.js');
96
+ env = readComposedSpec(abs);
96
97
  }
97
98
  catch {
98
99
  continue;
@@ -1,5 +1,4 @@
1
1
  import { realpathSync } from 'node:fs';
2
- import { readProjectFile } from './status.js';
3
2
  import { buildPromotedFromSnapshot, buildReviewDecisionsBlock, buildDetailPathHint, validatePromotionSource } from './promote.js';
4
3
  import { resolveAtTokenAsync, formatProjectFileList } from './workspace.js';
5
4
  import { askYesNo } from './yes-no.js';
@@ -105,9 +104,12 @@ export async function runPromoteCommand(args) {
105
104
  args.log(`--from: ${resolvedPath} is inside CSPeach's own folder and cannot be read.`);
106
105
  return null;
107
106
  }
107
+ // 2026-10-30 live retest F12: a shared spec is read with its answer
108
+ // records applied, so answered questions do not count as open.
109
+ const { readComposedSpec } = await import('./spec-share.js');
108
110
  let env;
109
111
  try {
110
- env = readProjectFile(resolvedPath);
112
+ env = readComposedSpec(resolvedPath);
111
113
  }
112
114
  catch (e) {
113
115
  args.log(`--from source error: ${String(e instanceof Error ? e.message : e)}`);
@@ -161,6 +161,16 @@ export function composeSpec(env, answers) {
161
161
  }
162
162
  return out;
163
163
  }
164
+ /**
165
+ * The spec at `absPath` as people read it: the file with the answers recorded
166
+ * for it applied (R11). A folder with no `answers/` gives the plain file.
167
+ * 2026-10-30 live retest F12: `--from` read the raw file and saw 25 answered
168
+ * questions as open.
169
+ */
170
+ export function readComposedSpec(absPath) {
171
+ const file = path.basename(absPath);
172
+ return composeSpec(readEnvelopeFile(absPath), readAnswers(path.dirname(absPath)).filter((a) => a.file === file));
173
+ }
164
174
  export async function writeSharedSpec(a) {
165
175
  const slug = specSlug(a.env.title);
166
176
  const specDir = path.join(a.teamDir, SPECS_DIR, slug);
@@ -38,7 +38,8 @@ export function teamBuildContextBlock(cwd, seat) {
38
38
  lines.push('When the person says a spec is ready for the developer, CSPeach records it; this seat does not build.');
39
39
  }
40
40
  else {
41
- lines.push('To build a ready spec: hand_off with skill "abap-app" and the spec\'s title in the request. CSPeach attaches the spec and shows its open questions first.');
41
+ // F13 (2026-10-30 live retest): a spec being built can be continued.
42
+ lines.push('To build a ready spec, or continue one being built: hand_off with skill "abap-app" and the spec\'s title in the request. CSPeach attaches the spec and shows its open questions first.');
42
43
  }
43
44
  lines.push('</team_build>');
44
45
  return lines.join('\n');
@@ -27,6 +27,24 @@ export const STOPPED_LINE = 'Stopped. The spec stays ready for the developer.';
27
27
  const markedThisRun = new Set();
28
28
  const dirKey = (d) => (process.platform === 'win32' ? path.resolve(d).toLowerCase() : path.resolve(d));
29
29
  export function noteBeingBuiltThisRun(specDir) { markedThisRun.add(dirKey(specDir)); }
30
+ /**
31
+ * 2026-10-30 live retest F12 — the gate letting the build go on ("Go ahead",
32
+ * answered, or nothing open) is the developer's yes to the open items. The
33
+ * spec file is kept per session, and the promote in that same turn takes it
34
+ * (once) so it does not ask "still open … Promote anyway?" again.
35
+ */
36
+ const goAhead = new WeakMap();
37
+ /** The gate's yes for this session, taken once: a check that the promote source is the same real file. */
38
+ export function takeGateGoAhead(session) {
39
+ const abs = goAhead.get(session);
40
+ if (abs === undefined)
41
+ return undefined;
42
+ goAhead.delete(session);
43
+ return async (p) => {
44
+ const { sameRealFile } = await import('../lane/app-chain.js');
45
+ return sameRealFile(p, abs);
46
+ };
47
+ }
30
48
  function openQuestions(env) {
31
49
  if (env.artefactType !== 'spec-gap')
32
50
  return [];
@@ -50,6 +68,9 @@ export function gateChooser(choose) {
50
68
  }
51
69
  const cardTitle = (n) => (n === 1 ? '1 open question travels with this spec.' : `${n} open questions travel with this spec.`);
52
70
  export async function openQuestionsGate(a) {
71
+ // F12: a yes from an earlier gate never carries over to this command.
72
+ if (a.session)
73
+ goAhead.delete(a.session);
53
74
  // F10 (2026-10-29): a typed `/abap-app @<saved spec-gap>` (no --from) is
54
75
  // the same request — the spec is already analysed — so it goes to the lane
55
76
  // step as `--from`, never back through step 1. Anything else typed with @
@@ -91,11 +112,10 @@ export async function openQuestionsGate(a) {
91
112
  const share = await import('../projects/spec-share.js');
92
113
  if (!share.isSharedSpecPath(here.dir, abs) || !abs.endsWith('.cspeach.json'))
93
114
  return null;
94
- const { readProjectFile } = await import('../projects/status.js');
95
115
  const specDir = path.dirname(abs);
96
116
  const file = path.basename(abs);
97
117
  // R11: only the answers recorded for this envelope.
98
- const read = (p) => share.composeSpec(readProjectFile(p), share.readAnswers(path.dirname(p)).filter((x) => x.file === path.basename(p)));
118
+ const read = share.readComposedSpec;
99
119
  let env;
100
120
  try {
101
121
  env = read(abs);
@@ -122,7 +142,13 @@ export async function openQuestionsGate(a) {
122
142
  }
123
143
  catch { /* the build still goes on */ }
124
144
  };
125
- const goOn = (r) => { markedThisRun.delete(dirKey(specDir)); markBeingBuilt(); return r; };
145
+ const goOn = (r) => {
146
+ markedThisRun.delete(dirKey(specDir));
147
+ markBeingBuilt();
148
+ if (a.session)
149
+ goAhead.set(a.session, abs);
150
+ return r;
151
+ };
126
152
  const carried = (qs) => goOn({ command: `${command}\nOpen questions carried as assumptions:\n${qs.map((q) => `- ${q.id}: ${q.text}`).join('\n')}` });
127
153
  for (;;) {
128
154
  const qs = openQuestions(env);
@@ -2,10 +2,11 @@
2
2
  * Team build (spec 2026-10-25) — the developer start screen.
3
3
  *
4
4
  * When CSPeach starts in a team build folder, it says what sits in the shared
5
- * `specs/` folder: each spec ready for build with its summary and open
6
- * questions, and one line for each draft or spec being built. A developer is
7
- * then offered each ready spec: build it, ask about it first, or later. A
8
- * functional seat sees the same lines and is never offered a build.
5
+ * `specs/` folder: each spec ready for build or being built with its summary
6
+ * and open questions, and one line for each draft. A developer is then
7
+ * offered each ready spec (build it, ask about it first, or later) and each
8
+ * spec being built (continue building it, F13). A functional seat sees the
9
+ * same lines and is never offered a build.
9
10
  *
10
11
  * Ruling R10: the build folder is the folder CSPeach runs in, never
11
12
  * `activeProject()`. Ruling R12: the status record goes to the spec folder.
@@ -13,12 +14,18 @@
13
14
  */
14
15
  import path from 'node:path';
15
16
  const day = (iso) => String(iso ?? '').slice(0, 10);
17
+ const trimQuestion = (q) => (q.length > 100 ? `${q.slice(0, 100)}…` : q);
16
18
  export function startScreenLines(specs, _seat) {
17
19
  const out = [];
18
20
  for (const s of specs) {
19
21
  const who = `"${s.title}" — ${s.author}, ${day(s.createdAt)}`;
20
- if (s.status === 'ready for build') {
21
- out.push(`Ready to build: ${who}`, ` ${s.summary}`);
22
+ if (s.status === 'ready for build' || s.status === 'being built') {
23
+ // F13 (2026-10-30 live retest): a spec being built reads like a ready
24
+ // one, with who started it, so the developer can continue it.
25
+ const head = s.status === 'ready for build'
26
+ ? `Ready to build: ${who}`
27
+ : `Being built: ${who}${s.statusBy ? ` · started by ${s.statusBy}, ${day(s.statusAt ?? '')}` : ''}`;
28
+ out.push(head, ` ${s.summary}`);
22
29
  if (s.openQuestions.length) {
23
30
  out.push(' Open questions:');
24
31
  for (const q of s.openQuestions)
@@ -29,29 +36,32 @@ export function startScreenLines(specs, _seat) {
29
36
  }
30
37
  }
31
38
  else {
32
- out.push(`${s.status === 'draft' ? 'Draft' : 'Being built'}: ${who}`);
39
+ out.push(`Draft: ${who}`);
33
40
  }
34
41
  // Owner, 2026-10-29: an answer someone else replaced is said, so the
35
42
  // spec's author learns the developer overrode it.
36
43
  const changed = s.changedAnswers ?? [];
37
44
  if (changed.length) {
38
45
  out.push(' Answers changed by someone else:');
46
+ // M7 (2026-10-30 live retest): a long question took 6 lines; cut at 100.
39
47
  for (const c of changed)
40
- out.push(` - "${c.question}" now "${c.answer}" (${c.by}, ${day(c.at)}), was "${c.was}" (${c.wasBy})`);
48
+ out.push(` - "${trimQuestion(c.question)}" now "${c.answer}" (${c.by}, ${day(c.at)}), was "${c.was}" (${c.wasBy})`);
41
49
  }
42
50
  }
43
51
  return out;
44
52
  }
45
53
  const BUILD = 'Build this spec';
54
+ const CONTINUE = 'Continue building';
46
55
  const ASK = 'Ask about it first';
47
56
  const LATER = 'Later';
48
57
  export const askSentence = (s) => `Tell me about the spec "${s.title}" in specs/${s.slug}/ before we build it.`;
49
58
  async function chooseFor(choose, s) {
50
59
  const n = s.openQuestions.length;
60
+ const going = s.status === 'being built';
51
61
  const v = await choose({
52
- title: `Ready to build: "${s.title}"`,
62
+ title: going ? `Continue building: "${s.title}"` : `Ready to build: "${s.title}"`,
53
63
  facts: [`${s.author}, ${day(s.createdAt)}`, n === 0 ? 'No open questions.' : n === 1 ? '1 open question.' : `${n} open questions.`],
54
- choices: [{ value: 'build', label: BUILD }, { value: 'ask', label: ASK }, { value: 'later', label: LATER }],
64
+ choices: [{ value: 'build', label: going ? CONTINUE : BUILD }, { value: 'ask', label: ASK }, { value: 'later', label: LATER }],
55
65
  });
56
66
  if (v === 'build')
57
67
  return { kind: 'build', spec: s };
@@ -68,7 +78,7 @@ export async function offerBuild(a) {
68
78
  if (a.seat !== 'developer')
69
79
  return false;
70
80
  for (const s of a.specs) {
71
- if (s.status !== 'ready for build')
81
+ if (s.status !== 'ready for build' && s.status !== 'being built')
72
82
  continue;
73
83
  const c = await chooseFor(a.choose, s);
74
84
  if (c.kind === 'later')
@@ -83,6 +93,12 @@ export async function offerBuild(a) {
83
93
  a.emit(`We could not start the build from specs/${s.slug}/. Run /abap-app --from @specs/${s.slug}/<file> yourself.`);
84
94
  return false;
85
95
  }
96
+ // F13: continuing a spec already being built writes no record, and it is
97
+ // not marked for this run, so a Stop at the gate leaves it being built.
98
+ if (s.status === 'being built') {
99
+ a.submit(`/abap-app --from ${t.token}`);
100
+ return true;
101
+ }
86
102
  const { writeStatusRecord } = await import('../projects/spec-share.js');
87
103
  writeStatusRecord(s.specDir, {
88
104
  projectId: a.projectId, slug: s.slug, file: path.basename(s.envelopePath),
package/dist/repl.js CHANGED
@@ -1608,7 +1608,7 @@ export async function runRepl(opts) {
1608
1608
  const g = await openQuestionsGate({
1609
1609
  cwd: process.cwd(), command: effectiveInput, identity: getAuthorIdentity(),
1610
1610
  emit: (line) => chunkEmitter.emit('chunk', line + '\n'),
1611
- choose: gateChooser(inkStartupChooser()), answer: inkAwareChoose, prompt: inkAwarePrompt, headless: isHeadless(),
1611
+ choose: gateChooser(inkStartupChooser()), answer: inkAwareChoose, prompt: inkAwarePrompt, headless: isHeadless(), session,
1612
1612
  });
1613
1613
  if (g && 'stop' in g) {
1614
1614
  chunkEmitter.emit('chunk', STOPPED_LINE + '\n');
@@ -3947,7 +3947,7 @@ export async function runRepl(opts) {
3947
3947
  cwd: process.cwd(), command: resolvedTrimmed, identity: getAuthorIdentity(),
3948
3948
  emit: (line) => console.log(line),
3949
3949
  choose: gateChooser(classicStartupChooser(inkAwarePrompt, (...l) => l.forEach((x) => console.log(x)))),
3950
- answer: inkAwareChoose, prompt: inkAwarePrompt, headless: isHeadless(),
3950
+ answer: inkAwareChoose, prompt: inkAwarePrompt, headless: isHeadless(), session,
3951
3951
  });
3952
3952
  if (g && 'stop' in g) {
3953
3953
  console.log(STOPPED_LINE);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cspeach/cli",
3
- "version": "1.1.55",
3
+ "version": "1.1.56",
4
4
  "description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, signed skills, safety gates.",
5
5
  "keywords": [
6
6
  "abap",