tumwater 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +25 -12
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +1 -8
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +9 -0
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +50 -27
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +1 -1
- package/dist/src/agent/subscription-account.js +3 -3
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +58 -102
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +1 -1
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-scoped.js +40 -16
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +34 -12
- package/dist/src/cli/cli-query-commands.js +1 -1
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-start.js +91 -58
- package/dist/src/cli/completion.js +82 -46
- package/dist/src/cli/config-commands.js +82 -71
- package/dist/src/cli/help.js +4 -6
- package/dist/src/cli/log-commands.js +43 -23
- package/dist/src/cli/notify-command.js +2 -2
- package/dist/src/cli/question-commands.js +86 -60
- package/dist/src/cli.js +181 -135
- package/dist/src/collections.js +31 -16
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +22 -0
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +5 -20
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +153 -102
- package/dist/src/config/config-schema.js +9 -11
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +4 -52
- package/dist/src/config/config-validation.js +4 -38
- package/dist/src/config/config-views.js +9 -34
- package/dist/src/config/config-write.js +6 -23
- package/dist/src/config/config.js +4 -5
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +24 -24
- package/dist/src/doctor/doctor-backlog.js +7 -5
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +5 -5
- package/dist/src/doctor/doctor-claude-checks.js +44 -28
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +44 -112
- package/dist/src/doctor/doctor-fallback-checks.js +2 -2
- package/dist/src/doctor/doctor-holds.js +6 -45
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +120 -20
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +3 -3
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +30 -54
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +1 -3
- package/dist/src/events/{notify.js → event-notify.js} +2 -2
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/failure/failure-cluster.js +6 -6
- package/dist/src/failure/failure-data.js +8 -4
- package/dist/src/failure/failure-render.js +44 -34
- package/dist/src/failure/failure-state-change.js +4 -34
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +27 -68
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +7 -7
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +22 -23
- package/dist/src/gui/gui-prompt-commands.js +23 -23
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +51 -33
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +8 -11
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +10 -13
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-verify.js +8 -18
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +76 -44
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +27 -32
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +1 -1
- package/dist/src/orchestrator/scheduling-skip.js +1 -1
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +8 -7
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +1 -1
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +32 -6
- package/dist/src/pi/pi-watchdogs.js +12 -5
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +49 -5
- package/dist/src/process/process.js +8 -8
- package/dist/src/process/run-marker.js +31 -13
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +1 -1
- package/dist/src/review/review-precheck.js +11 -1
- package/dist/src/review/review.js +40 -153
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +1 -14
- package/dist/src/roles/role-view.js +9 -21
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/run-apply.js +12 -6
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +135 -84
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +119 -50
- package/dist/src/run/stage-check.js +49 -32
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/runner-outcome.js +6 -1
- package/dist/src/runner/runner-pi.js +3 -10
- package/dist/src/runner/runner-recovery.js +1 -1
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
- package/dist/src/runner/runner-run.js +97 -35
- package/dist/src/runner/runner-state-load.js +12 -9
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-data.js +2 -2
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -17
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +98 -65
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
- package/dist/src/team/team-polls.js +28 -126
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +20 -15
- package/dist/src/text/text.js +3 -3
- package/dist/src/ui/alert-feeds.js +49 -73
- package/dist/src/ui/badges.js +3 -13
- package/dist/src/ui/gui/gui-client-boot.js +2 -2
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +4 -4
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +53 -40
- package/dist/src/ui/run-progress-model.js +9 -2
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +3 -7
- package/dist/src/ui/status-render.js +5 -9
- package/dist/src/ui/team-alert-backlog.js +6 -6
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +7 -8
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +1 -7
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +159 -92
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +3 -3
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -228
- package/dist/src/ui/tui/tui-pane-history.js +61 -70
- package/dist/src/ui/tui/tui-pane-pending.js +284 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +11 -7
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +29 -32
- package/dist/src/ui/tui/tui.js +3 -4
- package/dist/src/verdict/fix-claim.js +76 -54
- package/dist/src/verdict/refusal.js +1 -1
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
- /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
|
@@ -166,11 +166,9 @@ function applyHistoryModelFilter(rows, rest) {
|
|
|
166
166
|
}
|
|
167
167
|
return { rows, models };
|
|
168
168
|
}
|
|
169
|
-
/**
|
|
170
|
-
*
|
|
171
|
-
|
|
172
|
-
* row collection and filtering and before `--json`. */
|
|
173
|
-
function sayHistory(rows, q, covered, filtered) {
|
|
169
|
+
/** Validate `--by`'s value and its `--summary` requirement; return whether the grouping is by
|
|
170
|
+
* model (false for `role`). One home for "can this `--by` proceed, and does it group by model?" */
|
|
171
|
+
function historySummaryGroup(q) {
|
|
174
172
|
// `--by role|model` picks how --summary groups its lines (role by default).
|
|
175
173
|
const byRaw = flagValue(q.rest, "--by");
|
|
176
174
|
let byModel = false;
|
|
@@ -184,6 +182,52 @@ function sayHistory(rows, q, covered, filtered) {
|
|
|
184
182
|
fail("history --by needs --summary (it picks how the summary groups runs)");
|
|
185
183
|
byModel = byRaw === "model";
|
|
186
184
|
}
|
|
185
|
+
return byModel;
|
|
186
|
+
}
|
|
187
|
+
/** The empty-set prose: blame the filter that emptied the set (model, then result, then grep),
|
|
188
|
+
* else the log or window. Never the `no runs yet`/`no runs in <window>` prose when rows were
|
|
189
|
+
* scanned and filtered out — the runs are there, none match. */
|
|
190
|
+
function sayHistoryEmpty(q, filtered) {
|
|
191
|
+
// A grep that matched nothing must not borrow the empty-log prose: `no runs yet` and
|
|
192
|
+
// `no runs in 2h` are statements about the LOG and the WINDOW, and both are false when
|
|
193
|
+
// rows were scanned and filtered out — the operator would read a quiet team where the
|
|
194
|
+
// truth is `the runs are there, none match`. The logs --grep rule (its `no events
|
|
195
|
+
// matching "<pattern>"`, src/cli/log-commands.ts) names the pattern instead; --json stays
|
|
196
|
+
// a bare {"rows":[]} document above, never prose in either form.
|
|
197
|
+
//
|
|
198
|
+
// The result class is named only when it emptied a non-empty set (`afterGrep > 0`): an
|
|
199
|
+
// empty log or window scanned no rows, so the result filter removed nothing and the
|
|
200
|
+
// empty-log/window prose is the true one — the dashboard's History view draws the same
|
|
201
|
+
// line. A grep that admitted nothing keeps its own wording regardless of --result.
|
|
202
|
+
say(filtered.models !== null && filtered.afterResult > 0
|
|
203
|
+
? `no runs on model "${filtered.models.join(",")}"`
|
|
204
|
+
: filtered.classes !== null && filtered.afterGrep > 0
|
|
205
|
+
? `no runs with result "${filtered.classes.join(",")}"`
|
|
206
|
+
: q.grepPattern !== null
|
|
207
|
+
? `no runs matching "${q.grepPattern}"`
|
|
208
|
+
: q.sinceMs === null
|
|
209
|
+
? "no runs yet"
|
|
210
|
+
: q.untilMs === null
|
|
211
|
+
? `no runs in ${durationLabel(q.sinceMs)}`
|
|
212
|
+
: `no runs ${betweenWindow(q.sinceMs, q.untilMs)}`);
|
|
213
|
+
}
|
|
214
|
+
/** Render the non-empty rows: the summary (model or role) when `--summary`, else the aligned
|
|
215
|
+
* table. Called only with `rows.length > 0`.
|
|
216
|
+
*/
|
|
217
|
+
function sayHistoryRows(rows, summary, byModel) {
|
|
218
|
+
if (summary)
|
|
219
|
+
say(byModel ? renderModelSummary(summarizeRunRowsByModel(rows)) : renderSummary(summarizeRunRows(rows)));
|
|
220
|
+
else {
|
|
221
|
+
const widths = widestWidths(rows);
|
|
222
|
+
say(rows.map((r) => renderRow(r, widths)).join("\n"));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** Render the collected-and-filtered rows: the `--by`/`--summary` summary grouping, the `--json`
|
|
226
|
+
* document, the empty-state wording (blaming the filter that emptied the set, else the log or
|
|
227
|
+
* window), the aligned table, and the sparse-window note. A bad `--by` value fails here, after
|
|
228
|
+
* row collection and filtering and before `--json`. */
|
|
229
|
+
function sayHistory(rows, q, covered, filtered) {
|
|
230
|
+
const byModel = historySummaryGroup(q);
|
|
187
231
|
const summary = q.rest.includes("--summary") && rows.length > 0;
|
|
188
232
|
// --json swaps the renderer for the collector's own payload, exactly as status --json and
|
|
189
233
|
// report --json: the same rows the table prints, each with ts/tokens/costUsd kept raw. A
|
|
@@ -194,36 +238,10 @@ function sayHistory(rows, q, covered, filtered) {
|
|
|
194
238
|
return;
|
|
195
239
|
}
|
|
196
240
|
if (rows.length === 0) {
|
|
197
|
-
|
|
198
|
-
// `no runs in 2h` are statements about the LOG and the WINDOW, and both are false when
|
|
199
|
-
// rows were scanned and filtered out — the operator would read a quiet team where the
|
|
200
|
-
// truth is `the runs are there, none match`. The logs --grep rule (its `no events
|
|
201
|
-
// matching "<pattern>"`, src/cli/log-commands.ts) names the pattern instead; --json stays
|
|
202
|
-
// a bare {"rows":[]} document above, never prose in either form.
|
|
203
|
-
//
|
|
204
|
-
// The result class is named only when it emptied a non-empty set (`afterGrep > 0`): an
|
|
205
|
-
// empty log or window scanned no rows, so the result filter removed nothing and the
|
|
206
|
-
// empty-log/window prose is the true one — the dashboard's History view draws the same
|
|
207
|
-
// line. A grep that admitted nothing keeps its own wording regardless of --result.
|
|
208
|
-
say(filtered.models !== null && filtered.afterResult > 0
|
|
209
|
-
? `no runs on model "${filtered.models.join(",")}"`
|
|
210
|
-
: filtered.classes !== null && filtered.afterGrep > 0
|
|
211
|
-
? `no runs with result "${filtered.classes.join(",")}"`
|
|
212
|
-
: q.grepPattern !== null
|
|
213
|
-
? `no runs matching "${q.grepPattern}"`
|
|
214
|
-
: q.sinceMs === null
|
|
215
|
-
? "no runs yet"
|
|
216
|
-
: q.untilMs === null
|
|
217
|
-
? `no runs in ${durationLabel(q.sinceMs)}`
|
|
218
|
-
: `no runs ${betweenWindow(q.sinceMs, q.untilMs)}`);
|
|
241
|
+
sayHistoryEmpty(q, filtered);
|
|
219
242
|
return;
|
|
220
243
|
}
|
|
221
|
-
|
|
222
|
-
say(byModel ? renderModelSummary(summarizeRunRowsByModel(rows)) : renderSummary(summarizeRunRows(rows)));
|
|
223
|
-
else {
|
|
224
|
-
const widths = widestWidths(rows);
|
|
225
|
-
say(rows.map((r) => renderRow(r, widths)).join("\n"));
|
|
226
|
-
}
|
|
244
|
+
sayHistoryRows(rows, summary, byModel);
|
|
227
245
|
// A sparse window is never mistaken for a quiet team — but the note only ever rides rows
|
|
228
246
|
// (an empty window returned above), it never touches --json output, and it is the shared
|
|
229
247
|
// SPARSE_WINDOW_NOTE wording: the oldest retained event being inside the window is exactly
|
|
@@ -179,10 +179,10 @@ function runPromptImageGate(root, target) {
|
|
|
179
179
|
if (guidance.notice)
|
|
180
180
|
say(guidance.notice);
|
|
181
181
|
}
|
|
182
|
-
/** `--edit
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
function
|
|
182
|
+
/** The `--edit` attachment phase: read the `--attach` image list (undefined when there is
|
|
183
|
+
* none) and run the image-input gate under the role the edit targets — the position's
|
|
184
|
+
* resolved role when no `--role` narrows it. */
|
|
185
|
+
function readEditAttachments(root, position, attachPaths, role) {
|
|
186
186
|
// The same broken-config policy and list-wide resolution as cancel, one verb over: the
|
|
187
187
|
// file's new content rides the same queue file (position, enqueue stamp, and a pending
|
|
188
188
|
// --at deferral untouched), and a concurrent dequeue reports gone and exits clean. An
|
|
@@ -200,48 +200,66 @@ function runPromptEdit(root, position, text, attachPaths, role) {
|
|
|
200
200
|
const targetRole = role ?? resolveListedRole(root, position);
|
|
201
201
|
runPromptImageGate(root, targetRole);
|
|
202
202
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
// prompt with no text, which the enqueue forms refuse.
|
|
225
|
-
if (after.trim() === "" && body.trim() !== "")
|
|
226
|
-
fail("the edited prompt is empty; nothing changed (use --cancel <n> to drop it)");
|
|
227
|
-
if (after.trim() === body.trim() && images === undefined) {
|
|
228
|
-
say(`prompt ${position} unchanged`);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
sayPromptOutcome(position, targetRole, editQueuedFile(root, targetRole, name, after, images), role === null);
|
|
233
|
-
}
|
|
234
|
-
catch (err) {
|
|
235
|
-
fail(errorMessage(err));
|
|
236
|
-
}
|
|
203
|
+
return images;
|
|
204
|
+
}
|
|
205
|
+
/** The bare `--edit <n>` arm: resolve the role, read the queued body by file name (so a
|
|
206
|
+
* concurrent dequeue reports gone), open it in the editor and write the result back —
|
|
207
|
+
* unchanged, emptied or edited — with the carried `images`. */
|
|
208
|
+
function editBareQueuedPrompt(root, position, role, images) {
|
|
209
|
+
// Bare `--edit <n>`: open the prompt's body in the editor. The role is resolved first
|
|
210
|
+
// and the write goes by file name, so a dequeue while the editor is open reports gone
|
|
211
|
+
// instead of editing the neighbour that shifted into the position.
|
|
212
|
+
const targetRole = role ?? resolveListedRole(root, position);
|
|
213
|
+
let name;
|
|
214
|
+
let body;
|
|
215
|
+
try {
|
|
216
|
+
name = path.basename(queuedFileAtPosition(root, targetRole, position));
|
|
217
|
+
body = readQueuedPromptBody(root, targetRole, name);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
return fail(errorMessage(err));
|
|
221
|
+
}
|
|
222
|
+
if (body === null) {
|
|
223
|
+
sayPromptOutcome(position, targetRole, { status: "gone" }, role === null);
|
|
237
224
|
return;
|
|
238
225
|
}
|
|
226
|
+
const after = editInEditor(body, "prompt.md");
|
|
227
|
+
// An emptied buffer is an abort, not a blank prompt: the queue would otherwise hold a
|
|
228
|
+
// prompt with no text, which the enqueue forms refuse.
|
|
229
|
+
if (after.trim() === "" && body.trim() !== "")
|
|
230
|
+
fail("the edited prompt is empty; nothing changed (use --cancel <n> to drop it)");
|
|
231
|
+
if (after.trim() === body.trim() && images === undefined) {
|
|
232
|
+
say(`prompt ${position} unchanged`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
sayPromptOutcome(position, targetRole, editQueuedFile(root, targetRole, name, after, images), role === null);
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
fail(errorMessage(err));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** The typed `--edit <n> <text>` / `--file` arm: the list-wide `editListedPrompt` path when
|
|
243
|
+
* no `--role`, else the role-scoped `editRolePrompt` command, with the carried `images`. */
|
|
244
|
+
function editTypedQueuedPrompt(root, position, text, role, images) {
|
|
239
245
|
if (role === null) {
|
|
240
|
-
sayListedOutcome(editListedPrompt(root, listWideScope(root), position,
|
|
246
|
+
sayListedOutcome(editListedPrompt(root, listWideScope(root), position, text, images), position);
|
|
241
247
|
return;
|
|
242
248
|
}
|
|
243
249
|
const target = role ?? DIRECTOR_ROLE;
|
|
244
|
-
sayPromptOutcome(position, target, runRolePromptCommand(root, target, position, (r, role, position) => editRolePrompt(r, role, position,
|
|
250
|
+
sayPromptOutcome(position, target, runRolePromptCommand(root, target, position, (r, role, position) => editRolePrompt(r, role, position, text, images)), false);
|
|
251
|
+
}
|
|
252
|
+
/** `--edit <n>` mode — replace a queued prompt's body by position: read the `--attach` image
|
|
253
|
+
* list through the gate, then the editor for a bare position or the typed/`--file` text,
|
|
254
|
+
* list-wide or `--role`-scoped. The three phases live in `readEditAttachments`,
|
|
255
|
+
* `editBareQueuedPrompt` and `editTypedQueuedPrompt`, so each `--edit` job is found by name. */
|
|
256
|
+
function runPromptEdit(root, position, text, attachPaths, role) {
|
|
257
|
+
const images = readEditAttachments(root, position, attachPaths, role);
|
|
258
|
+
if (text === null) {
|
|
259
|
+
editBareQueuedPrompt(root, position, role, images);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
editTypedQueuedPrompt(root, position, text, role, images);
|
|
245
263
|
}
|
|
246
264
|
/** `--top <n>` mode — move a queued prompt to the head of its queue by position, list-wide or
|
|
247
265
|
* `--role`-scoped. One helper so `--top`'s rules are found by name. */
|
|
@@ -283,8 +301,7 @@ function runPromptSubmit(root, text, atDelayMs, attachPaths, role) {
|
|
|
283
301
|
// `--attach <path>`: read and validate through the shared helper (a bad path fails by name
|
|
284
302
|
// before anything is queued). An attachment the target role cannot read is refused here by
|
|
285
303
|
// name — the same gate submitRolePrompt enforces, surfaced early so the CLI exits non-zero
|
|
286
|
-
// with the fix before anything is written or queued
|
|
287
|
-
// not refused, but the operator is told where the prompt will actually run.
|
|
304
|
+
// with the fix before anything is written or queued.
|
|
288
305
|
const images = attachPaths.length > 0 ? readPromptImages(attachPaths) : undefined;
|
|
289
306
|
if (images && images.length > 0) {
|
|
290
307
|
runPromptImageGate(root, target);
|
package/dist/src/init/init.js
CHANGED
|
@@ -234,26 +234,8 @@ async function resolveInitPlan(root, briefText, branch, opts) {
|
|
|
234
234
|
const preferredBranch = await resolveInitPreferredBranch(root, branch, wasRepo);
|
|
235
235
|
return { dryRun, tpl, combinedPrompt, brief, readmeExists, adopted, wasRepo, preferredBranch };
|
|
236
236
|
}
|
|
237
|
-
/**
|
|
238
|
-
|
|
239
|
-
* bookkeeping locally; performs no validation and never refuses. Returns everything the
|
|
240
|
-
* composed `InitResult` needs except `adopted`, `dryRun` and `template`, which `initProject`
|
|
241
|
-
* reads off `plan`. */
|
|
242
|
-
async function seedProject(root, plan) {
|
|
243
|
-
const { dryRun, tpl, combinedPrompt, brief, readmeExists, adopted, wasRepo, preferredBranch } = plan;
|
|
244
|
-
let repoInitialized = false;
|
|
245
|
-
let createdBranch;
|
|
246
|
-
if (!wasRepo) {
|
|
247
|
-
if (!dryRun)
|
|
248
|
-
await git(root, "init", "-b", preferredBranch);
|
|
249
|
-
repoInitialized = true;
|
|
250
|
-
createdBranch = preferredBranch;
|
|
251
|
-
}
|
|
252
|
-
// Decided before any project file is written (init only seeds bootstrap on a fresh project).
|
|
253
|
-
// A repo the `git init` above just created (or a dry run would) has no commits, and init
|
|
254
|
-
// itself commits nothing before the end, so this one answer also picks the commit message.
|
|
255
|
-
const withCommits = wasRepo && (await hasCommits(root));
|
|
256
|
-
const fresh = await isFreshProject(root, withCommits);
|
|
237
|
+
/** The `created`/`leftAlone` arrays and the `claim`/`write` closures above them. */
|
|
238
|
+
function seedWriters(root, dryRun) {
|
|
257
239
|
const created = [];
|
|
258
240
|
const leftAlone = [];
|
|
259
241
|
/** Record a template path this run either created (via `make`, under a real run only) or
|
|
@@ -275,33 +257,64 @@ async function seedProject(root, plan) {
|
|
|
275
257
|
// because claim's existsSync would then treat the torn file as complete on a re-run and
|
|
276
258
|
// never repair it.
|
|
277
259
|
const write = (name, content) => claim(name, path.join(root, name), () => writeTextAtomic(path.join(root, name), content));
|
|
260
|
+
return { created, leftAlone, claim, write };
|
|
261
|
+
}
|
|
262
|
+
/** The new-repo `git init` job: seed the preferred branch unless the run is a dry run. */
|
|
263
|
+
async function initSeedRepo(root, plan) {
|
|
264
|
+
let repoInitialized = false;
|
|
265
|
+
let createdBranch;
|
|
266
|
+
if (!plan.wasRepo) {
|
|
267
|
+
if (!plan.dryRun)
|
|
268
|
+
await git(root, "init", "-b", plan.preferredBranch);
|
|
269
|
+
repoInitialized = true;
|
|
270
|
+
createdBranch = plan.preferredBranch;
|
|
271
|
+
}
|
|
272
|
+
return { repoInitialized, createdBranch };
|
|
273
|
+
}
|
|
274
|
+
/** Whether `root` is a fresh project and the repo has commits — decided before any project
|
|
275
|
+
* file is written (init only seeds bootstrap on a fresh project). A repo the `git init`
|
|
276
|
+
* above just created (or a dry run would) has no commits, and init itself commits nothing
|
|
277
|
+
* before the end, so this one answer also picks the commit message. */
|
|
278
|
+
async function resolveSeedFreshness(root, plan) {
|
|
279
|
+
const withCommits = plan.wasRepo && (await hasCommits(root));
|
|
280
|
+
const fresh = await isFreshProject(root, withCommits);
|
|
281
|
+
return { withCommits, fresh };
|
|
282
|
+
}
|
|
283
|
+
/** Where the brief goes: the existing brief file, TUMWATER.md (adopted), or README.md. */
|
|
284
|
+
function seedBriefFile(root, plan, writers) {
|
|
278
285
|
// README.md is the brief only when none exists yet and there is no README to adopt (a fresh
|
|
279
286
|
// repo): with TUMWATER.md owning the managed sections, a created README.md would carry a
|
|
280
287
|
// duplicate prompt + status block that readBriefText never reaches and the readme role
|
|
281
288
|
// never maintains.
|
|
282
289
|
const name = projectName(root);
|
|
283
|
-
if (brief !== null)
|
|
284
|
-
leftAlone.push(brief);
|
|
285
|
-
else if (adopted) {
|
|
286
|
-
if (readmeExists)
|
|
287
|
-
leftAlone.push("README.md");
|
|
288
|
-
write("TUMWATER.md", briefTemplate(name, combinedPrompt));
|
|
290
|
+
if (plan.brief !== null)
|
|
291
|
+
writers.leftAlone.push(plan.brief);
|
|
292
|
+
else if (plan.adopted) {
|
|
293
|
+
if (plan.readmeExists)
|
|
294
|
+
writers.leftAlone.push("README.md");
|
|
295
|
+
writers.write("TUMWATER.md", briefTemplate(name, plan.combinedPrompt));
|
|
289
296
|
}
|
|
290
297
|
else
|
|
291
|
-
write("README.md", readmeTemplate(name, combinedPrompt));
|
|
298
|
+
writers.write("README.md", readmeTemplate(name, plan.combinedPrompt));
|
|
299
|
+
}
|
|
300
|
+
/** The template backlog files: PLANS/BUGS/QUESTIONS/PRINCIPLES, with starter-plans subst. */
|
|
301
|
+
function seedBacklogFiles(tpl, writers) {
|
|
292
302
|
// A template with starter plans seeds the backlog with them, rendered as `### ` entries —
|
|
293
303
|
// the primary entry format every backlog reader parses — so the team's first runs land on
|
|
294
304
|
// real work. blank keeps the `_None yet._` placeholder byte-identical to today.
|
|
295
305
|
const plansContent = tpl.starterPlans.length > 0
|
|
296
306
|
? PLANS_TEMPLATE.replace("_None yet._", tpl.starterPlans.map((p) => `### ${p}`).join("\n\n"))
|
|
297
307
|
: PLANS_TEMPLATE;
|
|
298
|
-
write("PLANS.md", plansContent);
|
|
299
|
-
write("BUGS.md", BUGS_TEMPLATE);
|
|
300
|
-
write("QUESTIONS.md", QUESTIONS_TEMPLATE);
|
|
301
|
-
write("PRINCIPLES.md", PRINCIPLES_TEMPLATE);
|
|
308
|
+
writers.write("PLANS.md", plansContent);
|
|
309
|
+
writers.write("BUGS.md", BUGS_TEMPLATE);
|
|
310
|
+
writers.write("QUESTIONS.md", QUESTIONS_TEMPLATE);
|
|
311
|
+
writers.write("PRINCIPLES.md", PRINCIPLES_TEMPLATE);
|
|
312
|
+
}
|
|
313
|
+
/** The config seed and its fresh-project bootstrap; returns the seeded bootstrap count. */
|
|
314
|
+
function seedConfigFile(root, fresh, dryRun, writers) {
|
|
302
315
|
let bootstrap = null;
|
|
303
316
|
if (fs.existsSync(configPath(root)))
|
|
304
|
-
leftAlone.push(CONFIG_BASENAME);
|
|
317
|
+
writers.leftAlone.push(CONFIG_BASENAME);
|
|
305
318
|
else {
|
|
306
319
|
const seed = seedConfig(root);
|
|
307
320
|
if (fresh) {
|
|
@@ -310,26 +323,36 @@ async function seedProject(root, plan) {
|
|
|
310
323
|
}
|
|
311
324
|
if (!dryRun)
|
|
312
325
|
saveConfig(root, seed);
|
|
313
|
-
created.push(CONFIG_BASENAME);
|
|
326
|
+
writers.created.push(CONFIG_BASENAME);
|
|
314
327
|
}
|
|
328
|
+
return bootstrap;
|
|
329
|
+
}
|
|
330
|
+
/** The .gitignore edit: gain the missing lines, or record it as left alone. */
|
|
331
|
+
function seedGitignore(root, dryRun, writers) {
|
|
315
332
|
if (ensureGitignore(root, dryRun))
|
|
316
|
-
created.push(".gitignore");
|
|
333
|
+
writers.created.push(".gitignore");
|
|
317
334
|
else if (fs.existsSync(path.join(root, ".gitignore")))
|
|
318
|
-
leftAlone.push(".gitignore");
|
|
335
|
+
writers.leftAlone.push(".gitignore");
|
|
336
|
+
}
|
|
337
|
+
/** The template's starter directories — empty ones only, before the first run. */
|
|
338
|
+
function seedStarterDirs(root, tpl, writers) {
|
|
319
339
|
// The template's starter directories: empty ones only, before the first run — the team's
|
|
320
340
|
// own runs write any code. Existing directories are left alone, like existing files above.
|
|
321
341
|
for (const dir of tpl.starterDirs) {
|
|
322
|
-
claim(dir, path.join(root, dir), () => ensureDir(path.join(root, dir)));
|
|
342
|
+
writers.claim(dir, path.join(root, dir), () => ensureDir(path.join(root, dir)));
|
|
323
343
|
}
|
|
344
|
+
}
|
|
345
|
+
/** What gets committed: the committable filter and the `git add` / staged-check / commit. */
|
|
346
|
+
async function commitSeeded(root, plan, withCommits, writers) {
|
|
324
347
|
// The config stays out of the commit pathspec: `git add -- tumwater.json` fails on a path
|
|
325
348
|
// the just-written .gitignore ignores (plans/portability.md §4a/7). It still heads the
|
|
326
349
|
// `created …` line the CLI prints — the file WAS created, it just must not be tracked. When
|
|
327
350
|
// that leaves the pathspec empty (`git add --` with no pathspec exits 1), there is nothing
|
|
328
351
|
// to commit: a repo that only gained a config reports it and stays uncommitted.
|
|
329
|
-
const dirSet = new Set(tpl.starterDirs);
|
|
330
|
-
const committable = created.filter((name) => name !== CONFIG_BASENAME && !dirSet.has(name));
|
|
352
|
+
const dirSet = new Set(plan.tpl.starterDirs);
|
|
353
|
+
const committable = writers.created.filter((name) => name !== CONFIG_BASENAME && !dirSet.has(name));
|
|
331
354
|
let committed = false;
|
|
332
|
-
if (!dryRun && committable.length > 0) {
|
|
355
|
+
if (!plan.dryRun && committable.length > 0) {
|
|
333
356
|
await git(root, "add", "--", ...committable);
|
|
334
357
|
const staged = await gitTry(root, "diff", "--cached", "--quiet");
|
|
335
358
|
if (staged === null) {
|
|
@@ -339,7 +362,32 @@ async function seedProject(root, plan) {
|
|
|
339
362
|
committed = true;
|
|
340
363
|
}
|
|
341
364
|
}
|
|
342
|
-
return
|
|
365
|
+
return committed;
|
|
366
|
+
}
|
|
367
|
+
/** Seed `root` from a resolved `InitPlan`: git init (a new repo only), every absent project
|
|
368
|
+
* file and starter directory, the config, and the commit. Holds the `created`/`leftAlone`
|
|
369
|
+
* bookkeeping locally; performs no validation and never refuses. Returns everything the
|
|
370
|
+
* composed `InitResult` needs except `adopted`, `dryRun` and `template`, which `initProject`
|
|
371
|
+
* reads off `plan`. */
|
|
372
|
+
async function seedProject(root, plan) {
|
|
373
|
+
const { dryRun, tpl } = plan;
|
|
374
|
+
const { repoInitialized, createdBranch } = await initSeedRepo(root, plan);
|
|
375
|
+
const { withCommits, fresh } = await resolveSeedFreshness(root, plan);
|
|
376
|
+
const writers = seedWriters(root, dryRun);
|
|
377
|
+
seedBriefFile(root, plan, writers);
|
|
378
|
+
seedBacklogFiles(tpl, writers);
|
|
379
|
+
const bootstrap = seedConfigFile(root, fresh, dryRun, writers);
|
|
380
|
+
seedGitignore(root, dryRun, writers);
|
|
381
|
+
seedStarterDirs(root, tpl, writers);
|
|
382
|
+
const committed = await commitSeeded(root, plan, withCommits, writers);
|
|
383
|
+
return {
|
|
384
|
+
created: writers.created,
|
|
385
|
+
leftAlone: writers.leftAlone,
|
|
386
|
+
committed,
|
|
387
|
+
repoInitialized,
|
|
388
|
+
branch: createdBranch,
|
|
389
|
+
bootstrap,
|
|
390
|
+
};
|
|
343
391
|
}
|
|
344
392
|
/** Initialize a repo for tumwater: README (with prompt + status) when no project brief exists
|
|
345
393
|
* yet, PLANS, BUGS, QUESTIONS, PRINCIPLES, tumwater.json, .gitignore — then commit whatever was
|
|
@@ -314,7 +314,7 @@ export async function resolveBacklogInsertConflicts(wt, files) {
|
|
|
314
314
|
}
|
|
315
315
|
// Atomic (tmp + rename): a merge process killed mid-write (SIGKILL, the timeout
|
|
316
316
|
// group-kill) must leave either the conflicted file or the fully resolved one, never a
|
|
317
|
-
// truncated backlog — the CLI's backlog writers (backlog-write.ts, question-answer.ts)
|
|
317
|
+
// truncated backlog — the CLI's backlog writers (backlog-write.ts, backlog-question-answer.ts)
|
|
318
318
|
// already write these unbounded-history files through this helper.
|
|
319
319
|
writeTextAtomic(p, resolved);
|
|
320
320
|
await git(wt, "add", "--", file);
|
|
@@ -11,7 +11,7 @@ import { reviewAheadOfMain } from "../review/review.js";
|
|
|
11
11
|
import { reviewRunConfig, modelSelectorField } from "../config/config-views.js";
|
|
12
12
|
import { warnEventBestEffort, logEventBestEffort } from "../events/events.js";
|
|
13
13
|
import { saveRunnerStateBestEffort } from "../runner/runner-state.js";
|
|
14
|
-
import { REVISION_FINAL_ON_REPEAT, REVISION_BACKSTOP, holdEntryAfterExhaustion } from "../runner/revision.js";
|
|
14
|
+
import { REVISION_FINAL_ON_REPEAT, REVISION_BACKSTOP, holdEntryAfterExhaustion } from "../runner/runner-revision.js";
|
|
15
15
|
import { priorObjectionNumber } from "../review/review-verdict.js";
|
|
16
16
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
17
17
|
import { shortSha } from "../text/format.js";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { mergeApproved } from "./merge-batch.js";
|
|
2
2
|
import { removeMergeChange, resolveWaitStage, setMergeChangeStatus } from "./merge-slot.js";
|
|
3
|
-
import {
|
|
4
|
-
import { withProviderLease } from "../concurrency/provider-lease.js";
|
|
3
|
+
import { withRunPermit } from "../concurrency/run-permit.js";
|
|
5
4
|
import { dropMerge, entryRevisionFields, queuedMergeFiles } from "./merge-queue.js";
|
|
6
5
|
import { errorMessage } from "../text/text.js";
|
|
7
6
|
import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
@@ -123,22 +122,20 @@ function startMerge(ctx, p, picks) {
|
|
|
123
122
|
runPi: async (w, prompt, s, cfg) => {
|
|
124
123
|
// A parked resolver names its wait (resolve-wait, then resolving once granted) the way
|
|
125
124
|
// a parked check does (withCheckPermit): on 2026-10-12 one waited 2.5 min for a permit
|
|
126
|
-
// with the merge queue behind it, reading as "merging". The
|
|
127
|
-
//
|
|
128
|
-
// counts against the strong tier's provider cap like any other strong-tier run
|
|
125
|
+
// with the merge queue behind it, reading as "merging". The resolution waits on the
|
|
126
|
+
// one team total (maxConcurrent) like any other MERGE_TIER run
|
|
129
127
|
// (plans/provider-concurrency.md §4). canLeaseAtOnce is acquireLease's own fast path
|
|
130
128
|
// decided synchronously, so a free permit shows no wait; MERGE_TIER ignores the
|
|
131
129
|
// maintenance reserve.
|
|
132
|
-
const key = cfg ? seamConcurrencyKey(cfg, ctx.piDefault) : undefined;
|
|
133
130
|
const hooks = resolveWaitStage(root, v.entry.role);
|
|
134
|
-
if (!semaphore.canLeaseAtOnce(MERGE_TIER
|
|
131
|
+
if (!semaphore.canLeaseAtOnce(MERGE_TIER))
|
|
135
132
|
hooks.waiting();
|
|
136
|
-
const lease = await semaphore.acquireLease(MERGE_TIER
|
|
133
|
+
const lease = await semaphore.acquireLease(MERGE_TIER);
|
|
137
134
|
hooks.granted();
|
|
138
135
|
try {
|
|
139
|
-
// The resolver's lease rides in the
|
|
140
|
-
//
|
|
141
|
-
return await
|
|
136
|
+
// The resolver's lease rides in the run-permit store for its run, so an idle
|
|
137
|
+
// pause inside it (a failed review's in-place retry) re-takes the same permit.
|
|
138
|
+
return await withRunPermit(lease, () => v.author.runMergePi(w, prompt, s, cfg));
|
|
142
139
|
}
|
|
143
140
|
finally {
|
|
144
141
|
lease.release();
|
|
@@ -43,15 +43,14 @@ export function guardMergeTask(root, role, promise) {
|
|
|
43
43
|
warnEventBestEffort(root, TUMWATER_SOURCE, `merge task for ${role} could not settle: ${errorMessage(err)}`);
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
|
-
/** Take a permit at `tier`
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
export async function acquireUnlessAborted(semaphore, tier, signal, key) {
|
|
46
|
+
/** Take a permit at `tier` unless `signal` fires first: resolves to its release, or to null
|
|
47
|
+
* when the signal won — a grant that arrives after that is handed straight back (a hop, never a
|
|
48
|
+
* leak), so a parked verification that a shutdown settles holds nothing. A verification's
|
|
49
|
+
* permit is the one team total (`maxConcurrent`). */
|
|
50
|
+
export async function acquireUnlessAborted(semaphore, tier, signal) {
|
|
52
51
|
if (signal.aborted)
|
|
53
52
|
return null;
|
|
54
|
-
const granted = semaphore.acquireLease(tier
|
|
53
|
+
const granted = semaphore.acquireLease(tier);
|
|
55
54
|
const stopped = new Promise((resolve) => signal.addEventListener("abort", () => resolve(false), { once: true }));
|
|
56
55
|
if (await Promise.race([granted, stopped])) {
|
|
57
56
|
return granted;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { reviewRunConfig, seamConcurrencyKey } from "../config/config-views.js";
|
|
1
|
+
import { withRunPermit } from "../concurrency/run-permit.js";
|
|
3
2
|
import { subscribeEvents, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
4
3
|
import { deleteRef, headOf, subjectOfCommit } from "../git/git.js";
|
|
5
4
|
import { ensureDetachedWorktree } from "../git/worktree.js";
|
|
@@ -35,12 +34,11 @@ function startPostReview(ctx, p, entry) {
|
|
|
35
34
|
const task = { promise: Promise.resolve(), controller: new AbortController(), roles: [], userAborted: false, parked: true };
|
|
36
35
|
const detach = abortOnShutdown(ctx.signal, task.controller);
|
|
37
36
|
const author = resolveAuthor(ctx, entry.role);
|
|
38
|
-
const key = seamConcurrencyKey(reviewRunConfig(ctx.liveConfig), ctx.piDefault);
|
|
39
37
|
const run = (async () => {
|
|
40
38
|
try {
|
|
41
|
-
if (!semaphore.canLeaseAtOnce(VERIFY_TIER
|
|
39
|
+
if (!semaphore.canLeaseAtOnce(VERIFY_TIER))
|
|
42
40
|
return; // permits are busy; try again next poll
|
|
43
|
-
const lease = await acquireUnlessAborted(semaphore, VERIFY_TIER, task.controller.signal
|
|
41
|
+
const lease = await acquireUnlessAborted(semaphore, VERIFY_TIER, task.controller.signal);
|
|
44
42
|
task.parked = false;
|
|
45
43
|
if (lease === null)
|
|
46
44
|
return;
|
|
@@ -125,19 +123,18 @@ async function fileDroppedReview(ctx, entry, failure) {
|
|
|
125
123
|
* is never a throw. */
|
|
126
124
|
async function mergeReviewerFix(ctx, entry, fix) {
|
|
127
125
|
const author = resolveAuthor(ctx, entry.role);
|
|
128
|
-
// The fix merge's conflict resolver rides the same
|
|
129
|
-
// does (merge-drain.ts startMerge): the MERGE_TIER lease is
|
|
130
|
-
//
|
|
131
|
-
//
|
|
126
|
+
// The fix merge's conflict resolver rides the same team permit the merge slot's resolver
|
|
127
|
+
// does (merge-drain.ts startMerge): the MERGE_TIER lease is acquired on the one team total
|
|
128
|
+
// and rides in the run-permit store, so a conflict resolution counts against maxConcurrent
|
|
129
|
+
// like any other strong-tier run.
|
|
132
130
|
const runPi = async (wt, prompt, sessionName, config) => {
|
|
133
|
-
const key = config ? seamConcurrencyKey(config, ctx.piDefault) : undefined;
|
|
134
131
|
const hooks = resolveWaitStage(ctx.root, entry.role);
|
|
135
|
-
if (!ctx.semaphore.canLeaseAtOnce(MERGE_TIER
|
|
132
|
+
if (!ctx.semaphore.canLeaseAtOnce(MERGE_TIER))
|
|
136
133
|
hooks.waiting();
|
|
137
|
-
const lease = await ctx.semaphore.acquireLease(MERGE_TIER
|
|
134
|
+
const lease = await ctx.semaphore.acquireLease(MERGE_TIER);
|
|
138
135
|
hooks.granted();
|
|
139
136
|
try {
|
|
140
|
-
return await
|
|
137
|
+
return await withRunPermit(lease, () => author.runMergePi(wt, prompt, sessionName, config));
|
|
141
138
|
}
|
|
142
139
|
finally {
|
|
143
140
|
lease.release();
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* retriable result) or once main's head stops being red (a `main_red` pin). */
|
|
7
7
|
import { enqueueMerge, queuedMerges } from "./merge-queue.js";
|
|
8
8
|
import { RETRIABLE_MERGE_RESULTS } from "./merge-core.js";
|
|
9
|
-
import { pinnedRetrySha } from "../runner/leftover.js";
|
|
9
|
+
import { pinnedRetrySha } from "../runner/runner-leftover.js";
|
|
10
10
|
import { branchHead } from "../git/git.js";
|
|
11
11
|
import { cachedBaselineVerdict } from "../baseline/main-baseline.js";
|
|
12
12
|
import { saveRunnerStateBestEffort } from "../runner/runner-state.js";
|
|
@@ -192,14 +192,6 @@ export function checkWaitStage(root, roles) {
|
|
|
192
192
|
export function resolveWaitStage(root, role) {
|
|
193
193
|
return waitStageHooks(root, [role], "resolve-wait", "resolving");
|
|
194
194
|
}
|
|
195
|
-
/** The verification's hooks around a re-key onto a full provider (plans/provider-concurrency.md §5,
|
|
196
|
-
* "A re-key wait is visible"): `provider-wait` while the verification's lease parks on the target
|
|
197
|
-
* provider's full cap, `reviewing` once the key frees and the review proceeds. resolveWaitStage's
|
|
198
|
-
* shape for the re-key path — a verification that normally shares its review seam never
|
|
199
|
-
* re-keys, so the stage fires only when a run actually moves to a different, full provider. */
|
|
200
|
-
export function providerWaitStage(root, role) {
|
|
201
|
-
return waitStageHooks(root, [role], "provider-wait", "reviewing");
|
|
202
|
-
}
|
|
203
195
|
/** The marker's per-change records, whichever shape it has: `changes` as written, or — for a
|
|
204
196
|
* marker an older generation wrote with one top-level change — that change, `merging` since the
|
|
205
197
|
* marker's start at the marker's stage. Reads both shapes into records, so addMergeChange's
|