tumwater 0.1.2 → 0.2.0
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 +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -17,27 +17,28 @@ const CLAIM_RELEASING = new Set([
|
|
|
17
17
|
* happens AFTER the pi run, extracted from LoopRunner.tick (src/loop/loop.ts) so the runner keeps
|
|
18
18
|
* only tick lifecycle (worktree, sessions, abort) and this module owns the per-result
|
|
19
19
|
* bookkeeping policy. Order matters here and is pinned by behavior, not by a test seam: the
|
|
20
|
-
* outcome's folds
|
|
21
|
-
* read happens while `running` is still true (a poll
|
|
22
|
-
* the loop), and the warnings read the streaks exactly once per episode, after the
|
|
20
|
+
* outcome's folds are applied to state before applyTickOutcome schedules the next run, the
|
|
21
|
+
* main-head read happens while `running` is still true (a poll that runs in that window must not
|
|
22
|
+
* double-wake the loop), and the warnings read the streaks exactly once per episode, after the
|
|
23
|
+
* schedule. */
|
|
23
24
|
export async function finalizeTick(deps) {
|
|
24
25
|
const { root, role, mainBranch, config, state: s, usage, tick } = deps;
|
|
25
26
|
let { outcome } = deps;
|
|
26
|
-
// A re-queued leftover's
|
|
27
|
+
// A re-queued leftover's merge failure is not the tick's own result, so it rides the
|
|
27
28
|
// outcome separately: applyTickOutcome feeds it into the error streak, and the warning
|
|
28
29
|
// below names it — even though runTick cleared `lastError` so it never latched onto
|
|
29
|
-
// `
|
|
30
|
+
// `run_end` (BUGS.md 2026-09-21).
|
|
30
31
|
if (deps.recoveryFailure !== undefined)
|
|
31
32
|
outcome.recoveryFailure = deps.recoveryFailure;
|
|
32
33
|
// A main_red tick's cause is written for that result (src/baseline/main-red.ts), not a leftover
|
|
33
|
-
//
|
|
34
|
+
// merge failure: fold it onto the state here so `run_end` logs it like every other
|
|
34
35
|
// failure's and the digest can itemize what the Outcome table's main_red cells mean
|
|
35
36
|
// (BUGS.md 2026-09-28). runTick cleared `lastError` at its start, so nothing foreign
|
|
36
37
|
// can precede this.
|
|
37
38
|
if (outcome.error !== undefined)
|
|
38
39
|
s.lastError = outcome.error;
|
|
39
40
|
// Read main's current head while this tick is still reserved (running=true): the
|
|
40
|
-
// applyTickOutcome below clears running, and a poll
|
|
41
|
+
// applyTickOutcome below clears running, and a poll that runs between that clear and a later
|
|
41
42
|
// head update would see a stale lastMainHead and wake the loop again on the very move
|
|
42
43
|
// that triggered this tick — a duplicate wake event plus an extra tick for one world change.
|
|
43
44
|
// branchHead reads the ref files first (microsecond-scale, like the orchestrator's per-poll
|
|
@@ -57,22 +58,22 @@ export async function finalizeTick(deps) {
|
|
|
57
58
|
!hasOutstandingWork(s)) {
|
|
58
59
|
const title = s.claim.title;
|
|
59
60
|
s.claim = undefined;
|
|
60
|
-
logEventBestEffort(root, {
|
|
61
|
+
logEventBestEffort(root, { source: role, type: "claim", action: "released", title, reason: outcome.result });
|
|
61
62
|
}
|
|
62
63
|
// A wake consumed while this tick ran stamped the shared state in place, but the outcome
|
|
63
|
-
// schedule above overwrites it (
|
|
64
|
+
// schedule above overwrites it (lastRunEndedAt past wokenAt, nextRunAt a fresh gap or
|
|
64
65
|
// backoff out), so a plain `tumwater wake --role` with an empty queue would silently wait
|
|
65
66
|
// out the whole interval (BUGS.md 2026-09-25). Re-apply it here — exactly like a wake
|
|
66
67
|
// arriving one poll after the tick ended.
|
|
67
68
|
restoreMidTickWake(s);
|
|
68
69
|
saveLoopState(root, s);
|
|
69
70
|
// One warning per error episode (BUGS.md 2026-09-15: every loop failing identically
|
|
70
|
-
// looked like a quiet
|
|
71
|
-
// outcomes, preserves it across a plain `queued` tick (the tick's
|
|
71
|
+
// looked like a quiet team). applyTickOutcome increments the streak on error-class
|
|
72
|
+
// outcomes, preserves it across a plain `queued` tick (the tick's merge resolves it),
|
|
72
73
|
// and resets it on any other completed result — so warn only when THIS outcome incremented
|
|
73
74
|
// the streak up to the threshold: a preserved streak already sitting at the bar must not
|
|
74
|
-
// re-warn on the queued tick's end. A rejection's crossing warns from the
|
|
75
|
-
// instead (
|
|
75
|
+
// re-warn on the queued tick's end. A rejection's crossing warns from the merge side
|
|
76
|
+
// instead (writeMergeOutcome, src/merge/merge-slot.ts), which is where rejections resolve.
|
|
76
77
|
if ((outcome.result === "error" || outcome.recoveryFailure !== undefined) &&
|
|
77
78
|
(s.consecutiveErrors ?? 0) === ERROR_STREAK_WARN) {
|
|
78
79
|
warnEventBestEffort(root, role, consecutiveFailuresWarning(s.consecutiveErrors ?? 0, s.lastError ?? outcome.recoveryFailure ?? "unknown error"));
|
|
@@ -82,21 +83,21 @@ export async function finalizeTick(deps) {
|
|
|
82
83
|
// streak on each kill and resets it on any other result, so the crossing fires once;
|
|
83
84
|
// the give-up (fresh session + backoff) follows on the next kill.
|
|
84
85
|
if ((s.quietKillStreak ?? 0) === QUIET_KILL_RESUME_LIMIT) {
|
|
85
|
-
warnEventBestEffort(root, role, `${s.quietKillStreak} consecutive
|
|
86
|
+
warnEventBestEffort(root, role, `${s.quietKillStreak} consecutive stalls (no progress): ${s.lastError ?? "unknown hang"}`);
|
|
86
87
|
}
|
|
87
88
|
// Per-tick usage: the event feed is where operators see
|
|
88
|
-
// spend — this tick's tokens and cost ride on
|
|
89
|
+
// spend — this tick's tokens and cost ride on run_end so a budget pause or a money-burning
|
|
89
90
|
// no_change/error/rejected/refused tick shows what it spent without diffing status-table
|
|
90
91
|
// snapshots. Both fields are per-tick windows (reset at tick start, folded in over every
|
|
91
92
|
// pi run of the tick); they ride on HarnessEvent's index signature like other payloads and
|
|
92
93
|
// are omitted when zero so skipped ticks render byte-identical to a pre-feature line.
|
|
93
|
-
// The tick's wall-clock span rides on
|
|
94
|
+
// The tick's wall-clock span rides on run_end too: the
|
|
94
95
|
// failure digest prices a tick by its own durationMs first (src/failure/failure-data.ts
|
|
95
96
|
// tickDurationMs), so the field is ALWAYS present — a tick that ends within the same
|
|
96
97
|
// Date.now() millisecond it started (an instant early-abort error tick) or crosses a
|
|
97
98
|
// backward clock step still attests its 0 ms, where the old omit-when-zero convention
|
|
98
|
-
// dropped the field and broke the "every
|
|
99
|
-
// 2026-09-29). Unlike the usage fields,
|
|
99
|
+
// dropped the field and broke the "every run_end carries a span" contract (BUGS.md
|
|
100
|
+
// 2026-09-29). Unlike the usage fields, run_end's renderer never prints the span, so an
|
|
100
101
|
// always-present 0 changes no rendered line.
|
|
101
102
|
const tickDurationMs = Math.max(0, Date.now() - deps.tickStartedAt);
|
|
102
103
|
// The account state (plans/agent-backends.md part 6a): fold this tick's billing answer and
|
|
@@ -111,22 +112,28 @@ export async function finalizeTick(deps) {
|
|
|
111
112
|
// The next reporting run rewrites it; nothing this tick depends on is lost.
|
|
112
113
|
}
|
|
113
114
|
}
|
|
115
|
+
// The per-provider split of this tick's usage (plans: per-provider spend in the day
|
|
116
|
+
// report): a Record<provider, {costUsd, notionalCostUsd}> for the report's `Cost by
|
|
117
|
+
// provider` line. Omitted when empty like the prompt-token fields, so a tick whose runs
|
|
118
|
+
// carried no provider renders byte-identically to today.
|
|
119
|
+
const providerCost = usage.serializeProviderCost();
|
|
114
120
|
logEventBestEffort(root, {
|
|
115
|
-
|
|
116
|
-
type: "
|
|
117
|
-
tick,
|
|
121
|
+
source: role,
|
|
122
|
+
type: "run_end",
|
|
123
|
+
run: tick,
|
|
118
124
|
result: outcome.result,
|
|
119
125
|
summary: outcome.summary,
|
|
120
126
|
error: s.lastError,
|
|
121
127
|
durationMs: tickDurationMs,
|
|
122
128
|
...usageFragment(s.generatedTokens, usage.costUsd, usage.notionalCostUsd),
|
|
129
|
+
...(Object.keys(providerCost).length > 0 ? { providerCost } : {}),
|
|
123
130
|
// The billing answer the tick's runs reported, so the report can tell notional spend from
|
|
124
131
|
// real spend without re-deriving it from the file (plans/agent-backends.md part 6a).
|
|
125
132
|
...(usage.billing !== undefined ? { billing: usage.billing } : {}),
|
|
126
133
|
// Per-tick prompt-token telemetry: how many prompt
|
|
127
134
|
// tokens the tick sent, how many were cache reads, and how many went out before its first
|
|
128
135
|
// edit. Omitted when zero like usageFragment's fields, so a tick that sent no prompt — and
|
|
129
|
-
// every
|
|
136
|
+
// every run_end written by an older build — renders byte-identically.
|
|
130
137
|
...(usage.promptTokens > 0 ? { promptTokens: usage.promptTokens } : {}),
|
|
131
138
|
...(usage.cacheReadTokens > 0 ? { cacheReadTokens: usage.cacheReadTokens } : {}),
|
|
132
139
|
...(usage.preEditPromptTokens > 0 ? { preEditPromptTokens: usage.preEditPromptTokens } : {}),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/** What a finished tick or merge is (the TickResult/TickOutcome vocabulary), shared by the
|
|
2
3
|
* runner that produces it, the dashboards that render it, and the state machine that applies
|
|
3
4
|
* it. The application policy — which outcomes count which streaks, when a session resumes,
|
|
4
5
|
* how the next run is scheduled — lives next door in tick-apply.ts; the clock arithmetic it
|
|
@@ -5,26 +5,45 @@ import { configForRole } from "../config/config-views.js";
|
|
|
5
5
|
import { NOTHING_TO_DO } from "../verdict/reply-contract.js";
|
|
6
6
|
import { dequeuePromptEntry, dequeueRolePromptEntry, peekPrompt, peekRolePrompt } from "../inbox/inbox.js";
|
|
7
7
|
import { stripNotBeforeMarker } from "../inbox/prompt-not-before.js";
|
|
8
|
-
import { readBrief } from "../brief.js";
|
|
8
|
+
import { readBrief } from "../brief/brief.js";
|
|
9
9
|
import { buildDirectorPrompt, buildTickPrompt } from "../prompt/prompt.js";
|
|
10
10
|
import { readPrinciples } from "../prompt/principles.js";
|
|
11
11
|
import { buildCutOffNote } from "../prompt/prompt-followup.js";
|
|
12
|
-
import { buildAssignmentNote, buildConflictDiscardNote, buildHeldEntriesNote, buildRejectedReviewNote } from "../prompt/gate-prompts.js";
|
|
13
|
-
import { eligibleEntries } from "../backlog/backlog-eligibility.js";
|
|
12
|
+
import { buildAssignmentNote, buildConflictDiscardNote, buildHeldEntriesNote, buildMergedPrerequisitesNote, buildRejectedReviewNote, buildWaitingPartsNote } from "../prompt/gate-prompts.js";
|
|
13
|
+
import { eligibleEntries, mergedPrerequisiteRanges, waitingPartRanges } from "../backlog/backlog-eligibility.js";
|
|
14
14
|
import { detectBuildCheck } from "../build/build-check-detect.js";
|
|
15
|
+
import { flakeCounts, KNOWN_FLAKE_WINDOW_MS } from "../review/known-flakes.js";
|
|
16
|
+
import { readEventsSince } from "../events/event-window.js";
|
|
15
17
|
import { telemetryDigest } from "./telemetry-digest.js";
|
|
16
18
|
import { readQaCoverage, renderCoverageBlock } from "./qa-coverage.js";
|
|
17
19
|
import { roleNotesPath } from "../paths.js";
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
import { pathTokens } from "./stage-check.js";
|
|
22
|
+
import { readTextOrNull, statOrNull } from "../files/files.js";
|
|
23
|
+
import { dayKey } from "../text/datetime.js";
|
|
24
|
+
import { appendPromptNote } from "../text/format.js";
|
|
25
|
+
import { ROLE_NOTES_MAX_BYTES } from "../agent/role-notes-write.js";
|
|
20
26
|
import { truncateWithNote } from "../text/text.js";
|
|
21
27
|
import { renderBacklogIndexBlock, renderBacklogStructureBlock } from "../backlog/backlog-structure.js";
|
|
28
|
+
/** The repo paths the entry at `file` lines `range` names that are absent from the checkout at
|
|
29
|
+
* `root`: tokens whose first segment is a directory there but which do not exist themselves —
|
|
30
|
+
* the self-check's top-level-directory rule, read from the working tree. */
|
|
31
|
+
function missingEntryPaths(root, file, range) {
|
|
32
|
+
const text = readTextOrNull(path.join(root, file));
|
|
33
|
+
if (text === null)
|
|
34
|
+
return [];
|
|
35
|
+
const entry = text.split("\n").slice(range.start - 1, range.end).join("\n");
|
|
36
|
+
return pathTokens(entry).filter((p) => {
|
|
37
|
+
const top = p.slice(0, p.indexOf("/"));
|
|
38
|
+
return statOrNull(path.join(root, top))?.isDirectory() === true && statOrNull(path.join(root, p)) === null;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
22
41
|
/** Read a role's notebook through files.ts's readTextOrNull — the one read-and-swallow
|
|
23
42
|
* step — or undefined when it is missing or unreadable (an empty or whitespace-only note is
|
|
24
43
|
* left for buildTickPrompt to omit). The degradation is deliberate: a torn or absent
|
|
25
44
|
* runtime-state file must never fail a tick.
|
|
26
45
|
*
|
|
27
|
-
* The note is bounded on read as well as on write: the
|
|
46
|
+
* The note is bounded on read as well as on write: the write path rejects text over
|
|
28
47
|
* ROLE_NOTES_MAX_BYTES, but the file lives in hand-editable runtime state, so an oversized
|
|
29
48
|
* note (an old build, a manual edit) would otherwise ride into every one of this role's
|
|
30
49
|
* prefill-heavy tick prompts uncapped. The same defensive backstop readPrinciples and the
|
|
@@ -34,7 +53,30 @@ import { renderBacklogIndexBlock, renderBacklogStructureBlock } from "../backlog
|
|
|
34
53
|
* marker naming the loss. */
|
|
35
54
|
function readRoleNote(root, role) {
|
|
36
55
|
const text = readTextOrNull(roleNotesPath(root, role));
|
|
37
|
-
return text === null ? undefined : truncateWithNote(text, ROLE_NOTES_MAX_BYTES, "role
|
|
56
|
+
return text === null ? undefined : truncateWithNote(text, ROLE_NOTES_MAX_BYTES, "role notes");
|
|
57
|
+
}
|
|
58
|
+
/** The bugfix run's tally of checks the merge gate excused as flaky in the last day, or
|
|
59
|
+
* undefined when none (or the log is unreadable — a missing tally never fails a run). */
|
|
60
|
+
function flakyChecksNote(root) {
|
|
61
|
+
let counts;
|
|
62
|
+
try {
|
|
63
|
+
counts = flakeCounts(readEventsSince(root, KNOWN_FLAKE_WINDOW_MS).events);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
if (counts.length === 0)
|
|
69
|
+
return undefined;
|
|
70
|
+
const shown = counts
|
|
71
|
+
.slice(0, 5)
|
|
72
|
+
.map(([key, n]) => `"${key}" ×${n}`)
|
|
73
|
+
.join("; ");
|
|
74
|
+
const more = counts.length > 5 ? ` (and ${counts.length - 5} more)` : "";
|
|
75
|
+
return (`Flaky checks: in the last 24h the merge gate excused these check failures as flaky (each ` +
|
|
76
|
+
`failed, then passed on its one re-run), most frequent first: ${shown}${more}. An excused ` +
|
|
77
|
+
`flake blocks no merge, so it stays unfixed until it has a BUGS.md entry. When an open bug ` +
|
|
78
|
+
`names one of these, that bug is still firing. When \`## Open\` holds no entries, a check ` +
|
|
79
|
+
`excused 2 or more times is a confirmed latent bug: take it as this run's latent-bug candidate.`);
|
|
38
80
|
}
|
|
39
81
|
/** Assemble the prompt a loop's next tick runs on — the whole "what should this tick see"
|
|
40
82
|
* concern, split out of loop.ts so the runner owns only lifecycle and the prompt owns only
|
|
@@ -43,7 +85,7 @@ function readRoleNote(root, role) {
|
|
|
43
85
|
* queued by `tumwater prompt --role <id>` — back so the runner can record it as pending and
|
|
44
86
|
* re-queue it if the tick leaves it unfulfilled; `userPromptFile` is the queue file it came
|
|
45
87
|
* from, so that bookkeeping can clean the prompt's attachments by that file's stem. */
|
|
46
|
-
export function assembleTickPrompt({ root, config, role, state, preview = false, agent }) {
|
|
88
|
+
export function assembleTickPrompt({ root, config, role, state, preview = false, agent, compact = false }) {
|
|
47
89
|
// One read of the brief yields both the prompt it carries and the owning file's name
|
|
48
90
|
// (brief.ts's readBrief): readInitialPrompt + briefFile were two full reads of the same
|
|
49
91
|
// file on this per-tick path.
|
|
@@ -51,7 +93,7 @@ export function assembleTickPrompt({ root, config, role, state, preview = false,
|
|
|
51
93
|
const initialPrompt = briefState?.prompt ?? "";
|
|
52
94
|
// The brief's owning file (TUMWATER.md first, README.md as the compatibility path —
|
|
53
95
|
// plans/portability.md §7a/7), named in both prompt builders' rules instead of a hardcoded
|
|
54
|
-
// README.md. "README.md" is the fallback for a repo with no marked file yet — the
|
|
96
|
+
// README.md. "README.md" is the fallback for a repo with no marked file yet — the team
|
|
55
97
|
// runs blind on prompts either way, so the name in the rules should still point somewhere.
|
|
56
98
|
const brief = briefState?.file ?? "README.md";
|
|
57
99
|
// The project's design principles ride along in every prompt — tick and director alike — so
|
|
@@ -79,24 +121,26 @@ export function assembleTickPrompt({ root, config, role, state, preview = false,
|
|
|
79
121
|
return null;
|
|
80
122
|
userPrompt = stripNotBeforeMarker(dequeued);
|
|
81
123
|
userPromptFile = entry?.file ?? null;
|
|
82
|
-
|
|
124
|
+
// The compact prompt leaves the brief block out: the director reads the brief file in full,
|
|
125
|
+
// and that file carries the same text.
|
|
126
|
+
prompt = buildDirectorPrompt(userPrompt, compact ? "" : initialPrompt, principles, check, brief, undefined, backlogIndex, agent ?? configForRole(config, DIRECTOR_ROLE).agent, compact, config.roles[DIRECTOR_ROLE]?.instructions);
|
|
83
127
|
}
|
|
84
128
|
else {
|
|
85
129
|
// The loop id keys its inbox and notebook; every lookup ABOUT the role — catalog charter,
|
|
86
|
-
// instructions, the qa/
|
|
130
|
+
// instructions, the qa/selfcheck/clean blocks — resolves through the base role, so
|
|
87
131
|
// `feature-2` runs the feature charter and `roles.feature.instructions` while keeping its
|
|
88
132
|
// own queue and notes (plans/parallel-work-instances.md).
|
|
89
133
|
const base = baseRoleOf(role);
|
|
90
134
|
// Catalog first, then user-defined loops (plans/user-defined-loops.md): a custom's task
|
|
91
135
|
// is its entire find-something-to-do text and the title identifies it in the prompt.
|
|
92
|
-
const custom = config.
|
|
136
|
+
const custom = config.customRoles.find((c) => c.name === role);
|
|
93
137
|
const resolved = roleById(role) ?? (custom ? customRole(custom.name, custom.task) : undefined);
|
|
94
138
|
if (!resolved) {
|
|
95
139
|
// The shared unknownRoleMessage (parseRoleFlag, the operator commands) names the valid
|
|
96
140
|
// ids; this defensive path — a runner asked to tick a role no catalog entry or
|
|
97
|
-
//
|
|
141
|
+
// customRoles task can answer — says the same, so if it ever fires it reads as the
|
|
98
142
|
// harness bug it is instead of a bare dead end.
|
|
99
|
-
const validIds = [...allRoleIds(), ...config.
|
|
143
|
+
const validIds = [...allRoleIds(), ...config.customRoles.map((c) => c.name)];
|
|
100
144
|
throw new Error(unknownRoleMessage(role, validIds));
|
|
101
145
|
}
|
|
102
146
|
// A queued per-role prompt is dequeued here, before the prompt is built, so its text rides
|
|
@@ -109,9 +153,9 @@ export function assembleTickPrompt({ root, config, role, state, preview = false,
|
|
|
109
153
|
const entry = preview ? null : dequeueRolePromptEntry(root, role);
|
|
110
154
|
const dequeued = preview ? peekRolePrompt(root, role) : entry?.text ?? null;
|
|
111
155
|
const request = dequeued === null ? null : stripNotBeforeMarker(dequeued);
|
|
112
|
-
// The
|
|
156
|
+
// The selfcheck role's evidence is the harness's own event log, one level outside this
|
|
113
157
|
// worktree, so its evidence module renders it (telemetryDigest) and the tick injects it.
|
|
114
|
-
const digest = base === "
|
|
158
|
+
const digest = base === "selfcheck" ? telemetryDigest(root) : undefined;
|
|
115
159
|
// The `qa` observer's flow rotation needs a memory of what it last exercised; every tick
|
|
116
160
|
// is a fresh session, and a passing cheap check leaves nothing in the repo. The ledger is
|
|
117
161
|
// runtime state, and a missing or unreadable one degrades to no block
|
|
@@ -134,31 +178,47 @@ export function assembleTickPrompt({ root, config, role, state, preview = false,
|
|
|
134
178
|
// instruction in buildTickPrompt is present either way. The director branch above never
|
|
135
179
|
// reads this path, so it carries no notebook.
|
|
136
180
|
const notes = readRoleNote(root, role);
|
|
181
|
+
// The note file's mtime is when the notes were last saved (writes replace the file), so the
|
|
182
|
+
// run can weigh how stale they are.
|
|
183
|
+
const savedStat = notes === undefined ? null : statOrNull(roleNotesPath(root, role));
|
|
184
|
+
const notesSavedDay = savedStat ? dayKey(savedStat.mtimeMs) : undefined;
|
|
137
185
|
// The plan charter's stop threshold is derived from config (plans/parallel-work-
|
|
138
186
|
// instances.md "…keeping the plan loop ahead", part 5c/7). The catalog text stays a
|
|
139
187
|
// config-free description of the rule; this note supplies the concrete eligible-plan count
|
|
140
188
|
// so the plan loop stops only once every configured feature instance has a plan waiting.
|
|
141
189
|
const planTargetNote = base === "plan"
|
|
142
|
-
? `This
|
|
143
|
-
`${planBacklogTarget(config)} or more eligible plans, end with ${NOTHING_TO_DO}
|
|
190
|
+
? `This run's stop target: when PLANS.md \`## Planned\` already holds ` +
|
|
191
|
+
`${planBacklogTarget(config)} or more eligible plans, end with ${NOTHING_TO_DO}. ` +
|
|
192
|
+
`It holds ${eligibleEntries(root, "feature").length} now (tumwater's count of entries ` +
|
|
193
|
+
`feature could take, the same count that decides whether a plan run is deferred).`
|
|
144
194
|
: undefined;
|
|
195
|
+
const flakyChecks = base === "bugfix" ? flakyChecksNote(root) : undefined;
|
|
196
|
+
// A run holding a claim with no queued request is assigned its one entry (the assignment
|
|
197
|
+
// note below). In the compact prompt such a run reads that entry first, so the backlog index
|
|
198
|
+
// — what an unassigned run chooses its task from — stays out. The brief block stays out of
|
|
199
|
+
// every compact prompt: an unassigned run reads the brief file in full, which carries the
|
|
200
|
+
// same text, and an assigned one reads it only when its entry needs it.
|
|
201
|
+
const variant = compact ? { assigned: state.claim !== undefined && request === null } : undefined;
|
|
145
202
|
prompt = buildTickPrompt({
|
|
146
203
|
role: resolved,
|
|
147
|
-
initialPrompt,
|
|
204
|
+
initialPrompt: compact ? "" : initialPrompt,
|
|
148
205
|
principles,
|
|
149
206
|
digest,
|
|
150
207
|
coverage,
|
|
151
208
|
backlogStructure,
|
|
152
|
-
backlogIndex,
|
|
209
|
+
backlogIndex: variant?.assigned ? undefined : backlogIndex,
|
|
153
210
|
notes,
|
|
211
|
+
notesSavedDay,
|
|
154
212
|
planTargetNote,
|
|
213
|
+
flakyChecks,
|
|
155
214
|
extraInstructions: config.roles[base]?.instructions,
|
|
156
215
|
check,
|
|
157
216
|
briefFile: brief,
|
|
158
217
|
agent: agent ?? configForRole(config, role).agent,
|
|
218
|
+
compact: variant,
|
|
159
219
|
// A per-role prompt queued by `tumwater prompt --role <id>` rides as an explicit user
|
|
160
220
|
// request block; its text is also the tick's userPrompt,
|
|
161
|
-
// so the runner's pending-prompt machinery (re-queue on unfulfilled, clear on
|
|
221
|
+
// so the runner's pending-prompt machinery (re-queue on unfulfilled, clear on merge)
|
|
162
222
|
// treats it exactly like the director's dequeued request.
|
|
163
223
|
userRequest: request ?? undefined,
|
|
164
224
|
});
|
|
@@ -175,27 +235,35 @@ export function assembleTickPrompt({ root, config, role, state, preview = false,
|
|
|
175
235
|
const claim = role === DIRECTOR_ROLE ? undefined : state.claim;
|
|
176
236
|
if (claim && userPrompt === null) {
|
|
177
237
|
const range = eligibleEntries(root, role).find((e) => e.key === claim.key);
|
|
178
|
-
prompt
|
|
238
|
+
prompt = appendPromptNote(prompt, buildAssignmentNote(claim, range, range ? missingEntryPaths(root, claim.file, range) : []));
|
|
239
|
+
const merged = claim.file === "PLANS.md" ? mergedPrerequisiteRanges(root, claim.title) : [];
|
|
240
|
+
if (merged.length > 0)
|
|
241
|
+
prompt = appendPromptNote(prompt, buildMergedPrerequisitesNote(merged, compact));
|
|
242
|
+
}
|
|
243
|
+
if (claim && userPrompt === null && claim.file === "PLANS.md") {
|
|
244
|
+
const waiting = waitingPartRanges(root, claim.title);
|
|
245
|
+
if (waiting.length > 0)
|
|
246
|
+
prompt = appendPromptNote(prompt, buildWaitingPartsNote(waiting, compact));
|
|
179
247
|
}
|
|
180
248
|
if (!claim && userPrompt === null && role !== DIRECTOR_ROLE && state.heldElsewhere?.length) {
|
|
181
|
-
prompt
|
|
249
|
+
prompt = appendPromptNote(prompt, buildHeldEntriesNote(state.heldElsewhere));
|
|
182
250
|
}
|
|
183
251
|
// A change rejected in review is the only cross-tick memory of what was built and why it
|
|
184
252
|
// failed — every tick starts a fresh session, so the full reasons ride along on the next
|
|
185
253
|
// prompt until the role's next reviewed change replaces them.
|
|
186
254
|
if (state.lastReview?.verdict === "reject" && !state.revision) {
|
|
187
|
-
prompt
|
|
255
|
+
prompt = appendPromptNote(prompt, buildRejectedReviewNote(state.lastReview, role));
|
|
188
256
|
}
|
|
189
257
|
// Likewise a change leftover recovery discarded as unmergeable: named until the role queues
|
|
190
258
|
// its next change (the discarding tick itself appends it after recovery — see runTick).
|
|
191
259
|
const discard = state.conflictDiscard;
|
|
192
260
|
if (discard)
|
|
193
|
-
prompt
|
|
261
|
+
prompt = appendPromptNote(prompt, buildConflictDiscardNote(discard.summary, discard.attempts));
|
|
194
262
|
// A fresh tick after the previous run(s) were cut off at the context ceiling (the loop
|
|
195
263
|
// stopped resuming, or never resumed — the director re-runs its prompt fresh): the only
|
|
196
264
|
// memory that the last attempt was too big for the window is this note.
|
|
197
265
|
if ((state.cutOffStreak ?? 0) > 0) {
|
|
198
|
-
prompt
|
|
266
|
+
prompt = appendPromptNote(prompt, buildCutOffNote(state.cutOffStreak ?? 0));
|
|
199
267
|
}
|
|
200
268
|
return { prompt, userPrompt, userPromptFile };
|
|
201
269
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agentSessionResumable } from "../agent/agent-backend.js";
|
|
2
2
|
import { latestSession } from "../agent/session-ledger.js";
|
|
3
|
-
import {
|
|
3
|
+
import { RETRIABLE_MERGE_RESULTS } from "../merge/merge-core.js";
|
|
4
4
|
import { sessionDir } from "../paths.js";
|
|
5
5
|
import { buildHandoffPrompt, buildResumePrompt } from "../prompt/prompt-followup.js";
|
|
6
6
|
/** Decide how a tick starts, consuming the resume flags as a side effect (the flag is
|
|
@@ -10,12 +10,12 @@ import { buildHandoffPrompt, buildResumePrompt } from "../prompt/prompt-followup
|
|
|
10
10
|
* inbox); a role loop's assembly never returns null. */
|
|
11
11
|
export function planTickStart(opts) {
|
|
12
12
|
const s = opts.state;
|
|
13
|
-
// The last
|
|
14
|
-
// non-terminally kept its pin (the
|
|
13
|
+
// The last merge's failure, read before the clear below: a merge that failed
|
|
14
|
+
// non-terminally kept its pin (the merger's own vocabulary — review_error, merge_conflict,
|
|
15
15
|
// merge_blocked — with the detail it wrote into `lastError`), and when this tick's recovery
|
|
16
16
|
// re-queues that pin, the failure must still feed the error streak (finishRecoveryTick).
|
|
17
|
-
const
|
|
18
|
-
? (s.lastError ?? `
|
|
17
|
+
const priorMergeFailure = s.lastResult !== undefined && RETRIABLE_MERGE_RESULTS.has(s.lastResult)
|
|
18
|
+
? (s.lastError ?? `merge failed: ${s.lastResult}`)
|
|
19
19
|
: undefined;
|
|
20
20
|
s.lastError = undefined;
|
|
21
21
|
// A tick interrupted by a harness shutdown left its pi session and its worktree's
|
|
@@ -42,7 +42,7 @@ export function planTickStart(opts) {
|
|
|
42
42
|
// claude resumes an id (safe across a newer gate line). A backend with no `resumable` hook
|
|
43
43
|
// falls back to the backend-neutral session-file check.
|
|
44
44
|
const resumable = resumePending && !resumeMissing
|
|
45
|
-
? agentSessionResumable(dir, { kind: "author", agent })
|
|
45
|
+
? agentSessionResumable(dir, { kind: "author", agent }, opts.claudeConfigDir)
|
|
46
46
|
: false;
|
|
47
47
|
const resumableSession = resumable;
|
|
48
48
|
// A newest line our backend cannot resume is not ours to continue: another kind's or
|
|
@@ -53,7 +53,7 @@ export function planTickStart(opts) {
|
|
|
53
53
|
// session over the kept worktree, carrying the assembled tick prompt plus the handoff
|
|
54
54
|
// bridge (plans/agent-backends.md "Sessions"). The failed `resumeMissing` run arms
|
|
55
55
|
// `resumePending` and records `resumeMissing` from the verdict (its error turn carries no
|
|
56
|
-
// assistant content), so the very next tick
|
|
56
|
+
// assistant content), so the very next tick ends up here with the worktree intact. A dir with
|
|
57
57
|
// no ledger line at all is not a handoff — there is no recorded interrupted session to carry
|
|
58
58
|
// forward.
|
|
59
59
|
// Captured before the flag is consumed below; a stale cause must not leak into a later resume.
|
|
@@ -62,7 +62,12 @@ export function planTickStart(opts) {
|
|
|
62
62
|
// survive, but the next run is a FRESH session — the fallback seam may be a different backend,
|
|
63
63
|
// and claude keeps no flags across `--resume` (plans/agent-backends.md §7, part 7a/7).
|
|
64
64
|
const budgetCapped = pendingResumeCause === "budget-capped";
|
|
65
|
-
|
|
65
|
+
// An image-blind run with an image-capable peer tier ends `aborted` with this cause
|
|
66
|
+
// (plans/image-input.md part 4/5): the next tick runs a FRESH session on the TARGET seam
|
|
67
|
+
// over the kept worktree — never `--continue` — because the old session carries pi's
|
|
68
|
+
// "image omitted" note and the target may be a different backend.
|
|
69
|
+
const imageReroute = pendingResumeCause === "image-reroute";
|
|
70
|
+
const handoff = (latest !== null && !resumable) || budgetCapped || imageReroute;
|
|
66
71
|
s.resumePending = false;
|
|
67
72
|
s.resumeCause = undefined;
|
|
68
73
|
s.resumeMissing = undefined;
|
|
@@ -74,7 +79,7 @@ export function planTickStart(opts) {
|
|
|
74
79
|
const resumeBlocked = s.phase === "review";
|
|
75
80
|
// A budget-capped handback never resumes the session whose seam the cap moved off of, even
|
|
76
81
|
// when its transcript is still resumable: it hands off instead.
|
|
77
|
-
const resuming = resumableSession && !resumeBlocked && !budgetCapped;
|
|
82
|
+
const resuming = resumableSession && !resumeBlocked && !budgetCapped && !imageReroute;
|
|
78
83
|
const handingOff = handoff && !resumeBlocked;
|
|
79
84
|
// Reclaim the prompt the interrupted tick re-queued for its resume (src/inbox/pending-prompt.ts):
|
|
80
85
|
// the resumed session still owns that request in its context, so this tick's bookkeeping
|
|
@@ -91,19 +96,30 @@ export function planTickStart(opts) {
|
|
|
91
96
|
const resumeCause = handingOff
|
|
92
97
|
? budgetCapped
|
|
93
98
|
? "budget-capped"
|
|
94
|
-
:
|
|
99
|
+
: imageReroute
|
|
100
|
+
? "image-reroute"
|
|
101
|
+
: "handoff"
|
|
95
102
|
: pendingResumeCause ?? ((s.cutOffStreak ?? 0) > 0 ? "cut-off" : "restart");
|
|
96
103
|
let prompt;
|
|
97
104
|
if (resuming) {
|
|
98
105
|
prompt = buildResumePrompt(opts.role, resumeCause, agent);
|
|
99
106
|
}
|
|
100
107
|
else {
|
|
101
|
-
const assembled = opts.tickPrompt(agent);
|
|
108
|
+
const assembled = opts.tickPrompt(agent, opts.compact);
|
|
109
|
+
// The image-reroute handoff note names the model the blind run was on and the image it
|
|
110
|
+
// read; those facts ride the one-shot marker, which is consumed here. A handoff's fresh
|
|
111
|
+
// session also needs the to-be-run model named for its own side of the note, but the
|
|
112
|
+
// bridge already carries the facts that matter.
|
|
113
|
+
const rerouteFacts = imageReroute && s.imageReroute
|
|
114
|
+
? { from: s.imageReroute.from ?? "the prior model", path: s.imageReroute.path ?? "an image" }
|
|
115
|
+
: undefined;
|
|
116
|
+
if (imageReroute)
|
|
117
|
+
s.imageReroute = undefined;
|
|
102
118
|
prompt =
|
|
103
119
|
assembled === null
|
|
104
120
|
? null
|
|
105
121
|
: handingOff
|
|
106
|
-
? buildHandoffPrompt(opts.role, assembled, resumeCause)
|
|
122
|
+
? buildHandoffPrompt(opts.role, assembled, resumeCause, rerouteFacts)
|
|
107
123
|
: assembled;
|
|
108
124
|
}
|
|
109
125
|
if (prompt === null)
|
|
@@ -111,5 +127,5 @@ export function planTickStart(opts) {
|
|
|
111
127
|
// The raw user prompt a director tick is executing (null for role loops), so an
|
|
112
128
|
// unfulfilled outcome below can re-queue it. Captured before the field is cleared.
|
|
113
129
|
const userPrompt = opts.pending.get();
|
|
114
|
-
return {
|
|
130
|
+
return { priorMergeFailure, resuming, handoff: handingOff, resumeCause, prompt, userPrompt };
|
|
115
131
|
}
|