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
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
2
|
-
import { changeBaseRev, headOf
|
|
2
|
+
import { changeBaseRev, headOf } from "../git/git.js";
|
|
3
3
|
import { aheadOfMainFiles } from "../git/git-diff.js";
|
|
4
4
|
import { resetWorktreeToMain } from "../git/worktree.js";
|
|
5
5
|
import { recordReview } from "../run/run-apply.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { sleepInterruptible } from "../run/run-timing.js";
|
|
7
|
+
import { parseNonNegativeInt } from "../text/text.js";
|
|
8
8
|
import { firstReason } from "../text/phrases.js";
|
|
9
9
|
import { isExemptDiff } from "./exemptions.js";
|
|
10
|
-
import {
|
|
11
|
-
import { setMergeStage } from "../merge/merge-slot.js";
|
|
10
|
+
import { idleWithoutPermit } from "../concurrency/run-permit.js";
|
|
12
11
|
import { gateBuildPrecheck } from "./review-precheck.js";
|
|
13
12
|
import { diffRuleReasons } from "./diff-rules.js";
|
|
14
|
-
export { REVIEW_FAILURE_LIMIT } from "./review-run.js";
|
|
15
|
-
import { restoreReviewedHead, runReviewer, withRuns } from "./review-run.js";
|
|
13
|
+
export { REVIEW_FAILURE_LIMIT, restoreReviewedHead, runReviewer, withRuns } from "./review-run.js";
|
|
16
14
|
/** In-place retries of a failed reviewer run within one verify, after the first run. */
|
|
17
15
|
const REVIEW_RETRIES = 2;
|
|
18
16
|
/** The pause before each retry (ReviewContext.retryDelayMs overrides it in tests). */
|
|
@@ -23,89 +21,40 @@ const REVIEW_RETRY_DELAY_ENV = "TUMWATER_REVIEW_RETRY_DELAY_MS";
|
|
|
23
21
|
/** The pause before each retry: the context override wins; else the tunable env knob read through
|
|
24
22
|
* the shared plain-decimal rule (parseNonNegativeInt — a 0 ms delay, the test-suite's setting, is
|
|
25
23
|
* valid). A malformed spelling falls back to REVIEW_RETRY_DELAY_MS rather than silently becoming
|
|
26
|
-
* NaN: `
|
|
24
|
+
* NaN: `sleepInterruptible(NaN)` resolves at ~0 ms and the retries would fire back-to-back instead
|
|
27
25
|
* of 30 s apart. */
|
|
28
26
|
export function reviewRetryDelayMs(ctxRetryDelayMs) {
|
|
29
27
|
const raw = process.env[REVIEW_RETRY_DELAY_ENV];
|
|
30
28
|
return ctxRetryDelayMs ?? (raw === undefined ? REVIEW_RETRY_DELAY_MS : (parseNonNegativeInt(raw) ?? REVIEW_RETRY_DELAY_MS));
|
|
31
29
|
}
|
|
32
|
-
/**
|
|
33
|
-
* right now, or the lease failed (the check then runs first, in the change's own slot). */
|
|
34
|
-
async function leaseCheckSlot(ctx, head) {
|
|
35
|
-
if (!ctx.leaseCheckSlot)
|
|
36
|
-
return null;
|
|
37
|
-
try {
|
|
38
|
-
return await ctx.leaseCheckSlot(head);
|
|
39
|
-
}
|
|
40
|
-
catch (err) {
|
|
41
|
-
warnEventBestEffort(ctx.root, ctx.role, `no second workspace for the gate check (${errorMessage(err)}); running it before the review`);
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
/** Start the gate check in `lease`'s checkout of `head`. A rejection is recorded, not applied:
|
|
46
|
-
* resetting the change's branch is left to reviewAttempt, after the reviewer has stopped. */
|
|
47
|
-
function startOverlappedCheck(ctx, state, head, lease) {
|
|
48
|
-
const overlap = { abort: new AbortController(), done: false };
|
|
49
|
-
const reject = async (reasons) => {
|
|
50
|
-
overlap.rejectedReasons = reasons;
|
|
51
|
-
return { decision: "rejected", detail: firstReason(reasons) };
|
|
52
|
-
};
|
|
53
|
-
overlap.gate = gateBuildPrecheck({ ...ctx, wt: lease.dir }, state, head, reject)
|
|
54
|
-
.then((pre) => {
|
|
55
|
-
overlap.done = true;
|
|
56
|
-
if (pre.resolved)
|
|
57
|
-
overlap.abort.abort();
|
|
58
|
-
else
|
|
59
|
-
setMergeStage(ctx.root, ctx.role, "reviewing");
|
|
60
|
-
return pre;
|
|
61
|
-
}, (err) => {
|
|
62
|
-
overlap.done = true;
|
|
63
|
-
overlap.abort.abort();
|
|
64
|
-
throw err;
|
|
65
|
-
})
|
|
66
|
-
.finally(() => lease.release());
|
|
67
|
-
overlap.gate.catch(() => { }); // surfaced where reviewAttempt awaits it
|
|
68
|
-
return overlap;
|
|
69
|
-
}
|
|
70
|
-
/** Run the adversarial review gate over everything ahead of main in `wt` and update `state`
|
|
30
|
+
/** Run the deterministic review gate over everything ahead of main in `wt` and update `state`
|
|
71
31
|
* accordingly. Shared by the run path (after commitAll) and leftover recovery
|
|
72
|
-
* (leftover.ts's recoverLeftover) — every path
|
|
32
|
+
* (runner-leftover.ts's recoverLeftover) — every path
|
|
73
33
|
* that can move a commit into main routes through here, so no crash or abort path smuggles
|
|
74
34
|
* unreviewed work in:
|
|
75
35
|
* - gate disabled, or an already-approved HEAD, or an exempt (doc-only) diff → merge as-is;
|
|
76
|
-
* -
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* working-tree edits (its only output channel is the verdict);
|
|
36
|
+
* - approve → a passing build pre-check records lastApprovedHead and approves, with no
|
|
37
|
+
* reviewer run; every merged change is judged once, after it lands, by the post-landing
|
|
38
|
+
* review (part 3g/3, reviewMergedCommit, run by the merge pipeline's post-review drain);
|
|
80
39
|
* - reject → reset the branch to main, record reasons in state.lastReview (injected into the
|
|
81
40
|
* role's next run prompt), log review_rejected;
|
|
82
|
-
* - fail with an unparseable reply (the reviewer ran to completion but emitted no VERDICT) →
|
|
83
|
-
* one capped follow-up turn on the reviewer's own session asks for the missing line
|
|
84
|
-
* (mirroring requestSummary — BUGS.md 2026-09-29); only a follow-up that also yields
|
|
85
|
-
* nothing counts the strike. Leave the commit on the branch for the next run's re-review,
|
|
86
|
-
* counting consecutive failures per HEAD; past REVIEW_FAILURE_LIMIT discard the leftover
|
|
87
|
-
* with a warning.
|
|
88
|
-
* - fail because the run itself failed (transport/spawn/timeout, pi.ok false) → leave the
|
|
89
|
-
* commit and do NOT advance the count: the reviewer never judged the diff, so the failure
|
|
90
|
-
* is evidence about the backend, never about the commit (BUGS.md 2026-09-20).
|
|
91
41
|
* - the declared check fails, and fails again on its one re-run → reject with the check's
|
|
92
|
-
* reasons
|
|
93
|
-
*
|
|
42
|
+
* reasons when main's own tip is green (or has no verdict); when main is red too, fail
|
|
43
|
+
* without advancing the count and leave the commit — the failure is main's. Never throws.
|
|
94
44
|
* `highFriction` marks a change whose authoring run burned more than the configured turn/time
|
|
95
|
-
* thresholds (plans/refusal-and-thrash.md):
|
|
96
|
-
* reviewer
|
|
97
|
-
async function reviewAttempt(ctx, state,
|
|
45
|
+
* thresholds (plans/refusal-and-thrash.md): kept for the caller's convenience while the
|
|
46
|
+
* reviewer's remaining seams stay in review-run.ts/review-followup.ts. */
|
|
47
|
+
async function reviewAttempt(ctx, state, _summary, _commitBody, _highFriction, retry) {
|
|
98
48
|
const { root, role, wt, mainBranch, config } = ctx;
|
|
99
49
|
if (!config.review.enabled) {
|
|
100
50
|
state.reviewResume = undefined;
|
|
101
51
|
return { decision: "exempt" };
|
|
102
52
|
}
|
|
103
53
|
const head = await headOf(wt, "HEAD");
|
|
104
|
-
// The one reject path: every rejection of this HEAD — deterministic build-check
|
|
105
|
-
//
|
|
54
|
+
// The one reject path: every rejection of this HEAD — the deterministic build-check
|
|
55
|
+
// outcome — shares this bookkeeping. Record lastReview (injected into the role's next
|
|
106
56
|
// run prompt), reset the failure count (a verdict about this HEAD is a successful review
|
|
107
57
|
// either way), discard the branch by resetting it to main, and log review_rejected.
|
|
108
|
-
// Callers attach `run` when a pi run was consumed.
|
|
109
58
|
const reject = async (reasons, durationMs) => {
|
|
110
59
|
recordReview(state, "reject", reasons, head);
|
|
111
60
|
state.reviewResume = undefined;
|
|
@@ -120,22 +69,6 @@ async function reviewAttempt(ctx, state, summary, commitBody, highFriction, retr
|
|
|
120
69
|
});
|
|
121
70
|
return { decision: "rejected", detail: firstReason(reasons) };
|
|
122
71
|
};
|
|
123
|
-
// Every exit below that keeps the change leaves the worktree at the head this gate reviewed.
|
|
124
|
-
const keep = async (r) => {
|
|
125
|
-
await restoreReviewedHead(wt, head, root, role);
|
|
126
|
-
return r;
|
|
127
|
-
};
|
|
128
|
-
// Settle the attempt on an overlapped check that did not pass: the sequential pre-check's own
|
|
129
|
-
// outcome, with the reviewer's run (when one started) dropped and no verdict recorded. A
|
|
130
|
-
// rejection is applied now, after the reviewer stopped touching the tree; every other outcome
|
|
131
|
-
// keeps the change at the head the gate reviewed.
|
|
132
|
-
const settleFailedCheck = async (o, pre, pi) => {
|
|
133
|
-
state.reviewResume = undefined;
|
|
134
|
-
const settled = o.rejectedReasons
|
|
135
|
-
? await reject(o.rejectedReasons)
|
|
136
|
-
: await keep(pre.resolved ?? { decision: "failed", detail: "the gate check did not complete" });
|
|
137
|
-
return pi ? withRuns(settled, pi, null, null) : settled;
|
|
138
|
-
};
|
|
139
72
|
// Already reviewed this exact HEAD (e.g. a merge_blocked retry): do not burn another run.
|
|
140
73
|
if (state.lastApprovedHead === head) {
|
|
141
74
|
state.reviewResume = undefined;
|
|
@@ -164,67 +97,34 @@ async function reviewAttempt(ctx, state, summary, commitBody, highFriction, retr
|
|
|
164
97
|
return { decision: "exempt" };
|
|
165
98
|
}
|
|
166
99
|
// Deterministic build pre-check — after BOTH early returns above (an md-only diff cannot
|
|
167
|
-
// break the build)
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
// A retried attempt keeps the first attempt's passing pre-check (a green, or a red naming
|
|
176
|
-
// only main's known failures): the tree is the same head.
|
|
177
|
-
// A review judges a diff, not a sha: a head carrying exactly the patch this role's last
|
|
178
|
-
// approval judged (the approved head cleanly rebased onto a moved main) reuses that verdict
|
|
179
|
-
// instead of paying a second reviewer run. Only the model review is reused — the pre-check
|
|
180
|
-
// has just run on this tree (a failure rejected it there), and its verifiedHead rides on the
|
|
181
|
-
// result as for any approval.
|
|
182
|
-
const reviewedPatchId = (await patchId(wt, mainBranch, head)) ?? undefined;
|
|
183
|
-
const patchApproved = reviewedPatchId !== undefined && state.approvedPatchIds?.includes(reviewedPatchId) === true;
|
|
184
|
-
// The check runs alongside the reviewer when a second pool slot is free right now (Fast merge
|
|
185
|
-
// part 12): it starts here, in its own checkout of this head, while the reviewer run below
|
|
186
|
-
// starts in the change's own slot. An already-approved patch has no reviewer to overlap, and
|
|
187
|
-
// with no free slot (or no way to lease one) the check runs first, as it always did.
|
|
188
|
-
const afterMerge = reviewedAfterMerge(config, role);
|
|
189
|
-
let overlap;
|
|
100
|
+
// break the build): a deterministic rejection never shows as reviewing on the dashboards.
|
|
101
|
+
// Both gate callers (the run path and runner-leftover.ts's recoverLeftover) get it for free.
|
|
102
|
+
// Its run, flake re-run, and attribution live in review-precheck.ts (the check run itself
|
|
103
|
+
// lives in runScopedBuildCheck, shared with the merge path's in-lock re-check); a resolved
|
|
104
|
+
// result here is returned verbatim and a pass hands the verified head to the merge path below.
|
|
105
|
+
// The pre-check always runs in the change's own slot. A retried attempt keeps the first
|
|
106
|
+
// attempt's passing pre-check (a green, or a red naming only main's known failures): the
|
|
107
|
+
// tree is the same head.
|
|
190
108
|
if (!retry.pre) {
|
|
191
|
-
const
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const pre = await gateBuildPrecheck(ctx, state, head, reject);
|
|
196
|
-
if (pre.resolved) {
|
|
197
|
-
state.reviewResume = undefined;
|
|
198
|
-
return pre.resolved;
|
|
199
|
-
}
|
|
200
|
-
retry.pre = { verifiedHead: pre.verifiedHead, knownMainFailures: pre.knownMainFailures };
|
|
109
|
+
const pre = await gateBuildPrecheck(ctx, state, head, reject);
|
|
110
|
+
if (pre.resolved) {
|
|
111
|
+
state.reviewResume = undefined;
|
|
112
|
+
return pre.resolved;
|
|
201
113
|
}
|
|
114
|
+
retry.pre = { verifiedHead: pre.verifiedHead, knownMainFailures: pre.knownMainFailures };
|
|
202
115
|
}
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
state.reviewResume = undefined;
|
|
209
|
-
state.lastApprovedHead = head;
|
|
210
|
-
return { decision: "approved", verifiedHead: retry.pre.verifiedHead };
|
|
211
|
-
}
|
|
212
|
-
if (patchApproved && retry.pre) {
|
|
213
|
-
state.reviewResume = undefined;
|
|
214
|
-
state.lastApprovedHead = head;
|
|
215
|
-
return { decision: "approved", verifiedHead: retry.pre.verifiedHead };
|
|
216
|
-
}
|
|
217
|
-
return runReviewer(ctx, state, {
|
|
218
|
-
head, overlap, knownMainFailures, retry, summary, commitBody, highFriction,
|
|
219
|
-
reviewedPatchId, reject, keep, settleFailedCheck,
|
|
220
|
-
});
|
|
116
|
+
// A passing pre-check approves: the gate runs no model reviewer, and every merge is judged
|
|
117
|
+
// once, after it lands, by the post-landing review (part 3g/3, reviewMergedCommit).
|
|
118
|
+
state.reviewResume = undefined;
|
|
119
|
+
state.lastApprovedHead = head;
|
|
120
|
+
return { decision: "approved", verifiedHead: retry.pre.verifiedHead };
|
|
221
121
|
}
|
|
222
122
|
/** Review the change ahead of main, retrying a failed reviewer run in place (twice, 30 s apart)
|
|
223
123
|
* before the attempt ends `failed`: a run that timed out, was killed, died in transport or at
|
|
224
124
|
* the provider, or stated no VERDICT after its recovery turn. A resumable session is resumed,
|
|
225
125
|
* otherwise the retry starts fresh (reviewAttempt). A configuration error and an abort are not
|
|
226
126
|
* retried. While the reviewer's provider is held the retry waits for the hold to lift and
|
|
227
|
-
* costs nothing. Each pause runs without the caller's team permit (
|
|
127
|
+
* costs nothing. Each pause runs without the caller's team permit (idleWithoutPermit):
|
|
228
128
|
* a verification hands its lease back while it waits and takes it back before the next attempt.
|
|
229
129
|
* See reviewAttempt for the rest of the gate's contract. */
|
|
230
130
|
export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFriction) {
|
|
@@ -241,10 +141,10 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
241
141
|
warnEventBestEffort(ctx.root, ctx.role, `the reviewer run failed (${result.detail ?? "no detail"}) — ${held ? "waiting for the provider hold to lift, then " : ""}retrying the review in place`);
|
|
242
142
|
// The pause holds no team permit: a verification's lease is handed back while it waits and
|
|
243
143
|
// re-taken before the next attempt, so a reviewer that keeps failing does not starve the roles.
|
|
244
|
-
await
|
|
245
|
-
await
|
|
144
|
+
await idleWithoutPermit(async () => {
|
|
145
|
+
await sleepInterruptible(delayMs, ctx.signal);
|
|
246
146
|
while (ctx.reviewHeld?.() && !ctx.signal?.aborted)
|
|
247
|
-
await
|
|
147
|
+
await sleepInterruptible(Math.min(delayMs, 5000), ctx.signal);
|
|
248
148
|
}, ctx.signal);
|
|
249
149
|
if (ctx.signal?.aborted)
|
|
250
150
|
return { decision: "failed", aborted: true };
|
|
@@ -253,16 +153,3 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
253
153
|
retry.attempt += 1;
|
|
254
154
|
}
|
|
255
155
|
}
|
|
256
|
-
function sleepUnlessAborted(ms, signal) {
|
|
257
|
-
return new Promise((resolve) => {
|
|
258
|
-
if (signal?.aborted)
|
|
259
|
-
return resolve();
|
|
260
|
-
const done = () => {
|
|
261
|
-
clearTimeout(timer);
|
|
262
|
-
signal?.removeEventListener("abort", done);
|
|
263
|
-
resolve();
|
|
264
|
-
};
|
|
265
|
-
const timer = setTimeout(done, ms);
|
|
266
|
-
signal?.addEventListener("abort", done, { once: true });
|
|
267
|
-
});
|
|
268
|
-
}
|
|
@@ -32,47 +32,57 @@ export function secretEnvValues(env) {
|
|
|
32
32
|
}
|
|
33
33
|
return out;
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
/** Push `value` into `out` as a credential named `name`, unless it is not a string, is a `$NAME`
|
|
36
|
+
* interpolation or `!command`, or fails `looksLikeSecret`. */
|
|
37
|
+
function addPiSecret(out, name, value) {
|
|
38
|
+
if (typeof value !== "string" || value.startsWith("$") || value.startsWith("!"))
|
|
39
|
+
return;
|
|
40
|
+
if (looksLikeSecret(value))
|
|
41
|
+
out.push({ name, value });
|
|
42
|
+
}
|
|
43
|
+
function modelsJsonCredentials(modelsPath) {
|
|
40
44
|
const out = [];
|
|
41
|
-
const add = (name, value) => {
|
|
42
|
-
if (typeof value !== "string" || value.startsWith("$") || value.startsWith("!"))
|
|
43
|
-
return;
|
|
44
|
-
if (looksLikeSecret(value))
|
|
45
|
-
out.push({ name, value });
|
|
46
|
-
};
|
|
47
45
|
const models = readJsonFile(modelsPath);
|
|
48
46
|
if (models && isJsonObject(models.providers)) {
|
|
49
47
|
for (const [p, cfg] of Object.entries(models.providers)) {
|
|
50
48
|
if (!isJsonObject(cfg))
|
|
51
49
|
continue;
|
|
52
|
-
|
|
50
|
+
addPiSecret(out, `models.json ${p}.apiKey`, cfg.apiKey);
|
|
53
51
|
if (isJsonObject(cfg.headers)) {
|
|
54
|
-
for (const [h, v] of Object.entries(cfg.headers))
|
|
55
|
-
|
|
52
|
+
for (const [h, v] of Object.entries(cfg.headers)) {
|
|
53
|
+
addPiSecret(out, `models.json ${p}.headers.${h}`, v);
|
|
54
|
+
}
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function authJsonCredentials(modelsPath) {
|
|
61
|
+
const out = [];
|
|
59
62
|
const auth = readJsonFile(path.join(path.dirname(modelsPath), "auth.json"));
|
|
60
63
|
if (auth) {
|
|
61
64
|
for (const [p, cred] of Object.entries(auth)) {
|
|
62
65
|
if (!isJsonObject(cred))
|
|
63
66
|
continue;
|
|
64
67
|
for (const f of ["key", "access", "refresh"])
|
|
65
|
-
|
|
68
|
+
addPiSecret(out, `auth.json ${p}.${f}`, cred[f]);
|
|
66
69
|
if (isJsonObject(cred.env)) {
|
|
67
70
|
for (const [n, v] of Object.entries(cred.env)) {
|
|
68
71
|
if (SECRET_NAME.test(n))
|
|
69
|
-
|
|
72
|
+
addPiSecret(out, `auth.json ${p}.env.${n}`, v);
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
77
|
return out;
|
|
75
78
|
}
|
|
79
|
+
/** The credentials pi stores: literal `apiKey`s and header values in `models.json`, and the
|
|
80
|
+
* `key`/`access`/`refresh` fields (plus secret-named `env` values) of the sibling `auth.json`.
|
|
81
|
+
* `$NAME` interpolations (secretEnvValues covers those) and `!command` values are skipped. A
|
|
82
|
+
* missing or torn file yields nothing; never throws. */
|
|
83
|
+
export function piCredentialValues(modelsPath = piModelsPath()) {
|
|
84
|
+
return [...modelsJsonCredentials(modelsPath), ...authJsonCredentials(modelsPath)];
|
|
85
|
+
}
|
|
76
86
|
/** Every secret the gate knows: secret-named environment values (named `$NAME`) then pi's stored
|
|
77
87
|
* credentials (named `pi <file> <path>`), the first entry winning when two share a value. */
|
|
78
88
|
export function knownSecrets(env = process.env, modelsPath = piModelsPath()) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* in roles.ts. Pure catalog data — the shared instruction fragments role prompts embed live in
|
|
5
5
|
* role-guidance.ts (import-only downward, so prompt assembly depends on both without a cycle).
|
|
6
6
|
* roles.ts re-exports ROLES and Role, so every existing importer keeps its import path. */
|
|
7
|
-
import { backlogMoveGuidance, CHECK_DECLARATION_MARKER, CONVERGENCE_HOLD_MARKER, DECOMPOSITION_GUIDANCE, NEEDS_REVIEW_NOTE, PLAN_QUEUE_GUIDANCE, PLAN_SIZING, searchGuidance, VALIDATION_GAP_GUIDANCE, VALIDATION_GAP_QUERY, VALIDATION_GAP_TALLY, } from "./role-guidance.js";
|
|
7
|
+
import { backlogMoveGuidance, CHECK_DECLARATION_MARKER, CONVERGENCE_HOLD_MARKER, DECOMPOSITION_GUIDANCE, NEEDS_REVIEW_NOTE, PLAN_DEFECT_MARKER, PLAN_DEFECT_NOTE, PLAN_QUEUE_GUIDANCE, PLAN_SIZING, searchGuidance, VALIDATION_GAP_GUIDANCE, VALIDATION_GAP_QUERY, VALIDATION_GAP_TALLY, } from "./role-guidance.js";
|
|
8
8
|
import { roleCommitGrep } from "./legacy-role-ids.js";
|
|
9
9
|
import { NOTHING_TO_DO } from "../verdict/reply-contract.js";
|
|
10
10
|
/** The trailer the readme role's commits carry and its `git log --grep` reads back; git parses
|
|
@@ -50,8 +50,10 @@ export const ROLES = [
|
|
|
50
50
|
6. Mark the plan done in PLANS.md — ${backlogMoveGuidance("PLANS.md", "## Planned", "## Done", "PLANS-DONE.md", "done")}
|
|
51
51
|
7. Verify last, after the PLANS.md edit, so one run of the project's check covers everything
|
|
52
52
|
you changed (see "Leave the project working" below).
|
|
53
|
-
A plan
|
|
54
|
-
|
|
53
|
+
A plan whose approach or criteria are wrong is a finding the plan role owns, not a refusal:
|
|
54
|
+
append ${PLAN_DEFECT_NOTE} under its heading with the objection recorded, skip it, and implement
|
|
55
|
+
the next available plan that fits — do not force it. Reserve **Refused …** for a plan that
|
|
56
|
+
should not be done at all, which is the user's call to decide.`,
|
|
55
57
|
},
|
|
56
58
|
{
|
|
57
59
|
id: "bugfix",
|
|
@@ -113,6 +115,12 @@ within that budget, there is nothing to do.`,
|
|
|
113
115
|
the quoted objections and the code they name and rewrite the plan so an implementation
|
|
114
116
|
following it would answer each — correcting the approach or the anchors, tightening the
|
|
115
117
|
acceptance criteria, or splitting it per PLAN_SIZING — then remove the note.
|
|
118
|
+
- A plan carrying a ${PLAN_DEFECT_MARKER} note (the feature implementer found the plan's
|
|
119
|
+
approach or criteria wrong) also outranks adding another plan: read the objection after the
|
|
120
|
+
marker and the code it names and rewrite the plan so an implementation following it would
|
|
121
|
+
answer it — correcting the approach or the anchors, tightening the acceptance criteria,
|
|
122
|
+
splitting it per PLAN_SIZING, or refusing it with the objection recorded when it should not
|
|
123
|
+
be done at all — then remove the note. That replan is this run's task.
|
|
116
124
|
- A plan carrying a **Needs review …** note that quotes the reviewer's surviving objections
|
|
117
125
|
(a final-round rejection — \`rejected after <N> review rounds\`) also outranks adding another
|
|
118
126
|
plan: read the quoted objections and the code they name, then rewrite the plan so an
|
|
@@ -125,11 +125,6 @@ export const VALIDATION_GAP_QUERY = `grep -snE '\\*\\*Validation gap:\\*\\* (${V
|
|
|
125
125
|
* feature role appends (gate-notes.ts) also begins with this prefix, so it reads as a Needs-review
|
|
126
126
|
* note and is classified by entryHold accordingly. */
|
|
127
127
|
export const NEEDS_REVIEW_PREFIX = `**Needs review `;
|
|
128
|
-
/** The tag text before a held count, read by the backlog (backlog-eligibility's heldCount) so
|
|
129
|
-
* the note form and the parser cannot drift. No writer emits `(held N)` anymore — the feature
|
|
130
|
-
* note is flat — but heldCount still parses the tag from a Needs-review note line already on
|
|
131
|
-
* disk until part 2f/6 removes the reader. */
|
|
132
|
-
export const HELD_TAG_PREFIX = `(held `;
|
|
133
128
|
/** The markdown note the feature role appends to a plan it cannot finish in one run, so the plan
|
|
134
129
|
* role can split it. Mirrors the **Refused …** note convention: a note the next fresh run reads,
|
|
135
130
|
* with no code parsing it. Defined once so the feature and plan texts and the director's routing
|
|
@@ -137,7 +132,7 @@ export const HELD_TAG_PREFIX = `(held `;
|
|
|
137
132
|
* flat, so a new hold never escalates to the operator's desk. */
|
|
138
133
|
export const NEEDS_REVIEW_NOTE = `${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature: too large for one run**`;
|
|
139
134
|
/** The literal reason the automatic convergence hold writes into a held plan's note
|
|
140
|
-
* (holdEntryAfterExhaustion, src/runner/revision.ts, builds the note as
|
|
135
|
+
* (holdEntryAfterExhaustion, src/runner/runner-revision.ts, builds the note as
|
|
141
136
|
* `**Needs review … by <role>: could not converge — <surviving objections>**`, INV-BH4).
|
|
142
137
|
* Exported so the writer and the reader share one literal: it sits between holdEntryAfterExhaustion
|
|
143
138
|
* (which writes the note) and the plan role's charter (role-catalog.ts `find`) which reads it, so
|
|
@@ -146,12 +141,21 @@ export const NEEDS_REVIEW_NOTE = `${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature:
|
|
|
146
141
|
* each hold to its own action without matching on a second literal. Add no second spelling of
|
|
147
142
|
* this text. */
|
|
148
143
|
export const CONVERGENCE_HOLD_MARKER = "could not converge";
|
|
149
|
-
/** The marker
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
|
|
144
|
+
/** The literal marker the feature role writes inside a Needs-review note when a planned entry's
|
|
145
|
+
* approach or acceptance criteria resists implementation, so the plan role re-specifies the plan
|
|
146
|
+
* instead of the refusal landing on the human user's desk. Mirrors the convergence hold: the note
|
|
147
|
+
* reads `**Needs review <YYYY-MM-DD> by feature: plan defect — <objection>**`, carrying this
|
|
148
|
+
* text only in the note body after the prefix, so entryHold classes it as a plain needs-review
|
|
149
|
+
* hold and the feature role (which reads a Needs-review note as a park) skips it. Exported so the
|
|
150
|
+
* feature charter (which tells the feature role to write it) and the plan charter (which reads it
|
|
151
|
+
* to replan) share one literal like the convergence marker does. Add no second spelling of this
|
|
152
|
+
* text. */
|
|
153
|
+
export const PLAN_DEFECT_MARKER = "plan defect";
|
|
154
|
+
/** The template for the replan hand-back note the feature role appends to a planned entry whose
|
|
155
|
+
* approach or criteria are wrong, routing it to the plan role instead of a Refused note. The
|
|
156
|
+
* objection the feature role recorded goes after the marker's dash, so the plan charter can name
|
|
157
|
+
* it (`the objection after the marker`) without parsing the note body itself. */
|
|
158
|
+
export const PLAN_DEFECT_NOTE = `${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature: ${PLAN_DEFECT_MARKER} — <objection>**`;
|
|
155
159
|
/** The literal marker the plan role writes as its own line under the PLANS.md `## Planned` entry
|
|
156
160
|
* that declares this project's deterministic check, and the feature role reads as that entry's
|
|
157
161
|
* identity. It is the one decision point between the two no-check notes (gate-notes.ts
|
|
@@ -133,7 +133,7 @@ function modelLine(p) {
|
|
|
133
133
|
/** A fallback pair as one readable token, agent-qualified exactly like modelLine — a non-pi
|
|
134
134
|
* backend (its `agent`, or a provider that names one, `claude`) is named as the selector's
|
|
135
135
|
* namespace; a pi pair keeps `provider/model`; neither half present falls back to the
|
|
136
|
-
* backend-default label. One home for the
|
|
136
|
+
* backend-default label. One home for the Budget fallback line, so a
|
|
137
137
|
* claude fallback reads `claude/claude-sonnet-5-5`, not a bare id (PLANS.md "Mixed claude +
|
|
138
138
|
* pi fleets, part 3/4" gap 7). */
|
|
139
139
|
function fallbackPairLabel(fb, backendDefault) {
|
|
@@ -254,13 +254,6 @@ export function renderRoleMarkdown(p, now = Date.now()) {
|
|
|
254
254
|
// fallback's own documented state). `unknown` when it cannot be known (a non-pi seam, an
|
|
255
255
|
// unresolvable pair, a model with no declared window).
|
|
256
256
|
lines.push(`- Context window: ${p.contextWindow === null ? "unknown" : `${compactTokens(p.contextWindow)} tokens`}`);
|
|
257
|
-
// Per-provider concurrency cap (plans/provider-concurrency.md §8): name the key whose cap
|
|
258
|
-
// bounds the role's concurrent runs, plus the cap itself. Present only while
|
|
259
|
-
// `maxConcurrentPerProvider` maps this seam's key — a cap-less default team has no
|
|
260
|
-
// resolvable key entry, so its role view renders no new line and stays byte-identical.
|
|
261
|
-
if (p.providerKey !== undefined && p.providerCap !== undefined) {
|
|
262
|
-
lines.push(`- Provider: ${p.providerKey} (cap ${p.providerCap})`);
|
|
263
|
-
}
|
|
264
257
|
// Image input (plans/image-input.md "Surfaces", part 5/5): whether this role's own model
|
|
265
258
|
// reads images, and — when it cannot — where a run that reads an image reroutes. Not just
|
|
266
259
|
// `yes`/`no`: a text-only role that falls back names the target, so `tumwater role <id>`
|
|
@@ -280,12 +273,6 @@ export function renderRoleMarkdown(p, now = Date.now()) {
|
|
|
280
273
|
if (p.reviewedAfterMerge) {
|
|
281
274
|
lines.push(`- Review: after merge (a rejected change stays on main with the reviewer's recorded reasons)`);
|
|
282
275
|
}
|
|
283
|
-
// Model failure fallback, part 2/2: name the off-model pair, when the episode began, and
|
|
284
|
-
// the provider-class failure that tripped it, so `tumwater role <id>` explains the state.
|
|
285
|
-
if (p.modelFallback) {
|
|
286
|
-
const pair = fallbackPairLabel(p.modelFallback, backendDefault(p));
|
|
287
|
-
lines.push(`- Model fallback: ${pair} — on fallback since ${formatTimestamp(p.modelFallback.since)} (primary failing: ${p.modelFallback.reason})`);
|
|
288
|
-
}
|
|
289
276
|
if (p.fallback) {
|
|
290
277
|
const pair = fallbackPairLabel(p.fallback, backendDefault(p));
|
|
291
278
|
lines.push(`- Over-cap model: ${pair} (${p.fallbackFree ? "free" : "priced"})`);
|
|
@@ -19,11 +19,11 @@ import { projectCapHit } from "../budget/budget.js";
|
|
|
19
19
|
import { yieldMultiplierFor } from "../scheduling/backoff.js";
|
|
20
20
|
import { quietHoursStatus } from "../scheduling/quiet-hours.js";
|
|
21
21
|
import { assembleRunPrompt } from "../run/run-prompt.js";
|
|
22
|
-
import { configForRole, resolvedModelFields, reviewedAfterMerge, reviewRunConfig, roleSeamTier
|
|
23
|
-
import { fallbackPair
|
|
22
|
+
import { configForRole, resolvedModelFields, reviewedAfterMerge, reviewRunConfig, roleSeamTier } from "../config/config-views.js";
|
|
23
|
+
import { fallbackPair } from "../config/config-fallbacks.js";
|
|
24
24
|
import { fallbackModelPair, piModelsPath, seamContextWindowForRole } from "../pi/pi-models.js";
|
|
25
25
|
import { readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
|
|
26
|
-
import { formatModelSelector } from "../config/model-selector.js";
|
|
26
|
+
import { formatModelSelector } from "../config/config-model-selector.js";
|
|
27
27
|
import { roleImageInfo } from "../agent/image-input.js";
|
|
28
28
|
import { readPiListedModels } from "../pi/pi-listed-models.js";
|
|
29
29
|
import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
|
|
@@ -32,11 +32,11 @@ import { readAccount, windowEntries } from "../agent/subscription-account.js";
|
|
|
32
32
|
import { CONFIG_BASENAME, configPath, roleNotesPath } from "../paths.js";
|
|
33
33
|
import { coverageRows, readQaCoverage } from "../run/qa-coverage.js";
|
|
34
34
|
import { filingRows, readFeedbackFilings } from "../run/feedback-filings.js";
|
|
35
|
-
import {
|
|
35
|
+
import { readRegularTextOrNull, statOrNull } from "../files/files.js";
|
|
36
36
|
import { eventRole, eventUsage, readEvents } from "../events/event-read.js";
|
|
37
37
|
import { finiteNumber, objectOrEmpty } from "../files/json-object.js";
|
|
38
38
|
import { canonicalRoleId } from "./legacy-role-ids.js";
|
|
39
|
-
import { orchestratorAlive, readOrchestratorInfo } from "../team/orchestrator-info.js";
|
|
39
|
+
import { orchestratorAlive, readOrchestratorInfo } from "../team/team-orchestrator-info.js";
|
|
40
40
|
/** The roles.<base>.<field> value the FILE actually holds, or undefined when the file never
|
|
41
41
|
* set that field. Needed only for the seeded per-role fields: overlayDefaults fills every
|
|
42
42
|
* built-in role's minRunIntervalSeconds from config.ts's catalog, so the merged config cannot
|
|
@@ -103,14 +103,6 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
103
103
|
effective.provider === undefined &&
|
|
104
104
|
effective.model === undefined;
|
|
105
105
|
const piDefault = runsPiWithoutAModel ? readPiDefaultModel(piSettingsPath(modelsPath), root) : null;
|
|
106
|
-
// The role's provider key and its cap (plans/provider-concurrency.md §8): the same pure
|
|
107
|
-
// key the orchestrator's admission resolves for this role, answered from the same
|
|
108
|
-
// piDefault seam the orchestrator carries, so `tumwater role <id>` names the exact key
|
|
109
|
-
// whose cap bounds the role. The cap is the map's entry for that key, absent when no
|
|
110
|
-
// `maxConcurrentPerProvider` map is set or the key has no entry (then the role is bounded
|
|
111
|
-
// only by the team total, and the cap is omitted).
|
|
112
|
-
const providerKey = seamConcurrencyKey(effective, piDefault);
|
|
113
|
-
const providerCap = providerKey !== undefined ? cfg.maxConcurrentPerProvider?.[providerKey] : undefined;
|
|
114
106
|
// The fallback story the operator sees: the declared pair, but the price-resolved pair when
|
|
115
107
|
// one exists (a map with no `default` entry borrows another tier's free pair) and the
|
|
116
108
|
// freeness verdict from the same read. `fallbackFree` false keeps the declared pair visible
|
|
@@ -135,9 +127,8 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
135
127
|
const accounts = (b) => (b === "claude" ? claudeFallbackAccount : null);
|
|
136
128
|
const freeFallback = declaredFallback ? fallbackModelPair(cfg, modelsPath, accounts, opencodeCatalog) : null;
|
|
137
129
|
const fallback = freeFallback ?? declaredFallback;
|
|
138
|
-
// One state read serves
|
|
130
|
+
// One state read serves the next-run preview below.
|
|
139
131
|
const state = loadRunnerState(root, role);
|
|
140
|
-
const episode = state.modelFallback;
|
|
141
132
|
// The role's recent run outcomes, read from the event log (readEvents) so the command
|
|
142
133
|
// answers with the team stopped and never touches live state. readEvents returns the log's
|
|
143
134
|
// newest events in file (chronological) order, so filtering to this role's run_end events
|
|
@@ -233,7 +224,7 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
233
224
|
let noteSavedAt = null;
|
|
234
225
|
if (role !== DIRECTOR_ROLE) {
|
|
235
226
|
const notesPath = roleNotesPath(root, role);
|
|
236
|
-
const text =
|
|
227
|
+
const text = readRegularTextOrNull(notesPath);
|
|
237
228
|
note = text !== null && text.trim() !== "" ? text : null;
|
|
238
229
|
if (note !== null)
|
|
239
230
|
noteSavedAt = statOrNull(notesPath)?.mtimeMs ?? null;
|
|
@@ -270,11 +261,8 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
270
261
|
}
|
|
271
262
|
: {}),
|
|
272
263
|
backend: effective.agent ?? "pi",
|
|
273
|
-
...(providerKey !== undefined ? { providerKey } : {}),
|
|
274
|
-
...(providerCap !== undefined ? { providerCap } : {}),
|
|
275
264
|
fallback,
|
|
276
265
|
fallbackFree: freeFallback !== null,
|
|
277
|
-
modelFallback: modelFallbackView(episode, cfg, role),
|
|
278
266
|
...(account !== null ? { billing: account.billing, billingBackend } : {}),
|
|
279
267
|
...(billingWindow !== undefined ? { billingWindow } : {}),
|
|
280
268
|
chainStep,
|
|
@@ -334,8 +322,8 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
334
322
|
preview: true,
|
|
335
323
|
})?.prompt ?? null,
|
|
336
324
|
// The role's schedule — surfaced read-only from the same persisted state rolePayload
|
|
337
|
-
// already loaded for the
|
|
338
|
-
//
|
|
325
|
+
// already loaded for the next-run preview, so no new read path exists and the collector
|
|
326
|
+
// stays one loadRunnerState per role. `lastRunEndedAt`
|
|
339
327
|
// is omitted (JSON unchanged) until the role has actually run.
|
|
340
328
|
nextRunAt: state.nextRunAt,
|
|
341
329
|
backoffSeconds: state.backoffSeconds,
|