@cspeach/cli 1.1.26 → 1.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/loop.js +41 -5
- package/dist/agent/provider-shape.js +9 -2
- package/dist/agent/providers/ai-hub-provider.js +2 -2
- package/dist/agent/providers/byok-provider.js +2 -2
- package/dist/agent/providers/local-provider.js +4 -4
- package/dist/agent/system-prompt.js +34 -7
- package/dist/agent/tool-dispatch.js +20 -1
- package/dist/approvals/grant-claims.js +70 -0
- package/dist/approvals/jwt.js +21 -1
- package/dist/approvals/plan-grant.js +297 -0
- package/dist/commands/config-set.js +12 -0
- package/dist/commands/deploy-card.js +10 -5
- package/dist/commands/export-audit.js +1 -1
- package/dist/commands/plan-audit-evidence.js +47 -1
- package/dist/commands/plan-audit.js +23 -8
- package/dist/commands/plan-batch-audit.js +349 -0
- package/dist/commands/plan-boundary.js +88 -0
- package/dist/commands/plan-chain.js +409 -566
- package/dist/commands/plan-continue.js +21 -7
- package/dist/commands/plan-gate.js +28 -49
- package/dist/commands/plan-reground.js +174 -0
- package/dist/commands/plan-resume.js +227 -103
- package/dist/commands/plan-sap-pause.js +105 -0
- package/dist/commands/plan-startup.js +218 -0
- package/dist/commands/whoami.js +1 -1
- package/dist/config/loader.js +9 -0
- package/dist/doctor/checks/forge-rules.js +67 -85
- package/dist/doctor/checks/skill.js +60 -11
- package/dist/doctor/run.js +4 -6
- package/dist/lane/app-chain.js +116 -9
- package/dist/one-shot.js +1 -0
- package/dist/projects/extract-design.js +4 -3
- package/dist/projects/extract-estimate.js +4 -3
- package/dist/projects/extract-plan.js +34 -1
- package/dist/projects/extract-spec-gap.js +28 -5
- package/dist/projects/handover-md.js +1 -1
- package/dist/projects/plan-objects.js +75 -0
- package/dist/projects/plan-run.js +74 -18
- package/dist/projects/plan-schema.js +6 -0
- package/dist/projects/plan-sources.js +199 -0
- package/dist/projects/promote-command.js +1 -1
- package/dist/projects/save-command.js +15 -14
- package/dist/projects/saved-lines.js +36 -0
- package/dist/projects/title-heading.js +36 -0
- package/dist/renderer/checklist-format.js +5 -5
- package/dist/renderer/machine-blocks.js +141 -0
- package/dist/renderer/thinking-heartbeat.js +2 -1
- package/dist/renderer/tool-labels.js +3 -0
- package/dist/repl/plan-hop.js +22 -0
- package/dist/repl/rule8-detector.js +16 -1
- package/dist/repl.js +320 -55
- package/dist/router/router-core.js +2 -2
- package/dist/sap/reachability-state.js +25 -0
- package/dist/sap/unreachable.js +4 -1
- package/dist/session/audit-export.js +10 -16
- package/dist/session/recap.js +16 -26
- package/dist/skills/bundled-skills.js +1 -1
- package/dist/tools/_filesystem-shared.js +37 -0
- package/dist/tools/approval.js +48 -2
- package/dist/tools/dispatch-skill.js +3 -2
- package/dist/tools/filesystem/file-read.js +5 -3
- package/dist/tools/filesystem/glob.js +27 -4
- package/dist/tools/fiori/tools.js +6 -0
- package/dist/tools/lane/app-lane-resolve.js +1 -1
- package/dist/tools/sap-read.js +37 -3
- package/dist/tools/sap-write.js +65 -0
- package/dist/tools/text-elements/pool.js +250 -0
- package/dist/tools/text-elements/tools.js +433 -0
- package/dist/tools/tool-loading.js +3 -0
- package/dist/tools/transport.js +26 -5
- package/dist/tools/write-mode.js +4 -3
- package/package.json +3 -4
- package/dist/doctor/checks/zcspeach.js +0 -96
- package/dist/projects/email-template.js +0 -56
package/dist/repl.js
CHANGED
|
@@ -109,7 +109,7 @@ function prefillReadline(rl, text) {
|
|
|
109
109
|
import { BracketedPasteDecoder, enableBracketedPaste, disableBracketedPaste, } from './repl/bracketed-paste.js';
|
|
110
110
|
import { InkStdinPasteGuard } from './repl/ink-stdin-guard.js';
|
|
111
111
|
import { printHelp } from './commands/help.js';
|
|
112
|
-
import { shouldUseInk, setOneShotOverride } from './renderer/tty.js';
|
|
112
|
+
import { shouldUseInk, setOneShotOverride, isHeadless } from './renderer/tty.js';
|
|
113
113
|
import { applyStartupRendering } from './renderer/rendering-mode.js';
|
|
114
114
|
import { createClassicOutputEmitter } from './renderer/notices.js';
|
|
115
115
|
import { saveConfig } from './config/loader.js';
|
|
@@ -118,6 +118,10 @@ import { existsSync } from 'node:fs';
|
|
|
118
118
|
import { join } from 'node:path';
|
|
119
119
|
import { streamsRoot } from './agent/turn-stream.js';
|
|
120
120
|
import { parseAndValidateReroute } from './commands/reroute.js';
|
|
121
|
+
import { DEFAULT_PLAN_MODE } from './commands/plan-gate.js';
|
|
122
|
+
import { autoRoutedNotice } from './commands/plan-resume.js';
|
|
123
|
+
import { makeRegroundDeps } from './commands/plan-reground.js';
|
|
124
|
+
import { createPlanHopTracker } from './repl/plan-hop.js';
|
|
121
125
|
import { emitPlanTurnEnd } from './repl/plan-turn-end.js';
|
|
122
126
|
import { SKILL_CATALOG } from './skill-catalog.js';
|
|
123
127
|
import { replPreviewHook, inkPreviewHook } from './repl/update-method-preview-hook.js';
|
|
@@ -156,6 +160,7 @@ import './tools/cap/sap-link-tool.js';
|
|
|
156
160
|
import './tools/cap/link-smoke.js';
|
|
157
161
|
import './tools/lane/app-lane-resolve.js';
|
|
158
162
|
import './tools/extend-model/tool.js';
|
|
163
|
+
import './tools/text-elements/tools.js';
|
|
159
164
|
import './tools/capability/tool.js';
|
|
160
165
|
import './tools/web/web_fetch.js';
|
|
161
166
|
import './tools/web/web_search.js';
|
|
@@ -419,6 +424,7 @@ export async function runRepl(opts) {
|
|
|
419
424
|
// flow below when the file is absent (back-compat for pre-`cspeach login`
|
|
420
425
|
// users).
|
|
421
426
|
let signIn;
|
|
427
|
+
let signedInAs;
|
|
422
428
|
try {
|
|
423
429
|
const authFile = await readAuthFile();
|
|
424
430
|
if (authFile) {
|
|
@@ -437,6 +443,7 @@ export async function runRepl(opts) {
|
|
|
437
443
|
console.log(note.trimEnd());
|
|
438
444
|
}
|
|
439
445
|
else {
|
|
446
|
+
signedInAs = meResult.profile.email;
|
|
440
447
|
const banner = formatLoginBanner(meResult.profile);
|
|
441
448
|
if (getLook() === 'panels') {
|
|
442
449
|
signIn = signInFragment(meResult.profile);
|
|
@@ -811,8 +818,8 @@ export async function runRepl(opts) {
|
|
|
811
818
|
? formatAttention({ text: 'Last turn was stopped by you', next: 'type continue to finish where it left off' })
|
|
812
819
|
: formatAttention({ text: 'Last turn was interrupted', fact: readableInterruptReason(session.turnInterruptedReason), next: 'type continue to finish where it left off' }));
|
|
813
820
|
}
|
|
814
|
-
|
|
815
|
-
|
|
821
|
+
// 2026-10-04 — no passive "Unfinished plan" line: the startup
|
|
822
|
+
// "Continue '<plan>'?" offer (commands/plan-startup.ts) replaces it.
|
|
816
823
|
process.stdout.write(panelsStartTail({ status: panelsStatus, third: panelsResumeLine(recap.last, turnCount), notices: startupNotices }));
|
|
817
824
|
}
|
|
818
825
|
else {
|
|
@@ -857,6 +864,11 @@ export async function runRepl(opts) {
|
|
|
857
864
|
get: () => pendingDispatchValue,
|
|
858
865
|
set: (cmd) => { pendingDispatchValue = cmd; },
|
|
859
866
|
};
|
|
867
|
+
// Task 13 fix round — the plan chain queues through planHops.queue, which
|
|
868
|
+
// marks the command as an in-run hop (one progress line, no guarded
|
|
869
|
+
// notice). Anything else in the slot (the /abap-app start, a skill switch)
|
|
870
|
+
// is not a hop. Shared by the Ink and readline branches like the slot.
|
|
871
|
+
const planHops = createPlanHopTracker(pendingDispatchAccessor);
|
|
860
872
|
// v0.3: Ink-mode REPL branch. When running under a real TTY in REPL mode
|
|
861
873
|
// (not one-shot, not CI), mount the <App /> frame instead of the readline
|
|
862
874
|
// loop. The non-Ink path below is preserved byte-for-byte for piped/CI
|
|
@@ -947,7 +959,9 @@ export async function runRepl(opts) {
|
|
|
947
959
|
chunkEmitter.emit('chunk', '\n' + formatInfo(line));
|
|
948
960
|
};
|
|
949
961
|
/** Auto-dispatch of the next plan phase, unless a post-turn cancel stopped it. */
|
|
950
|
-
|
|
962
|
+
// Task 13 fix round — `planHop` travels WITH the command into handleSubmit,
|
|
963
|
+
// so a refused submit can never leave a stale hop mark behind.
|
|
964
|
+
const autoDispatch = (cmd, planHop = false) => {
|
|
951
965
|
// Round 4 item 9b — the exit is already under way: start nothing new.
|
|
952
966
|
// Final review B-I2 — `/exit` typed mid-turn (exitAfterTurn) stops it too.
|
|
953
967
|
const decision = autoDispatchDecision({ done, exitRequested: exitAfterTurn, cancelPending: pendingCancel.pending }, cmd);
|
|
@@ -958,14 +972,17 @@ export async function runRepl(opts) {
|
|
|
958
972
|
chunkEmitter.emit('chunk', formatInfo(decision.line));
|
|
959
973
|
return;
|
|
960
974
|
}
|
|
961
|
-
|
|
962
|
-
|
|
975
|
+
// Task 13 — no echo for a plan hop: the chain already said "Next: <title>".
|
|
976
|
+
const notice = autoRoutedNotice(cmd);
|
|
977
|
+
if (notice)
|
|
978
|
+
chunkEmitter.emit('chunk', formatInfo(notice));
|
|
979
|
+
setImmediate(() => { void handleSubmit(cmd, { planHop }); });
|
|
963
980
|
};
|
|
964
981
|
// BUG-2b — the actual dispatch. Wrapped by handleSubmit (below) which
|
|
965
982
|
// owns the single-flight reserve()/release() lifecycle. All re-entrant
|
|
966
983
|
// callers (the setImmediate auto-dispatch / re-audit legs) go through the
|
|
967
984
|
// WRAPPER so they are guarded and reserved too.
|
|
968
|
-
const dispatchSubmit = async (line) => {
|
|
985
|
+
const dispatchSubmit = async (line, submitOpts) => {
|
|
969
986
|
const trimmed = line.trim();
|
|
970
987
|
if (!trimmed)
|
|
971
988
|
return;
|
|
@@ -1026,6 +1043,7 @@ export async function runRepl(opts) {
|
|
|
1026
1043
|
}
|
|
1027
1044
|
await runExportAuditCommand({
|
|
1028
1045
|
session,
|
|
1046
|
+
operator: signedInAs,
|
|
1029
1047
|
transport: getCurrentTransport(),
|
|
1030
1048
|
filename,
|
|
1031
1049
|
emit: (l) => chunkEmitter.emit('chunk', l + '\n'),
|
|
@@ -1453,6 +1471,8 @@ export async function runRepl(opts) {
|
|
|
1453
1471
|
// persists the version-N+1 REVISION after the turn.
|
|
1454
1472
|
let planResume = null;
|
|
1455
1473
|
let planResumeMsgStart = 0;
|
|
1474
|
+
// Task 13 — this dispatch is an in-run hop of a plan run (queued by the chain).
|
|
1475
|
+
let planHop = false;
|
|
1456
1476
|
// D-B — the wall-clock start of THIS phase attempt's turn, threaded to
|
|
1457
1477
|
// the post-phase audit as the evidence-window cutoff (so an audit never
|
|
1458
1478
|
// sees an earlier attempt's tool-call rows in the shared session JSONL).
|
|
@@ -1469,9 +1489,12 @@ export async function runRepl(opts) {
|
|
|
1469
1489
|
try {
|
|
1470
1490
|
const { preparePlanResume } = await import('./commands/plan-resume.js');
|
|
1471
1491
|
const { inkAwarePrompt, inkAwareConfirm } = await import('./agent/loop.js');
|
|
1492
|
+
planHop = submitOpts?.planHop === true;
|
|
1472
1493
|
planResume = await preparePlanResume({
|
|
1473
1494
|
body: prompt,
|
|
1474
1495
|
cwd: process.cwd(),
|
|
1496
|
+
// Task 13 — an in-run hop prints one progress line, not the board.
|
|
1497
|
+
boardStyle: planHop ? 'line' : 'full',
|
|
1475
1498
|
configMode: cfg.plan_mode,
|
|
1476
1499
|
// Audit off-switch (2026-07-06, owner) — config default; per-run
|
|
1477
1500
|
// --audit/--no-audit flags in the body override it (parsed inside).
|
|
@@ -1481,6 +1504,9 @@ export async function runRepl(opts) {
|
|
|
1481
1504
|
sessionId: session.id,
|
|
1482
1505
|
// 2026-10-02 — deploy card on a direct resume into ui.deploy.
|
|
1483
1506
|
sapAlias: session.sap_system ?? undefined,
|
|
1507
|
+
// 2026-10-04 — re-check SAP before the step (skipped without a SAP system).
|
|
1508
|
+
reground: standalone ? undefined : makeRegroundDeps(adt),
|
|
1509
|
+
interactive: !isHeadless(),
|
|
1484
1510
|
prompt: inkAwarePrompt,
|
|
1485
1511
|
confirm: inkAwareConfirm,
|
|
1486
1512
|
log: (...lines) => chunkEmitter.emit('chunk', lines.join('\n') + '\n'),
|
|
@@ -1490,10 +1516,33 @@ export async function runRepl(opts) {
|
|
|
1490
1516
|
chunkEmitter.emit('chunk', '\n' + formatFailure({ what: 'Could not resume the plan', detail: [err.message] }));
|
|
1491
1517
|
}
|
|
1492
1518
|
if (!planResume) {
|
|
1519
|
+
// 2026-10-04 — the resume could not start: the run is over, and the
|
|
1520
|
+
// plan object-list grant ends with it.
|
|
1521
|
+
{
|
|
1522
|
+
const { clearPlanGrant } = await import('./approvals/plan-grant.js');
|
|
1523
|
+
clearPlanGrant();
|
|
1524
|
+
}
|
|
1493
1525
|
session.last_skill = previousSkill ?? null;
|
|
1494
1526
|
session.last_object = extractedObject ?? previousObject ?? null;
|
|
1495
1527
|
return;
|
|
1496
1528
|
}
|
|
1529
|
+
// 2026-10-04 (F13) — a typed resume in guarded mode with no grant for
|
|
1530
|
+
// this plan shows the object list once before the step. Yes approves
|
|
1531
|
+
// it; No runs the step with a card per write. Never on a chain hop.
|
|
1532
|
+
if (!isHeadless()) {
|
|
1533
|
+
try {
|
|
1534
|
+
const { offerObjectListOnResume, inkStartupChooser } = await import('./commands/plan-startup.js');
|
|
1535
|
+
await offerObjectListOnResume({
|
|
1536
|
+
prepared: planResume,
|
|
1537
|
+
planHop,
|
|
1538
|
+
sessionId: session.id,
|
|
1539
|
+
sapAlias: session.sap_system ?? undefined,
|
|
1540
|
+
choose: inkStartupChooser(),
|
|
1541
|
+
log: (...lines) => chunkEmitter.emit('chunk', lines.join('\n') + '\n'),
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
catch { /* best-effort: each write asks */ }
|
|
1545
|
+
}
|
|
1497
1546
|
// Task 6 (agentic-flow) — re-audit-on-resume: the last executed phase
|
|
1498
1547
|
// has an unresolved audit (crash before the verdict write, auditor
|
|
1499
1548
|
// infra outage, or a guarded-mode hand-edit). NO model turn this
|
|
@@ -1506,10 +1555,11 @@ export async function runRepl(opts) {
|
|
|
1506
1555
|
await runReauditPass({
|
|
1507
1556
|
savedPath: planResume.path,
|
|
1508
1557
|
phaseIds: planResume.reauditPhaseIds ?? [planResume.reauditPhaseId],
|
|
1558
|
+
force: planResume.reauditForce,
|
|
1509
1559
|
sessionId: session.id,
|
|
1510
1560
|
cwd: process.cwd(),
|
|
1511
1561
|
sapAlias: session.sap_system ?? undefined,
|
|
1512
|
-
mode: planResume.effectiveMode ??
|
|
1562
|
+
mode: planResume.effectiveMode ?? DEFAULT_PLAN_MODE,
|
|
1513
1563
|
modeFlag: planResume.mode,
|
|
1514
1564
|
// Off-switch (I3) — footer honesty on re-audit boards; (I2) the
|
|
1515
1565
|
// per-run audit flag survives onto chain-built resume commands.
|
|
@@ -1517,7 +1567,7 @@ export async function runRepl(opts) {
|
|
|
1517
1567
|
auditFlag: planResume.auditFlag,
|
|
1518
1568
|
prompt: inkAwarePrompt,
|
|
1519
1569
|
confirm: inkAwareConfirm,
|
|
1520
|
-
queueDispatch: (cmd) =>
|
|
1570
|
+
queueDispatch: (cmd) => planHops.queue(cmd),
|
|
1521
1571
|
log: (...lines) => chunkEmitter.emit('chunk', lines.join('\n') + '\n'),
|
|
1522
1572
|
});
|
|
1523
1573
|
}
|
|
@@ -1538,22 +1588,30 @@ export async function runRepl(opts) {
|
|
|
1538
1588
|
const { releaseRunLeaseUnlessChained } = await import('./projects/run-lease.js');
|
|
1539
1589
|
releaseRunLeaseUnlessChained(planResume.path, reauditDispatched);
|
|
1540
1590
|
}
|
|
1591
|
+
// 2026-10-04 — the re-audit leg queued nothing: the run is over and
|
|
1592
|
+
// the plan object-list grant ends with it.
|
|
1593
|
+
if (!reauditDispatched) {
|
|
1594
|
+
const { clearPlanGrant } = await import('./approvals/plan-grant.js');
|
|
1595
|
+
clearPlanGrant();
|
|
1596
|
+
}
|
|
1541
1597
|
if (reauditDispatched) {
|
|
1542
1598
|
pendingDispatchAccessor.set(null);
|
|
1543
|
-
autoDispatch(reauditDispatched);
|
|
1599
|
+
autoDispatch(reauditDispatched, planHops.take(reauditDispatched));
|
|
1544
1600
|
}
|
|
1545
1601
|
return;
|
|
1546
1602
|
}
|
|
1547
|
-
//
|
|
1548
|
-
//
|
|
1549
|
-
//
|
|
1550
|
-
//
|
|
1551
|
-
//
|
|
1603
|
+
// 2026-10-04 (continuous run) — a phase boundary is save + COMPACT:
|
|
1604
|
+
// the previous phase becomes one summary message (spec-gap answers,
|
|
1605
|
+
// decisions, transport survive). Falls back to today's reset if the
|
|
1606
|
+
// summariser fails, so the run never dead-ends. Still bounded: prior
|
|
1607
|
+
// transcript never snowballs (A1 / defect D23).
|
|
1552
1608
|
{
|
|
1553
|
-
const {
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1609
|
+
const { beginPlanPhaseContext, makePhaseBoundarySummariser } = await import('./commands/plan-boundary.js');
|
|
1610
|
+
await beginPlanPhaseContext({
|
|
1611
|
+
session,
|
|
1612
|
+
summarise: makePhaseBoundarySummariser(provider, resolveModelRole('compact', cfg)),
|
|
1613
|
+
log: (line) => chunkEmitter.emit('chunk', line + '\n'),
|
|
1614
|
+
});
|
|
1557
1615
|
}
|
|
1558
1616
|
planResumeMsgStart = session.messages.length;
|
|
1559
1617
|
planPhaseStartedAt = new Date().toISOString(); // D-B evidence-window cutoff
|
|
@@ -1616,7 +1674,8 @@ export async function runRepl(opts) {
|
|
|
1616
1674
|
planContinue = { prepared: r.prepared, marker };
|
|
1617
1675
|
planPhaseStartedAt = marker.attemptStartedAt;
|
|
1618
1676
|
planResumeMsgStart = session.messages.length;
|
|
1619
|
-
|
|
1677
|
+
const { continuationNotice } = await import('./commands/plan-continue.js');
|
|
1678
|
+
chunkEmitter.emit('chunk', formatInfo(continuationNotice(r.prepared)));
|
|
1620
1679
|
}
|
|
1621
1680
|
else if (r.stale) {
|
|
1622
1681
|
session.plan_in_flight = null; // plan/phase gone for good — marker is noise
|
|
@@ -1674,7 +1733,7 @@ export async function runRepl(opts) {
|
|
|
1674
1733
|
// Task 7 — plan-phase subagent read-only enforcement: set for EVERY
|
|
1675
1734
|
// plan-resume phase turn regardless of mode (guarded AND step);
|
|
1676
1735
|
// agent_run consults the flag and forces its read-only tool filter.
|
|
1677
|
-
setPlanPhaseActive(true);
|
|
1736
|
+
setPlanPhaseActive(true, planPhaseCtx.envelope.id);
|
|
1678
1737
|
// Task 10 — the writes declaration of the phase EXECUTING this turn
|
|
1679
1738
|
// (fail-safe: undefined ⇒ true, the deviation backstop never fires).
|
|
1680
1739
|
// request_approval consults it in guarded mode to deny undeclared
|
|
@@ -1682,11 +1741,15 @@ export async function runRepl(opts) {
|
|
|
1682
1741
|
// never reached its post-phase chain so it can't block THIS phase.
|
|
1683
1742
|
setCurrentPhaseWrites(planPhaseCtx.nextPhaseWrites ?? true);
|
|
1684
1743
|
getAndResetPlanDeviation();
|
|
1744
|
+
// Stale "SAP unreachable" from an earlier turn must not pause THIS step.
|
|
1745
|
+
(await import('./sap/reachability-state.js')).getAndResetSapUnreachable();
|
|
1685
1746
|
planPhaseFlagSet = true;
|
|
1686
|
-
if ((planPhaseCtx.effectiveMode ??
|
|
1747
|
+
if ((planPhaseCtx.effectiveMode ?? DEFAULT_PLAN_MODE) === 'guarded') {
|
|
1687
1748
|
setGuardedRunActive(true);
|
|
1688
1749
|
guardedRunFlagSet = true;
|
|
1689
|
-
|
|
1750
|
+
// Task 13 — once per run (first hop only), in plain words.
|
|
1751
|
+
if (!planHop)
|
|
1752
|
+
chunkEmitter.emit('chunk', formatInfo('Continuous build · auto-approve is off; each write still runs snapshot, syntax check, activate, verify'));
|
|
1690
1753
|
}
|
|
1691
1754
|
// Off-switch (2026-07-06, owner) — announce plainly that this run's
|
|
1692
1755
|
// phases are NOT audited (honesty: off is a choice, never a lie).
|
|
@@ -1694,6 +1757,8 @@ export async function runRepl(opts) {
|
|
|
1694
1757
|
chunkEmitter.emit('chunk', chalk.dim('plan audits: off\n'));
|
|
1695
1758
|
}
|
|
1696
1759
|
}
|
|
1760
|
+
// 2026-10-04 — the phase's expert skill rides along in the system prompt.
|
|
1761
|
+
const planPhaseDelegate = planPhaseCtx?.nextDelegateTo;
|
|
1697
1762
|
await runTurn({
|
|
1698
1763
|
provider,
|
|
1699
1764
|
userMessage: prompt,
|
|
@@ -1707,6 +1772,7 @@ export async function runRepl(opts) {
|
|
|
1707
1772
|
// Task 19 — auto-compact at turn end, except plan resume/continue
|
|
1708
1773
|
// turns (their post-turn persist indexes messages from turn start).
|
|
1709
1774
|
autoCompact: planResume === null && planContinue === null,
|
|
1775
|
+
delegateSkill: planPhaseDelegate,
|
|
1710
1776
|
// A4 — undefined on every non-plan turn (and when tiering is off).
|
|
1711
1777
|
modelOverride: planModelOverride,
|
|
1712
1778
|
// Task 9 — a tier-down, never the session role (proxy must not enforce it).
|
|
@@ -1849,7 +1915,7 @@ export async function runRepl(opts) {
|
|
|
1849
1915
|
sessionId: session.id,
|
|
1850
1916
|
cwd: process.cwd(),
|
|
1851
1917
|
sapAlias: session.sap_system ?? undefined,
|
|
1852
|
-
mode: planResume.effectiveMode ??
|
|
1918
|
+
mode: planResume.effectiveMode ?? DEFAULT_PLAN_MODE,
|
|
1853
1919
|
modeFlag: planResume.mode,
|
|
1854
1920
|
// Off-switch — skips the normal post-phase audit when 'off'; the
|
|
1855
1921
|
// per-run flag survives onto chain-built resume commands (I2).
|
|
@@ -1862,7 +1928,7 @@ export async function runRepl(opts) {
|
|
|
1862
1928
|
subagentDispatches: planSubagentDispatches,
|
|
1863
1929
|
prompt: inkAwarePrompt,
|
|
1864
1930
|
confirm: inkAwareConfirm,
|
|
1865
|
-
queueDispatch: (cmd) =>
|
|
1931
|
+
queueDispatch: (cmd) => planHops.queue(cmd),
|
|
1866
1932
|
log: (...lines) => chunkEmitter.emit('chunk', lines.join('\n') + '\n'),
|
|
1867
1933
|
});
|
|
1868
1934
|
}
|
|
@@ -1901,8 +1967,9 @@ export async function runRepl(opts) {
|
|
|
1901
1967
|
emit: (line) => chunkEmitter.emit('chunk', line + '\n'),
|
|
1902
1968
|
});
|
|
1903
1969
|
}
|
|
1904
|
-
else if (r.kind === 'none') {
|
|
1905
|
-
|
|
1970
|
+
else if (r.kind === 'none' && !(await import('./sap/reachability-state.js')).isSapUnreachableNoted()) {
|
|
1971
|
+
const { stillInFlightLine } = await import('./commands/plan-continue.js');
|
|
1972
|
+
chunkEmitter.emit('chunk', chalk.dim(`${stillInFlightLine(planContinue.prepared.envelope, planContinue.marker.phaseId)}\n`));
|
|
1906
1973
|
}
|
|
1907
1974
|
}
|
|
1908
1975
|
catch (err) {
|
|
@@ -1923,7 +1990,7 @@ export async function runRepl(opts) {
|
|
|
1923
1990
|
subagentDispatches: planSubagentDispatches,
|
|
1924
1991
|
prompt: inkAwarePrompt,
|
|
1925
1992
|
confirm: inkAwareConfirm,
|
|
1926
|
-
queueDispatch: (cmd) =>
|
|
1993
|
+
queueDispatch: (cmd) => planHops.queue(cmd),
|
|
1927
1994
|
log: (...lines) => chunkEmitter.emit('chunk', lines.join('\n') + '\n'),
|
|
1928
1995
|
});
|
|
1929
1996
|
}
|
|
@@ -1934,6 +2001,35 @@ export async function runRepl(opts) {
|
|
|
1934
2001
|
// C1 — the phase reached a resolved terminal (revision persisted, chain
|
|
1935
2002
|
// ran or is queued): the marker's job is done. Applies to --resume AND
|
|
1936
2003
|
// continuation dispatches; a null outcome keeps the marker (still in flight).
|
|
2004
|
+
// 2026-10-04 (spec §3.5) — SAP went unreachable during the step and
|
|
2005
|
+
// nothing was saved: pause, Enter to retry, then run the SAME step
|
|
2006
|
+
// again as an in-run hop. A queued re-run keeps the lease and the plan
|
|
2007
|
+
// grant (the run goes on); a stop queues nothing, so the run-end
|
|
2008
|
+
// release + grant clear below apply as for any other stop.
|
|
2009
|
+
if ((planResume || planContinue) && !pendingDispatchAccessor.get()) {
|
|
2010
|
+
try {
|
|
2011
|
+
const { handleSapUnreachableAfterPlanTurn, makeSapProbe } = await import('./commands/plan-sap-pause.js');
|
|
2012
|
+
const { inkAwarePrompt } = await import('./agent/loop.js');
|
|
2013
|
+
const { isHeadless } = await import('./renderer/tty.js');
|
|
2014
|
+
await handleSapUnreachableAfterPlanTurn({
|
|
2015
|
+
prepared: planResume ?? planContinue.prepared,
|
|
2016
|
+
persisted: planOutcome !== null,
|
|
2017
|
+
probe: makeSapProbe(adt),
|
|
2018
|
+
prompt: inkAwarePrompt,
|
|
2019
|
+
interactive: !isHeadless(),
|
|
2020
|
+
queue: (cmd) => planHops.queue(cmd),
|
|
2021
|
+
log: (...lines) => chunkEmitter.emit('chunk', lines.join('\n') + '\n'),
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
catch (err) {
|
|
2025
|
+
// The plan file is already safe; the user still gets one instruction.
|
|
2026
|
+
try {
|
|
2027
|
+
const { reportSapPauseFailure } = await import('./commands/plan-sap-pause.js');
|
|
2028
|
+
reportSapPauseFailure(planResume ?? planContinue.prepared, err, (...lines) => chunkEmitter.emit('chunk', lines.join('\n') + '\n'));
|
|
2029
|
+
}
|
|
2030
|
+
catch { /* nothing more can be shown */ }
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
1937
2033
|
if (planOutcome)
|
|
1938
2034
|
session.plan_in_flight = null;
|
|
1939
2035
|
// Task 9 — the chain leg for this dispatch truly ends here (verdict
|
|
@@ -1950,6 +2046,28 @@ export async function runRepl(opts) {
|
|
|
1950
2046
|
if (planResume || planContinue) {
|
|
1951
2047
|
const { releaseRunLeaseUnlessChained } = await import('./projects/run-lease.js');
|
|
1952
2048
|
releaseRunLeaseUnlessChained((planResume ?? planContinue.prepared).path, pendingDispatchAccessor.get());
|
|
2049
|
+
// 2026-10-04 — the run is over (no next step queued, e.g. the turn
|
|
2050
|
+
// failed before the chain ran): the plan object-list grant ends with it.
|
|
2051
|
+
if (pendingDispatchAccessor.get() === null) {
|
|
2052
|
+
const { clearPlanGrant } = await import('./approvals/plan-grant.js');
|
|
2053
|
+
clearPlanGrant();
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
// 2026-10-04 — auto-compact is ON for plan turns: after the revision is
|
|
2057
|
+
// saved, and only when no next step is queued (that step compacts itself).
|
|
2058
|
+
if (planResume || planContinue) {
|
|
2059
|
+
try {
|
|
2060
|
+
const { maybeAutoCompactAfterPlanTurn, makeAutoCompactSummariser } = await import('./commands/plan-boundary.js');
|
|
2061
|
+
const { resolveAutoCompactThreshold } = await import('./models/resolve.js');
|
|
2062
|
+
await maybeAutoCompactAfterPlanTurn({
|
|
2063
|
+
session,
|
|
2064
|
+
config: { ...cfg.compact, auto_threshold_tokens: resolveAutoCompactThreshold(cfg) },
|
|
2065
|
+
queued: pendingDispatchAccessor.get() !== null,
|
|
2066
|
+
summarise: makeAutoCompactSummariser(provider, resolveModelRole('compact', cfg)),
|
|
2067
|
+
emit: (line) => chunkEmitter.emit('chunk', line + '\n'),
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
catch { /* auto-compact must never fail a finished turn */ }
|
|
1953
2071
|
}
|
|
1954
2072
|
// A4 — turn-cost line must price at the model that actually ran the
|
|
1955
2073
|
// turn (planModelOverride on tiered plan phases), not session.model.
|
|
@@ -1972,16 +2090,16 @@ export async function runRepl(opts) {
|
|
|
1972
2090
|
const dispatched = pendingDispatchAccessor.get();
|
|
1973
2091
|
if (dispatched) {
|
|
1974
2092
|
pendingDispatchAccessor.set(null);
|
|
1975
|
-
// No context clear here:
|
|
1976
|
-
// plan-resume prepare branch of handleSubmit) is the
|
|
1977
|
-
// the
|
|
2093
|
+
// No context clear here: beginPlanPhaseContext (plan-boundary.ts,
|
|
2094
|
+
// called in the plan-resume prepare branch of handleSubmit) is the
|
|
2095
|
+
// single owner of the phase-boundary compaction — it runs for typed AND dispatched resumes,
|
|
1978
2096
|
// and only AFTER preparePlanResume succeeds, so a failed prepare
|
|
1979
2097
|
// never destroys the session context.
|
|
1980
2098
|
// Schedule on next tick so the current handleSubmit's promise
|
|
1981
2099
|
// resolves cleanly before the next one starts. Recursion via
|
|
1982
2100
|
// setImmediate keeps the call stack flat across long chains.
|
|
1983
2101
|
// (autoDispatch skips it when a post-turn Ctrl+C / /exit asked to stop.)
|
|
1984
|
-
autoDispatch(dispatched);
|
|
2102
|
+
autoDispatch(dispatched, planHops.take(dispatched));
|
|
1985
2103
|
}
|
|
1986
2104
|
};
|
|
1987
2105
|
// BUG-2b (2026-07-05 live smoke) — single-flight dispatch guard.
|
|
@@ -2002,7 +2120,7 @@ export async function runRepl(opts) {
|
|
|
2002
2120
|
// beginSubmit does mid-turn, instead of starting a twin.
|
|
2003
2121
|
/** Round 4 item 9b — the in-flight (or last) dispatch; settled before unmount on exit. */
|
|
2004
2122
|
let dispatchTail = null;
|
|
2005
|
-
const handleSubmit = async (line) => {
|
|
2123
|
+
const handleSubmit = async (line, submitOpts) => {
|
|
2006
2124
|
if (turnStatusEmitter.isBusy()) {
|
|
2007
2125
|
const t = line.trim();
|
|
2008
2126
|
// Fix round 3 (item 5): slash commands are never steering text.
|
|
@@ -2027,7 +2145,7 @@ export async function runRepl(opts) {
|
|
|
2027
2145
|
postTurnMayChain = false;
|
|
2028
2146
|
try {
|
|
2029
2147
|
// Round 4 item 9b — the exit path waits (capped) for this to finish.
|
|
2030
|
-
const run = dispatchSubmit(line);
|
|
2148
|
+
const run = dispatchSubmit(line, submitOpts);
|
|
2031
2149
|
dispatchTail = run;
|
|
2032
2150
|
await run;
|
|
2033
2151
|
}
|
|
@@ -2232,6 +2350,28 @@ export async function runRepl(opts) {
|
|
|
2232
2350
|
// in commit 349ff75.
|
|
2233
2351
|
patchConsole: true,
|
|
2234
2352
|
});
|
|
2353
|
+
// 2026-10-04 (spec §3.5) — "Continue '<plan>'?" when an unfinished plan
|
|
2354
|
+
// sits in this folder. Best-effort: never blocks the prompt.
|
|
2355
|
+
if (!isHeadless()) {
|
|
2356
|
+
void (async () => {
|
|
2357
|
+
try {
|
|
2358
|
+
const { offerContinueAtStartup, inkStartupChooser } = await import('./commands/plan-startup.js');
|
|
2359
|
+
const { resolveWorkspacePath } = await import('./projects/workspace.js');
|
|
2360
|
+
const ws = await resolveWorkspacePath().catch(() => undefined);
|
|
2361
|
+
if (!ws)
|
|
2362
|
+
return;
|
|
2363
|
+
await offerContinueAtStartup({
|
|
2364
|
+
workspaceDir: ws,
|
|
2365
|
+
sessionId: session.id,
|
|
2366
|
+
sapAlias: session.sap_system ?? undefined,
|
|
2367
|
+
choose: inkStartupChooser(),
|
|
2368
|
+
submit: (cmd) => { void handleSubmit(cmd); },
|
|
2369
|
+
log: (...lines) => chunkEmitter.emit('chunk', lines.join('\n') + '\n'),
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
catch { /* the offer is best-effort */ }
|
|
2373
|
+
})();
|
|
2374
|
+
}
|
|
2235
2375
|
await donePromise;
|
|
2236
2376
|
// Round 4 item 9b — a second idle Ctrl+C right after a turn resolved the
|
|
2237
2377
|
// exit while post-turn work (saving, audit) still ran. Let it finish,
|
|
@@ -2556,6 +2696,29 @@ export async function runRepl(opts) {
|
|
|
2556
2696
|
})),
|
|
2557
2697
|
log: (line) => console.log(chalk.dim(line)),
|
|
2558
2698
|
});
|
|
2699
|
+
// 2026-10-04 (spec §3.5) — "Continue '<plan>'?" (classic: numbered
|
|
2700
|
+
// choices, Enter = the first). Continue queues the resume for the loop.
|
|
2701
|
+
if (!isHeadless()) {
|
|
2702
|
+
try {
|
|
2703
|
+
const { offerContinueAtStartup, classicStartupChooser } = await import('./commands/plan-startup.js');
|
|
2704
|
+
const { inkAwarePrompt } = await import('./agent/loop.js');
|
|
2705
|
+
const { resolveWorkspacePath } = await import('./projects/workspace.js');
|
|
2706
|
+
const ws = await resolveWorkspacePath().catch(() => undefined);
|
|
2707
|
+
if (ws) {
|
|
2708
|
+
await offerContinueAtStartup({
|
|
2709
|
+
workspaceDir: ws,
|
|
2710
|
+
sessionId: session.id,
|
|
2711
|
+
sapAlias: session.sap_system ?? undefined,
|
|
2712
|
+
choose: classicStartupChooser(inkAwarePrompt, (...l) => l.forEach((x) => console.log(x))),
|
|
2713
|
+
submit: (cmd) => pendingDispatchAccessor.set(cmd),
|
|
2714
|
+
log: (...l) => l.forEach((x) => console.log(x)),
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
catch { /* best-effort */ }
|
|
2719
|
+
}
|
|
2720
|
+
// Task 13 — classic in-run hop flag: set by the loop-top consumer from planHops.take().
|
|
2721
|
+
let classicPlanHop = false;
|
|
2559
2722
|
while (true) {
|
|
2560
2723
|
// Re-attach the bare-Esc detector every iteration. Inquirer prompts
|
|
2561
2724
|
// (slash-picker, approval, ask_question, safety-confirm) remove
|
|
@@ -2568,15 +2731,22 @@ export async function runRepl(opts) {
|
|
|
2568
2731
|
// wait and feed that command to the loop directly — the user's
|
|
2569
2732
|
// consent was the picker pick, no Enter required. Cleared
|
|
2570
2733
|
// immediately so a stale value never re-fires.
|
|
2734
|
+
classicPlanHop = false; // a typed line is never an in-run hop
|
|
2571
2735
|
const dispatchedCmd = pendingDispatchAccessor.get();
|
|
2572
2736
|
if (dispatchedCmd) {
|
|
2573
2737
|
pendingDispatchAccessor.set(null);
|
|
2574
|
-
// No context clear here:
|
|
2575
|
-
// plan-resume prepare branch below) is the single owner
|
|
2576
|
-
//
|
|
2738
|
+
// No context clear here: beginPlanPhaseContext (plan-boundary.ts,
|
|
2739
|
+
// called in the plan-resume prepare branch below) is the single owner
|
|
2740
|
+
// of the phase-boundary compaction — it runs for typed AND dispatched resumes, and
|
|
2577
2741
|
// only AFTER preparePlanResume succeeds, so a failed prepare never
|
|
2578
2742
|
// destroys the session context.
|
|
2579
|
-
|
|
2743
|
+
// Task 13 — no echo for a plan hop: the chain already said "Next: <title>".
|
|
2744
|
+
{
|
|
2745
|
+
const n = autoRoutedNotice(dispatchedCmd);
|
|
2746
|
+
if (n)
|
|
2747
|
+
console.log(formatInfo(n).trimEnd());
|
|
2748
|
+
classicPlanHop = planHops.take(dispatchedCmd);
|
|
2749
|
+
}
|
|
2580
2750
|
line = dispatchedCmd;
|
|
2581
2751
|
}
|
|
2582
2752
|
else if (earlyLines.size() > 0) {
|
|
@@ -2713,6 +2883,7 @@ export async function runRepl(opts) {
|
|
|
2713
2883
|
}
|
|
2714
2884
|
await runExportAuditCommand({
|
|
2715
2885
|
session,
|
|
2886
|
+
operator: signedInAs,
|
|
2716
2887
|
transport: getCurrentTransport(),
|
|
2717
2888
|
filename,
|
|
2718
2889
|
emit: (l) => console.log(l),
|
|
@@ -3221,6 +3392,8 @@ export async function runRepl(opts) {
|
|
|
3221
3392
|
// envelope revision after the turn.
|
|
3222
3393
|
let planResume = null;
|
|
3223
3394
|
let planResumeMsgStart = 0;
|
|
3395
|
+
// Task 13 — in-run hop flag (see Ink path).
|
|
3396
|
+
let planHop = false;
|
|
3224
3397
|
// D-B — see the Ink-path mirror above: this attempt's turn-start cutoff.
|
|
3225
3398
|
let planPhaseStartedAt = '';
|
|
3226
3399
|
// A4 — set only when plan model tiering picks Sonnet for this phase.
|
|
@@ -3233,9 +3406,13 @@ export async function runRepl(opts) {
|
|
|
3233
3406
|
try {
|
|
3234
3407
|
const { preparePlanResume } = await import('./commands/plan-resume.js');
|
|
3235
3408
|
const { inkAwarePrompt, inkAwareConfirm } = await import('./agent/loop.js');
|
|
3409
|
+
planHop = classicPlanHop;
|
|
3410
|
+
classicPlanHop = false;
|
|
3236
3411
|
planResume = await preparePlanResume({
|
|
3237
3412
|
body: prompt,
|
|
3238
3413
|
cwd: process.cwd(),
|
|
3414
|
+
// Task 13 — an in-run hop prints one progress line, not the board.
|
|
3415
|
+
boardStyle: planHop ? 'line' : 'full',
|
|
3239
3416
|
configMode: cfg.plan_mode,
|
|
3240
3417
|
// Audit off-switch (2026-07-06, owner) — config default; per-run
|
|
3241
3418
|
// --audit/--no-audit flags in the body override it (parsed inside).
|
|
@@ -3244,6 +3421,9 @@ export async function runRepl(opts) {
|
|
|
3244
3421
|
sessionId: session.id,
|
|
3245
3422
|
// 2026-10-02 — deploy card on a direct resume into ui.deploy.
|
|
3246
3423
|
sapAlias: session.sap_system ?? undefined,
|
|
3424
|
+
// 2026-10-04 — re-check SAP before the step (skipped without a SAP system).
|
|
3425
|
+
reground: standalone ? undefined : makeRegroundDeps(adt),
|
|
3426
|
+
interactive: !isHeadless(),
|
|
3247
3427
|
prompt: inkAwarePrompt,
|
|
3248
3428
|
confirm: inkAwareConfirm,
|
|
3249
3429
|
log: (...lines) => lines.forEach((l) => console.log(l)),
|
|
@@ -3253,10 +3433,31 @@ export async function runRepl(opts) {
|
|
|
3253
3433
|
console.log('\n' + formatFailure({ what: 'Could not resume the plan', detail: [err.message] }));
|
|
3254
3434
|
}
|
|
3255
3435
|
if (!planResume) {
|
|
3436
|
+
// 2026-10-04 — resume could not start: the grant ends with the run (classic mirror).
|
|
3437
|
+
{
|
|
3438
|
+
const { clearPlanGrant } = await import('./approvals/plan-grant.js');
|
|
3439
|
+
clearPlanGrant();
|
|
3440
|
+
}
|
|
3256
3441
|
session.last_skill = previousSkill ?? null;
|
|
3257
3442
|
session.last_object = extractedObject ?? previousObject ?? null;
|
|
3258
3443
|
continue;
|
|
3259
3444
|
}
|
|
3445
|
+
// 2026-10-04 (F13) — classic mirror: the object list once before a typed guarded step.
|
|
3446
|
+
if (!isHeadless()) {
|
|
3447
|
+
try {
|
|
3448
|
+
const { offerObjectListOnResume, classicStartupChooser } = await import('./commands/plan-startup.js');
|
|
3449
|
+
const { inkAwarePrompt } = await import('./agent/loop.js');
|
|
3450
|
+
await offerObjectListOnResume({
|
|
3451
|
+
prepared: planResume,
|
|
3452
|
+
planHop,
|
|
3453
|
+
sessionId: session.id,
|
|
3454
|
+
sapAlias: session.sap_system ?? undefined,
|
|
3455
|
+
choose: classicStartupChooser(inkAwarePrompt, (...l) => l.forEach((x) => console.log(x))),
|
|
3456
|
+
log: (...l) => l.forEach((x) => console.log(x)),
|
|
3457
|
+
});
|
|
3458
|
+
}
|
|
3459
|
+
catch { /* best-effort: each write asks */ }
|
|
3460
|
+
}
|
|
3260
3461
|
// Task 6 — re-audit-on-resume (classic mirror of the Ink branch; see
|
|
3261
3462
|
// there for the design note). No model turn: the chain re-judges the
|
|
3262
3463
|
// unresolved phase and queues any follow-up through pendingDispatch,
|
|
@@ -3268,17 +3469,18 @@ export async function runRepl(opts) {
|
|
|
3268
3469
|
await runReauditPass({
|
|
3269
3470
|
savedPath: planResume.path,
|
|
3270
3471
|
phaseIds: planResume.reauditPhaseIds ?? [planResume.reauditPhaseId],
|
|
3472
|
+
force: planResume.reauditForce,
|
|
3271
3473
|
sessionId: session.id,
|
|
3272
3474
|
cwd: process.cwd(),
|
|
3273
3475
|
sapAlias: session.sap_system ?? undefined,
|
|
3274
|
-
mode: planResume.effectiveMode ??
|
|
3476
|
+
mode: planResume.effectiveMode ?? DEFAULT_PLAN_MODE,
|
|
3275
3477
|
modeFlag: planResume.mode,
|
|
3276
3478
|
// Off-switch (I3/I2) — classic mirror; see the Ink re-audit site.
|
|
3277
3479
|
auditMode: planResume.auditMode,
|
|
3278
3480
|
auditFlag: planResume.auditFlag,
|
|
3279
3481
|
prompt: inkAwarePrompt,
|
|
3280
3482
|
confirm: inkAwareConfirm,
|
|
3281
|
-
queueDispatch: (cmd) =>
|
|
3483
|
+
queueDispatch: (cmd) => planHops.queue(cmd),
|
|
3282
3484
|
log: (...lines) => lines.forEach((l) => console.log(l)),
|
|
3283
3485
|
});
|
|
3284
3486
|
}
|
|
@@ -3293,17 +3495,24 @@ export async function runRepl(opts) {
|
|
|
3293
3495
|
const { releaseRunLeaseUnlessChained } = await import('./projects/run-lease.js');
|
|
3294
3496
|
releaseRunLeaseUnlessChained(planResume.path, pendingDispatchAccessor.get());
|
|
3295
3497
|
}
|
|
3498
|
+
// 2026-10-04 — re-audit leg queued nothing: grant ends with the run (classic mirror).
|
|
3499
|
+
if (pendingDispatchAccessor.get() === null) {
|
|
3500
|
+
const { clearPlanGrant } = await import('./approvals/plan-grant.js');
|
|
3501
|
+
clearPlanGrant();
|
|
3502
|
+
}
|
|
3296
3503
|
session.last_skill = previousSkill ?? null;
|
|
3297
3504
|
session.last_object = extractedObject ?? previousObject ?? null;
|
|
3298
3505
|
continue;
|
|
3299
3506
|
}
|
|
3300
|
-
//
|
|
3301
|
-
//
|
|
3507
|
+
// 2026-10-04 (continuous run) — see the Ink path: a phase boundary is
|
|
3508
|
+
// save + COMPACT, falling back to today's reset if the summariser fails.
|
|
3302
3509
|
{
|
|
3303
|
-
const {
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3510
|
+
const { beginPlanPhaseContext, makePhaseBoundarySummariser } = await import('./commands/plan-boundary.js');
|
|
3511
|
+
await beginPlanPhaseContext({
|
|
3512
|
+
session,
|
|
3513
|
+
summarise: makePhaseBoundarySummariser(provider, resolveModelRole('compact', cfg)),
|
|
3514
|
+
log: (line) => console.log(line),
|
|
3515
|
+
});
|
|
3307
3516
|
}
|
|
3308
3517
|
planResumeMsgStart = session.messages.length;
|
|
3309
3518
|
planPhaseStartedAt = new Date().toISOString(); // D-B evidence-window cutoff
|
|
@@ -3355,7 +3564,8 @@ export async function runRepl(opts) {
|
|
|
3355
3564
|
planContinue = { prepared: r.prepared, marker };
|
|
3356
3565
|
planPhaseStartedAt = marker.attemptStartedAt;
|
|
3357
3566
|
planResumeMsgStart = session.messages.length;
|
|
3358
|
-
|
|
3567
|
+
const { continuationNotice } = await import('./commands/plan-continue.js');
|
|
3568
|
+
console.log(formatInfo(continuationNotice(r.prepared)).trimEnd());
|
|
3359
3569
|
}
|
|
3360
3570
|
else if (r.stale) {
|
|
3361
3571
|
session.plan_in_flight = null; // plan/phase gone for good — marker is noise
|
|
@@ -3431,16 +3641,20 @@ export async function runRepl(opts) {
|
|
|
3431
3641
|
const { setPlanPhaseActive, setGuardedRunActive, setCurrentPhaseWrites, getAndResetPlanDeviation } = await import('./commands/plan-gate.js');
|
|
3432
3642
|
// Task 7 — plan-phase subagent read-only enforcement (classic mirror):
|
|
3433
3643
|
// set for EVERY plan-resume phase turn regardless of mode.
|
|
3434
|
-
setPlanPhaseActive(true);
|
|
3644
|
+
setPlanPhaseActive(true, planPhaseCtx.envelope.id);
|
|
3435
3645
|
// Task 10 — executing phase's writes declaration for the deviation
|
|
3436
3646
|
// backstop + stale-flag discard (classic mirror; see Ink path note).
|
|
3437
3647
|
setCurrentPhaseWrites(planPhaseCtx.nextPhaseWrites ?? true);
|
|
3438
3648
|
getAndResetPlanDeviation();
|
|
3649
|
+
// Stale "SAP unreachable" from an earlier turn must not pause THIS step (classic mirror).
|
|
3650
|
+
(await import('./sap/reachability-state.js')).getAndResetSapUnreachable();
|
|
3439
3651
|
planPhaseFlagSet = true;
|
|
3440
|
-
if ((planPhaseCtx.effectiveMode ??
|
|
3652
|
+
if ((planPhaseCtx.effectiveMode ?? DEFAULT_PLAN_MODE) === 'guarded') {
|
|
3441
3653
|
setGuardedRunActive(true);
|
|
3442
3654
|
guardedRunFlagSet = true;
|
|
3443
|
-
|
|
3655
|
+
// Task 13 — once per run (first hop only), in plain words.
|
|
3656
|
+
if (!planHop)
|
|
3657
|
+
console.log(formatInfo('Continuous build · auto-approve is off; each write still runs snapshot, syntax check, activate, verify').trimEnd());
|
|
3444
3658
|
}
|
|
3445
3659
|
// Off-switch (2026-07-06, owner) — announce plainly that this run's
|
|
3446
3660
|
// phases are NOT audited (honesty: off is a choice, never a lie).
|
|
@@ -3448,6 +3662,8 @@ export async function runRepl(opts) {
|
|
|
3448
3662
|
console.log(chalk.dim('plan audits: off'));
|
|
3449
3663
|
}
|
|
3450
3664
|
}
|
|
3665
|
+
// 2026-10-04 — the phase's expert skill rides along in the system prompt.
|
|
3666
|
+
const planPhaseDelegate = planPhaseCtx?.nextDelegateTo;
|
|
3451
3667
|
await runTurn({
|
|
3452
3668
|
provider,
|
|
3453
3669
|
userMessage: prompt,
|
|
@@ -3460,6 +3676,7 @@ export async function runRepl(opts) {
|
|
|
3460
3676
|
// Task 19 — auto-compact at turn end, except plan resume/continue
|
|
3461
3677
|
// turns (their post-turn persist indexes messages from turn start).
|
|
3462
3678
|
autoCompact: planResume === null && planContinue === null,
|
|
3679
|
+
delegateSkill: planPhaseDelegate,
|
|
3463
3680
|
// A4 — undefined on every non-plan turn (and when tiering is off).
|
|
3464
3681
|
modelOverride: planModelOverride,
|
|
3465
3682
|
// Task 9 — a tier-down, never the session role (proxy must not enforce it).
|
|
@@ -3583,7 +3800,7 @@ export async function runRepl(opts) {
|
|
|
3583
3800
|
sessionId: session.id,
|
|
3584
3801
|
cwd: process.cwd(),
|
|
3585
3802
|
sapAlias: session.sap_system ?? undefined,
|
|
3586
|
-
mode: planResume.effectiveMode ??
|
|
3803
|
+
mode: planResume.effectiveMode ?? DEFAULT_PLAN_MODE,
|
|
3587
3804
|
modeFlag: planResume.mode,
|
|
3588
3805
|
// Off-switch (I3/I2) — classic mirror of the Ink chain call: skip
|
|
3589
3806
|
// the audit when 'off'; per-run flag survives onto resume commands.
|
|
@@ -3595,7 +3812,7 @@ export async function runRepl(opts) {
|
|
|
3595
3812
|
subagentDispatches: planSubagentDispatches,
|
|
3596
3813
|
prompt: inkAwarePrompt,
|
|
3597
3814
|
confirm: inkAwareConfirm,
|
|
3598
|
-
queueDispatch: (cmd) =>
|
|
3815
|
+
queueDispatch: (cmd) => planHops.queue(cmd),
|
|
3599
3816
|
log: (...lines) => lines.forEach((l) => console.log(l)),
|
|
3600
3817
|
});
|
|
3601
3818
|
}
|
|
@@ -3634,8 +3851,9 @@ export async function runRepl(opts) {
|
|
|
3634
3851
|
emit: (line) => console.log(line),
|
|
3635
3852
|
});
|
|
3636
3853
|
}
|
|
3637
|
-
else if (r.kind === 'none') {
|
|
3638
|
-
|
|
3854
|
+
else if (r.kind === 'none' && !(await import('./sap/reachability-state.js')).isSapUnreachableNoted()) {
|
|
3855
|
+
const { stillInFlightLine } = await import('./commands/plan-continue.js');
|
|
3856
|
+
console.log(chalk.dim(stillInFlightLine(planContinue.prepared.envelope, planContinue.marker.phaseId)));
|
|
3639
3857
|
}
|
|
3640
3858
|
}
|
|
3641
3859
|
catch (err) {
|
|
@@ -3656,7 +3874,7 @@ export async function runRepl(opts) {
|
|
|
3656
3874
|
subagentDispatches: planSubagentDispatches,
|
|
3657
3875
|
prompt: inkAwarePrompt,
|
|
3658
3876
|
confirm: inkAwareConfirm,
|
|
3659
|
-
queueDispatch: (cmd) =>
|
|
3877
|
+
queueDispatch: (cmd) => planHops.queue(cmd),
|
|
3660
3878
|
log: (...lines) => lines.forEach((l) => console.log(l)),
|
|
3661
3879
|
});
|
|
3662
3880
|
}
|
|
@@ -3667,6 +3885,32 @@ export async function runRepl(opts) {
|
|
|
3667
3885
|
// C1 — the phase reached a resolved terminal (revision persisted, chain
|
|
3668
3886
|
// ran or is queued): the marker's job is done. Applies to --resume AND
|
|
3669
3887
|
// continuation dispatches; a null outcome keeps the marker (still in flight).
|
|
3888
|
+
// 2026-10-04 (spec §3.5) — SAP unreachable mid-step: pause, Enter to
|
|
3889
|
+
// retry, re-run the SAME step as an in-run hop (classic mirror of the Ink site).
|
|
3890
|
+
if ((planResume || planContinue) && !pendingDispatchAccessor.get()) {
|
|
3891
|
+
try {
|
|
3892
|
+
const { handleSapUnreachableAfterPlanTurn, makeSapProbe } = await import('./commands/plan-sap-pause.js');
|
|
3893
|
+
const { inkAwarePrompt } = await import('./agent/loop.js');
|
|
3894
|
+
const { isHeadless } = await import('./renderer/tty.js');
|
|
3895
|
+
await handleSapUnreachableAfterPlanTurn({
|
|
3896
|
+
prepared: planResume ?? planContinue.prepared,
|
|
3897
|
+
persisted: planOutcome !== null,
|
|
3898
|
+
probe: makeSapProbe(adt),
|
|
3899
|
+
prompt: inkAwarePrompt,
|
|
3900
|
+
interactive: !isHeadless(),
|
|
3901
|
+
queue: (cmd) => planHops.queue(cmd),
|
|
3902
|
+
log: (...lines) => lines.forEach((l) => console.log(l)),
|
|
3903
|
+
});
|
|
3904
|
+
}
|
|
3905
|
+
catch (err) {
|
|
3906
|
+
// The plan file is already safe; the user still gets one instruction (classic mirror).
|
|
3907
|
+
try {
|
|
3908
|
+
const { reportSapPauseFailure } = await import('./commands/plan-sap-pause.js');
|
|
3909
|
+
reportSapPauseFailure(planResume ?? planContinue.prepared, err, (...lines) => lines.forEach((l) => console.log(l)));
|
|
3910
|
+
}
|
|
3911
|
+
catch { /* nothing more can be shown */ }
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3670
3914
|
if (planOutcome)
|
|
3671
3915
|
session.plan_in_flight = null;
|
|
3672
3916
|
// Task 9 — chain leg truly ends here: release the run lease UNLESS a
|
|
@@ -3677,6 +3921,27 @@ export async function runRepl(opts) {
|
|
|
3677
3921
|
if (planResume || planContinue) {
|
|
3678
3922
|
const { releaseRunLeaseUnlessChained } = await import('./projects/run-lease.js');
|
|
3679
3923
|
releaseRunLeaseUnlessChained((planResume ?? planContinue.prepared).path, pendingDispatchAccessor.get());
|
|
3924
|
+
// 2026-10-04 — run over: the plan object-list grant ends with it (classic mirror).
|
|
3925
|
+
if (pendingDispatchAccessor.get() === null) {
|
|
3926
|
+
const { clearPlanGrant } = await import('./approvals/plan-grant.js');
|
|
3927
|
+
clearPlanGrant();
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
// 2026-10-04 — auto-compact is ON for plan turns (classic mirror of the
|
|
3931
|
+
// Ink site): after the revision is saved, only when no next step is queued.
|
|
3932
|
+
if (planResume || planContinue) {
|
|
3933
|
+
try {
|
|
3934
|
+
const { maybeAutoCompactAfterPlanTurn, makeAutoCompactSummariser } = await import('./commands/plan-boundary.js');
|
|
3935
|
+
const { resolveAutoCompactThreshold } = await import('./models/resolve.js');
|
|
3936
|
+
await maybeAutoCompactAfterPlanTurn({
|
|
3937
|
+
session,
|
|
3938
|
+
config: { ...cfg.compact, auto_threshold_tokens: resolveAutoCompactThreshold(cfg) },
|
|
3939
|
+
queued: pendingDispatchAccessor.get() !== null,
|
|
3940
|
+
summarise: makeAutoCompactSummariser(provider, resolveModelRole('compact', cfg)),
|
|
3941
|
+
emit: (line) => console.log(line),
|
|
3942
|
+
});
|
|
3943
|
+
}
|
|
3944
|
+
catch { /* auto-compact must never fail a finished turn */ }
|
|
3680
3945
|
}
|
|
3681
3946
|
// Status footer after each turn.
|
|
3682
3947
|
// tool_result messages also have role:'user' but content is an array —
|