@cspeach/cli 1.1.29 → 1.1.31
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/.cspeach/project.json +4 -0
- package/README.md +11 -1
- package/dist/agent/loop.js +127 -11
- package/dist/auth/auth-file.js +2 -2
- package/dist/cli.js +39 -13
- package/dist/commands/config-set.js +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/commands/logout.js +1 -1
- package/dist/commands/plan-continue.js +1 -1
- package/dist/commands/plan-gate.js +8 -1
- package/dist/commands/plan-resume.js +24 -18
- package/dist/commands/plan-startup.js +49 -6
- package/dist/commands/team-discover.js +195 -0
- package/dist/commands/team-plan.js +585 -0
- package/dist/commands/team.js +652 -46
- package/dist/commands/whoami.js +1 -1
- package/dist/config/paths.js +15 -1
- package/dist/journey/chain.js +728 -0
- package/dist/journey/clean-core.js +213 -0
- package/dist/journey/flow.js +166 -0
- package/dist/journey/run.js +435 -0
- package/dist/journey/session-deps.js +77 -0
- package/dist/journey/trigger.js +153 -0
- package/dist/lane/app-chain.js +9 -7
- package/dist/lane/chain-token.js +9 -5
- package/dist/lane/roots.js +97 -41
- package/dist/one-shot.js +21 -1
- package/dist/projects/clean-core-confirm.js +192 -0
- package/dist/projects/detail-path.js +9 -4
- package/dist/projects/extract-clean-core.js +82 -0
- package/dist/projects/migration.js +4 -105
- package/dist/projects/plan-lane.js +67 -17
- package/dist/projects/plan-sources.js +13 -7
- package/dist/projects/project-folder.js +131 -0
- package/dist/projects/promote-command.js +39 -0
- package/dist/projects/save-command.js +115 -15
- package/dist/projects/saved-lines.js +17 -3
- package/dist/projects/sort-command.js +499 -0
- package/dist/projects/sort-core.js +248 -0
- package/dist/projects/validate.js +2 -1
- package/dist/projects/workspace-retire.js +48 -0
- package/dist/projects/workspace.js +182 -120
- package/dist/register/atc-file.js +9 -1
- package/dist/register/discover.js +901 -0
- package/dist/register/findings.js +82 -0
- package/dist/register/measure.js +60 -1
- package/dist/register/offer.js +125 -0
- package/dist/register/picture.js +25 -0
- package/dist/register/plan-import.js +325 -0
- package/dist/register/records.js +57 -0
- package/dist/register/status-file/estate-parts.js +168 -0
- package/dist/register/status-file/plan-parts.js +68 -0
- package/dist/register/status-file/progress-chart.js +110 -0
- package/dist/register/status-file/register-map.js +221 -0
- package/dist/register/status-file/register-model.js +341 -0
- package/dist/register/status-file/register-parts.js +555 -0
- package/dist/register/status-file/register-status-file.js +189 -0
- package/dist/register/status-file/team-css.js +5 -0
- package/dist/register/status-file-sync.js +22 -0
- package/dist/register/status-html.js +19 -0
- package/dist/register/store.js +40 -2
- package/dist/register/workbooks.js +124 -0
- package/dist/register/write-whole.js +22 -0
- package/dist/register/xlsx.js +127 -0
- package/dist/register/xref.js +117 -0
- package/dist/renderer/startup-lines.js +11 -0
- package/dist/renderer/tool-labels.js +3 -1
- package/dist/renderer/tool-widget.js +15 -1
- package/dist/repl/at-completer.js +6 -2
- package/dist/repl/at-picker.js +1 -1
- package/dist/repl/journey-entry.js +25 -0
- package/dist/repl.js +158 -39
- package/dist/session/backfill.js +342 -0
- package/dist/session/project-match.js +63 -0
- package/dist/session/resume.js +72 -24
- package/dist/session/schema.js +3 -1
- package/dist/session/store.js +96 -51
- package/dist/skill-catalog.js +2 -2
- package/dist/skills/bundled-skills.js +45 -45
- package/dist/tools/_filesystem-shared.js +190 -21
- package/dist/tools/_team-work-path.js +133 -0
- package/dist/tools/approval.js +7 -1
- package/dist/tools/filesystem/extract-xlsx.js +27 -3
- package/dist/tools/filesystem/file-edit.js +12 -4
- package/dist/tools/filesystem/file-read.js +8 -4
- package/dist/tools/filesystem/file-write.js +12 -4
- package/dist/tools/filesystem/glob.js +33 -8
- package/dist/tools/filesystem/grep.js +9 -4
- package/dist/tools/filesystem/read-document.js +6 -3
- package/dist/tools/project/cca_merge.js +8 -2
- package/dist/tools/project/team_estate.js +91 -0
- package/dist/tools/project/team_findings.js +145 -0
- package/dist/tools/project/upgrade_merge_progress.js +8 -2
- package/dist/tools/sap-read.js +21 -4
- package/dist/tools/sap-write.js +203 -5
- package/dist/tools/subagent/read_agent.js +3 -0
- package/dist/tools/tool-loading.js +2 -2
- package/dist/tools/transport-fit-check.js +117 -0
- package/dist/tools/transport-fit.js +15 -7
- package/dist/tools/transport.js +57 -2
- package/dist/ui/app.js +2 -2
- package/dist/ui/approval-modal.js +74 -6
- package/dist/ui/widgets/confirm-card.js +68 -5
- package/dist/util/open-in-browser.js +143 -0
- package/package.json +120 -120
package/README.md
CHANGED
|
@@ -77,6 +77,14 @@ session abc123 │ S4H │ /abap-explain │ 2 turns │ 487 tok │ $0.07 │ s
|
|
|
77
77
|
|
|
78
78
|
On `/exit` you get a session summary + the `cspeach --resume <id>` command in plain text so you can pick up where you left off.
|
|
79
79
|
|
|
80
|
+
To pick up an earlier session:
|
|
81
|
+
|
|
82
|
+
- `cspeach --continue` (or `-c`) — continue the latest session in this folder
|
|
83
|
+
- `cspeach --resume` — pick a session from this folder, or all folders
|
|
84
|
+
- `cspeach --resume <id>` — works from any folder
|
|
85
|
+
|
|
86
|
+
Files always save in the folder you start CSPeach in.
|
|
87
|
+
|
|
80
88
|
## UI modes
|
|
81
89
|
|
|
82
90
|
An interactive terminal gets the standard display (the Ink frame). `/ui auto` (the default) picks it for you; `/ui ink` asks for it explicitly. Both save to `~/.cspeach/config.toml` under `[ui]` and apply after a relaunch.
|
|
@@ -111,7 +119,9 @@ cspeach whoami show signed-in identity + plan/trial
|
|
|
111
119
|
cspeach logout clear local auth
|
|
112
120
|
cspeach config add-sap add a SAP system
|
|
113
121
|
cspeach doctor diagnose install + connectivity
|
|
114
|
-
cspeach --
|
|
122
|
+
cspeach --continue, -c continue the latest session in this folder
|
|
123
|
+
cspeach --resume pick a session from this folder, or all folders
|
|
124
|
+
cspeach --resume <id> works from any folder
|
|
115
125
|
cspeach --upgrade pull the latest CLI release
|
|
116
126
|
cspeach --version
|
|
117
127
|
```
|
package/dist/agent/loop.js
CHANGED
|
@@ -3,6 +3,7 @@ import { basename } 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 { saveSession } from '../session/store.js';
|
|
6
|
+
import { tagSessionOnSave } from '../session/project-match.js';
|
|
6
7
|
import { recordCompletedToolCall } from '../session/pending.js';
|
|
7
8
|
import { loadConfig } from '../config/loader.js';
|
|
8
9
|
import { resolveModelRole, resolveEffort, modelAcceptsEffort, resolveAutoCompactThreshold, callModelFor } from '../models/resolve.js';
|
|
@@ -17,7 +18,7 @@ import { nextWritingActivity } from '../renderer/fence-state.js';
|
|
|
17
18
|
import { renderToolCallTop, renderToolCallBottom, startToolSpinner, startThinkingSpinner, isWidgetSuppressedTool, toolActivityText, toolHeartbeatLabel } from '../renderer/tool-widget.js';
|
|
18
19
|
import { formatToolErrorSummary } from '../sap-errors/parse-adt-exception.js';
|
|
19
20
|
import { startThinkingHeartbeat } from '../renderer/thinking-heartbeat.js';
|
|
20
|
-
import { isDeclaredOneShot } from '../renderer/tty.js';
|
|
21
|
+
import { isDeclaredOneShot, isHeadless } from '../renderer/tty.js';
|
|
21
22
|
import { USER_STOP_REASON } from '../session/interrupt-reason.js';
|
|
22
23
|
import { writeStdout, stdoutFlow } from '../renderer/transcript-flow.js';
|
|
23
24
|
import { formatFailure, formatAttention, formatInfo } from '../renderer/notice-shape.js';
|
|
@@ -40,6 +41,8 @@ import { consumeTodoSetThisTurn } from '../ui/todo-emitter.js';
|
|
|
40
41
|
import { runSaveCommand, runPromoteCommand, detectArtifactSkill, offerAnswerBlockers } from '../projects/index.js';
|
|
41
42
|
import { planLaneBlockForTurn } from '../projects/plan-lane.js';
|
|
42
43
|
import { clearAppFlow, getAppFlow, setGapConfirmed, startAppFlow } from '../lane/app-flow.js';
|
|
44
|
+
import { afterJourneyTurn, gateJourneyForTurn, journeyAsk, journeyCardConfirm, journeyTurnBlock, pauseJourneyForInterruptedTurn } from '../journey/chain.js';
|
|
45
|
+
import { addJourneyLeftOut, addJourneySkips, clearJourneyFlow, getJourneyFlow } from '../journey/flow.js';
|
|
43
46
|
import { afterPlanSave, afterSpecGapNotSaved, afterSpecGapSave, cleanSpecToken, extractSpecToken, FLOW_ENDED_LINE, gateAppFlowForTurn, resolveAppSpec, sameRealFile, STEP_1, STEP_3, } from '../lane/app-chain.js';
|
|
44
47
|
import { shownToken } from '../lane/card.js';
|
|
45
48
|
import { parseFromFlag, coerceDocFromFlagToAttachment } from '../skills/promotion-dispatch.js';
|
|
@@ -68,6 +71,7 @@ import { CacheKeepalive, snapshotParams, recordLastRequest, isCacheablePrompt, n
|
|
|
68
71
|
import { maybeColdPrune } from './cold-prune.js';
|
|
69
72
|
import { createAdtLock, serializeAdt } from '../tools/subagent/adt-serial.js';
|
|
70
73
|
import { hasReaderReadTools, INTERRUPTED_LINE } from '../tools/subagent/reader-prompt.js';
|
|
74
|
+
import { openStatusPage } from '../util/open-in-browser.js';
|
|
71
75
|
/**
|
|
72
76
|
* Author identity for project-file metadata. Reads CSPEACH_AUTHOR_NAME first,
|
|
73
77
|
* then platform USER/USERNAME, then a generic fallback. Role is fixed to
|
|
@@ -98,11 +102,11 @@ const SAVE_HOOK_SKILLS = new Set([
|
|
|
98
102
|
'abap-upgrade-fix',
|
|
99
103
|
'abap-upgrade-verify',
|
|
100
104
|
'abap-upgrade-merge',
|
|
101
|
-
// 2026-05-09: CCA pipeline manifest envelopes. /abap-cca
|
|
102
|
-
// <!-- csforge:cca-manifest --> block wrapping the heavy project.json
|
|
103
|
-
// detail at .cspeach/cca/projects/<slug>/. /abap-cca-merge produces
|
|
105
|
+
// 2026-05-09: CCA pipeline manifest envelopes. /abap-cca-merge produces
|
|
104
106
|
// a consolidated cca-assessment from N parallel consultant slices.
|
|
105
|
-
|
|
107
|
+
// 2026-10-06: the thin /abap-cca (1.1.31) reads the estate inventory and
|
|
108
|
+
// writes no envelope, so it is no longer a save-hook skill. An old
|
|
109
|
+
// cca-manifest in a turn is still offered through detectArtifactSkill.
|
|
106
110
|
'abap-cca-merge',
|
|
107
111
|
// 2026-05-10: v0.7 trio chain. /abap-modernize emits modernize-result;
|
|
108
112
|
// /abap-test emits test-coverage. Both accept cca-assessment via --from
|
|
@@ -283,7 +287,10 @@ export async function maybeOfferSave(p) {
|
|
|
283
287
|
if (!detected)
|
|
284
288
|
return;
|
|
285
289
|
saveSkill = detected;
|
|
286
|
-
|
|
290
|
+
// Inside a journey the triage is saved as part of the step: nothing to offer.
|
|
291
|
+
if (!(p.journey && detected === p.skill)) {
|
|
292
|
+
p.emit(formatInfo(`${detected} output found in this turn (routed as ${p.skill}) · offering to save it`).trimEnd());
|
|
293
|
+
}
|
|
287
294
|
}
|
|
288
295
|
// B5 (2026-06-11) — defect D2: in headless runs the artifact IS the point
|
|
289
296
|
// of the run, so the "Save as project file? [y/N]" prompt defaults YES
|
|
@@ -291,9 +298,12 @@ export async function maybeOfferSave(p) {
|
|
|
291
298
|
// logged so the transcript stays honest about who said yes.
|
|
292
299
|
//
|
|
293
300
|
// CONTRACT: this headless prompt blanket-answers 'y' to ANY question it
|
|
294
|
-
// is asked, so
|
|
295
|
-
//
|
|
296
|
-
//
|
|
301
|
+
// is asked, so `prompt` may carry only ONE question (the save confirm).
|
|
302
|
+
// runSaveCommand asks 1+N cards: the save confirm, then (clean-core
|
|
303
|
+
// decisions) one card per row — and the row cards go through `confirm`
|
|
304
|
+
// only, never `prompt`, so a headless run confirms no row. See the
|
|
305
|
+
// matching note at the prompt site in src/projects/save-command.ts
|
|
306
|
+
// before adding further prompts there.
|
|
297
307
|
const { isHeadless } = await import('../renderer/tty.js');
|
|
298
308
|
const prompt = isHeadless()
|
|
299
309
|
? async (q) => {
|
|
@@ -324,6 +334,9 @@ export async function maybeOfferSave(p) {
|
|
|
324
334
|
// found in another skill's output (detectArtifactSkill) never gets it.
|
|
325
335
|
...(p.lane && p.skill === saveSkill ? { lane: p.lane } : {}),
|
|
326
336
|
onNotSaved: (reason) => { notSaved = reason; },
|
|
337
|
+
...(p.onProjectId ? { onProjectId: p.onProjectId } : {}),
|
|
338
|
+
// Never headless: a headless journey queues nothing.
|
|
339
|
+
...(p.journey && !isHeadless() && p.skill === saveSkill ? { journey: p.journey } : {}),
|
|
327
340
|
});
|
|
328
341
|
// After a spec-gap file is saved, let a developer answer the open BLOCKER
|
|
329
342
|
// questions inline (recorded as `answered` items in the SAME file) — the
|
|
@@ -402,6 +415,49 @@ function readerRunFrom(content, start) {
|
|
|
402
415
|
function stripModelDate(model) {
|
|
403
416
|
return model?.replace(/-\d{8}$/, '');
|
|
404
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* Guided clean-core journey (Task 4) — what the triage save needs from the
|
|
420
|
+
* journey: its card (Ink card slot or a typed line), a typed reason, the
|
|
421
|
+
* session skips, the pause, and the page the triage was shown.
|
|
422
|
+
*/
|
|
423
|
+
function journeySaveFor(session, emit) {
|
|
424
|
+
const page = getJourneyFlow(session)?.page;
|
|
425
|
+
return {
|
|
426
|
+
card: journeyCardConfirm(emit),
|
|
427
|
+
// Esc on "Why keep it?" pauses the journey, as Esc on the card does.
|
|
428
|
+
ask: journeyAsk(),
|
|
429
|
+
skip: (keys) => addJourneySkips(session, { decide: new Set(keys) }),
|
|
430
|
+
leaveForLater: (keys) => addJourneyLeftOut(session, keys),
|
|
431
|
+
pause: () => {
|
|
432
|
+
const line = pauseJourneyForInterruptedTurn(session);
|
|
433
|
+
if (line)
|
|
434
|
+
emit(line);
|
|
435
|
+
},
|
|
436
|
+
...(page ? { page } : {}),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Guided clean-core journey (Task 3) — the chain's hooks from a top-level
|
|
441
|
+
* turn: the session's SAP client, the pending-dispatch slot, the turn's abort
|
|
442
|
+
* signal (Ctrl-C reaches a running check), the turn's own output.
|
|
443
|
+
*/
|
|
444
|
+
function journeyHooks(params, emit) {
|
|
445
|
+
const ctx = params.ctx;
|
|
446
|
+
const standaloneTurn = isStandalone(ctx);
|
|
447
|
+
return {
|
|
448
|
+
session: ctx.session,
|
|
449
|
+
cwd: ctx.cwd,
|
|
450
|
+
headless: isHeadless(),
|
|
451
|
+
emit,
|
|
452
|
+
...(ctx.pendingDispatch ? { queueNext: (cmd) => ctx.pendingDispatch.set(cmd) } : {}),
|
|
453
|
+
...(!standaloneTurn && ctx.adt ? { adt: ctx.adt } : {}),
|
|
454
|
+
...(ctx.sapAlias ? { sapAlias: ctx.sapAlias } : {}),
|
|
455
|
+
...(standaloneTurn ? { noSap: true } : {}),
|
|
456
|
+
...(params.signal ? { signal: params.signal } : {}),
|
|
457
|
+
// Task 6 — the status page opens in the browser after Yes on its card.
|
|
458
|
+
openFile: (file) => openStatusPage(file),
|
|
459
|
+
};
|
|
460
|
+
}
|
|
405
461
|
export async function runTurn(params) {
|
|
406
462
|
// Task 18 — a reader subagent runs INSIDE the parent's turn: it must not
|
|
407
463
|
// reset the parent's Rule 8 batch counter, drain the parent's steering
|
|
@@ -467,6 +523,19 @@ export async function runTurn(params) {
|
|
|
467
523
|
else
|
|
468
524
|
console.log(line);
|
|
469
525
|
}
|
|
526
|
+
// Guided clean-core journey (Task 3) — the journey survives only its own
|
|
527
|
+
// queued step (or a plain reply inside it); any other turn ends the chain
|
|
528
|
+
// with one line. No journey → nothing happens here.
|
|
529
|
+
if (!readerChild && !params.ctx.subagent) {
|
|
530
|
+
const journeyGate = gateJourneyForTurn(session, {
|
|
531
|
+
skill: params.skill,
|
|
532
|
+
userMessage: params.userMessage,
|
|
533
|
+
explicitSlash: params.explicitSlash === true,
|
|
534
|
+
previousSkill: session.skill,
|
|
535
|
+
});
|
|
536
|
+
if (journeyGate.ended && journeyGate.line && !isHeadless())
|
|
537
|
+
emit(journeyGate.line);
|
|
538
|
+
}
|
|
470
539
|
// Phase J — `--from @<source>` phase promotion.
|
|
471
540
|
// If the user typed `/abap-design --from @./spec-gap.cspeach.json` (or the
|
|
472
541
|
// estimate equivalent), strip the flag, validate + snapshot the source,
|
|
@@ -639,6 +708,11 @@ ${imageExpansion.error}`));
|
|
|
639
708
|
}
|
|
640
709
|
if (laneBlock)
|
|
641
710
|
userMessageForLLM = `${userMessageForLLM}\n\n${laneBlock}`;
|
|
711
|
+
// Guided clean-core journey (final fix wave) — the queued triage turn says
|
|
712
|
+
// in its own prompt: no commands, CSPeach asks row by row. '' otherwise.
|
|
713
|
+
const journeyBlock = params.ctx.subagent ? '' : journeyTurnBlock(session, params.skill);
|
|
714
|
+
if (journeyBlock)
|
|
715
|
+
userMessageForLLM = `${userMessageForLLM}\n\n${journeyBlock}`;
|
|
642
716
|
}
|
|
643
717
|
// 2026-05-06: alongside the deterministic prompt-fact extraction
|
|
644
718
|
// (package / transport / etc.), inject the connected SAP system's
|
|
@@ -863,7 +937,7 @@ ${imageExpansion.error}`));
|
|
|
863
937
|
session.turn_seq = (session.turn_seq ?? Math.max(turnNumber - 1, session.lastAutoCompactTurn ?? 0)) + 1;
|
|
864
938
|
// Snapshot session.messages.length at turn start so the save hook can
|
|
865
939
|
// walk every assistant message added during this turn — not just the
|
|
866
|
-
// final one. Skills like /abap-cca emit their manifest in an early
|
|
940
|
+
// final one. Skills like /abap-cca-merge emit their manifest in an early
|
|
867
941
|
// message before a closing ask_question widget; the final wrap-up
|
|
868
942
|
// message ends the turn but doesn't carry the manifest. See
|
|
869
943
|
// ./turn-assistant-text.ts for the full rationale.
|
|
@@ -1526,6 +1600,13 @@ ${imageExpansion.error}`));
|
|
|
1526
1600
|
await turnStreamWriter.close(interruptedError !== null
|
|
1527
1601
|
? `_interrupted: ${session.turnInterruptedReason ?? 'unknown'}_`
|
|
1528
1602
|
: undefined);
|
|
1603
|
+
// Guided clean-core journey (Task 3, fix round 1) — the queued step was
|
|
1604
|
+
// interrupted: pause the journey so "carry on" resumes it on the first try.
|
|
1605
|
+
if (interruptedError !== null && !readerChild && !params.ctx.subagent) {
|
|
1606
|
+
const journeyLine = pauseJourneyForInterruptedTurn(session);
|
|
1607
|
+
if (journeyLine && !isHeadless())
|
|
1608
|
+
emit(journeyLine);
|
|
1609
|
+
}
|
|
1529
1610
|
// Re-throw AFTER persistence so the outer REPL catch can render the
|
|
1530
1611
|
// graceful error UX with full session-id + log-path context.
|
|
1531
1612
|
if (interruptedError !== null)
|
|
@@ -1582,7 +1663,7 @@ ${imageExpansion.error}`));
|
|
|
1582
1663
|
// a background block on the next REPL prompt.
|
|
1583
1664
|
//
|
|
1584
1665
|
// 2026-05-12: collect text from ALL assistant messages added during
|
|
1585
|
-
// this turn, not just the final one. Skills like /abap-cca emit
|
|
1666
|
+
// this turn, not just the final one. Skills like /abap-cca-merge emit
|
|
1586
1667
|
// their manifest in an early message before a closing ask_question
|
|
1587
1668
|
// widget; the final wrap-up message ends the turn but doesn't
|
|
1588
1669
|
// carry the manifest. See ./turn-assistant-text.ts for details.
|
|
@@ -1594,6 +1675,7 @@ ${imageExpansion.error}`));
|
|
|
1594
1675
|
// while the user was the one being waited on (Laeeq's dry run). Pause
|
|
1595
1676
|
// for the whole hook — the row hides while paused — and resume after,
|
|
1596
1677
|
// so any real work that follows (auto-compact) shows again.
|
|
1678
|
+
let projectTagChanged = false;
|
|
1597
1679
|
status.pause();
|
|
1598
1680
|
try {
|
|
1599
1681
|
if (!params.suppressSaveHook) {
|
|
@@ -1615,6 +1697,14 @@ ${imageExpansion.error}`));
|
|
|
1615
1697
|
const flowLane = getAppFlow(session)?.lane;
|
|
1616
1698
|
return flowLane ? { lane: { ...flowLane, cwd: params.ctx.cwd } } : {};
|
|
1617
1699
|
})(),
|
|
1700
|
+
// Sessions/folders (Task 6) — the first save tags the session with
|
|
1701
|
+
// its folder's project id, only when the session belongs to this
|
|
1702
|
+
// folder (ruling 18). Saved to disk right after the hook.
|
|
1703
|
+
onProjectId: (id) => { if (tagSessionOnSave(session, process.cwd(), id))
|
|
1704
|
+
projectTagChanged = true; },
|
|
1705
|
+
// Guided clean-core journey (Task 4) — the queued triage's rows.
|
|
1706
|
+
...(!readerChild && !params.ctx.subagent && getJourneyFlow(session)?.queued?.stage === 'DECIDE'
|
|
1707
|
+
? { journey: journeySaveFor(session, emit) } : {}),
|
|
1618
1708
|
// Task 7 — subagent / reader turns never drive the app chain.
|
|
1619
1709
|
...(!readerChild && !params.ctx.subagent ? {
|
|
1620
1710
|
appChain: {
|
|
@@ -1631,6 +1721,32 @@ ${imageExpansion.error}`));
|
|
|
1631
1721
|
keepalive.disarm();
|
|
1632
1722
|
status.resume();
|
|
1633
1723
|
}
|
|
1724
|
+
// Sessions/folders (Task 6) — the session was saved before the hook;
|
|
1725
|
+
// persist the project id the save just filled in. Best-effort.
|
|
1726
|
+
if (projectTagChanged) {
|
|
1727
|
+
try {
|
|
1728
|
+
await saveSession(session);
|
|
1729
|
+
}
|
|
1730
|
+
catch { /* tagging never fails the turn */ }
|
|
1731
|
+
}
|
|
1732
|
+
// Guided clean-core journey (Task 3) — the queued step's turn ended:
|
|
1733
|
+
// derive the stage from the folder again and go on (the next code
|
|
1734
|
+
// stage, a step card, or the next queued command).
|
|
1735
|
+
if (!readerChild && !params.ctx.subagent && getJourneyFlow(session)?.queued) {
|
|
1736
|
+
try {
|
|
1737
|
+
await afterJourneyTurn(journeyHooks(params, emit), {
|
|
1738
|
+
skill: params.skill,
|
|
1739
|
+
awaitingAnswer: session.awaitingSkillAnswer === true,
|
|
1740
|
+
assistantText,
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
catch (err) {
|
|
1744
|
+
if (process.env['CSPEACH_DEBUG'] === '1')
|
|
1745
|
+
throw err;
|
|
1746
|
+
clearJourneyFlow(session);
|
|
1747
|
+
emit(chalk.yellow('The clean-core steps stopped here. Say carry on to continue.'));
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1634
1750
|
// Phase 2 #9 (2026-05-16) — auto-compact end-of-turn hook.
|
|
1635
1751
|
// Runs only on the success path (after maybeOfferSave) so an interrupted
|
|
1636
1752
|
// turn never triggers a summariser call on partial state. The maybeAutoCompact
|
package/dist/auth/auth-file.js
CHANGED
|
@@ -202,9 +202,9 @@ export async function migrateLegacyByokKey(deps) {
|
|
|
202
202
|
export function legacyByokKeyNotice(outcome) {
|
|
203
203
|
switch (outcome) {
|
|
204
204
|
case 'conflict':
|
|
205
|
-
return 'cspeach:
|
|
205
|
+
return 'cspeach: auth.json in your CSPeach home still holds a pasted Anthropic API key in plain text that is different from the one in your OS keychain (the one CSPeach uses). It was kept in case it is your only copy — remove the "byokAnthropicKey" line from auth.json once you no longer need it.';
|
|
206
206
|
case 'keychain-unavailable':
|
|
207
|
-
return 'cspeach:
|
|
207
|
+
return 'cspeach: auth.json in your CSPeach home still holds your Anthropic API key in plain text — the OS keychain is not available, so it could not be moved there (on Linux: install and unlock a Secret Service keyring such as gnome-keyring). CSPeach will move it on the next start once the keychain works.';
|
|
208
208
|
default:
|
|
209
209
|
return null;
|
|
210
210
|
}
|
package/dist/cli.js
CHANGED
|
@@ -67,6 +67,20 @@ export async function main(argv) {
|
|
|
67
67
|
sapAlias = parsed.sapAlias;
|
|
68
68
|
args = parsed.rest;
|
|
69
69
|
}
|
|
70
|
+
// 2026-10-08 (sessions/folders) — `--continue` / `-c`: the latest session of this folder.
|
|
71
|
+
if (args[0] === '--continue' || args[0] === '-c') {
|
|
72
|
+
if (args[1] !== undefined && !args[1].startsWith('-')) {
|
|
73
|
+
console.error(chalk.red('error: --continue takes no session id. To pick one: cspeach --resume'));
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
// Like --resume: a trailing flag is refused (--sap-alias / --no-sap were taken out above).
|
|
77
|
+
if (args[1] !== undefined) {
|
|
78
|
+
console.error(chalk.red(`error: --continue takes no other flag, not '${args[1]}'. Usage: cspeach --continue`));
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
const { runContinue } = await import('./session/resume.js');
|
|
82
|
+
return runContinue({ sapAlias, noSap });
|
|
83
|
+
}
|
|
70
84
|
if (args[0] === '--resume') {
|
|
71
85
|
// Final batch 5 — `--resume --one-shot "…"`: a flag is never a session id.
|
|
72
86
|
if (args[1]?.startsWith('-')) {
|
|
@@ -153,17 +167,10 @@ export async function main(argv) {
|
|
|
153
167
|
const { input } = await import('@inquirer/prompts');
|
|
154
168
|
const { loadConfig } = await import('./config/loader.js');
|
|
155
169
|
const cfg = await loadConfig();
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }).unref();
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
164
|
-
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
165
|
-
}
|
|
166
|
-
};
|
|
170
|
+
// The shared opener (util/open-in-browser.ts): https only, never through
|
|
171
|
+
// cmd or a shell; rejects when nothing opened, so login prints its own line.
|
|
172
|
+
const { loginOpener } = await import('./util/open-in-browser.js');
|
|
173
|
+
const open = loginOpener();
|
|
167
174
|
return runLoginCommand({
|
|
168
175
|
proxyUrl: cfg.proxy_url,
|
|
169
176
|
open,
|
|
@@ -234,6 +241,11 @@ export async function main(argv) {
|
|
|
234
241
|
console.log('Unknown command: cspeach project. Team projects use: cspeach team init|join|lane|refresh|status|outbox|rule|leave');
|
|
235
242
|
return 1;
|
|
236
243
|
}
|
|
244
|
+
// Sessions/folders (2026-10-08): sort project files from earlier builds into their folders. Copy only.
|
|
245
|
+
if (args[0] === 'files' && args[1] === 'sort') {
|
|
246
|
+
const { runFilesSort } = await import('./projects/sort-command.js');
|
|
247
|
+
return runFilesSort(args.slice(2), { log: (s) => console.log(s), cwd: process.cwd() });
|
|
248
|
+
}
|
|
237
249
|
if (args[0] === 'analyze' && args[1] === 'project-context') {
|
|
238
250
|
const { runProjectContextImpactCommand } = await import('./commands/project-context-impact.js');
|
|
239
251
|
return runProjectContextImpactCommand(args.slice(2));
|
|
@@ -270,7 +282,8 @@ Usage:
|
|
|
270
282
|
cspeach --one-shot "<prompt>" Same as above, explicit flag
|
|
271
283
|
cspeach --sap-alias <name> Use a specific SAP alias (overrides default)
|
|
272
284
|
cspeach --no-sap Start without a SAP system (paste-mode skills only)
|
|
273
|
-
cspeach --
|
|
285
|
+
cspeach --continue, -c Continue the latest session in this folder
|
|
286
|
+
cspeach --resume [id] Pick a session from this folder (or all folders); an id works anywhere
|
|
274
287
|
cspeach config add-sap Add a SAP system interactively
|
|
275
288
|
cspeach config standalone Set the standalone SAP profile (no system)
|
|
276
289
|
cspeach standards init Capture your team's ABAP development standards
|
|
@@ -283,14 +296,27 @@ Usage:
|
|
|
283
296
|
cspeach login Sign in via device-code flow
|
|
284
297
|
cspeach logout Sign out (keep keychain key, if any)
|
|
285
298
|
cspeach whoami Print current account info
|
|
299
|
+
cspeach team discover Read the whole estate from SAP into this folder (read only)
|
|
300
|
+
cspeach team start <packages> Start a team project in this folder: object list, first check, you own it all
|
|
286
301
|
cspeach team init <folder> Lead: pull the object list from SAP, start a team project
|
|
287
302
|
cspeach team join <folder> Record your results against a team project (on this laptop only)
|
|
288
303
|
cspeach team lane add|list Lead: who owns which packages
|
|
304
|
+
cspeach team lane propose --developers "A,B" [--apply] Lead: a first cut of the lanes, balanced by the work
|
|
305
|
+
cspeach team focus "<label>" --packages … --reason "…" Lead: what the team works on now (focus list | clear <label>)
|
|
306
|
+
cspeach team priority show|set|reset The priority rules; set and reset are the lead's
|
|
307
|
+
cspeach team plan move|scope|defer|set|note|history Lead: per-object plan changes, each with a reason
|
|
308
|
+
cspeach team plan import <file.xlsx> [--yes] Lead: read back the Plan sheet edited in Excel; shows the changes, asks once
|
|
289
309
|
cspeach team refresh --lane <id> Measure with ATC: what is verified, what did not hold
|
|
310
|
+
cspeach team refresh --all --per-package [--again] One record per package; skips packages already measured
|
|
290
311
|
cspeach team status [folder] Where every object stands, who owns it, what is proven
|
|
291
312
|
cspeach team outbox What this laptop has recorded, and where
|
|
292
|
-
cspeach team rule T N <d> Lead: settle a conflicting decision
|
|
313
|
+
cspeach team rule T N <d> Lead: settle a conflicting decision (a retire needs --reason)
|
|
314
|
+
cspeach team decide T N <d> Record a clean-core decision: keep, fix, rebuild, retire or exception (a retire needs --reason)
|
|
315
|
+
cspeach team fixed T N Claim a fix; the next refresh checks it
|
|
316
|
+
cspeach team sheet Write your working list (Excel) from the team project
|
|
317
|
+
cspeach team report Lead: the combined workbook and status page, in reports
|
|
293
318
|
cspeach team leave Stop recording
|
|
319
|
+
cspeach files sort [--apply <plan>] [--clean <plan>] Sort files from earlier builds into their folders (copies; --clean deletes checked originals)
|
|
294
320
|
cspeach analyze project-context Compare flag-on vs flag-off session metrics
|
|
295
321
|
cspeach --upgrade Upgrade to latest via npm
|
|
296
322
|
cspeach --version Print version
|
|
@@ -212,7 +212,7 @@ const UNSET_ONLY_KEYS = new Set(['standalone']);
|
|
|
212
212
|
* remains), keeping the top-level entry in `file_keys` is what stops sparse
|
|
213
213
|
* save from dropping the now-default-equal parent table.
|
|
214
214
|
*/
|
|
215
|
-
function unsetConfigKey(cfg, key) {
|
|
215
|
+
export function unsetConfigKey(cfg, key) {
|
|
216
216
|
const parts = key.split('.');
|
|
217
217
|
const top = parts[0];
|
|
218
218
|
const obj = cfg;
|
package/dist/commands/login.js
CHANGED
|
@@ -97,7 +97,7 @@ export async function runLoginCommand(deps) {
|
|
|
97
97
|
apiKey: polled.api_key,
|
|
98
98
|
customerId: polled.customer_id,
|
|
99
99
|
});
|
|
100
|
-
deps.print(done('Saved your CSPeach key to
|
|
100
|
+
deps.print(done('Saved your CSPeach key to auth.json in your CSPeach home'));
|
|
101
101
|
// 6) /v1/me — figure out if BYOK is required.
|
|
102
102
|
try {
|
|
103
103
|
const r = await fetch(`${proxy}/v1/me`, { headers: { Authorization: `Bearer ${polled.api_key}` } });
|
package/dist/commands/logout.js
CHANGED
|
@@ -10,7 +10,7 @@ export async function runLogoutCommand(deps) {
|
|
|
10
10
|
const before = await readAuthFile();
|
|
11
11
|
await deleteAuthFile();
|
|
12
12
|
if (before) {
|
|
13
|
-
deps.print(`${paint('ok', getGlyphs().done)} Signed out · removed
|
|
13
|
+
deps.print(`${paint('ok', getGlyphs().done)} Signed out · removed auth.json from your CSPeach home`);
|
|
14
14
|
deps.print(` ${paint('dim', 'The REPL uses the keychain-stored key if you have one.')}`);
|
|
15
15
|
deps.print(` ${paint('dim', 'Next:')} cspeach login (to sign in again)`);
|
|
16
16
|
}
|
|
@@ -119,7 +119,7 @@ export async function preparePlanContinuation(args) {
|
|
|
119
119
|
// App lane routing — same gate as preparePlanResume: pinned read, record
|
|
120
120
|
// re-validation, stored phases checked against the lane. Before the lease,
|
|
121
121
|
// so a paused plan never leaves a lock behind. Lane-less plans pass.
|
|
122
|
-
const laneGate = await loadPlanLaneForRun(args.cwd ?? process.cwd(), parsed.content);
|
|
122
|
+
const laneGate = await loadPlanLaneForRun(args.cwd ?? process.cwd(), parsed.content, path);
|
|
123
123
|
if (!laneGate.ok) {
|
|
124
124
|
args.log(...laneGate.lines);
|
|
125
125
|
return { prepared: null, stale: false, paused: true };
|
|
@@ -31,8 +31,15 @@ let guardedRunActive = false;
|
|
|
31
31
|
export function setGuardedRunActive(active) {
|
|
32
32
|
guardedRunActive = active;
|
|
33
33
|
}
|
|
34
|
+
/* Guided clean-core journey (2026-10-07, Task 3) — the journey's own share of
|
|
35
|
+
* the same override, kept apart so a plan turn's set/clear and the journey's
|
|
36
|
+
* start/end never undo each other. Off ⇒ byte-identical to before. */
|
|
37
|
+
let journeyRunActive = false;
|
|
38
|
+
export function setJourneyRunActive(active) {
|
|
39
|
+
journeyRunActive = active;
|
|
40
|
+
}
|
|
34
41
|
export function isGuardedRunActive() {
|
|
35
|
-
return guardedRunActive;
|
|
42
|
+
return guardedRunActive || journeyRunActive;
|
|
36
43
|
}
|
|
37
44
|
/* ── plan-phase read-only enforcement (Task 7, 2026-07-03) ─────────────────
|
|
38
45
|
* Module-level flag consulted by tools/subagent/agent_run.ts. The REPL sets
|
|
@@ -260,7 +260,7 @@ export async function preparePlanResume(args) {
|
|
|
260
260
|
// skip this entirely.
|
|
261
261
|
// R5 fix round: the stored phases must also fit the lane, checked here
|
|
262
262
|
// before any phase, lease or transport work.
|
|
263
|
-
const laneGate = await loadPlanLaneForRun(args.cwd, content);
|
|
263
|
+
const laneGate = await loadPlanLaneForRun(args.cwd, content, path);
|
|
264
264
|
if (!laneGate.ok) {
|
|
265
265
|
args.log(...laneGate.lines);
|
|
266
266
|
return null;
|
|
@@ -632,7 +632,7 @@ export async function preparePlanResume(args) {
|
|
|
632
632
|
// 2026-10-04 (plan sources) — the spec-gap items this phase reads, in full,
|
|
633
633
|
// from the file the plan was made from (or the plan's creation snapshot).
|
|
634
634
|
// A phase must never have to hunt the folder for its gap answers.
|
|
635
|
-
const sourcesBlock = renderPlanSourcesBlock(await resolvePlanSources({ cwd: args.cwd, envelope, content, ...(laneRecord ? { laneRecord } : {}) }), next);
|
|
635
|
+
const sourcesBlock = renderPlanSourcesBlock(await resolvePlanSources({ cwd: args.cwd, envelope, content, planPath: path, ...(laneRecord ? { laneRecord } : {}) }), next);
|
|
636
636
|
const llmPrompt = [
|
|
637
637
|
`Resume execution of the project plan "${envelope.title}" (envelope v${envelope.version}).`,
|
|
638
638
|
'',
|
|
@@ -981,14 +981,9 @@ export async function finishPlanResume(args) {
|
|
|
981
981
|
logNotSaved(args, 'the updated plan did not pass its own checks', `revision failed validation: ${check.error.message}`);
|
|
982
982
|
return null;
|
|
983
983
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
}
|
|
988
|
-
catch {
|
|
989
|
-
outDir = process.cwd();
|
|
990
|
-
}
|
|
991
|
-
const savedPath = await saveProject(revision, { cwd: outDir });
|
|
984
|
+
// 2026-10-08 — the new version is saved beside the plan file, wherever it
|
|
985
|
+
// is (an old git root, a copied folder), so a plan family never splits.
|
|
986
|
+
const savedPath = await savePlanVersion(revision, args.prepared.path, args.log);
|
|
992
987
|
// Task 8 — regenerate the write-only markdown handover projection beside
|
|
993
988
|
// the envelope. Best-effort: a failed projection write logs one dim line
|
|
994
989
|
// and never blocks the run (the envelope is the source of truth).
|
|
@@ -1048,6 +1043,23 @@ export async function finishPlanResume(args) {
|
|
|
1048
1043
|
nextTitle: next ? phaseDisplayTitle(next, extract.content.phases) : null,
|
|
1049
1044
|
};
|
|
1050
1045
|
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Final fix wave, item 5 — save a new plan version beside the plan file. When
|
|
1048
|
+
* that write fails (a read-only or missing folder), save it in the working
|
|
1049
|
+
* folder instead, so the run's progress is never lost, and say where.
|
|
1050
|
+
*/
|
|
1051
|
+
async function savePlanVersion(revision, planPath, log) {
|
|
1052
|
+
const beside = dirname(planPath);
|
|
1053
|
+
try {
|
|
1054
|
+
return await saveProject(revision, { cwd: beside });
|
|
1055
|
+
}
|
|
1056
|
+
catch {
|
|
1057
|
+
const ws = await ensureWorkspace();
|
|
1058
|
+
const saved = await saveProject(revision, { cwd: ws });
|
|
1059
|
+
log(`Could not save beside the plan in ${beside}. Saved in ${ws} instead.`);
|
|
1060
|
+
return saved;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1051
1063
|
/**
|
|
1052
1064
|
* Shared revision writer for audit verdicts and chain-stop records: load the
|
|
1053
1065
|
* envelope, patch ONE phase (audit and/or status) + content.lastStop,
|
|
@@ -1103,14 +1115,8 @@ async function savePlanPatch(envelopePath, phaseId, patch, log) {
|
|
|
1103
1115
|
if (!check.ok) {
|
|
1104
1116
|
throw new Error(`audit revision failed validation: ${check.error.message}`);
|
|
1105
1117
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
outDir = await ensureWorkspace();
|
|
1109
|
-
}
|
|
1110
|
-
catch {
|
|
1111
|
-
outDir = dirname(envelopePath);
|
|
1112
|
-
}
|
|
1113
|
-
const saved = await saveProject(revision, { cwd: outDir });
|
|
1118
|
+
// 2026-10-08 — saved beside the plan file (the working folder only as a fallback).
|
|
1119
|
+
const saved = await savePlanVersion(revision, envelopePath, log ?? console.log);
|
|
1114
1120
|
// Task 8 — every envelope save in the plan flow regenerates the markdown
|
|
1115
1121
|
// handover. This is the single chokepoint for ALL chain writes:
|
|
1116
1122
|
// applyAuditResult and applyPhaseResolution both route through here.
|
|
@@ -1,15 +1,45 @@
|
|
|
1
|
+
// cspeach-cli/src/commands/plan-startup.ts
|
|
2
|
+
//
|
|
3
|
+
// 2026-10-04 (continuous run, spec §3.5) — typing `cspeach` in the project
|
|
4
|
+
// folder offers to continue an unfinished plan:
|
|
5
|
+
// one plan → "Continue 'FI Document Aging' (3 of 6 done, next: Service)?"
|
|
6
|
+
// several → "Continue an unfinished plan?", newest first (max 5), + "Not now"
|
|
7
|
+
// Approvals do not survive a restart (the session is new), so Continue shows
|
|
8
|
+
// the plan's object list again and installs a fresh object-list grant
|
|
9
|
+
// (installPlanGrant: same policy as "Build this now?"), then submits the plain
|
|
10
|
+
// resume command (no mode flag: the user's plan_mode decides).
|
|
11
|
+
//
|
|
12
|
+
// F13 — a resume the user typed in guarded mode, with no grant for that plan,
|
|
13
|
+
// shows the same object list once before the step (offerObjectListOnResume).
|
|
14
|
+
// Yes installs the grant; No runs the step with a card per write. Never on a
|
|
15
|
+
// chain-queued hop, never twice for one Continue.
|
|
16
|
+
import { basename, dirname } from 'node:path';
|
|
1
17
|
import { readProjectFile } from '../projects/status.js';
|
|
2
18
|
import { parsePlanContent } from '../projects/plan-schema.js';
|
|
3
19
|
import { computeNextPhaseForRun, isPhaseSatisfiedForRun, phaseDisplayTitle, statusesFromItems, } from '../projects/plan-run.js';
|
|
4
20
|
import { collectPlannedObjects, plannedObjectFacts, planGrantFactLines } from '../projects/plan-objects.js';
|
|
5
21
|
import { adoptPlanTransportOnResume, buildResumeCommand, planBuildTransport } from './plan-resume.js';
|
|
6
22
|
import { newestPlanFamilyFiles } from '../session/recap.js';
|
|
23
|
+
import { legacyDirsFor } from '../projects/project-folder.js';
|
|
7
24
|
import { getPlanGrant, installPlanGrant, planGrantAllowed } from '../approvals/plan-grant.js';
|
|
8
25
|
import { DEFAULT_PLAN_MODE } from './plan-gate.js';
|
|
9
|
-
/**
|
|
10
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Plans in the folder with a step left to build, newest first. With `cwd`,
|
|
28
|
+
* the old save locations for that folder are read too (Task 11, one
|
|
29
|
+
* release), after the new folder; a same-name file in the new folder wins.
|
|
30
|
+
*/
|
|
31
|
+
export function listUnfinishedPlans(workspaceDir, max = 5, cwd) {
|
|
11
32
|
const out = [];
|
|
12
|
-
|
|
33
|
+
const dirs = [workspaceDir, ...(cwd ? safeLegacyDirs(cwd) : [])];
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
const files = dirs.flatMap((d, i) => newestPlanFamilyFiles(d).map((f) => ({ ...f, earlier: i > 0 }))).filter((f) => {
|
|
36
|
+
const name = basename(f.path).toLowerCase();
|
|
37
|
+
if (seen.has(name))
|
|
38
|
+
return false;
|
|
39
|
+
seen.add(name);
|
|
40
|
+
return true;
|
|
41
|
+
});
|
|
42
|
+
for (const f of files) {
|
|
13
43
|
if (out.length >= max)
|
|
14
44
|
break;
|
|
15
45
|
let env;
|
|
@@ -42,13 +72,23 @@ export function listUnfinishedPlans(workspaceDir, max = 5) {
|
|
|
42
72
|
nextTitle: phaseDisplayTitle(next, phases),
|
|
43
73
|
objects: collectPlannedObjects(pc.content),
|
|
44
74
|
...(transport ? { transport } : {}),
|
|
75
|
+
...(f.earlier ? { earlier: true } : {}),
|
|
45
76
|
});
|
|
46
77
|
}
|
|
47
78
|
return out;
|
|
48
79
|
}
|
|
80
|
+
function safeLegacyDirs(cwd) {
|
|
81
|
+
try {
|
|
82
|
+
return legacyDirsFor(cwd);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
49
88
|
function progress(p) {
|
|
50
89
|
return `${p.done} of ${p.total} done, next: ${p.nextTitle ?? 'the independent check'}`;
|
|
51
90
|
}
|
|
91
|
+
const EARLIER = '(earlier location)';
|
|
52
92
|
export function continueQuestion(p) {
|
|
53
93
|
return `Continue '${p.title}' (${progress(p)})?`;
|
|
54
94
|
}
|
|
@@ -97,7 +137,7 @@ function grantInstalledFor(planId, sessionId) {
|
|
|
97
137
|
&& typeof g.expiresAt === 'number' && Date.now() < g.expiresAt;
|
|
98
138
|
}
|
|
99
139
|
export async function offerContinueAtStartup(a) {
|
|
100
|
-
const plans = listUnfinishedPlans(a.workspaceDir);
|
|
140
|
+
const plans = listUnfinishedPlans(a.workspaceDir, 5, a.cwd);
|
|
101
141
|
if (plans.length === 0)
|
|
102
142
|
return 'none';
|
|
103
143
|
const allowed = a.grantAllowed ?? planGrantAllowed;
|
|
@@ -109,7 +149,10 @@ export async function offerContinueAtStartup(a) {
|
|
|
109
149
|
grantable = p.objects.length > 0 && await allowed(a.sapAlias).catch(() => false);
|
|
110
150
|
const v = await a.choose({
|
|
111
151
|
title: continueQuestion(p),
|
|
112
|
-
facts:
|
|
152
|
+
facts: [
|
|
153
|
+
...(p.earlier ? [`This plan is in an earlier location: ${dirname(p.path)}`] : []),
|
|
154
|
+
...objectListFacts(p.objects, p.transport, grantable),
|
|
155
|
+
],
|
|
113
156
|
choices: [{ value: 'continue', label: 'Continue' }, { value: 'later', label: 'Not now' }],
|
|
114
157
|
});
|
|
115
158
|
if (v === 'continue') {
|
|
@@ -122,7 +165,7 @@ export async function offerContinueAtStartup(a) {
|
|
|
122
165
|
title: 'Continue an unfinished plan?',
|
|
123
166
|
facts: ['Newest first.'],
|
|
124
167
|
choices: [
|
|
125
|
-
...plans.map((p, i) => ({ value: String(i), label: `${p.title} (${progress(p)})` })),
|
|
168
|
+
...plans.map((p, i) => ({ value: String(i), label: `${p.title} (${progress(p)})${p.earlier ? ` ${EARLIER}` : ''}` })),
|
|
126
169
|
{ value: 'later', label: 'Not now' },
|
|
127
170
|
],
|
|
128
171
|
});
|