tumwater 0.4.2 → 0.4.3
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 +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +25 -12
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +1 -8
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +9 -0
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +50 -27
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +1 -1
- package/dist/src/agent/subscription-account.js +3 -3
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +58 -102
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +1 -1
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-scoped.js +40 -16
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +34 -12
- package/dist/src/cli/cli-query-commands.js +1 -1
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-start.js +91 -58
- package/dist/src/cli/completion.js +82 -46
- package/dist/src/cli/config-commands.js +82 -71
- package/dist/src/cli/help.js +4 -6
- package/dist/src/cli/log-commands.js +43 -23
- package/dist/src/cli/notify-command.js +2 -2
- package/dist/src/cli/question-commands.js +86 -60
- package/dist/src/cli.js +181 -135
- package/dist/src/collections.js +31 -16
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +22 -0
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +5 -20
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +153 -102
- package/dist/src/config/config-schema.js +9 -11
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +4 -52
- package/dist/src/config/config-validation.js +4 -38
- package/dist/src/config/config-views.js +9 -34
- package/dist/src/config/config-write.js +6 -23
- package/dist/src/config/config.js +4 -5
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +24 -24
- package/dist/src/doctor/doctor-backlog.js +7 -5
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +5 -5
- package/dist/src/doctor/doctor-claude-checks.js +44 -28
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +44 -112
- package/dist/src/doctor/doctor-fallback-checks.js +2 -2
- package/dist/src/doctor/doctor-holds.js +6 -45
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +120 -20
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +3 -3
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +30 -54
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +1 -3
- package/dist/src/events/{notify.js → event-notify.js} +2 -2
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/failure/failure-cluster.js +6 -6
- package/dist/src/failure/failure-data.js +8 -4
- package/dist/src/failure/failure-render.js +44 -34
- package/dist/src/failure/failure-state-change.js +4 -34
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +27 -68
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +7 -7
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +22 -23
- package/dist/src/gui/gui-prompt-commands.js +23 -23
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +51 -33
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +8 -11
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +10 -13
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-verify.js +8 -18
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +76 -44
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +27 -32
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +1 -1
- package/dist/src/orchestrator/scheduling-skip.js +1 -1
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +8 -7
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +1 -1
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +32 -6
- package/dist/src/pi/pi-watchdogs.js +12 -5
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +49 -5
- package/dist/src/process/process.js +8 -8
- package/dist/src/process/run-marker.js +31 -13
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +1 -1
- package/dist/src/review/review-precheck.js +11 -1
- package/dist/src/review/review.js +40 -153
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +1 -14
- package/dist/src/roles/role-view.js +9 -21
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/run-apply.js +12 -6
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +135 -84
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +119 -50
- package/dist/src/run/stage-check.js +49 -32
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/runner-outcome.js +6 -1
- package/dist/src/runner/runner-pi.js +3 -10
- package/dist/src/runner/runner-recovery.js +1 -1
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
- package/dist/src/runner/runner-run.js +97 -35
- package/dist/src/runner/runner-state-load.js +12 -9
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-data.js +2 -2
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -17
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +98 -65
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
- package/dist/src/team/team-polls.js +28 -126
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +20 -15
- package/dist/src/text/text.js +3 -3
- package/dist/src/ui/alert-feeds.js +49 -73
- package/dist/src/ui/badges.js +3 -13
- package/dist/src/ui/gui/gui-client-boot.js +2 -2
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +4 -4
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +53 -40
- package/dist/src/ui/run-progress-model.js +9 -2
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +3 -7
- package/dist/src/ui/status-render.js +5 -9
- package/dist/src/ui/team-alert-backlog.js +6 -6
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +7 -8
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +1 -7
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +159 -92
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +3 -3
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -228
- package/dist/src/ui/tui/tui-pane-history.js +61 -70
- package/dist/src/ui/tui/tui-pane-pending.js +284 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +11 -7
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +29 -32
- package/dist/src/ui/tui/tui.js +3 -4
- package/dist/src/verdict/fix-claim.js +76 -54
- package/dist/src/verdict/refusal.js +1 -1
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
- /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
|
@@ -3,7 +3,7 @@ import { changedFiles } from "../git/git-diff.js";
|
|
|
3
3
|
import { buildCommitMessage, commitTrailer, extractCommitBody, extractSummary, summaryOverCap, fallbackSummary, formatCommitBody, appendSelfCheckNotes, stampedSubject, } from "../git/commit-message.js";
|
|
4
4
|
import { logEventBestEffort } from "../events/events.js";
|
|
5
5
|
import { enqueueMerge, entryRevisionFields, queuedMerges } from "../merge/merge-queue.js";
|
|
6
|
-
import { hasUnmergedPin } from "../runner/leftover.js";
|
|
6
|
+
import { hasUnmergedPin } from "../runner/runner-leftover.js";
|
|
7
7
|
import { plural } from "../text/phrases.js";
|
|
8
8
|
import { squash } from "../text/text.js";
|
|
9
9
|
import { recordDismissedFiling, recordDismissedFlow } from "./run-dismiss.js";
|
|
@@ -56,75 +56,114 @@ function revisionFields(ctx) {
|
|
|
56
56
|
: undefined;
|
|
57
57
|
return { revisionRound, priorReview };
|
|
58
58
|
}
|
|
59
|
+
/** The bounded follow-up turn both recovery paths share: fold the follow-up's commits past the
|
|
60
|
+
* head it started from, and end the run aborted (returning the outcome to surface) when the
|
|
61
|
+
* follow-up aborted, else hand back the follow-up run. */
|
|
62
|
+
async function requestSummaryFollowUp(ctx, over) {
|
|
63
|
+
const base = await headOf(ctx.wt, "HEAD");
|
|
64
|
+
const followUp = await ctx.requestSummary(ctx.wt, over);
|
|
65
|
+
await foldAgentCommitsSince(ctx, ctx.wt, base);
|
|
66
|
+
if (followUp?.aborted)
|
|
67
|
+
return { aborted: await ctx.finishAbortedRun() };
|
|
68
|
+
return { followUp };
|
|
69
|
+
}
|
|
70
|
+
/** Recover a summary when the reply left none: take the shared follow-up turn, refill `pending`
|
|
71
|
+
* from it, fall back to the changed paths when it still yields none, and warn about the recovery.
|
|
72
|
+
* Returns an outcome only when the follow-up aborted, else `undefined`. */
|
|
73
|
+
async function recoverMissingSummary(ctx, pending) {
|
|
74
|
+
const result = await requestSummaryFollowUp(ctx);
|
|
75
|
+
if ("aborted" in result)
|
|
76
|
+
return result.aborted;
|
|
77
|
+
const followUp = result.followUp;
|
|
78
|
+
if (followUp) {
|
|
79
|
+
pending.summary = extractSummary(followUp.finalText);
|
|
80
|
+
pending.body = pending.body ?? extractCommitBody(followUp.finalText);
|
|
81
|
+
}
|
|
82
|
+
if (pending.summary === null) {
|
|
83
|
+
pending.summary = fallbackSummary(await changedFiles(ctx.wt), ctx.role, ctx.state.runs);
|
|
84
|
+
pending.summaryDerived = true;
|
|
85
|
+
}
|
|
86
|
+
ctx.warn(missingSummaryWarning(followUp, pending.summary));
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
/** The missing-SUMMARY warning's four-way wording: "recovered it with a follow-up turn", or the
|
|
90
|
+
* derived-subject variants naming the follow-up's failure or absence. */
|
|
91
|
+
function missingSummaryWarning(followUp, summary) {
|
|
92
|
+
return (`reply had no SUMMARY line — ` +
|
|
93
|
+
(followUp && extractSummary(followUp.finalText) !== null
|
|
94
|
+
? "recovered it with a follow-up turn"
|
|
95
|
+
: followUp && !followUp.ok
|
|
96
|
+
? `the follow-up run failed (${followUp.errorMessage || "no error message"}); ` +
|
|
97
|
+
`subject derived from the changed files: "${summary}"`
|
|
98
|
+
: followUp
|
|
99
|
+
? `follow-up gave none; subject derived from the changed files: "${summary}"`
|
|
100
|
+
: `no follow-up session was available; subject derived from the changed files: "${summary}"`));
|
|
101
|
+
}
|
|
102
|
+
/** Shorten a SUMMARY the cap would cut mid-word: take the shared follow-up turn asking for it
|
|
103
|
+
* restated short, keep the restated subject when it fits, and warn. Returns an outcome only when
|
|
104
|
+
* the follow-up aborted, else `undefined`. */
|
|
105
|
+
async function shortenOverCapSummary(ctx, pending) {
|
|
106
|
+
const over = summaryOverCap(ctx.finalText);
|
|
107
|
+
if (over === null)
|
|
108
|
+
return undefined;
|
|
109
|
+
// A SUMMARY the cap would cut mid-word gets one follow-up turn asking for it restated short.
|
|
110
|
+
const result = await requestSummaryFollowUp(ctx, over);
|
|
111
|
+
if ("aborted" in result)
|
|
112
|
+
return result.aborted;
|
|
113
|
+
const followUp = result.followUp;
|
|
114
|
+
const short = followUp && summaryOverCap(followUp.finalText) === null ? extractSummary(followUp.finalText) : null;
|
|
115
|
+
if (short !== null) {
|
|
116
|
+
pending.summary = short;
|
|
117
|
+
pending.body = pending.body ?? extractCommitBody(followUp.finalText);
|
|
118
|
+
}
|
|
119
|
+
ctx.warn(short !== null
|
|
120
|
+
? `SUMMARY was ${over} characters — shortened with a follow-up turn`
|
|
121
|
+
: `SUMMARY was ${over} characters — follow-up gave no shorter one; subject truncated`);
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
59
124
|
/** Derive the commit's subject and body from the reply's SUMMARY block, running one bounded
|
|
60
125
|
* follow-up turn when the reply left none or a SUMMARY the cap would cut mid-word. Returns the
|
|
61
126
|
* aborted-run outcome when a follow-up aborts, otherwise the description. */
|
|
62
127
|
async function resolveCommitDescription(ctx) {
|
|
63
|
-
const s = ctx.state;
|
|
64
128
|
// The commit subject and body come from the reply's closing block. A run that changed files
|
|
65
129
|
// without one — a cut-off final message, or plain non-compliance — gets one bounded follow-up
|
|
66
130
|
// turn in its own session to produce it (the session still holds everything the run did);
|
|
67
131
|
// only if that too yields nothing is the subject derived from the changed paths.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
await
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
body = body ?? extractCommitBody(followUp.finalText);
|
|
81
|
-
}
|
|
82
|
-
if (summary === null) {
|
|
83
|
-
summary = fallbackSummary(await changedFiles(ctx.wt), ctx.role, s.runs);
|
|
84
|
-
summaryDerived = true;
|
|
85
|
-
}
|
|
86
|
-
ctx.warn(`reply had no SUMMARY line — ` +
|
|
87
|
-
(followUp && extractSummary(followUp.finalText) !== null
|
|
88
|
-
? "recovered it with a follow-up turn"
|
|
89
|
-
: followUp && !followUp.ok
|
|
90
|
-
? `the follow-up run failed (${followUp.errorMessage || "no error message"}); ` +
|
|
91
|
-
`subject derived from the changed files: "${summary}"`
|
|
92
|
-
: followUp
|
|
93
|
-
? `follow-up gave none; subject derived from the changed files: "${summary}"`
|
|
94
|
-
: `no follow-up session was available; subject derived from the changed files: "${summary}"`));
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
// A SUMMARY the cap would cut mid-word gets one follow-up turn asking for it restated short.
|
|
98
|
-
const over = summaryOverCap(ctx.finalText);
|
|
99
|
-
if (over !== null) {
|
|
100
|
-
const base = await headOf(ctx.wt, "HEAD");
|
|
101
|
-
const followUp = await ctx.requestSummary(ctx.wt, over);
|
|
102
|
-
await foldAgentCommitsSince(ctx, ctx.wt, base);
|
|
103
|
-
if (followUp?.aborted)
|
|
104
|
-
return ctx.finishAbortedRun();
|
|
105
|
-
const short = followUp && summaryOverCap(followUp.finalText) === null ? extractSummary(followUp.finalText) : null;
|
|
106
|
-
if (short !== null) {
|
|
107
|
-
summary = short;
|
|
108
|
-
body = body ?? extractCommitBody(followUp.finalText);
|
|
109
|
-
}
|
|
110
|
-
ctx.warn(short !== null
|
|
111
|
-
? `SUMMARY was ${over} characters — shortened with a follow-up turn`
|
|
112
|
-
: `SUMMARY was ${over} characters — follow-up gave no shorter one; subject truncated`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return { summary, body, summaryDerived };
|
|
132
|
+
const pending = {
|
|
133
|
+
summary: extractSummary(ctx.finalText),
|
|
134
|
+
body: extractCommitBody(ctx.finalText),
|
|
135
|
+
// True when no reply named the change, so the subject below is derived from its paths.
|
|
136
|
+
summaryDerived: false,
|
|
137
|
+
};
|
|
138
|
+
const outcome = pending.summary === null
|
|
139
|
+
? await recoverMissingSummary(ctx, pending)
|
|
140
|
+
: await shortenOverCapSummary(ctx, pending);
|
|
141
|
+
if (outcome)
|
|
142
|
+
return outcome;
|
|
143
|
+
return { summary: pending.summary, body: pending.body, summaryDerived: pending.summaryDerived };
|
|
116
144
|
}
|
|
117
145
|
/** Run the gate's deterministic backlog checks before the commit, giving the author's session
|
|
118
146
|
* one bounded fix-up turn, and end the run `no_change` when the fix-up reverts the whole change
|
|
119
147
|
* (leaving nothing to commit). Returns an outcome only when the run must end early; otherwise
|
|
120
148
|
* mutates `desc.summary`/`desc.body` with any fix-up recovery, exactly as the inline block did. */
|
|
121
149
|
async function runStageSelfCheck(ctx, desc) {
|
|
122
|
-
const s = ctx.state;
|
|
123
150
|
// The gate's deterministic backlog checks run here, before the commit, while the authoring
|
|
124
151
|
// session can still be continued: a finding that reaches review costs a queue slot,
|
|
125
152
|
// a verification, and a rejection that discards the work. One bounded follow-up turn on the
|
|
126
153
|
// author's own session gets to fix them; the gate still has the final say, and the change commits
|
|
127
154
|
// and queues either way.
|
|
155
|
+
const moved = await stagedBacklogMoves(ctx);
|
|
156
|
+
const movedFindings = recordStagedClaims(ctx, moved);
|
|
157
|
+
const findings = await ctx.stageCheck(ctx.wt);
|
|
158
|
+
findings.push(...movedFindings);
|
|
159
|
+
if (findings.length === 0)
|
|
160
|
+
return undefined;
|
|
161
|
+
return applyStageFixUp(ctx, desc, findings);
|
|
162
|
+
}
|
|
163
|
+
/** Decide whether this is a claim-staging run and, if so, read the backlog entries its diff moved
|
|
164
|
+
* out of their actionable section (plans/parallel-work-instances.md "Claims", part 4/7). Returns
|
|
165
|
+
* `[]` for every non-claim-staging run and on any unreadable file or git hiccup. */
|
|
166
|
+
async function stagedBacklogMoves(ctx) {
|
|
128
167
|
// Claims made at staging (plans/parallel-work-instances.md "Claims", part 4/7): an
|
|
129
168
|
// unassigned run of a MULTI-instance base whose diff moved a backlog entry out of its
|
|
130
169
|
// actionable section records the claim so a sibling is not assigned it while this change
|
|
@@ -139,6 +178,13 @@ async function runStageSelfCheck(ctx, desc) {
|
|
|
139
178
|
configuredInstances(ctx.config, ctx.role) > 1
|
|
140
179
|
? await stagedMovedEntries(ctx.wt, ctx.mainBranch, ctx.role).catch(() => [])
|
|
141
180
|
: [];
|
|
181
|
+
return moved;
|
|
182
|
+
}
|
|
183
|
+
/** Record the run's staged claim for the first claimable moved entry (when it has none yet) and
|
|
184
|
+
* return the moved-entry findings in the order the inline body pushed them: `assignedMoved`
|
|
185
|
+
* first, then `heldMoved`. */
|
|
186
|
+
function recordStagedClaims(ctx, moved) {
|
|
187
|
+
const s = ctx.state;
|
|
142
188
|
const heldBySiblings = new Set(s.claim === undefined ? (s.heldElsewhere ?? []).map(entryKey) : []);
|
|
143
189
|
const heldMoved = heldMovedFinding(heldBySiblings, moved);
|
|
144
190
|
const claimable = moved.filter((m) => !heldBySiblings.has(m.key));
|
|
@@ -157,45 +203,50 @@ async function runStageSelfCheck(ctx, desc) {
|
|
|
157
203
|
});
|
|
158
204
|
}
|
|
159
205
|
const assignedMoved = s.claim ? assignedMovedFinding(s.claim.key, moved) : undefined;
|
|
160
|
-
const findings =
|
|
206
|
+
const findings = [];
|
|
161
207
|
if (assignedMoved)
|
|
162
208
|
findings.push(assignedMoved);
|
|
163
209
|
if (heldMoved)
|
|
164
210
|
findings.push(heldMoved);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
211
|
+
return findings;
|
|
212
|
+
}
|
|
213
|
+
/** Run the author's one bounded fix-up turn over the stage findings, fold its commits, recover
|
|
214
|
+
* `desc.summary`/`desc.body` from the reply, and end the run `no_change` when the follow-up left
|
|
215
|
+
* the worktree clean; otherwise stage-check again and log the outcome warning. Returns an outcome
|
|
216
|
+
* only when it ends the run early, else `undefined`. */
|
|
217
|
+
async function applyStageFixUp(ctx, desc, findings) {
|
|
218
|
+
const before = findings.length;
|
|
219
|
+
const fixBase = await headOf(ctx.wt, "HEAD");
|
|
220
|
+
const fixUp = await ctx.requestStageFix(ctx.wt, findings, {
|
|
221
|
+
summary: desc.summaryDerived ? null : desc.summary,
|
|
222
|
+
body: desc.body ? formatCommitBody(desc.body) : "",
|
|
223
|
+
});
|
|
224
|
+
await foldAgentCommitsSince(ctx, ctx.wt, fixBase);
|
|
225
|
+
if (fixUp) {
|
|
226
|
+
if (fixUp.aborted)
|
|
174
227
|
return ctx.finishAbortedRun();
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return { result: "no_change" };
|
|
193
|
-
}
|
|
194
|
-
const remaining = await ctx.stageCheck(ctx.wt);
|
|
195
|
-
logSelfCheckOutcome(ctx, remaining.length === 0
|
|
196
|
-
? `stage self-check: ${plural(before, "finding")} — fixed by the follow-up turn`
|
|
197
|
-
: `stage self-check: ${plural(before, "finding")}; ${remaining.length} still open, queued for the gate`, findings, remaining);
|
|
228
|
+
// The fix-up reply answers the findings, not the change, so it never replaces the
|
|
229
|
+
// change's description (BUGS.md 2026-10-21): its SUMMARY is taken only over a subject
|
|
230
|
+
// derived from the paths, and its RISK/VERIFIED are appended as self-check notes.
|
|
231
|
+
const fixSummary = desc.summaryDerived ? extractSummary(fixUp.finalText) : null;
|
|
232
|
+
if (fixSummary !== null)
|
|
233
|
+
desc.summary = fixSummary;
|
|
234
|
+
desc.body = appendSelfCheckNotes(desc.body, extractCommitBody(fixUp.finalText));
|
|
235
|
+
}
|
|
236
|
+
// A fix-up turn may resolve its findings by reverting the whole change. With nothing left
|
|
237
|
+
// in the worktree there is nothing to commit or queue: end the run no_change here instead
|
|
238
|
+
// of handing a clean tree to commitAll. The worktree is left clean and no pin is taken.
|
|
239
|
+
// isDirty, not changedFiles: a `git status` that fails must throw, not read as clean — on
|
|
240
|
+
// 2026-10-21 a spawn failure here ended bugfix-2's 17-minute run no_change and released
|
|
241
|
+
// its edits unpinned; thrown, the run ends unsettled and its next run gets them back.
|
|
242
|
+
if (!(await isDirty(ctx.wt))) {
|
|
243
|
+
logSelfCheckOutcome(ctx, "stage self-check: the follow-up turn left no change; nothing to land", findings);
|
|
244
|
+
return { result: "no_change" };
|
|
198
245
|
}
|
|
246
|
+
const remaining = await ctx.stageCheck(ctx.wt);
|
|
247
|
+
logSelfCheckOutcome(ctx, remaining.length === 0
|
|
248
|
+
? `stage self-check: ${plural(before, "finding")} — fixed by the follow-up turn`
|
|
249
|
+
: `stage self-check: ${plural(before, "finding")}; ${remaining.length} still open, queued for the gate`, findings, remaining);
|
|
199
250
|
return undefined;
|
|
200
251
|
}
|
|
201
252
|
/** Compute the run's friction signal over this run's pre-commit authoring runs: minutes of wall
|
|
@@ -89,16 +89,22 @@ export async function runTimedRoleRun(signal, acquire, release, run, now = Date.
|
|
|
89
89
|
/** Sleep up to ms, but wake immediately when `signal` aborts — so shutdown (SIGTERM →
|
|
90
90
|
* abort) is prompt instead of waiting out the current poll cycle. The listener is removed
|
|
91
91
|
* on either exit path so long-running orchestrators don't accumulate one per poll, and an
|
|
92
|
-
* already-aborted signal returns synchronously through runOnAbort.
|
|
92
|
+
* already-aborted signal returns synchronously through runOnAbort. Without a signal it is a
|
|
93
|
+
* plain setTimeout sleep. The review gate's retry wait (src/review/review.ts, whose signal is
|
|
94
|
+
* optional) shares this helper rather than keeping a second copy. Exported as a unit-test
|
|
93
95
|
* seam, like runTimedRoleRun above. */
|
|
94
96
|
export function sleepInterruptible(ms, signal) {
|
|
95
97
|
return new Promise((resolve) => {
|
|
98
|
+
if (signal === undefined) {
|
|
99
|
+
setTimeout(resolve, ms);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
96
102
|
const onAbort = () => {
|
|
97
103
|
clearTimeout(timer);
|
|
98
104
|
resolve();
|
|
99
105
|
};
|
|
100
106
|
const timer = setTimeout(() => {
|
|
101
|
-
signal
|
|
107
|
+
signal?.removeEventListener("abort", onAbort);
|
|
102
108
|
resolve();
|
|
103
109
|
}, ms);
|
|
104
110
|
runOnAbort(signal, onAbort);
|
|
@@ -108,9 +108,24 @@ export class RunUsage {
|
|
|
108
108
|
* through foldMergeUsage: those never advance the run's pre-edit counter, which measures
|
|
109
109
|
* the authoring search. */
|
|
110
110
|
fold(s, run, authoring = true, provider, pair) {
|
|
111
|
+
this.foldLifetimeTotals(s, run);
|
|
112
|
+
this.foldCost(s, run, provider, pair);
|
|
113
|
+
this.foldAccount(run);
|
|
114
|
+
this.foldPromptTokens(run, authoring);
|
|
115
|
+
this.turns += run.turns;
|
|
116
|
+
this.foldEpisodes(run, provider);
|
|
117
|
+
}
|
|
118
|
+
/** Grow the role's lifetime totals on `s` from this run: its generated tokens and the real
|
|
119
|
+
* spend both accumulate through the healed counter, and the peak context is the new max. */
|
|
120
|
+
foldLifetimeTotals(s, run) {
|
|
111
121
|
s.generatedTokens = counter(s.generatedTokens) + run.outputTokens;
|
|
112
122
|
s.peakContextTokens = Math.max(counter(s.peakContextTokens), run.peakContextTokens);
|
|
113
123
|
s.totalCostUsd = counter(s.totalCostUsd) + run.costUsd;
|
|
124
|
+
}
|
|
125
|
+
/** Split this run's cost real from notional: a subscription run's cost is notional and never
|
|
126
|
+
* charges the daily cap; an API run's — or a run that ever saw overage — is real money.
|
|
127
|
+
* totalCostUsd still counts both: it is lifetime usage, not a budget figure. */
|
|
128
|
+
foldCost(s, run, provider, pair) {
|
|
114
129
|
// The billing split (plans/agent-backends.md §7): a subscription run's cost is notional
|
|
115
130
|
// and never charges the daily cap; an API run's — or a run that ever saw overage — is real
|
|
116
131
|
// money. totalCostUsd still counts both: it is lifetime usage, not a budget figure.
|
|
@@ -126,12 +141,21 @@ export class RunUsage {
|
|
|
126
141
|
this.notionalCostUsd += run.costUsd;
|
|
127
142
|
this.bumpProviderCost(provider, run.costUsd, 'notionalCostUsd');
|
|
128
143
|
}
|
|
144
|
+
}
|
|
145
|
+
/** Carry the newest account readings this run reported: the billing state, the subscription
|
|
146
|
+
* rate windows, and the agent they came from (whose name is only meaningful beside them). */
|
|
147
|
+
foldAccount(run) {
|
|
129
148
|
if (run.billing !== undefined)
|
|
130
149
|
this.billing = run.billing;
|
|
131
150
|
if (run.rateWindows !== undefined)
|
|
132
151
|
this.rateWindows = run.rateWindows;
|
|
133
152
|
if (run.billing !== undefined || run.rateWindows !== undefined)
|
|
134
153
|
this.agent = run.agent;
|
|
154
|
+
}
|
|
155
|
+
/** Advance the token counters: the prompt and cache-read shares count every run, while the
|
|
156
|
+
* pre-edit prefix freezes at the first edit — authoring-only, and skipped for a merge/review
|
|
157
|
+
* run. */
|
|
158
|
+
foldPromptTokens(run, authoring) {
|
|
135
159
|
this.promptTokens += run.promptTokens;
|
|
136
160
|
this.cacheReadTokens += run.cacheReadTokens;
|
|
137
161
|
// The pre-edit prefix freezes at the run's first edit: every authoring run while no edit
|
|
@@ -143,7 +167,13 @@ export class RunUsage {
|
|
|
143
167
|
if (run.firstEditTurn !== undefined)
|
|
144
168
|
this.editSeen = true;
|
|
145
169
|
}
|
|
146
|
-
|
|
170
|
+
}
|
|
171
|
+
/** Stamp the team-wide hold's failure observations for a run that ENDED on them: pi exits on
|
|
172
|
+
* a 429 or backend error, so "now" is when the provider refused. A run that merely logged one
|
|
173
|
+
* inside pi's own auto-retry and then finished would stamp a false observation, possibly hours
|
|
174
|
+
* late. The backend kind travels with its stamp (src/pi/pi-failure.ts classified the text at
|
|
175
|
+
* parse time); a 429 never ends up here — pi-stream checks rate-limit first. */
|
|
176
|
+
foldEpisodes(run, provider) {
|
|
147
177
|
// The team-wide hold's rate-limit input (lastRateLimit above), from the same
|
|
148
178
|
// once-per-run choke point. Only a run that ENDED on the 429 counts: pi exits on the error,
|
|
149
179
|
// so "now" is when the provider refused — a run that merely logged one inside pi's own
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { imageReroute } from "../agent/image-input.js";
|
|
1
2
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
2
3
|
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
3
4
|
import { isDirty, deleteRef } from "../git/git.js";
|
|
@@ -9,6 +10,7 @@ import { errorMessage } from "../text/text.js";
|
|
|
9
10
|
import { recordParity } from "./ci-parity.js";
|
|
10
11
|
import { resetWorktreeToMain } from "../git/worktree.js";
|
|
11
12
|
import { recordDismissedFiling, recordDismissedFlow } from "./run-dismiss.js";
|
|
13
|
+
import { readPiListedModels } from "../pi/pi-listed-models.js";
|
|
12
14
|
import { rejectedRefName } from "../paths.js";
|
|
13
15
|
import { logEventBestEffort } from "../events/events.js";
|
|
14
16
|
import { subscriptionWindowRejected } from "../agent/claude/claude-account.js";
|
|
@@ -23,6 +25,17 @@ function abortWithResume(s, ctx, pi, lastError, result, resumeCause) {
|
|
|
23
25
|
ctx.pending.requeueForResume(s, ctx.userPrompt);
|
|
24
26
|
return { result, resumeCause };
|
|
25
27
|
}
|
|
28
|
+
/** End a run as an unfulfilled error: record `lastError`, re-queue the raw prompt for a later
|
|
29
|
+
* fresh run (requeueUnfulfilled) and return the error outcome — the terminal the timeout and
|
|
30
|
+
* image-blind arms share, filed here so the "record lastError, re-queue unfulfilled, end as
|
|
31
|
+
* error" contract cannot drift between them (the resume-arming twin is abortWithResume above;
|
|
32
|
+
* failedRunArm's error path keeps its own split because it arms a resume for a dirty worktree
|
|
33
|
+
* and sets lastError before that choice). */
|
|
34
|
+
function unfulfilledError(s, ctx, message) {
|
|
35
|
+
s.lastError = message;
|
|
36
|
+
ctx.pending.requeueUnfulfilled(ctx.userPrompt);
|
|
37
|
+
return { result: "error" };
|
|
38
|
+
}
|
|
26
39
|
/** Classify what the finished run left behind; null means the run is fulfillable and staging
|
|
27
40
|
* should take over. Never throws: every branch returns an outcome, recording what went wrong
|
|
28
41
|
* in `state.lastError` or a warning event. */
|
|
@@ -134,11 +147,9 @@ function progressingTimeoutArm(ctx) {
|
|
|
134
147
|
function timeoutArm(ctx) {
|
|
135
148
|
const { pi, state: s } = ctx;
|
|
136
149
|
if (pi.timedOut) {
|
|
137
|
-
s.lastError = pi.errorMessage ?? "timed out";
|
|
138
150
|
// The request never ran to completion and no work merged: put it back so the next
|
|
139
151
|
// run retries it. (A killed run's half-done edits are discarded by the reset.)
|
|
140
|
-
|
|
141
|
-
return { result: "error" };
|
|
152
|
+
return unfulfilledError(s, ctx, pi.errorMessage ?? "timed out");
|
|
142
153
|
}
|
|
143
154
|
return undefined;
|
|
144
155
|
}
|
|
@@ -179,21 +190,63 @@ function budgetCappedArm(ctx) {
|
|
|
179
190
|
return undefined;
|
|
180
191
|
}
|
|
181
192
|
/** The image-blind decision: a run stopped the moment its agent read an image its model cannot
|
|
182
|
-
* see
|
|
183
|
-
*
|
|
184
|
-
* the run"). */
|
|
193
|
+
* see. One hop to an image-capable reroute target is taken when one exists; otherwise the
|
|
194
|
+
* run fails and the prompt re-queues (plans/image-input.md "Failing the run"). */
|
|
185
195
|
function imageBlindArm(ctx) {
|
|
186
196
|
const { pi, state: s } = ctx;
|
|
187
197
|
// A run the runner killed the moment its agent read an image its model cannot see
|
|
188
198
|
// (plans/image-input.md "Failing the run"): pi strips such an image from the request, so
|
|
189
199
|
// the agent was about to answer about a picture it never saw. The run was deliberately
|
|
190
200
|
// stopped before it could work on, so its half-done edits are discarded exactly as a
|
|
191
|
-
// timed-out run's are — nothing to resume, and the prompt re-queues to run fresh
|
|
192
|
-
// The error backoff and streak apply
|
|
201
|
+
// timed-out run's are — nothing to resume, and the prompt re-queues to run fresh on a
|
|
202
|
+
// model that CAN see (part 4 adds the reroute arm here). The error backoff and streak apply
|
|
203
|
+
// as for any error.
|
|
193
204
|
if (pi.imageBlind) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
205
|
+
// One hop only (plans/image-input.md "Rerunning on a model that can read images"): a
|
|
206
|
+
// run that ALREADY started on the image-capable target and still read blind is not
|
|
207
|
+
// rerouted a second time — it fails, so a misreadable image cannot bounce the role
|
|
208
|
+
// between tiers forever. Set at run start by runPhase when it routes a run onto the
|
|
209
|
+
// target (blind-read handoff or attachment preflight).
|
|
210
|
+
if (s.imageRerouteActive) {
|
|
211
|
+
// No work was done (the run was killed at the first read): nothing to resume, and the
|
|
212
|
+
// prompt re-queues to run fresh later like any other error. The error backoff/streak
|
|
213
|
+
// apply as usual.
|
|
214
|
+
return unfulfilledError(s, ctx, pi.errorMessage ?? `read ${pi.imageBlind.path}, but the model cannot read images`);
|
|
215
|
+
}
|
|
216
|
+
// Otherwise, when the role's tier has an image-capable reroute target, the failed work
|
|
217
|
+
// is not discarded: the next run starts directly on that target over the kept worktree.
|
|
218
|
+
// The one-shot field carries the target seam plus the `from`/`path` facts the handoff
|
|
219
|
+
// note needs; the run ends `aborted` with the `image-reroute` resume cause, and
|
|
220
|
+
// applyRunOutcome schedules it promptly (nextRunAt = now) while planRunStart hands the
|
|
221
|
+
// kept worktree off to a FRESH session on the target.
|
|
222
|
+
const regime = ctx.rerouteRegime ?? "primary";
|
|
223
|
+
const target = ctx.config !== undefined ? imageReroute(ctx.config, ctx.role, regime, undefined, null, null, readPiListedModels(ctx.root))
|
|
224
|
+
: null;
|
|
225
|
+
if (target) {
|
|
226
|
+
const from = ctx.runModel ?? target.config.model ?? "the model";
|
|
227
|
+
s.imageReroute = {
|
|
228
|
+
tier: target.tier,
|
|
229
|
+
backend: target.config.agent ?? "pi",
|
|
230
|
+
provider: target.config.provider,
|
|
231
|
+
model: target.config.model,
|
|
232
|
+
thinking: target.config.thinking,
|
|
233
|
+
from,
|
|
234
|
+
path: pi.imageBlind.path,
|
|
235
|
+
};
|
|
236
|
+
s.lastError = `read ${pi.imageBlind.path}, but ${from} cannot read images — rerouting to ${target.tier}`;
|
|
237
|
+
ctx.pending.requeueForResume(s, ctx.userPrompt);
|
|
238
|
+
logEventBestEffort(ctx.root, {
|
|
239
|
+
source: ctx.role,
|
|
240
|
+
type: "image_reroute",
|
|
241
|
+
run: s.runs,
|
|
242
|
+
from,
|
|
243
|
+
to: target.config.model,
|
|
244
|
+
tier: target.tier,
|
|
245
|
+
preflight: false,
|
|
246
|
+
});
|
|
247
|
+
return { result: "aborted", resumeCause: "image-reroute" };
|
|
248
|
+
}
|
|
249
|
+
return unfulfilledError(s, ctx, pi.errorMessage ?? `read ${pi.imageBlind.path}, but the model cannot read images`);
|
|
197
250
|
}
|
|
198
251
|
return undefined;
|
|
199
252
|
}
|
|
@@ -300,51 +353,67 @@ function noChangeArm(ctx, changed) {
|
|
|
300
353
|
// diagnoseNoChange (src/verdict/no-change.ts) tells which, so the warning event below is
|
|
301
354
|
// diagnosable on its own.
|
|
302
355
|
const diagnosis = diagnoseNoChange(pi);
|
|
303
|
-
|
|
304
|
-
ctx.warn(`the agent finished without changes and without declaring nothing-to-do` +
|
|
305
|
-
(diagnosis.notes.length ? ` (${diagnosis.notes.join(", ")})` : ""));
|
|
306
|
-
}
|
|
356
|
+
warnUndeclaredNoChange(ctx, pi, diagnosis.notes);
|
|
307
357
|
// A cut-off run did real work and was NOT fulfilled: a director prompt goes back
|
|
308
358
|
// to the inbox to rerun fresh; a role runner resumes the just-compacted session
|
|
309
359
|
// next run (see the cutOff handling in run()).
|
|
310
360
|
if (diagnosis.cutOff)
|
|
311
361
|
ctx.pending.requeueForResume(s, ctx.userPrompt);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
362
|
+
recordNoChangeBookkeeping(ctx, pi, diagnosis.cutOff);
|
|
363
|
+
return {
|
|
364
|
+
result: "no_change",
|
|
365
|
+
cutOff: diagnosis.cutOff || undefined,
|
|
366
|
+
summary: noChangeSummary(ctx, pi, diagnosis.cutOff),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
/** Warn when a no-change run declared neither a sentinel nor nothing-to-do, so the non-
|
|
372
|
+
* compliance or truncation the diagnosis saw is diagnosable on its own. */
|
|
373
|
+
function warnUndeclaredNoChange(ctx, pi, notes) {
|
|
374
|
+
if (!pi.nothingToDo) {
|
|
375
|
+
ctx.warn(`the agent finished without changes and without declaring nothing-to-do` +
|
|
376
|
+
(notes.length ? ` (${notes.join(", ")})` : ""));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/** A no-change run's bookkeeping: the qa flow, the filed feedback ledger entry and the ci
|
|
380
|
+
* parity coverage, each recorded only when its observer saw it and the run was not cut off. */
|
|
381
|
+
function recordNoChangeBookkeeping(ctx, pi, cutOff) {
|
|
382
|
+
// A cut-off run did real work but was truncated before declaring its outcome: the FLOW
|
|
383
|
+
// line it left mid-stream is not a finished verdict, so recording it would advance the
|
|
384
|
+
// rotation past a check that did not complete. Only a run that was not cut off records.
|
|
385
|
+
if (ctx.flow && !cutOff) {
|
|
386
|
+
// The ledger only orders the qa menu, so a failed write warns rather than failing the run.
|
|
387
|
+
recordDismissedFlow(ctx, ctx.root, ctx.flow.flow, ctx.flow.result, ctx.flow.result === "bug" ? extractSummary(pi.finalText) ?? undefined : undefined);
|
|
388
|
+
}
|
|
389
|
+
// A fulfilled feedback run that filed records its per-install ledger entry and logs the
|
|
390
|
+
// issue_filed event so the filing is visible in the operator's activity feed. Recording is
|
|
391
|
+
// bookkeeping that never fails the run: a failed ledger write warns and the no_change
|
|
392
|
+
// outcome stands. A cut-off run records nothing — truncation is not a filing the run
|
|
393
|
+
// declared (the same guard the FLOW line carries).
|
|
394
|
+
if (ctx.filed && !cutOff) {
|
|
395
|
+
recordDismissedFiling(ctx, ctx.root, ctx.filed);
|
|
396
|
+
}
|
|
397
|
+
// A fulfilled ci run that declared its coverage records it in the parity ledger so the
|
|
398
|
+
// next fresh session's block knows what the last run covered (part 3/3). The ledger is
|
|
399
|
+
// bookkeeping that never fails the run: a failed write warns and the no_change outcome
|
|
400
|
+
// stands. A cut-off run records nothing — truncation is not a coverage the run declared.
|
|
401
|
+
if (ctx.ci && !cutOff) {
|
|
402
|
+
try {
|
|
403
|
+
recordParity(ctx.root, ctx.ci.sha, ctx.ci.result);
|
|
326
404
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
// bookkeeping that never fails the run: a failed write warns and the no_change outcome
|
|
330
|
-
// stands. A cut-off run records nothing — truncation is not a coverage the run declared.
|
|
331
|
-
if (ctx.ci && !diagnosis.cutOff) {
|
|
332
|
-
try {
|
|
333
|
-
recordParity(ctx.root, ctx.ci.sha, ctx.ci.result);
|
|
334
|
-
}
|
|
335
|
-
catch (err) {
|
|
336
|
-
ctx.warn(`ci parity not recorded: ${errorMessage(err)}`);
|
|
337
|
-
}
|
|
405
|
+
catch (err) {
|
|
406
|
+
ctx.warn(`ci parity not recorded: ${errorMessage(err)}`);
|
|
338
407
|
}
|
|
339
|
-
// A prompt-driven (director) run that answers a question in prose ends `no_change` with
|
|
340
|
-
// no SUMMARY block, so its reply would be lost — the dashboard reads a bare "No change"
|
|
341
|
-
// and the feed carries nothing (BUGS.md 2026-10-14). Keep the prose above the sentinel as
|
|
342
|
-
// the run's summary so `lastSummary` and the run_end event show the answer where the
|
|
343
|
-
// operator asked the question. A role runner's bare nothing-to-do stays summary-less.
|
|
344
|
-
const summary = ctx.userPrompt !== null && !diagnosis.cutOff
|
|
345
|
-
? (extractNoChangeAnswer(pi.finalText) ?? undefined)
|
|
346
|
-
: undefined;
|
|
347
|
-
return { result: "no_change", cutOff: diagnosis.cutOff || undefined, summary };
|
|
348
408
|
}
|
|
349
|
-
|
|
409
|
+
}
|
|
410
|
+
/** A prompt-driven (director) run that answers a question in prose ends `no_change` with
|
|
411
|
+
* no SUMMARY block, so its reply would be lost — the dashboard reads a bare "No change"
|
|
412
|
+
* and the feed carries nothing (BUGS.md 2026-10-14). Keep the prose above the sentinel as
|
|
413
|
+
* the run's summary so `lastSummary` and the run_end event show the answer where the
|
|
414
|
+
* operator asked the question. A role runner's bare nothing-to-do stays summary-less. */
|
|
415
|
+
function noChangeSummary(ctx, pi, cutOff) {
|
|
416
|
+
return ctx.userPrompt !== null && !cutOff
|
|
417
|
+
? (extractNoChangeAnswer(pi.finalText) ?? undefined)
|
|
418
|
+
: undefined;
|
|
350
419
|
}
|