@cspeach/cli 1.1.30 → 1.1.32
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/README.md +11 -1
- package/dist/agent/loop.js +132 -9
- package/dist/cli.js +49 -18
- package/dist/commands/config-set.js +1 -1
- package/dist/commands/help.js +16 -1
- package/dist/commands/plan-chain.js +3 -0
- package/dist/commands/plan-continue.js +1 -1
- package/dist/commands/plan-gate.js +17 -1
- package/dist/commands/plan-resume.js +28 -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-rules.js +187 -0
- package/dist/commands/team.js +971 -95
- package/dist/journey/chain.js +978 -0
- package/dist/journey/clean-core.js +235 -0
- package/dist/journey/flow.js +216 -0
- package/dist/journey/rebuild.js +464 -0
- package/dist/journey/run.js +628 -0
- package/dist/journey/session-deps.js +81 -0
- package/dist/journey/trigger.js +153 -0
- package/dist/lane/app-chain.js +21 -9
- package/dist/lane/chain-token.js +9 -5
- package/dist/lane/roots.js +97 -41
- package/dist/one-shot.js +28 -2
- package/dist/projects/clean-core-confirm.js +192 -3
- 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 +100 -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/baseline.js +39 -2
- package/dist/register/decision-rules.js +35 -0
- package/dist/register/discover.js +931 -0
- package/dist/register/export-files.js +58 -0
- package/dist/register/findings.js +5 -2
- package/dist/register/measure.js +191 -9
- package/dist/register/offer.js +100 -8
- package/dist/register/picture.js +6 -2
- package/dist/register/plan-import.js +328 -0
- package/dist/register/records.js +40 -3
- package/dist/register/rule-proposal.js +4 -0
- package/dist/register/rules-edit.js +257 -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 +106 -90
- package/dist/register/status-file/report-model.js +569 -0
- package/dist/register/status-file/team-css.js +1 -1
- package/dist/register/status-file-sync.js +4 -0
- package/dist/register/status-html.js +27 -6
- package/dist/register/store.js +44 -5
- package/dist/register/variant.js +65 -0
- package/dist/register/workbooks.js +28 -42
- package/dist/register/write-whole.js +30 -2
- 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 +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/builtin-commands.js +4 -0
- package/dist/repl/journey-entry.js +27 -0
- package/dist/repl/plan-hop.js +2 -9
- package/dist/repl/rules-menu.js +177 -0
- package/dist/repl/slash-completer.js +1 -0
- package/dist/repl/slash-picker.js +5 -4
- package/dist/repl.js +236 -59
- package/dist/router/classifier.js +8 -4
- package/dist/router/router-core.js +43 -7
- package/dist/router/routing-decision.js +1 -1
- package/dist/runs/bundled-runs.js +100 -0
- package/dist/runs/front-door.js +19 -0
- package/dist/runs/manifest-runs.js +50 -0
- package/dist/runs/run-command.js +24 -0
- package/dist/runs/runs.js +27 -0
- package/dist/runs/shadow.js +100 -0
- package/dist/runs/types.js +1 -0
- package/dist/session/backfill.js +342 -0
- package/dist/session/project-match.js +63 -0
- package/dist/session/queued-step.js +45 -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 +53 -53
- 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/dispatch-skill.js +8 -4
- 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/lane/app-lane-resolve.js +1 -1
- package/dist/tools/project/cca_merge.js +8 -2
- package/dist/tools/project/rules_propose.js +59 -0
- package/dist/tools/project/team_estate.js +91 -0
- package/dist/tools/project/team_findings.js +89 -5
- 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/upgrade-check.js +49 -4
- package/dist/util/open-in-browser.js +143 -0
- package/package.json +3 -3
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,9 @@ 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 { afterRebuildTurn } from '../journey/rebuild.js';
|
|
45
|
+
import { afterJourneyTurn, gateJourneyForTurn, journeyAsk, journeyCardConfirm, journeyStoppedOnError, journeyTurnBlock, pauseJourneyForInterruptedTurn } from '../journey/chain.js';
|
|
46
|
+
import { addJourneyLeftOut, addJourneySkips, getJourneyFlow } from '../journey/flow.js';
|
|
43
47
|
import { afterPlanSave, afterSpecGapNotSaved, afterSpecGapSave, cleanSpecToken, extractSpecToken, FLOW_ENDED_LINE, gateAppFlowForTurn, resolveAppSpec, sameRealFile, STEP_1, STEP_3, } from '../lane/app-chain.js';
|
|
44
48
|
import { shownToken } from '../lane/card.js';
|
|
45
49
|
import { parseFromFlag, coerceDocFromFlagToAttachment } from '../skills/promotion-dispatch.js';
|
|
@@ -68,6 +72,7 @@ import { CacheKeepalive, snapshotParams, recordLastRequest, isCacheablePrompt, n
|
|
|
68
72
|
import { maybeColdPrune } from './cold-prune.js';
|
|
69
73
|
import { createAdtLock, serializeAdt } from '../tools/subagent/adt-serial.js';
|
|
70
74
|
import { hasReaderReadTools, INTERRUPTED_LINE } from '../tools/subagent/reader-prompt.js';
|
|
75
|
+
import { openStatusPage } from '../util/open-in-browser.js';
|
|
71
76
|
/**
|
|
72
77
|
* Author identity for project-file metadata. Reads CSPEACH_AUTHOR_NAME first,
|
|
73
78
|
* then platform USER/USERNAME, then a generic fallback. Role is fixed to
|
|
@@ -98,11 +103,11 @@ const SAVE_HOOK_SKILLS = new Set([
|
|
|
98
103
|
'abap-upgrade-fix',
|
|
99
104
|
'abap-upgrade-verify',
|
|
100
105
|
'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
|
|
106
|
+
// 2026-05-09: CCA pipeline manifest envelopes. /abap-cca-merge produces
|
|
104
107
|
// a consolidated cca-assessment from N parallel consultant slices.
|
|
105
|
-
|
|
108
|
+
// 2026-10-06: the thin /abap-cca (1.1.31) reads the estate inventory and
|
|
109
|
+
// writes no envelope, so it is no longer a save-hook skill. An old
|
|
110
|
+
// cca-manifest in a turn is still offered through detectArtifactSkill.
|
|
106
111
|
'abap-cca-merge',
|
|
107
112
|
// 2026-05-10: v0.7 trio chain. /abap-modernize emits modernize-result;
|
|
108
113
|
// /abap-test emits test-coverage. Both accept cca-assessment via --from
|
|
@@ -283,7 +288,10 @@ export async function maybeOfferSave(p) {
|
|
|
283
288
|
if (!detected)
|
|
284
289
|
return;
|
|
285
290
|
saveSkill = detected;
|
|
286
|
-
|
|
291
|
+
// Inside a journey the triage is saved as part of the step: nothing to offer.
|
|
292
|
+
if (!(p.journey && detected === p.skill)) {
|
|
293
|
+
p.emit(formatInfo(`${detected} output found in this turn (routed as ${p.skill}) · offering to save it`).trimEnd());
|
|
294
|
+
}
|
|
287
295
|
}
|
|
288
296
|
// B5 (2026-06-11) — defect D2: in headless runs the artifact IS the point
|
|
289
297
|
// of the run, so the "Save as project file? [y/N]" prompt defaults YES
|
|
@@ -327,6 +335,9 @@ export async function maybeOfferSave(p) {
|
|
|
327
335
|
// found in another skill's output (detectArtifactSkill) never gets it.
|
|
328
336
|
...(p.lane && p.skill === saveSkill ? { lane: p.lane } : {}),
|
|
329
337
|
onNotSaved: (reason) => { notSaved = reason; },
|
|
338
|
+
...(p.onProjectId ? { onProjectId: p.onProjectId } : {}),
|
|
339
|
+
// Never headless: a headless journey queues nothing.
|
|
340
|
+
...(p.journey && !isHeadless() && p.skill === saveSkill ? { journey: p.journey } : {}),
|
|
330
341
|
});
|
|
331
342
|
// After a spec-gap file is saved, let a developer answer the open BLOCKER
|
|
332
343
|
// questions inline (recorded as `answered` items in the SAME file) — the
|
|
@@ -405,6 +416,49 @@ function readerRunFrom(content, start) {
|
|
|
405
416
|
function stripModelDate(model) {
|
|
406
417
|
return model?.replace(/-\d{8}$/, '');
|
|
407
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Guided clean-core journey (Task 4) — what the triage save needs from the
|
|
421
|
+
* journey: its card (Ink card slot or a typed line), a typed reason, the
|
|
422
|
+
* session skips, the pause, and the page the triage was shown.
|
|
423
|
+
*/
|
|
424
|
+
function journeySaveFor(session, emit) {
|
|
425
|
+
const page = getJourneyFlow(session)?.page;
|
|
426
|
+
return {
|
|
427
|
+
card: journeyCardConfirm(emit),
|
|
428
|
+
// Esc on "Why keep it?" pauses the journey, as Esc on the card does.
|
|
429
|
+
ask: journeyAsk(),
|
|
430
|
+
skip: (keys) => addJourneySkips(session, { decide: new Set(keys) }),
|
|
431
|
+
leaveForLater: (keys) => addJourneyLeftOut(session, keys),
|
|
432
|
+
pause: () => {
|
|
433
|
+
const line = pauseJourneyForInterruptedTurn(session);
|
|
434
|
+
if (line)
|
|
435
|
+
emit(line);
|
|
436
|
+
},
|
|
437
|
+
...(page ? { page } : {}),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Guided clean-core journey (Task 3) — the chain's hooks from a top-level
|
|
442
|
+
* turn: the session's SAP client, the pending-dispatch slot, the turn's abort
|
|
443
|
+
* signal (Ctrl-C reaches a running check), the turn's own output.
|
|
444
|
+
*/
|
|
445
|
+
function journeyHooks(params, emit) {
|
|
446
|
+
const ctx = params.ctx;
|
|
447
|
+
const standaloneTurn = isStandalone(ctx);
|
|
448
|
+
return {
|
|
449
|
+
session: ctx.session,
|
|
450
|
+
cwd: ctx.cwd,
|
|
451
|
+
headless: isHeadless(),
|
|
452
|
+
emit,
|
|
453
|
+
...(ctx.pendingDispatch ? { queueNext: (cmd, origin) => ctx.pendingDispatch.set(cmd, origin) } : {}),
|
|
454
|
+
...(!standaloneTurn && ctx.adt ? { adt: ctx.adt } : {}),
|
|
455
|
+
...(ctx.sapAlias ? { sapAlias: ctx.sapAlias } : {}),
|
|
456
|
+
...(standaloneTurn ? { noSap: true } : {}),
|
|
457
|
+
...(params.signal ? { signal: params.signal } : {}),
|
|
458
|
+
// Task 6 — the status page opens in the browser after Yes on its card.
|
|
459
|
+
openFile: (file) => openStatusPage(file),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
408
462
|
export async function runTurn(params) {
|
|
409
463
|
// Task 18 — a reader subagent runs INSIDE the parent's turn: it must not
|
|
410
464
|
// reset the parent's Rule 8 batch counter, drain the parent's steering
|
|
@@ -470,6 +524,19 @@ export async function runTurn(params) {
|
|
|
470
524
|
else
|
|
471
525
|
console.log(line);
|
|
472
526
|
}
|
|
527
|
+
// Guided clean-core journey (Task 3) — the journey survives only its own
|
|
528
|
+
// queued step (or a plain reply inside it); any other turn ends the chain
|
|
529
|
+
// with one line. No journey → nothing happens here.
|
|
530
|
+
if (!readerChild && !params.ctx.subagent) {
|
|
531
|
+
const journeyGate = gateJourneyForTurn(session, {
|
|
532
|
+
skill: params.skill,
|
|
533
|
+
userMessage: params.userMessage,
|
|
534
|
+
explicitSlash: params.explicitSlash === true,
|
|
535
|
+
previousSkill: session.skill,
|
|
536
|
+
});
|
|
537
|
+
if (journeyGate.ended && journeyGate.line && !isHeadless())
|
|
538
|
+
emit(journeyGate.line);
|
|
539
|
+
}
|
|
473
540
|
// Phase J — `--from @<source>` phase promotion.
|
|
474
541
|
// If the user typed `/abap-design --from @./spec-gap.cspeach.json` (or the
|
|
475
542
|
// estimate equivalent), strip the flag, validate + snapshot the source,
|
|
@@ -642,6 +709,11 @@ ${imageExpansion.error}`));
|
|
|
642
709
|
}
|
|
643
710
|
if (laneBlock)
|
|
644
711
|
userMessageForLLM = `${userMessageForLLM}\n\n${laneBlock}`;
|
|
712
|
+
// Guided clean-core journey (final fix wave) — the queued triage turn says
|
|
713
|
+
// in its own prompt: no commands, CSPeach asks row by row. '' otherwise.
|
|
714
|
+
const journeyBlock = params.ctx.subagent ? '' : journeyTurnBlock(session, params.skill);
|
|
715
|
+
if (journeyBlock)
|
|
716
|
+
userMessageForLLM = `${userMessageForLLM}\n\n${journeyBlock}`;
|
|
645
717
|
}
|
|
646
718
|
// 2026-05-06: alongside the deterministic prompt-fact extraction
|
|
647
719
|
// (package / transport / etc.), inject the connected SAP system's
|
|
@@ -866,7 +938,7 @@ ${imageExpansion.error}`));
|
|
|
866
938
|
session.turn_seq = (session.turn_seq ?? Math.max(turnNumber - 1, session.lastAutoCompactTurn ?? 0)) + 1;
|
|
867
939
|
// Snapshot session.messages.length at turn start so the save hook can
|
|
868
940
|
// walk every assistant message added during this turn — not just the
|
|
869
|
-
// final one. Skills like /abap-cca emit their manifest in an early
|
|
941
|
+
// final one. Skills like /abap-cca-merge emit their manifest in an early
|
|
870
942
|
// message before a closing ask_question widget; the final wrap-up
|
|
871
943
|
// message ends the turn but doesn't carry the manifest. See
|
|
872
944
|
// ./turn-assistant-text.ts for the full rationale.
|
|
@@ -1529,6 +1601,13 @@ ${imageExpansion.error}`));
|
|
|
1529
1601
|
await turnStreamWriter.close(interruptedError !== null
|
|
1530
1602
|
? `_interrupted: ${session.turnInterruptedReason ?? 'unknown'}_`
|
|
1531
1603
|
: undefined);
|
|
1604
|
+
// Guided clean-core journey (Task 3, fix round 1) — the queued step was
|
|
1605
|
+
// interrupted: pause the journey so "carry on" resumes it on the first try.
|
|
1606
|
+
if (interruptedError !== null && !readerChild && !params.ctx.subagent) {
|
|
1607
|
+
const journeyLine = pauseJourneyForInterruptedTurn(session);
|
|
1608
|
+
if (journeyLine && !isHeadless())
|
|
1609
|
+
emit(journeyLine);
|
|
1610
|
+
}
|
|
1532
1611
|
// Re-throw AFTER persistence so the outer REPL catch can render the
|
|
1533
1612
|
// graceful error UX with full session-id + log-path context.
|
|
1534
1613
|
if (interruptedError !== null)
|
|
@@ -1585,7 +1664,7 @@ ${imageExpansion.error}`));
|
|
|
1585
1664
|
// a background block on the next REPL prompt.
|
|
1586
1665
|
//
|
|
1587
1666
|
// 2026-05-12: collect text from ALL assistant messages added during
|
|
1588
|
-
// this turn, not just the final one. Skills like /abap-cca emit
|
|
1667
|
+
// this turn, not just the final one. Skills like /abap-cca-merge emit
|
|
1589
1668
|
// their manifest in an early message before a closing ask_question
|
|
1590
1669
|
// widget; the final wrap-up message ends the turn but doesn't
|
|
1591
1670
|
// carry the manifest. See ./turn-assistant-text.ts for details.
|
|
@@ -1597,6 +1676,7 @@ ${imageExpansion.error}`));
|
|
|
1597
1676
|
// while the user was the one being waited on (Laeeq's dry run). Pause
|
|
1598
1677
|
// for the whole hook — the row hides while paused — and resume after,
|
|
1599
1678
|
// so any real work that follows (auto-compact) shows again.
|
|
1679
|
+
let projectTagChanged = false;
|
|
1600
1680
|
status.pause();
|
|
1601
1681
|
try {
|
|
1602
1682
|
if (!params.suppressSaveHook) {
|
|
@@ -1618,12 +1698,20 @@ ${imageExpansion.error}`));
|
|
|
1618
1698
|
const flowLane = getAppFlow(session)?.lane;
|
|
1619
1699
|
return flowLane ? { lane: { ...flowLane, cwd: params.ctx.cwd } } : {};
|
|
1620
1700
|
})(),
|
|
1701
|
+
// Sessions/folders (Task 6) — the first save tags the session with
|
|
1702
|
+
// its folder's project id, only when the session belongs to this
|
|
1703
|
+
// folder (ruling 18). Saved to disk right after the hook.
|
|
1704
|
+
onProjectId: (id) => { if (tagSessionOnSave(session, process.cwd(), id))
|
|
1705
|
+
projectTagChanged = true; },
|
|
1706
|
+
// Guided clean-core journey (Task 4) — the queued triage's rows.
|
|
1707
|
+
...(!readerChild && !params.ctx.subagent && getJourneyFlow(session)?.queued?.stage === 'DECIDE'
|
|
1708
|
+
? { journey: journeySaveFor(session, emit) } : {}),
|
|
1621
1709
|
// Task 7 — subagent / reader turns never drive the app chain.
|
|
1622
1710
|
...(!readerChild && !params.ctx.subagent ? {
|
|
1623
1711
|
appChain: {
|
|
1624
1712
|
session,
|
|
1625
1713
|
cwd: params.ctx.cwd,
|
|
1626
|
-
...(params.ctx.pendingDispatch ? { queueNext: (cmd) => params.ctx.pendingDispatch.set(cmd) } : {}),
|
|
1714
|
+
...(params.ctx.pendingDispatch ? { queueNext: (cmd, origin) => params.ctx.pendingDispatch.set(cmd, origin) } : {}),
|
|
1627
1715
|
...(params.ctx.sapAlias && params.ctx.sapAlias !== 'none' ? { sapAlias: params.ctx.sapAlias } : {}),
|
|
1628
1716
|
},
|
|
1629
1717
|
} : {}),
|
|
@@ -1634,6 +1722,41 @@ ${imageExpansion.error}`));
|
|
|
1634
1722
|
keepalive.disarm();
|
|
1635
1723
|
status.resume();
|
|
1636
1724
|
}
|
|
1725
|
+
// Sessions/folders (Task 6) — the session was saved before the hook;
|
|
1726
|
+
// persist the project id the save just filled in. Best-effort.
|
|
1727
|
+
if (projectTagChanged) {
|
|
1728
|
+
try {
|
|
1729
|
+
await saveSession(session);
|
|
1730
|
+
}
|
|
1731
|
+
catch { /* tagging never fails the turn */ }
|
|
1732
|
+
}
|
|
1733
|
+
// Guided clean-core journey (Task 3) — the queued step's turn ended:
|
|
1734
|
+
// derive the stage from the folder again and go on (the next code
|
|
1735
|
+
// stage, a step card, or the next queued command).
|
|
1736
|
+
if (!readerChild && !params.ctx.subagent && getJourneyFlow(session)?.queued) {
|
|
1737
|
+
try {
|
|
1738
|
+
await afterJourneyTurn(journeyHooks(params, emit), {
|
|
1739
|
+
skill: params.skill,
|
|
1740
|
+
awaitingAnswer: session.awaitingSkillAnswer === true,
|
|
1741
|
+
assistantText,
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
catch (err) {
|
|
1745
|
+
if (process.env['CSPEACH_DEBUG'] === '1')
|
|
1746
|
+
throw err;
|
|
1747
|
+
// Audit M-1: paused (not ended), so the line's "carry on" works; the
|
|
1748
|
+
// error is logged, and a second stop at the same step says so (follow-up a).
|
|
1749
|
+
emit(chalk.yellow(journeyStoppedOnError(session, params.ctx.cwd, err)));
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
// Step 2, Task 5 — a /abap-modernize rebuild handed off by the journey
|
|
1753
|
+
// reads `started` once one of its turns wrote to SAP (F9).
|
|
1754
|
+
if (!readerChild && !params.ctx.subagent) {
|
|
1755
|
+
try {
|
|
1756
|
+
await afterRebuildTurn(session, params.skill);
|
|
1757
|
+
}
|
|
1758
|
+
catch { /* the row stays handed-off */ }
|
|
1759
|
+
}
|
|
1637
1760
|
// Phase 2 #9 (2026-05-16) — auto-compact end-of-turn hook.
|
|
1638
1761
|
// Runs only on the success path (after maybeOfferSave) so an interrupted
|
|
1639
1762
|
// turn never triggers a summariser call on partial state. The maybeAutoCompact
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import { migrateLegacyByokKey, legacyByokKeyNotice, cleanStaleAuthTemps } from '
|
|
|
10
10
|
* The verbs that make `cspeach project …` a mistyped team command rather than
|
|
11
11
|
* a prompt somebody meant to send. See the check in `main`.
|
|
12
12
|
*/
|
|
13
|
-
const TEAM_SUBCOMMANDS = new Set(['init', 'join', 'lane', 'refresh', 'status', 'outbox', 'rule', 'leave']);
|
|
13
|
+
const TEAM_SUBCOMMANDS = new Set(['init', 'join', 'lane', 'refresh', 'status', 'outbox', 'rule', 'rules', 'target', 'decide', 'leave', 'export']);
|
|
14
14
|
export async function main(argv) {
|
|
15
15
|
// NO_COLOR (piece 2 §1.2) — before ANY output, for every command (REPL,
|
|
16
16
|
// one-shot, doctor, login, help, config …).
|
|
@@ -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,
|
|
@@ -202,7 +209,10 @@ export async function main(argv) {
|
|
|
202
209
|
return {
|
|
203
210
|
sql: (q, max) => adt.sqlQuery(q, max),
|
|
204
211
|
system: { sid: name },
|
|
205
|
-
atc: {
|
|
212
|
+
atc: {
|
|
213
|
+
runSet: (refs, variant, opts) => adt.atcRunSet(refs, variant, opts),
|
|
214
|
+
findingDocument: (href) => adt.atcFindingDocument(href),
|
|
215
|
+
},
|
|
206
216
|
};
|
|
207
217
|
},
|
|
208
218
|
ask: async (question) => {
|
|
@@ -231,9 +241,14 @@ export async function main(argv) {
|
|
|
231
241
|
// a PAID prompt on a machine that is configured and logged in. A hint, not
|
|
232
242
|
// an alias: the old family does not run, and nothing is renamed back.
|
|
233
243
|
if (args[0] === 'project' && TEAM_SUBCOMMANDS.has(args[1] ?? '')) {
|
|
234
|
-
console.log('Unknown command: cspeach project. Team projects use: cspeach team init|join|lane|refresh|status|outbox|rule|leave');
|
|
244
|
+
console.log('Unknown command: cspeach project. Team projects use: cspeach team init|join|lane|refresh|status|outbox|rule|rules|target|decide|leave|export');
|
|
235
245
|
return 1;
|
|
236
246
|
}
|
|
247
|
+
// Sessions/folders (2026-10-08): sort project files from earlier builds into their folders. Copy only.
|
|
248
|
+
if (args[0] === 'files' && args[1] === 'sort') {
|
|
249
|
+
const { runFilesSort } = await import('./projects/sort-command.js');
|
|
250
|
+
return runFilesSort(args.slice(2), { log: (s) => console.log(s), cwd: process.cwd() });
|
|
251
|
+
}
|
|
237
252
|
if (args[0] === 'analyze' && args[1] === 'project-context') {
|
|
238
253
|
const { runProjectContextImpactCommand } = await import('./commands/project-context-impact.js');
|
|
239
254
|
return runProjectContextImpactCommand(args.slice(2));
|
|
@@ -270,7 +285,8 @@ Usage:
|
|
|
270
285
|
cspeach --one-shot "<prompt>" Same as above, explicit flag
|
|
271
286
|
cspeach --sap-alias <name> Use a specific SAP alias (overrides default)
|
|
272
287
|
cspeach --no-sap Start without a SAP system (paste-mode skills only)
|
|
273
|
-
cspeach --
|
|
288
|
+
cspeach --continue, -c Continue the latest session in this folder
|
|
289
|
+
cspeach --resume [id] Pick a session from this folder (or all folders); an id works anywhere
|
|
274
290
|
cspeach config add-sap Add a SAP system interactively
|
|
275
291
|
cspeach config standalone Set the standalone SAP profile (no system)
|
|
276
292
|
cspeach standards init Capture your team's ABAP development standards
|
|
@@ -283,18 +299,33 @@ Usage:
|
|
|
283
299
|
cspeach login Sign in via device-code flow
|
|
284
300
|
cspeach logout Sign out (keep keychain key, if any)
|
|
285
301
|
cspeach whoami Print current account info
|
|
302
|
+
cspeach team discover Read the whole estate from SAP into this folder (read only)
|
|
303
|
+
cspeach team start <packages> Start a team project in this folder: object list, first check, you own it all
|
|
304
|
+
cspeach team start --for "FI and SD" --developers "James Miller, me" Lead: set up by name from this folder's discovery; one lane each
|
|
286
305
|
cspeach team init <folder> Lead: pull the object list from SAP, start a team project
|
|
287
|
-
cspeach team join <folder>
|
|
306
|
+
cspeach team join <folder> [--as "<name>"] Record your results against a team project (on this laptop only), under the lane owner's name
|
|
288
307
|
cspeach team lane add|list Lead: who owns which packages
|
|
308
|
+
cspeach team lane move --packages <names> --to <developer> Lead: move packages by name (lanes of names only)
|
|
309
|
+
cspeach team lane propose --developers "A,B" [--apply] Lead: a first cut of the lanes, balanced by the work
|
|
310
|
+
cspeach team focus "<label>" --packages … --reason "…" Lead: what the team works on now (focus list | clear <label>)
|
|
311
|
+
cspeach team priority show|set|reset The priority rules; set and reset are the lead's
|
|
312
|
+
cspeach team plan move|scope|defer|set|note|history Lead: per-object plan changes, each with a reason
|
|
313
|
+
cspeach team plan import <file.xlsx> [--yes] Lead: read back the Plan sheet edited in Excel; shows the changes, asks once
|
|
289
314
|
cspeach team refresh --lane <id> Measure with ATC: what is verified, what did not hold
|
|
315
|
+
cspeach team refresh --all --per-package [--again] One record per package; skips packages already measured
|
|
290
316
|
cspeach team status [folder] Where every object stands, who owns it, what is proven
|
|
291
317
|
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
|
|
318
|
+
cspeach team rule T N <d> Lead: settle a conflicting decision (a retire needs --reason)
|
|
319
|
+
cspeach team rules [on|off|set|add|remove|reset] The decision rules: anyone views them, the lead changes them (also /rules in a session)
|
|
320
|
+
cspeach team decide T N <d> Record a clean-core decision: keep, fix, rebuild, retire or exception (a retire needs --reason)
|
|
321
|
+
cspeach team target <A-D> --reason "…" [--packages P1,P2] [--variant <NAME>] Lead: the level fixes are proven against (team target clear removes it)
|
|
294
322
|
cspeach team fixed T N Claim a fix; the next refresh checks it
|
|
323
|
+
cspeach team rebuild built T N --app <APP> Say a rebuild is built (the report then reads built, not yet checked)
|
|
295
324
|
cspeach team sheet Write your working list (Excel) from the team project
|
|
296
325
|
cspeach team report Lead: the combined workbook and status page, in reports
|
|
326
|
+
cspeach team export [--out <folder>] [--always | --never] The CSV files for your own tools, in reports/exports (--always: with every report; --never: stop that)
|
|
297
327
|
cspeach team leave Stop recording
|
|
328
|
+
cspeach files sort [--apply <plan>] [--clean <plan>] Sort files from earlier builds into their folders (copies; --clean deletes checked originals)
|
|
298
329
|
cspeach analyze project-context Compare flag-on vs flag-off session metrics
|
|
299
330
|
cspeach --upgrade Upgrade to latest via npm
|
|
300
331
|
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/help.js
CHANGED
|
@@ -6,6 +6,7 @@ import { paint } from '../renderer/theme.js';
|
|
|
6
6
|
import { uiWidth, truncateToWidth } from '../renderer/ui-width.js';
|
|
7
7
|
// I2's shortcuts table and note — one source for the `?` sheet and /help.
|
|
8
8
|
import { SHORTCUT_ROWS, SHORTCUTS_NOTE } from '../ui/widgets/shortcuts-sheet.js';
|
|
9
|
+
import { BUNDLED_RUNS } from '../runs/runs.js';
|
|
9
10
|
// Workflow order: plan → understand → modify → build → upgrade → ship.
|
|
10
11
|
const CATEGORY_ORDER = [
|
|
11
12
|
'Pre-Coding',
|
|
@@ -26,11 +27,20 @@ const fixed = (s) => () => s;
|
|
|
26
27
|
* follows the cost display mode (credits for managed customers).
|
|
27
28
|
*/
|
|
28
29
|
export const HELP_GROUPS = [
|
|
30
|
+
// Guided runs (spec 2026-10-08 §3.2) — above "Work on SAP", one row per
|
|
31
|
+
// declared run, from the bundled declarations (RUN.md), never from code.
|
|
32
|
+
{ title: 'Guided runs', rows: BUNDLED_RUNS.map((r) => ({
|
|
33
|
+
name: r.entry,
|
|
34
|
+
description: fixed(`${r.title} — ${r.steps.length} steps. Also: ${r.alias}`),
|
|
35
|
+
// The RUN.md body paragraph (spec §3.1, "one paragraph for /help").
|
|
36
|
+
detail: r.paragraph,
|
|
37
|
+
})) },
|
|
29
38
|
{ title: 'Work on SAP', rows: [
|
|
30
39
|
{ name: '/transport [TR]', description: fixed('View or set the transport for this session') },
|
|
31
40
|
{ name: '/mode [advisory|gated|auto]', description: fixed('Set the write mode (Shift+Tab also cycles)') },
|
|
32
41
|
{ name: '/safety-mode on|off|status', description: fixed('Turn the Forge Rule confirmation cards on or off') },
|
|
33
42
|
{ name: '/rewind', description: fixed('Undo a write from this session (Esc Esc)') },
|
|
43
|
+
{ name: '/rules', description: fixed("View or change the team project's decision rules") },
|
|
34
44
|
{ name: '/reroute <skill>', description: fixed('Re-run your last prompt with a different skill') },
|
|
35
45
|
] },
|
|
36
46
|
{ title: 'Local files', rows: [
|
|
@@ -102,8 +112,13 @@ export function printHelp(emit = (s) => console.log(s)) {
|
|
|
102
112
|
for (const group of HELP_GROUPS) {
|
|
103
113
|
emit('');
|
|
104
114
|
emit(groupTitle(group.title));
|
|
105
|
-
for (const r of group.rows)
|
|
115
|
+
for (const r of group.rows) {
|
|
106
116
|
emit(row(r.name, r.description()));
|
|
117
|
+
// A row's detail (a guided run's paragraph) wraps in full, dim, under the description column.
|
|
118
|
+
if (r.detail)
|
|
119
|
+
for (const l of wrapDescription(r.detail, descWidth, Number.MAX_SAFE_INTEGER))
|
|
120
|
+
emit(`${continuationIndent}${paint('dim', l)}`);
|
|
121
|
+
}
|
|
107
122
|
if (group.title === 'Work on SAP') {
|
|
108
123
|
// Skills live under "Work on SAP", one dim sub-heading per category.
|
|
109
124
|
for (const category of CATEGORY_ORDER) {
|
|
@@ -28,6 +28,7 @@ import { readProjectFile } from '../projects/status.js';
|
|
|
28
28
|
import { parsePlanContent } from '../projects/plan-schema.js';
|
|
29
29
|
import { statusesFromItems, computeNextPhaseForRun, isPhaseSatisfiedForRun, planCompletionLines, phaseDisplayTitle, } from '../projects/plan-run.js';
|
|
30
30
|
import { savedLocation } from '../projects/saved-lines.js';
|
|
31
|
+
import { afterPlanComplete } from '../journey/rebuild.js';
|
|
31
32
|
import { clearPlanGrant, getPlanGrant, getGrantTransport, installPlanGrant, noteDeltaOffered, offeredDeltaObjects, planGrantAllowed, } from '../approvals/plan-grant.js';
|
|
32
33
|
import { collectPlannedObjects, plannedObjectFacts } from '../projects/plan-objects.js';
|
|
33
34
|
import { isDeployPhase } from '../projects/plan-lane.js';
|
|
@@ -251,6 +252,8 @@ async function postPhaseChain(args) {
|
|
|
251
252
|
const nextAfter = computeNextPhaseForRun(after.content.phases, st);
|
|
252
253
|
if (built && !nextAfter) {
|
|
253
254
|
args.log('', ...planCompletionLines(after.content.phases));
|
|
255
|
+
// Step 2 (C5): a rebuild whose plan this is now reads "built, not yet checked".
|
|
256
|
+
await afterPlanComplete(cur);
|
|
254
257
|
args.log(`Plan file: ${savedLocation(cur, args.cwd)}`);
|
|
255
258
|
}
|
|
256
259
|
else if (!built && auditFailed && !stopShown) {
|
|
@@ -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,24 @@ 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
|
+
}
|
|
41
|
+
/* Step 2 (final review I2) — the rebuild hand-off's share. The journey ends
|
|
42
|
+
* (its share off) BEFORE it queues the rebuild skill; that queued turn is
|
|
43
|
+
* auto-dispatched, so it must not inherit auto_approve either. Set just before
|
|
44
|
+
* the queue, cleared after the first top-level turn that follows the hand-off
|
|
45
|
+
* turn (journey/rebuild.ts afterRebuildTurn), whatever its skill. */
|
|
46
|
+
let rebuildRunActive = false;
|
|
47
|
+
export function setRebuildRunActive(active) {
|
|
48
|
+
rebuildRunActive = active;
|
|
49
|
+
}
|
|
34
50
|
export function isGuardedRunActive() {
|
|
35
|
-
return guardedRunActive;
|
|
51
|
+
return guardedRunActive || journeyRunActive || rebuildRunActive;
|
|
36
52
|
}
|
|
37
53
|
/* ── plan-phase read-only enforcement (Task 7, 2026-07-03) ─────────────────
|
|
38
54
|
* Module-level flag consulted by tools/subagent/agent_run.ts. The REPL sets
|
|
@@ -53,6 +53,7 @@ import { acquireRunLease, releaseRunLease } from '../projects/run-lease.js';
|
|
|
53
53
|
import { validateEnvelope } from '../projects/validate.js';
|
|
54
54
|
import { collectTurnAssistantText } from '../agent/turn-assistant-text.js';
|
|
55
55
|
import { getAuthorIdentity } from '../agent/loop.js';
|
|
56
|
+
import { afterPlanComplete } from '../journey/rebuild.js';
|
|
56
57
|
import { DEFAULT_PLAN_MODE } from './plan-gate.js';
|
|
57
58
|
import { FLOORPLAN_TO_TEMPLATE } from '../tools/fiori/floorplan-map.js';
|
|
58
59
|
import { getCurrentTransport, setCurrentTransport } from '../repl/current-transport.js';
|
|
@@ -260,7 +261,7 @@ export async function preparePlanResume(args) {
|
|
|
260
261
|
// skip this entirely.
|
|
261
262
|
// R5 fix round: the stored phases must also fit the lane, checked here
|
|
262
263
|
// before any phase, lease or transport work.
|
|
263
|
-
const laneGate = await loadPlanLaneForRun(args.cwd, content);
|
|
264
|
+
const laneGate = await loadPlanLaneForRun(args.cwd, content, path);
|
|
264
265
|
if (!laneGate.ok) {
|
|
265
266
|
args.log(...laneGate.lines);
|
|
266
267
|
return null;
|
|
@@ -632,7 +633,7 @@ export async function preparePlanResume(args) {
|
|
|
632
633
|
// 2026-10-04 (plan sources) — the spec-gap items this phase reads, in full,
|
|
633
634
|
// from the file the plan was made from (or the plan's creation snapshot).
|
|
634
635
|
// 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);
|
|
636
|
+
const sourcesBlock = renderPlanSourcesBlock(await resolvePlanSources({ cwd: args.cwd, envelope, content, planPath: path, ...(laneRecord ? { laneRecord } : {}) }), next);
|
|
636
637
|
const llmPrompt = [
|
|
637
638
|
`Resume execution of the project plan "${envelope.title}" (envelope v${envelope.version}).`,
|
|
638
639
|
'',
|
|
@@ -981,14 +982,9 @@ export async function finishPlanResume(args) {
|
|
|
981
982
|
logNotSaved(args, 'the updated plan did not pass its own checks', `revision failed validation: ${check.error.message}`);
|
|
982
983
|
return null;
|
|
983
984
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
}
|
|
988
|
-
catch {
|
|
989
|
-
outDir = process.cwd();
|
|
990
|
-
}
|
|
991
|
-
const savedPath = await saveProject(revision, { cwd: outDir });
|
|
985
|
+
// 2026-10-08 — the new version is saved beside the plan file, wherever it
|
|
986
|
+
// is (an old git root, a copied folder), so a plan family never splits.
|
|
987
|
+
const savedPath = await savePlanVersion(revision, args.prepared.path, args.log);
|
|
992
988
|
// Task 8 — regenerate the write-only markdown handover projection beside
|
|
993
989
|
// the envelope. Best-effort: a failed projection write logs one dim line
|
|
994
990
|
// and never blocks the run (the envelope is the source of truth).
|
|
@@ -1024,6 +1020,9 @@ export async function finishPlanResume(args) {
|
|
|
1024
1020
|
if (!next && provisionalPhases.every((p) => isPhaseSatisfied(statuses[p.id], p.audit))) {
|
|
1025
1021
|
// C2 (D24): same chain as preparePlanResume — single source in plan-run.ts.
|
|
1026
1022
|
args.log(...planCompletionLines(provisionalPhases));
|
|
1023
|
+
// Step 2 (C5): the live run finished — a rebuild whose plan this is reads built.
|
|
1024
|
+
// Not at the re-opened complete plan above (preparePlanResume).
|
|
1025
|
+
await afterPlanComplete(args.prepared.path);
|
|
1027
1026
|
}
|
|
1028
1027
|
else if (!next) {
|
|
1029
1028
|
const blocked = provisionalPhases.filter((p) => statuses[p.id] === 'blocked').map((p) => phaseDisplayTitle(p, provisionalPhases));
|
|
@@ -1048,6 +1047,23 @@ export async function finishPlanResume(args) {
|
|
|
1048
1047
|
nextTitle: next ? phaseDisplayTitle(next, extract.content.phases) : null,
|
|
1049
1048
|
};
|
|
1050
1049
|
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Final fix wave, item 5 — save a new plan version beside the plan file. When
|
|
1052
|
+
* that write fails (a read-only or missing folder), save it in the working
|
|
1053
|
+
* folder instead, so the run's progress is never lost, and say where.
|
|
1054
|
+
*/
|
|
1055
|
+
async function savePlanVersion(revision, planPath, log) {
|
|
1056
|
+
const beside = dirname(planPath);
|
|
1057
|
+
try {
|
|
1058
|
+
return await saveProject(revision, { cwd: beside });
|
|
1059
|
+
}
|
|
1060
|
+
catch {
|
|
1061
|
+
const ws = await ensureWorkspace();
|
|
1062
|
+
const saved = await saveProject(revision, { cwd: ws });
|
|
1063
|
+
log(`Could not save beside the plan in ${beside}. Saved in ${ws} instead.`);
|
|
1064
|
+
return saved;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1051
1067
|
/**
|
|
1052
1068
|
* Shared revision writer for audit verdicts and chain-stop records: load the
|
|
1053
1069
|
* envelope, patch ONE phase (audit and/or status) + content.lastStop,
|
|
@@ -1103,14 +1119,8 @@ async function savePlanPatch(envelopePath, phaseId, patch, log) {
|
|
|
1103
1119
|
if (!check.ok) {
|
|
1104
1120
|
throw new Error(`audit revision failed validation: ${check.error.message}`);
|
|
1105
1121
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
outDir = await ensureWorkspace();
|
|
1109
|
-
}
|
|
1110
|
-
catch {
|
|
1111
|
-
outDir = dirname(envelopePath);
|
|
1112
|
-
}
|
|
1113
|
-
const saved = await saveProject(revision, { cwd: outDir });
|
|
1122
|
+
// 2026-10-08 — saved beside the plan file (the working folder only as a fallback).
|
|
1123
|
+
const saved = await savePlanVersion(revision, envelopePath, log ?? console.log);
|
|
1114
1124
|
// Task 8 — every envelope save in the plan flow regenerates the markdown
|
|
1115
1125
|
// handover. This is the single chokepoint for ALL chain writes:
|
|
1116
1126
|
// applyAuditResult and applyPhaseResolution both route through here.
|