@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.
- package/dist/agent/loop.js +80 -7
- package/dist/lane/ready-spec.js +3 -1
- package/dist/projects/plan-sources.js +3 -2
- package/dist/projects/promote-command.js +4 -2
- package/dist/projects/spec-share.js +10 -0
- package/dist/projects/team-build-context.js +2 -1
- package/dist/repl/open-questions-gate.js +29 -3
- package/dist/repl/team-build-screen.js +27 -11
- package/dist/repl.js +2 -2
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -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,
|
|
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:
|
|
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
|
-
|
|
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
|
}
|
package/dist/lane/ready-spec.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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) => {
|
|
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
|
|
6
|
-
* questions, and one line for each draft
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
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(
|
|
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);
|