tumwater 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/backend-traits.js +14 -0
- package/dist/src/agent/claude/claude-stream.js +27 -31
- package/dist/src/agent/codex/codex-rollout.js +38 -24
- package/dist/src/agent/codex/codex-stream.js +71 -38
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +4 -4
- package/dist/src/agent/run-agent.js +11 -2
- package/dist/src/agent/run-settle.js +22 -13
- package/dist/src/agent/run-spawn.js +3 -12
- package/dist/src/agent/run-stream.js +2 -10
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/backlog/backlog-archive.js +64 -165
- package/dist/src/backlog/backlog-eligibility.js +50 -58
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +2 -2
- package/dist/src/backlog/backlog-md.js +38 -27
- package/dist/src/backlog/backlog-release.js +11 -19
- package/dist/src/backlog/backlog-structure.js +14 -1
- package/dist/src/backlog/backlog-write.js +1 -1
- package/dist/src/backlog/backlog.js +5 -4
- package/dist/src/backlog/question-answer.js +30 -14
- package/dist/src/baseline/main-baseline.js +30 -20
- package/dist/src/baseline/main-guardrails.js +54 -17
- package/dist/src/baseline/main-watch.js +1 -1
- package/dist/src/budget/budget-wiring.js +0 -5
- package/dist/src/budget/budget.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +1 -4
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +27 -50
- package/dist/src/cli/cli-start.js +62 -12
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +6 -7
- package/dist/src/cli/log-commands.js +3 -1
- package/dist/src/cli.js +3 -7
- package/dist/src/collections.js +21 -7
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +19 -26
- package/dist/src/doctor/doctor-backlog-bugs.js +17 -115
- package/dist/src/doctor/doctor-backlog-plans.js +8 -8
- package/dist/src/doctor/doctor-backlog.js +22 -36
- package/dist/src/doctor/doctor-check-names.js +2 -2
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-contention-checks.js +1 -1
- package/dist/src/doctor/doctor-holds.js +13 -11
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +85 -48
- package/dist/src/doctor/doctor-project-checks.js +66 -17
- package/dist/src/doctor/doctor-role-notes.js +6 -24
- package/dist/src/doctor/doctor.js +6 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +5 -15
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/failure/failure-state-change.js +183 -68
- package/dist/src/failure/time-spend.js +1 -1
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/git/slot-salvage.js +5 -4
- package/dist/src/gui/gui-admin-endpoints.js +2 -3
- package/dist/src/gui/gui-backlog-commands.js +1 -1
- package/dist/src/gui/gui-backlog-endpoints.js +1 -7
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-operator-commands.js +0 -12
- package/dist/src/gui/gui-prompt-commands.js +26 -20
- package/dist/src/gui/gui-report-endpoints.js +2 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/history/history.js +102 -67
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/prompt-commands.js +24 -19
- package/dist/src/init/init.js +68 -15
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-post-review-drain.js +1 -1
- package/dist/src/operator/operator-commands.js +68 -47
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +0 -6
- package/dist/src/pi/pi-watchdogs.js +20 -1
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/process/process.js +23 -0
- package/dist/src/process/supervisor.js +17 -1
- package/dist/src/prompt/gate-notes.js +9 -9
- package/dist/src/prompt/notes-backlog.js +11 -24
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +4 -5
- package/dist/src/report/report-data.js +10 -225
- package/dist/src/report/report-fold.js +20 -295
- package/dist/src/report/report-main-red.js +45 -42
- package/dist/src/report/report-render.js +66 -39
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +3 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +9 -9
- package/dist/src/review/quarantine-render.js +2 -1
- package/dist/src/roles/role-catalog.js +18 -30
- package/dist/src/roles/role-guidance.js +18 -43
- package/dist/src/roles/role-view.js +1 -2
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-detail-data.js +23 -7
- package/dist/src/run/run-detail-render.js +6 -0
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-prompt.js +100 -56
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +4 -21
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +8 -73
- package/dist/src/run/stage-backlog-checks.js +2 -29
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-run.js +2 -79
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +3 -10
- package/dist/src/status/status-data.js +5 -9
- package/dist/src/status/status-role-notes.js +18 -14
- package/dist/src/status/status-roles.js +2 -35
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +7 -212
- package/dist/src/ui/badges.js +2 -3
- package/dist/src/ui/gui/gui-client-boot.js +19 -4
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-report-chart.js +10 -8
- package/dist/src/ui/gui/gui-client-report.js +1 -2
- package/dist/src/ui/gui/gui-client-settings.js +0 -23
- package/dist/src/ui/gui/gui-client-team.js +26 -0
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/status-model.js +2 -2
- package/dist/src/ui/status-payload.js +3 -3
- package/dist/src/ui/status-render.js +3 -3
- package/dist/src/ui/team-alert-backlog.js +23 -54
- package/dist/src/ui/team-alert-shared.js +23 -10
- package/dist/src/ui/team-alert-status.js +8 -3
- package/dist/src/ui/team-alert-tail.js +1 -14
- package/dist/src/ui/team-alerts.js +6 -17
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +105 -93
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-pause.js +209 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +44 -34
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +121 -108
- package/dist/src/ui/tui/tui-editor-release.js +50 -42
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +58 -46
- package/dist/src/ui/tui/tui-editor-role-key.js +49 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-frame.js +1 -1
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-health.js +3 -24
- package/dist/src/ui/tui/tui-pane-history.js +20 -24
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +3 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +151 -128
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +2 -3
- package/dist/src/ui/tui/tui.js +3 -3
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +30 -6
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- package/dist/src/ui/team-alert-backlog-feed.js +0 -181
- package/dist/src/ui/team-alert-merge.js +0 -183
|
@@ -47,207 +47,234 @@ export function roleInspectMarkdown(root, role) {
|
|
|
47
47
|
* the head with the selected entry's full body in entry mode, usage/failures re-window
|
|
48
48
|
* the cached Markdown set on view entry, and the activity pane keeps the event tail. */
|
|
49
49
|
export function paneBody(pane, opts) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// Filtered: scan the drawer's 200 entries with the shared keep rule (a lowercased needle).
|
|
75
|
-
return {
|
|
76
|
-
body: readTranscript(root, pane.role, 200, { grep: pane.grep.toLowerCase() }).slice(-eventBudget).map((l) => toneLine(l, width, transcriptTone(l))),
|
|
77
|
-
emptyNote: `(no transcript entries match "${pane.grep}")`,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
50
|
+
if (pane.kind === "transcript")
|
|
51
|
+
return transcriptBody(pane, opts);
|
|
52
|
+
if (pane.kind === "backlog")
|
|
53
|
+
return backlogBody(pane, opts);
|
|
54
|
+
if (pane.kind === "history")
|
|
55
|
+
return historyBody(pane, opts);
|
|
56
|
+
if (pane.kind === "pending")
|
|
57
|
+
return pendingBody(pane, opts);
|
|
58
|
+
if (pane.kind === "health")
|
|
59
|
+
return healthBody(pane, opts);
|
|
60
|
+
if (pane.kind === "brief")
|
|
61
|
+
return briefBody(pane, opts);
|
|
62
|
+
if (pane.kind === "queued")
|
|
63
|
+
return queuedBody(pane, opts);
|
|
64
|
+
if (pane.kind === "usage" || pane.kind === "failures")
|
|
65
|
+
return reportBody(pane, opts);
|
|
66
|
+
return activityBody(pane, opts);
|
|
67
|
+
}
|
|
68
|
+
function transcriptBody(pane, opts) {
|
|
69
|
+
const { root, width, eventBudget, state } = opts;
|
|
70
|
+
// The notes view (Ctrl+N) windows the cached inspection Markdown exactly like the
|
|
71
|
+
// usage/failures panes, headings bold; the live transcript keeps the tail.
|
|
72
|
+
if (pane.note) {
|
|
73
|
+
const win = entryBodyWindow(state.noteCache ?? "", state.paneScroll, eventBudget, width);
|
|
80
74
|
return {
|
|
81
|
-
body:
|
|
82
|
-
emptyNote: "(
|
|
75
|
+
body: win.lines.map((l) => toneLine(l, width, l.startsWith("#") ? "bold" : undefined)),
|
|
76
|
+
emptyNote: "(nothing to inspect yet)",
|
|
83
77
|
};
|
|
84
78
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// then again inside the flat list, so every render paid twice for the same reads and verdicts.
|
|
93
|
-
const flat = browsableBacklogEntries(root);
|
|
94
|
-
// A `/` search narrows the browsable list to the matching open entries (the answered/done/
|
|
95
|
-
// fixed rows are read-only and hidden while a search is applied); entry mode still opens
|
|
96
|
-
// `flat[sel]`, the unfiltered index the selection holds.
|
|
97
|
-
const grep = state.backlogGrep ?? null;
|
|
98
|
-
const shown = flat.filter((e) => {
|
|
99
|
-
if (grep !== null && !(isOpenBacklogLabel(e.label) && entryMatches(e, grep)))
|
|
100
|
-
return false;
|
|
101
|
-
if (state.backlogReadyOnly && isOpenBacklogLabel(e.label) && e.hold !== null)
|
|
102
|
-
return false;
|
|
103
|
-
return true;
|
|
104
|
-
});
|
|
105
|
-
// A stale selection (an entry removed since the last render) clamps to the last; no
|
|
106
|
-
// entries at all falls back to list mode.
|
|
107
|
-
const sel = state.selectedEntry === null || flat.length === 0 ? null : Math.min(state.selectedEntry, flat.length - 1);
|
|
108
|
-
if (sel === null) {
|
|
109
|
-
// List mode: section headings stand out, empty sections read dim. Keeps the HEAD of the
|
|
110
|
-
// list when it overflows — file order is newest-first, unlike events which keep the tail.
|
|
111
|
-
const listed = (label) => shown.filter((e) => e.label === label).map((e) => e.title + e.mark);
|
|
112
|
-
// Plans list a series' blocked parts as one line (as `plan --list` does); the selection
|
|
113
|
-
// below still walks the flat list, one entry per part.
|
|
114
|
-
const plans = shown.filter((e) => e.label === "plan");
|
|
115
|
-
// A `/` search active: the answered/done/fixed sections are hidden, the sections build
|
|
116
|
-
// only from the matching open entries under a `search: "<grep>"` header, and nothing
|
|
117
|
-
// matching paints a self-explanatory line.
|
|
118
|
-
if (grep !== null) {
|
|
119
|
-
const prefix = [`search: "${grep}"`];
|
|
120
|
-
const rest = shown.length === 0
|
|
121
|
-
? [`(no entries match "${grep}")`]
|
|
122
|
-
: backlogLines(planListLines(plans), listed("bug"), listed("question"), plans.length);
|
|
123
|
-
return {
|
|
124
|
-
body: [...prefix, ...rest]
|
|
125
|
-
.slice(0, eventBudget)
|
|
126
|
-
.map((l) => toneLine(l, width, /^(?:plans|open bugs|open questions) \(\d+\):$/.test(l) ? "bold" : l.startsWith("(") ? "dim" : undefined)),
|
|
127
|
-
emptyNote: "(no events yet)",
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
// The newest answered questions follow the open ones: they are in `flat` (up/down open
|
|
131
|
-
// their bodies), but no edit key acts on them.
|
|
132
|
-
const answered = answeredQuestionSummaries(root, DASHBOARD_ANSWERED_QUESTIONS).map((q) => q.decision ? `${q.title} — ${q.decision}` : q.title);
|
|
133
|
-
// Done plans and fixed bugs follow, browsable and read-only the same way.
|
|
134
|
-
const finishedRows = (rows, verb) => rows.map((r) => (r.completedOn ? `${r.title} — ${verb} ${r.completedOn}` : r.title));
|
|
135
|
-
const finished = {
|
|
136
|
-
done: finishedRows(recentFinished(donePlanEntries(root), DONE_DATE_RE), "done"),
|
|
137
|
-
fixed: finishedRows(recentFinished(fixedBugEntries(root), FIXED_DATE_RE), "fixed"),
|
|
138
|
-
};
|
|
139
|
-
return {
|
|
140
|
-
body: backlogLines(planListLines(plans), listed("bug"), listed("question"), plans.length, answered, finished)
|
|
141
|
-
.slice(0, eventBudget)
|
|
142
|
-
.map((l) => toneLine(l, width, /^(?:plans|open bugs|open questions|answered recently|done recently|fixed recently) \(\d+\):$/.test(l) ? "bold" : l.startsWith("(") ? "dim" : undefined)),
|
|
143
|
-
emptyNote: "(no events yet)",
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
// Entry browsing: the selected entry's full body under a line naming its section and
|
|
147
|
-
// title.
|
|
148
|
-
const e = flat[sel];
|
|
149
|
-
const win = entryBodyWindow(e.body, state.entryScroll, entryBudget, width);
|
|
79
|
+
// The change view (the second Ctrl+N step) windows the collected unmerged-change text the
|
|
80
|
+
// same way; its async collector leaves the cache null for the first frames, so the pane
|
|
81
|
+
// paints the loading note rather than entryBodyWindow's "no details" placeholder.
|
|
82
|
+
if (pane.change) {
|
|
83
|
+
if (state.changeCache === null)
|
|
84
|
+
return { body: [], emptyNote: "(loading pending change…)" };
|
|
85
|
+
const win = entryBodyWindow(state.changeCache, state.paneScroll, eventBudget, width);
|
|
150
86
|
return {
|
|
151
|
-
body:
|
|
152
|
-
emptyNote: "(
|
|
87
|
+
body: win.lines.map((l) => toneLine(l, width, l.startsWith("#") ? "bold" : undefined)),
|
|
88
|
+
emptyNote: "(loading pending change…)",
|
|
153
89
|
};
|
|
154
90
|
}
|
|
155
|
-
if (pane.
|
|
156
|
-
//
|
|
157
|
-
// read found no run) paints the pane's own note rather than entryBodyWindow's placeholder.
|
|
158
|
-
if (pane.trail) {
|
|
159
|
-
if (state.trailCache === null)
|
|
160
|
-
return { body: [], emptyNote: "(no run trail)" };
|
|
161
|
-
const win = entryBodyWindow(state.trailCache, state.paneScroll, eventBudget, width);
|
|
162
|
-
return {
|
|
163
|
-
body: win.lines.map((l) => toneLine(l, width)),
|
|
164
|
-
emptyNote: "(no run trail)",
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
// historyPaneText returns "" when the log holds no completed runs; return no lines
|
|
168
|
-
// (rather than windowing an empty string, which entryBodyWindow replaces with its
|
|
169
|
-
// "no details" placeholder) so the frame prints the pane's own empty note instead.
|
|
170
|
-
const win = state.paneCache ? entryBodyWindow(state.paneCache, state.paneScroll, eventBudget, width) : { lines: [] };
|
|
91
|
+
if (pane.grep) {
|
|
92
|
+
// Filtered: scan the drawer's 200 entries with the shared keep rule (a lowercased needle).
|
|
171
93
|
return {
|
|
172
|
-
body:
|
|
173
|
-
emptyNote:
|
|
94
|
+
body: readTranscript(root, pane.role, 200, { grep: pane.grep.toLowerCase() }).slice(-eventBudget).map((l) => toneLine(l, width, transcriptTone(l))),
|
|
95
|
+
emptyNote: `(no transcript entries match "${pane.grep}")`,
|
|
174
96
|
};
|
|
175
97
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
98
|
+
return {
|
|
99
|
+
body: readTranscript(root, pane.role, eventBudget).slice(-eventBudget).map((l) => toneLine(l, width, transcriptTone(l))),
|
|
100
|
+
emptyNote: "(no transcript yet)",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function backlogBody(_pane, opts) {
|
|
104
|
+
const { root, width, eventBudget, entryBudget, state } = opts;
|
|
105
|
+
// The flat, labeled entry list is the one backlog read this frame needs: it carries every
|
|
106
|
+
// planned feature, open bug, and open question (then the newest answered questions, done
|
|
107
|
+
// plans and fixed bugs)
|
|
108
|
+
// from PLANS.md/BUGS.md/QUESTIONS.md with its section label and eligibility mark (the same
|
|
109
|
+
// entryHold verdict `tumwater backlog` renders). List and entry mode share this one pass;
|
|
110
|
+
// entry mode used to read the three sections and derive the holds directly and
|
|
111
|
+
// then again inside the flat list, so every render paid twice for the same reads and verdicts.
|
|
112
|
+
const flat = browsableBacklogEntries(root);
|
|
113
|
+
// A `/` search narrows the browsable list to the matching open entries (the answered/done/
|
|
114
|
+
// fixed rows are read-only and hidden while a search is applied); entry mode still opens
|
|
115
|
+
// `flat[sel]`, the unfiltered index the selection holds.
|
|
116
|
+
const grep = state.backlogGrep ?? null;
|
|
117
|
+
const shown = flat.filter((e) => {
|
|
118
|
+
if (grep !== null && !(isOpenBacklogLabel(e.label) && entryMatches(e, grep)))
|
|
119
|
+
return false;
|
|
120
|
+
if (state.backlogReadyOnly && isOpenBacklogLabel(e.label) && e.hold !== null)
|
|
121
|
+
return false;
|
|
122
|
+
return true;
|
|
123
|
+
});
|
|
124
|
+
// A stale selection (an entry removed since the last render) clamps to the last; no
|
|
125
|
+
// entries at all falls back to list mode.
|
|
126
|
+
const sel = state.selectedEntry === null || flat.length === 0 ? null : Math.min(state.selectedEntry, flat.length - 1);
|
|
127
|
+
if (sel === null) {
|
|
128
|
+
// List mode: section headings stand out, empty sections read dim. Keeps the HEAD of the
|
|
129
|
+
// list when it overflows — file order is newest-first, unlike events which keep the tail.
|
|
130
|
+
const listed = (label) => shown.filter((e) => e.label === label).map((e) => e.title + e.mark);
|
|
131
|
+
// Plans list a series' blocked parts as one line (as `plan --list` does); the selection
|
|
132
|
+
// below still walks the flat list, one entry per part.
|
|
133
|
+
const plans = shown.filter((e) => e.label === "plan");
|
|
134
|
+
// A `/` search active: the answered/done/fixed sections are hidden, the sections build
|
|
135
|
+
// only from the matching open entries under a `search: "<grep>"` header, and nothing
|
|
136
|
+
// matching paints a self-explanatory line.
|
|
137
|
+
if (grep !== null) {
|
|
138
|
+
const prefix = [`search: "${grep}"`];
|
|
139
|
+
const rest = shown.length === 0
|
|
140
|
+
? [`(no entries match "${grep}")`]
|
|
141
|
+
: backlogLines(planListLines(plans), listed("bug"), listed("question"), plans.length);
|
|
184
142
|
return {
|
|
185
|
-
body:
|
|
186
|
-
|
|
143
|
+
body: [...prefix, ...rest]
|
|
144
|
+
.slice(0, eventBudget)
|
|
145
|
+
.map((l) => toneLine(l, width, /^(?:plans|open bugs|open questions) \(\d+\):$/.test(l) ? "bold" : l.startsWith("(") ? "dim" : undefined)),
|
|
146
|
+
emptyNote: "(no events yet)",
|
|
187
147
|
};
|
|
188
148
|
}
|
|
189
|
-
|
|
149
|
+
// The newest answered questions follow the open ones: they are in `flat` (up/down open
|
|
150
|
+
// their bodies), but no edit key acts on them.
|
|
151
|
+
const answered = answeredQuestionSummaries(root, DASHBOARD_ANSWERED_QUESTIONS).map((q) => q.decision ? `${q.title} — ${q.decision}` : q.title);
|
|
152
|
+
// Done plans and fixed bugs follow, browsable and read-only the same way.
|
|
153
|
+
const finishedRows = (rows, verb) => rows.map((r) => (r.completedOn ? `${r.title} — ${verb} ${r.completedOn}` : r.title));
|
|
154
|
+
const finished = {
|
|
155
|
+
done: finishedRows(recentFinished(donePlanEntries(root), DONE_DATE_RE), "done"),
|
|
156
|
+
fixed: finishedRows(recentFinished(fixedBugEntries(root), FIXED_DATE_RE), "fixed"),
|
|
157
|
+
};
|
|
190
158
|
return {
|
|
191
|
-
body:
|
|
192
|
-
|
|
159
|
+
body: backlogLines(planListLines(plans), listed("bug"), listed("question"), plans.length, answered, finished)
|
|
160
|
+
.slice(0, eventBudget)
|
|
161
|
+
.map((l) => toneLine(l, width, /^(?:plans|open bugs|open questions|answered recently|done recently|fixed recently) \(\d+\):$/.test(l) ? "bold" : l.startsWith("(") ? "dim" : undefined)),
|
|
162
|
+
emptyNote: "(no events yet)",
|
|
193
163
|
};
|
|
194
164
|
}
|
|
195
|
-
|
|
196
|
-
|
|
165
|
+
// Entry browsing: the selected entry's full body under a line naming its section and
|
|
166
|
+
// title.
|
|
167
|
+
const e = flat[sel];
|
|
168
|
+
const win = entryBodyWindow(e.body, state.entryScroll, entryBudget, width);
|
|
169
|
+
return {
|
|
170
|
+
body: [toneLine(`${e.label}: ${e.title}${e.mark}`, width, "bold"), ...win.lines.map((l) => toneLine(l, width))],
|
|
171
|
+
emptyNote: "(no events yet)",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function historyBody(pane, opts) {
|
|
175
|
+
const { width, eventBudget, state } = opts;
|
|
176
|
+
// The run trail (Ctrl+N) windows the cached renderRunDetail text; a null cache (the
|
|
177
|
+
// read found no run) paints the pane's own note rather than entryBodyWindow's placeholder.
|
|
178
|
+
if (pane.trail) {
|
|
179
|
+
if (state.trailCache === null)
|
|
180
|
+
return { body: [], emptyNote: "(no run trail)" };
|
|
181
|
+
const win = entryBodyWindow(state.trailCache, state.paneScroll, eventBudget, width);
|
|
197
182
|
return {
|
|
198
183
|
body: win.lines.map((l) => toneLine(l, width)),
|
|
199
|
-
emptyNote: "(
|
|
184
|
+
emptyNote: "(no run trail)",
|
|
200
185
|
};
|
|
201
186
|
}
|
|
202
|
-
|
|
203
|
-
|
|
187
|
+
// historyPaneText returns "" when the log holds no completed runs; return no lines
|
|
188
|
+
// (rather than windowing an empty string, which entryBodyWindow replaces with its
|
|
189
|
+
// "no details" placeholder) so the frame prints the pane's own empty note instead.
|
|
190
|
+
const win = state.paneCache ? entryBodyWindow(state.paneCache, state.paneScroll, eventBudget, width) : { lines: [] };
|
|
191
|
+
return {
|
|
192
|
+
body: win.lines.map((l) => toneLine(l, width)),
|
|
193
|
+
emptyNote: "(no runs yet)",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function pendingBody(_pane, opts) {
|
|
197
|
+
const { width, eventBudget, state } = opts;
|
|
198
|
+
// A selected queued merge shows its detail in place of the queue list; otherwise paneCache
|
|
199
|
+
// stays null until the async collector resolves (the Ctrl+T handler kicks it off
|
|
200
|
+
// fire-and-forget); like history, return no lines while null so the frame prints the
|
|
201
|
+
// loading note rather than windowing an empty string into the "no details" placeholder.
|
|
202
|
+
if (state.queueDetail) {
|
|
203
|
+
const detail = state.queueDetail;
|
|
204
|
+
const sel = entryBodyWindow(detail, state.paneScroll, eventBudget, width);
|
|
204
205
|
return {
|
|
205
|
-
body:
|
|
206
|
-
emptyNote: "(
|
|
206
|
+
body: sel.lines.map((l) => toneLine(l, width)),
|
|
207
|
+
emptyNote: "(loading pending changes…)",
|
|
207
208
|
};
|
|
208
209
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
210
|
+
const win = state.paneCache ? entryBodyWindow(state.paneCache, state.paneScroll, eventBudget, width) : { lines: [] };
|
|
211
|
+
return {
|
|
212
|
+
body: win.lines.map((l) => toneLine(l, width)),
|
|
213
|
+
emptyNote: "(loading pending changes…)",
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function healthBody(_pane, opts) {
|
|
217
|
+
const { width, eventBudget, state } = opts;
|
|
218
|
+
const win = state.paneCache ? entryBodyWindow(state.paneCache, state.paneScroll, eventBudget, width) : { lines: [] };
|
|
219
|
+
return {
|
|
220
|
+
body: win.lines.map((l) => toneLine(l, width)),
|
|
221
|
+
emptyNote: "(running health checks…)",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function briefBody(_pane, opts) {
|
|
225
|
+
const { width, eventBudget, state } = opts;
|
|
226
|
+
const win = state.paneCache ? entryBodyWindow(state.paneCache, state.paneScroll, eventBudget, width) : { lines: [] };
|
|
227
|
+
return {
|
|
228
|
+
body: win.lines.map((l) => toneLine(l, width)),
|
|
229
|
+
emptyNote: "(no project brief with a prompt block — run `tumwater init` first)",
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function queuedBody(pane, opts) {
|
|
233
|
+
const { width, eventBudget, entryBudget, state } = opts;
|
|
234
|
+
// A set selection shows the row's full prompt under a cancel header (the numbered list
|
|
235
|
+
// clips long prompts); list mode windows the cached renderQueuedList string, which is
|
|
236
|
+
// computed synchronously on Ctrl+T entry over the same queue `tumwater prompt --list`
|
|
237
|
+
// prints, so paneCache is already set this frame. An empty queue falls to the pane's
|
|
238
|
+
// own note either way.
|
|
239
|
+
const sel = state.selectedQueued === null || state.queuedRows.length === 0
|
|
240
|
+
? null
|
|
241
|
+
: Math.min(state.selectedQueued, state.queuedRows.length - 1);
|
|
242
|
+
if (sel !== null) {
|
|
243
|
+
const row = state.queuedRows[sel];
|
|
244
|
+
const win = entryBodyWindow(row.text, state.paneScroll, entryBudget, width);
|
|
237
245
|
return {
|
|
238
|
-
body: [
|
|
246
|
+
body: [
|
|
247
|
+
toneLine(`${row.role}'s inbox #${row.position} (Ctrl+X to cancel)`, width, "bold"),
|
|
248
|
+
...win.lines.map((l) => toneLine(l, width)),
|
|
249
|
+
],
|
|
239
250
|
emptyNote: "(inbox empty)",
|
|
240
251
|
};
|
|
241
252
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
body: win.lines.map((l) => toneLine(l, width, l.startsWith("#") ? "bold" : undefined)),
|
|
248
|
-
emptyNote: "(no events yet)",
|
|
249
|
-
};
|
|
253
|
+
// An applied search names itself above the list; one that matched nothing says so instead
|
|
254
|
+
// of claiming the inbox is empty.
|
|
255
|
+
const searchLine = pane.grep ? [toneLine(`search: "${pane.grep}"`, width, "bold")] : [];
|
|
256
|
+
if (pane.grep && state.queuedRows.length === 0) {
|
|
257
|
+
return { body: [...searchLine, toneLine(`(no queued prompt matches "${pane.grep}")`, width)], emptyNote: "(inbox empty)" };
|
|
250
258
|
}
|
|
259
|
+
const listBudget = Math.max(1, eventBudget - searchLine.length);
|
|
260
|
+
const win = state.paneCache ? entryBodyWindow(state.paneCache, state.paneScroll, listBudget, width) : { lines: [] };
|
|
261
|
+
return {
|
|
262
|
+
body: [...searchLine, ...win.lines.map((l) => toneLine(l, width))],
|
|
263
|
+
emptyNote: "(inbox empty)",
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function reportBody(_pane, opts) {
|
|
267
|
+
const { width, eventBudget, state } = opts;
|
|
268
|
+
// The Markdown `tumwater report`/`report --failures` prints, windowed like backlog entry
|
|
269
|
+
// mode. The cache is set on view entry by the Ctrl+T handler, so this only re-windows a string.
|
|
270
|
+
const win = entryBodyWindow(state.paneCache ?? "", state.paneScroll, eventBudget, width);
|
|
271
|
+
return {
|
|
272
|
+
body: win.lines.map((l) => toneLine(l, width, l.startsWith("#") ? "bold" : undefined)),
|
|
273
|
+
emptyNote: "(no events yet)",
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function activityBody(pane, opts) {
|
|
277
|
+
const { root, width, eventBudget, state } = opts;
|
|
251
278
|
// The Activity pane loads a fixed tail once per read, narrows it by the active role scope
|
|
252
279
|
// and text search, and windows it: null follows the live tail (the newest eventBudget
|
|
253
280
|
// events), a stored offset pages back through the loaded events. Both clamp to
|
|
@@ -176,9 +176,8 @@ export function createTuiPanes(deps) {
|
|
|
176
176
|
if (key.ctrl && key.name === "p" && (session.view === session.roleIds.length + 2 || session.view === session.roleIds.length + 3)) {
|
|
177
177
|
return report.handleKey?.(key, ctx) ?? false;
|
|
178
178
|
}
|
|
179
|
-
// Health owns Ctrl+P
|
|
180
|
-
|
|
181
|
-
return health.handleKey?.(key, ctx) ?? false;
|
|
179
|
+
// Health owns none of the Ctrl+P cycle now that the image-model re-probe is gone; it
|
|
180
|
+
// falls through to History's Ctrl+P below.
|
|
182
181
|
if (key.ctrl && key.name === "p")
|
|
183
182
|
return history.handleKey?.(key, ctx) ?? false;
|
|
184
183
|
return false;
|
package/dist/src/ui/tui/tui.js
CHANGED
|
@@ -2,8 +2,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { render as inkRender } from "ink";
|
|
3
3
|
import { TuiApp } from "./tui-app.js";
|
|
4
4
|
import { openQuestions } from "../../backlog/backlog.js";
|
|
5
|
-
import {
|
|
6
|
-
import { capacityContention,
|
|
5
|
+
import { refusedEntries, stalledEntries } from "../../backlog/backlog-eligibility.js";
|
|
6
|
+
import { capacityContention, instancePoolProblems, planDependencyAlert, providerCapProblems, regressedFixesAlert, staleOpenQuestions, staleWorkspacePins } from "../alert-feeds.js";
|
|
7
7
|
import { strandedPlanEntriesFor } from "../../backlog/backlog-structure.js";
|
|
8
8
|
import { duplicatedBacklogHeadings, fixClaimProblems, liveFinishedEntriesFor, validationGapProblems } from "../../doctor/doctor-backlog-plans.js";
|
|
9
9
|
import { snapshot } from "../../status/status-data.js";
|
|
@@ -93,7 +93,7 @@ export async function runTui(root, seams = {}) {
|
|
|
93
93
|
const view = s.view;
|
|
94
94
|
// What needs the operator — the dashboard's alert banners (team-alerts.ts's teamAlerts), as
|
|
95
95
|
// attention lines under the header. Each consumes one line of the height budget.
|
|
96
|
-
const attention = alertLines(teamAlerts(snap, openQuestions(root), status.roles, Date.now(), refusedEntries(root),
|
|
96
|
+
const attention = alertLines(teamAlerts(snap, openQuestions(root), status.roles, Date.now(), refusedEntries(root), planDependencyAlert(root), stalledEntries(root), strandedPlanEntriesFor(root), duplicatedBacklogHeadings(root), validationGapProblems(root), capacityContention(root), regressedFixesAlert(root, Date.now()), staleClaimEntries(root, status.roles.map((r) => r.role), Date.now()), staleOpenQuestions(root), fixClaimProblems(root), staleWorkspacePins(root, Date.now()), liveFinishedEntriesFor(root), instancePoolProblems(root), providerCapProblems(root)), width);
|
|
97
97
|
const statusLines = [status.lines[0] ?? [], ...attention, ...status.lines.slice(1)];
|
|
98
98
|
// Numbered previews of prompts queued for the director, between the table and the
|
|
99
99
|
// activity pane: what will run next, in execution order. Each line consumes exactly
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* - A `path:line` location (`status-roles.ts:66`) was checked as a symbol, and no file is named
|
|
44
44
|
* that. A location now claims its file, a bare line reference (`:368`) claims nothing, and a
|
|
45
45
|
* bare file name is backed by a file of that name anywhere in the walked source dirs. */
|
|
46
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
46
|
+
import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
|
|
47
47
|
import { extname, join } from "node:path";
|
|
48
48
|
import { readTextOrNull, statOrNull, walkFiles } from "../files/files.js";
|
|
49
49
|
import { fenceTracker, finishedArchiveOf, parseEntryDetails, sectionLines, trailingParenthetical, } from "../backlog/backlog-md.js";
|
|
@@ -307,11 +307,23 @@ export async function falseFixReason(wt, mainBranch, files) {
|
|
|
307
307
|
// is no claim at all. Both sides of the comparison read both files.
|
|
308
308
|
if (!files.includes("BUGS.md") && !files.includes(BUGS_ARCHIVE))
|
|
309
309
|
return undefined;
|
|
310
|
-
// A deleted or
|
|
311
|
-
// so this check passes and lets the gate's sibling structural check
|
|
312
|
-
// report the dropped section. Reading
|
|
313
|
-
//
|
|
314
|
-
|
|
310
|
+
// A deleted, unreadable, or non-regular (symlinked) file reads as empty: there is no Fixed
|
|
311
|
+
// transition to verify, so this check passes and lets the gate's sibling structural check
|
|
312
|
+
// (backlog-structure.ts) report the dropped section. Reading a symlink raw would pull an
|
|
313
|
+
// outside host file's `## Fixed` heading into this gate's verdict (the same lstat policy the
|
|
314
|
+
// backlog-conflict resolver and backlogStructureReason pin under INV-G3), so a head is read
|
|
315
|
+
// only when lstat shows a regular file; ENOENT and symlinks both read as empty.
|
|
316
|
+
const heads = ["BUGS.md", BUGS_ARCHIVE].map((f) => {
|
|
317
|
+
const full = join(wt, f);
|
|
318
|
+
try {
|
|
319
|
+
if (!lstatSync(full).isFile())
|
|
320
|
+
return "";
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return "";
|
|
324
|
+
}
|
|
325
|
+
return readTextOrNull(full) ?? "";
|
|
326
|
+
});
|
|
315
327
|
if (!heads.some((head) => head.includes("## Fixed")))
|
|
316
328
|
return undefined;
|
|
317
329
|
// The diff's own base, not main's tip: the gate diffs `mainBranch...HEAD` against the
|
|
@@ -88,6 +88,15 @@ export function newlyRefusedTitles(baseMd, headMd, section) {
|
|
|
88
88
|
* logs a warning and leaves PLANS.md out of the commit, so the other notes still merge. */
|
|
89
89
|
export async function handleRefusal(ctx, state, wt, pi) {
|
|
90
90
|
const reason = (pi.refusedReason ?? "").trim() || "no reason given";
|
|
91
|
+
const plansUnstripped = await stripBlockedPlanRefusalNotes(ctx, wt);
|
|
92
|
+
const notes = await collectRefusalNotes(wt, plansUnstripped);
|
|
93
|
+
const entries = await collectRefusedEntryTitles(ctx, wt, notes);
|
|
94
|
+
return mergeRefusalNotes(ctx, state, wt, reason, notes, entries);
|
|
95
|
+
}
|
|
96
|
+
/** Strip this run's newly added Refused notes from any `## Planned` entry main still holds as
|
|
97
|
+
* blocked by a planned prerequisite. Returns whether the strip write failed: a throw leaves the
|
|
98
|
+
* flag set and PLANS.md out of the commit, so the other notes still merge. */
|
|
99
|
+
async function stripBlockedPlanRefusalNotes(ctx, wt) {
|
|
91
100
|
const plansFile = path.join(wt, "PLANS.md");
|
|
92
101
|
let plansUnstripped = false;
|
|
93
102
|
if (fs.existsSync(plansFile)) {
|
|
@@ -118,9 +127,17 @@ export async function handleRefusal(ctx, state, wt, pi) {
|
|
|
118
127
|
}
|
|
119
128
|
}
|
|
120
129
|
}
|
|
121
|
-
|
|
130
|
+
return plansUnstripped;
|
|
131
|
+
}
|
|
132
|
+
/** The markdown notes this refusal may commit: changed `.md` files, PLANS.md dropped only when
|
|
133
|
+
* the blocked-plan strip write failed. */
|
|
134
|
+
async function collectRefusalNotes(wt, plansUnstripped) {
|
|
135
|
+
return (await changedFiles(wt)).filter((f) => f.toLowerCase().endsWith(".md") && !(plansUnstripped && f === "PLANS.md"));
|
|
136
|
+
}
|
|
137
|
+
/** Best-effort names for the entry_refused event, per changed file's section — it must never
|
|
138
|
+
* fail the refusal, so a read error swallows and leaves that file unnamed. */
|
|
139
|
+
async function collectRefusedEntryTitles(ctx, wt, notes) {
|
|
122
140
|
const entries = [];
|
|
123
|
-
// Best-effort naming for the entry_refused event; it must never fail the refusal.
|
|
124
141
|
for (const { file, section } of [{ file: "PLANS.md", section: "Planned" }, { file: "BUGS.md", section: "Open" }]) {
|
|
125
142
|
if (!notes.includes(file))
|
|
126
143
|
continue;
|
|
@@ -131,10 +148,19 @@ export async function handleRefusal(ctx, state, wt, pi) {
|
|
|
131
148
|
// Unnamed is fine: the event still carries reason and files.
|
|
132
149
|
}
|
|
133
150
|
}
|
|
151
|
+
return entries;
|
|
152
|
+
}
|
|
153
|
+
/** Merge this refusal's notes onto main: commit only the markdown notes (subject + trailer only —
|
|
154
|
+
* a refusal carries no WHY/RISK/VERIFIED body; the reason is the subject, and the trailer's turn
|
|
155
|
+
* count is the same field the friction flag reads), then merge directly. A failed note merge (a
|
|
156
|
+
* throw such as a lock timeout, or a non-"changed" result) is recorded in `state.lastError` and
|
|
157
|
+
* reported through the "refused" result like any other non-merge. A merged refusal note supersedes
|
|
158
|
+
* a stale rejection exactly as applyMergeOutcome's merged branch does (this path merges directly,
|
|
159
|
+
* bypassing that fold), then pages the operator: the entry stays held until a human or the
|
|
160
|
+
* director lifts it. */
|
|
161
|
+
async function mergeRefusalNotes(ctx, state, wt, reason, notes, entries) {
|
|
134
162
|
let commit;
|
|
135
163
|
if (notes.length > 0) {
|
|
136
|
-
// Subject + trailer only — a refusal carries no WHY/RISK/VERIFIED body; the reason is
|
|
137
|
-
// the subject, and the trailer's turn count is the same field the friction flag reads.
|
|
138
164
|
const message = buildCommitMessage(stampedSubject(baseRoleOf(ctx.role), `refuse — ${reason}`), null, commitTrailer(ctx.role, state.runs, ctx.turns, state.peakContextTokens, undefined, undefined, ctx.modelLine));
|
|
139
165
|
commit = (await commitPathsAndDiscardRest(wt, message, notes)) ?? undefined;
|
|
140
166
|
}
|
|
@@ -156,8 +182,6 @@ export async function handleRefusal(ctx, state, wt, pi) {
|
|
|
156
182
|
}
|
|
157
183
|
if (result !== "changed")
|
|
158
184
|
state.lastError = `refusal note merge failed: ${result}`;
|
|
159
|
-
// A merged refusal note is a merged change too: it supersedes a stale rejection exactly as
|
|
160
|
-
// applyMergeOutcome's merged branch does (this path merges directly, bypassing that fold).
|
|
161
185
|
else {
|
|
162
186
|
clearSupersededRejection(state);
|
|
163
187
|
// Page the operator: the entry stays held until a human or the director lifts it. Best-effort —
|
package/package.json
CHANGED