@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
package/engine/journal/replay.ts
CHANGED
|
@@ -1,755 +1,90 @@
|
|
|
1
|
-
import mapValues from 'lodash/mapValues'
|
|
2
|
-
import omit from 'lodash/omit'
|
|
3
|
-
import omitBy from 'lodash/omitBy'
|
|
4
1
|
import uniq from 'lodash/uniq'
|
|
5
2
|
|
|
6
|
-
import type {
|
|
7
|
-
AgentFailure,
|
|
8
|
-
CommitStage,
|
|
9
|
-
JournalRecord,
|
|
10
|
-
RunStuckReason,
|
|
11
|
-
StuckReason,
|
|
12
|
-
UsageLineWindow,
|
|
13
|
-
} from './journal-record'
|
|
14
|
-
import { crashesAfter, type CrashCounts } from './step-records'
|
|
15
|
-
|
|
16
|
-
import type { RejoinContext } from '../agents/role-prompts'
|
|
17
3
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
} from '
|
|
32
|
-
import
|
|
33
|
-
import
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
} from '
|
|
45
|
-
import {
|
|
46
|
-
import
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
export
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** One gate run, as journaled. */
|
|
93
|
-
export type ReplayedGates = { ok: boolean; checks: GateCheck[] }
|
|
94
|
-
|
|
95
|
-
/** The red check's verdict, plus the test run's (clipped) output. */
|
|
96
|
-
export type ReplayedRedCheck = RedCheckResult & { output: string }
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* The open review fix round: the blockers and should-fixes the latest ticket
|
|
100
|
-
* review sent back, and which fixers have answered. Test findings go to a
|
|
101
|
-
* fresh test-writer first, then code findings to the implementer.
|
|
102
|
-
*/
|
|
103
|
-
export type ReviewFix = {
|
|
104
|
-
/** 1 after the first review, 2 after the first re-review, ... */
|
|
105
|
-
round: number
|
|
106
|
-
findings: Finding[]
|
|
107
|
-
/** True once the test-writer answered, or when no finding is a test finding. */
|
|
108
|
-
tests_answered: boolean
|
|
109
|
-
/** True once the implementer answered, or when no finding is a code finding. */
|
|
110
|
-
code_answered: boolean
|
|
111
|
-
/** The fixers' answers, oldest first. */
|
|
112
|
-
responses: FindingResponse[]
|
|
113
|
-
/** A test the implementer sent back as bad while fixing, if any. */
|
|
114
|
-
bad_test: BadTest | null
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* A finding a fixer answered "won't fix" and the next reviewer let go: its
|
|
119
|
-
* ruling was `accepted`, or it gave none and did not list the finding again.
|
|
120
|
-
*/
|
|
121
|
-
export type DeclinedFinding = {
|
|
122
|
-
finding: Finding
|
|
123
|
-
/** The fixer's reason. */
|
|
124
|
-
reason: string
|
|
125
|
-
/** The reviewer's reason for accepting it, if it gave one. */
|
|
126
|
-
ruling: string
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* How far one ticket got, from its journal records. Each field holds the
|
|
131
|
-
* latest record of its kind; `null` means the step has not finished.
|
|
132
|
-
*
|
|
133
|
-
* The fix-loop counts are derived from record order, never written by hand,
|
|
134
|
-
* so a crashed run counts them the same way again: a test-writer result that
|
|
135
|
-
* comes after a red check answers a fix round (and the red check runs again),
|
|
136
|
-
* and an implementer result after the gates does the same for the gates.
|
|
137
|
-
*/
|
|
138
|
-
export type TicketProgress = {
|
|
139
|
-
worktree: ReplayedWorktree | null
|
|
140
|
-
/** The install in the new worktree; its `check` is `null` with no manifest. */
|
|
141
|
-
install: ReplayedInstall | null
|
|
142
|
-
/** The prepare command run before its baseline tests (`baseline_prepared`). */
|
|
143
|
-
prepare: GateCheck | null
|
|
144
|
-
/** Its own baseline test run, or the one it reused (`baseline_reused`). */
|
|
145
|
-
baseline: TestRun | null
|
|
146
|
-
/**
|
|
147
|
-
* The run-branch commit the baseline was taken at. It stays when a rebase
|
|
148
|
-
* moves the worktree, so the baseline is only lent to tickets from there.
|
|
149
|
-
*/
|
|
150
|
-
baseline_sha: string | null
|
|
151
|
-
test_writer: TestWriterResult | null
|
|
152
|
-
red_check: ReplayedRedCheck | null
|
|
153
|
-
/** Follow-ups the test-writer answered after a failed red check. */
|
|
154
|
-
red_fix_rounds: number
|
|
155
|
-
implementer: ImplementerResult | null
|
|
156
|
-
/** Follow-ups the implementer answered after failed gates. */
|
|
157
|
-
gate_fix_rounds: number
|
|
158
|
-
/** How many times an implementer sent a test back as bad. */
|
|
159
|
-
bad_test_bounces: number
|
|
160
|
-
/** The latest test sent back as bad, for the fresh test-writer's prompt. */
|
|
161
|
-
bad_test: BadTest | null
|
|
162
|
-
/** The session each role's latest turn ran in, for follow-ups. */
|
|
163
|
-
sessions: Partial<Record<AgentRole, string>>
|
|
164
|
-
/** Every assumption any agent on the ticket made, oldest first. */
|
|
165
|
-
assumptions: string[]
|
|
166
|
-
/** The latest ticket review's result. */
|
|
167
|
-
review: TicketReviewResult | null
|
|
168
|
-
/** Ticket reviews finished on this ticket. */
|
|
169
|
-
review_rounds: number
|
|
170
|
-
/** The commit the latest review looked at. */
|
|
171
|
-
reviewed_sha: string | null
|
|
172
|
-
/** The open review fix round; `null` before a review or once approved. */
|
|
173
|
-
review_fix: ReviewFix | null
|
|
174
|
-
/** Every nit reviewers reported, oldest first, one per id. */
|
|
175
|
-
nits: Finding[]
|
|
176
|
-
/** Findings declined through "won't fix", oldest first. */
|
|
177
|
-
declined: DeclinedFinding[]
|
|
178
|
-
/**
|
|
179
|
-
* The seq of the review that finally approved the ticket (no blocker or
|
|
180
|
-
* should-fix left), `null` until one does. Approved tickets join the run
|
|
181
|
-
* branch in this order, one at a time.
|
|
182
|
-
*/
|
|
183
|
-
approved_seq: number | null
|
|
184
|
-
/** How many times the ticket was sent back onto the run branch. */
|
|
185
|
-
rejoins: number
|
|
186
|
-
/** The latest time it was sent back, `null` if it never was. */
|
|
187
|
-
rejoin: ReplayedRejoin | null
|
|
188
|
-
/**
|
|
189
|
-
* The latest failed agent turn, if no agent finished after it: its role,
|
|
190
|
-
* error, how it failed, and the session it ran in (for a follow-up).
|
|
191
|
-
*/
|
|
192
|
-
agent_failure: {
|
|
193
|
-
role: AgentRole
|
|
194
|
-
error: string
|
|
195
|
-
failure: AgentFailure
|
|
196
|
-
session_id: string | null
|
|
197
|
-
} | null
|
|
198
|
-
/**
|
|
199
|
-
* Failed tries (agent, result, and guard failures) each role has used on
|
|
200
|
-
* this ticket, in all. Engine failures never count here.
|
|
201
|
-
*/
|
|
202
|
-
failed_tries: Partial<Record<AgentRole, number>>
|
|
203
|
-
/** Engine failures in a row; any other turn's end resets it. */
|
|
204
|
-
engine_failures: number
|
|
205
|
-
leftovers: Record<CommitStage, LeftoverHit[] | null>
|
|
206
|
-
commits: Record<CommitStage, string | null>
|
|
207
|
-
/** The files each stage's latest commit changed. */
|
|
208
|
-
commit_files: Record<CommitStage, string[]>
|
|
209
|
-
gates: ReplayedGates | null
|
|
210
|
-
joined: { ok: true; shas: string[] } | { ok: false; error: string } | null
|
|
211
|
-
join_gates: ReplayedGates | null
|
|
212
|
-
pushed: string | null
|
|
213
|
-
stuck: ReplayedStuck | null
|
|
214
|
-
/** The spec issue's comment that told the owner, once posted. */
|
|
215
|
-
stuck_report: { comment_id: number } | null
|
|
216
|
-
/** The owner's reply to this stuck ticket, until the engine acts on it. */
|
|
217
|
-
reply: { word: 'retry' | 'skip'; comment_id: number } | null
|
|
218
|
-
/** Left out of the run: skipped by reply, or waits on a skipped ticket. */
|
|
219
|
-
skipped: { because: number | null } | null
|
|
220
|
-
/**
|
|
221
|
-
* Its work was already on the base branch, done by these commits (#484):
|
|
222
|
-
* the ticket is done without a join, like a pushed one.
|
|
223
|
-
*/
|
|
224
|
-
already_done: { shas: string[] } | null
|
|
225
|
-
/** The engine closed it (an already-done ticket in a run with no PR). */
|
|
226
|
-
closed: boolean
|
|
227
|
-
/**
|
|
228
|
-
* Why the ticket got stuck before its latest `retry` resumed it; the next
|
|
229
|
-
* fresh agent is told. Cleared once an agent starts.
|
|
230
|
-
*/
|
|
231
|
-
retried: ReplayedStuck | null
|
|
232
|
-
/** A test setup file change an agent asked for; the ticket is stuck on it. */
|
|
233
|
-
setup_change: { role: AgentRole; file: string; reason: string } | null
|
|
234
|
-
/**
|
|
235
|
-
* The role whose turn a crash cut off (`run_resumed`), so the fresh
|
|
236
|
-
* agent that takes it again is told. Cleared once an agent starts.
|
|
237
|
-
*/
|
|
238
|
-
crashed_turn: AgentRole | null
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** Why a ticket is stuck, as journaled. */
|
|
242
|
-
export type ReplayedStuck = { reason: StuckReason; detail: string }
|
|
243
|
-
|
|
244
|
-
/** A comment the engine read on the spec issue. */
|
|
245
|
-
export type ReplayedComment = {
|
|
246
|
-
comment_id: number
|
|
247
|
-
author: string
|
|
248
|
-
body: string
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/** A final review finding: its id namespaced as `<lens>-<id>`, and its lens. */
|
|
252
|
-
export type FinalFinding = Finding & { lens: LensName }
|
|
253
|
-
|
|
254
|
-
/** A finding declined in the final review, with the lens that let it go. */
|
|
255
|
-
export type FinalDeclined = DeclinedFinding & { lens: LensName }
|
|
256
|
-
|
|
257
|
-
/** A rule file for the rules lens, as the engine read it (`null`: unreadable). */
|
|
258
|
-
export type RuleFile = { path: string; text: string | null }
|
|
259
|
-
|
|
260
|
-
/** A failed agent turn no agent of that role finished after. */
|
|
261
|
-
export type ReplayedAgentFailure = {
|
|
262
|
-
role: AgentRole
|
|
263
|
-
error: string
|
|
264
|
-
failure: AgentFailure
|
|
265
|
-
session_id: string | null
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* The open final review fix round: the blocking findings of every lens due
|
|
270
|
-
* in the round, and which fixers answered. Like a ticket's `ReviewFix`.
|
|
271
|
-
*/
|
|
272
|
-
export type FinalReviewFix = Omit<ReviewFix, 'findings'> & {
|
|
273
|
-
findings: FinalFinding[]
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Where the final review stands, from its records: its round, the lenses
|
|
278
|
-
* due, each lens's result this round, the open fix round and its fixers,
|
|
279
|
-
* gates, commit, and push, and what goes in the PR. Ticket-less agent,
|
|
280
|
-
* gate, scan, commit, and push records belong here once
|
|
281
|
-
* `final_review_started` is journaled.
|
|
282
|
-
*/
|
|
283
|
-
export type FinalReviewState = {
|
|
284
|
-
/** 0 before the final review starts, then 1, 2, ... */
|
|
285
|
-
round: number
|
|
286
|
-
from_sha: string | null
|
|
287
|
-
head_sha: string | null
|
|
288
|
-
/** The files `git diff <from_sha>..<head_sha>` changes. */
|
|
289
|
-
files: string[]
|
|
290
|
-
/** The lenses due this round. */
|
|
291
|
-
lenses_due: LensName[]
|
|
292
|
-
rules: RuleFile[]
|
|
293
|
-
/** Each due lens's result this round, finding ids namespaced. */
|
|
294
|
-
results: Partial<Record<LensName, LensReviewResult>>
|
|
295
|
-
/** The open fix round; `null` before the first review settles or once clean. */
|
|
296
|
-
fix: FinalReviewFix | null
|
|
297
|
-
/** `final_review_fixing` is journaled for the open fix round. */
|
|
298
|
-
fixing_started: boolean
|
|
299
|
-
/** The session of each role's latest turn, for follow-ups. */
|
|
300
|
-
sessions: Partial<Record<AgentRole, string>>
|
|
301
|
-
/** Each role's failed turn, if no agent of that role finished after it. */
|
|
302
|
-
agent_failures: Partial<Record<AgentRole, ReplayedAgentFailure>>
|
|
303
|
-
/** Failed tries (agent, result, guard) per role, over the whole final review. */
|
|
304
|
-
failed_tries: Partial<Record<AgentRole, number>>
|
|
305
|
-
/** Engine failures in a row, per role. */
|
|
306
|
-
engine_failures: Partial<Record<AgentRole, number>>
|
|
307
|
-
/** The open fix round's gates, and its gate fix follow-ups. */
|
|
308
|
-
gates: ReplayedGates | null
|
|
309
|
-
gate_fix_rounds: number
|
|
310
|
-
/** The open fix round's leftover scan and commit. */
|
|
311
|
-
leftovers: LeftoverHit[] | null
|
|
312
|
-
commit: { sha: string; files: string[] } | null
|
|
313
|
-
/** The open fix round's push. */
|
|
314
|
-
pushed: string | null
|
|
315
|
-
/** Every nit the lenses reported, one per id. */
|
|
316
|
-
nits: FinalFinding[]
|
|
317
|
-
declined: FinalDeclined[]
|
|
318
|
-
/** Every assumption a final review agent made, oldest first. */
|
|
319
|
-
assumptions: string[]
|
|
320
|
-
passed: boolean
|
|
321
|
-
stuck: { reason: StuckReason; detail: string } | null
|
|
322
|
-
shipped: boolean
|
|
323
|
-
/** The fixer whose turn a crash cut off; cleared once a fixer starts. */
|
|
324
|
-
crashed_turn: AgentRole | null
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/** The final review before it starts. */
|
|
328
|
-
export const EMPTY_FINAL_REVIEW: FinalReviewState = {
|
|
329
|
-
round: 0,
|
|
330
|
-
from_sha: null,
|
|
331
|
-
head_sha: null,
|
|
332
|
-
files: [],
|
|
333
|
-
lenses_due: [],
|
|
334
|
-
rules: [],
|
|
335
|
-
results: {},
|
|
336
|
-
fix: null,
|
|
337
|
-
fixing_started: false,
|
|
338
|
-
sessions: {},
|
|
339
|
-
agent_failures: {},
|
|
340
|
-
failed_tries: {},
|
|
341
|
-
engine_failures: {},
|
|
342
|
-
gates: null,
|
|
343
|
-
gate_fix_rounds: 0,
|
|
344
|
-
leftovers: null,
|
|
345
|
-
commit: null,
|
|
346
|
-
pushed: null,
|
|
347
|
-
nits: [],
|
|
348
|
-
declined: [],
|
|
349
|
-
assumptions: [],
|
|
350
|
-
passed: false,
|
|
351
|
-
stuck: null,
|
|
352
|
-
shipped: false,
|
|
353
|
-
crashed_turn: null,
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/** The engine's install in a new worktree: `null` check means nothing to install. */
|
|
357
|
-
export type ReplayedInstall = { check: GateCheck | null }
|
|
358
|
-
|
|
359
|
-
/** The run's pull request, once opened. */
|
|
360
|
-
export type ReplayedPullRequest = { number: number; url: string }
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Where the run stands with the plan, from agents' rate-limit readings and
|
|
364
|
-
* the limit-wait and stop records.
|
|
365
|
-
*/
|
|
366
|
-
export type PlanState = {
|
|
367
|
-
/**
|
|
368
|
-
* The latest rejected rate limit no limit wait has started for yet: the
|
|
369
|
-
* window, its reset (seconds since the epoch), the agent that hit it, and
|
|
370
|
-
* when the session was journaled.
|
|
371
|
-
*/
|
|
372
|
-
hit: {
|
|
373
|
-
rate_limit_type: string | null
|
|
374
|
-
resets_at: number | null
|
|
375
|
-
ticket: number | null
|
|
376
|
-
role: AgentRole | null
|
|
377
|
-
time: string
|
|
378
|
-
} | null
|
|
379
|
-
/** The limit wait under way: when it ends, and what it is for. */
|
|
380
|
-
wait: {
|
|
381
|
-
until: string
|
|
382
|
-
resets_at: string | null
|
|
383
|
-
rate_limit_type: string | null
|
|
384
|
-
} | null
|
|
385
|
-
/** Every limit wait's reset so far, oldest first; the spec hears of each new one once. */
|
|
386
|
-
resets_announced: (string | null)[]
|
|
387
|
-
/** The first sign of per-token billing in any session, if any. */
|
|
388
|
-
billing: { reason: string } | null
|
|
389
|
-
/** Set once a billing stop is journaled. It sticks. */
|
|
390
|
-
billing_stopped: { reason: string } | null
|
|
391
|
-
/** The usage-line wait under way: its window, line, reading, and end. */
|
|
392
|
-
usage_wait: {
|
|
393
|
-
window: UsageLineWindow
|
|
394
|
-
line: number
|
|
395
|
-
percent: number
|
|
396
|
-
until: string
|
|
397
|
-
} | null
|
|
398
|
-
/**
|
|
399
|
-
* The latest time the run is known to have waited until: the `until` of
|
|
400
|
-
* a limit wait that ended, or of a usage-line wait that ended at its
|
|
401
|
-
* reset. A reading whose window resets by then is out of date.
|
|
402
|
-
*/
|
|
403
|
-
waited_until: string | null
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* The learner at the end of a run (#370): its answer, or its failed tries,
|
|
407
|
-
* or that the engine gave up on it. Learner records have `ticket: null` and
|
|
408
|
-
* role `learner`; they never count as the final review's.
|
|
409
|
-
*/
|
|
410
|
-
export type LearnerState = {
|
|
411
|
-
/** Its result, once it finished. */
|
|
412
|
-
result: LearnerResult | null
|
|
413
|
-
/** The seq of its `agent_finished`, for the saves' op ids. */
|
|
414
|
-
result_seq: number | null
|
|
415
|
-
/** Its latest failed turn, if none finished after it. */
|
|
416
|
-
agent_failure: ReplayedAgentFailure | null
|
|
417
|
-
/** Failed tries (agent, result, guard), in all. */
|
|
418
|
-
failed_tries: number
|
|
419
|
-
/** Engine failures in a row. */
|
|
420
|
-
engine_failures: number
|
|
421
|
-
/** Why the engine gave up on it (`learning_skipped`), if it did. */
|
|
422
|
-
skipped: string | null
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/** Where memory stands in a run (#370), from its records. */
|
|
426
|
-
export type MemoryState = {
|
|
427
|
-
/** Memory is on for this run (`run_started.memory`). */
|
|
428
|
-
on: boolean
|
|
429
|
-
/** The project's vault; `null` searches only `default`. */
|
|
430
|
-
project_vault: string | null
|
|
431
|
-
/** Each recall point's memories, by its key, once searched. */
|
|
432
|
-
recalls: Record<string, { point: RecallPoint; memories: RecalledMemory[] }>
|
|
433
|
-
/** Every memory shown in the run, oldest first; may repeat. */
|
|
434
|
-
shown: RecalledMemory[]
|
|
435
|
-
learner: LearnerState
|
|
436
|
-
/** The learner's memories as saved, and the feedback sent. */
|
|
437
|
-
saved: { saves: MemorySave[]; feedback: MemoryFeedback[] } | null
|
|
438
|
-
/** The new memories went on the spec issue (a run with no PR). */
|
|
439
|
-
reported: boolean
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/** A run with memory off, before any memory record. */
|
|
443
|
-
export const EMPTY_MEMORY: MemoryState = {
|
|
444
|
-
on: false,
|
|
445
|
-
project_vault: null,
|
|
446
|
-
recalls: {},
|
|
447
|
-
shown: [],
|
|
448
|
-
learner: {
|
|
449
|
-
result: null,
|
|
450
|
-
result_seq: null,
|
|
451
|
-
agent_failure: null,
|
|
452
|
-
failed_tries: 0,
|
|
453
|
-
engine_failures: 0,
|
|
454
|
-
skipped: null,
|
|
455
|
-
},
|
|
456
|
-
saved: null,
|
|
457
|
-
reported: false,
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/** One run note, with the agent that wrote it. */
|
|
461
|
-
export type ReplayedRunNote = { ticket: number; role: AgentRole; note: string }
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* The run budget (#435): whether the run is stuck on it and told, and how
|
|
465
|
-
* many more full budgets the owner's `retry` replies added.
|
|
466
|
-
*/
|
|
467
|
-
export type RunBudgetState = {
|
|
468
|
-
/** The run is stuck, until the owner's `retry`. */
|
|
469
|
-
stuck: { reason: RunStuckReason; detail: string } | null
|
|
470
|
-
/** The spec issue's comment that told the owner the run is stuck. */
|
|
471
|
-
report: { comment_id: number } | null
|
|
472
|
-
/** Full budgets added by `retry`: the run may use `1 + retries` budgets. */
|
|
473
|
-
retries: number
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/** A run's state, rebuilt only from its journal. There is no status file. */
|
|
477
|
-
export type RunState = {
|
|
478
|
-
phase: RunPhase
|
|
479
|
-
spec_number: number | null
|
|
480
|
-
config: EngineConfig | null
|
|
481
|
-
base_branch: string | null
|
|
482
|
-
intake: IntakeRead | null
|
|
483
|
-
problems: IntakeProblem[] | null
|
|
484
|
-
snapshot: ReplayedSnapshot | null
|
|
485
|
-
run_branch: ReplayedWorktree | null
|
|
486
|
-
/** The install in the run branch's checkout, once it ran. */
|
|
487
|
-
run_branch_install: ReplayedInstall | null
|
|
488
|
-
tickets: Record<number, TicketProgress>
|
|
489
|
-
/**
|
|
490
|
-
* The repo had a changesets config when the run branch was made, so a
|
|
491
|
-
* changeset is written before the PR (#461).
|
|
492
|
-
*/
|
|
493
|
-
changesets: boolean
|
|
494
|
-
/** The run's changeset, once written and pushed. */
|
|
495
|
-
changeset: { path: string | null; sha: string } | null
|
|
496
|
-
pull_request: ReplayedPullRequest | null
|
|
497
|
-
plan: PlanState
|
|
498
|
-
/**
|
|
499
|
-
* The seq of each ticket's latest usage record (a retried ticket that
|
|
500
|
-
* finishes again gets another), and whether the run's is recorded.
|
|
501
|
-
*/
|
|
502
|
-
usage_recorded: { tickets: Record<number, number>; run: boolean }
|
|
503
|
-
/** The worktrees the engine removed at the end of the run. */
|
|
504
|
-
removed_worktrees: string[]
|
|
505
|
-
/**
|
|
506
|
-
* Every run note from every finished agent in the run, oldest first
|
|
507
|
-
* (journal order), word for word.
|
|
508
|
-
*/
|
|
509
|
-
run_notes: ReplayedRunNote[]
|
|
510
|
-
/** The latest gates on the run branch: after a join, or a final review fix. */
|
|
511
|
-
run_branch_gates: ReplayedGates | null
|
|
512
|
-
/**
|
|
513
|
-
* Every untracked path a prepare run made in any of the run's checkouts
|
|
514
|
-
* (`prepare_made`), in journal order. Never an agent's work: the engine
|
|
515
|
-
* leaves them out of commits, the leftover scan, and role rules.
|
|
516
|
-
*/
|
|
517
|
-
prepare_made: string[]
|
|
518
|
-
final_review: FinalReviewState
|
|
519
|
-
/** Comments read on the spec issue while waiting for replies, oldest first. */
|
|
520
|
-
comments: ReplayedComment[]
|
|
521
|
-
/** Comments already taken as a reply, or sent back. */
|
|
522
|
-
handled_comments: number[]
|
|
523
|
-
/** Comments the engine itself posted on the spec issue. */
|
|
524
|
-
engine_comments: number[]
|
|
525
|
-
/** The owner replied `stop`: nothing new starts, and the run ends without a PR. */
|
|
526
|
-
stop: { comment_id: number } | null
|
|
527
|
-
/** The spec issue's comment that told the owner the final review is stuck. */
|
|
528
|
-
final_stuck_report: { comment_id: number } | null
|
|
529
|
-
/** The owner's reply to the stuck final review, until the engine acts on it. */
|
|
530
|
-
final_reply: { word: 'retry' | 'ship'; comment_id: number } | null
|
|
531
|
-
/** The run's budget of tokens: stuck on it, and the budgets added (#435). */
|
|
532
|
-
run_budget: RunBudgetState
|
|
533
|
-
/** Memory's recalls, the learner, and the saves (#370). */
|
|
534
|
-
memory: MemoryState
|
|
535
|
-
/**
|
|
536
|
-
* Crashes in a row per scheduler key (`run_resumed`), until a step under
|
|
537
|
-
* the key ends. A ticket's `retry`, or the final review's, clears its own.
|
|
538
|
-
*/
|
|
539
|
-
crashes: CrashCounts
|
|
540
|
-
/** Set once a stop for crashes is journaled. It sticks. */
|
|
541
|
-
crash_stopped: { reason: string } | null
|
|
542
|
-
last_seq: number
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
/** A ticket nothing has happened to yet. */
|
|
546
|
-
export const EMPTY_TICKET_PROGRESS: TicketProgress = {
|
|
547
|
-
worktree: null,
|
|
548
|
-
install: null,
|
|
549
|
-
prepare: null,
|
|
550
|
-
baseline: null,
|
|
551
|
-
baseline_sha: null,
|
|
552
|
-
test_writer: null,
|
|
553
|
-
red_check: null,
|
|
554
|
-
red_fix_rounds: 0,
|
|
555
|
-
implementer: null,
|
|
556
|
-
gate_fix_rounds: 0,
|
|
557
|
-
bad_test_bounces: 0,
|
|
558
|
-
bad_test: null,
|
|
559
|
-
sessions: {},
|
|
560
|
-
assumptions: [],
|
|
561
|
-
review: null,
|
|
562
|
-
review_rounds: 0,
|
|
563
|
-
reviewed_sha: null,
|
|
564
|
-
review_fix: null,
|
|
565
|
-
nits: [],
|
|
566
|
-
declined: [],
|
|
567
|
-
approved_seq: null,
|
|
568
|
-
rejoins: 0,
|
|
569
|
-
rejoin: null,
|
|
570
|
-
agent_failure: null,
|
|
571
|
-
failed_tries: {},
|
|
572
|
-
engine_failures: 0,
|
|
573
|
-
leftovers: { red: null, green: null, fix: null },
|
|
574
|
-
commits: { red: null, green: null, fix: null },
|
|
575
|
-
commit_files: { red: [], green: [], fix: [] },
|
|
576
|
-
gates: null,
|
|
577
|
-
joined: null,
|
|
578
|
-
join_gates: null,
|
|
579
|
-
pushed: null,
|
|
580
|
-
stuck: null,
|
|
581
|
-
stuck_report: null,
|
|
582
|
-
reply: null,
|
|
583
|
-
skipped: null,
|
|
584
|
-
already_done: null,
|
|
585
|
-
closed: false,
|
|
586
|
-
retried: null,
|
|
587
|
-
setup_change: null,
|
|
588
|
-
crashed_turn: null,
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const EMPTY_STATE: RunState = {
|
|
592
|
-
phase: 'new',
|
|
593
|
-
spec_number: null,
|
|
594
|
-
config: null,
|
|
595
|
-
base_branch: null,
|
|
596
|
-
intake: null,
|
|
597
|
-
problems: null,
|
|
598
|
-
snapshot: null,
|
|
599
|
-
run_branch: null,
|
|
600
|
-
run_branch_install: null,
|
|
601
|
-
tickets: {},
|
|
602
|
-
changesets: false,
|
|
603
|
-
changeset: null,
|
|
604
|
-
pull_request: null,
|
|
605
|
-
plan: {
|
|
606
|
-
hit: null,
|
|
607
|
-
wait: null,
|
|
608
|
-
resets_announced: [],
|
|
609
|
-
billing: null,
|
|
610
|
-
billing_stopped: null,
|
|
611
|
-
usage_wait: null,
|
|
612
|
-
waited_until: null,
|
|
613
|
-
},
|
|
614
|
-
usage_recorded: { tickets: {}, run: false },
|
|
615
|
-
removed_worktrees: [],
|
|
616
|
-
run_notes: [],
|
|
617
|
-
run_branch_gates: null,
|
|
618
|
-
prepare_made: [],
|
|
619
|
-
final_review: EMPTY_FINAL_REVIEW,
|
|
620
|
-
comments: [],
|
|
621
|
-
handled_comments: [],
|
|
622
|
-
engine_comments: [],
|
|
623
|
-
stop: null,
|
|
624
|
-
final_stuck_report: null,
|
|
625
|
-
final_reply: null,
|
|
626
|
-
run_budget: { stuck: null, report: null, retries: 0 },
|
|
627
|
-
memory: EMPTY_MEMORY,
|
|
628
|
-
crashes: {},
|
|
629
|
-
crash_stopped: null,
|
|
630
|
-
last_seq: 0,
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
const snapshotPhase = ({
|
|
634
|
-
snapshot,
|
|
635
|
-
}: {
|
|
636
|
-
snapshot: ReplayedSnapshot
|
|
637
|
-
}): RunPhase =>
|
|
638
|
-
snapshot.ticket_order.every((number) => number in snapshot.tickets)
|
|
639
|
-
? 'intake_passed'
|
|
640
|
-
: 'snapshotting'
|
|
641
|
-
|
|
642
|
-
type PlanRecord = Extract<
|
|
643
|
-
JournalRecord,
|
|
644
|
-
{
|
|
645
|
-
kind:
|
|
646
|
-
| 'agent_session'
|
|
647
|
-
| 'run_stopped'
|
|
648
|
-
| 'limit_wait_started'
|
|
649
|
-
| 'limit_wait_ended'
|
|
650
|
-
| 'usage_line_wait_started'
|
|
651
|
-
| 'usage_line_wait_ended'
|
|
652
|
-
}
|
|
653
|
-
>
|
|
654
|
-
|
|
655
|
-
/** The later of two ISO times; the first may be missing. */
|
|
656
|
-
const laterOf = (a: string | null, b: string): string =>
|
|
657
|
-
a !== null && Date.parse(a) >= Date.parse(b) ? a : b
|
|
658
|
-
|
|
659
|
-
/** The plan state after one record. */
|
|
660
|
-
const planAfter = ({
|
|
661
|
-
plan,
|
|
662
|
-
record,
|
|
663
|
-
}: {
|
|
664
|
-
plan: PlanState
|
|
665
|
-
record: PlanRecord
|
|
666
|
-
}): PlanState => {
|
|
667
|
-
switch (record.kind) {
|
|
668
|
-
case 'agent_session': {
|
|
669
|
-
const signal = sessionSignal({ session: record.content.session })
|
|
670
|
-
if (signal.kind === 'billing') {
|
|
671
|
-
return {
|
|
672
|
-
...plan,
|
|
673
|
-
billing: plan.billing ?? { reason: signal.reason },
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
if (signal.kind === 'ok') return plan
|
|
677
|
-
return {
|
|
678
|
-
...plan,
|
|
679
|
-
hit: {
|
|
680
|
-
rate_limit_type: signal.rate_limit_type,
|
|
681
|
-
resets_at: signal.resets_at,
|
|
682
|
-
ticket: record.ticket,
|
|
683
|
-
role: record.content.role,
|
|
684
|
-
time: record.time,
|
|
685
|
-
},
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
case 'run_stopped':
|
|
689
|
-
return record.content.billing
|
|
690
|
-
? {
|
|
691
|
-
...plan,
|
|
692
|
-
billing: plan.billing ?? {
|
|
693
|
-
reason: record.content.reason,
|
|
694
|
-
},
|
|
695
|
-
billing_stopped: { reason: record.content.reason },
|
|
696
|
-
}
|
|
697
|
-
: plan
|
|
698
|
-
case 'limit_wait_started': {
|
|
699
|
-
const { until, resets_at, rate_limit_type } = record.content
|
|
700
|
-
return {
|
|
701
|
-
...plan,
|
|
702
|
-
hit: null,
|
|
703
|
-
wait: { until, resets_at, rate_limit_type },
|
|
704
|
-
resets_announced: [...plan.resets_announced, resets_at],
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
case 'limit_wait_ended':
|
|
708
|
-
return {
|
|
709
|
-
...plan,
|
|
710
|
-
wait: null,
|
|
711
|
-
waited_until: laterOf(plan.waited_until, record.content.until),
|
|
712
|
-
}
|
|
713
|
-
case 'usage_line_wait_started': {
|
|
714
|
-
const { window, line, percent, until } = record.content
|
|
715
|
-
return { ...plan, usage_wait: { window, line, percent, until } }
|
|
716
|
-
}
|
|
717
|
-
case 'usage_line_wait_ended':
|
|
718
|
-
return {
|
|
719
|
-
...plan,
|
|
720
|
-
usage_wait: null,
|
|
721
|
-
waited_until:
|
|
722
|
-
record.content.reason === 'reset'
|
|
723
|
-
? laterOf(plan.waited_until, record.content.until)
|
|
724
|
-
: plan.waited_until,
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
4
|
+
closedSessionsAfter,
|
|
5
|
+
goneSessionOf,
|
|
6
|
+
resumedAfter,
|
|
7
|
+
undoneJoinsAfter,
|
|
8
|
+
} from './cross-ticket-replay'
|
|
9
|
+
import {
|
|
10
|
+
finalRecordOf,
|
|
11
|
+
finalRepliesAfter,
|
|
12
|
+
finalReviewAfter,
|
|
13
|
+
omitFinalKeys,
|
|
14
|
+
resumedFinalReview,
|
|
15
|
+
} from './final-review-replay'
|
|
16
|
+
import type { JournalRecord } from './journal-record'
|
|
17
|
+
import { EMPTY_MEMORY, learnerAfter, learnerRecordOf } from './memory-replay'
|
|
18
|
+
import { planAfter } from './plan-replay'
|
|
19
|
+
import {
|
|
20
|
+
replyReceivedAfter,
|
|
21
|
+
stuckReportedAfter,
|
|
22
|
+
ticketRetriedAfter,
|
|
23
|
+
} from './reply-replay'
|
|
24
|
+
import {
|
|
25
|
+
EMPTY_STATE,
|
|
26
|
+
engineCommentsAfter,
|
|
27
|
+
notesAfter,
|
|
28
|
+
snapshotPhase,
|
|
29
|
+
type RunState,
|
|
30
|
+
} from './run-state'
|
|
31
|
+
import { sharedFailuresAfter } from './shared-failure-replay'
|
|
32
|
+
import { crashesAfter } from './step-records'
|
|
33
|
+
import { applyTicketRecord, reusedBaselineAfter } from './ticket-replay'
|
|
34
|
+
|
|
35
|
+
import { withConfigChanges } from '../config/config-reload'
|
|
36
|
+
|
|
37
|
+
export type {
|
|
38
|
+
OldTestFailure,
|
|
39
|
+
RunBudgetState,
|
|
40
|
+
ReplayedComment,
|
|
41
|
+
ReplayedPullRequest,
|
|
42
|
+
ReplayedRunNote,
|
|
43
|
+
ReplayedSnapshot,
|
|
44
|
+
RunPhase,
|
|
45
|
+
RunState,
|
|
46
|
+
} from './run-state'
|
|
47
|
+
export type {
|
|
48
|
+
DeclinedFinding,
|
|
49
|
+
EnvironmentRerun,
|
|
50
|
+
ReplayedGates,
|
|
51
|
+
ReplayedInstall,
|
|
52
|
+
ReplayedRedCheck,
|
|
53
|
+
ReplayedRejoin,
|
|
54
|
+
ReplayedStuck,
|
|
55
|
+
ReplayedWorktree,
|
|
56
|
+
ReviewFix,
|
|
57
|
+
TestUpdate,
|
|
58
|
+
TicketProgress,
|
|
59
|
+
WriterRole,
|
|
60
|
+
} from './ticket-progress'
|
|
61
|
+
export { EMPTY_TICKET_PROGRESS } from './ticket-progress'
|
|
62
|
+
export type {
|
|
63
|
+
FinalDeclined,
|
|
64
|
+
FinalFinding,
|
|
65
|
+
FinalReviewFix,
|
|
66
|
+
FinalReviewState,
|
|
67
|
+
ReplayedAgentFailure,
|
|
68
|
+
RuleFile,
|
|
69
|
+
} from './final-review-state'
|
|
70
|
+
export { EMPTY_FINAL_REVIEW } from './final-review-state'
|
|
71
|
+
export type { PlanState } from './plan-replay'
|
|
72
|
+
export type { LearnerState, MemoryState } from './memory-replay'
|
|
73
|
+
export { EMPTY_MEMORY } from './memory-replay'
|
|
74
|
+
export { namespacedId } from './final-review-round'
|
|
75
|
+
export { isFinalKey } from './final-review-replay'
|
|
76
|
+
export { handoffRole } from './agent-turn-replay'
|
|
728
77
|
|
|
729
|
-
|
|
730
|
-
const notesAfter = ({
|
|
78
|
+
const applyRecord = ({
|
|
731
79
|
state,
|
|
732
80
|
record,
|
|
733
81
|
}: {
|
|
734
82
|
state: RunState
|
|
735
|
-
record:
|
|
736
|
-
}):
|
|
737
|
-
|
|
738
|
-
// Reviewers (the ticket reviewer and the lenses) leave no run notes.
|
|
739
|
-
if (ticket === null || !('run_notes' in content.result)) {
|
|
740
|
-
return state.run_notes
|
|
741
|
-
}
|
|
742
|
-
return [
|
|
743
|
-
...state.run_notes,
|
|
744
|
-
...content.result.run_notes.map((note) => ({
|
|
745
|
-
ticket,
|
|
746
|
-
role: content.role,
|
|
747
|
-
note,
|
|
748
|
-
})),
|
|
749
|
-
]
|
|
750
|
-
}
|
|
83
|
+
record: JournalRecord
|
|
84
|
+
}): RunState =>
|
|
85
|
+
closedSessionsAfter({ state: applyKind({ state, record }), record })
|
|
751
86
|
|
|
752
|
-
const
|
|
87
|
+
const applyKind = ({
|
|
753
88
|
state,
|
|
754
89
|
record,
|
|
755
90
|
}: {
|
|
@@ -761,6 +96,10 @@ const applyRecord = ({
|
|
|
761
96
|
last_seq: record.seq,
|
|
762
97
|
crashes: crashesAfter({ crashes: state.crashes, record }),
|
|
763
98
|
}
|
|
99
|
+
// A follow-up found its session gone (#521): only the session is
|
|
100
|
+
// forgotten (see `closedSessionsAfter`), so its fix round goes to a
|
|
101
|
+
// fresh agent, and no engine failure is counted.
|
|
102
|
+
if (goneSessionOf(record) !== null) return base
|
|
764
103
|
const next =
|
|
765
104
|
record.kind === 'gates_run' && record.content.target === 'run_branch'
|
|
766
105
|
? {
|
|
@@ -877,6 +216,7 @@ const applyRecord = ({
|
|
|
877
216
|
ticket_order: record.content.ticket_order,
|
|
878
217
|
closed_tickets: record.content.closed_tickets,
|
|
879
218
|
tickets: {},
|
|
219
|
+
left_out: record.content.left_out,
|
|
880
220
|
}
|
|
881
221
|
return { ...next, snapshot, phase: snapshotPhase({ snapshot }) }
|
|
882
222
|
}
|
|
@@ -933,10 +273,12 @@ const applyRecord = ({
|
|
|
933
273
|
url: record.content.url,
|
|
934
274
|
},
|
|
935
275
|
}
|
|
936
|
-
//
|
|
276
|
+
// The decision model's shadow-mode records change nothing: the
|
|
277
|
+
// engine ignores them.
|
|
937
278
|
case 'jev_asked':
|
|
938
279
|
case 'jev_answered':
|
|
939
280
|
case 'jev_failed':
|
|
281
|
+
case 'decision_model_off':
|
|
940
282
|
return next
|
|
941
283
|
// A session's readings and the stops and limit waits change no
|
|
942
284
|
// ticket: the step they cut off is picked up again afterwards.
|
|
@@ -946,6 +288,21 @@ const applyRecord = ({
|
|
|
946
288
|
case 'usage_line_wait_started':
|
|
947
289
|
case 'usage_line_wait_ended':
|
|
948
290
|
return { ...next, plan: planAfter({ plan: state.plan, record }) }
|
|
291
|
+
case 'repo_wait_started':
|
|
292
|
+
return {
|
|
293
|
+
...next,
|
|
294
|
+
repo_wait: { status: 'waiting', since: record.time },
|
|
295
|
+
}
|
|
296
|
+
case 'repo_wait_ended':
|
|
297
|
+
return {
|
|
298
|
+
...next,
|
|
299
|
+
repo_wait: { ...state.repo_wait, status: 'ended' },
|
|
300
|
+
}
|
|
301
|
+
case 'repo_wait_skipped':
|
|
302
|
+
return {
|
|
303
|
+
...next,
|
|
304
|
+
repo_wait: { ...state.repo_wait, status: 'skipped' },
|
|
305
|
+
}
|
|
949
306
|
case 'run_stopped':
|
|
950
307
|
return {
|
|
951
308
|
...next,
|
|
@@ -958,6 +315,9 @@ const applyRecord = ({
|
|
|
958
315
|
case 'step_started':
|
|
959
316
|
case 'step_ended':
|
|
960
317
|
return next
|
|
318
|
+
// A wait between steps changes no step: only the board reads it.
|
|
319
|
+
case 'ticket_waiting':
|
|
320
|
+
return next
|
|
961
321
|
// Only a join's redo reads it (see `openJoin`).
|
|
962
322
|
case 'join_started':
|
|
963
323
|
return next
|
|
@@ -970,6 +330,24 @@ const applyRecord = ({
|
|
|
970
330
|
// Luca's version on a resume changes no step: only the board reads it.
|
|
971
331
|
case 'engine_resumed':
|
|
972
332
|
return next
|
|
333
|
+
// A resume's new build fields (#516): every later step uses them.
|
|
334
|
+
case 'config_reloaded':
|
|
335
|
+
return {
|
|
336
|
+
...next,
|
|
337
|
+
config:
|
|
338
|
+
state.config === null
|
|
339
|
+
? null
|
|
340
|
+
: withConfigChanges({
|
|
341
|
+
config: state.config,
|
|
342
|
+
changes: record.content.changes,
|
|
343
|
+
}),
|
|
344
|
+
}
|
|
345
|
+
// The run kept its config; only the board and the log read why.
|
|
346
|
+
case 'config_reload_refused':
|
|
347
|
+
return next
|
|
348
|
+
// A failing `ps` changes no step: the wait goes on (#624).
|
|
349
|
+
case 'repo_wait_ps_failed':
|
|
350
|
+
return next
|
|
973
351
|
case 'comment_read':
|
|
974
352
|
return { ...next, comments: [...state.comments, record.content] }
|
|
975
353
|
case 'run_stuck':
|
|
@@ -1002,70 +380,10 @@ const applyRecord = ({
|
|
|
1002
380
|
review: state.final_review,
|
|
1003
381
|
}),
|
|
1004
382
|
}
|
|
1005
|
-
case 'reply_received':
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
...state.handled_comments,
|
|
1010
|
-
record.content.comment_id,
|
|
1011
|
-
],
|
|
1012
|
-
}
|
|
1013
|
-
const { word, comment_id, ticket } = record.content
|
|
1014
|
-
if (word === 'stop') return { ...handled, stop: { comment_id } }
|
|
1015
|
-
// A bare `retry` while the run is stuck on its budget adds one
|
|
1016
|
-
// more full budget, and the run carries on.
|
|
1017
|
-
if (
|
|
1018
|
-
ticket === null &&
|
|
1019
|
-
word === 'retry' &&
|
|
1020
|
-
state.run_budget.stuck !== null
|
|
1021
|
-
) {
|
|
1022
|
-
return {
|
|
1023
|
-
...handled,
|
|
1024
|
-
run_budget: {
|
|
1025
|
-
stuck: null,
|
|
1026
|
-
report: null,
|
|
1027
|
-
retries: state.run_budget.retries + 1,
|
|
1028
|
-
},
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
if (ticket === null && (word === 'retry' || word === 'ship')) {
|
|
1032
|
-
return { ...handled, final_reply: { word, comment_id } }
|
|
1033
|
-
}
|
|
1034
|
-
return applyTicketRecord({ state: handled, record })
|
|
1035
|
-
}
|
|
1036
|
-
case 'stuck_reported': {
|
|
1037
|
-
const reported = {
|
|
1038
|
-
...next,
|
|
1039
|
-
engine_comments: engineCommentsAfter({
|
|
1040
|
-
state,
|
|
1041
|
-
comment_id: record.content.comment_id,
|
|
1042
|
-
}),
|
|
1043
|
-
}
|
|
1044
|
-
// A report with no ticket is the stuck run's, while it is not
|
|
1045
|
-
// told yet, else the final review's.
|
|
1046
|
-
const { run_budget } = state
|
|
1047
|
-
if (
|
|
1048
|
-
record.ticket === null &&
|
|
1049
|
-
run_budget.stuck !== null &&
|
|
1050
|
-
run_budget.report === null
|
|
1051
|
-
) {
|
|
1052
|
-
return {
|
|
1053
|
-
...reported,
|
|
1054
|
-
run_budget: {
|
|
1055
|
-
...run_budget,
|
|
1056
|
-
report: { comment_id: record.content.comment_id },
|
|
1057
|
-
},
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
return record.ticket === null
|
|
1061
|
-
? {
|
|
1062
|
-
...reported,
|
|
1063
|
-
final_stuck_report: {
|
|
1064
|
-
comment_id: record.content.comment_id,
|
|
1065
|
-
},
|
|
1066
|
-
}
|
|
1067
|
-
: applyTicketRecord({ state: reported, record })
|
|
1068
|
-
}
|
|
383
|
+
case 'reply_received':
|
|
384
|
+
return replyReceivedAfter({ state, next, record })
|
|
385
|
+
case 'stuck_reported':
|
|
386
|
+
return stuckReportedAfter({ state, next, record })
|
|
1069
387
|
// Its note on the spec issue is the engine's own, never a reply.
|
|
1070
388
|
case 'ticket_already_done':
|
|
1071
389
|
return applyTicketRecord({
|
|
@@ -1078,33 +396,8 @@ const applyRecord = ({
|
|
|
1078
396
|
},
|
|
1079
397
|
record,
|
|
1080
398
|
})
|
|
1081
|
-
case 'ticket_retried':
|
|
1082
|
-
|
|
1083
|
-
const install = state.run_branch_install?.check
|
|
1084
|
-
// A run branch whose install failed is installed again on a retry.
|
|
1085
|
-
const run_branch_install =
|
|
1086
|
-
mode === 'refused' ||
|
|
1087
|
-
install === undefined ||
|
|
1088
|
-
install?.ok !== false
|
|
1089
|
-
? state.run_branch_install
|
|
1090
|
-
: null
|
|
1091
|
-
return applyTicketRecord({
|
|
1092
|
-
state: {
|
|
1093
|
-
...next,
|
|
1094
|
-
// A retry gives the ticket fresh crash counts too.
|
|
1095
|
-
crashes:
|
|
1096
|
-
record.ticket === null
|
|
1097
|
-
? next.crashes
|
|
1098
|
-
: omit(next.crashes, String(record.ticket)),
|
|
1099
|
-
run_branch_install,
|
|
1100
|
-
engine_comments: engineCommentsAfter({
|
|
1101
|
-
state,
|
|
1102
|
-
comment_id: answer_id,
|
|
1103
|
-
}),
|
|
1104
|
-
},
|
|
1105
|
-
record,
|
|
1106
|
-
})
|
|
1107
|
-
}
|
|
399
|
+
case 'ticket_retried':
|
|
400
|
+
return ticketRetriedAfter({ state, next, record })
|
|
1108
401
|
case 'usage_recorded': {
|
|
1109
402
|
const { scope, ticket } = record.content
|
|
1110
403
|
const { tickets, run } = state.usage_recorded
|
|
@@ -1127,6 +420,13 @@ const applyRecord = ({
|
|
|
1127
420
|
// Others' changes to the shared .git: noted, never acted on.
|
|
1128
421
|
case 'shared_git_changed':
|
|
1129
422
|
return next
|
|
423
|
+
// A writer's chores (#618): noted; one the engine could not do is
|
|
424
|
+
// the `agent_failed` after it.
|
|
425
|
+
case 'chores_done':
|
|
426
|
+
return next
|
|
427
|
+
// An agent's changeset the engine removed: noted, never acted on.
|
|
428
|
+
case 'changeset_dropped':
|
|
429
|
+
return next
|
|
1130
430
|
case 'prepare_made':
|
|
1131
431
|
return {
|
|
1132
432
|
...next,
|
|
@@ -1135,17 +435,34 @@ const applyRecord = ({
|
|
|
1135
435
|
...record.content.paths,
|
|
1136
436
|
]),
|
|
1137
437
|
}
|
|
438
|
+
// Forgotten in `closedSessionsAfter`, after every record.
|
|
1138
439
|
case 'agent_session_closed':
|
|
1139
|
-
return
|
|
1140
|
-
state: next,
|
|
1141
|
-
session_id: record.content.session_id,
|
|
1142
|
-
})
|
|
440
|
+
return next
|
|
1143
441
|
case 'agent_finished':
|
|
1144
442
|
return applyTicketRecord({
|
|
1145
443
|
state: { ...next, run_notes: notesAfter({ state, record }) },
|
|
1146
444
|
record,
|
|
1147
445
|
})
|
|
1148
|
-
//
|
|
446
|
+
// A ticket's checks also keep the run's old-test failures (#544).
|
|
447
|
+
case 'red_check':
|
|
448
|
+
case 'gates_run':
|
|
449
|
+
return sharedFailuresAfter({
|
|
450
|
+
state: applyTicketRecord({ state: next, record }),
|
|
451
|
+
record,
|
|
452
|
+
})
|
|
453
|
+
case 'join_undone':
|
|
454
|
+
return undoneJoinsAfter({
|
|
455
|
+
state: applyTicketRecord({ state: next, record }),
|
|
456
|
+
ticket: record.ticket,
|
|
457
|
+
shas: record.content.shas,
|
|
458
|
+
})
|
|
459
|
+
case 'ticket_rebased':
|
|
460
|
+
return undoneJoinsAfter({
|
|
461
|
+
state: applyTicketRecord({ state: next, record }),
|
|
462
|
+
ticket: record.ticket,
|
|
463
|
+
shas: record.content.undone,
|
|
464
|
+
})
|
|
465
|
+
// Always the final review's (see `finalRecordOf`).
|
|
1149
466
|
case 'final_review_started':
|
|
1150
467
|
case 'lens_started':
|
|
1151
468
|
case 'lens_finished':
|
|
@@ -1159,1310 +476,6 @@ const applyRecord = ({
|
|
|
1159
476
|
}
|
|
1160
477
|
}
|
|
1161
478
|
|
|
1162
|
-
/** The agent kinds a learner's turn journals. */
|
|
1163
|
-
type LearnerRecord = Extract<
|
|
1164
|
-
JournalRecord,
|
|
1165
|
-
{
|
|
1166
|
-
kind:
|
|
1167
|
-
| 'agent_started'
|
|
1168
|
-
| 'agent_finished'
|
|
1169
|
-
| 'agent_failed'
|
|
1170
|
-
| 'agent_session'
|
|
1171
|
-
}
|
|
1172
|
-
>
|
|
1173
|
-
|
|
1174
|
-
/** The record if it is one of the learner's agent records, else `null`. */
|
|
1175
|
-
const learnerRecordOf = ({
|
|
1176
|
-
record,
|
|
1177
|
-
}: {
|
|
1178
|
-
record: JournalRecord
|
|
1179
|
-
}): LearnerRecord | null => {
|
|
1180
|
-
switch (record.kind) {
|
|
1181
|
-
case 'agent_started':
|
|
1182
|
-
case 'agent_finished':
|
|
1183
|
-
case 'agent_failed':
|
|
1184
|
-
case 'agent_session':
|
|
1185
|
-
return record.content.role === 'learner' ? record : null
|
|
1186
|
-
default:
|
|
1187
|
-
return null
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
/**
|
|
1192
|
-
* The learner after one of its records: its answer ends any failure; a
|
|
1193
|
-
* failed turn counts a try (or an engine failure in a row), like any
|
|
1194
|
-
* agent's.
|
|
1195
|
-
*/
|
|
1196
|
-
const learnerAfter = ({
|
|
1197
|
-
learner,
|
|
1198
|
-
record,
|
|
1199
|
-
}: {
|
|
1200
|
-
learner: LearnerState
|
|
1201
|
-
record: LearnerRecord
|
|
1202
|
-
}): LearnerState => {
|
|
1203
|
-
switch (record.kind) {
|
|
1204
|
-
case 'agent_started':
|
|
1205
|
-
case 'agent_session':
|
|
1206
|
-
return learner
|
|
1207
|
-
case 'agent_finished':
|
|
1208
|
-
return record.content.role === 'learner'
|
|
1209
|
-
? {
|
|
1210
|
-
...learner,
|
|
1211
|
-
result: record.content.result,
|
|
1212
|
-
result_seq: record.seq,
|
|
1213
|
-
agent_failure: null,
|
|
1214
|
-
engine_failures: 0,
|
|
1215
|
-
}
|
|
1216
|
-
: learner
|
|
1217
|
-
case 'agent_failed': {
|
|
1218
|
-
const { role, error, failure, session_id } = record.content
|
|
1219
|
-
const agent_failure = { role, error, failure, session_id }
|
|
1220
|
-
return failure === 'engine'
|
|
1221
|
-
? {
|
|
1222
|
-
...learner,
|
|
1223
|
-
agent_failure,
|
|
1224
|
-
engine_failures: learner.engine_failures + 1,
|
|
1225
|
-
}
|
|
1226
|
-
: {
|
|
1227
|
-
...learner,
|
|
1228
|
-
agent_failure,
|
|
1229
|
-
engine_failures: 0,
|
|
1230
|
-
failed_tries: learner.failed_tries + 1,
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
/** The final review's own kinds, and the kinds it shares with tickets. */
|
|
1237
|
-
type FinalRecord = Extract<
|
|
1238
|
-
JournalRecord,
|
|
1239
|
-
{
|
|
1240
|
-
kind:
|
|
1241
|
-
| 'final_review_started'
|
|
1242
|
-
| 'lens_started'
|
|
1243
|
-
| 'lens_finished'
|
|
1244
|
-
| 'final_review_fixing'
|
|
1245
|
-
| 'final_review_stuck'
|
|
1246
|
-
| 'final_review_passed'
|
|
1247
|
-
| 'final_review_shipped'
|
|
1248
|
-
| 'agent_started'
|
|
1249
|
-
| 'agent_finished'
|
|
1250
|
-
| 'agent_failed'
|
|
1251
|
-
| 'agent_session'
|
|
1252
|
-
| 'gates_run'
|
|
1253
|
-
| 'leftover_scan'
|
|
1254
|
-
| 'commit_made'
|
|
1255
|
-
| 'run_branch_pushed'
|
|
1256
|
-
}
|
|
1257
|
-
>
|
|
1258
|
-
|
|
1259
|
-
const FINAL_KINDS = new Set<JournalRecord['kind']>([
|
|
1260
|
-
'final_review_started',
|
|
1261
|
-
'lens_started',
|
|
1262
|
-
'lens_finished',
|
|
1263
|
-
'final_review_fixing',
|
|
1264
|
-
'final_review_stuck',
|
|
1265
|
-
'final_review_passed',
|
|
1266
|
-
'final_review_shipped',
|
|
1267
|
-
])
|
|
1268
|
-
|
|
1269
|
-
/** Kinds tickets journal too; ticket-less ones are the final review's once it started. */
|
|
1270
|
-
const SHARED_KINDS = new Set<JournalRecord['kind']>([
|
|
1271
|
-
'agent_started',
|
|
1272
|
-
'agent_finished',
|
|
1273
|
-
'agent_failed',
|
|
1274
|
-
'agent_session',
|
|
1275
|
-
'gates_run',
|
|
1276
|
-
'leftover_scan',
|
|
1277
|
-
'commit_made',
|
|
1278
|
-
'run_branch_pushed',
|
|
1279
|
-
])
|
|
1280
|
-
|
|
1281
|
-
/**
|
|
1282
|
-
* The record if it is the final review's, else `null`: one of its own
|
|
1283
|
-
* kinds, or a ticket-less agent, gate, scan, commit, or push record once
|
|
1284
|
-
* the final review started. Before that, ticket-less records keep their old
|
|
1285
|
-
* meaning (such as the run branch's gates after a join).
|
|
1286
|
-
*/
|
|
1287
|
-
const finalRecordOf = ({
|
|
1288
|
-
state,
|
|
1289
|
-
record,
|
|
1290
|
-
}: {
|
|
1291
|
-
state: RunState
|
|
1292
|
-
record: JournalRecord
|
|
1293
|
-
}): FinalRecord | null => {
|
|
1294
|
-
const mine =
|
|
1295
|
-
FINAL_KINDS.has(record.kind) ||
|
|
1296
|
-
(record.ticket === null &&
|
|
1297
|
-
state.final_review.round > 0 &&
|
|
1298
|
-
SHARED_KINDS.has(record.kind))
|
|
1299
|
-
return mine ? (record as FinalRecord) : null
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
/**
|
|
1303
|
-
* A lens's finding id, namespaced by its lens (`<lens>-<id>`), since lenses
|
|
1304
|
-
* choose their ids on their own. An id that already starts with the lens's
|
|
1305
|
-
* prefix is kept, so a re-review listing a finding again keeps its id.
|
|
1306
|
-
*/
|
|
1307
|
-
export const namespacedId = ({
|
|
1308
|
-
lens,
|
|
1309
|
-
id,
|
|
1310
|
-
}: {
|
|
1311
|
-
lens: LensName
|
|
1312
|
-
id: string
|
|
1313
|
-
}): string => (id.startsWith(`${lens}-`) ? id : `${lens}-${id}`)
|
|
1314
|
-
|
|
1315
|
-
/** A lens's result with every finding and ruling id namespaced. */
|
|
1316
|
-
const namespaced = ({
|
|
1317
|
-
lens,
|
|
1318
|
-
result,
|
|
1319
|
-
}: {
|
|
1320
|
-
lens: LensName
|
|
1321
|
-
result: LensReviewResult
|
|
1322
|
-
}): LensReviewResult => ({
|
|
1323
|
-
...result,
|
|
1324
|
-
findings: result.findings.map((entry) => ({
|
|
1325
|
-
...entry,
|
|
1326
|
-
id: namespacedId({ lens, id: entry.id }),
|
|
1327
|
-
})),
|
|
1328
|
-
rulings: result.rulings.map((entry) => ({
|
|
1329
|
-
...entry,
|
|
1330
|
-
finding_id: namespacedId({ lens, id: entry.finding_id }),
|
|
1331
|
-
})),
|
|
1332
|
-
})
|
|
1333
|
-
|
|
1334
|
-
/**
|
|
1335
|
-
* The final review once every due lens finished its round: settles the
|
|
1336
|
-
* last fix round's "won't fix" answers per lens (see `declinedBy`), keeps
|
|
1337
|
-
* the nits, and opens a fix round on the blocking findings of every due
|
|
1338
|
-
* lens, with its fixers, gates, commit, and push afresh (and fresh fixer
|
|
1339
|
-
* sessions). No blocking finding leaves `fix` null: the review is clean.
|
|
1340
|
-
*/
|
|
1341
|
-
const settleRound = ({
|
|
1342
|
-
review,
|
|
1343
|
-
}: {
|
|
1344
|
-
review: FinalReviewState
|
|
1345
|
-
}): FinalReviewState => {
|
|
1346
|
-
const due = review.lenses_due.flatMap((lens) => {
|
|
1347
|
-
const result = review.results[lens]
|
|
1348
|
-
return result === undefined ? [] : [{ lens, result }]
|
|
1349
|
-
})
|
|
1350
|
-
const tagged = (lens: LensName, findings: Finding[]): FinalFinding[] =>
|
|
1351
|
-
findings.map((entry) => ({ ...entry, lens }))
|
|
1352
|
-
const declined = due.flatMap(({ lens, result }) =>
|
|
1353
|
-
declinedBy({
|
|
1354
|
-
fix:
|
|
1355
|
-
review.fix === null
|
|
1356
|
-
? null
|
|
1357
|
-
: {
|
|
1358
|
-
...review.fix,
|
|
1359
|
-
findings: review.fix.findings.filter(
|
|
1360
|
-
(entry) => entry.lens === lens
|
|
1361
|
-
),
|
|
1362
|
-
},
|
|
1363
|
-
review: result,
|
|
1364
|
-
}).map((entry) => ({ ...entry, lens }))
|
|
1365
|
-
)
|
|
1366
|
-
const nits = due.flatMap(({ lens, result }) =>
|
|
1367
|
-
tagged(
|
|
1368
|
-
lens,
|
|
1369
|
-
result.findings.filter((entry) => !isBlocking(entry))
|
|
1370
|
-
)
|
|
1371
|
-
)
|
|
1372
|
-
const blocking = due.flatMap(({ lens, result }) =>
|
|
1373
|
-
tagged(lens, result.findings.filter(isBlocking))
|
|
1374
|
-
)
|
|
1375
|
-
const settled: FinalReviewState = {
|
|
1376
|
-
...review,
|
|
1377
|
-
declined: [...review.declined, ...declined],
|
|
1378
|
-
nits: [
|
|
1379
|
-
...review.nits,
|
|
1380
|
-
...nits.filter(
|
|
1381
|
-
(entry, index) =>
|
|
1382
|
-
!review.nits.some(({ id }) => id === entry.id) &&
|
|
1383
|
-
nits.findIndex(({ id }) => id === entry.id) === index
|
|
1384
|
-
),
|
|
1385
|
-
],
|
|
1386
|
-
}
|
|
1387
|
-
if (blocking.length === 0) return { ...settled, fix: null }
|
|
1388
|
-
return {
|
|
1389
|
-
...settled,
|
|
1390
|
-
fix: {
|
|
1391
|
-
round: review.round,
|
|
1392
|
-
findings: blocking,
|
|
1393
|
-
tests_answered: !blocking.some(({ kind }) => kind === 'test'),
|
|
1394
|
-
code_answered: !blocking.some(({ kind }) => kind === 'code'),
|
|
1395
|
-
responses: [],
|
|
1396
|
-
bad_test: null,
|
|
1397
|
-
},
|
|
1398
|
-
fixing_started: false,
|
|
1399
|
-
sessions: omit(review.sessions, ['test-writer', 'implementer']),
|
|
1400
|
-
gates: null,
|
|
1401
|
-
gate_fix_rounds: 0,
|
|
1402
|
-
leftovers: null,
|
|
1403
|
-
commit: null,
|
|
1404
|
-
pushed: null,
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
/** A fixer's bad test, or the implementer's summary as its reason. */
|
|
1409
|
-
const badTestOf = (result: ImplementerResult): BadTest | null =>
|
|
1410
|
-
result.outcome === 'bad_test'
|
|
1411
|
-
? (result.bad_test ?? { file: '', name: '', reason: result.summary })
|
|
1412
|
-
: null
|
|
1413
|
-
|
|
1414
|
-
/**
|
|
1415
|
-
* Where a finished agent's result goes in the final review: a lens's
|
|
1416
|
-
* result this round (settling the round once every due lens finished), or
|
|
1417
|
-
* a fixer's answer to the open fix round. An implementer result after
|
|
1418
|
-
* failed gates answers a gate fix round, like a ticket's.
|
|
1419
|
-
*/
|
|
1420
|
-
const finalResultChange = ({
|
|
1421
|
-
review,
|
|
1422
|
-
finished,
|
|
1423
|
-
}: {
|
|
1424
|
-
review: FinalReviewState
|
|
1425
|
-
finished: FinishedContent
|
|
1426
|
-
}): FinalReviewState => {
|
|
1427
|
-
const lens = lensOf({ role: finished.role })
|
|
1428
|
-
const { fix } = review
|
|
1429
|
-
if (lens !== null && 'verdict' in finished.result) {
|
|
1430
|
-
if (!review.lenses_due.includes(lens)) return review
|
|
1431
|
-
const next = {
|
|
1432
|
-
...review,
|
|
1433
|
-
results: {
|
|
1434
|
-
...review.results,
|
|
1435
|
-
[lens]: namespaced({ lens, result: finished.result }),
|
|
1436
|
-
},
|
|
1437
|
-
}
|
|
1438
|
-
const pending = next.lenses_due.some(
|
|
1439
|
-
(due) => next.results[due] === undefined
|
|
1440
|
-
)
|
|
1441
|
-
return pending ? next : settleRound({ review: next })
|
|
1442
|
-
}
|
|
1443
|
-
if (finished.role === 'test-writer') {
|
|
1444
|
-
if (fix === null || fix.tests_answered) return review
|
|
1445
|
-
return {
|
|
1446
|
-
...review,
|
|
1447
|
-
fix: {
|
|
1448
|
-
...fix,
|
|
1449
|
-
tests_answered: true,
|
|
1450
|
-
responses: [
|
|
1451
|
-
...fix.responses,
|
|
1452
|
-
...finished.result.finding_responses,
|
|
1453
|
-
],
|
|
1454
|
-
},
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
if (finished.role === 'implementer') {
|
|
1458
|
-
const { result } = finished
|
|
1459
|
-
if (fix !== null && fix.tests_answered && !fix.code_answered) {
|
|
1460
|
-
return {
|
|
1461
|
-
...review,
|
|
1462
|
-
fix: {
|
|
1463
|
-
...fix,
|
|
1464
|
-
code_answered: true,
|
|
1465
|
-
responses: [...fix.responses, ...result.finding_responses],
|
|
1466
|
-
bad_test: badTestOf(result),
|
|
1467
|
-
},
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
if (review.gates === null) return review
|
|
1471
|
-
return {
|
|
1472
|
-
...review,
|
|
1473
|
-
gates: null,
|
|
1474
|
-
gate_fix_rounds: review.gate_fix_rounds + 1,
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
return review
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
/** The final review after one of its records. */
|
|
1481
|
-
const finalReviewAfter = ({
|
|
1482
|
-
review,
|
|
1483
|
-
record,
|
|
1484
|
-
}: {
|
|
1485
|
-
review: FinalReviewState
|
|
1486
|
-
record: FinalRecord
|
|
1487
|
-
}): FinalReviewState => {
|
|
1488
|
-
switch (record.kind) {
|
|
1489
|
-
case 'final_review_started': {
|
|
1490
|
-
const { round, from_sha, head_sha, lenses, files, rules } =
|
|
1491
|
-
record.content
|
|
1492
|
-
return {
|
|
1493
|
-
...review,
|
|
1494
|
-
round,
|
|
1495
|
-
from_sha,
|
|
1496
|
-
head_sha,
|
|
1497
|
-
files,
|
|
1498
|
-
lenses_due: lenses,
|
|
1499
|
-
rules,
|
|
1500
|
-
results: {},
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
case 'agent_started':
|
|
1504
|
-
return isFixerRole(record.content.role)
|
|
1505
|
-
? { ...review, crashed_turn: null }
|
|
1506
|
-
: review
|
|
1507
|
-
case 'lens_started':
|
|
1508
|
-
case 'lens_finished':
|
|
1509
|
-
case 'agent_session':
|
|
1510
|
-
return review
|
|
1511
|
-
case 'final_review_fixing':
|
|
1512
|
-
return review.fix !== null &&
|
|
1513
|
-
record.content.round === review.fix.round
|
|
1514
|
-
? { ...review, fixing_started: true }
|
|
1515
|
-
: review
|
|
1516
|
-
case 'final_review_stuck':
|
|
1517
|
-
return { ...review, stuck: record.content }
|
|
1518
|
-
case 'final_review_passed':
|
|
1519
|
-
return { ...review, passed: true }
|
|
1520
|
-
case 'final_review_shipped':
|
|
1521
|
-
return review.stuck === null ? review : { ...review, shipped: true }
|
|
1522
|
-
case 'agent_finished': {
|
|
1523
|
-
const finished = record.content
|
|
1524
|
-
const { role } = finished
|
|
1525
|
-
return finalResultChange({
|
|
1526
|
-
review: {
|
|
1527
|
-
...review,
|
|
1528
|
-
agent_failures: omit(review.agent_failures, role),
|
|
1529
|
-
engine_failures: omit(review.engine_failures, role),
|
|
1530
|
-
sessions: sessionsAfter({
|
|
1531
|
-
sessions: review.sessions,
|
|
1532
|
-
role,
|
|
1533
|
-
session_id: finished.session_id,
|
|
1534
|
-
}),
|
|
1535
|
-
assumptions: [
|
|
1536
|
-
...review.assumptions,
|
|
1537
|
-
...assumptionsOf(finished),
|
|
1538
|
-
],
|
|
1539
|
-
},
|
|
1540
|
-
finished,
|
|
1541
|
-
})
|
|
1542
|
-
}
|
|
1543
|
-
case 'agent_failed': {
|
|
1544
|
-
const { role, error, failure, session_id } = record.content
|
|
1545
|
-
const agent_failures = {
|
|
1546
|
-
...review.agent_failures,
|
|
1547
|
-
[role]: { role, error, failure, session_id },
|
|
1548
|
-
}
|
|
1549
|
-
if (failure === 'engine') {
|
|
1550
|
-
return {
|
|
1551
|
-
...review,
|
|
1552
|
-
agent_failures,
|
|
1553
|
-
engine_failures: {
|
|
1554
|
-
...review.engine_failures,
|
|
1555
|
-
[role]: (review.engine_failures[role] ?? 0) + 1,
|
|
1556
|
-
},
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
return {
|
|
1560
|
-
...review,
|
|
1561
|
-
agent_failures,
|
|
1562
|
-
engine_failures: omit(review.engine_failures, role),
|
|
1563
|
-
failed_tries: {
|
|
1564
|
-
...review.failed_tries,
|
|
1565
|
-
[role]: (review.failed_tries[role] ?? 0) + 1,
|
|
1566
|
-
},
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
case 'gates_run':
|
|
1570
|
-
return {
|
|
1571
|
-
...review,
|
|
1572
|
-
gates: { ok: record.content.ok, checks: record.content.checks },
|
|
1573
|
-
}
|
|
1574
|
-
case 'leftover_scan':
|
|
1575
|
-
return { ...review, leftovers: record.content.hits }
|
|
1576
|
-
case 'commit_made':
|
|
1577
|
-
return {
|
|
1578
|
-
...review,
|
|
1579
|
-
commit: {
|
|
1580
|
-
sha: record.content.sha,
|
|
1581
|
-
files: record.content.files,
|
|
1582
|
-
},
|
|
1583
|
-
}
|
|
1584
|
-
case 'run_branch_pushed':
|
|
1585
|
-
return { ...review, pushed: record.content.sha }
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
type TicketRecord = Exclude<
|
|
1590
|
-
JournalRecord,
|
|
1591
|
-
{
|
|
1592
|
-
kind:
|
|
1593
|
-
| 'run_started'
|
|
1594
|
-
| 'intake_read'
|
|
1595
|
-
| 'intake_refused'
|
|
1596
|
-
| 'nothing_to_do'
|
|
1597
|
-
| 'spec_snapshot'
|
|
1598
|
-
| 'ticket_snapshot'
|
|
1599
|
-
| 'run_branch_created'
|
|
1600
|
-
| 'worktrees_removed'
|
|
1601
|
-
| 'changeset_written'
|
|
1602
|
-
| 'pull_request_opened'
|
|
1603
|
-
| 'jev_asked'
|
|
1604
|
-
| 'jev_answered'
|
|
1605
|
-
| 'jev_failed'
|
|
1606
|
-
| 'agent_session'
|
|
1607
|
-
| 'agent_session_closed'
|
|
1608
|
-
| 'run_stopped'
|
|
1609
|
-
| 'limit_wait_started'
|
|
1610
|
-
| 'limit_wait_ended'
|
|
1611
|
-
| 'usage_line_wait_started'
|
|
1612
|
-
| 'usage_line_wait_ended'
|
|
1613
|
-
| 'usage_recorded'
|
|
1614
|
-
| 'agent_message'
|
|
1615
|
-
| 'agent_message_delivered'
|
|
1616
|
-
| 'shared_git_changed'
|
|
1617
|
-
| 'final_review_started'
|
|
1618
|
-
| 'lens_started'
|
|
1619
|
-
| 'lens_finished'
|
|
1620
|
-
| 'final_review_fixing'
|
|
1621
|
-
| 'final_review_stuck'
|
|
1622
|
-
| 'final_review_passed'
|
|
1623
|
-
| 'final_review_shipped'
|
|
1624
|
-
| 'comment_read'
|
|
1625
|
-
| 'run_stuck'
|
|
1626
|
-
| 'reply_ignored'
|
|
1627
|
-
| 'final_review_retried'
|
|
1628
|
-
| 'memory_recalled'
|
|
1629
|
-
| 'memories_saved'
|
|
1630
|
-
| 'learning_skipped'
|
|
1631
|
-
| 'memories_reported'
|
|
1632
|
-
| 'step_started'
|
|
1633
|
-
| 'step_ended'
|
|
1634
|
-
| 'run_resumed'
|
|
1635
|
-
| 'engine_resumed'
|
|
1636
|
-
| 'join_started'
|
|
1637
|
-
| 'memory_write_started'
|
|
1638
|
-
| 'memory_write_done'
|
|
1639
|
-
| 'baseline_reused'
|
|
1640
|
-
| 'prepare_made'
|
|
1641
|
-
}
|
|
1642
|
-
>
|
|
1643
|
-
|
|
1644
|
-
type FinishedContent = Extract<
|
|
1645
|
-
JournalRecord,
|
|
1646
|
-
{ kind: 'agent_finished' }
|
|
1647
|
-
>['content']
|
|
1648
|
-
|
|
1649
|
-
/** A finished agent's assumptions; the learner makes none. */
|
|
1650
|
-
const assumptionsOf = (finished: FinishedContent): string[] =>
|
|
1651
|
-
'assumptions' in finished.result ? finished.result.assumptions : []
|
|
1652
|
-
|
|
1653
|
-
/**
|
|
1654
|
-
* Where a finished agent's result goes in its ticket's progress. A result
|
|
1655
|
-
* that answers a fix-loop follow-up counts a round and clears the failed
|
|
1656
|
-
* check, so it runs again.
|
|
1657
|
-
*/
|
|
1658
|
-
const resultChange = ({
|
|
1659
|
-
progress,
|
|
1660
|
-
finished,
|
|
1661
|
-
seq,
|
|
1662
|
-
}: {
|
|
1663
|
-
progress: TicketProgress
|
|
1664
|
-
finished: FinishedContent
|
|
1665
|
-
seq: number
|
|
1666
|
-
}): Partial<TicketProgress> => {
|
|
1667
|
-
const fix = progress.review_fix
|
|
1668
|
-
const { rejoin } = progress
|
|
1669
|
-
switch (finished.role) {
|
|
1670
|
-
case 'test-writer':
|
|
1671
|
-
// The fresh test-writer after a rebase fixed the clashed tests:
|
|
1672
|
-
// no fix round, and the red check stands.
|
|
1673
|
-
if (rejoin?.tests_pending) {
|
|
1674
|
-
return { rejoin: { ...rejoin, tests_pending: false } }
|
|
1675
|
-
}
|
|
1676
|
-
if (fix !== null && !fix.tests_answered) {
|
|
1677
|
-
return {
|
|
1678
|
-
review_fix: {
|
|
1679
|
-
...fix,
|
|
1680
|
-
tests_answered: true,
|
|
1681
|
-
responses: [
|
|
1682
|
-
...fix.responses,
|
|
1683
|
-
...finished.result.finding_responses,
|
|
1684
|
-
],
|
|
1685
|
-
},
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
return progress.red_check === null
|
|
1689
|
-
? { test_writer: finished.result }
|
|
1690
|
-
: {
|
|
1691
|
-
test_writer: finished.result,
|
|
1692
|
-
red_check: null,
|
|
1693
|
-
red_fix_rounds: progress.red_fix_rounds + 1,
|
|
1694
|
-
}
|
|
1695
|
-
case 'implementer': {
|
|
1696
|
-
const { result } = finished
|
|
1697
|
-
// The implementer's answer to the clash message is no fix round.
|
|
1698
|
-
if (rejoin?.code_pending) {
|
|
1699
|
-
return {
|
|
1700
|
-
implementer: result,
|
|
1701
|
-
rejoin: { ...rejoin, code_pending: false },
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
if (fix !== null && fix.tests_answered && !fix.code_answered) {
|
|
1705
|
-
return {
|
|
1706
|
-
review_fix: {
|
|
1707
|
-
...fix,
|
|
1708
|
-
code_answered: true,
|
|
1709
|
-
responses: [
|
|
1710
|
-
...fix.responses,
|
|
1711
|
-
...result.finding_responses,
|
|
1712
|
-
],
|
|
1713
|
-
bad_test:
|
|
1714
|
-
result.outcome === 'bad_test'
|
|
1715
|
-
? (result.bad_test ?? {
|
|
1716
|
-
file: '',
|
|
1717
|
-
name: '',
|
|
1718
|
-
reason: result.summary,
|
|
1719
|
-
})
|
|
1720
|
-
: null,
|
|
1721
|
-
},
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
const bounce: Partial<TicketProgress> =
|
|
1725
|
-
result.outcome === 'bad_test'
|
|
1726
|
-
? {
|
|
1727
|
-
bad_test_bounces: progress.bad_test_bounces + 1,
|
|
1728
|
-
bad_test: result.bad_test ?? {
|
|
1729
|
-
file: '',
|
|
1730
|
-
name: '',
|
|
1731
|
-
reason: result.summary,
|
|
1732
|
-
},
|
|
1733
|
-
}
|
|
1734
|
-
: {}
|
|
1735
|
-
const round: Partial<TicketProgress> =
|
|
1736
|
-
progress.gates === null
|
|
1737
|
-
? {}
|
|
1738
|
-
: {
|
|
1739
|
-
gates: null,
|
|
1740
|
-
gate_fix_rounds: progress.gate_fix_rounds + 1,
|
|
1741
|
-
}
|
|
1742
|
-
return { implementer: result, ...round, ...bounce }
|
|
1743
|
-
}
|
|
1744
|
-
case 'ticket-reviewer':
|
|
1745
|
-
return reviewChange({ progress, review: finished.result, seq })
|
|
1746
|
-
// Lenses review the run branch, never a ticket.
|
|
1747
|
-
default:
|
|
1748
|
-
return {}
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
/**
|
|
1753
|
-
* The fixers' "won't fix" answers the new review let go: its ruling was
|
|
1754
|
-
* `accepted`, or it gave none and did not list the finding again.
|
|
1755
|
-
*/
|
|
1756
|
-
const declinedBy = ({
|
|
1757
|
-
fix,
|
|
1758
|
-
review,
|
|
1759
|
-
}: {
|
|
1760
|
-
fix: Pick<ReviewFix, 'findings' | 'responses'> | null
|
|
1761
|
-
review: TicketReviewResult
|
|
1762
|
-
}): DeclinedFinding[] => {
|
|
1763
|
-
if (fix === null) return []
|
|
1764
|
-
const listed = new Set(review.findings.map(({ id }) => id))
|
|
1765
|
-
return fix.responses.flatMap(({ finding_id, response, reason }) => {
|
|
1766
|
-
if (response !== 'wont_fix') return []
|
|
1767
|
-
const finding = fix.findings.find(({ id }) => id === finding_id)
|
|
1768
|
-
if (finding === undefined) return []
|
|
1769
|
-
const ruling = review.rulings.find(
|
|
1770
|
-
(entry) => entry.finding_id === finding_id
|
|
1771
|
-
)
|
|
1772
|
-
const accepted =
|
|
1773
|
-
ruling === undefined
|
|
1774
|
-
? !listed.has(finding_id)
|
|
1775
|
-
: ruling.ruling === 'accepted'
|
|
1776
|
-
return accepted
|
|
1777
|
-
? [{ finding, reason, ruling: ruling?.reason ?? '' }]
|
|
1778
|
-
: []
|
|
1779
|
-
})
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
/**
|
|
1783
|
-
* A finished ticket review: counts the round, settles the last round's
|
|
1784
|
-
* "won't fix" answers, keeps its nits, and opens a fix round when a finding
|
|
1785
|
-
* is a blocker or a should-fix. A new fix round starts its gates, gate fix
|
|
1786
|
-
* rounds, and fix commit afresh.
|
|
1787
|
-
*/
|
|
1788
|
-
const reviewChange = ({
|
|
1789
|
-
progress,
|
|
1790
|
-
review,
|
|
1791
|
-
seq,
|
|
1792
|
-
}: {
|
|
1793
|
-
progress: TicketProgress
|
|
1794
|
-
review: TicketReviewResult
|
|
1795
|
-
/** The review's record, for the join queue's order once it approves. */
|
|
1796
|
-
seq: number
|
|
1797
|
-
}): Partial<TicketProgress> => {
|
|
1798
|
-
const review_rounds = progress.review_rounds + 1
|
|
1799
|
-
const blocking = review.findings.filter(isBlocking)
|
|
1800
|
-
const newNits = review.findings.filter(
|
|
1801
|
-
(finding) =>
|
|
1802
|
-
!isBlocking(finding) &&
|
|
1803
|
-
!progress.nits.some(({ id }) => id === finding.id)
|
|
1804
|
-
)
|
|
1805
|
-
const settled: Partial<TicketProgress> = {
|
|
1806
|
-
review,
|
|
1807
|
-
review_rounds,
|
|
1808
|
-
reviewed_sha: progress.commits.fix ?? progress.commits.green,
|
|
1809
|
-
nits: [...progress.nits, ...newNits],
|
|
1810
|
-
declined: [
|
|
1811
|
-
...progress.declined,
|
|
1812
|
-
...declinedBy({ fix: progress.review_fix, review }),
|
|
1813
|
-
],
|
|
1814
|
-
}
|
|
1815
|
-
if (blocking.length === 0) {
|
|
1816
|
-
return { ...settled, review_fix: null, approved_seq: seq }
|
|
1817
|
-
}
|
|
1818
|
-
return {
|
|
1819
|
-
...settled,
|
|
1820
|
-
approved_seq: null,
|
|
1821
|
-
review_fix: {
|
|
1822
|
-
round: review_rounds,
|
|
1823
|
-
findings: blocking,
|
|
1824
|
-
tests_answered: !blocking.some(({ kind }) => kind === 'test'),
|
|
1825
|
-
code_answered: !blocking.some(({ kind }) => kind === 'code'),
|
|
1826
|
-
responses: [],
|
|
1827
|
-
bad_test: null,
|
|
1828
|
-
},
|
|
1829
|
-
gates: null,
|
|
1830
|
-
gate_fix_rounds: 0,
|
|
1831
|
-
leftovers: { ...progress.leftovers, fix: null },
|
|
1832
|
-
commits: { ...progress.commits, fix: null },
|
|
1833
|
-
commit_files: { ...progress.commit_files, fix: [] },
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
/** The sessions after a turn: the role's latest, or none if unknown. */
|
|
1838
|
-
const sessionsAfter = ({
|
|
1839
|
-
sessions,
|
|
1840
|
-
role,
|
|
1841
|
-
session_id,
|
|
1842
|
-
}: {
|
|
1843
|
-
sessions: Partial<Record<AgentRole, string>>
|
|
1844
|
-
role: AgentRole
|
|
1845
|
-
session_id: string | null
|
|
1846
|
-
}): Partial<Record<AgentRole, string>> => {
|
|
1847
|
-
const rest = omit(sessions, role)
|
|
1848
|
-
return session_id === null ? rest : { ...rest, [role]: session_id }
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
const progressChange = ({
|
|
1852
|
-
progress,
|
|
1853
|
-
record,
|
|
1854
|
-
}: {
|
|
1855
|
-
progress: TicketProgress
|
|
1856
|
-
record: TicketRecord
|
|
1857
|
-
}): Partial<TicketProgress> => {
|
|
1858
|
-
switch (record.kind) {
|
|
1859
|
-
case 'ticket_worktree_created':
|
|
1860
|
-
return { worktree: record.content }
|
|
1861
|
-
case 'dependencies_installed':
|
|
1862
|
-
return { install: { check: record.content.check } }
|
|
1863
|
-
case 'baseline_prepared':
|
|
1864
|
-
return { prepare: record.content.check }
|
|
1865
|
-
case 'baseline_tests':
|
|
1866
|
-
return {
|
|
1867
|
-
baseline: record.content,
|
|
1868
|
-
baseline_sha: progress.worktree?.base_sha ?? null,
|
|
1869
|
-
}
|
|
1870
|
-
case 'agent_started':
|
|
1871
|
-
return { retried: null, crashed_turn: null }
|
|
1872
|
-
case 'agent_finished': {
|
|
1873
|
-
const finished = record.content
|
|
1874
|
-
return {
|
|
1875
|
-
setup_change: setupChangeOf({ finished }),
|
|
1876
|
-
agent_failure: null,
|
|
1877
|
-
engine_failures: 0,
|
|
1878
|
-
sessions: sessionsAfter({
|
|
1879
|
-
sessions: progress.sessions,
|
|
1880
|
-
role: finished.role,
|
|
1881
|
-
session_id: finished.session_id,
|
|
1882
|
-
}),
|
|
1883
|
-
assumptions: [
|
|
1884
|
-
...progress.assumptions,
|
|
1885
|
-
...assumptionsOf(finished),
|
|
1886
|
-
],
|
|
1887
|
-
...resultChange({ progress, finished, seq: record.seq }),
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
case 'agent_failed': {
|
|
1891
|
-
const { role, error, failure, session_id } = record.content
|
|
1892
|
-
const agent_failure = { role, error, failure, session_id }
|
|
1893
|
-
if (failure === 'engine') {
|
|
1894
|
-
return {
|
|
1895
|
-
agent_failure,
|
|
1896
|
-
engine_failures: progress.engine_failures + 1,
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
// The engine's side worked this time, so the run of engine
|
|
1900
|
-
// failures is over.
|
|
1901
|
-
return {
|
|
1902
|
-
agent_failure,
|
|
1903
|
-
engine_failures: 0,
|
|
1904
|
-
failed_tries: {
|
|
1905
|
-
...progress.failed_tries,
|
|
1906
|
-
[role]: (progress.failed_tries[role] ?? 0) + 1,
|
|
1907
|
-
},
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
case 'red_check': {
|
|
1911
|
-
const { ok, problems, notes, tests } = record.content
|
|
1912
|
-
return { red_check: { ok, problems, notes, output: tests.output } }
|
|
1913
|
-
}
|
|
1914
|
-
case 'worktree_reset':
|
|
1915
|
-
// A fresh test-writer and implementer start over on a clean
|
|
1916
|
-
// worktree; the bad test and its count stay.
|
|
1917
|
-
return {
|
|
1918
|
-
test_writer: null,
|
|
1919
|
-
red_check: null,
|
|
1920
|
-
red_fix_rounds: 0,
|
|
1921
|
-
implementer: null,
|
|
1922
|
-
gates: null,
|
|
1923
|
-
gate_fix_rounds: 0,
|
|
1924
|
-
leftovers: { red: null, green: null, fix: null },
|
|
1925
|
-
commits: { ...progress.commits, red: null },
|
|
1926
|
-
sessions: omit(progress.sessions, [
|
|
1927
|
-
'test-writer',
|
|
1928
|
-
'implementer',
|
|
1929
|
-
]),
|
|
1930
|
-
}
|
|
1931
|
-
case 'leftover_scan':
|
|
1932
|
-
return {
|
|
1933
|
-
leftovers: {
|
|
1934
|
-
...progress.leftovers,
|
|
1935
|
-
[record.content.stage]: record.content.hits,
|
|
1936
|
-
},
|
|
1937
|
-
}
|
|
1938
|
-
case 'commit_made':
|
|
1939
|
-
return {
|
|
1940
|
-
commits: {
|
|
1941
|
-
...progress.commits,
|
|
1942
|
-
[record.content.stage]: record.content.sha,
|
|
1943
|
-
},
|
|
1944
|
-
commit_files: {
|
|
1945
|
-
...progress.commit_files,
|
|
1946
|
-
[record.content.stage]: record.content.files,
|
|
1947
|
-
},
|
|
1948
|
-
}
|
|
1949
|
-
case 'gates_run': {
|
|
1950
|
-
const { ok, checks } = record.content
|
|
1951
|
-
return record.content.target === 'ticket'
|
|
1952
|
-
? { gates: { ok, checks } }
|
|
1953
|
-
: { join_gates: { ok, checks } }
|
|
1954
|
-
}
|
|
1955
|
-
case 'ticket_joined':
|
|
1956
|
-
return { joined: record.content }
|
|
1957
|
-
case 'ticket_rebased':
|
|
1958
|
-
return rebasedChange({ progress, rebased: record.content })
|
|
1959
|
-
case 'run_branch_pushed':
|
|
1960
|
-
return { pushed: record.content.sha }
|
|
1961
|
-
case 'ticket_stuck':
|
|
1962
|
-
return { stuck: record.content, stuck_report: null, reply: null }
|
|
1963
|
-
case 'stuck_reported':
|
|
1964
|
-
return { stuck_report: { comment_id: record.content.comment_id } }
|
|
1965
|
-
case 'reply_received': {
|
|
1966
|
-
const { word, comment_id } = record.content
|
|
1967
|
-
// `stop` is the run's, and `ship` the final review's.
|
|
1968
|
-
return word === 'retry' || word === 'skip'
|
|
1969
|
-
? { reply: { word, comment_id } }
|
|
1970
|
-
: {}
|
|
1971
|
-
}
|
|
1972
|
-
case 'ticket_retried':
|
|
1973
|
-
return retriedChange({ progress, retried: record.content })
|
|
1974
|
-
case 'ticket_skipped':
|
|
1975
|
-
return {
|
|
1976
|
-
skipped: { because: record.content.because },
|
|
1977
|
-
reply: null,
|
|
1978
|
-
}
|
|
1979
|
-
case 'ticket_already_done':
|
|
1980
|
-
return { already_done: { shas: record.content.shas } }
|
|
1981
|
-
case 'ticket_closed':
|
|
1982
|
-
return { closed: true }
|
|
1983
|
-
case 'join_undone':
|
|
1984
|
-
return { joined: null, join_gates: null }
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
/**
|
|
1989
|
-
* The final review's report and reply after one of its records: a new
|
|
1990
|
-
* stuck is told afresh, and a ship settles the reply.
|
|
1991
|
-
*/
|
|
1992
|
-
const finalRepliesAfter = ({
|
|
1993
|
-
state,
|
|
1994
|
-
record,
|
|
1995
|
-
}: {
|
|
1996
|
-
state: RunState
|
|
1997
|
-
record: JournalRecord
|
|
1998
|
-
}): Pick<RunState, 'final_stuck_report' | 'final_reply'> => {
|
|
1999
|
-
switch (record.kind) {
|
|
2000
|
-
case 'final_review_stuck':
|
|
2001
|
-
return { final_stuck_report: null, final_reply: null }
|
|
2002
|
-
case 'final_review_shipped':
|
|
2003
|
-
return {
|
|
2004
|
-
final_stuck_report: state.final_stuck_report,
|
|
2005
|
-
final_reply: null,
|
|
2006
|
-
}
|
|
2007
|
-
default:
|
|
2008
|
-
return {
|
|
2009
|
-
final_stuck_report: state.final_stuck_report,
|
|
2010
|
-
final_reply: state.final_reply,
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
/**
|
|
2016
|
-
* The stuck final review resumed by `retry`, like a resumed ticket: fresh
|
|
2017
|
-
* fixers (no session kept) and fresh counts, and the owner's edits in the
|
|
2018
|
-
* run branch's worktree kept. A leftover scan, failed gates, or a step
|
|
2019
|
-
* crashes cut off just run again; a failed lens gets a fresh one; otherwise the open fix round
|
|
2020
|
-
* starts over as round 1, so the fixes (and the owner's edits) are gated,
|
|
2021
|
-
* committed, pushed, and re-reviewed.
|
|
2022
|
-
*/
|
|
2023
|
-
const resumedFinalReview = ({
|
|
2024
|
-
review,
|
|
2025
|
-
}: {
|
|
2026
|
-
review: FinalReviewState
|
|
2027
|
-
}): FinalReviewState => {
|
|
2028
|
-
const base: FinalReviewState = {
|
|
2029
|
-
...review,
|
|
2030
|
-
stuck: null,
|
|
2031
|
-
shipped: false,
|
|
2032
|
-
sessions: {},
|
|
2033
|
-
agent_failures: {},
|
|
2034
|
-
failed_tries: {},
|
|
2035
|
-
engine_failures: {},
|
|
2036
|
-
gate_fix_rounds: 0,
|
|
2037
|
-
gates: review.commit === null ? null : review.gates,
|
|
2038
|
-
leftovers: review.commit === null ? null : review.leftovers,
|
|
2039
|
-
}
|
|
2040
|
-
const { fix, stuck } = review
|
|
2041
|
-
if (
|
|
2042
|
-
fix === null ||
|
|
2043
|
-
stuck?.reason === 'leftovers_found' ||
|
|
2044
|
-
stuck?.reason === 'gates_failed' ||
|
|
2045
|
-
stuck?.reason === 'agent_failed' ||
|
|
2046
|
-
stuck?.reason === 'crashed'
|
|
2047
|
-
) {
|
|
2048
|
-
return base
|
|
2049
|
-
}
|
|
2050
|
-
return {
|
|
2051
|
-
...base,
|
|
2052
|
-
round: 1,
|
|
2053
|
-
fix: {
|
|
2054
|
-
...fix,
|
|
2055
|
-
round: 1,
|
|
2056
|
-
tests_answered: !fix.findings.some(({ kind }) => kind === 'test'),
|
|
2057
|
-
code_answered: !fix.findings.some(({ kind }) => kind === 'code'),
|
|
2058
|
-
responses: [],
|
|
2059
|
-
bad_test: null,
|
|
2060
|
-
},
|
|
2061
|
-
fixing_started: false,
|
|
2062
|
-
commit: null,
|
|
2063
|
-
pushed: null,
|
|
2064
|
-
}
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
/** The spec comments the engine posted, plus this one if it posted one. */
|
|
2068
|
-
const engineCommentsAfter = ({
|
|
2069
|
-
state,
|
|
2070
|
-
comment_id,
|
|
2071
|
-
}: {
|
|
2072
|
-
state: RunState
|
|
2073
|
-
comment_id: number | null
|
|
2074
|
-
}): number[] =>
|
|
2075
|
-
comment_id === null
|
|
2076
|
-
? state.engine_comments
|
|
2077
|
-
: [...state.engine_comments, comment_id]
|
|
2078
|
-
|
|
2079
|
-
/** The test setup change an agent asked for in its result, if any. */
|
|
2080
|
-
const setupChangeOf = ({
|
|
2081
|
-
finished,
|
|
2082
|
-
}: {
|
|
2083
|
-
finished: FinishedContent
|
|
2084
|
-
}): TicketProgress['setup_change'] => {
|
|
2085
|
-
if (finished.role !== 'test-writer' && finished.role !== 'implementer') {
|
|
2086
|
-
return null
|
|
2087
|
-
}
|
|
2088
|
-
const { outcome, setup_change } = finished.result
|
|
2089
|
-
if (outcome !== 'needs_setup_change') return null
|
|
2090
|
-
return {
|
|
2091
|
-
role: finished.role,
|
|
2092
|
-
file: setup_change?.file ?? '',
|
|
2093
|
-
reason: setup_change?.reason ?? finished.result.summary,
|
|
2094
|
-
}
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
type RetriedContent = Extract<
|
|
2098
|
-
JournalRecord,
|
|
2099
|
-
{ kind: 'ticket_retried' }
|
|
2100
|
-
>['content']
|
|
2101
|
-
|
|
2102
|
-
/**
|
|
2103
|
-
* A `retry` of a stuck ticket. `refused` leaves it stuck, waiting for the
|
|
2104
|
-
* next reply. `restart` starts it over from scratch (its new snapshot was
|
|
2105
|
-
* journaled just before), in its worktree reset to the run branch's tip.
|
|
2106
|
-
* `resume` picks up where it stopped (see `resumedProgress`).
|
|
2107
|
-
*/
|
|
2108
|
-
const retriedChange = ({
|
|
2109
|
-
progress,
|
|
2110
|
-
retried,
|
|
2111
|
-
}: {
|
|
2112
|
-
progress: TicketProgress
|
|
2113
|
-
retried: RetriedContent
|
|
2114
|
-
}): Partial<TicketProgress> => {
|
|
2115
|
-
switch (retried.mode) {
|
|
2116
|
-
case 'refused':
|
|
2117
|
-
return { reply: null }
|
|
2118
|
-
case 'restart':
|
|
2119
|
-
return {
|
|
2120
|
-
...EMPTY_TICKET_PROGRESS,
|
|
2121
|
-
worktree:
|
|
2122
|
-
progress.worktree === null || retried.base_sha === null
|
|
2123
|
-
? progress.worktree
|
|
2124
|
-
: { ...progress.worktree, base_sha: retried.base_sha },
|
|
2125
|
-
}
|
|
2126
|
-
case 'resume':
|
|
2127
|
-
return resumedProgress({ progress })
|
|
2128
|
-
}
|
|
2129
|
-
}
|
|
2130
|
-
|
|
2131
|
-
/**
|
|
2132
|
-
* A stuck ticket resumed by `retry`: it picks up where it stopped, with a
|
|
2133
|
-
* fresh agent (no session is kept) and fresh counts (fix rounds, failed
|
|
2134
|
-
* tries, bad-test bounces, rebases), and keeps whatever the user changed in
|
|
2135
|
-
* its worktree. The step that got stuck runs again:
|
|
2136
|
-
* - before the red commit, a fresh test-writer writes the tests;
|
|
2137
|
-
* - before the green commit, a fresh implementer builds (on top of the run
|
|
2138
|
-
* branch after a rebase), then the gates;
|
|
2139
|
-
* - in a review fix round, the round starts over as round 1 with fresh
|
|
2140
|
-
* fixers, so the user's changes are gated, committed, and re-reviewed;
|
|
2141
|
-
* - a leftover scan, a failed install, a failed prepare command at the
|
|
2142
|
-
* baseline, a reviewer's failed tries, a failed join, or a step crashes
|
|
2143
|
-
* cut off just run again.
|
|
2144
|
-
*/
|
|
2145
|
-
const resumedProgress = ({
|
|
2146
|
-
progress,
|
|
2147
|
-
}: {
|
|
2148
|
-
progress: TicketProgress
|
|
2149
|
-
}): Partial<TicketProgress> => {
|
|
2150
|
-
const { stuck, commits, review_fix, rejoin } = progress
|
|
2151
|
-
const base: Partial<TicketProgress> = {
|
|
2152
|
-
stuck: null,
|
|
2153
|
-
stuck_report: null,
|
|
2154
|
-
reply: null,
|
|
2155
|
-
retried: stuck,
|
|
2156
|
-
setup_change: null,
|
|
2157
|
-
agent_failure: null,
|
|
2158
|
-
failed_tries: {},
|
|
2159
|
-
engine_failures: 0,
|
|
2160
|
-
sessions: {},
|
|
2161
|
-
red_fix_rounds: 0,
|
|
2162
|
-
gate_fix_rounds: 0,
|
|
2163
|
-
bad_test_bounces: 0,
|
|
2164
|
-
rejoins: 0,
|
|
2165
|
-
install:
|
|
2166
|
-
progress.install?.check?.ok === false ? null : progress.install,
|
|
2167
|
-
prepare: progress.prepare?.ok === false ? null : progress.prepare,
|
|
2168
|
-
leftovers: {
|
|
2169
|
-
red: commits.red === null ? null : progress.leftovers.red,
|
|
2170
|
-
green: commits.green === null ? null : progress.leftovers.green,
|
|
2171
|
-
fix: commits.fix === null ? null : progress.leftovers.fix,
|
|
2172
|
-
},
|
|
2173
|
-
}
|
|
2174
|
-
const reason = stuck?.reason
|
|
2175
|
-
const reviewerFailed = progress.agent_failure?.role === 'ticket-reviewer'
|
|
2176
|
-
if (
|
|
2177
|
-
reason === 'leftovers_found' ||
|
|
2178
|
-
reason === 'install_failed' ||
|
|
2179
|
-
reason === 'prepare_failed' ||
|
|
2180
|
-
reason === 'join_failed' ||
|
|
2181
|
-
reason === 'join_gates_failed' ||
|
|
2182
|
-
reason === 'crashed' ||
|
|
2183
|
-
reviewerFailed
|
|
2184
|
-
) {
|
|
2185
|
-
return base
|
|
2186
|
-
}
|
|
2187
|
-
if (commits.green === null) {
|
|
2188
|
-
if (
|
|
2189
|
-
rejoin === null &&
|
|
2190
|
-
commits.red === null &&
|
|
2191
|
-
progress.test_writer !== null
|
|
2192
|
-
) {
|
|
2193
|
-
return { ...base, test_writer: null, red_check: null }
|
|
2194
|
-
}
|
|
2195
|
-
return { ...base, implementer: null, gates: null }
|
|
2196
|
-
}
|
|
2197
|
-
if (review_fix === null) return base
|
|
2198
|
-
return {
|
|
2199
|
-
...base,
|
|
2200
|
-
review_rounds: 1,
|
|
2201
|
-
review_fix: {
|
|
2202
|
-
...review_fix,
|
|
2203
|
-
round: 1,
|
|
2204
|
-
tests_answered: !review_fix.findings.some(
|
|
2205
|
-
({ kind }) => kind === 'test'
|
|
2206
|
-
),
|
|
2207
|
-
code_answered: !review_fix.findings.some(
|
|
2208
|
-
({ kind }) => kind === 'code'
|
|
2209
|
-
),
|
|
2210
|
-
responses: [],
|
|
2211
|
-
bad_test: null,
|
|
2212
|
-
},
|
|
2213
|
-
gates: null,
|
|
2214
|
-
leftovers: { ...base.leftovers!, fix: null },
|
|
2215
|
-
commits: { ...commits, fix: null },
|
|
2216
|
-
commit_files: { ...progress.commit_files, fix: [] },
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
type RebasedContent = Extract<
|
|
2221
|
-
JournalRecord,
|
|
2222
|
-
{ kind: 'ticket_rebased' }
|
|
2223
|
-
>['content']
|
|
2224
|
-
|
|
2225
|
-
/**
|
|
2226
|
-
* A ticket sent back onto the run branch: its change now sits uncommitted on
|
|
2227
|
-
* the run branch's tip. Its tests and code are fixed there, then the gates,
|
|
2228
|
-
* one green commit, a fresh review of the new changes (with its own fix
|
|
2229
|
-
* rounds), and a new join. The review loop starts over, but its nits and
|
|
2230
|
-
* declined findings stay for the PR. The implementer keeps its session; the
|
|
2231
|
-
* test-writer and reviewer start fresh. A worktree whose dependencies moved
|
|
2232
|
-
* under it is installed again.
|
|
2233
|
-
*/
|
|
2234
|
-
const rebasedChange = ({
|
|
2235
|
-
progress,
|
|
2236
|
-
rebased,
|
|
2237
|
-
}: {
|
|
2238
|
-
progress: TicketProgress
|
|
2239
|
-
rebased: RebasedContent
|
|
2240
|
-
}): Partial<TicketProgress> => {
|
|
2241
|
-
const { cause, base_sha, tests, code, reinstall } = rebased
|
|
2242
|
-
return {
|
|
2243
|
-
worktree:
|
|
2244
|
-
progress.worktree === null
|
|
2245
|
-
? null
|
|
2246
|
-
: { ...progress.worktree, base_sha },
|
|
2247
|
-
install: reinstall ? null : progress.install,
|
|
2248
|
-
rejoins: progress.rejoins + 1,
|
|
2249
|
-
rejoin: {
|
|
2250
|
-
cause,
|
|
2251
|
-
base_sha,
|
|
2252
|
-
tests,
|
|
2253
|
-
code,
|
|
2254
|
-
tests_pending: tests.length > 0,
|
|
2255
|
-
code_pending: code.length > 0,
|
|
2256
|
-
earlier_findings: [
|
|
2257
|
-
...(progress.review?.findings ?? []),
|
|
2258
|
-
...progress.declined.map(({ finding }) => finding),
|
|
2259
|
-
],
|
|
2260
|
-
},
|
|
2261
|
-
gates: null,
|
|
2262
|
-
gate_fix_rounds: 0,
|
|
2263
|
-
leftovers: { ...progress.leftovers, green: null, fix: null },
|
|
2264
|
-
commits: { ...progress.commits, green: null, fix: null },
|
|
2265
|
-
commit_files: { ...progress.commit_files, green: [], fix: [] },
|
|
2266
|
-
review: null,
|
|
2267
|
-
review_rounds: 0,
|
|
2268
|
-
reviewed_sha: null,
|
|
2269
|
-
review_fix: null,
|
|
2270
|
-
approved_seq: null,
|
|
2271
|
-
joined: null,
|
|
2272
|
-
join_gates: null,
|
|
2273
|
-
pushed: null,
|
|
2274
|
-
agent_failure: null,
|
|
2275
|
-
engine_failures: 0,
|
|
2276
|
-
sessions: omit(progress.sessions, ['test-writer', 'ticket-reviewer']),
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
/** The final review's scheduler keys: `final`, and `lens:<lens>`. */
|
|
2281
|
-
export const isFinalKey = (key: string): boolean =>
|
|
2282
|
-
key === 'final' || key.startsWith('lens:')
|
|
2283
|
-
|
|
2284
|
-
/** The crash counts without the final review's keys, for its `retry`. */
|
|
2285
|
-
const omitFinalKeys = ({ crashes }: { crashes: CrashCounts }): CrashCounts =>
|
|
2286
|
-
Object.fromEntries(
|
|
2287
|
-
Object.entries(crashes).filter(([key]) => !isFinalKey(key))
|
|
2288
|
-
)
|
|
2289
|
-
|
|
2290
|
-
const isFixerRole = (role: AgentRole): boolean =>
|
|
2291
|
-
role === 'test-writer' || role === 'implementer'
|
|
2292
|
-
|
|
2293
|
-
/** A failed turn's session forgotten, when it is this role's. */
|
|
2294
|
-
const failureWithoutSession = <
|
|
2295
|
-
Failure extends { role: AgentRole; session_id: string | null },
|
|
2296
|
-
>({
|
|
2297
|
-
failure,
|
|
2298
|
-
role,
|
|
2299
|
-
}: {
|
|
2300
|
-
failure: Failure | null | undefined
|
|
2301
|
-
role: AgentRole
|
|
2302
|
-
}): Failure | null =>
|
|
2303
|
-
failure === null || failure === undefined
|
|
2304
|
-
? null
|
|
2305
|
-
: failure.role === role
|
|
2306
|
-
? { ...failure, session_id: null }
|
|
2307
|
-
: failure
|
|
2308
|
-
|
|
2309
|
-
/**
|
|
2310
|
-
* A restarted engine's `run_resumed`: each agent turn a crash cut off is
|
|
2311
|
-
* taken again in a fresh session, so its role's session (and a failed
|
|
2312
|
-
* turn's session, for a follow-up) is forgotten, on its ticket or in the
|
|
2313
|
-
* final review, and the fresh agent is told of the crash. The counts it
|
|
2314
|
-
* adds live in `crashes`.
|
|
2315
|
-
*/
|
|
2316
|
-
const resumedAfter = ({
|
|
2317
|
-
state,
|
|
2318
|
-
record,
|
|
2319
|
-
}: {
|
|
2320
|
-
state: RunState
|
|
2321
|
-
record: Extract<JournalRecord, { kind: 'run_resumed' }>
|
|
2322
|
-
}): RunState =>
|
|
2323
|
-
record.content.interrupted.reduce((current, { key, ticket, role }) => {
|
|
2324
|
-
const parsed = AgentRoleSchema.safeParse(role)
|
|
2325
|
-
if (!parsed.success) return current
|
|
2326
|
-
const agent = parsed.data
|
|
2327
|
-
if (key === 'final') {
|
|
2328
|
-
const review = current.final_review
|
|
2329
|
-
const failed = failureWithoutSession({
|
|
2330
|
-
failure: review.agent_failures[agent],
|
|
2331
|
-
role: agent,
|
|
2332
|
-
})
|
|
2333
|
-
return {
|
|
2334
|
-
...current,
|
|
2335
|
-
final_review: {
|
|
2336
|
-
...review,
|
|
2337
|
-
sessions: omit(review.sessions, agent),
|
|
2338
|
-
agent_failures:
|
|
2339
|
-
failed === null
|
|
2340
|
-
? review.agent_failures
|
|
2341
|
-
: { ...review.agent_failures, [agent]: failed },
|
|
2342
|
-
crashed_turn: isFixerRole(agent)
|
|
2343
|
-
? agent
|
|
2344
|
-
: review.crashed_turn,
|
|
2345
|
-
},
|
|
2346
|
-
}
|
|
2347
|
-
}
|
|
2348
|
-
if (ticket === null || key !== String(ticket)) return current
|
|
2349
|
-
const progress = current.tickets[ticket] ?? EMPTY_TICKET_PROGRESS
|
|
2350
|
-
return {
|
|
2351
|
-
...current,
|
|
2352
|
-
tickets: {
|
|
2353
|
-
...current.tickets,
|
|
2354
|
-
[ticket]: {
|
|
2355
|
-
...progress,
|
|
2356
|
-
sessions: omit(progress.sessions, agent),
|
|
2357
|
-
agent_failure: failureWithoutSession({
|
|
2358
|
-
failure: progress.agent_failure,
|
|
2359
|
-
role: agent,
|
|
2360
|
-
}),
|
|
2361
|
-
crashed_turn: agent,
|
|
2362
|
-
},
|
|
2363
|
-
},
|
|
2364
|
-
}
|
|
2365
|
-
}, state)
|
|
2366
|
-
|
|
2367
|
-
/**
|
|
2368
|
-
* A closed session can take no follow-up: it is forgotten wherever the
|
|
2369
|
-
* decision step would follow it up (a role's latest session, or a failed
|
|
2370
|
-
* turn's), on its ticket, in the final review, or the learner's, so a fix
|
|
2371
|
-
* round that would have gone to it goes to a fresh agent.
|
|
2372
|
-
*/
|
|
2373
|
-
const closedSessionAfter = ({
|
|
2374
|
-
state,
|
|
2375
|
-
session_id,
|
|
2376
|
-
}: {
|
|
2377
|
-
state: RunState
|
|
2378
|
-
session_id: string
|
|
2379
|
-
}): RunState => {
|
|
2380
|
-
const forget = <Failure extends { session_id: string | null }>(
|
|
2381
|
-
failure: Failure
|
|
2382
|
-
): Failure =>
|
|
2383
|
-
failure.session_id === session_id
|
|
2384
|
-
? { ...failure, session_id: null }
|
|
2385
|
-
: failure
|
|
2386
|
-
const withoutIt = (sessions: Partial<Record<AgentRole, string>>) =>
|
|
2387
|
-
omitBy(sessions, (id) => id === session_id)
|
|
2388
|
-
const review = state.final_review
|
|
2389
|
-
const { learner } = state.memory
|
|
2390
|
-
return {
|
|
2391
|
-
...state,
|
|
2392
|
-
tickets: mapValues(state.tickets, (progress) => ({
|
|
2393
|
-
...progress,
|
|
2394
|
-
sessions: withoutIt(progress.sessions),
|
|
2395
|
-
agent_failure:
|
|
2396
|
-
progress.agent_failure === null
|
|
2397
|
-
? null
|
|
2398
|
-
: forget(progress.agent_failure),
|
|
2399
|
-
})),
|
|
2400
|
-
final_review: {
|
|
2401
|
-
...review,
|
|
2402
|
-
sessions: withoutIt(review.sessions),
|
|
2403
|
-
agent_failures: mapValues(review.agent_failures, (failure) =>
|
|
2404
|
-
failure === undefined ? failure : forget(failure)
|
|
2405
|
-
),
|
|
2406
|
-
},
|
|
2407
|
-
memory: {
|
|
2408
|
-
...state.memory,
|
|
2409
|
-
learner: {
|
|
2410
|
-
...learner,
|
|
2411
|
-
agent_failure:
|
|
2412
|
-
learner.agent_failure === null
|
|
2413
|
-
? null
|
|
2414
|
-
: forget(learner.agent_failure),
|
|
2415
|
-
},
|
|
2416
|
-
},
|
|
2417
|
-
}
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
const applyTicketRecord = ({
|
|
2421
|
-
state,
|
|
2422
|
-
record,
|
|
2423
|
-
}: {
|
|
2424
|
-
state: RunState
|
|
2425
|
-
record: TicketRecord
|
|
2426
|
-
}): RunState => {
|
|
2427
|
-
if (record.ticket === null) return state
|
|
2428
|
-
const progress = state.tickets[record.ticket] ?? EMPTY_TICKET_PROGRESS
|
|
2429
|
-
return {
|
|
2430
|
-
...state,
|
|
2431
|
-
tickets: {
|
|
2432
|
-
...state.tickets,
|
|
2433
|
-
[record.ticket]: {
|
|
2434
|
-
...progress,
|
|
2435
|
-
...progressChange({ progress, record }),
|
|
2436
|
-
},
|
|
2437
|
-
},
|
|
2438
|
-
}
|
|
2439
|
-
}
|
|
2440
|
-
|
|
2441
|
-
/**
|
|
2442
|
-
* A ticket took another ticket's baseline, as it stood when the reuse was
|
|
2443
|
-
* journaled, so a replay gives it the same one.
|
|
2444
|
-
*/
|
|
2445
|
-
const reusedBaselineAfter = ({
|
|
2446
|
-
state,
|
|
2447
|
-
record,
|
|
2448
|
-
}: {
|
|
2449
|
-
state: RunState
|
|
2450
|
-
record: Extract<JournalRecord, { kind: 'baseline_reused' }>
|
|
2451
|
-
}): RunState => {
|
|
2452
|
-
if (record.ticket === null) return state
|
|
2453
|
-
const { from_ticket, base_sha } = record.content
|
|
2454
|
-
const baseline = state.tickets[from_ticket]?.baseline ?? null
|
|
2455
|
-
if (baseline === null) return state
|
|
2456
|
-
const progress = state.tickets[record.ticket] ?? EMPTY_TICKET_PROGRESS
|
|
2457
|
-
return {
|
|
2458
|
-
...state,
|
|
2459
|
-
tickets: {
|
|
2460
|
-
...state.tickets,
|
|
2461
|
-
[record.ticket]: { ...progress, baseline, baseline_sha: base_sha },
|
|
2462
|
-
},
|
|
2463
|
-
}
|
|
2464
|
-
}
|
|
2465
|
-
|
|
2466
479
|
/**
|
|
2467
480
|
* Rebuilds a run's state purely from its journal records, in order.
|
|
2468
481
|
*
|