@alecsibilia/luca 14.0.0-alpha.2 → 14.0.0-alpha.21
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/CHANGELOG.md +76 -0
- package/README.md +7 -2
- package/board/client/board-look.ts +31 -1
- package/board/client/board-panel-parts.tsx +150 -0
- package/board/client/board-panel-stages.ts +99 -0
- package/board/client/board-panel-styles.ts +223 -0
- package/board/client/board-panel.tsx +77 -1275
- package/board/client/engine-life.ts +48 -0
- package/board/client/final-review-stack.tsx +144 -0
- package/board/client/heartbeat.tsx +61 -0
- package/board/client/helper-lines.tsx +69 -0
- package/board/client/live-feed.tsx +86 -0
- package/board/client/live-line.ts +73 -0
- package/board/client/model-line.tsx +178 -0
- package/board/client/needs-you-section.tsx +133 -0
- package/board/client/one-line.tsx +193 -0
- package/board/client/repo-wait-banner.tsx +69 -0
- package/board/client/run-footer.tsx +68 -0
- package/board/client/run-header.tsx +189 -0
- package/board/client/run-picker.tsx +98 -0
- package/board/client/status-bar.tsx +131 -0
- package/board/client/step-dots.tsx +127 -0
- package/board/client/stuck-actions.tsx +269 -0
- package/board/client/ticket-card-view.tsx +238 -0
- package/board/client/ticket-detail.tsx +87 -0
- package/board/client/ticket-stack.tsx +119 -0
- package/board/client/ticket-wait.ts +188 -0
- package/board/client/timeline-rows.tsx +41 -14
- package/board/client/usage-line.tsx +51 -0
- package/board/client/wait-line.tsx +39 -0
- package/board/index.client.tsx +1 -1
- package/board/index.server.ts +7 -1
- package/board/package.json +1 -1
- package/board/server/board-panel-state-helpers.ts +67 -0
- package/board/server/board-server-check.ts +294 -0
- package/board/server/board-server-engine.ts +129 -0
- package/board/server/board-server-events.ts +134 -0
- package/board/server/board-server-read.ts +190 -0
- package/board/server/board-server-resume.ts +164 -0
- package/board/server/board-server-start.ts +174 -0
- package/board/server/board-server-store.ts +281 -0
- package/board/server/board-server-types.ts +114 -0
- package/board/server/board-server-util.ts +4 -0
- package/board/server/board-server.ts +52 -943
- package/board/server/board-vocabulary-engine.ts +144 -0
- package/board/server/board-vocabulary-review.ts +115 -0
- package/board/server/board-vocabulary-run.ts +145 -0
- package/board/server/board-vocabulary-shapes.ts +153 -0
- package/board/server/board-vocabulary.ts +20 -375
- package/board/server/current-step-panel-helpers.ts +108 -0
- package/board/server/describe-record.ts +403 -0
- package/board/server/engine-launch.ts +20 -3
- package/board/server/engine-restart-helpers.ts +105 -0
- package/board/server/engine-watch.ts +45 -5
- package/board/server/journal-rebuild-helpers.ts +169 -0
- package/board/server/live-file.ts +130 -0
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +14 -629
- package/board/server/record-texts.ts +350 -0
- package/board/server/reduce-board-agent-start.ts +224 -0
- package/board/server/reduce-board-agents.ts +160 -0
- package/board/server/reduce-board-counters.ts +138 -0
- package/board/server/reduce-board-create.ts +148 -0
- package/board/server/reduce-board-final-review.ts +174 -0
- package/board/server/reduce-board-needs-you.ts +283 -0
- package/board/server/reduce-board-reasons.ts +59 -0
- package/board/server/reduce-board-run.ts +255 -0
- package/board/server/reduce-board-settle.ts +88 -0
- package/board/server/reduce-board-steps.ts +226 -0
- package/board/server/reduce-board-text.ts +139 -0
- package/board/server/reduce-board-ticket-checks.ts +168 -0
- package/board/server/reduce-board-ticket-join.ts +144 -0
- package/board/server/reduce-board-ticket-start.ts +132 -0
- package/board/server/reduce-board-update.ts +117 -0
- package/board/server/reduce-board-usage.ts +247 -0
- package/board/server/reduce-board.ts +100 -1917
- package/board/server/reply-post.ts +407 -0
- package/board/server/run-registry.ts +8 -4
- package/board/server/ticket-stages-panel-helpers.ts +296 -0
- package/board/shared/board-rows.ts +9 -0
- package/board/shared/board-rpc.ts +42 -0
- package/board/shared/board-state-counts.ts +77 -0
- package/board/shared/board-state-live.ts +99 -0
- package/board/shared/board-state-review.ts +59 -0
- package/board/shared/board-state-run.ts +236 -0
- package/board/shared/board-state-ticket.ts +257 -0
- package/board/shared/board-state-usage.ts +107 -0
- package/board/shared/board-state.ts +97 -567
- package/board/shared/reply-actions.ts +207 -0
- package/engine/agents/agent-launcher.ts +27 -4
- package/engine/agents/claude-launcher-helpers.ts +233 -0
- package/engine/agents/claude-launcher.ts +28 -669
- package/engine/agents/claude-messages.ts +212 -0
- package/engine/agents/claude-options.ts +43 -8
- package/engine/agents/common-instructions.ts +138 -0
- package/engine/agents/input-channel.ts +36 -0
- package/engine/agents/launch-paths.ts +29 -0
- package/engine/agents/launch-session.ts +227 -0
- package/engine/agents/learner-instructions.ts +31 -0
- package/engine/agents/open-session.ts +210 -0
- package/engine/agents/prompt-sections.ts +95 -0
- package/engine/agents/rejoin-prompts.ts +135 -0
- package/engine/agents/reviewer-instructions.ts +99 -0
- package/engine/agents/role-instructions.ts +31 -244
- package/engine/agents/role-prompts.ts +39 -109
- package/engine/agents/role-results.ts +106 -7
- package/engine/agents/scripted-launcher.ts +21 -6
- package/engine/agents/session-pool.ts +124 -0
- package/engine/agents/turn-progress.ts +283 -0
- package/engine/agents/writer-instructions.ts +151 -0
- package/engine/cli/computer-adapters-real.ts +15 -3
- package/engine/cli/computer-adapters.ts +2 -0
- package/engine/cli/computer-checks.ts +87 -4
- package/engine/cli/config-reload-text.ts +39 -0
- package/engine/cli/decision-model-setup.ts +70 -0
- package/engine/cli/doctor-command.ts +1 -0
- package/engine/cli/doctor.ts +21 -3
- package/engine/cli/going-runs.ts +192 -36
- package/engine/cli/init-command.ts +1 -0
- package/engine/cli/init.ts +23 -1
- package/engine/cli/live-runs.ts +73 -0
- package/engine/cli/luca-run.ts +42 -7
- package/engine/cli/luca-skills.ts +183 -0
- package/engine/cli/luca.ts +8 -1
- package/engine/cli/models-command.ts +119 -0
- package/engine/cli/read-run.ts +81 -0
- package/engine/cli/run-args.ts +33 -6
- package/engine/cli/run-demo.ts +143 -0
- package/engine/cli/run-end.ts +184 -0
- package/engine/cli/run-modes-helpers.ts +81 -0
- package/engine/cli/run-modes.ts +62 -439
- package/engine/cli/setup.ts +5 -48
- package/engine/cli/unfinished-runs.ts +122 -0
- package/engine/cli/upgrade-command.ts +212 -30
- package/engine/cli/upgrade-finish.ts +149 -0
- package/engine/cli/upgrade.ts +112 -71
- package/engine/config/config-fields.ts +127 -0
- package/engine/config/config-reload.ts +250 -0
- package/engine/config/engine-config-schema.ts +235 -0
- package/engine/config/engine-config.ts +72 -142
- package/engine/config/test-commands.ts +102 -0
- package/engine/core/agent-guards-helpers.ts +84 -0
- package/engine/core/agent-turn.ts +395 -0
- package/engine/core/already-done.ts +44 -1
- package/engine/core/build-action.ts +214 -0
- package/engine/core/build-context.ts +135 -0
- package/engine/core/build-launch.ts +259 -0
- package/engine/core/commit-in.ts +282 -0
- package/engine/core/decide-build-early.ts +108 -0
- package/engine/core/decide-build-ending.ts +145 -0
- package/engine/core/decide-build-helpers.ts +87 -0
- package/engine/core/decide-build-tickets.ts +207 -0
- package/engine/core/decide-build-view.ts +103 -0
- package/engine/core/decide-build.ts +101 -1368
- package/engine/core/decide-final-fix.ts +203 -0
- package/engine/core/decide-final-review.ts +55 -310
- package/engine/core/decide-red-green.ts +398 -0
- package/engine/core/decide-rejoin.ts +259 -0
- package/engine/core/decide-repo-wait.ts +129 -0
- package/engine/core/decide-run-budget.ts +86 -12
- package/engine/core/decide-stuck-helpers.ts +127 -0
- package/engine/core/decide-stuck.ts +60 -8
- package/engine/core/decide-ticket-review.ts +95 -0
- package/engine/core/decide-ticket.ts +267 -0
- package/engine/core/decide-waits.ts +47 -0
- package/engine/core/decide.ts +74 -23
- package/engine/core/engine-chores.ts +163 -0
- package/engine/core/environment-rerun.ts +83 -0
- package/engine/core/evidence-text.ts +84 -0
- package/engine/core/execute-action.ts +303 -0
- package/engine/core/execute-already-done-check.ts +133 -0
- package/engine/core/execute-build.ts +68 -944
- package/engine/core/execute-changeset.ts +5 -7
- package/engine/core/execute-final-review.ts +125 -13
- package/engine/core/execute-gates.ts +182 -0
- package/engine/core/execute-intake.ts +130 -0
- package/engine/core/execute-join.ts +276 -0
- package/engine/core/execute-memory.ts +12 -0
- package/engine/core/execute-pull-request.ts +84 -0
- package/engine/core/execute-red-check.ts +219 -0
- package/engine/core/execute-repo-wait.ts +129 -0
- package/engine/core/execute-stuck.ts +25 -10
- package/engine/core/execute-waits.ts +158 -0
- package/engine/core/execute-with-jev.ts +118 -0
- package/engine/core/execute.ts +124 -831
- package/engine/core/final-failure-cap.ts +38 -0
- package/engine/core/final-review-action.ts +125 -0
- package/engine/core/final-review-text.ts +63 -17
- package/engine/core/final-stuck-text.ts +86 -0
- package/engine/core/fix-loop-text.ts +49 -2
- package/engine/core/left-out.ts +120 -0
- package/engine/core/live-file.ts +372 -0
- package/engine/core/loop-caps.ts +25 -2
- package/engine/core/not-in-run.ts +7 -3
- package/engine/core/pull-request-text.ts +255 -6
- package/engine/core/repo-wait-text.ts +81 -0
- package/engine/core/review-text.ts +55 -1
- package/engine/core/run-crash-recovery-helpers.ts +16 -0
- package/engine/core/run-session-close-helpers.ts +96 -0
- package/engine/core/run-start.ts +148 -0
- package/engine/core/run-stuck-text.ts +86 -0
- package/engine/core/schedule-rules.ts +193 -0
- package/engine/core/stuck-detail-text.ts +89 -0
- package/engine/core/stuck-format.ts +22 -0
- package/engine/core/stuck-text.ts +29 -332
- package/engine/core/ticket-stuck-text.ts +192 -0
- package/engine/core/worktree-changes.ts +106 -0
- package/engine/gates/already-done-check.ts +120 -0
- package/engine/gates/feature-map.ts +96 -0
- package/engine/gates/gate-runner.ts +108 -108
- package/engine/gates/gate-schemas.ts +61 -2
- package/engine/gates/leftover-scan.ts +154 -31
- package/engine/gates/new-tests.ts +45 -0
- package/engine/gates/own-files-check.ts +98 -0
- package/engine/gates/prepare-check.ts +60 -0
- package/engine/gates/prepare-slots.ts +99 -0
- package/engine/gates/red-check.ts +184 -16
- package/engine/gates/shell-check.ts +64 -0
- package/engine/gates/slot-waits.ts +55 -0
- package/engine/gates/slow-test-runner.ts +59 -0
- package/engine/gates/test-checks.ts +161 -0
- package/engine/gates/test-runner.ts +164 -5
- package/engine/gates/test-slots.ts +48 -0
- package/engine/git/git-adapter.ts +55 -396
- package/engine/git/git-commits.ts +132 -0
- package/engine/git/git-join.ts +139 -0
- package/engine/git/git-read.ts +123 -0
- package/engine/git/git-run.ts +32 -0
- package/engine/git/git-worktrees.ts +143 -0
- package/engine/git/outside-links.ts +55 -0
- package/engine/guards/after-turn-check.ts +31 -5
- package/engine/guards/guard-hook.ts +60 -12
- package/engine/guards/guard-roles.ts +68 -0
- package/engine/guards/helper-rules.ts +92 -0
- package/engine/guards/permission-rules.ts +161 -0
- package/engine/guards/role-rules-helpers.ts +48 -0
- package/engine/guards/role-rules.ts +34 -606
- package/engine/guards/shell-rules.ts +344 -0
- package/engine/guards/worktree-git.ts +208 -0
- package/engine/guards/worktree-snapshot.ts +412 -0
- package/engine/guards/worktree-state.ts +28 -825
- package/engine/guards/worktree-undo.ts +260 -0
- package/engine/guards/write-rules.ts +149 -0
- package/engine/index.ts +30 -9
- package/engine/intake/criteria-checks.ts +114 -0
- package/engine/intake/intake-checks.ts +164 -30
- package/engine/intake/intake-schemas.ts +37 -2
- package/engine/jev/clef-client.ts +259 -0
- package/engine/jev/decision-model-credentials.ts +212 -0
- package/engine/jev/jev-jobs.ts +144 -60
- package/engine/jev/jev-schemas.ts +86 -5
- package/engine/jev/jev-shadow-helpers.ts +218 -0
- package/engine/jev/jev-shadow.ts +109 -64
- package/engine/jev/model-pick.ts +275 -0
- package/engine/journal/agent-entries.ts +167 -0
- package/engine/journal/agent-message-entries.ts +72 -0
- package/engine/journal/agent-turn-replay.ts +203 -0
- package/engine/journal/already-done-entries.ts +55 -0
- package/engine/journal/commit-entries.ts +56 -0
- package/engine/journal/cross-ticket-replay.ts +222 -0
- package/engine/journal/decision-model-entries.ts +86 -0
- package/engine/journal/entry-fields.ts +10 -0
- package/engine/journal/final-review-entries.ts +128 -0
- package/engine/journal/final-review-replay.ts +324 -0
- package/engine/journal/final-review-round.ts +320 -0
- package/engine/journal/final-review-state.ts +180 -0
- package/engine/journal/gate-entries.ts +81 -0
- package/engine/journal/intake-entries.ts +87 -0
- package/engine/journal/join-entries.ts +126 -0
- package/engine/journal/journal-entry-schema.ts +218 -0
- package/engine/journal/journal-kinds.ts +97 -0
- package/engine/journal/journal-record-schema.ts +218 -0
- package/engine/journal/journal-record.ts +43 -1400
- package/engine/journal/journal.ts +6 -1
- package/engine/journal/limit-entries.ts +90 -0
- package/engine/journal/memory-entries.ts +124 -0
- package/engine/journal/memory-replay.ts +157 -0
- package/engine/journal/plan-replay.ts +139 -0
- package/engine/journal/pull-request-entries.ts +41 -0
- package/engine/journal/replay.ts +157 -2144
- package/engine/journal/reply-replay.ts +144 -0
- package/engine/journal/repo-wait-entries.ts +53 -0
- package/engine/journal/run-entries.ts +172 -0
- package/engine/journal/run-state.ts +264 -0
- package/engine/journal/shared-failure-replay.ts +133 -0
- package/engine/journal/stuck-entries.ts +193 -0
- package/engine/journal/ticket-progress.ts +392 -0
- package/engine/journal/ticket-replay.ts +411 -0
- package/engine/journal/ticket-result-replay.ts +364 -0
- package/engine/journal/ticket-retry-replay.ts +142 -0
- package/engine/journal/worktree-entries.ts +122 -0
- package/engine/models/model-score.ts +512 -0
- package/engine/shell/run-command.ts +187 -18
- package/engine/testing/agent-turn-fixtures.ts +287 -0
- package/engine/testing/already-done-fixtures.ts +97 -0
- package/engine/testing/build-fixtures.ts +68 -973
- package/engine/testing/checkouts-fixtures.ts +48 -0
- package/engine/testing/final-review-fixtures.ts +62 -1
- package/engine/testing/final-review-steps.ts +143 -0
- package/engine/testing/limit-fixtures.ts +85 -0
- package/engine/testing/live-file-fixtures.ts +57 -0
- package/engine/testing/practice-repo.ts +10 -4
- package/engine/testing/practice-run.ts +1 -1
- package/engine/testing/reply-fixtures.ts +88 -0
- package/engine/testing/run-budget-fixtures.ts +165 -0
- package/engine/testing/run-fixtures.ts +219 -0
- package/engine/testing/sdk-play.ts +177 -0
- package/engine/testing/setup-repo.ts +194 -0
- package/engine/testing/step-record-fixtures.ts +55 -0
- package/engine/testing/ticket-step-fixtures.ts +272 -0
- package/engine/testing/wait-scenarios.ts +186 -0
- package/engine/tracker/gh-retry.ts +217 -0
- package/engine/tracker/github-tracker.ts +310 -90
- package/engine/tracker/in-memory-tracker.ts +8 -4
- package/engine/tracker/tracker.ts +131 -6
- package/package.json +2 -1
- package/skills/luca-retro/SKILL.md +74 -0
- package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
- package/skills/luca-unstick/SKILL.md +153 -0
- package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
- package/engine/jev/jev-client.ts +0 -173
|
@@ -1,100 +1,77 @@
|
|
|
1
|
-
import type { BoardRecord } from './board-vocabulary'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
ALL_STEPS_DONE,
|
|
5
|
-
DEFAULT_RUN_BUDGET_TOKENS,
|
|
6
|
-
LENS_NAMES,
|
|
7
|
-
LOOP_CAP,
|
|
8
|
-
NO_MEMORY,
|
|
9
|
-
USAGE_LABEL,
|
|
10
|
-
usageLevel,
|
|
11
|
-
windowRank,
|
|
12
|
-
windowWords,
|
|
13
|
-
type BoardState,
|
|
14
|
-
type EngineEnded,
|
|
15
|
-
type FinalReview,
|
|
16
|
-
type FindingCounts,
|
|
17
|
-
type LensCard,
|
|
18
|
-
type NeedsYou,
|
|
19
|
-
type PlanUsed,
|
|
20
|
-
type RunInfo,
|
|
21
|
-
type RunStatus,
|
|
22
|
-
type TicketCard,
|
|
23
|
-
type Usage,
|
|
24
|
-
} from '../shared/board-state'
|
|
25
|
-
|
|
26
1
|
/**
|
|
27
2
|
* The board's reducer: pure functions that turn journal records into board
|
|
28
3
|
* state. The same records in the same order always give the same state, so
|
|
29
4
|
* a replayed journal rebuilds the board exactly.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
/** How many "tried" lines a ticket or the final review keeps. */
|
|
33
|
-
const TRIED_KEPT = 6
|
|
34
|
-
|
|
35
|
-
/** Longest single "tried" line or stuck detail kept in state. */
|
|
36
|
-
const LINE_MAX = 240
|
|
37
|
-
const DETAIL_MAX = 2000
|
|
38
|
-
|
|
39
|
-
const STUCK_REASONS: Record<string, string> = {
|
|
40
|
-
agent_failed: 'An agent failed on its last try.',
|
|
41
|
-
red_check_failed: 'The red check failed.',
|
|
42
|
-
nothing_new_to_test:
|
|
43
|
-
'The test-writer says the ticket changes no behavior, so there is nothing new to test. Label it refactor and retry it.',
|
|
44
|
-
leftovers_found:
|
|
45
|
-
'The leftover scan found files that must not be committed.',
|
|
46
|
-
gates_failed: 'The checks failed.',
|
|
47
|
-
bad_test: 'The implementer sent back a bad test.',
|
|
48
|
-
changes_requested: 'The ticket review asked for changes.',
|
|
49
|
-
join_failed: "The ticket couldn't join the run branch.",
|
|
50
|
-
join_gates_failed: 'The checks failed after the ticket joined.',
|
|
51
|
-
install_failed: 'Installing the dependencies failed.',
|
|
52
|
-
prepare_failed: "The repo's prepare command failed.",
|
|
53
|
-
setup_change_needed:
|
|
54
|
-
'An agent needs a test setup file changed; only you may change one.',
|
|
55
|
-
crashed: 'The engine crashed in the same step again and again.',
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* A commit's short sha, as git and GitHub show it.
|
|
60
|
-
*
|
|
61
|
-
* @example
|
|
62
|
-
* shortSha({ sha: '3559c25f5a1b' }) // '3559c25'
|
|
63
|
-
*/
|
|
64
|
-
export const shortSha = ({ sha }: { sha: string }): string => sha.slice(0, 7)
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* An already-done ticket's activity (#484): calm, with the commits that
|
|
68
|
-
* did its work.
|
|
69
5
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
6
|
+
* This module applies a record and passes it on by kind; the handlers are in
|
|
7
|
+
* modules by responsibility, each with the record kinds it handles:
|
|
8
|
+
* - `reduce-board-run.ts`: the run's own records (start, intake, snapshot,
|
|
9
|
+
* run branch, pull request, stop).
|
|
10
|
+
* - `reduce-board-ticket-start.ts`, `reduce-board-ticket-checks.ts` and
|
|
11
|
+
* `reduce-board-ticket-join.ts`: a ticket from its start, through its
|
|
12
|
+
* checks, to its way onto the run branch.
|
|
13
|
+
* - `reduce-board-agent-start.ts` and `reduce-board-agents.ts`: an agent's
|
|
14
|
+
* turn.
|
|
15
|
+
* - `reduce-board-steps.ts`: the steps in flight.
|
|
16
|
+
* - `reduce-board-needs-you.ts`: stuck items, skips, retries and replies.
|
|
17
|
+
* - `reduce-board-final-review.ts`: the final review and its lenses.
|
|
18
|
+
* - `reduce-board-usage.ts`: tokens, plan usage and limit waits.
|
|
19
|
+
* - `reduce-board-counters.ts`: the run-wide counters.
|
|
20
|
+
* - `reduce-board-settle.ts`: what depends on the whole board.
|
|
21
|
+
* The pieces they share are `reduce-board-text.ts` (words),
|
|
22
|
+
* `reduce-board-reasons.ts` (stuck reasons), `reduce-board-update.ts` (small
|
|
23
|
+
* state updates) and `reduce-board-create.ts` (starting shapes).
|
|
24
|
+
*/
|
|
25
|
+
import type { BoardKind, BoardRecord } from './board-vocabulary'
|
|
26
|
+
import { AGENT_KINDS, applyAgentKind } from './reduce-board-agents'
|
|
27
|
+
import { COUNTER_KINDS, applyCounterKind } from './reduce-board-counters'
|
|
28
|
+
import {
|
|
29
|
+
FINAL_REVIEW_KINDS,
|
|
30
|
+
applyFinalReviewKind,
|
|
31
|
+
} from './reduce-board-final-review'
|
|
32
|
+
import { NEEDS_YOU_KINDS, applyNeedsYouKind } from './reduce-board-needs-you'
|
|
33
|
+
import { RUN_KINDS, applyRunKind } from './reduce-board-run'
|
|
34
|
+
import { settle } from './reduce-board-settle'
|
|
35
|
+
import { STEP_KINDS, applyStepKind, withoutSteps } from './reduce-board-steps'
|
|
36
|
+
import {
|
|
37
|
+
TICKET_CHECKS_KINDS,
|
|
38
|
+
applyTicketChecksKind,
|
|
39
|
+
} from './reduce-board-ticket-checks'
|
|
40
|
+
import {
|
|
41
|
+
TICKET_JOIN_KINDS,
|
|
42
|
+
applyTicketJoinKind,
|
|
43
|
+
} from './reduce-board-ticket-join'
|
|
44
|
+
import {
|
|
45
|
+
TICKET_START_KINDS,
|
|
46
|
+
applyTicketStartKind,
|
|
47
|
+
} from './reduce-board-ticket-start'
|
|
48
|
+
import { USAGE_KINDS, applyUsageKind } from './reduce-board-usage'
|
|
49
|
+
|
|
50
|
+
import type { BoardState, EngineEnded } from '../shared/board-state'
|
|
51
|
+
|
|
52
|
+
export { startKind } from './reduce-board-agent-start'
|
|
53
|
+
export { createBoardState } from './reduce-board-create'
|
|
54
|
+
export {
|
|
55
|
+
RETRY_REFUSED_TEXT,
|
|
56
|
+
finalReasonText,
|
|
57
|
+
reasonText,
|
|
58
|
+
runReasonText,
|
|
59
|
+
} from './reduce-board-reasons'
|
|
60
|
+
export { unfinishedBlockers } from './reduce-board-settle'
|
|
61
|
+
export { stepText } from './reduce-board-steps'
|
|
62
|
+
export {
|
|
63
|
+
alreadyDoneText,
|
|
64
|
+
clip,
|
|
65
|
+
countFindings,
|
|
66
|
+
failureText,
|
|
67
|
+
numbersText,
|
|
68
|
+
rebasedText,
|
|
69
|
+
reviewCountsText,
|
|
70
|
+
roleWords,
|
|
71
|
+
shortSha,
|
|
72
|
+
testsSentBackText,
|
|
73
|
+
} from './reduce-board-text'
|
|
74
|
+
export { LEARNER } from './reduce-board-update'
|
|
98
75
|
|
|
99
76
|
/** Record kinds that don't mean the engine moved on after a stop. */
|
|
100
77
|
const QUIET_KINDS = new Set([
|
|
@@ -104,6 +81,7 @@ const QUIET_KINDS = new Set([
|
|
|
104
81
|
'jev_asked',
|
|
105
82
|
'jev_answered',
|
|
106
83
|
'jev_failed',
|
|
84
|
+
'decision_model_off',
|
|
107
85
|
'agent_message',
|
|
108
86
|
'agent_message_delivered',
|
|
109
87
|
'shared_git_changed',
|
|
@@ -111,639 +89,6 @@ const QUIET_KINDS = new Set([
|
|
|
111
89
|
'step_ended',
|
|
112
90
|
])
|
|
113
91
|
|
|
114
|
-
/** The learner's role (#370): its records have no ticket. */
|
|
115
|
-
export const LEARNER = 'learner'
|
|
116
|
-
|
|
117
|
-
const withLearner = ({
|
|
118
|
-
state,
|
|
119
|
-
learner,
|
|
120
|
-
}: {
|
|
121
|
-
state: BoardState
|
|
122
|
-
learner: BoardState['memory']['learner']
|
|
123
|
-
}): BoardState => ({ ...state, memory: { ...state.memory, learner } })
|
|
124
|
-
|
|
125
|
-
/** A stuck reason code in words. */
|
|
126
|
-
export const reasonText = ({ reason }: { reason: string }): string =>
|
|
127
|
-
STUCK_REASONS[reason] ?? `${reason.replaceAll('_', ' ')}.`
|
|
128
|
-
|
|
129
|
-
/** The final review's stuck reasons that read differently from a ticket's. */
|
|
130
|
-
const FINAL_STUCK_REASONS: Record<string, string> = {
|
|
131
|
-
changes_requested: `The lenses still ask for changes after ${LOOP_CAP} fix rounds.`,
|
|
132
|
-
gates_failed: "The checks still fail on the final review's fixes.",
|
|
133
|
-
bad_test:
|
|
134
|
-
"The implementer sent back a bad test while fixing the final review's findings.",
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** A final review's stuck reason code in words. */
|
|
138
|
-
export const finalReasonText = ({ reason }: { reason: string }): string =>
|
|
139
|
-
FINAL_STUCK_REASONS[reason] ?? reasonText({ reason })
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* The run's version note after a resume on Luca `resumed_on`: both versions
|
|
143
|
-
* when it differs from the one the run started on, else the note so far (a
|
|
144
|
-
* run that once changed version keeps saying so). Nothing when the journal
|
|
145
|
-
* doesn't say what the run started on.
|
|
146
|
-
*/
|
|
147
|
-
const versionNote = ({
|
|
148
|
-
run,
|
|
149
|
-
resumed_on,
|
|
150
|
-
}: {
|
|
151
|
-
run: RunInfo
|
|
152
|
-
resumed_on: string
|
|
153
|
-
}): string | null =>
|
|
154
|
-
run.started_on === null || run.started_on === resumed_on
|
|
155
|
-
? run.version_note
|
|
156
|
-
: `This run started on Luca ${run.started_on} and resumed on Luca ${resumed_on}. It keeps going.`
|
|
157
|
-
|
|
158
|
-
const noFindings = (): FindingCounts => ({ blocker: 0, should_fix: 0, nit: 0 })
|
|
159
|
-
|
|
160
|
-
const freshLenses = (): LensCard[] =>
|
|
161
|
-
LENS_NAMES.map((name) => ({
|
|
162
|
-
name,
|
|
163
|
-
state: 'waiting',
|
|
164
|
-
findings: noFindings(),
|
|
165
|
-
}))
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* A new run's board, before any record.
|
|
169
|
-
*
|
|
170
|
-
* @example
|
|
171
|
-
* const state = createBoardState({
|
|
172
|
-
* run_id: 'luca-20260923-123042-ab12',
|
|
173
|
-
* spec_number: 10,
|
|
174
|
-
* demo: false,
|
|
175
|
-
* started_at: '2026-09-23T12:30:42.000Z',
|
|
176
|
-
* log_path: '/tmp/luca-20260923-123042-ab12.log',
|
|
177
|
-
* })
|
|
178
|
-
*/
|
|
179
|
-
export const createBoardState = ({
|
|
180
|
-
run_id,
|
|
181
|
-
spec_number,
|
|
182
|
-
demo,
|
|
183
|
-
started_at,
|
|
184
|
-
log_path,
|
|
185
|
-
}: {
|
|
186
|
-
run_id: string
|
|
187
|
-
spec_number: number | null
|
|
188
|
-
demo: boolean
|
|
189
|
-
started_at: string
|
|
190
|
-
log_path: string | null
|
|
191
|
-
}): BoardState => ({
|
|
192
|
-
run: {
|
|
193
|
-
run_id,
|
|
194
|
-
spec_number,
|
|
195
|
-
spec_title: null,
|
|
196
|
-
demo,
|
|
197
|
-
branch: null,
|
|
198
|
-
phase: 'starting',
|
|
199
|
-
status: 'starting',
|
|
200
|
-
pr_url: null,
|
|
201
|
-
pr_number: null,
|
|
202
|
-
refusal: [],
|
|
203
|
-
started_at,
|
|
204
|
-
last_time: null,
|
|
205
|
-
engine_ended: null,
|
|
206
|
-
stopped: null,
|
|
207
|
-
log_path,
|
|
208
|
-
started_on: null,
|
|
209
|
-
version_note: null,
|
|
210
|
-
},
|
|
211
|
-
usage: null,
|
|
212
|
-
usage_label: USAGE_LABEL,
|
|
213
|
-
run_tokens: 0,
|
|
214
|
-
run_budget_tokens: DEFAULT_RUN_BUDGET_TOKENS,
|
|
215
|
-
run_budget_each: DEFAULT_RUN_BUDGET_TOKENS,
|
|
216
|
-
limit_wait: null,
|
|
217
|
-
usage_line_wait: null,
|
|
218
|
-
run_plan_used: [],
|
|
219
|
-
needs_you: [],
|
|
220
|
-
tickets: [],
|
|
221
|
-
final_review: {
|
|
222
|
-
state: 'waiting',
|
|
223
|
-
active: false,
|
|
224
|
-
round: 0,
|
|
225
|
-
fix_round: 0,
|
|
226
|
-
lenses: freshLenses(),
|
|
227
|
-
tried: [],
|
|
228
|
-
},
|
|
229
|
-
jev: { asked: 0, answered: 0, failed: 0 },
|
|
230
|
-
messages: { sent: 0, refused: 0, delivered: 0 },
|
|
231
|
-
memory: NO_MEMORY,
|
|
232
|
-
shared_git_changed: 0,
|
|
233
|
-
run_steps: [],
|
|
234
|
-
current_step: null,
|
|
235
|
-
event_count: 0,
|
|
236
|
-
latest: null,
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
const newTicket = ({
|
|
240
|
-
number,
|
|
241
|
-
title,
|
|
242
|
-
refactor,
|
|
243
|
-
blockers,
|
|
244
|
-
}: {
|
|
245
|
-
number: number
|
|
246
|
-
title: string
|
|
247
|
-
refactor: boolean
|
|
248
|
-
blockers: number[]
|
|
249
|
-
}): TicketCard => ({
|
|
250
|
-
number,
|
|
251
|
-
title,
|
|
252
|
-
refactor,
|
|
253
|
-
blockers,
|
|
254
|
-
started: false,
|
|
255
|
-
stage: 'building',
|
|
256
|
-
step: -1,
|
|
257
|
-
activity: 'queued',
|
|
258
|
-
role: null,
|
|
259
|
-
current_step: null,
|
|
260
|
-
fix_round: 0,
|
|
261
|
-
review_round: 0,
|
|
262
|
-
review_fix_round: 0,
|
|
263
|
-
open_check: null,
|
|
264
|
-
failed_turn: null,
|
|
265
|
-
tests: null,
|
|
266
|
-
baseline_tests: null,
|
|
267
|
-
findings: null,
|
|
268
|
-
tokens: 0,
|
|
269
|
-
agent_tokens: {},
|
|
270
|
-
tried: [],
|
|
271
|
-
plan_used: [],
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
const withTried = ({
|
|
275
|
-
tried,
|
|
276
|
-
line,
|
|
277
|
-
}: {
|
|
278
|
-
tried: string[]
|
|
279
|
-
line: string
|
|
280
|
-
}): string[] => [...tried, firstLine({ text: line })].slice(-TRIED_KEPT)
|
|
281
|
-
|
|
282
|
-
const updateTicket = ({
|
|
283
|
-
state,
|
|
284
|
-
number,
|
|
285
|
-
update,
|
|
286
|
-
}: {
|
|
287
|
-
state: BoardState
|
|
288
|
-
number: number | null
|
|
289
|
-
update: (ticket: TicketCard) => TicketCard
|
|
290
|
-
}): BoardState => {
|
|
291
|
-
if (number === null) return state
|
|
292
|
-
return {
|
|
293
|
-
...state,
|
|
294
|
-
tickets: state.tickets.map((ticket) =>
|
|
295
|
-
ticket.number === number ? update(ticket) : ticket
|
|
296
|
-
),
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const updateFinal = ({
|
|
301
|
-
state,
|
|
302
|
-
update,
|
|
303
|
-
}: {
|
|
304
|
-
state: BoardState
|
|
305
|
-
update: (review: FinalReview) => FinalReview
|
|
306
|
-
}): BoardState => ({ ...state, final_review: update(state.final_review) })
|
|
307
|
-
|
|
308
|
-
const updateLens = ({
|
|
309
|
-
state,
|
|
310
|
-
lens,
|
|
311
|
-
update,
|
|
312
|
-
}: {
|
|
313
|
-
state: BoardState
|
|
314
|
-
lens: string
|
|
315
|
-
update: (card: LensCard) => LensCard
|
|
316
|
-
}): BoardState =>
|
|
317
|
-
updateFinal({
|
|
318
|
-
state,
|
|
319
|
-
update: (review) => ({
|
|
320
|
-
...review,
|
|
321
|
-
lenses: review.lenses.map((card) =>
|
|
322
|
-
card.name === lens ? update(card) : card
|
|
323
|
-
),
|
|
324
|
-
}),
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
const addNeedsYou = ({
|
|
328
|
-
state,
|
|
329
|
-
item,
|
|
330
|
-
}: {
|
|
331
|
-
state: BoardState
|
|
332
|
-
item: NeedsYou
|
|
333
|
-
}): BoardState => ({
|
|
334
|
-
...state,
|
|
335
|
-
needs_you: [
|
|
336
|
-
...state.needs_you.filter((entry) => entry.key !== item.key),
|
|
337
|
-
item,
|
|
338
|
-
],
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
const resolveNeedsYou = ({
|
|
342
|
-
state,
|
|
343
|
-
key,
|
|
344
|
-
}: {
|
|
345
|
-
state: BoardState
|
|
346
|
-
key: string | null
|
|
347
|
-
}): BoardState => ({
|
|
348
|
-
...state,
|
|
349
|
-
needs_you:
|
|
350
|
-
key === null
|
|
351
|
-
? []
|
|
352
|
-
: state.needs_you.filter((entry) => entry.key !== key),
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
/** The key of a ticket's "Needs you" item. */
|
|
356
|
-
export const ticketKey = ({ ticket }: { ticket: number }): string =>
|
|
357
|
-
`ticket-${ticket}`
|
|
358
|
-
|
|
359
|
-
/** The key of the final review's "Needs you" item. */
|
|
360
|
-
export const FINAL_KEY = 'final'
|
|
361
|
-
|
|
362
|
-
/** The key of the "Needs you" item for the whole run, stuck on its budget. */
|
|
363
|
-
export const RUN_KEY = 'run'
|
|
364
|
-
|
|
365
|
-
/** Why the whole run is stuck (`run_stuck`'s reason), in words. */
|
|
366
|
-
const RUN_STUCK_REASONS: Record<string, string> = {
|
|
367
|
-
run_budget:
|
|
368
|
-
'The run used up its run budget of tokens. `retry` adds one more full budget.',
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/** A run stuck reason code in words. */
|
|
372
|
-
export const runReasonText = ({ reason }: { reason: string }): string =>
|
|
373
|
-
RUN_STUCK_REASONS[reason] ?? `${reason.replaceAll('_', ' ')}.`
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* A role in words: a final review lens's role (`security-lens`) reads
|
|
377
|
-
* "security lens"; any other role as it is.
|
|
378
|
-
*
|
|
379
|
-
* @example
|
|
380
|
-
* roleWords({ role: 'security-lens' }) // 'security lens'
|
|
381
|
-
*/
|
|
382
|
-
export const roleWords = ({ role }: { role: string }): string =>
|
|
383
|
-
role.endsWith('-lens') ? `${role.slice(0, -'-lens'.length)} lens` : role
|
|
384
|
-
|
|
385
|
-
/** A step's action type in words, when it reads better than its name. */
|
|
386
|
-
const STEP_WORDS: Record<string, string> = {
|
|
387
|
-
install_dependencies: 'installing packages',
|
|
388
|
-
run_baseline_tests: 'running the baseline tests',
|
|
389
|
-
run_red_check: 'running the red check',
|
|
390
|
-
run_gates: 'running the checks',
|
|
391
|
-
run_final_gates: 'running the checks',
|
|
392
|
-
join_run_branch: 'joining the run branch',
|
|
393
|
-
push_run_branch: 'pushing the run branch',
|
|
394
|
-
push_final_fixes: 'pushing the run branch',
|
|
395
|
-
write_changeset: 'writing the changeset',
|
|
396
|
-
open_pull_request: 'opening the pull request',
|
|
397
|
-
mark_already_done: 'noting the work is already done',
|
|
398
|
-
close_ticket: 'closing the ticket',
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* A `step_started`'s step in words: an agent's turn (`<type>:<role>`)
|
|
403
|
-
* waits for its role; any other step as `STEP_WORDS` has it, else its
|
|
404
|
-
* action type.
|
|
405
|
-
*
|
|
406
|
-
* @example
|
|
407
|
-
* stepText({ step: 'run_gates' }) // 'running the checks'
|
|
408
|
-
* stepText({ step: 'follow_up_agent:implementer' }) // 'waiting for the implementer'
|
|
409
|
-
*/
|
|
410
|
-
export const stepText = ({ step }: { step: string }): string => {
|
|
411
|
-
const colon = step.indexOf(':')
|
|
412
|
-
if (colon >= 0) {
|
|
413
|
-
return `waiting for the ${roleWords({ role: step.slice(colon + 1) })}`
|
|
414
|
-
}
|
|
415
|
-
return STEP_WORDS[step] ?? step.replaceAll('_', ' ')
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/** The run's running steps, and the newest of them as its current step. */
|
|
419
|
-
const withRunSteps = ({
|
|
420
|
-
state,
|
|
421
|
-
run_steps,
|
|
422
|
-
}: {
|
|
423
|
-
state: BoardState
|
|
424
|
-
run_steps: BoardState['run_steps']
|
|
425
|
-
}): BoardState => {
|
|
426
|
-
const newest = run_steps.at(-1)
|
|
427
|
-
return {
|
|
428
|
-
...state,
|
|
429
|
-
run_steps,
|
|
430
|
-
current_step: newest
|
|
431
|
-
? { text: newest.text, since: newest.since }
|
|
432
|
-
: null,
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/** No step running anywhere: the engine ended, or the run stopped. */
|
|
437
|
-
const withoutSteps = ({ state }: { state: BoardState }): BoardState =>
|
|
438
|
-
withRunSteps({
|
|
439
|
-
state: {
|
|
440
|
-
...state,
|
|
441
|
-
tickets: state.tickets.map((card) =>
|
|
442
|
-
card.current_step === null
|
|
443
|
-
? card
|
|
444
|
-
: { ...card, current_step: null }
|
|
445
|
-
),
|
|
446
|
-
},
|
|
447
|
-
run_steps: [],
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* A step began: it is its ticket's current step, or one of the run's.
|
|
452
|
-
* A new step on the same key (a crash's redo) replaces the one before it.
|
|
453
|
-
*/
|
|
454
|
-
const stepStarted = ({
|
|
455
|
-
state,
|
|
456
|
-
record,
|
|
457
|
-
}: {
|
|
458
|
-
state: BoardState
|
|
459
|
-
record: Extract<BoardRecord, { kind: 'step_started' }>
|
|
460
|
-
}): BoardState => {
|
|
461
|
-
// A replayed journal of an engine that ended has no step running.
|
|
462
|
-
if (state.run.engine_ended !== null) return state
|
|
463
|
-
const current_step = {
|
|
464
|
-
text: stepText({ step: record.content.step }),
|
|
465
|
-
since: record.time,
|
|
466
|
-
}
|
|
467
|
-
if (record.ticket !== null) {
|
|
468
|
-
return updateTicket({
|
|
469
|
-
state,
|
|
470
|
-
number: record.ticket,
|
|
471
|
-
update: (card) => ({ ...card, current_step }),
|
|
472
|
-
})
|
|
473
|
-
}
|
|
474
|
-
const { key } = record.content
|
|
475
|
-
return withRunSteps({
|
|
476
|
-
state,
|
|
477
|
-
run_steps: [
|
|
478
|
-
...state.run_steps.filter((step) => step.key !== key),
|
|
479
|
-
{ key, ...current_step },
|
|
480
|
-
],
|
|
481
|
-
})
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const stepEnded = ({
|
|
485
|
-
state,
|
|
486
|
-
record,
|
|
487
|
-
}: {
|
|
488
|
-
state: BoardState
|
|
489
|
-
record: Extract<BoardRecord, { kind: 'step_ended' }>
|
|
490
|
-
}): BoardState => {
|
|
491
|
-
if (record.ticket !== null) {
|
|
492
|
-
return updateTicket({
|
|
493
|
-
state,
|
|
494
|
-
number: record.ticket,
|
|
495
|
-
update: (card) => ({ ...card, current_step: null }),
|
|
496
|
-
})
|
|
497
|
-
}
|
|
498
|
-
return withRunSteps({
|
|
499
|
-
state,
|
|
500
|
-
run_steps: state.run_steps.filter(
|
|
501
|
-
(step) => step.key !== record.content.key
|
|
502
|
-
),
|
|
503
|
-
})
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/** The final review with one more "tried" line. */
|
|
507
|
-
const finalTried = ({
|
|
508
|
-
state,
|
|
509
|
-
line,
|
|
510
|
-
}: {
|
|
511
|
-
state: BoardState
|
|
512
|
-
line: string
|
|
513
|
-
}): BoardState =>
|
|
514
|
-
updateFinal({
|
|
515
|
-
state,
|
|
516
|
-
update: (review) => ({
|
|
517
|
-
...review,
|
|
518
|
-
tried: withTried({ tried: review.tried, line }),
|
|
519
|
-
}),
|
|
520
|
-
})
|
|
521
|
-
|
|
522
|
-
const testCounts = ({
|
|
523
|
-
cases,
|
|
524
|
-
}: {
|
|
525
|
-
cases: { status: string }[]
|
|
526
|
-
}): { failing: number; total: number } => ({
|
|
527
|
-
failing: cases.filter((entry) => entry.status === 'failed').length,
|
|
528
|
-
total: cases.length,
|
|
529
|
-
})
|
|
530
|
-
|
|
531
|
-
/** A ticket review's findings counted by severity. */
|
|
532
|
-
export const countFindings = ({
|
|
533
|
-
findings,
|
|
534
|
-
}: {
|
|
535
|
-
findings: { severity: 'blocker' | 'should_fix' | 'nit' }[]
|
|
536
|
-
}): FindingCounts => ({
|
|
537
|
-
blocker: findings.filter((finding) => finding.severity === 'blocker')
|
|
538
|
-
.length,
|
|
539
|
-
should_fix: findings.filter((finding) => finding.severity === 'should_fix')
|
|
540
|
-
.length,
|
|
541
|
-
nit: findings.filter((finding) => finding.severity === 'nit').length,
|
|
542
|
-
})
|
|
543
|
-
|
|
544
|
-
type SessionContent = Extract<BoardRecord, { kind: 'agent_session' }>['content']
|
|
545
|
-
|
|
546
|
-
/** Every token a session read or wrote, cache reads included. */
|
|
547
|
-
const sessionTokens = ({ session }: SessionContent): number =>
|
|
548
|
-
session.usage.input_tokens +
|
|
549
|
-
session.usage.output_tokens +
|
|
550
|
-
session.usage.cache_read_input_tokens +
|
|
551
|
-
session.usage.cache_creation_input_tokens
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* A session's exact tokens: input, output, and cache-creation tokens over
|
|
555
|
-
* its tokens per model, subagents included. An older journal's session,
|
|
556
|
-
* with none, counts its main loop's `usage`. Cache reads are left out.
|
|
557
|
-
*/
|
|
558
|
-
const countedTokens = ({ session }: SessionContent): number => {
|
|
559
|
-
const models = Object.values(session.model_usage)
|
|
560
|
-
return (models.length === 0 ? [session.usage] : models).reduce(
|
|
561
|
-
(total, each) =>
|
|
562
|
-
total +
|
|
563
|
-
each.input_tokens +
|
|
564
|
-
each.output_tokens +
|
|
565
|
-
each.cache_creation_input_tokens,
|
|
566
|
-
0
|
|
567
|
-
)
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/** A 0-to-1 utilization as a whole percent from 0 to 100. */
|
|
571
|
-
const percentOf = ({ utilization }: { utilization: number }): number =>
|
|
572
|
-
Math.min(100, Math.max(0, Math.round(utilization * 100)))
|
|
573
|
-
|
|
574
|
-
/** Epoch seconds as an ISO time. */
|
|
575
|
-
const isoOf = ({ seconds }: { seconds: number }): string =>
|
|
576
|
-
new Date(seconds * 1000).toISOString()
|
|
577
|
-
|
|
578
|
-
type Reading = SessionContent['session']['rate_limit_events'][number]
|
|
579
|
-
|
|
580
|
-
/**
|
|
581
|
-
* One reading's windows as `{ type, utilization, resetsAt }`: its top-level
|
|
582
|
-
* `rateLimitType` form, then each of its `unifiedWindows`.
|
|
583
|
-
*/
|
|
584
|
-
const windowsOf = ({
|
|
585
|
-
reading,
|
|
586
|
-
}: {
|
|
587
|
-
reading: Reading
|
|
588
|
-
}): { type: string; utilization: number; resetsAt: number | undefined }[] => {
|
|
589
|
-
const { rateLimitType, utilization, resetsAt, unifiedWindows } = reading
|
|
590
|
-
const top =
|
|
591
|
-
rateLimitType !== undefined && utilization !== undefined
|
|
592
|
-
? [{ type: rateLimitType, utilization, resetsAt }]
|
|
593
|
-
: []
|
|
594
|
-
const unified = Object.entries(unifiedWindows ?? {}).flatMap(
|
|
595
|
-
([type, window]) =>
|
|
596
|
-
window.utilization === undefined
|
|
597
|
-
? []
|
|
598
|
-
: [
|
|
599
|
-
{
|
|
600
|
-
type,
|
|
601
|
-
utilization: window.utilization,
|
|
602
|
-
resetsAt: window.resetsAt,
|
|
603
|
-
},
|
|
604
|
-
]
|
|
605
|
-
)
|
|
606
|
-
return [...top, ...unified]
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
/**
|
|
610
|
-
* The plan usage after a session's rate-limit readings. The latest
|
|
611
|
-
* `five_hour` reading sets the five-hour window (and when it resets); the
|
|
612
|
-
* latest reading of any `seven_day*` type sets the weekly one, and inside
|
|
613
|
-
* one reading the highest `seven_day*` window wins, since the tightest
|
|
614
|
-
* weekly cap is the one that binds. A window with no reading keeps what it
|
|
615
|
-
* had. `null` while no window has a reading.
|
|
616
|
-
*/
|
|
617
|
-
const usageAfter = ({
|
|
618
|
-
usage,
|
|
619
|
-
session,
|
|
620
|
-
time,
|
|
621
|
-
}: {
|
|
622
|
-
usage: Usage | null
|
|
623
|
-
session: SessionContent['session']
|
|
624
|
-
time: string
|
|
625
|
-
}): Usage | null => {
|
|
626
|
-
let five_hour = usage?.five_hour_percent ?? null
|
|
627
|
-
let weekly = usage?.weekly_percent ?? null
|
|
628
|
-
let resets_at = usage?.resets_at ?? null
|
|
629
|
-
let read = false
|
|
630
|
-
for (const reading of session.rate_limit_events) {
|
|
631
|
-
const windows = windowsOf({ reading })
|
|
632
|
-
const fiveHour = windows.filter(({ type }) => type === 'five_hour')
|
|
633
|
-
const latest = fiveHour.at(-1)
|
|
634
|
-
if (latest) {
|
|
635
|
-
five_hour = percentOf({ utilization: latest.utilization })
|
|
636
|
-
resets_at =
|
|
637
|
-
latest.resetsAt === undefined
|
|
638
|
-
? resets_at
|
|
639
|
-
: isoOf({ seconds: latest.resetsAt })
|
|
640
|
-
read = true
|
|
641
|
-
}
|
|
642
|
-
const weeklies = windows
|
|
643
|
-
.filter(({ type }) => type.startsWith('seven_day'))
|
|
644
|
-
.map(({ utilization }) => percentOf({ utilization }))
|
|
645
|
-
if (weeklies.length > 0) {
|
|
646
|
-
weekly = Math.max(...weeklies)
|
|
647
|
-
read = true
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
if (!read) return usage
|
|
651
|
-
return {
|
|
652
|
-
five_hour_percent: five_hour,
|
|
653
|
-
weekly_percent: weekly,
|
|
654
|
-
five_hour_level:
|
|
655
|
-
five_hour === null ? null : usageLevel({ percent: five_hour }),
|
|
656
|
-
weekly_level: weekly === null ? null : usageLevel({ percent: weekly }),
|
|
657
|
-
resets_at,
|
|
658
|
-
read_at: time,
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
/** The roles that fix a ticket review's findings. */
|
|
663
|
-
const REVIEW_FIXERS = new Set(['test-writer', 'implementer'])
|
|
664
|
-
|
|
665
|
-
const plural = ({ count, word }: { count: number; word: string }) =>
|
|
666
|
-
`${count} ${word}${count === 1 ? '' : 's'}`
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
* A ticket review's finding counts in words, such as "1 blocker, 2
|
|
670
|
-
* should-fix, 2 nits". Nits are left out unless `nits` is set; `null` when
|
|
671
|
-
* nothing is left to say.
|
|
672
|
-
*
|
|
673
|
-
* @example
|
|
674
|
-
* reviewCountsText({ findings: { blocker: 2, should_fix: 1, nit: 1 }, nits: false })
|
|
675
|
-
* // '2 blockers, 1 should-fix'
|
|
676
|
-
*/
|
|
677
|
-
export const reviewCountsText = ({
|
|
678
|
-
findings,
|
|
679
|
-
nits,
|
|
680
|
-
}: {
|
|
681
|
-
findings: FindingCounts
|
|
682
|
-
nits: boolean
|
|
683
|
-
}): string | null => {
|
|
684
|
-
const parts = [
|
|
685
|
-
findings.blocker > 0
|
|
686
|
-
? plural({ count: findings.blocker, word: 'blocker' })
|
|
687
|
-
: null,
|
|
688
|
-
findings.should_fix > 0 ? `${findings.should_fix} should-fix` : null,
|
|
689
|
-
nits && findings.nit > 0
|
|
690
|
-
? plural({ count: findings.nit, word: 'nit' })
|
|
691
|
-
: null,
|
|
692
|
-
].filter((part) => part !== null)
|
|
693
|
-
return parts.length > 0 ? parts.join(', ') : null
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
/**
|
|
697
|
-
* A usage record's windows as whole percents in words: five-hour first,
|
|
698
|
-
* then the weekly ones, then the rest, each group in the record's order.
|
|
699
|
-
*/
|
|
700
|
-
const planUsedOf = ({
|
|
701
|
-
windows,
|
|
702
|
-
}: {
|
|
703
|
-
windows: Record<string, { used: number }>
|
|
704
|
-
}): PlanUsed[] =>
|
|
705
|
-
Object.entries(windows)
|
|
706
|
-
.map(([window, { used }], index) => ({ window, used, index }))
|
|
707
|
-
.toSorted(
|
|
708
|
-
(left, right) =>
|
|
709
|
-
windowRank({ window: left.window }) -
|
|
710
|
-
windowRank({ window: right.window }) ||
|
|
711
|
-
left.index - right.index
|
|
712
|
-
)
|
|
713
|
-
.map(({ window, used }) => ({
|
|
714
|
-
window: windowWords({ window }),
|
|
715
|
-
percent: Math.round(used),
|
|
716
|
-
}))
|
|
717
|
-
|
|
718
|
-
/**
|
|
719
|
-
* What an `agent_started` is, from the ticket's card before it:
|
|
720
|
-
* - `retry`: a new try after the role's (or any) turn failed;
|
|
721
|
-
* - `review_fix`: a test-writer (always fresh) or the implementer (a
|
|
722
|
-
* follow-up, or fresh if its session was lost) sent a ticket review's
|
|
723
|
-
* findings, in a review fix round;
|
|
724
|
-
* - `fix`: a follow-up answering a failed red check or gates, a new round;
|
|
725
|
-
* - `resent`: the same fix sent again (its turn never ended, such as after
|
|
726
|
-
* a crash);
|
|
727
|
-
* - `fresh`: anything else.
|
|
728
|
-
*/
|
|
729
|
-
export const startKind = ({
|
|
730
|
-
card,
|
|
731
|
-
role,
|
|
732
|
-
follow_up_of,
|
|
733
|
-
}: {
|
|
734
|
-
card: TicketCard | undefined
|
|
735
|
-
role: string
|
|
736
|
-
follow_up_of: string | null
|
|
737
|
-
}): 'retry' | 'review_fix' | 'fix' | 'resent' | 'fresh' => {
|
|
738
|
-
if (card === undefined) return 'fresh'
|
|
739
|
-
if (card.failed_turn !== null) return 'retry'
|
|
740
|
-
if (card.open_check === 'review' && REVIEW_FIXERS.has(role)) {
|
|
741
|
-
return card.role === null ? 'review_fix' : 'resent'
|
|
742
|
-
}
|
|
743
|
-
if (follow_up_of === null || card.open_check === null) return 'fresh'
|
|
744
|
-
return card.role === null ? 'fix' : 'resent'
|
|
745
|
-
}
|
|
746
|
-
|
|
747
92
|
/**
|
|
748
93
|
* Applies one journal record to the board. Pure: returns a new state.
|
|
749
94
|
*
|
|
@@ -783,1211 +128,49 @@ export const applyEnded = ({
|
|
|
783
128
|
}),
|
|
784
129
|
})
|
|
785
130
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Whether `record` is one of `kinds`, narrowing it to those kinds. A type
|
|
133
|
+
* guard, so it takes its arguments by position.
|
|
134
|
+
*/
|
|
135
|
+
const isOneOf = <Kinds extends readonly BoardKind[]>(
|
|
136
|
+
kinds: Kinds,
|
|
791
137
|
record: BoardRecord
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
switch (record.kind) {
|
|
795
|
-
case 'run_started': {
|
|
796
|
-
const budget =
|
|
797
|
-
record.content.config?.run_budget_tokens ??
|
|
798
|
-
DEFAULT_RUN_BUDGET_TOKENS
|
|
799
|
-
return {
|
|
800
|
-
...state,
|
|
801
|
-
run_budget_tokens: budget,
|
|
802
|
-
run_budget_each: budget,
|
|
803
|
-
run: {
|
|
804
|
-
...state.run,
|
|
805
|
-
spec_number: record.content.spec_number,
|
|
806
|
-
phase: 'intake',
|
|
807
|
-
started_on: record.content.luca_version,
|
|
808
|
-
},
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
case 'engine_resumed':
|
|
812
|
-
return {
|
|
813
|
-
...state,
|
|
814
|
-
run: {
|
|
815
|
-
...state.run,
|
|
816
|
-
version_note: versionNote({
|
|
817
|
-
run: state.run,
|
|
818
|
-
resumed_on: record.content.luca_version,
|
|
819
|
-
}),
|
|
820
|
-
},
|
|
821
|
-
}
|
|
822
|
-
case 'intake_read':
|
|
823
|
-
return {
|
|
824
|
-
...state,
|
|
825
|
-
run: {
|
|
826
|
-
...state.run,
|
|
827
|
-
spec_title:
|
|
828
|
-
state.run.spec_title ?? record.content.spec.title,
|
|
829
|
-
phase: 'intake',
|
|
830
|
-
},
|
|
831
|
-
}
|
|
832
|
-
case 'intake_refused':
|
|
833
|
-
return {
|
|
834
|
-
...state,
|
|
835
|
-
run: {
|
|
836
|
-
...state.run,
|
|
837
|
-
phase: 'refused',
|
|
838
|
-
refusal: record.content.problems.map(
|
|
839
|
-
(problem) =>
|
|
840
|
-
`${problem.ticket === null ? 'The run' : `#${problem.ticket}`}: missing ${problem.missing.join(', ')}`
|
|
841
|
-
),
|
|
842
|
-
},
|
|
843
|
-
}
|
|
844
|
-
case 'nothing_to_do':
|
|
845
|
-
return { ...state, run: { ...state.run, phase: 'nothing_to_do' } }
|
|
846
|
-
case 'spec_snapshot':
|
|
847
|
-
return {
|
|
848
|
-
...state,
|
|
849
|
-
run: {
|
|
850
|
-
...state.run,
|
|
851
|
-
spec_number: record.content.spec.number,
|
|
852
|
-
spec_title: record.content.spec.title,
|
|
853
|
-
phase: 'building',
|
|
854
|
-
},
|
|
855
|
-
}
|
|
856
|
-
case 'ticket_snapshot': {
|
|
857
|
-
const { number, title, labels, blockers } = record.content
|
|
858
|
-
const refactor = labels.includes('refactor')
|
|
859
|
-
const known = state.tickets.some((card) => card.number === number)
|
|
860
|
-
if (known) {
|
|
861
|
-
return updateTicket({
|
|
862
|
-
state,
|
|
863
|
-
number,
|
|
864
|
-
update: (card) => ({ ...card, title, refactor, blockers }),
|
|
865
|
-
})
|
|
866
|
-
}
|
|
867
|
-
return {
|
|
868
|
-
...state,
|
|
869
|
-
tickets: [
|
|
870
|
-
...state.tickets,
|
|
871
|
-
newTicket({ number, title, refactor, blockers }),
|
|
872
|
-
],
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
case 'run_branch_created':
|
|
876
|
-
return {
|
|
877
|
-
...state,
|
|
878
|
-
run: { ...state.run, branch: record.content.branch },
|
|
879
|
-
}
|
|
880
|
-
case 'ticket_worktree_created':
|
|
881
|
-
return updateTicket({
|
|
882
|
-
state,
|
|
883
|
-
number: ticket,
|
|
884
|
-
update: (card) => ({
|
|
885
|
-
...card,
|
|
886
|
-
started: true,
|
|
887
|
-
stage: 'building',
|
|
888
|
-
activity: 'starting',
|
|
889
|
-
}),
|
|
890
|
-
})
|
|
891
|
-
case 'dependencies_installed': {
|
|
892
|
-
const { check } = record.content
|
|
893
|
-
if (check === null || check.ok || ticket === null) return state
|
|
894
|
-
return updateTicket({
|
|
895
|
-
state,
|
|
896
|
-
number: ticket,
|
|
897
|
-
update: (card) => ({
|
|
898
|
-
...card,
|
|
899
|
-
activity: 'install failed',
|
|
900
|
-
tried: withTried({
|
|
901
|
-
tried: card.tried,
|
|
902
|
-
line: `The install failed: \`${check.command}\``,
|
|
903
|
-
}),
|
|
904
|
-
}),
|
|
905
|
-
})
|
|
906
|
-
}
|
|
907
|
-
case 'baseline_tests': {
|
|
908
|
-
const tests = testCounts({ cases: record.content.cases })
|
|
909
|
-
return updateTicket({
|
|
910
|
-
state,
|
|
911
|
-
number: ticket,
|
|
912
|
-
update: (card) => ({
|
|
913
|
-
...card,
|
|
914
|
-
started: true,
|
|
915
|
-
activity: 'baseline tests',
|
|
916
|
-
tests,
|
|
917
|
-
baseline_tests: tests,
|
|
918
|
-
}),
|
|
919
|
-
})
|
|
920
|
-
}
|
|
921
|
-
case 'baseline_reused': {
|
|
922
|
-
// Another ticket's baseline from the same run-branch commit (#404).
|
|
923
|
-
const { from_ticket } = record.content
|
|
924
|
-
const tests =
|
|
925
|
-
state.tickets.find((card) => card.number === from_ticket)
|
|
926
|
-
?.baseline_tests ?? null
|
|
927
|
-
return updateTicket({
|
|
928
|
-
state,
|
|
929
|
-
number: ticket,
|
|
930
|
-
update: (card) => ({
|
|
931
|
-
...card,
|
|
932
|
-
started: true,
|
|
933
|
-
activity: `baseline tests (reused from #${from_ticket})`,
|
|
934
|
-
tests,
|
|
935
|
-
baseline_tests: tests,
|
|
936
|
-
}),
|
|
937
|
-
})
|
|
938
|
-
}
|
|
939
|
-
case 'agent_started':
|
|
940
|
-
// The learner (#370) has no ticket and is no final review agent.
|
|
941
|
-
if (record.content.role === LEARNER) {
|
|
942
|
-
return withLearner({ state, learner: 'learning' })
|
|
943
|
-
}
|
|
944
|
-
return agentStarted({ state, record })
|
|
945
|
-
case 'agent_finished':
|
|
946
|
-
if (record.content.role === LEARNER) {
|
|
947
|
-
return withLearner({ state, learner: 'learned' })
|
|
948
|
-
}
|
|
949
|
-
return agentFinished({ state, record })
|
|
950
|
-
case 'agent_failed': {
|
|
951
|
-
const { role, error, failure } = record.content
|
|
952
|
-
// A failed learner gets a fresh one; its row says so.
|
|
953
|
-
if (role === LEARNER) return state
|
|
954
|
-
// A final review agent (a lens or a fixer) has no ticket.
|
|
955
|
-
if (ticket === null) {
|
|
956
|
-
return finalTried({
|
|
957
|
-
state,
|
|
958
|
-
line: `The ${roleWords({ role })} ${failureText({ failure })}: ${error}`,
|
|
959
|
-
})
|
|
960
|
-
}
|
|
961
|
-
return updateTicket({
|
|
962
|
-
state,
|
|
963
|
-
number: ticket,
|
|
964
|
-
update: (card) => ({
|
|
965
|
-
...card,
|
|
966
|
-
role: null,
|
|
967
|
-
failed_turn: role,
|
|
968
|
-
activity: `${role} ${failure === 'engine' ? 'could not run' : 'failed'}`,
|
|
969
|
-
tried: withTried({
|
|
970
|
-
tried: card.tried,
|
|
971
|
-
line: `The ${role} ${failureText({ failure })}: ${error}`,
|
|
972
|
-
}),
|
|
973
|
-
}),
|
|
974
|
-
})
|
|
975
|
-
}
|
|
976
|
-
case 'agent_session': {
|
|
977
|
-
const tokens = sessionTokens(record.content)
|
|
978
|
-
const { role } = record.content
|
|
979
|
-
const withTokens = updateTicket({
|
|
980
|
-
state,
|
|
981
|
-
number: ticket,
|
|
982
|
-
update: (card) => ({
|
|
983
|
-
...card,
|
|
984
|
-
tokens: card.tokens + tokens,
|
|
985
|
-
agent_tokens: {
|
|
986
|
-
...card.agent_tokens,
|
|
987
|
-
[role]: (card.agent_tokens[role] ?? 0) + tokens,
|
|
988
|
-
},
|
|
989
|
-
}),
|
|
990
|
-
})
|
|
991
|
-
return {
|
|
992
|
-
...withTokens,
|
|
993
|
-
run_tokens: state.run_tokens + countedTokens(record.content),
|
|
994
|
-
usage: usageAfter({
|
|
995
|
-
usage: state.usage,
|
|
996
|
-
session: record.content.session,
|
|
997
|
-
time: record.time,
|
|
998
|
-
}),
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
case 'run_stopped': {
|
|
1002
|
-
const { reason, role, billing, crashed } = record.content
|
|
1003
|
-
const stopped = updateTicket({
|
|
1004
|
-
state,
|
|
1005
|
-
number: ticket,
|
|
1006
|
-
update: (card) => ({
|
|
1007
|
-
...card,
|
|
1008
|
-
role: null,
|
|
1009
|
-
activity: 'run stopped',
|
|
1010
|
-
tried: withTried({
|
|
1011
|
-
tried: card.tried,
|
|
1012
|
-
line: `${billing ? 'Stopped for billing' : crashed ? 'Stopped after crashes' : 'The run stopped'}: ${reason}`,
|
|
1013
|
-
}),
|
|
1014
|
-
}),
|
|
1015
|
-
})
|
|
1016
|
-
return {
|
|
1017
|
-
...stopped,
|
|
1018
|
-
run: {
|
|
1019
|
-
...stopped.run,
|
|
1020
|
-
stopped: {
|
|
1021
|
-
reason,
|
|
1022
|
-
role,
|
|
1023
|
-
ticket,
|
|
1024
|
-
billing,
|
|
1025
|
-
crashed,
|
|
1026
|
-
since: record.time,
|
|
1027
|
-
},
|
|
1028
|
-
},
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
case 'worktree_reset':
|
|
1032
|
-
return updateTicket({
|
|
1033
|
-
state,
|
|
1034
|
-
number: ticket,
|
|
1035
|
-
update: (card) => ({
|
|
1036
|
-
...card,
|
|
1037
|
-
step: 0,
|
|
1038
|
-
fix_round: 0,
|
|
1039
|
-
open_check: null,
|
|
1040
|
-
activity: 'starting over',
|
|
1041
|
-
tried: withTried({
|
|
1042
|
-
tried: card.tried,
|
|
1043
|
-
line: 'Started over from the tests after a bad test',
|
|
1044
|
-
}),
|
|
1045
|
-
}),
|
|
1046
|
-
})
|
|
1047
|
-
case 'red_check':
|
|
1048
|
-
return updateTicket({
|
|
1049
|
-
state,
|
|
1050
|
-
number: ticket,
|
|
1051
|
-
update: (card) => ({
|
|
1052
|
-
...card,
|
|
1053
|
-
step: record.content.ok ? 2 : 1,
|
|
1054
|
-
open_check: record.content.ok ? null : 'red_check',
|
|
1055
|
-
fix_round: record.content.ok ? 0 : card.fix_round,
|
|
1056
|
-
activity: record.content.ok
|
|
1057
|
-
? 'red check passed'
|
|
1058
|
-
: 'red check failed',
|
|
1059
|
-
tests: record.content.tests
|
|
1060
|
-
? testCounts({ cases: record.content.tests.cases })
|
|
1061
|
-
: card.tests,
|
|
1062
|
-
tried: record.content.ok
|
|
1063
|
-
? card.tried
|
|
1064
|
-
: withTried({
|
|
1065
|
-
tried: card.tried,
|
|
1066
|
-
line: `The red check failed: ${record.content.problems[0] ?? 'no detail'}`,
|
|
1067
|
-
}),
|
|
1068
|
-
}),
|
|
1069
|
-
})
|
|
1070
|
-
case 'leftover_scan':
|
|
1071
|
-
if (record.content.hits.length === 0) return state
|
|
1072
|
-
if (ticket === null) {
|
|
1073
|
-
return finalTried({
|
|
1074
|
-
state,
|
|
1075
|
-
line: `The leftover scan found ${record.content.hits.map((hit) => hit.path).join(', ')} in the final review's fixes`,
|
|
1076
|
-
})
|
|
1077
|
-
}
|
|
1078
|
-
return updateTicket({
|
|
1079
|
-
state,
|
|
1080
|
-
number: ticket,
|
|
1081
|
-
update: (card) => ({
|
|
1082
|
-
...card,
|
|
1083
|
-
activity: 'leftovers found',
|
|
1084
|
-
tried: withTried({
|
|
1085
|
-
tried: card.tried,
|
|
1086
|
-
line: `The leftover scan found ${record.content.hits.map((hit) => hit.path).join(', ')}`,
|
|
1087
|
-
}),
|
|
1088
|
-
}),
|
|
1089
|
-
})
|
|
1090
|
-
case 'commit_made':
|
|
1091
|
-
return updateTicket({
|
|
1092
|
-
state,
|
|
1093
|
-
number: ticket,
|
|
1094
|
-
update: (card) => {
|
|
1095
|
-
switch (record.content.stage) {
|
|
1096
|
-
case 'green':
|
|
1097
|
-
return {
|
|
1098
|
-
...card,
|
|
1099
|
-
step: 4,
|
|
1100
|
-
activity: 'code committed',
|
|
1101
|
-
}
|
|
1102
|
-
case 'fix':
|
|
1103
|
-
return {
|
|
1104
|
-
...card,
|
|
1105
|
-
activity: 'review fixes committed',
|
|
1106
|
-
}
|
|
1107
|
-
default:
|
|
1108
|
-
return { ...card, activity: 'tests committed' }
|
|
1109
|
-
}
|
|
1110
|
-
},
|
|
1111
|
-
})
|
|
1112
|
-
case 'gates_run':
|
|
1113
|
-
return gatesRun({ state, record })
|
|
1114
|
-
case 'ticket_joined':
|
|
1115
|
-
return updateTicket({
|
|
1116
|
-
state,
|
|
1117
|
-
number: ticket,
|
|
1118
|
-
update: (card) =>
|
|
1119
|
-
record.content.ok
|
|
1120
|
-
? {
|
|
1121
|
-
...card,
|
|
1122
|
-
stage: 'done',
|
|
1123
|
-
step: ALL_STEPS_DONE,
|
|
1124
|
-
role: null,
|
|
1125
|
-
activity: 'joined',
|
|
1126
|
-
}
|
|
1127
|
-
: {
|
|
1128
|
-
...card,
|
|
1129
|
-
activity: 'join failed',
|
|
1130
|
-
tried: withTried({
|
|
1131
|
-
tried: card.tried,
|
|
1132
|
-
line: `Joining the run branch failed: ${record.content.error ?? 'no detail'}`,
|
|
1133
|
-
}),
|
|
1134
|
-
},
|
|
1135
|
-
})
|
|
1136
|
-
case 'ticket_rebased':
|
|
1137
|
-
return ticketRebased({ state, record })
|
|
1138
|
-
case 'run_branch_pushed':
|
|
1139
|
-
return updateTicket({
|
|
1140
|
-
state,
|
|
1141
|
-
number: ticket,
|
|
1142
|
-
update: (card) =>
|
|
1143
|
-
card.stage === 'done'
|
|
1144
|
-
? { ...card, activity: 'pushed' }
|
|
1145
|
-
: card,
|
|
1146
|
-
})
|
|
1147
|
-
case 'ticket_stuck': {
|
|
1148
|
-
if (ticket === null) return state
|
|
1149
|
-
const card = state.tickets.find((entry) => entry.number === ticket)
|
|
1150
|
-
const stuck = updateTicket({
|
|
1151
|
-
state,
|
|
1152
|
-
number: ticket,
|
|
1153
|
-
update: (entry) => ({
|
|
1154
|
-
...entry,
|
|
1155
|
-
stage: 'stuck',
|
|
1156
|
-
role: null,
|
|
1157
|
-
activity: 'stuck',
|
|
1158
|
-
}),
|
|
1159
|
-
})
|
|
1160
|
-
return addNeedsYou({
|
|
1161
|
-
state: stuck,
|
|
1162
|
-
item: {
|
|
1163
|
-
key: ticketKey({ ticket }),
|
|
1164
|
-
ticket,
|
|
1165
|
-
subject: `Ticket #${ticket} is stuck${card ? `: ${card.title}` : ''}`,
|
|
1166
|
-
reason: reasonText({ reason: record.content.reason }),
|
|
1167
|
-
detail: clip({
|
|
1168
|
-
text: record.content.detail,
|
|
1169
|
-
max: DETAIL_MAX,
|
|
1170
|
-
}),
|
|
1171
|
-
tried: card?.tried ?? [],
|
|
1172
|
-
replies: [`retry #${ticket}`, `skip #${ticket}`, 'stop'],
|
|
1173
|
-
since: record.time,
|
|
1174
|
-
},
|
|
1175
|
-
})
|
|
1176
|
-
}
|
|
1177
|
-
case 'run_stuck':
|
|
1178
|
-
return addNeedsYou({
|
|
1179
|
-
state,
|
|
1180
|
-
item: {
|
|
1181
|
-
key: RUN_KEY,
|
|
1182
|
-
ticket: null,
|
|
1183
|
-
subject: 'The run is stuck',
|
|
1184
|
-
reason: runReasonText({ reason: record.content.reason }),
|
|
1185
|
-
detail: clip({
|
|
1186
|
-
text: record.content.detail,
|
|
1187
|
-
max: DETAIL_MAX,
|
|
1188
|
-
}),
|
|
1189
|
-
tried: [],
|
|
1190
|
-
replies: ['retry', 'stop'],
|
|
1191
|
-
since: record.time,
|
|
1192
|
-
},
|
|
1193
|
-
})
|
|
1194
|
-
case 'pull_request_opened':
|
|
1195
|
-
return {
|
|
1196
|
-
...state,
|
|
1197
|
-
run: {
|
|
1198
|
-
...state.run,
|
|
1199
|
-
phase: 'done',
|
|
1200
|
-
pr_url: record.content.url,
|
|
1201
|
-
pr_number: record.content.number,
|
|
1202
|
-
},
|
|
1203
|
-
}
|
|
1204
|
-
// Removing the worktrees at the end changes nothing on the board.
|
|
1205
|
-
case 'worktrees_removed':
|
|
1206
|
-
return state
|
|
1207
|
-
case 'jev_asked':
|
|
1208
|
-
return {
|
|
1209
|
-
...state,
|
|
1210
|
-
jev: { ...state.jev, asked: state.jev.asked + 1 },
|
|
1211
|
-
}
|
|
1212
|
-
case 'jev_answered':
|
|
1213
|
-
return {
|
|
1214
|
-
...state,
|
|
1215
|
-
jev: { ...state.jev, answered: state.jev.answered + 1 },
|
|
1216
|
-
}
|
|
1217
|
-
case 'jev_failed':
|
|
1218
|
-
return {
|
|
1219
|
-
...state,
|
|
1220
|
-
jev: { ...state.jev, failed: state.jev.failed + 1 },
|
|
1221
|
-
}
|
|
1222
|
-
case 'agent_message': {
|
|
1223
|
-
const refused = record.content.status === 'refused'
|
|
1224
|
-
return {
|
|
1225
|
-
...state,
|
|
1226
|
-
messages: {
|
|
1227
|
-
...state.messages,
|
|
1228
|
-
sent: state.messages.sent + (refused ? 0 : 1),
|
|
1229
|
-
refused: state.messages.refused + (refused ? 1 : 0),
|
|
1230
|
-
},
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
case 'agent_message_delivered':
|
|
1234
|
-
return {
|
|
1235
|
-
...state,
|
|
1236
|
-
messages: {
|
|
1237
|
-
...state.messages,
|
|
1238
|
-
delivered:
|
|
1239
|
-
state.messages.delivered + record.content.ids.length,
|
|
1240
|
-
},
|
|
1241
|
-
}
|
|
1242
|
-
// Another process's change to the shared `.git`: only counted.
|
|
1243
|
-
case 'shared_git_changed':
|
|
1244
|
-
return {
|
|
1245
|
-
...state,
|
|
1246
|
-
shared_git_changed: state.shared_git_changed + 1,
|
|
1247
|
-
}
|
|
1248
|
-
case 'limit_wait_started': {
|
|
1249
|
-
const { resets_at, until, rate_limit_type } = record.content
|
|
1250
|
-
return {
|
|
1251
|
-
...state,
|
|
1252
|
-
limit_wait: {
|
|
1253
|
-
resets_at: resets_at ?? until,
|
|
1254
|
-
window:
|
|
1255
|
-
rate_limit_type === null
|
|
1256
|
-
? null
|
|
1257
|
-
: windowWords({ window: rate_limit_type }),
|
|
1258
|
-
since: record.time,
|
|
1259
|
-
},
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
case 'limit_wait_ended':
|
|
1263
|
-
return { ...state, limit_wait: null }
|
|
1264
|
-
case 'usage_line_wait_started': {
|
|
1265
|
-
const { window, line, percent, resets_at } = record.content
|
|
1266
|
-
return {
|
|
1267
|
-
...state,
|
|
1268
|
-
usage_line_wait: {
|
|
1269
|
-
window: windowWords({ window }),
|
|
1270
|
-
line,
|
|
1271
|
-
percent,
|
|
1272
|
-
resets_at,
|
|
1273
|
-
since: record.time,
|
|
1274
|
-
},
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
case 'usage_line_wait_ended':
|
|
1278
|
-
return { ...state, usage_line_wait: null }
|
|
1279
|
-
case 'usage_recorded': {
|
|
1280
|
-
const plan_used = planUsedOf({ windows: record.content.windows })
|
|
1281
|
-
if (record.content.scope === 'run') {
|
|
1282
|
-
return { ...state, run_plan_used: plan_used }
|
|
1283
|
-
}
|
|
1284
|
-
return updateTicket({
|
|
1285
|
-
state,
|
|
1286
|
-
number: record.content.ticket ?? ticket,
|
|
1287
|
-
update: (card) => ({ ...card, plan_used }),
|
|
1288
|
-
})
|
|
1289
|
-
}
|
|
1290
|
-
case 'reply_received':
|
|
1291
|
-
return replyReceived({ state, record })
|
|
1292
|
-
case 'ticket_skipped':
|
|
1293
|
-
return skipTicket({
|
|
1294
|
-
state,
|
|
1295
|
-
ticket,
|
|
1296
|
-
because: record.content.because,
|
|
1297
|
-
})
|
|
1298
|
-
// Done, not stuck (#484): its work was already on the base branch.
|
|
1299
|
-
case 'ticket_already_done':
|
|
1300
|
-
return updateTicket({
|
|
1301
|
-
state,
|
|
1302
|
-
number: ticket,
|
|
1303
|
-
update: (card) => ({
|
|
1304
|
-
...card,
|
|
1305
|
-
stage: 'done',
|
|
1306
|
-
step: ALL_STEPS_DONE,
|
|
1307
|
-
role: null,
|
|
1308
|
-
open_check: null,
|
|
1309
|
-
activity: alreadyDoneText({ shas: record.content.shas }),
|
|
1310
|
-
}),
|
|
1311
|
-
})
|
|
1312
|
-
// Its closing changes nothing the board shows.
|
|
1313
|
-
case 'ticket_closed':
|
|
1314
|
-
return state
|
|
1315
|
-
case 'ticket_retried':
|
|
1316
|
-
return ticketRetried({ state, record })
|
|
1317
|
-
// An ignored reply is only a chat row; the engine answered it.
|
|
1318
|
-
case 'reply_ignored':
|
|
1319
|
-
return state
|
|
1320
|
-
case 'memory_recalled': {
|
|
1321
|
-
const { vaults, memories } = record.content
|
|
1322
|
-
const { memory } = state
|
|
1323
|
-
return {
|
|
1324
|
-
...state,
|
|
1325
|
-
memory: {
|
|
1326
|
-
...memory,
|
|
1327
|
-
searches: memory.searches + 1,
|
|
1328
|
-
search_errors:
|
|
1329
|
-
memory.search_errors +
|
|
1330
|
-
vaults.filter(({ ok }) => !ok).length,
|
|
1331
|
-
shown: memory.shown + memories.length,
|
|
1332
|
-
},
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
case 'memories_saved': {
|
|
1336
|
-
const count = (outcome: string) =>
|
|
1337
|
-
record.content.saves.filter((save) => save.outcome === outcome)
|
|
1338
|
-
.length
|
|
1339
|
-
const { memory } = state
|
|
1340
|
-
return {
|
|
1341
|
-
...state,
|
|
1342
|
-
memory: {
|
|
1343
|
-
...memory,
|
|
1344
|
-
added: memory.added + count('added'),
|
|
1345
|
-
updated: memory.updated + count('updated'),
|
|
1346
|
-
refused: memory.refused + count('refused'),
|
|
1347
|
-
failed: memory.failed + count('failed'),
|
|
1348
|
-
},
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
case 'learning_skipped':
|
|
1352
|
-
return withLearner({ state, learner: 'skipped' })
|
|
1353
|
-
// The listing on the spec issue is only a chat row.
|
|
1354
|
-
case 'memories_reported':
|
|
1355
|
-
return state
|
|
1356
|
-
case 'step_started':
|
|
1357
|
-
return stepStarted({ state, record })
|
|
1358
|
-
case 'step_ended':
|
|
1359
|
-
return stepEnded({ state, record })
|
|
1360
|
-
case 'final_review_started':
|
|
1361
|
-
return {
|
|
1362
|
-
...updateFinal({
|
|
1363
|
-
state,
|
|
1364
|
-
update: (review) => ({
|
|
1365
|
-
...review,
|
|
1366
|
-
state: 'reviewing',
|
|
1367
|
-
round: review.round + 1,
|
|
1368
|
-
lenses: review.lenses.map((lens) =>
|
|
1369
|
-
lens.state === 'clean'
|
|
1370
|
-
? lens
|
|
1371
|
-
: { ...lens, state: 'waiting' }
|
|
1372
|
-
),
|
|
1373
|
-
}),
|
|
1374
|
-
}),
|
|
1375
|
-
run: { ...state.run, phase: 'final_review' },
|
|
1376
|
-
}
|
|
1377
|
-
case 'lens_started':
|
|
1378
|
-
return updateLens({
|
|
1379
|
-
state,
|
|
1380
|
-
lens: record.content.lens,
|
|
1381
|
-
update: (lens) => ({ ...lens, state: 'reviewing' }),
|
|
1382
|
-
})
|
|
1383
|
-
case 'lens_finished': {
|
|
1384
|
-
const { findings } = record.content
|
|
1385
|
-
const counts = {
|
|
1386
|
-
blocker: findings.blocker,
|
|
1387
|
-
should_fix: findings.should_fix,
|
|
1388
|
-
nit: findings.nit,
|
|
1389
|
-
}
|
|
1390
|
-
return updateLens({
|
|
1391
|
-
state,
|
|
1392
|
-
lens: record.content.lens,
|
|
1393
|
-
update: (lens) => ({
|
|
1394
|
-
...lens,
|
|
1395
|
-
findings: counts,
|
|
1396
|
-
state:
|
|
1397
|
-
counts.blocker + counts.should_fix > 0
|
|
1398
|
-
? 'fixing'
|
|
1399
|
-
: 'clean',
|
|
1400
|
-
}),
|
|
1401
|
-
})
|
|
1402
|
-
}
|
|
1403
|
-
case 'final_review_fixing':
|
|
1404
|
-
return updateFinal({
|
|
1405
|
-
state,
|
|
1406
|
-
update: (review) => ({
|
|
1407
|
-
...review,
|
|
1408
|
-
state: 'fixing',
|
|
1409
|
-
fix_round: record.content.round,
|
|
1410
|
-
tried: withTried({
|
|
1411
|
-
tried: review.tried,
|
|
1412
|
-
line: `Fix round ${record.content.round}/${LOOP_CAP} on the lenses' findings`,
|
|
1413
|
-
}),
|
|
1414
|
-
}),
|
|
1415
|
-
})
|
|
1416
|
-
case 'final_review_stuck':
|
|
1417
|
-
return addNeedsYou({
|
|
1418
|
-
state: updateFinal({
|
|
1419
|
-
state,
|
|
1420
|
-
update: (review) => ({ ...review, state: 'stuck' }),
|
|
1421
|
-
}),
|
|
1422
|
-
item: {
|
|
1423
|
-
key: FINAL_KEY,
|
|
1424
|
-
ticket: null,
|
|
1425
|
-
subject: 'The final review is stuck',
|
|
1426
|
-
reason: finalReasonText({ reason: record.content.reason }),
|
|
1427
|
-
detail: clip({
|
|
1428
|
-
text: record.content.detail,
|
|
1429
|
-
max: DETAIL_MAX,
|
|
1430
|
-
}),
|
|
1431
|
-
tried: state.final_review.tried,
|
|
1432
|
-
replies: ['retry', 'stop', 'ship'],
|
|
1433
|
-
since: record.time,
|
|
1434
|
-
},
|
|
1435
|
-
})
|
|
1436
|
-
// A `ship` reply to the stuck final review, as the engine journals
|
|
1437
|
-
// it: the same as the reply itself.
|
|
1438
|
-
case 'final_review_shipped':
|
|
1439
|
-
return resolveNeedsYou({
|
|
1440
|
-
state: updateFinal({
|
|
1441
|
-
state,
|
|
1442
|
-
update: (review) => ({ ...review, state: 'passed' }),
|
|
1443
|
-
}),
|
|
1444
|
-
key: FINAL_KEY,
|
|
1445
|
-
})
|
|
1446
|
-
case 'final_review_passed':
|
|
1447
|
-
return resolveNeedsYou({
|
|
1448
|
-
state: updateFinal({
|
|
1449
|
-
state,
|
|
1450
|
-
update: (review) => ({
|
|
1451
|
-
...review,
|
|
1452
|
-
state: 'passed',
|
|
1453
|
-
lenses: review.lenses.map((lens) => ({
|
|
1454
|
-
...lens,
|
|
1455
|
-
state: 'clean',
|
|
1456
|
-
})),
|
|
1457
|
-
}),
|
|
1458
|
-
}),
|
|
1459
|
-
key: FINAL_KEY,
|
|
1460
|
-
})
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
const OPEN_CHECK_TEXT = {
|
|
1465
|
-
red_check: 'red check',
|
|
1466
|
-
gates: 'checks',
|
|
1467
|
-
review: 'ticket review',
|
|
1468
|
-
} as const
|
|
138
|
+
): record is Extract<BoardRecord, { kind: Kinds[number] }> =>
|
|
139
|
+
kinds.includes(record.kind)
|
|
1469
140
|
|
|
1470
141
|
/**
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1473
|
-
* fixer adds its "tried" line. The card stays in Reviewing at its step.
|
|
142
|
+
* Passes a record to the module that handles its kind. Every kind belongs to
|
|
143
|
+
* one module's `_KINDS`; a kind that belongs to none fails the type check.
|
|
1474
144
|
*/
|
|
1475
|
-
const
|
|
1476
|
-
card,
|
|
1477
|
-
role,
|
|
1478
|
-
}: {
|
|
1479
|
-
card: TicketCard
|
|
1480
|
-
role: string
|
|
1481
|
-
}): TicketCard => {
|
|
1482
|
-
const round = Math.max(card.review_round, 1)
|
|
1483
|
-
const counts = card.findings
|
|
1484
|
-
? reviewCountsText({ findings: card.findings, nits: false })
|
|
1485
|
-
: null
|
|
1486
|
-
return {
|
|
1487
|
-
...card,
|
|
1488
|
-
started: true,
|
|
1489
|
-
role,
|
|
1490
|
-
stage: 'reviewing',
|
|
1491
|
-
review_fix_round: round,
|
|
1492
|
-
activity: `fixing the review's findings (${round}/${LOOP_CAP})`,
|
|
1493
|
-
tried:
|
|
1494
|
-
card.review_fix_round < round
|
|
1495
|
-
? withTried({
|
|
1496
|
-
tried: card.tried,
|
|
1497
|
-
line: `Review fix round ${round}/${LOOP_CAP}${counts ? `: ${counts}` : ''}`,
|
|
1498
|
-
})
|
|
1499
|
-
: card.tried,
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
const agentStarted = ({
|
|
1504
|
-
state,
|
|
1505
|
-
record,
|
|
1506
|
-
}: {
|
|
1507
|
-
state: BoardState
|
|
1508
|
-
record: Extract<BoardRecord, { kind: 'agent_started' }>
|
|
1509
|
-
}): BoardState => {
|
|
1510
|
-
const { role, follow_up_of } = record.content
|
|
1511
|
-
return updateTicket({
|
|
1512
|
-
state,
|
|
1513
|
-
number: record.ticket,
|
|
1514
|
-
update: (card) => {
|
|
1515
|
-
const kind = startKind({ card, role, follow_up_of })
|
|
1516
|
-
if (
|
|
1517
|
-
kind === 'review_fix' ||
|
|
1518
|
-
(kind === 'resent' && card.open_check === 'review')
|
|
1519
|
-
) {
|
|
1520
|
-
return reviewFixStarted({ card, role })
|
|
1521
|
-
}
|
|
1522
|
-
const fix_round =
|
|
1523
|
-
kind === 'fix' ? card.fix_round + 1 : card.fix_round
|
|
1524
|
-
const started = { ...card, started: true, role, fix_round }
|
|
1525
|
-
const tried =
|
|
1526
|
-
kind === 'fix' && card.open_check !== null
|
|
1527
|
-
? withTried({
|
|
1528
|
-
tried: card.tried,
|
|
1529
|
-
line: `Fix round ${fix_round}/${LOOP_CAP} after the ${OPEN_CHECK_TEXT[card.open_check]}`,
|
|
1530
|
-
})
|
|
1531
|
-
: card.tried
|
|
1532
|
-
const fixing = kind === 'fix' || kind === 'resent'
|
|
1533
|
-
if (role === 'test-writer') {
|
|
1534
|
-
// During a ticket review (a retried review fixer) the card
|
|
1535
|
-
// stays in Reviewing at its step.
|
|
1536
|
-
const reviewing = card.stage === 'reviewing'
|
|
1537
|
-
return {
|
|
1538
|
-
...started,
|
|
1539
|
-
tried,
|
|
1540
|
-
stage: reviewing ? 'reviewing' : 'building',
|
|
1541
|
-
step: reviewing ? card.step : 0,
|
|
1542
|
-
activity: fixing
|
|
1543
|
-
? `fixing tests (${fix_round}/${LOOP_CAP})`
|
|
1544
|
-
: kind === 'retry'
|
|
1545
|
-
? 'writing tests again'
|
|
1546
|
-
: 'writing tests',
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
if (role === 'implementer') {
|
|
1550
|
-
return {
|
|
1551
|
-
...started,
|
|
1552
|
-
tried,
|
|
1553
|
-
stage:
|
|
1554
|
-
card.stage === 'reviewing' ? 'reviewing' : 'building',
|
|
1555
|
-
step: 2,
|
|
1556
|
-
activity: fixing
|
|
1557
|
-
? `fixing (${fix_round}/${LOOP_CAP})`
|
|
1558
|
-
: kind === 'retry'
|
|
1559
|
-
? 'coding again'
|
|
1560
|
-
: 'coding',
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
if (role === 'ticket-reviewer') {
|
|
1564
|
-
const retry = kind === 'retry'
|
|
1565
|
-
return {
|
|
1566
|
-
...started,
|
|
1567
|
-
stage: 'reviewing',
|
|
1568
|
-
step: 4,
|
|
1569
|
-
open_check: retry ? card.open_check : null,
|
|
1570
|
-
review_round: retry
|
|
1571
|
-
? Math.max(card.review_round, 1)
|
|
1572
|
-
: card.review_round + 1,
|
|
1573
|
-
activity: retry
|
|
1574
|
-
? 'reviewing again'
|
|
1575
|
-
: card.review_fix_round > 0
|
|
1576
|
-
? 're-reviewing'
|
|
1577
|
-
: 'reviewing',
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
return { ...started, activity: role }
|
|
1581
|
-
},
|
|
1582
|
-
})
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
const agentFinished = ({
|
|
1586
|
-
state,
|
|
1587
|
-
record,
|
|
1588
|
-
}: {
|
|
1589
|
-
state: BoardState
|
|
1590
|
-
record: Extract<BoardRecord, { kind: 'agent_finished' }>
|
|
1591
|
-
}): BoardState => {
|
|
1592
|
-
const { role, result } = record.content
|
|
1593
|
-
return updateTicket({
|
|
1594
|
-
state,
|
|
1595
|
-
number: record.ticket,
|
|
1596
|
-
update: (card) => {
|
|
1597
|
-
const wontFix = result.finding_responses.filter(
|
|
1598
|
-
({ response }) => response === 'wont_fix'
|
|
1599
|
-
)
|
|
1600
|
-
const done = {
|
|
1601
|
-
...card,
|
|
1602
|
-
role: null,
|
|
1603
|
-
failed_turn: null,
|
|
1604
|
-
tried: wontFix.reduce(
|
|
1605
|
-
(tried, { finding_id, reason }) =>
|
|
1606
|
-
withTried({
|
|
1607
|
-
tried,
|
|
1608
|
-
line: `Won't fix ${finding_id}: ${reason}`,
|
|
1609
|
-
}),
|
|
1610
|
-
card.tried
|
|
1611
|
-
),
|
|
1612
|
-
}
|
|
1613
|
-
if (role === 'test-writer') {
|
|
1614
|
-
if (card.open_check === 'review') {
|
|
1615
|
-
return { ...done, activity: 'test findings answered' }
|
|
1616
|
-
}
|
|
1617
|
-
if (result.outcome === 'already_done') {
|
|
1618
|
-
return { ...done, activity: 'already done' }
|
|
1619
|
-
}
|
|
1620
|
-
return result.outcome === 'nothing_new_to_test'
|
|
1621
|
-
? { ...done, step: 2, activity: 'nothing new to test' }
|
|
1622
|
-
: { ...done, step: 1, activity: 'red check' }
|
|
1623
|
-
}
|
|
1624
|
-
if (role === 'implementer') {
|
|
1625
|
-
return result.outcome === 'bad_test'
|
|
1626
|
-
? { ...done, activity: 'bad test' }
|
|
1627
|
-
: { ...done, step: 3, activity: 'checks' }
|
|
1628
|
-
}
|
|
1629
|
-
if (role === 'ticket-reviewer') {
|
|
1630
|
-
const findings = result.findings
|
|
1631
|
-
? countFindings({ findings: result.findings })
|
|
1632
|
-
: card.findings
|
|
1633
|
-
const ruled = result.rulings.reduce(
|
|
1634
|
-
(tried, { finding_id, ruling, reason }) =>
|
|
1635
|
-
withTried({
|
|
1636
|
-
tried,
|
|
1637
|
-
line:
|
|
1638
|
-
ruling === 'accepted'
|
|
1639
|
-
? `Declined ${finding_id} accepted: ${reason}`
|
|
1640
|
-
: `${finding_id} still stands: ${reason}`,
|
|
1641
|
-
}),
|
|
1642
|
-
done.tried
|
|
1643
|
-
)
|
|
1644
|
-
return result.verdict === 'approve'
|
|
1645
|
-
? {
|
|
1646
|
-
...done,
|
|
1647
|
-
step: ALL_STEPS_DONE,
|
|
1648
|
-
findings,
|
|
1649
|
-
open_check: null,
|
|
1650
|
-
tried: ruled,
|
|
1651
|
-
activity: 'approved',
|
|
1652
|
-
}
|
|
1653
|
-
: {
|
|
1654
|
-
...done,
|
|
1655
|
-
findings,
|
|
1656
|
-
open_check: 'review',
|
|
1657
|
-
tried: ruled,
|
|
1658
|
-
activity: 'changes requested',
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
return done
|
|
1662
|
-
},
|
|
1663
|
-
})
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
const gatesRun = ({
|
|
145
|
+
const applyKind = ({
|
|
1667
146
|
state,
|
|
1668
147
|
record,
|
|
1669
148
|
}: {
|
|
1670
149
|
state: BoardState
|
|
1671
|
-
record:
|
|
150
|
+
record: BoardRecord
|
|
1672
151
|
}): BoardState => {
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
.map((check) => check.name)
|
|
1677
|
-
.join(', ')
|
|
1678
|
-
const afterJoin = target === 'run_branch'
|
|
1679
|
-
// The final review's fixes are checked on the run branch, with no ticket.
|
|
1680
|
-
if (record.ticket === null) {
|
|
1681
|
-
return ok
|
|
1682
|
-
? state
|
|
1683
|
-
: finalTried({
|
|
1684
|
-
state,
|
|
1685
|
-
line: `The checks failed on the final review's fixes: ${failed || 'no detail'}`,
|
|
1686
|
-
})
|
|
152
|
+
if (isOneOf(RUN_KINDS, record)) return applyRunKind({ state, record })
|
|
153
|
+
if (isOneOf(TICKET_START_KINDS, record)) {
|
|
154
|
+
return applyTicketStartKind({ state, record })
|
|
1687
155
|
}
|
|
1688
|
-
return
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
update: (card) => {
|
|
1692
|
-
if (ok) {
|
|
1693
|
-
return afterJoin
|
|
1694
|
-
? { ...card, activity: 'checks passed after joining' }
|
|
1695
|
-
: {
|
|
1696
|
-
...card,
|
|
1697
|
-
step: 4,
|
|
1698
|
-
open_check: null,
|
|
1699
|
-
fix_round: 0,
|
|
1700
|
-
activity: 'checks passed',
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
return {
|
|
1704
|
-
...card,
|
|
1705
|
-
step: afterJoin ? card.step : 3,
|
|
1706
|
-
open_check: afterJoin ? card.open_check : 'gates',
|
|
1707
|
-
activity: afterJoin
|
|
1708
|
-
? 'checks failed after joining'
|
|
1709
|
-
: 'checks failed',
|
|
1710
|
-
tried: withTried({
|
|
1711
|
-
tried: card.tried,
|
|
1712
|
-
line: `${afterJoin ? 'After joining, the' : 'The'} checks failed: ${failed || 'no detail'}`,
|
|
1713
|
-
}),
|
|
1714
|
-
}
|
|
1715
|
-
},
|
|
1716
|
-
})
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
/** What happened when a ticket was sent back onto the run branch, in words. */
|
|
1720
|
-
export const rebasedText = ({
|
|
1721
|
-
content,
|
|
1722
|
-
}: {
|
|
1723
|
-
content: Extract<BoardRecord, { kind: 'ticket_rebased' }>['content']
|
|
1724
|
-
}): string => {
|
|
1725
|
-
if (content.cause === 'join_gates') {
|
|
1726
|
-
return 'The checks failed after joining; fixing on top of the run branch'
|
|
156
|
+
if (isOneOf(AGENT_KINDS, record)) return applyAgentKind({ state, record })
|
|
157
|
+
if (isOneOf(TICKET_CHECKS_KINDS, record)) {
|
|
158
|
+
return applyTicketChecksKind({ state, record })
|
|
1727
159
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
? 'Clashed with the run branch; fixing on top of it'
|
|
1731
|
-
: `Clashed with the run branch in ${files.join(', ')}`
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
/**
|
|
1735
|
-
* A ticket sent back onto the run branch builds again: its tests first if
|
|
1736
|
-
* test files clashed, else its code; then the checks and a fresh review,
|
|
1737
|
-
* whose fix rounds count from zero again.
|
|
1738
|
-
*/
|
|
1739
|
-
const ticketRebased = ({
|
|
1740
|
-
state,
|
|
1741
|
-
record,
|
|
1742
|
-
}: {
|
|
1743
|
-
state: BoardState
|
|
1744
|
-
record: Extract<BoardRecord, { kind: 'ticket_rebased' }>
|
|
1745
|
-
}): BoardState =>
|
|
1746
|
-
updateTicket({
|
|
1747
|
-
state,
|
|
1748
|
-
number: record.ticket,
|
|
1749
|
-
update: (card) => ({
|
|
1750
|
-
...card,
|
|
1751
|
-
stage: 'building',
|
|
1752
|
-
step: record.content.tests.length > 0 ? 0 : 2,
|
|
1753
|
-
role: null,
|
|
1754
|
-
fix_round: 0,
|
|
1755
|
-
// The engine starts the ticket's review loop over on top of the
|
|
1756
|
-
// run branch: a fresh reviewer, fresh review fix rounds.
|
|
1757
|
-
review_fix_round: 0,
|
|
1758
|
-
findings: null,
|
|
1759
|
-
open_check: null,
|
|
1760
|
-
failed_turn: null,
|
|
1761
|
-
activity: 'fixing on the run branch',
|
|
1762
|
-
tried: withTried({
|
|
1763
|
-
tried: card.tried,
|
|
1764
|
-
line: rebasedText({ content: record.content }),
|
|
1765
|
-
}),
|
|
1766
|
-
}),
|
|
1767
|
-
})
|
|
1768
|
-
|
|
1769
|
-
const skipTicket = ({
|
|
1770
|
-
state,
|
|
1771
|
-
ticket,
|
|
1772
|
-
because,
|
|
1773
|
-
}: {
|
|
1774
|
-
state: BoardState
|
|
1775
|
-
ticket: number | null
|
|
1776
|
-
because: number | null
|
|
1777
|
-
}): BoardState => {
|
|
1778
|
-
if (ticket === null) return state
|
|
1779
|
-
return resolveNeedsYou({
|
|
1780
|
-
state: updateTicket({
|
|
1781
|
-
state,
|
|
1782
|
-
number: ticket,
|
|
1783
|
-
update: (card) => ({
|
|
1784
|
-
...card,
|
|
1785
|
-
stage: 'skipped',
|
|
1786
|
-
role: null,
|
|
1787
|
-
activity:
|
|
1788
|
-
because === null
|
|
1789
|
-
? 'skipped'
|
|
1790
|
-
: `skipped: waits on skipped #${because}`,
|
|
1791
|
-
}),
|
|
1792
|
-
}),
|
|
1793
|
-
key: ticketKey({ ticket }),
|
|
1794
|
-
})
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
/**
|
|
1798
|
-
* How the engine took a `retry`: `resume` changes nothing more than the
|
|
1799
|
-
* reply did; `restart` builds the edited ticket from scratch (its new
|
|
1800
|
-
* snapshot came just before); `refused` leaves it stuck, back in Needs you.
|
|
1801
|
-
*/
|
|
1802
|
-
const ticketRetried = ({
|
|
1803
|
-
state,
|
|
1804
|
-
record,
|
|
1805
|
-
}: {
|
|
1806
|
-
state: BoardState
|
|
1807
|
-
record: Extract<BoardRecord, { kind: 'ticket_retried' }>
|
|
1808
|
-
}): BoardState => {
|
|
1809
|
-
const { ticket } = record
|
|
1810
|
-
if (ticket === null) return state
|
|
1811
|
-
switch (record.content.mode) {
|
|
1812
|
-
case 'resume':
|
|
1813
|
-
return state
|
|
1814
|
-
case 'restart':
|
|
1815
|
-
return updateTicket({
|
|
1816
|
-
state,
|
|
1817
|
-
number: ticket,
|
|
1818
|
-
update: (card) => ({
|
|
1819
|
-
...card,
|
|
1820
|
-
stage: 'building',
|
|
1821
|
-
step: 0,
|
|
1822
|
-
role: null,
|
|
1823
|
-
fix_round: 0,
|
|
1824
|
-
review_fix_round: 0,
|
|
1825
|
-
findings: null,
|
|
1826
|
-
open_check: null,
|
|
1827
|
-
failed_turn: null,
|
|
1828
|
-
activity: 'starting over from the edited ticket',
|
|
1829
|
-
tried: withTried({
|
|
1830
|
-
tried: card.tried,
|
|
1831
|
-
line: 'Started over from the edited ticket',
|
|
1832
|
-
}),
|
|
1833
|
-
}),
|
|
1834
|
-
})
|
|
1835
|
-
case 'refused': {
|
|
1836
|
-
const card = state.tickets.find((entry) => entry.number === ticket)
|
|
1837
|
-
return addNeedsYou({
|
|
1838
|
-
state: updateTicket({
|
|
1839
|
-
state,
|
|
1840
|
-
number: ticket,
|
|
1841
|
-
update: (entry) => ({
|
|
1842
|
-
...entry,
|
|
1843
|
-
stage: 'stuck',
|
|
1844
|
-
role: null,
|
|
1845
|
-
activity: 'stuck',
|
|
1846
|
-
}),
|
|
1847
|
-
}),
|
|
1848
|
-
item: {
|
|
1849
|
-
key: ticketKey({ ticket }),
|
|
1850
|
-
ticket,
|
|
1851
|
-
subject: `Retry of #${ticket} was refused${card ? `: ${card.title}` : ''}`,
|
|
1852
|
-
reason: RETRY_REFUSED_TEXT,
|
|
1853
|
-
detail: clip({
|
|
1854
|
-
text: record.content.problems.join(' '),
|
|
1855
|
-
max: DETAIL_MAX,
|
|
1856
|
-
}),
|
|
1857
|
-
tried: card?.tried ?? [],
|
|
1858
|
-
replies: [`retry #${ticket}`, `skip #${ticket}`, 'stop'],
|
|
1859
|
-
since: record.time,
|
|
1860
|
-
},
|
|
1861
|
-
})
|
|
1862
|
-
}
|
|
160
|
+
if (isOneOf(TICKET_JOIN_KINDS, record)) {
|
|
161
|
+
return applyTicketJoinKind({ state, record })
|
|
1863
162
|
}
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
const replyReceived = ({
|
|
1867
|
-
state,
|
|
1868
|
-
record,
|
|
1869
|
-
}: {
|
|
1870
|
-
state: BoardState
|
|
1871
|
-
record: Extract<BoardRecord, { kind: 'reply_received' }>
|
|
1872
|
-
}): BoardState => {
|
|
1873
|
-
const { word } = record.content
|
|
1874
|
-
const ticket = record.content.ticket ?? record.ticket
|
|
1875
|
-
switch (word) {
|
|
1876
|
-
case 'retry':
|
|
1877
|
-
if (ticket !== null) {
|
|
1878
|
-
return resolveNeedsYou({
|
|
1879
|
-
state: updateTicket({
|
|
1880
|
-
state,
|
|
1881
|
-
number: ticket,
|
|
1882
|
-
update: (card) => ({
|
|
1883
|
-
...card,
|
|
1884
|
-
stage: 'building',
|
|
1885
|
-
activity: 'retrying',
|
|
1886
|
-
}),
|
|
1887
|
-
}),
|
|
1888
|
-
key: ticketKey({ ticket }),
|
|
1889
|
-
})
|
|
1890
|
-
}
|
|
1891
|
-
// A bare `retry` while the run is stuck on its budget adds one
|
|
1892
|
-
// more full budget; the final review is left as it is.
|
|
1893
|
-
if (state.needs_you.some((item) => item.key === RUN_KEY)) {
|
|
1894
|
-
return resolveNeedsYou({
|
|
1895
|
-
state: {
|
|
1896
|
-
...state,
|
|
1897
|
-
run_budget_tokens:
|
|
1898
|
-
state.run_budget_tokens + state.run_budget_each,
|
|
1899
|
-
},
|
|
1900
|
-
key: RUN_KEY,
|
|
1901
|
-
})
|
|
1902
|
-
}
|
|
1903
|
-
return resolveNeedsYou({
|
|
1904
|
-
state: updateFinal({
|
|
1905
|
-
state,
|
|
1906
|
-
update: (review) => ({ ...review, state: 'reviewing' }),
|
|
1907
|
-
}),
|
|
1908
|
-
key: FINAL_KEY,
|
|
1909
|
-
})
|
|
1910
|
-
case 'skip':
|
|
1911
|
-
return skipTicket({ state, ticket, because: null })
|
|
1912
|
-
case 'ship':
|
|
1913
|
-
return resolveNeedsYou({
|
|
1914
|
-
state: updateFinal({
|
|
1915
|
-
state,
|
|
1916
|
-
update: (review) => ({ ...review, state: 'passed' }),
|
|
1917
|
-
}),
|
|
1918
|
-
key: FINAL_KEY,
|
|
1919
|
-
})
|
|
1920
|
-
case 'stop':
|
|
1921
|
-
return resolveNeedsYou({ state, key: null })
|
|
163
|
+
if (isOneOf(NEEDS_YOU_KINDS, record)) {
|
|
164
|
+
return applyNeedsYouKind({ state, record })
|
|
1922
165
|
}
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
export const unfinishedBlockers = ({
|
|
1927
|
-
state,
|
|
1928
|
-
ticket,
|
|
1929
|
-
}: {
|
|
1930
|
-
state: BoardState
|
|
1931
|
-
ticket: TicketCard
|
|
1932
|
-
}): number[] =>
|
|
1933
|
-
ticket.blockers.filter((number) => {
|
|
1934
|
-
const blocker = state.tickets.find((card) => card.number === number)
|
|
1935
|
-
return blocker !== undefined && blocker.stage !== 'done'
|
|
1936
|
-
})
|
|
1937
|
-
|
|
1938
|
-
const isActive = ({ state }: { state: BoardState }): boolean =>
|
|
1939
|
-
state.tickets.some(
|
|
1940
|
-
(ticket) =>
|
|
1941
|
-
ticket.started &&
|
|
1942
|
-
(ticket.stage === 'building' || ticket.stage === 'reviewing')
|
|
1943
|
-
) ||
|
|
1944
|
-
state.final_review.state === 'reviewing' ||
|
|
1945
|
-
state.final_review.state === 'fixing'
|
|
1946
|
-
|
|
1947
|
-
const runStatus = ({ state }: { state: BoardState }): RunStatus => {
|
|
1948
|
-
const { phase, engine_ended, stopped } = state.run
|
|
1949
|
-
if (phase === 'done' || phase === 'refused' || phase === 'nothing_to_do')
|
|
1950
|
-
return phase
|
|
1951
|
-
if (stopped) return 'stopped'
|
|
1952
|
-
if (engine_ended && !engine_ended.ok) return 'ended_with_error'
|
|
1953
|
-
if (state.limit_wait) return 'limit_wait'
|
|
1954
|
-
if (state.usage_line_wait) return 'usage_line_wait'
|
|
1955
|
-
// Nothing new starts while the run is stuck on its budget.
|
|
1956
|
-
if (state.needs_you.some((item) => item.key === RUN_KEY)) return 'stuck'
|
|
1957
|
-
if (state.needs_you.length > 0 && !isActive({ state })) return 'stuck'
|
|
1958
|
-
return phase
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
/**
|
|
1962
|
-
* Recomputes what depends on the whole board: which unstarted tickets are
|
|
1963
|
-
* blocked, whether the final review is active, and the run's status.
|
|
1964
|
-
*/
|
|
1965
|
-
const settle = ({ state }: { state: BoardState }): BoardState => {
|
|
1966
|
-
const tickets = state.tickets.map((ticket): TicketCard => {
|
|
1967
|
-
if (ticket.started) return ticket
|
|
1968
|
-
if (ticket.stage !== 'blocked' && ticket.stage !== 'building')
|
|
1969
|
-
return ticket
|
|
1970
|
-
const waiting = unfinishedBlockers({ state, ticket })
|
|
1971
|
-
return waiting.length > 0
|
|
1972
|
-
? {
|
|
1973
|
-
...ticket,
|
|
1974
|
-
stage: 'blocked',
|
|
1975
|
-
activity: `waits on ${waiting.map((number) => `#${number}`).join(', ')}`,
|
|
1976
|
-
}
|
|
1977
|
-
: { ...ticket, stage: 'building', activity: 'queued' }
|
|
1978
|
-
})
|
|
1979
|
-
const settledTickets = { ...state, tickets }
|
|
1980
|
-
const active =
|
|
1981
|
-
tickets.length > 0 &&
|
|
1982
|
-
tickets.every(
|
|
1983
|
-
(ticket) => ticket.stage === 'done' || ticket.stage === 'skipped'
|
|
1984
|
-
)
|
|
1985
|
-
const withReview = {
|
|
1986
|
-
...settledTickets,
|
|
1987
|
-
final_review: { ...state.final_review, active },
|
|
166
|
+
if (isOneOf(STEP_KINDS, record)) return applyStepKind({ state, record })
|
|
167
|
+
if (isOneOf(FINAL_REVIEW_KINDS, record)) {
|
|
168
|
+
return applyFinalReviewKind({ state, record })
|
|
1988
169
|
}
|
|
1989
|
-
return {
|
|
1990
|
-
|
|
1991
|
-
|
|
170
|
+
if (isOneOf(USAGE_KINDS, record)) return applyUsageKind({ state, record })
|
|
171
|
+
if (isOneOf(COUNTER_KINDS, record)) {
|
|
172
|
+
return applyCounterKind({ state, record })
|
|
1992
173
|
}
|
|
174
|
+
record satisfies never
|
|
175
|
+
return state
|
|
1993
176
|
}
|