tumwater 0.4.0 → 0.4.2
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 +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- 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 +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -9,10 +9,10 @@ import { briefFile } from "../brief/brief.js";
|
|
|
9
9
|
import { buildDirectorPrompt, buildRunPrompt } 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, buildMergedPrerequisitesNote, buildRejectedReviewNote, buildWaitingPartsNote } from "../prompt/gate-notes.js";
|
|
12
|
+
import { buildAssignmentNote, buildConflictDiscardNote, buildHeldEntriesNote, buildMergedPrerequisitesNote, buildNoCheckNote, buildRejectedReviewNote, buildWaitingPartsNote } from "../prompt/gate-notes.js";
|
|
13
13
|
import { eligibleEntries, mergedPrerequisiteRanges, waitingPartRanges } from "../backlog/backlog-eligibility.js";
|
|
14
14
|
import { notesBacklogBlock } from "../prompt/notes-backlog.js";
|
|
15
|
-
import { donePlans, openBugEntries, plannedPlanEntries } from "../backlog/backlog.js";
|
|
15
|
+
import { donePlans, openBugEntries, plannedPlanEntries, declarationPlanEntry } from "../backlog/backlog.js";
|
|
16
16
|
import { detectBuildCheck, gateCommandOf } from "../build/build-check-detect.js";
|
|
17
17
|
import { flakeCounts, KNOWN_FLAKE_WINDOW_MS } from "../review/known-flakes.js";
|
|
18
18
|
import { readEventsSince } from "../events/event-window.js";
|
|
@@ -83,199 +83,238 @@ function flakyChecksNote(root) {
|
|
|
83
83
|
`names one of these, that bug is still firing. When \`## Open\` holds no entries, a check ` +
|
|
84
84
|
`excused 2 or more times is a confirmed latent bug: take it as this run's latent-bug candidate.`);
|
|
85
85
|
}
|
|
86
|
-
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
86
|
+
/** Build the director's prompt from its deferred request: the queue peek/dequeue, the
|
|
87
|
+
* empty-inbox null return, the not-before marker strip, and the buildDirectorPrompt call.
|
|
88
|
+
* Returns null when the director has nothing to run (an empty inbox). */
|
|
89
|
+
function buildDirectorRunPrompt(root, config, preview, backend, brief, principles, check, backlogIndex) {
|
|
90
|
+
// A deferred director prompt's `tumwater:not-before` marker line is plumbing
|
|
91
|
+
// (src/inbox/prompt-not-before.ts),
|
|
92
|
+
// not content — stripped here exactly like the per-role path below, so the director's run
|
|
93
|
+
// prompt and the runner's re-queue both see the operator's text alone.
|
|
94
|
+
const entry = preview ? null : dequeuePromptEntry(root);
|
|
95
|
+
const dequeued = preview ? peekPrompt(root) : entry?.text ?? null;
|
|
96
|
+
if (!dequeued)
|
|
97
|
+
return null;
|
|
98
|
+
const userPrompt = stripNotBeforeMarker(dequeued);
|
|
99
|
+
const userPromptFile = entry?.file ?? null;
|
|
100
|
+
const prompt = buildDirectorPrompt(userPrompt, principles, check, brief, undefined, backlogIndex, backend ?? configForRole(config, DIRECTOR_ROLE).agent, config.roles[DIRECTOR_ROLE]?.instructions);
|
|
101
|
+
return { prompt, userPrompt, userPromptFile };
|
|
102
|
+
}
|
|
103
|
+
/** Build a non-director role's prompt from its queued request and state: the catalog/custom
|
|
104
|
+
* resolution and unknown-role throw, the role-prompt dequeue and marker strip, the
|
|
105
|
+
* observer evidence blocks, the notes and their backlog state, the plan target, and the
|
|
106
|
+
* flaky/no-check notes, feeding the single buildRunPrompt call. Never null — a role always
|
|
107
|
+
* has a prompt to run on. Each stage is read by one named helper below, so the body is the
|
|
108
|
+
* ordered calls and the single buildRunPrompt call. */
|
|
109
|
+
function buildRoleRunPrompt(root, config, role, state, preview, backend, brief, principles, check, backlogIndex, rejectedReview) {
|
|
110
|
+
const { base, resolved } = resolveRoleForPrompt(config, role);
|
|
111
|
+
const { request, promptFile } = collectRoleRequest(root, role, preview);
|
|
112
|
+
const digest = collectSelfcheckDigest(root, base);
|
|
113
|
+
const coverage = collectQaCoverageBlock(root, base);
|
|
114
|
+
const ciParity = collectCiParityBlock(root, config, base);
|
|
115
|
+
const filings = collectFeedbackFilingsBlock(root, base);
|
|
116
|
+
const backlogStructure = collectBacklogStructureBlock(root, base);
|
|
117
|
+
const { notes, notesSavedDay, notesBacklog } = readPromptNotes(root, role);
|
|
118
|
+
const { planTargetNote, flakyChecks, noCheck } = collectRoleDirectives(root, config, base, check);
|
|
119
|
+
// A run holding a claim with no queued request is assigned its one entry (the assignment
|
|
120
|
+
// note below). Such a run reads that entry first, so the backlog index — what an unassigned
|
|
121
|
+
// run chooses its task from — stays out.
|
|
122
|
+
const assigned = state.claim !== undefined && request === null;
|
|
121
123
|
let userPrompt = null;
|
|
122
124
|
let userPromptFile = null;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
125
|
+
const prompt = buildRunPrompt({
|
|
126
|
+
role: resolved,
|
|
127
|
+
principles,
|
|
128
|
+
digest,
|
|
129
|
+
coverage,
|
|
130
|
+
ciParity,
|
|
131
|
+
filings,
|
|
132
|
+
backlogStructure,
|
|
133
|
+
backlogIndex: assigned ? undefined : backlogIndex,
|
|
134
|
+
notes,
|
|
135
|
+
notesSavedDay,
|
|
136
|
+
notesBacklog,
|
|
137
|
+
planTargetNote,
|
|
138
|
+
flakyChecks,
|
|
139
|
+
noCheck,
|
|
140
|
+
extraInstructions: config.roles[base]?.instructions,
|
|
141
|
+
check,
|
|
142
|
+
gateCommand: gateCommandOf(config),
|
|
143
|
+
briefFile: brief,
|
|
144
|
+
agent: backend ?? configForRole(config, role).agent,
|
|
145
|
+
assigned,
|
|
146
|
+
rejectedReview,
|
|
147
|
+
// A per-role prompt queued by `tumwater prompt --role <id>` rides as an explicit user
|
|
148
|
+
// request block; its text is also the run's userPrompt,
|
|
149
|
+
// so the runner's pending-prompt machinery (re-queue on unfulfilled, clear on merge)
|
|
150
|
+
// treats it exactly like the director's dequeued request.
|
|
151
|
+
userRequest: request ?? undefined,
|
|
152
|
+
});
|
|
153
|
+
if (request) {
|
|
154
|
+
userPrompt = request;
|
|
155
|
+
userPromptFile = promptFile;
|
|
156
|
+
}
|
|
157
|
+
return { prompt, userPrompt, userPromptFile };
|
|
158
|
+
}
|
|
159
|
+
/** Resolve the run's role: the base role that keys the observer evidence, and the catalog or
|
|
160
|
+
* user-defined charter. Throws the shared unknown-role message when neither answers. */
|
|
161
|
+
function resolveRoleForPrompt(config, role) {
|
|
162
|
+
// The role id keys its inbox and notes; every lookup ABOUT the role — catalog charter,
|
|
163
|
+
// instructions, the qa/selfcheck/clean blocks — resolves through the base role, so
|
|
164
|
+
// `feature-2` runs the feature charter and `roles.feature.instructions` while keeping its
|
|
165
|
+
// own queue and notes (plans/parallel-work-instances.md).
|
|
166
|
+
const base = baseRoleOf(role);
|
|
167
|
+
// Catalog first, then user-defined roles (plans/user-defined-loops.md): a custom's task
|
|
168
|
+
// is its entire find-something-to-do text and the title identifies it in the prompt.
|
|
169
|
+
const custom = config.customRoles.find((c) => c.name === role);
|
|
170
|
+
const resolved = roleById(role) ?? (custom ? customRole(custom.name, custom.task) : undefined);
|
|
171
|
+
if (!resolved) {
|
|
172
|
+
// The shared unknownRoleMessage (parseRoleFlag, the operator commands) names the valid
|
|
173
|
+
// ids; this defensive path — a runner asked to run a role no catalog entry or
|
|
174
|
+
// customRoles task can answer — says the same, so if it ever fires it reads as the
|
|
175
|
+
// harness bug it is instead of a bare dead end.
|
|
176
|
+
const validIds = [...allRoleIds(), ...config.customRoles.map((c) => c.name)];
|
|
177
|
+
throw new Error(unknownRoleMessage(role, validIds));
|
|
135
178
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
179
|
+
return { base, resolved };
|
|
180
|
+
}
|
|
181
|
+
/** Dequeue (or, in preview, peek) the role's queued request and strip its not-before marker,
|
|
182
|
+
* returning the delivered text and the queue file it came from. */
|
|
183
|
+
function collectRoleRequest(root, role, preview) {
|
|
184
|
+
// A queued per-role prompt is dequeued here, before the prompt is built, so its text rides
|
|
185
|
+
// in the run's prompt; PendingPrompt records it as pending and re-queues it on every
|
|
186
|
+
// unfulfilled outcome — including a merge-side `main_red` pin, which returns before any run.
|
|
187
|
+
// A deferred prompt's `tumwater:not-before` marker line is plumbing
|
|
188
|
+
// (src/inbox/prompt-not-before.ts), not
|
|
189
|
+
// content: it is stripped at delivery, so the role sees the operator's text alone and the
|
|
190
|
+
// runner's re-queue writes clean text.
|
|
191
|
+
const entry = preview ? null : dequeueRolePromptEntry(root, role);
|
|
192
|
+
const dequeued = preview ? peekRolePrompt(root, role) : entry?.text ?? null;
|
|
193
|
+
const request = dequeued === null ? null : stripNotBeforeMarker(dequeued);
|
|
194
|
+
return { request, promptFile: entry?.file ?? null };
|
|
195
|
+
}
|
|
196
|
+
/** The selfcheck and feedback observers' evidence is the harness's own event log, one level
|
|
197
|
+
* outside this worktree, so its evidence module renders it (selfcheckDigest) and the run
|
|
198
|
+
* injects it. */
|
|
199
|
+
function collectSelfcheckDigest(root, base) {
|
|
200
|
+
return base === "selfcheck" || base === "feedback" ? selfcheckDigest(root) : undefined;
|
|
201
|
+
}
|
|
202
|
+
/** The `qa` observer's flow-coverage ledger: rerendered from disk each run, degrading to no
|
|
203
|
+
* block on a missing or unreadable ledger (plans/observer-roles.md 2/2). */
|
|
204
|
+
function collectQaCoverageBlock(root, base) {
|
|
205
|
+
// The `qa` observer's flow rotation needs a memory of what it last exercised; every run
|
|
206
|
+
// is a fresh session, and a passing cheap check leaves nothing in the repo. The ledger is
|
|
207
|
+
// runtime state, and a missing or unreadable one degrades to no block.
|
|
208
|
+
let coverage;
|
|
209
|
+
if (base === "qa") {
|
|
210
|
+
try {
|
|
211
|
+
coverage = renderCoverageBlock(readQaCoverage(root));
|
|
153
212
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// unfulfilled outcome — including a red-main gate block, which returns before any run.
|
|
157
|
-
// A deferred prompt's `tumwater:not-before` marker line is plumbing
|
|
158
|
-
// (src/inbox/prompt-not-before.ts), not
|
|
159
|
-
// content: it is stripped at delivery, so the role sees the operator's text alone and the
|
|
160
|
-
// runner's re-queue writes clean text.
|
|
161
|
-
const entry = preview ? null : dequeueRolePromptEntry(root, role);
|
|
162
|
-
const dequeued = preview ? peekRolePrompt(root, role) : entry?.text ?? null;
|
|
163
|
-
const request = dequeued === null ? null : stripNotBeforeMarker(dequeued);
|
|
164
|
-
// The selfcheck and feedback observers' evidence is the harness's own event log, one level
|
|
165
|
-
// outside this worktree, so its evidence module renders it (selfcheckDigest) and the run
|
|
166
|
-
// injects it.
|
|
167
|
-
const digest = base === "selfcheck" || base === "feedback" ? selfcheckDigest(root) : undefined;
|
|
168
|
-
// The `qa` observer's flow rotation needs a memory of what it last exercised; every run
|
|
169
|
-
// is a fresh session, and a passing cheap check leaves nothing in the repo. The ledger is
|
|
170
|
-
// runtime state, and a missing or unreadable one degrades to no block
|
|
171
|
-
// (plans/observer-roles.md 2/2).
|
|
172
|
-
let coverage;
|
|
173
|
-
if (base === "qa") {
|
|
174
|
-
try {
|
|
175
|
-
coverage = renderCoverageBlock(readQaCoverage(root));
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
coverage = undefined;
|
|
179
|
-
}
|
|
213
|
+
catch {
|
|
214
|
+
coverage = undefined;
|
|
180
215
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
216
|
+
}
|
|
217
|
+
return coverage;
|
|
218
|
+
}
|
|
219
|
+
/** The `ci` observer's parity block: what the last run covered (tip and lock hash) so a
|
|
220
|
+
* fresh session can decide nothing-to-do without re-running the whole suite (part 3/3).
|
|
221
|
+
* Runtime state, and a missing/unreadable record degrades to no block, exactly like qa's. */
|
|
222
|
+
function collectCiParityBlock(root, config, base) {
|
|
223
|
+
let ciParity;
|
|
224
|
+
if (base === "ci") {
|
|
225
|
+
try {
|
|
226
|
+
ciParity = parityBlockFor(root, config);
|
|
192
227
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// "previously filed" block instead of the network alone. Runtime state, and a missing or
|
|
196
|
-
// unreadable ledger degrades to no block (plans/feedback-role.md 2/2).
|
|
197
|
-
// The render returns undefined for an empty ledger, so the prompt is unchanged in the common
|
|
198
|
-
// first-run case.
|
|
199
|
-
let filings;
|
|
200
|
-
if (base === "feedback") {
|
|
201
|
-
try {
|
|
202
|
-
filings = renderFilingsBlock(readFeedbackFilings(root));
|
|
203
|
-
}
|
|
204
|
-
catch {
|
|
205
|
-
filings = undefined;
|
|
206
|
-
}
|
|
228
|
+
catch {
|
|
229
|
+
ciParity = undefined;
|
|
207
230
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const notesBacklog = notes === undefined || notes.trim() === ""
|
|
226
|
-
? undefined
|
|
227
|
-
: notesBacklogBlock(notes, {
|
|
228
|
-
planned: plannedPlanEntries(root),
|
|
229
|
-
openBugs: openBugEntries(root),
|
|
230
|
-
donePlanTitles: donePlans(root),
|
|
231
|
-
});
|
|
232
|
-
// The plan charter's stop threshold is derived from config (plans/parallel-work-
|
|
233
|
-
// instances.md "…keeping the plan role ahead", part 5c/7). The catalog text stays a
|
|
234
|
-
// config-free description of the rule; this note supplies the concrete eligible-plan count
|
|
235
|
-
// so the plan role stops only once every configured feature instance has a plan waiting.
|
|
236
|
-
const planTargetNote = base === "plan"
|
|
237
|
-
? `This run's stop target: when PLANS.md \`## Planned\` already holds ` +
|
|
238
|
-
`${planBacklogTarget(config)} or more eligible plans, end with ${NOTHING_TO_DO}. ` +
|
|
239
|
-
`It holds ${eligibleEntries(root, "feature").length} now (tumwater's count of entries ` +
|
|
240
|
-
`feature could take, the same count that decides whether a plan run is deferred).`
|
|
241
|
-
: undefined;
|
|
242
|
-
const flakyChecks = base === "bugfix" ? flakyChecksNote(root) : undefined;
|
|
243
|
-
// A run holding a claim with no queued request is assigned its one entry (the assignment
|
|
244
|
-
// note below). Such a run reads that entry first, so the backlog index — what an unassigned
|
|
245
|
-
// run chooses its task from — stays out.
|
|
246
|
-
const assigned = state.claim !== undefined && request === null;
|
|
247
|
-
prompt = buildRunPrompt({
|
|
248
|
-
role: resolved,
|
|
249
|
-
principles,
|
|
250
|
-
digest,
|
|
251
|
-
coverage,
|
|
252
|
-
ciParity,
|
|
253
|
-
filings,
|
|
254
|
-
backlogStructure,
|
|
255
|
-
backlogIndex: assigned ? undefined : backlogIndex,
|
|
256
|
-
notes,
|
|
257
|
-
notesSavedDay,
|
|
258
|
-
notesBacklog,
|
|
259
|
-
planTargetNote,
|
|
260
|
-
flakyChecks,
|
|
261
|
-
extraInstructions: config.roles[base]?.instructions,
|
|
262
|
-
check,
|
|
263
|
-
gateCommand: gateCommandOf(config),
|
|
264
|
-
briefFile: brief,
|
|
265
|
-
agent: backend ?? configForRole(config, role).agent,
|
|
266
|
-
assigned,
|
|
267
|
-
rejectedReview,
|
|
268
|
-
// A per-role prompt queued by `tumwater prompt --role <id>` rides as an explicit user
|
|
269
|
-
// request block; its text is also the run's userPrompt,
|
|
270
|
-
// so the runner's pending-prompt machinery (re-queue on unfulfilled, clear on merge)
|
|
271
|
-
// treats it exactly like the director's dequeued request.
|
|
272
|
-
userRequest: request ?? undefined,
|
|
273
|
-
});
|
|
274
|
-
if (request) {
|
|
275
|
-
userPrompt = request;
|
|
276
|
-
userPromptFile = entry?.file ?? null;
|
|
231
|
+
}
|
|
232
|
+
return ciParity;
|
|
233
|
+
}
|
|
234
|
+
/** The `feedback` observer's per-install filing ledger: what this install already filed as
|
|
235
|
+
* GitHub issues on znation/tumwater, so the fresh session's charter dedupes against a
|
|
236
|
+
* "previously filed" block instead of the network alone. Runtime state, and a missing or
|
|
237
|
+
* unreadable ledger degrades to no block (plans/feedback-role.md 2/2). */
|
|
238
|
+
function collectFeedbackFilingsBlock(root, base) {
|
|
239
|
+
// The render returns undefined for an empty ledger, so the prompt is unchanged in the common
|
|
240
|
+
// first-run case.
|
|
241
|
+
let filings;
|
|
242
|
+
if (base === "feedback") {
|
|
243
|
+
try {
|
|
244
|
+
filings = renderFilingsBlock(readFeedbackFilings(root));
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
filings = undefined;
|
|
277
248
|
}
|
|
278
249
|
}
|
|
250
|
+
return filings;
|
|
251
|
+
}
|
|
252
|
+
/** The clean role's deterministic backlog repair: stranded plan headings in the primary
|
|
253
|
+
* checkout's PLANS.md (plans, part 3/4), rendered like the digest and coverage blocks. */
|
|
254
|
+
function collectBacklogStructureBlock(root, base) {
|
|
255
|
+
// An unreadable or clean file gives no block, so the prompt is unchanged in the common case.
|
|
256
|
+
return base === "clean" ? renderBacklogStructureBlock(root) : undefined;
|
|
257
|
+
}
|
|
258
|
+
/** Read the role's own notes plus their saved day and live backlog state (notes-backlog.ts),
|
|
259
|
+
* returning all three in one place. */
|
|
260
|
+
function readPromptNotes(root, role) {
|
|
261
|
+
// The role's notes (PLANS.md "Role notebook"): its own earlier runs' note. A missing,
|
|
262
|
+
// empty, or unreadable file yields undefined, so the block is omitted; the write
|
|
263
|
+
// instruction in buildRunPrompt is present either way. The director branch above never
|
|
264
|
+
// reads this path, so it carries no notes.
|
|
265
|
+
const notes = readRoleNote(root, role);
|
|
266
|
+
// The note file's mtime is when the notes were last saved (writes replace the file), so the
|
|
267
|
+
// run can weigh how stale they are.
|
|
268
|
+
const savedStat = notes === undefined ? null : statOrNull(roleNotesPath(root, role));
|
|
269
|
+
const notesSavedDay = savedStat ? dayKey(savedStat.mtimeMs) : undefined;
|
|
270
|
+
// The live state of each entry and series the notes name, so a stale note cannot keep a hold
|
|
271
|
+
// the backlog has lifted (notes-backlog.ts). The three readers are already stat-cached. A
|
|
272
|
+
// note naming nothing live yields undefined and no block — the common case, since most notes
|
|
273
|
+
// are about the code, not the backlog.
|
|
274
|
+
const notesBacklog = notes === undefined || notes.trim() === ""
|
|
275
|
+
? undefined
|
|
276
|
+
: notesBacklogBlock(notes, {
|
|
277
|
+
planned: plannedPlanEntries(root),
|
|
278
|
+
openBugs: openBugEntries(root),
|
|
279
|
+
donePlanTitles: donePlans(root),
|
|
280
|
+
});
|
|
281
|
+
return { notes, notesSavedDay, notesBacklog };
|
|
282
|
+
}
|
|
283
|
+
/** The directive notes a run may carry: the plan charter's stop target, the bugfix flake
|
|
284
|
+
* tally, and the plan/feature no-check note — each exactly as it stood inline. */
|
|
285
|
+
function collectRoleDirectives(root, config, base, check) {
|
|
286
|
+
// The plan charter's stop threshold is derived from config (plans/parallel-work-
|
|
287
|
+
// instances.md "…keeping the plan role ahead", part 5c/7). The catalog text stays a
|
|
288
|
+
// config-free description of the rule; this note supplies the concrete eligible-plan count
|
|
289
|
+
// so the plan role stops only once every configured feature instance has a plan waiting.
|
|
290
|
+
const planTargetNote = base === "plan"
|
|
291
|
+
? `This run's stop target: when PLANS.md \`## Planned\` already holds ` +
|
|
292
|
+
`${planBacklogTarget(config)} or more eligible plans, end with ${NOTHING_TO_DO}. ` +
|
|
293
|
+
`It holds ${eligibleEntries(root, "feature").length} now (tumwater's count of entries ` +
|
|
294
|
+
`feature could take, the same count that decides whether a plan run is deferred).`
|
|
295
|
+
: undefined;
|
|
296
|
+
const flakyChecks = base === "bugfix" ? flakyChecksNote(root) : undefined;
|
|
297
|
+
// The no-check note: when the project declares no deterministic check and the team is
|
|
298
|
+
// closing the gap itself, the plan note tells the plan role to write the declaration entry
|
|
299
|
+
// (no marker entry waits yet) and the feature note tells the feature role to implement the
|
|
300
|
+
// marker entry that does wait. Decided by CHECK_DECLARATION_MARKER (declarationPlanEntry)
|
|
301
|
+
// rather than prose, so a feature run never invents a declaration the plan role never wrote
|
|
302
|
+
// and a plan run cannot stack a second declaration on one already waiting. Any other role,
|
|
303
|
+
// or a declared check, gets neither note — byte-unchanged prompts.
|
|
304
|
+
const declaration = (base === "plan" || base === "feature") && check === undefined ? declarationPlanEntry(root) : undefined;
|
|
305
|
+
// Whole declaration drives the decision: `declarationPlanEntry` returns null (not
|
|
306
|
+
// undefined) when no marker entry waits, so a truthy test selects the feature note and a
|
|
307
|
+
// falsy one the plan note.
|
|
308
|
+
const noCheck = check === undefined && (base === "plan" || base === "feature")
|
|
309
|
+
? (declaration
|
|
310
|
+
? (base === "feature" ? buildNoCheckNote(base) : undefined)
|
|
311
|
+
: (base === "plan" ? buildNoCheckNote(base) : undefined))
|
|
312
|
+
: undefined;
|
|
313
|
+
return { planTargetNote, flakyChecks, noCheck };
|
|
314
|
+
}
|
|
315
|
+
/** Append the shared claim/held/discard/cut-off tail — identical for a director and a role —
|
|
316
|
+
* in its current order, and return the finished prompt. */
|
|
317
|
+
function appendRunEpilogue(root, role, state, prompt, userPrompt, rejectedReview) {
|
|
279
318
|
// A work instance holding a claim is told which entry is its one task (plans/parallel-work-
|
|
280
319
|
// instances.md "Claims", part 4/7) — the harness picked it, so the charter's choosing step
|
|
281
320
|
// is replaced. A run that dequeued a user request runs that request instead and keeps the
|
|
@@ -314,5 +353,50 @@ export function assembleRunPrompt({ root, config, role, state, preview = false,
|
|
|
314
353
|
if ((state.cutOffStreak ?? 0) > 0) {
|
|
315
354
|
prompt = appendPromptNote(prompt, buildCutOffNote(state.cutOffStreak ?? 0));
|
|
316
355
|
}
|
|
317
|
-
return
|
|
356
|
+
return prompt;
|
|
357
|
+
}
|
|
358
|
+
/** Assemble the prompt a role's next run operates on — the whole "what should this run see"
|
|
359
|
+
* concern, split out of runner.ts so the runner owns only lifecycle and the prompt owns only
|
|
360
|
+
* content. Returns null when the role has nothing to run this run (a director with an empty
|
|
361
|
+
* inbox); `userPrompt` carries the dequeued user request — the director's, or a per-role one
|
|
362
|
+
* queued by `tumwater prompt --role <id>` — back so the runner can record it as pending and
|
|
363
|
+
* re-queue it if the run leaves it unfulfilled; `userPromptFile` is the queue file it came
|
|
364
|
+
* from, so that bookkeeping can clean the prompt's attachments by that file's stem. */
|
|
365
|
+
export function assembleRunPrompt({ root, config, role, state, preview = false, backend }) {
|
|
366
|
+
// The brief's owning file (TUMWATER.md first, README.md as the compatibility path —
|
|
367
|
+
// plans/portability.md §7a/7), named in both prompt builders' rules instead of a hardcoded
|
|
368
|
+
// README.md. "README.md" is the fallback for a repo with no marked file yet — the team
|
|
369
|
+
// runs blind on prompts either way, so the name in the rules should still point somewhere.
|
|
370
|
+
const brief = briefFile(root) ?? "README.md";
|
|
371
|
+
// The project's design principles ride along in every prompt — run and director alike — so
|
|
372
|
+
// all roles share one standard of taste. Empty when the repo has no PRINCIPLES.md.
|
|
373
|
+
const principles = readPrinciples(root);
|
|
374
|
+
// The project's resolved check (plans/portability.md §6/7): the prompt names the actual
|
|
375
|
+
// verify command instead of asserting npm. Detection is a handful of stat calls — a
|
|
376
|
+
// per-run recompute keeps a config edit live on the next run.
|
|
377
|
+
const check = detectBuildCheck(root, config) ?? undefined;
|
|
378
|
+
// The bounded actionable index every run and director prompt carries, replacing the model's
|
|
379
|
+
// per-run `grep -n '^##'` map of the backlog files (BUGS.md 2026-10-06). One render serves
|
|
380
|
+
// both branches; the worktree starts at main at run start, so the ranges hold then.
|
|
381
|
+
const backlogIndex = renderBacklogIndexBlock(root);
|
|
382
|
+
// A change rejected in review is the only cross-run memory of what was built and why it
|
|
383
|
+
// failed — every run starts a fresh session, so the full reasons (with the reviewed head and
|
|
384
|
+
// timestamp) ride along on the next prompt until the role's next reviewed change replaces
|
|
385
|
+
// them. buildRunPrompt renders it at the very top, before the role charter, so the author
|
|
386
|
+
// cannot miss it (plans/reviewer-as-role.md part 4/4). A revision run carries the reasons in
|
|
387
|
+
// buildRevisionNote instead (the rejected diff is already in the worktree), so the note is
|
|
388
|
+
// suppressed here exactly as it was before this part.
|
|
389
|
+
const rejectedReview = (state.lastReview?.verdict === "reject" || state.lastReview?.verdict === "reverted") && !state.revision
|
|
390
|
+
? buildRejectedReviewNote(state.lastReview, role)
|
|
391
|
+
: undefined;
|
|
392
|
+
const built = role === DIRECTOR_ROLE
|
|
393
|
+
? buildDirectorRunPrompt(root, config, preview, backend, brief, principles, check, backlogIndex)
|
|
394
|
+
: buildRoleRunPrompt(root, config, role, state, preview, backend, brief, principles, check, backlogIndex, rejectedReview);
|
|
395
|
+
if (built === null)
|
|
396
|
+
return null;
|
|
397
|
+
return {
|
|
398
|
+
prompt: appendRunEpilogue(root, role, state, built.prompt, built.userPrompt, rejectedReview),
|
|
399
|
+
userPrompt: built.userPrompt,
|
|
400
|
+
userPromptFile: built.userPromptFile,
|
|
401
|
+
};
|
|
318
402
|
}
|
|
@@ -29,12 +29,11 @@ export function planWithSalvagedWork(role, plan, restored, note) {
|
|
|
29
29
|
restored,
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
-
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
export function planRunStart(opts) {
|
|
32
|
+
/** Resolve how a run starts, consuming the resume flags as a side effect (a flag is consumed
|
|
33
|
+
* here so a resume that fails falls back to a normal fresh run; another shutdown mid-resume
|
|
34
|
+
* sets it again) and reclaiming the interrupted run's re-queued prompt. Owns every read and
|
|
35
|
+
* write of the resume flags, so a new cause or handoff rule is added here. */
|
|
36
|
+
function resolveRunStartDecision(opts) {
|
|
38
37
|
const s = opts.state;
|
|
39
38
|
// The last merge's failure, read before the clear below: a merge that failed
|
|
40
39
|
// non-terminally kept its pin (the merger's own vocabulary — review_error, merge_conflict,
|
|
@@ -88,12 +87,7 @@ export function planRunStart(opts) {
|
|
|
88
87
|
// survive, but the next run is a FRESH session — the fallback seam may be a different backend,
|
|
89
88
|
// and claude keeps no flags across `--resume` (plans/agent-backends.md §7, part 7a/7).
|
|
90
89
|
const budgetCapped = pendingResumeCause === "budget-capped";
|
|
91
|
-
|
|
92
|
-
// (plans/image-input.md part 4/5): the next run runs a FRESH session on the TARGET seam
|
|
93
|
-
// over the kept worktree — never `--continue` — because the old session carries pi's
|
|
94
|
-
// "image omitted" note and the target may be a different backend.
|
|
95
|
-
const imageReroute = pendingResumeCause === "image-reroute";
|
|
96
|
-
const handoff = (latest !== null && !resumable) || budgetCapped || imageReroute;
|
|
90
|
+
const handoff = (latest !== null && !resumable) || budgetCapped;
|
|
97
91
|
s.resumePending = false;
|
|
98
92
|
s.resumeCause = undefined;
|
|
99
93
|
s.resumeMissing = undefined;
|
|
@@ -105,7 +99,7 @@ export function planRunStart(opts) {
|
|
|
105
99
|
const resumeBlocked = s.phase === "review";
|
|
106
100
|
// A budget-capped handback never resumes the session whose seam the cap moved off of, even
|
|
107
101
|
// when its transcript is still resumable: it hands off instead.
|
|
108
|
-
const resuming = resumableSession && !resumeBlocked && !budgetCapped
|
|
102
|
+
const resuming = resumableSession && !resumeBlocked && !budgetCapped;
|
|
109
103
|
const handingOff = handoff && !resumeBlocked;
|
|
110
104
|
// Reclaim the prompt the interrupted run re-queued for its resume (src/inbox/pending-prompt.ts):
|
|
111
105
|
// the resumed session still owns that request in its context, so this run's bookkeeping
|
|
@@ -122,39 +116,46 @@ export function planRunStart(opts) {
|
|
|
122
116
|
const resumeCause = handingOff
|
|
123
117
|
? budgetCapped
|
|
124
118
|
? "budget-capped"
|
|
125
|
-
:
|
|
126
|
-
? "image-reroute"
|
|
127
|
-
: "handoff"
|
|
119
|
+
: "handoff"
|
|
128
120
|
: pendingResumeCause ?? ((s.cutOffStreak ?? 0) > 0 ? "cut-off" : "restart");
|
|
121
|
+
return { priorMergeFailure, resuming, handoff: handingOff, resumeCause };
|
|
122
|
+
}
|
|
123
|
+
/** Build the run's prompt from the resolved decision: the resume bridge on a resume, the
|
|
124
|
+
* handoff bridge on a handoff, the assembled prompt otherwise. Returns null only when a
|
|
125
|
+
* handoff/fresh assemble yields nothing. The raw user prompt is read by the caller only when a
|
|
126
|
+
* prompt exists. */
|
|
127
|
+
function buildRunStartPrompt(opts, d) {
|
|
128
|
+
const agent = opts.agent ?? "pi";
|
|
129
129
|
let prompt;
|
|
130
|
-
if (resuming) {
|
|
131
|
-
prompt = buildResumePrompt(opts.role, resumeCause, agent);
|
|
130
|
+
if (d.resuming) {
|
|
131
|
+
prompt = buildResumePrompt(opts.role, d.resumeCause, agent);
|
|
132
132
|
}
|
|
133
133
|
else {
|
|
134
134
|
const assembled = opts.runPrompt(agent);
|
|
135
|
-
// The image-reroute handoff note names the model the blind run was on and the image it
|
|
136
|
-
// read; those facts ride the one-shot marker, which is consumed here. A handoff's fresh
|
|
137
|
-
// session also needs the to-be-run model named for its own side of the note, but the
|
|
138
|
-
// bridge already carries the facts that matter.
|
|
139
|
-
const rerouteFacts = imageReroute && s.imageReroute
|
|
140
|
-
? { from: s.imageReroute.from ?? "the prior model", path: s.imageReroute.path ?? "an image" }
|
|
141
|
-
: undefined;
|
|
142
|
-
if (imageReroute)
|
|
143
|
-
s.imageReroute = undefined;
|
|
144
135
|
prompt =
|
|
145
136
|
assembled === null
|
|
146
137
|
? null
|
|
147
|
-
:
|
|
148
|
-
? buildHandoffPrompt(opts.role, assembled, resumeCause
|
|
138
|
+
: d.handoff
|
|
139
|
+
? buildHandoffPrompt(opts.role, assembled, d.resumeCause)
|
|
149
140
|
: assembled;
|
|
150
141
|
}
|
|
142
|
+
return prompt;
|
|
143
|
+
}
|
|
144
|
+
/** Decide how a run starts, consuming the resume flags as a side effect (a flag is consumed
|
|
145
|
+
* here so a resume that fails falls back to a normal fresh run; another shutdown mid-resume
|
|
146
|
+
* sets it again) and reclaiming the interrupted run's re-queued prompt. Returns null — meaning
|
|
147
|
+
* the run is skipped — when the role has nothing to run (an empty director inbox); a role
|
|
148
|
+
* runner's assembly never returns null. */
|
|
149
|
+
export function planRunStart(opts) {
|
|
150
|
+
const d = resolveRunStartDecision(opts);
|
|
151
|
+
const prompt = buildRunStartPrompt(opts, d);
|
|
151
152
|
if (prompt === null) {
|
|
152
153
|
if (!opts.recoveryOnly)
|
|
153
154
|
return null;
|
|
154
|
-
return { priorMergeFailure, resuming: false, handoff: false, resumeCause, prompt: null, userPrompt: null };
|
|
155
|
+
return { priorMergeFailure: d.priorMergeFailure, resuming: false, handoff: false, resumeCause: d.resumeCause, prompt: null, userPrompt: null };
|
|
155
156
|
}
|
|
156
157
|
// The raw user prompt a director run is executing (null for role runners), so an
|
|
157
158
|
// unfulfilled outcome below can re-queue it. Captured before the field is cleared.
|
|
158
159
|
const userPrompt = opts.pending.get();
|
|
159
|
-
return { priorMergeFailure, resuming, handoff:
|
|
160
|
+
return { priorMergeFailure: d.priorMergeFailure, resuming: d.resuming, handoff: d.handoff, resumeCause: d.resumeCause, prompt, userPrompt };
|
|
160
161
|
}
|