@cspeach/cli 1.1.30 → 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 +121 -8
- package/dist/cli.js +36 -14
- package/dist/commands/config-set.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 +375 -41
- 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 +20 -1
- package/dist/projects/clean-core-confirm.js +156 -0
- package/dist/projects/detail-path.js +9 -4
- package/dist/projects/extract-clean-core.js +3 -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 +83 -12
- 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/workspace-retire.js +48 -0
- package/dist/projects/workspace.js +182 -121
- package/dist/register/discover.js +901 -0
- package/dist/register/findings.js +5 -2
- package/dist/register/offer.js +100 -8
- package/dist/register/picture.js +6 -2
- package/dist/register/plan-import.js +325 -0
- package/dist/register/records.js +25 -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 +11 -3
- package/dist/register/status-file/register-model.js +40 -6
- package/dist/register/status-file/register-parts.js +57 -11
- package/dist/register/status-file/register-status-file.js +24 -6
- package/dist/register/status-file/team-css.js +1 -1
- package/dist/register/status-file-sync.js +3 -0
- package/dist/register/status-html.js +3 -1
- package/dist/register/store.js +40 -2
- package/dist/register/workbooks.js +27 -7
- package/dist/register/xlsx.js +13 -3
- package/dist/register/xref.js +117 -0
- package/dist/renderer/startup-lines.js +11 -0
- package/dist/renderer/tool-labels.js +2 -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 +157 -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 +1 -1
- package/dist/tools/_filesystem-shared.js +174 -12
- 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 +77 -4
- package/dist/tools/project/upgrade_merge_progress.js +8 -2
- package/dist/tools/sap-read.js +21 -4
- package/dist/tools/sap-write.js +200 -10
- package/dist/tools/subagent/read_agent.js +3 -0
- package/dist/tools/tool-loading.js +1 -1
- 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
|
|
@@ -327,6 +334,9 @@ export async function maybeOfferSave(p) {
|
|
|
327
334
|
// found in another skill's output (detectArtifactSkill) never gets it.
|
|
328
335
|
...(p.lane && p.skill === saveSkill ? { lane: p.lane } : {}),
|
|
329
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 } : {}),
|
|
330
340
|
});
|
|
331
341
|
// After a spec-gap file is saved, let a developer answer the open BLOCKER
|
|
332
342
|
// questions inline (recorded as `answered` items in the SAME file) — the
|
|
@@ -405,6 +415,49 @@ function readerRunFrom(content, start) {
|
|
|
405
415
|
function stripModelDate(model) {
|
|
406
416
|
return model?.replace(/-\d{8}$/, '');
|
|
407
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
|
+
}
|
|
408
461
|
export async function runTurn(params) {
|
|
409
462
|
// Task 18 — a reader subagent runs INSIDE the parent's turn: it must not
|
|
410
463
|
// reset the parent's Rule 8 batch counter, drain the parent's steering
|
|
@@ -470,6 +523,19 @@ export async function runTurn(params) {
|
|
|
470
523
|
else
|
|
471
524
|
console.log(line);
|
|
472
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
|
+
}
|
|
473
539
|
// Phase J — `--from @<source>` phase promotion.
|
|
474
540
|
// If the user typed `/abap-design --from @./spec-gap.cspeach.json` (or the
|
|
475
541
|
// estimate equivalent), strip the flag, validate + snapshot the source,
|
|
@@ -642,6 +708,11 @@ ${imageExpansion.error}`));
|
|
|
642
708
|
}
|
|
643
709
|
if (laneBlock)
|
|
644
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}`;
|
|
645
716
|
}
|
|
646
717
|
// 2026-05-06: alongside the deterministic prompt-fact extraction
|
|
647
718
|
// (package / transport / etc.), inject the connected SAP system's
|
|
@@ -866,7 +937,7 @@ ${imageExpansion.error}`));
|
|
|
866
937
|
session.turn_seq = (session.turn_seq ?? Math.max(turnNumber - 1, session.lastAutoCompactTurn ?? 0)) + 1;
|
|
867
938
|
// Snapshot session.messages.length at turn start so the save hook can
|
|
868
939
|
// walk every assistant message added during this turn — not just the
|
|
869
|
-
// 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
|
|
870
941
|
// message before a closing ask_question widget; the final wrap-up
|
|
871
942
|
// message ends the turn but doesn't carry the manifest. See
|
|
872
943
|
// ./turn-assistant-text.ts for the full rationale.
|
|
@@ -1529,6 +1600,13 @@ ${imageExpansion.error}`));
|
|
|
1529
1600
|
await turnStreamWriter.close(interruptedError !== null
|
|
1530
1601
|
? `_interrupted: ${session.turnInterruptedReason ?? 'unknown'}_`
|
|
1531
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
|
+
}
|
|
1532
1610
|
// Re-throw AFTER persistence so the outer REPL catch can render the
|
|
1533
1611
|
// graceful error UX with full session-id + log-path context.
|
|
1534
1612
|
if (interruptedError !== null)
|
|
@@ -1585,7 +1663,7 @@ ${imageExpansion.error}`));
|
|
|
1585
1663
|
// a background block on the next REPL prompt.
|
|
1586
1664
|
//
|
|
1587
1665
|
// 2026-05-12: collect text from ALL assistant messages added during
|
|
1588
|
-
// 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
|
|
1589
1667
|
// their manifest in an early message before a closing ask_question
|
|
1590
1668
|
// widget; the final wrap-up message ends the turn but doesn't
|
|
1591
1669
|
// carry the manifest. See ./turn-assistant-text.ts for details.
|
|
@@ -1597,6 +1675,7 @@ ${imageExpansion.error}`));
|
|
|
1597
1675
|
// while the user was the one being waited on (Laeeq's dry run). Pause
|
|
1598
1676
|
// for the whole hook — the row hides while paused — and resume after,
|
|
1599
1677
|
// so any real work that follows (auto-compact) shows again.
|
|
1678
|
+
let projectTagChanged = false;
|
|
1600
1679
|
status.pause();
|
|
1601
1680
|
try {
|
|
1602
1681
|
if (!params.suppressSaveHook) {
|
|
@@ -1618,6 +1697,14 @@ ${imageExpansion.error}`));
|
|
|
1618
1697
|
const flowLane = getAppFlow(session)?.lane;
|
|
1619
1698
|
return flowLane ? { lane: { ...flowLane, cwd: params.ctx.cwd } } : {};
|
|
1620
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) } : {}),
|
|
1621
1708
|
// Task 7 — subagent / reader turns never drive the app chain.
|
|
1622
1709
|
...(!readerChild && !params.ctx.subagent ? {
|
|
1623
1710
|
appChain: {
|
|
@@ -1634,6 +1721,32 @@ ${imageExpansion.error}`));
|
|
|
1634
1721
|
keepalive.disarm();
|
|
1635
1722
|
status.resume();
|
|
1636
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
|
+
}
|
|
1637
1750
|
// Phase 2 #9 (2026-05-16) — auto-compact end-of-turn hook.
|
|
1638
1751
|
// Runs only on the success path (after maybeOfferSave) so an interrupted
|
|
1639
1752
|
// turn never triggers a summariser call on partial state. The maybeAutoCompact
|
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,18 +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
|
|
293
|
-
cspeach team decide T N <d> Record a clean-core decision: keep, fix, rebuild, retire or exception
|
|
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)
|
|
294
315
|
cspeach team fixed T N Claim a fix; the next refresh checks it
|
|
295
316
|
cspeach team sheet Write your working list (Excel) from the team project
|
|
296
317
|
cspeach team report Lead: the combined workbook and status page, in reports
|
|
297
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)
|
|
298
320
|
cspeach analyze project-context Compare flag-on vs flag-off session metrics
|
|
299
321
|
cspeach --upgrade Upgrade to latest via npm
|
|
300
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;
|
|
@@ -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
|
});
|