tumwater 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -28
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-eligibility.js +332 -43
- package/dist/src/backlog/backlog-md.js +79 -17
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +178 -441
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +32 -30
- package/dist/src/{brief-edit.js → brief/brief-edit.js} +12 -11
- package/dist/src/{brief.js → brief/brief.js} +33 -8
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +13 -11
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +9 -7
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +43 -10
- package/dist/src/cli/cli-editor.js +13 -1
- package/dist/src/cli/cli-flag-specs.js +31 -6
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-query-commands.js +439 -56
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +124 -68
- package/dist/src/cli/completion.js +70 -12
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +264 -124
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +13 -224
- package/dist/src/cli.js +78 -36
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +107 -325
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +494 -68
- package/dist/src/doctor/doctor-checks.js +509 -58
- package/dist/src/doctor/doctor-holds.js +139 -78
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +2 -3
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +76 -25
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +219 -106
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +80 -30
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +6 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +79 -59
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -716
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +105 -54
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +25 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +179 -104
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +29 -70
- package/dist/src/inbox/inbox-attachments.js +66 -10
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +114 -73
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +104 -50
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +130 -13
- package/dist/src/loop/loop-tick.js +190 -88
- package/dist/src/loop/loop.js +68 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -20
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +113 -45
- package/dist/src/operator/operator-intent.js +74 -53
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +47 -28
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +74 -43
- package/dist/src/orchestrator/orchestrator.js +149 -96
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +124 -38
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +4 -3
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +36 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +29 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +259 -254
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +235 -44
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +4 -3
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +55 -34
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +109 -131
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +16 -14
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +15 -8
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +28 -25
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +72 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +50 -39
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +84 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +39 -38
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +142 -25
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +23 -22
- package/dist/src/tick/tick-detail.js +37 -24
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +67 -53
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +75 -10
- package/dist/src/ui/badges.js +113 -69
- package/dist/src/ui/gui/gui-client-boot.js +202 -29
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +94 -37
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +90 -47
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +60 -43
- package/dist/src/ui/gui/gui-client-operator.js +73 -43
- package/dist/src/ui/gui/gui-client-pending.js +34 -31
- package/dist/src/ui/gui/gui-client-report.js +330 -47
- package/dist/src/ui/gui/gui-client-settings.js +126 -18
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +190 -74
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +18 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +176 -102
- package/dist/src/ui/status-payload.js +114 -36
- package/dist/src/ui/status-render.js +121 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +9 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +61 -57
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +44 -50
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +87 -1821
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +52 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +44 -26
- package/dist/src/verdict/fix-claim.js +56 -33
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +43 -11
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -56
- package/dist/src/landing/landing-slot.js +0 -256
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,200 +1,100 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { readSlotsState } from "../git/slots-state.js";
|
|
3
2
|
import { openBugs, openQuestions } from "../backlog/backlog.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { claudeWindowEntries, readClaudeAccount } from "../agent/claude/claude-account.js";
|
|
8
|
-
import { isJsonObject } from "../files/json-object.js";
|
|
9
|
-
import { fallbackModelFree, fleetModelsFree, piModelsPath, piPairFree, readPiProviders } from "../pi/pi-models.js";
|
|
10
|
-
import { configForStatus, queueInFlight, loopStateForPoll, mainCheckForPoll, maintenanceQuotaForPoll } from "./status-polls.js";
|
|
11
|
-
import { queuedRolePromptEntries } from "../inbox/inbox.js";
|
|
12
|
-
import { deliverableAt } from "../inbox/prompt-not-before.js";
|
|
3
|
+
import { baseRoleOf, loopIds, loopPaused } from "../roles/loop-ids.js";
|
|
4
|
+
import { piModelsPath } from "../pi/pi-models.js";
|
|
5
|
+
import { configForStatus, queueInFlight, mainCheckForPoll, maintenanceQuotaForPoll } from "./status-polls.js";
|
|
13
6
|
import { quietHoursStatus, roleQuietHold } from "../scheduling/quiet-hours.js";
|
|
14
7
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
15
|
-
import { pausedRoles, pausedRolesUntil, scheduledStopAt, scheduledWakes,
|
|
16
|
-
import { orchestratorAlive, readOrchestratorInfo, } from "../
|
|
17
|
-
import { budgetReached, fleetDailyCost, modelPairName, projectCapHit } from "../budget/budget.js";
|
|
18
|
-
import { roleCapVerdicts } from "../gates/role-cap-gates.js";
|
|
8
|
+
import { pausedRoles, pausedRolesUntil, scheduledStopAt, scheduledWakes, standingTeamPause } from "../team/team-state.js";
|
|
9
|
+
import { orchestratorAlive, readOrchestratorInfo, } from "../team/orchestrator-info.js";
|
|
19
10
|
import { bootstrapHoldsRole, bootstrapStatus } from "../gates/bootstrap-gates.js";
|
|
20
|
-
import {
|
|
11
|
+
import { readMergeQueue } from "../merge/merge-queue.js";
|
|
12
|
+
import { collectImageProbeFailures, collectRoleRows } from "./status-roles.js";
|
|
13
|
+
import { collectInboxes } from "./status-inbox.js";
|
|
14
|
+
import { collectBudget } from "./status-budget.js";
|
|
15
|
+
import { collectRootInstallDrift } from "./status-install.js";
|
|
21
16
|
/** The earlier of the two optional stop deadlines (`run --for`'s and a scheduled stop's). */
|
|
22
17
|
function earliestStop(a, b) {
|
|
23
18
|
return a === undefined ? b : b === undefined ? a : Math.min(a, b);
|
|
24
19
|
}
|
|
25
|
-
/**
|
|
26
|
-
* unless the account reads subscription billing with at least one window AND a resolved seam
|
|
27
|
-
* routes to claude. `only` is the free-ness classification the display needs: every paying seam
|
|
28
|
-
* is a subscription claude seam exactly when no other seam is priced — a pi seam judged by
|
|
29
|
-
* piPairFree, any other backend's billing unseen so it counts as paying too. The reviewer and
|
|
30
|
-
* the strong-tier resolver are included, like fleetModelsFree, since they can spend. */
|
|
31
|
-
function claudeWindowBlock(cfg, providers, root, now) {
|
|
32
|
-
const account = readClaudeAccount(root, now);
|
|
33
|
-
if (account?.billing !== "subscription")
|
|
34
|
-
return undefined;
|
|
35
|
-
const windows = claudeWindowEntries(account);
|
|
36
|
-
if (windows.length === 0)
|
|
37
|
-
return undefined;
|
|
38
|
-
const seams = enabledRoleIds(cfg).map((role) => configForRole(cfg, role));
|
|
39
|
-
if (cfg.review.enabled)
|
|
40
|
-
seams.push(reviewConfig(cfg));
|
|
41
|
-
if (seams.length > 0)
|
|
42
|
-
seams.push(resolverConfig(cfg));
|
|
43
|
-
if (!seams.some((s) => s.agent === "claude"))
|
|
44
|
-
return undefined;
|
|
45
|
-
const priced = seams.some((s) => {
|
|
46
|
-
if (s.agent === undefined || s.agent === "pi")
|
|
47
|
-
return !piPairFree(providers, s.provider, s.model);
|
|
48
|
-
return s.agent !== "claude"; // a backend the account cannot bill is never subscription-paid
|
|
49
|
-
});
|
|
50
|
-
const heldUntil = windows
|
|
51
|
-
.filter((w) => w.status === "rejected" && w.isUsingOverage !== true && typeof w.resetsAt === "number")
|
|
52
|
-
.reduce((max, w) => Math.max(max, w.resetsAt), 0) || null;
|
|
53
|
-
return { windows, primary: windows[0], only: !priced, heldUntil };
|
|
54
|
-
}
|
|
55
|
-
/** One fresh fleet snapshot for observers. `modelsPath` overrides pi's model definitions
|
|
20
|
+
/** One fresh team snapshot for observers. `modelsPath` overrides pi's model definitions
|
|
56
21
|
* location (default ~/.pi/agent/models.json) — a test seam, like doctor's pathEnv. `now` pins
|
|
57
22
|
* the poll's clock for the budget figures (the persisted day-stamp check and the burn-rate
|
|
58
23
|
* projection) — one instant for the whole block, and the seam that keeps a test's expected
|
|
59
24
|
* projection from racing the snapshot's own `Date.now()`; production callers take the default
|
|
60
|
-
* and read the live clock, as always.
|
|
25
|
+
* and read the live clock, as always. The three per-domain collectors (status-roles, status-
|
|
26
|
+
* inbox, status-budget) each take the shared reads and the pinned `now` as parameters, so
|
|
27
|
+
* every poll still reads a shared resource exactly once and the one `now` pins the whole
|
|
28
|
+
* snapshot — the consistency property the per-collector comments call out. */
|
|
61
29
|
export function snapshot(root, modelsPath = piModelsPath(), now = Date.now()) {
|
|
62
30
|
const cfg = configForStatus(root);
|
|
31
|
+
const rootDrift = collectRootInstallDrift(root, now);
|
|
63
32
|
// The gate's read-only bootstrap verdict (plans/work-ratio.md, part 2/2), computed over the
|
|
64
33
|
// same last-known-good config the caps read, so the dashboards show the hold the scheduler
|
|
65
34
|
// enforces. Null when no bootstrap is configured.
|
|
66
35
|
const bootstrapNow = bootstrapStatus(root, cfg);
|
|
67
36
|
// One row per loop id (part 5b/7): an instance role shows `feature` and `feature-2`.
|
|
68
37
|
const roles = loopIds(cfg);
|
|
38
|
+
const markerRoles = pausedRoles(root);
|
|
39
|
+
const markerSet = new Set(markerRoles);
|
|
69
40
|
// One read of the orchestrator info file per poll: it serves both the displayed pid and the
|
|
70
41
|
// liveness check (passing it to orchestratorAlive skips its own re-read).
|
|
71
42
|
const info = readOrchestratorInfo(root);
|
|
72
43
|
// One slots.json read per poll serves every row: the pooled slot a role leases (a running
|
|
73
|
-
// tick or
|
|
44
|
+
// tick or verification) or is pinned to — threaded into the roles collector so `running`,
|
|
45
|
+
// `disk` and the rows agree.
|
|
74
46
|
const slots = readSlotsState(root).slots;
|
|
75
|
-
const loops =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// and a role selector override both resolve through configForRole — the same view the
|
|
83
|
-
// seam consumes. Part 7a (model-tiers.md "Observability"): the seam tier tag is added
|
|
84
|
-
// only with a tier map declared, where it names the tier a role rides.
|
|
85
|
-
const eff = configForRole(cfg, r);
|
|
86
|
-
// Model failure fallback, part 2/2: while the role's episode is active its ticks run on
|
|
87
|
-
// the tier-resolved fallback pair, so the row names that pair, the episode's start, and
|
|
88
|
-
// the failure that tripped it. `now` is the poll's own clock (the snapshot seam), so
|
|
89
|
-
// every row agrees on what "active" means.
|
|
90
|
-
const episode = modelFallbackView(base.modelFallback, cfg, r, now);
|
|
91
|
-
return {
|
|
92
|
-
...base,
|
|
93
|
-
...modelSelectorField(eff),
|
|
94
|
-
...(isJsonObject(cfg.model) ? { modelTier: roleSeamTier(cfg, r) } : {}),
|
|
95
|
-
...(episode !== null ? { fallback: episode } : {}),
|
|
96
|
-
...(slot ? { slot: path.basename(slot.dir), slotPinned: pinned !== undefined } : {}),
|
|
97
|
-
...(baseRoleOf(r) === r ? {} : { instanceOf: baseRoleOf(r) }),
|
|
98
|
-
};
|
|
99
|
-
});
|
|
100
|
-
// One inbox pass per poll serves all four director fields (queuedRolePromptEntries lists
|
|
101
|
-
// the directory and reads each file once): the count is the entries' length, so a prompt
|
|
102
|
-
// enqueued or dequeued mid-snapshot can never make the header badge disagree with its
|
|
103
|
-
// numbered previews, and each preview keeps its queue-file address beside it.
|
|
104
|
-
const queued = queuedRolePromptEntries(root, DIRECTOR_ROLE);
|
|
105
|
-
const inboxPrompts = queued.map((e) => e.preview);
|
|
106
|
-
const inboxFiles = queued.map((e) => e.file);
|
|
107
|
-
const inboxQueuedAt = queued.map((e) => e.queuedAtMs);
|
|
108
|
-
const inboxNotBefore = queued.map((e) => e.notBeforeMs);
|
|
109
|
-
// One directory listing pass per role per poll (below) fills the per-role counts; the
|
|
110
|
-
// director is excluded because its queue is the shared inbox above.
|
|
111
|
-
// A role with prompts queued also fills its cancel-addressable entries — the
|
|
112
|
-
// stat-keyed prompt cache keeps an unchanged file at one stat per poll. Entries follow the
|
|
113
|
-
// full list, deferred prompts included (the Queued tab lists and cancels them; only the
|
|
114
|
-
// deliverable count above excludes them).
|
|
115
|
-
const roleInbox = {};
|
|
116
|
-
const roleInboxPrompts = {};
|
|
117
|
-
// One listing pass per role serves both the deliverable count and the cancel-addressable
|
|
118
|
-
// entries: queuedRolePromptRecords already carries each readable prompt's notBeforeMs, and
|
|
119
|
-
// deliverableAt is exactly `notBeforeMs === null || notBeforeMs <= now` (prompt-not-before.ts)
|
|
120
|
-
// over the same cached text — so the old second pass (queuedRolePromptCount's own readdir +
|
|
121
|
-
// deliverability filter, once per role per poll across all 12 non-director loops) repeated
|
|
122
|
-
// work the entries pass had just done. One instant — the snapshot's own pinned `now` — pins
|
|
123
|
-
// the count and the entries to the same moment, so a caller that pins the clock sees
|
|
124
|
-
// deliverability advance with it instead of the wall clock.
|
|
125
|
-
for (const r of roles) {
|
|
126
|
-
if (r === DIRECTOR_ROLE)
|
|
127
|
-
continue;
|
|
128
|
-
const entries = queuedRolePromptEntries(root, r);
|
|
129
|
-
roleInbox[r] = entries.filter((e) => deliverableAt(e.notBeforeMs, now)).length;
|
|
130
|
-
if (entries.length)
|
|
131
|
-
roleInboxPrompts[r] = entries;
|
|
132
|
-
}
|
|
47
|
+
const loops = collectRoleRows(root, cfg, now, slots, modelsPath);
|
|
48
|
+
// The doctor's cached image-probe failures (part 5/5): one read per snapshot, labelled, so
|
|
49
|
+
// the amber probe alert and `status --json` both answer from the same collector.
|
|
50
|
+
const imageProbeFailures = collectImageProbeFailures(root, now);
|
|
51
|
+
// One inbox pass per poll (status-inbox.ts) serves all four director fields and the per-role
|
|
52
|
+
// counts/entries from the same queuedRolePromptEntries reads.
|
|
53
|
+
const { inbox, inboxPrompts, inboxFiles, inboxQueuedAt, inboxNotBefore, roleInbox, roleInboxPrompts } = collectInboxes(root, roles, now);
|
|
133
54
|
const running = orchestratorAlive(root, info);
|
|
134
|
-
// One
|
|
135
|
-
//
|
|
136
|
-
const
|
|
137
|
-
// One read of the pending scheduled wakes feeds both the earliest and the full list, so they
|
|
55
|
+
// One team-pause-marker read per poll serves both the paused flag and its deadline:
|
|
56
|
+
// isTeamPaused + pausedUntil would each re-read the same marker file every second.
|
|
57
|
+
const teamPause = standingTeamPause(root);
|
|
58
|
+
// One read of the pending scheduled wakes feeds both the earliest and the full list, so they
|
|
59
|
+
// cannot disagree.
|
|
138
60
|
const wakes = scheduledWakes(root, now);
|
|
139
|
-
// One
|
|
61
|
+
// One merge queue pass per poll (merge-queue.ts's stat cache keeps an unchanged queue at one
|
|
140
62
|
// stat per file) serves the depth; inFlight is the 4/5 marker only when a live orchestrator
|
|
141
63
|
// still has a queue entry for what the marker names (per change, for a batch) — the
|
|
142
64
|
// cross-check makes every crash ordering self-healing (a stale marker alone never displays,
|
|
143
65
|
// and needs no cleanup pass).
|
|
144
|
-
const queue =
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
depth:
|
|
66
|
+
const queue = readMergeQueue(root);
|
|
67
|
+
const merges = queue.entries;
|
|
68
|
+
const mergeQueue = {
|
|
69
|
+
depth: merges.length + queue.unreadable,
|
|
148
70
|
};
|
|
149
|
-
if (
|
|
150
|
-
|
|
71
|
+
if (merges.length > 0) {
|
|
72
|
+
mergeQueue.entries = merges.map((e) => ({
|
|
151
73
|
role: e.role,
|
|
152
74
|
sha: e.sha,
|
|
153
|
-
|
|
75
|
+
run: e.run,
|
|
154
76
|
summary: e.summary,
|
|
155
77
|
enqueuedAt: e.enqueuedAt,
|
|
78
|
+
...(e.revisionRound !== undefined ? { revisionRound: e.revisionRound } : {}),
|
|
79
|
+
...(Array.isArray(e.priorReview?.reasons) && e.priorReview.reasons.length > 0 ? { priorObjections: e.priorReview.reasons.length } : {}),
|
|
156
80
|
}));
|
|
157
81
|
}
|
|
158
|
-
const inFlight = queueInFlight(root,
|
|
82
|
+
const inFlight = queueInFlight(root, merges, running);
|
|
159
83
|
if (inFlight)
|
|
160
|
-
|
|
161
|
-
// The running orchestrator's published gate figures (read once above, with the pid and the
|
|
162
|
-
// demotion): the scheduler's own sum over its live runner states. Only while running — the
|
|
163
|
-
// exit removes the info file, so a stale file beside a dead pid must not speak for a fleet
|
|
164
|
-
// whose persisted files are final.
|
|
165
|
-
const publishedSpend = running && info?.budget ? info.budget.spentUsd : null;
|
|
166
|
-
// The one materialized spend the budget block renders and the burn-rate projection both
|
|
167
|
-
// read — the scheduler's published figure while running, the persisted states' sum otherwise.
|
|
168
|
-
const spentUsd = publishedSpend ?? fleetDailyCost(loops, now);
|
|
84
|
+
mergeQueue.inFlight = inFlight;
|
|
169
85
|
// Quiet hours 2/2 — one parse and one clock read per poll serve both the window string the
|
|
170
86
|
// header badge renders and the in-window boolean the active reading turns on; the value is
|
|
171
87
|
// read fresh from the same cached config load the budget block uses, so a live edit shows
|
|
172
88
|
// on the next poll exactly when the gate applies it.
|
|
173
89
|
const quiet = quietHoursStatus(cfg.quietHours, new Date());
|
|
174
|
-
// The
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
// the orchestrator's memory, so it publishes them; the legacy single-pair field
|
|
178
|
-
// `fallbackDemoted` is the engaged pair's entry of the same map). Computed only while the
|
|
179
|
-
// cap is reached — the hold never stands under an open gate — and the director exempt.
|
|
180
|
-
const budgetReachedNow = budgetReached({ spentUsd, capUsd: cfg.maxDailyCostUsd });
|
|
181
|
-
const providers = readPiProviders(modelsPath);
|
|
182
|
-
// The persisted billing answer (6a/7), read once for every free-ness verdict below: a
|
|
183
|
-
// subscription claude seam is free, so the badge and the pause set judge it as such.
|
|
184
|
-
const account = readClaudeAccount(root);
|
|
185
|
-
const publishedDemotions = info?.fallbackDemotions ?? {};
|
|
186
|
-
const usable = (p) => piPairFree(providers, p.provider, p.model, account) && !(modelPairName(p) in publishedDemotions);
|
|
187
|
-
const servingResolved = resolveTierFallbacks(cfg, usable);
|
|
188
|
-
// The subscription usage window (part 6d/7): one account read per poll serves the block.
|
|
189
|
-
const claudeWindow = claudeWindowBlock(cfg, providers, root, now);
|
|
190
|
-
const budgetPausedRoles = budgetReachedNow
|
|
191
|
-
? roles.filter((r) => r !== DIRECTOR_ROLE && servingResolved[roleSeamTier(cfg, r)].pair === null)
|
|
192
|
-
: [];
|
|
90
|
+
// The spend/billing block (status-budget.ts): the budget object, the claude window, and the
|
|
91
|
+
// two per-role pause verdicts, all from one readPiProviders and one readClaudeAccount per poll.
|
|
92
|
+
const { budget, claudeWindow, budgetPausedRoles, capPaused } = collectBudget(root, cfg, loops, roles, running, info, modelsPath, now);
|
|
193
93
|
return {
|
|
194
94
|
running,
|
|
195
95
|
pid: info?.pid,
|
|
196
96
|
build: running && info?.build ? info.build : null,
|
|
197
|
-
inbox
|
|
97
|
+
inbox,
|
|
198
98
|
inboxPrompts,
|
|
199
99
|
inboxFiles,
|
|
200
100
|
inboxQueuedAt,
|
|
@@ -202,42 +102,26 @@ export function snapshot(root, modelsPath = piModelsPath(), now = Date.now()) {
|
|
|
202
102
|
roleInbox,
|
|
203
103
|
roleInboxPrompts,
|
|
204
104
|
questions: openQuestions(root).length,
|
|
105
|
+
imageProbeFailures,
|
|
205
106
|
loops,
|
|
206
|
-
// Unconditional (never null): a disabled
|
|
107
|
+
// Unconditional (never null): a disabled team still shows its spend and the badge is
|
|
207
108
|
// the affordance for SETTING a cap. models.json itself is stat-cached inside pi-models.ts,
|
|
208
109
|
// so an unchanged catalog costs one stat per poll, not a re-read plus parse.
|
|
209
|
-
budget
|
|
210
|
-
spentUsd,
|
|
211
|
-
capUsd: cfg.maxDailyCostUsd,
|
|
212
|
-
// One projection per poll, from the same spend the field above renders: null exactly
|
|
213
|
-
// when no forecast may be stated (no cap, no spend, cap reached, burn misses midnight).
|
|
214
|
-
capHitAt: projectCapHit({ spentUsd, capUsd: cfg.maxDailyCostUsd }, now),
|
|
215
|
-
free: fleetModelsFree(cfg, modelsPath, account),
|
|
216
|
-
agent: cfg.agent ?? "pi",
|
|
217
|
-
// Null unless the gate could actually engage it (configured AND priced at zero AND not
|
|
218
|
-
// demoted by the running orchestrator's breaker): a fallback the scheduler would refuse
|
|
219
|
-
// must not be advertised as one that will save the fleet — and a demoted one is refused
|
|
220
|
-
// until its probe serves, so the dashboards read `budget paused` exactly while the
|
|
221
|
-
// scheduler is (BUGS.md 2026-09-20). The pair is the price-aware default tier's
|
|
222
|
-
// resolution, so a map with no `default` entry names the pair the gate actually runs.
|
|
223
|
-
fallback: fallbackModelFree(cfg, modelsPath, account) && !(running && info?.fallbackDemoted)
|
|
224
|
-
? (servingResolved.default.pair ?? null)
|
|
225
|
-
: null,
|
|
226
|
-
tiers: budgetReachedNow && tiersResolveDistinctPairs(servingResolved)
|
|
227
|
-
? tierFallbackLabels(servingResolved)
|
|
228
|
-
: null,
|
|
229
|
-
},
|
|
110
|
+
budget,
|
|
230
111
|
...(claudeWindow ? { claudeWindow } : {}),
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
112
|
+
...(running && info?.chain && Object.keys(info.chain).length > 0 ? { chain: info.chain } : {}),
|
|
113
|
+
// Only while running: a stale info file beside a dead pid must not speak for a stopped
|
|
114
|
+
// team, and only while a hold stands (an empty list is the same as none).
|
|
115
|
+
...(running && info?.providerHolds && info.providerHolds.length > 0 ? { providerHolds: info.providerHolds } : {}),
|
|
116
|
+
paused: teamPause !== null,
|
|
117
|
+
pausedRoles: markerRoles,
|
|
118
|
+
pausedLoops: roles.filter((id) => loopPaused(markerSet, id)),
|
|
119
|
+
capPaused,
|
|
236
120
|
// The maintenance allowance (plans/work-ratio.md, part 1b/4): the same pure verdict the
|
|
237
121
|
// gate folds, over the same event log, so the dashboards show the hold the scheduler
|
|
238
|
-
// enforces. `
|
|
239
|
-
//
|
|
240
|
-
maintenanceQuota: maintenanceQuotaForPoll(root, cfg.
|
|
122
|
+
// enforces. `merges` is the one merge queue snapshot read above, so in-flight maintenance
|
|
123
|
+
// merges count once here and in mergeQueue.
|
|
124
|
+
maintenanceQuota: maintenanceQuotaForPoll(root, cfg.maintenancePerWorkMerge, loops, new Set(merges.map((e) => e.role)), now),
|
|
241
125
|
// The new-project bootstrap hold (plans/work-ratio.md, part 2/2): the same read-only
|
|
242
126
|
// verdict the gate computes (bootstrapStatus), so the dashboards and the scheduler agree.
|
|
243
127
|
// The held set is computed with the gate's own predicate over the same loop list.
|
|
@@ -249,28 +133,32 @@ export function snapshot(root, modelsPath = piModelsPath(), now = Date.now()) {
|
|
|
249
133
|
: [],
|
|
250
134
|
budgetPausedRoles,
|
|
251
135
|
// The per-role quiet windows' held roles (PLANS.md quietHoursPerRole): keyed role →
|
|
252
|
-
// window, so the dashboards can render the same `quiet until <end>` wording the
|
|
136
|
+
// window, so the dashboards can render the same `quiet until <end>` wording the team
|
|
253
137
|
// badge uses, scoped to the loop's own schedule. The window string comes back out of
|
|
254
138
|
// the same config the hold was decided from, so the label cannot name a window that is
|
|
255
139
|
// not holding (roleQuietHold true implies a parseable, non-empty string there).
|
|
256
140
|
roleQuietPaused: Object.fromEntries(loops
|
|
257
141
|
.filter((l) => l.role !== DIRECTOR_ROLE && roleQuietHold(cfg.quietHoursPerRole, baseRoleOf(l.role), new Date()))
|
|
258
142
|
.map((l) => [l.role, cfg.quietHoursPerRole?.[baseRoleOf(l.role)] ?? ""])),
|
|
259
|
-
pausedUntil:
|
|
143
|
+
pausedUntil: teamPause?.until,
|
|
260
144
|
pausedRolesUntil: pausedRolesUntil(root),
|
|
261
145
|
scheduledWake: wakes[0],
|
|
262
146
|
scheduledWakes: wakes.length > 0 ? wakes : undefined,
|
|
263
|
-
pauseReason:
|
|
147
|
+
pauseReason: teamPause?.reason,
|
|
264
148
|
quietHours: quiet.window ?? undefined,
|
|
265
149
|
inQuietHours: quiet.inWindow,
|
|
266
|
-
|
|
150
|
+
mergeQueue,
|
|
267
151
|
mainCheck: mainCheckForPoll(root, cfg),
|
|
268
152
|
// The disk floor's published state (plans/disk-floor.md, part 4/4): only while the
|
|
269
153
|
// orchestrator runs — the exit removes the info file, so a stale file beside a dead pid
|
|
270
|
-
// must not speak for a stopped
|
|
154
|
+
// must not speak for a stopped team — and only when the last poll could measure the
|
|
271
155
|
// volume. Absent otherwise, so the surfaces render exactly as before.
|
|
272
156
|
disk: running && info?.disk ? info.disk : undefined,
|
|
273
|
-
//
|
|
157
|
+
// Independent of the team running: a stopped team's worktrees still borrow the root install.
|
|
158
|
+
rootInstallDrift: rootDrift.length ? rootDrift : undefined,
|
|
159
|
+
// Only while running: a stale info file beside a dead pid must not speak for a stopped team.
|
|
160
|
+
permits: running && info?.permits ? info.permits : undefined,
|
|
161
|
+
// Only while running: a stale info file beside a dead pid must not speak for a stopped team.
|
|
274
162
|
runStopsAt: running ? earliestStop(info?.stopsAt, scheduledStopAt(root)) : undefined,
|
|
275
163
|
scheduledStopAt: running ? scheduledStopAt(root) : undefined,
|
|
276
164
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { queuedRolePromptEntries } from "../inbox/inbox.js";
|
|
2
|
+
import { deliverableAt } from "../inbox/prompt-not-before.js";
|
|
3
|
+
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
4
|
+
/** One inbox pass per poll serves all four director fields (queuedRolePromptEntries lists the
|
|
5
|
+
* directory and reads each file once): the count is the deliverable entries' length, so a
|
|
6
|
+
* prompt enqueued or dequeued mid-snapshot can never make the header badge disagree with its
|
|
7
|
+
* numbered previews, and each preview keeps its queue-file address beside it. A second
|
|
8
|
+
* listing pass per role (all non-director loops) fills the per-role counts and their
|
|
9
|
+
* cancel-addressable entries from the same `queuedRolePromptEntries` records, with
|
|
10
|
+
* deliverableAt (prompt-not-before.ts) over the same cached text. `roles` is the
|
|
11
|
+
* already-computed loop list so one `loopIds` read serves the caller and both passes; `now`
|
|
12
|
+
* is the snapshot's pinned clock, so deliverability and the counts advance together instead
|
|
13
|
+
* of racing the wall clock. */
|
|
14
|
+
export function collectInboxes(root, roles, now) {
|
|
15
|
+
const queued = queuedRolePromptEntries(root, DIRECTOR_ROLE);
|
|
16
|
+
const inboxPrompts = queued.map((e) => e.preview);
|
|
17
|
+
const inboxFiles = queued.map((e) => e.file);
|
|
18
|
+
const inboxQueuedAt = queued.map((e) => e.queuedAtMs);
|
|
19
|
+
const inboxNotBefore = queued.map((e) => e.notBeforeMs);
|
|
20
|
+
// The director and the per-role queues count queued prompts by the one rule — deliverableAt
|
|
21
|
+
// (prompt-not-before.ts) over the same cached text — so a deferred prompt never inflates
|
|
22
|
+
// either the header badge or a per-role marker while it is still listed and cancelable.
|
|
23
|
+
// One directory listing pass per role per poll fills the per-role counts; the director is
|
|
24
|
+
// excluded because its queue is the shared inbox above. A role with prompts queued also
|
|
25
|
+
// fills its cancel-addressable entries — the stat-keyed prompt cache keeps an unchanged
|
|
26
|
+
// file at one stat per poll. Entries follow the full list, deferred prompts included (the
|
|
27
|
+
// Queued tab lists and cancels them; only the deliverable count above excludes them).
|
|
28
|
+
const roleInbox = {};
|
|
29
|
+
const roleInboxPrompts = {};
|
|
30
|
+
for (const r of roles) {
|
|
31
|
+
if (r === DIRECTOR_ROLE)
|
|
32
|
+
continue;
|
|
33
|
+
const entries = queuedRolePromptEntries(root, r);
|
|
34
|
+
roleInbox[r] = entries.filter((e) => deliverableAt(e.notBeforeMs, now)).length;
|
|
35
|
+
if (entries.length)
|
|
36
|
+
roleInboxPrompts[r] = entries;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
inbox: queued.filter((e) => deliverableAt(e.notBeforeMs, now)).length,
|
|
40
|
+
inboxPrompts,
|
|
41
|
+
inboxFiles,
|
|
42
|
+
inboxQueuedAt,
|
|
43
|
+
inboxNotBefore,
|
|
44
|
+
roleInbox,
|
|
45
|
+
roleInboxPrompts,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
import { installDrift } from "../build/dep-install.js";
|
|
3
|
+
/** How long a computed drift answer is reused. The answer depends on two files (the lockfile and
|
|
4
|
+
* each installed manifest), so it is throttled by time rather than keyed by stat: the dashboard
|
|
5
|
+
* polls every second and must not re-parse a large lockfile each time. */
|
|
6
|
+
const ROOT_INSTALL_POLL_MS = 60_000;
|
|
7
|
+
/** Safety cap, as in files/stat-cache.ts: many short-lived roots (tests) cannot grow the map
|
|
8
|
+
* unbounded. */
|
|
9
|
+
const MAX_ROOTS_CACHED = 64;
|
|
10
|
+
const cache = new Map();
|
|
11
|
+
/** Direct dependencies `root`'s node_modules does not hold at package-lock.json's pin (the
|
|
12
|
+
* doctor's `root install` row), recomputed at most once per ROOT_INSTALL_POLL_MS per root.
|
|
13
|
+
* `[]` when there is no readable lockfile. The caller owns the returned array. */
|
|
14
|
+
export function collectRootInstallDrift(root, now) {
|
|
15
|
+
const hit = cache.get(root);
|
|
16
|
+
if (hit && now - hit.at < ROOT_INSTALL_POLL_MS)
|
|
17
|
+
return [...hit.drift];
|
|
18
|
+
const drift = installDrift(root);
|
|
19
|
+
if (cache.size >= MAX_ROOTS_CACHED)
|
|
20
|
+
cache.clear();
|
|
21
|
+
cache.set(root, { at: now, drift });
|
|
22
|
+
return [...drift];
|
|
23
|
+
}
|
|
24
|
+
/** Forget every cached answer (tests). */
|
|
25
|
+
export function resetRootInstallCache() {
|
|
26
|
+
cache.clear();
|
|
27
|
+
}
|