@alecsibilia/luca 14.0.0-alpha.2 → 14.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +76 -0
- package/README.md +7 -2
- package/board/client/board-look.ts +31 -1
- package/board/client/board-panel-parts.tsx +150 -0
- package/board/client/board-panel-stages.ts +99 -0
- package/board/client/board-panel-styles.ts +223 -0
- package/board/client/board-panel.tsx +77 -1275
- package/board/client/engine-life.ts +48 -0
- package/board/client/final-review-stack.tsx +144 -0
- package/board/client/heartbeat.tsx +61 -0
- package/board/client/helper-lines.tsx +69 -0
- package/board/client/live-feed.tsx +86 -0
- package/board/client/live-line.ts +73 -0
- package/board/client/model-line.tsx +178 -0
- package/board/client/needs-you-section.tsx +133 -0
- package/board/client/one-line.tsx +193 -0
- package/board/client/repo-wait-banner.tsx +69 -0
- package/board/client/run-footer.tsx +68 -0
- package/board/client/run-header.tsx +189 -0
- package/board/client/run-picker.tsx +98 -0
- package/board/client/status-bar.tsx +131 -0
- package/board/client/step-dots.tsx +127 -0
- package/board/client/stuck-actions.tsx +269 -0
- package/board/client/ticket-card-view.tsx +238 -0
- package/board/client/ticket-detail.tsx +87 -0
- package/board/client/ticket-stack.tsx +119 -0
- package/board/client/ticket-wait.ts +188 -0
- package/board/client/timeline-rows.tsx +41 -14
- package/board/client/usage-line.tsx +51 -0
- package/board/client/wait-line.tsx +39 -0
- package/board/index.client.tsx +1 -1
- package/board/index.server.ts +7 -1
- package/board/package.json +1 -1
- package/board/server/board-panel-state-helpers.ts +67 -0
- package/board/server/board-server-check.ts +294 -0
- package/board/server/board-server-engine.ts +129 -0
- package/board/server/board-server-events.ts +134 -0
- package/board/server/board-server-read.ts +190 -0
- package/board/server/board-server-resume.ts +164 -0
- package/board/server/board-server-start.ts +174 -0
- package/board/server/board-server-store.ts +281 -0
- package/board/server/board-server-types.ts +114 -0
- package/board/server/board-server-util.ts +4 -0
- package/board/server/board-server.ts +52 -943
- package/board/server/board-vocabulary-engine.ts +144 -0
- package/board/server/board-vocabulary-review.ts +115 -0
- package/board/server/board-vocabulary-run.ts +145 -0
- package/board/server/board-vocabulary-shapes.ts +153 -0
- package/board/server/board-vocabulary.ts +20 -375
- package/board/server/current-step-panel-helpers.ts +108 -0
- package/board/server/describe-record.ts +403 -0
- package/board/server/engine-launch.ts +20 -3
- package/board/server/engine-restart-helpers.ts +105 -0
- package/board/server/engine-watch.ts +45 -5
- package/board/server/journal-rebuild-helpers.ts +169 -0
- package/board/server/live-file.ts +130 -0
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +14 -629
- package/board/server/record-texts.ts +350 -0
- package/board/server/reduce-board-agent-start.ts +224 -0
- package/board/server/reduce-board-agents.ts +160 -0
- package/board/server/reduce-board-counters.ts +138 -0
- package/board/server/reduce-board-create.ts +148 -0
- package/board/server/reduce-board-final-review.ts +174 -0
- package/board/server/reduce-board-needs-you.ts +283 -0
- package/board/server/reduce-board-reasons.ts +59 -0
- package/board/server/reduce-board-run.ts +255 -0
- package/board/server/reduce-board-settle.ts +88 -0
- package/board/server/reduce-board-steps.ts +226 -0
- package/board/server/reduce-board-text.ts +139 -0
- package/board/server/reduce-board-ticket-checks.ts +168 -0
- package/board/server/reduce-board-ticket-join.ts +144 -0
- package/board/server/reduce-board-ticket-start.ts +132 -0
- package/board/server/reduce-board-update.ts +117 -0
- package/board/server/reduce-board-usage.ts +247 -0
- package/board/server/reduce-board.ts +100 -1917
- package/board/server/reply-post.ts +407 -0
- package/board/server/run-registry.ts +8 -4
- package/board/server/ticket-stages-panel-helpers.ts +296 -0
- package/board/shared/board-rows.ts +9 -0
- package/board/shared/board-rpc.ts +42 -0
- package/board/shared/board-state-counts.ts +77 -0
- package/board/shared/board-state-live.ts +99 -0
- package/board/shared/board-state-review.ts +59 -0
- package/board/shared/board-state-run.ts +236 -0
- package/board/shared/board-state-ticket.ts +257 -0
- package/board/shared/board-state-usage.ts +107 -0
- package/board/shared/board-state.ts +97 -567
- package/board/shared/reply-actions.ts +207 -0
- package/engine/agents/agent-launcher.ts +27 -4
- package/engine/agents/claude-launcher-helpers.ts +233 -0
- package/engine/agents/claude-launcher.ts +28 -669
- package/engine/agents/claude-messages.ts +212 -0
- package/engine/agents/claude-options.ts +43 -8
- package/engine/agents/common-instructions.ts +138 -0
- package/engine/agents/input-channel.ts +36 -0
- package/engine/agents/launch-paths.ts +29 -0
- package/engine/agents/launch-session.ts +227 -0
- package/engine/agents/learner-instructions.ts +31 -0
- package/engine/agents/open-session.ts +210 -0
- package/engine/agents/prompt-sections.ts +95 -0
- package/engine/agents/rejoin-prompts.ts +135 -0
- package/engine/agents/reviewer-instructions.ts +99 -0
- package/engine/agents/role-instructions.ts +31 -244
- package/engine/agents/role-prompts.ts +39 -109
- package/engine/agents/role-results.ts +106 -7
- package/engine/agents/scripted-launcher.ts +21 -6
- package/engine/agents/session-pool.ts +124 -0
- package/engine/agents/turn-progress.ts +283 -0
- package/engine/agents/writer-instructions.ts +151 -0
- package/engine/cli/computer-adapters-real.ts +15 -3
- package/engine/cli/computer-adapters.ts +2 -0
- package/engine/cli/computer-checks.ts +87 -4
- package/engine/cli/config-reload-text.ts +39 -0
- package/engine/cli/decision-model-setup.ts +70 -0
- package/engine/cli/doctor-command.ts +1 -0
- package/engine/cli/doctor.ts +21 -3
- package/engine/cli/going-runs.ts +192 -36
- package/engine/cli/init-command.ts +1 -0
- package/engine/cli/init.ts +23 -1
- package/engine/cli/live-runs.ts +73 -0
- package/engine/cli/luca-run.ts +42 -7
- package/engine/cli/luca-skills.ts +183 -0
- package/engine/cli/luca.ts +8 -1
- package/engine/cli/models-command.ts +119 -0
- package/engine/cli/read-run.ts +81 -0
- package/engine/cli/run-args.ts +33 -6
- package/engine/cli/run-demo.ts +143 -0
- package/engine/cli/run-end.ts +184 -0
- package/engine/cli/run-modes-helpers.ts +81 -0
- package/engine/cli/run-modes.ts +62 -439
- package/engine/cli/setup.ts +5 -48
- package/engine/cli/unfinished-runs.ts +122 -0
- package/engine/cli/upgrade-command.ts +212 -30
- package/engine/cli/upgrade-finish.ts +149 -0
- package/engine/cli/upgrade.ts +112 -71
- package/engine/config/config-fields.ts +127 -0
- package/engine/config/config-reload.ts +250 -0
- package/engine/config/engine-config-schema.ts +235 -0
- package/engine/config/engine-config.ts +72 -142
- package/engine/config/test-commands.ts +102 -0
- package/engine/core/agent-guards-helpers.ts +84 -0
- package/engine/core/agent-turn.ts +395 -0
- package/engine/core/already-done.ts +44 -1
- package/engine/core/build-action.ts +214 -0
- package/engine/core/build-context.ts +135 -0
- package/engine/core/build-launch.ts +259 -0
- package/engine/core/commit-in.ts +282 -0
- package/engine/core/decide-build-early.ts +108 -0
- package/engine/core/decide-build-ending.ts +145 -0
- package/engine/core/decide-build-helpers.ts +87 -0
- package/engine/core/decide-build-tickets.ts +207 -0
- package/engine/core/decide-build-view.ts +103 -0
- package/engine/core/decide-build.ts +101 -1368
- package/engine/core/decide-final-fix.ts +203 -0
- package/engine/core/decide-final-review.ts +55 -310
- package/engine/core/decide-red-green.ts +398 -0
- package/engine/core/decide-rejoin.ts +259 -0
- package/engine/core/decide-repo-wait.ts +129 -0
- package/engine/core/decide-run-budget.ts +86 -12
- package/engine/core/decide-stuck-helpers.ts +127 -0
- package/engine/core/decide-stuck.ts +60 -8
- package/engine/core/decide-ticket-review.ts +95 -0
- package/engine/core/decide-ticket.ts +267 -0
- package/engine/core/decide-waits.ts +47 -0
- package/engine/core/decide.ts +74 -23
- package/engine/core/engine-chores.ts +163 -0
- package/engine/core/environment-rerun.ts +83 -0
- package/engine/core/evidence-text.ts +84 -0
- package/engine/core/execute-action.ts +303 -0
- package/engine/core/execute-already-done-check.ts +133 -0
- package/engine/core/execute-build.ts +68 -944
- package/engine/core/execute-changeset.ts +5 -7
- package/engine/core/execute-final-review.ts +125 -13
- package/engine/core/execute-gates.ts +182 -0
- package/engine/core/execute-intake.ts +130 -0
- package/engine/core/execute-join.ts +276 -0
- package/engine/core/execute-memory.ts +12 -0
- package/engine/core/execute-pull-request.ts +84 -0
- package/engine/core/execute-red-check.ts +219 -0
- package/engine/core/execute-repo-wait.ts +129 -0
- package/engine/core/execute-stuck.ts +25 -10
- package/engine/core/execute-waits.ts +158 -0
- package/engine/core/execute-with-jev.ts +118 -0
- package/engine/core/execute.ts +124 -831
- package/engine/core/final-failure-cap.ts +38 -0
- package/engine/core/final-review-action.ts +125 -0
- package/engine/core/final-review-text.ts +63 -17
- package/engine/core/final-stuck-text.ts +86 -0
- package/engine/core/fix-loop-text.ts +49 -2
- package/engine/core/left-out.ts +120 -0
- package/engine/core/live-file.ts +372 -0
- package/engine/core/loop-caps.ts +25 -2
- package/engine/core/not-in-run.ts +7 -3
- package/engine/core/pull-request-text.ts +255 -6
- package/engine/core/repo-wait-text.ts +81 -0
- package/engine/core/review-text.ts +55 -1
- package/engine/core/run-crash-recovery-helpers.ts +16 -0
- package/engine/core/run-session-close-helpers.ts +96 -0
- package/engine/core/run-start.ts +148 -0
- package/engine/core/run-stuck-text.ts +86 -0
- package/engine/core/schedule-rules.ts +193 -0
- package/engine/core/stuck-detail-text.ts +89 -0
- package/engine/core/stuck-format.ts +22 -0
- package/engine/core/stuck-text.ts +29 -332
- package/engine/core/ticket-stuck-text.ts +192 -0
- package/engine/core/worktree-changes.ts +106 -0
- package/engine/gates/already-done-check.ts +120 -0
- package/engine/gates/feature-map.ts +96 -0
- package/engine/gates/gate-runner.ts +108 -108
- package/engine/gates/gate-schemas.ts +61 -2
- package/engine/gates/leftover-scan.ts +154 -31
- package/engine/gates/new-tests.ts +45 -0
- package/engine/gates/own-files-check.ts +98 -0
- package/engine/gates/prepare-check.ts +60 -0
- package/engine/gates/prepare-slots.ts +99 -0
- package/engine/gates/red-check.ts +184 -16
- package/engine/gates/shell-check.ts +64 -0
- package/engine/gates/slot-waits.ts +55 -0
- package/engine/gates/slow-test-runner.ts +59 -0
- package/engine/gates/test-checks.ts +161 -0
- package/engine/gates/test-runner.ts +164 -5
- package/engine/gates/test-slots.ts +48 -0
- package/engine/git/git-adapter.ts +55 -396
- package/engine/git/git-commits.ts +132 -0
- package/engine/git/git-join.ts +139 -0
- package/engine/git/git-read.ts +123 -0
- package/engine/git/git-run.ts +32 -0
- package/engine/git/git-worktrees.ts +143 -0
- package/engine/git/outside-links.ts +55 -0
- package/engine/guards/after-turn-check.ts +31 -5
- package/engine/guards/guard-hook.ts +60 -12
- package/engine/guards/guard-roles.ts +68 -0
- package/engine/guards/helper-rules.ts +92 -0
- package/engine/guards/permission-rules.ts +161 -0
- package/engine/guards/role-rules-helpers.ts +48 -0
- package/engine/guards/role-rules.ts +34 -606
- package/engine/guards/shell-rules.ts +344 -0
- package/engine/guards/worktree-git.ts +208 -0
- package/engine/guards/worktree-snapshot.ts +412 -0
- package/engine/guards/worktree-state.ts +28 -825
- package/engine/guards/worktree-undo.ts +260 -0
- package/engine/guards/write-rules.ts +149 -0
- package/engine/index.ts +30 -9
- package/engine/intake/criteria-checks.ts +114 -0
- package/engine/intake/intake-checks.ts +164 -30
- package/engine/intake/intake-schemas.ts +37 -2
- package/engine/jev/clef-client.ts +259 -0
- package/engine/jev/decision-model-credentials.ts +212 -0
- package/engine/jev/jev-jobs.ts +144 -60
- package/engine/jev/jev-schemas.ts +86 -5
- package/engine/jev/jev-shadow-helpers.ts +218 -0
- package/engine/jev/jev-shadow.ts +109 -64
- package/engine/jev/model-pick.ts +275 -0
- package/engine/journal/agent-entries.ts +167 -0
- package/engine/journal/agent-message-entries.ts +72 -0
- package/engine/journal/agent-turn-replay.ts +203 -0
- package/engine/journal/already-done-entries.ts +55 -0
- package/engine/journal/commit-entries.ts +56 -0
- package/engine/journal/cross-ticket-replay.ts +222 -0
- package/engine/journal/decision-model-entries.ts +86 -0
- package/engine/journal/entry-fields.ts +10 -0
- package/engine/journal/final-review-entries.ts +128 -0
- package/engine/journal/final-review-replay.ts +324 -0
- package/engine/journal/final-review-round.ts +320 -0
- package/engine/journal/final-review-state.ts +180 -0
- package/engine/journal/gate-entries.ts +81 -0
- package/engine/journal/intake-entries.ts +87 -0
- package/engine/journal/join-entries.ts +126 -0
- package/engine/journal/journal-entry-schema.ts +218 -0
- package/engine/journal/journal-kinds.ts +97 -0
- package/engine/journal/journal-record-schema.ts +218 -0
- package/engine/journal/journal-record.ts +43 -1400
- package/engine/journal/journal.ts +6 -1
- package/engine/journal/limit-entries.ts +90 -0
- package/engine/journal/memory-entries.ts +124 -0
- package/engine/journal/memory-replay.ts +157 -0
- package/engine/journal/plan-replay.ts +139 -0
- package/engine/journal/pull-request-entries.ts +41 -0
- package/engine/journal/replay.ts +157 -2144
- package/engine/journal/reply-replay.ts +144 -0
- package/engine/journal/repo-wait-entries.ts +53 -0
- package/engine/journal/run-entries.ts +172 -0
- package/engine/journal/run-state.ts +264 -0
- package/engine/journal/shared-failure-replay.ts +133 -0
- package/engine/journal/stuck-entries.ts +193 -0
- package/engine/journal/ticket-progress.ts +392 -0
- package/engine/journal/ticket-replay.ts +411 -0
- package/engine/journal/ticket-result-replay.ts +364 -0
- package/engine/journal/ticket-retry-replay.ts +142 -0
- package/engine/journal/worktree-entries.ts +122 -0
- package/engine/models/model-score.ts +512 -0
- package/engine/shell/run-command.ts +187 -18
- package/engine/testing/agent-turn-fixtures.ts +287 -0
- package/engine/testing/already-done-fixtures.ts +97 -0
- package/engine/testing/build-fixtures.ts +68 -973
- package/engine/testing/checkouts-fixtures.ts +48 -0
- package/engine/testing/final-review-fixtures.ts +62 -1
- package/engine/testing/final-review-steps.ts +143 -0
- package/engine/testing/limit-fixtures.ts +85 -0
- package/engine/testing/live-file-fixtures.ts +57 -0
- package/engine/testing/practice-repo.ts +10 -4
- package/engine/testing/practice-run.ts +1 -1
- package/engine/testing/reply-fixtures.ts +88 -0
- package/engine/testing/run-budget-fixtures.ts +165 -0
- package/engine/testing/run-fixtures.ts +219 -0
- package/engine/testing/sdk-play.ts +177 -0
- package/engine/testing/setup-repo.ts +194 -0
- package/engine/testing/step-record-fixtures.ts +55 -0
- package/engine/testing/ticket-step-fixtures.ts +272 -0
- package/engine/testing/wait-scenarios.ts +186 -0
- package/engine/tracker/gh-retry.ts +217 -0
- package/engine/tracker/github-tracker.ts +310 -90
- package/engine/tracker/in-memory-tracker.ts +8 -4
- package/engine/tracker/tracker.ts +131 -6
- package/package.json +2 -1
- package/skills/luca-retro/SKILL.md +74 -0
- package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
- package/skills/luca-unstick/SKILL.md +153 -0
- package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
- package/engine/jev/jev-client.ts +0 -173
|
@@ -1,1060 +1,27 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from './
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from './
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
replySteps,
|
|
17
|
-
skipDependent,
|
|
18
|
-
stuckTicketSteps,
|
|
19
|
-
type StuckAction,
|
|
20
|
-
} from './decide-stuck'
|
|
21
|
-
import {
|
|
22
|
-
clashFixMessage,
|
|
23
|
-
CRASH_SECTION,
|
|
24
|
-
failedChecks,
|
|
25
|
-
failedTryMessage,
|
|
26
|
-
followUpSection,
|
|
27
|
-
gateFixMessage,
|
|
28
|
-
redFixMessage,
|
|
29
|
-
} from './fix-loop-text'
|
|
30
|
-
import {
|
|
31
|
-
MAX_BAD_TEST_BOUNCES,
|
|
32
|
-
MAX_ENGINE_FAILURES,
|
|
33
|
-
MAX_FIX_ROUNDS,
|
|
34
|
-
MAX_REJOINS,
|
|
35
|
-
} from './loop-caps'
|
|
36
|
-
import { pullRequestText } from './pull-request-text'
|
|
37
|
-
import {
|
|
38
|
-
openFindingsText,
|
|
39
|
-
reviewFixMessage,
|
|
40
|
-
reviewFixSection,
|
|
41
|
-
reviewSections,
|
|
42
|
-
} from './review-text'
|
|
43
|
-
import { retrySection, setupChangeDetail } from './stuck-text'
|
|
44
|
-
|
|
45
|
-
import {
|
|
46
|
-
prepareSection,
|
|
47
|
-
rejoinSection,
|
|
48
|
-
rolePrompt,
|
|
49
|
-
} from '../agents/role-prompts'
|
|
50
|
-
import type { AgentRole, CriterionTests } from '../agents/role-results'
|
|
51
|
-
import { endedText } from '../gates/gate-schemas'
|
|
52
|
-
import type { TicketSnapshot } from '../intake/intake-schemas'
|
|
53
|
-
import type {
|
|
54
|
-
ChangesetBump,
|
|
55
|
-
CommitStage,
|
|
56
|
-
GateTarget,
|
|
57
|
-
RejoinCause,
|
|
58
|
-
StuckReason,
|
|
59
|
-
} from '../journal/journal-record'
|
|
60
|
-
import {
|
|
61
|
-
EMPTY_TICKET_PROGRESS,
|
|
62
|
-
type ReplayedInstall,
|
|
63
|
-
type ReplayedRunNote,
|
|
64
|
-
type ReplayedSnapshot,
|
|
65
|
-
type ReplayedWorktree,
|
|
66
|
-
type RunState,
|
|
67
|
-
type TicketProgress,
|
|
68
|
-
} from '../journal/replay'
|
|
69
|
-
import { REFACTOR_LABEL } from '../tracker/tracker'
|
|
70
|
-
|
|
1
|
+
import type { BuildAction } from './build-action'
|
|
2
|
+
import { withChangesetsSections, withWriterSection } from './build-launch'
|
|
3
|
+
import { haltSteps, installSteps } from './decide-build-early'
|
|
4
|
+
import { finishSteps } from './decide-build-ending'
|
|
5
|
+
import { ticketSteps, type TicketWait } from './decide-build-tickets'
|
|
6
|
+
import { buildViewOf } from './decide-build-view'
|
|
7
|
+
import { replySteps } from './decide-stuck'
|
|
8
|
+
|
|
9
|
+
import { featureMapSection, prepareSection } from '../agents/role-prompts'
|
|
10
|
+
import { featureMapPath } from '../config/engine-config'
|
|
11
|
+
import type { RunState } from '../journal/replay'
|
|
12
|
+
|
|
13
|
+
export { isRefactorTicket, type BuildAction } from './build-action'
|
|
14
|
+
export { MAX_RUN_NOTES, mayEditTests, newestRunNotes } from './build-launch'
|
|
15
|
+
export { EMPTY_TEST_REASON } from './decide-red-green'
|
|
71
16
|
export {
|
|
72
17
|
MAX_BAD_TEST_BOUNCES,
|
|
73
18
|
MAX_ENGINE_FAILURES,
|
|
74
19
|
MAX_FIX_ROUNDS,
|
|
75
20
|
MAX_REJOINS,
|
|
21
|
+
MAX_TEST_UPDATES,
|
|
22
|
+
MAX_TESTS_PER_CRITERION,
|
|
76
23
|
} from './loop-caps'
|
|
77
24
|
|
|
78
|
-
/**
|
|
79
|
-
* How many run notes a fresh agent is handed: the newest ones in the run,
|
|
80
|
-
* whatever ticket or role wrote them.
|
|
81
|
-
*/
|
|
82
|
-
export const MAX_RUN_NOTES = 10
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* The run notes a fresh agent gets: the `MAX_RUN_NOTES` newest, oldest
|
|
86
|
-
* first. The same text written twice counts once, where it was last written.
|
|
87
|
-
*
|
|
88
|
-
* @example
|
|
89
|
-
* newestRunNotes({ run_notes: state.run_notes }) // at most 10, oldest first
|
|
90
|
-
*/
|
|
91
|
-
export const newestRunNotes = ({
|
|
92
|
-
run_notes,
|
|
93
|
-
}: {
|
|
94
|
-
run_notes: ReplayedRunNote[]
|
|
95
|
-
}): ReplayedRunNote[] => {
|
|
96
|
-
const seen = new Set<string>()
|
|
97
|
-
const newest: ReplayedRunNote[] = []
|
|
98
|
-
for (const entry of [...run_notes].reverse()) {
|
|
99
|
-
if (newest.length === MAX_RUN_NOTES) break
|
|
100
|
-
if (seen.has(entry.note)) continue
|
|
101
|
-
seen.add(entry.note)
|
|
102
|
-
newest.push(entry)
|
|
103
|
-
}
|
|
104
|
-
return newest.reverse()
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** The next build step for a run whose intake passed. */
|
|
108
|
-
export type BuildAction =
|
|
109
|
-
/** Make the run branch, in its own worktree, from the base branch. */
|
|
110
|
-
| { type: 'create_run_branch'; spec_number: number; base_branch: string }
|
|
111
|
-
/** Make the ticket's worktree on a new branch from the run branch. */
|
|
112
|
-
| { type: 'create_ticket_worktree'; ticket: number; run_branch: string }
|
|
113
|
-
/**
|
|
114
|
-
* Install the dependencies from the lockfile, without changing it, in a
|
|
115
|
-
* new ticket worktree or the run branch's checkout (`ticket` is `null`),
|
|
116
|
-
* before any agent or gate runs there.
|
|
117
|
-
*/
|
|
118
|
-
| {
|
|
119
|
-
type: 'install_dependencies'
|
|
120
|
-
target: GateTarget
|
|
121
|
-
ticket: number | null
|
|
122
|
-
}
|
|
123
|
-
/** Run the tests before any agent works, to know the old tests. */
|
|
124
|
-
| { type: 'run_baseline_tests'; ticket: number }
|
|
125
|
-
/**
|
|
126
|
-
* Take `from_ticket`'s baseline instead of running the tests again: both
|
|
127
|
-
* worktrees start from the same run-branch commit, `base_sha`.
|
|
128
|
-
*/
|
|
129
|
-
| {
|
|
130
|
-
type: 'reuse_baseline_tests'
|
|
131
|
-
ticket: number
|
|
132
|
-
from_ticket: number
|
|
133
|
-
base_sha: string
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Start a fresh agent session in the ticket's worktree with this prompt.
|
|
137
|
-
* `may_edit_tests` is true for the test-writer and for a refactor
|
|
138
|
-
* ticket's implementer; the launcher's guards enforce it.
|
|
139
|
-
*/
|
|
140
|
-
| {
|
|
141
|
-
type: 'launch_agent'
|
|
142
|
-
ticket: number
|
|
143
|
-
role: AgentRole
|
|
144
|
-
prompt: string
|
|
145
|
-
may_edit_tests: boolean
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Send a follow-up message, such as a failed check's output, to the agent
|
|
149
|
-
* session that is still open, in a fix loop.
|
|
150
|
-
*/
|
|
151
|
-
| {
|
|
152
|
-
type: 'follow_up_agent'
|
|
153
|
-
ticket: number
|
|
154
|
-
role: AgentRole
|
|
155
|
-
session_id: string
|
|
156
|
-
message: string
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Throw away every uncommitted change in the ticket's worktree, so a
|
|
160
|
-
* fresh test-writer starts clean after a bad test.
|
|
161
|
-
*/
|
|
162
|
-
| { type: 'reset_ticket_worktree'; ticket: number }
|
|
163
|
-
/** Prove every criterion has a test, new tests fail, old tests pass. */
|
|
164
|
-
| {
|
|
165
|
-
type: 'run_red_check'
|
|
166
|
-
ticket: number
|
|
167
|
-
criteria_ids: string[]
|
|
168
|
-
mapping: CriterionTests[]
|
|
169
|
-
}
|
|
170
|
-
/** Scan for leftovers, then commit everything in the worktree. */
|
|
171
|
-
| {
|
|
172
|
-
type: 'commit_ticket'
|
|
173
|
-
ticket: number
|
|
174
|
-
stage: CommitStage
|
|
175
|
-
message: string
|
|
176
|
-
}
|
|
177
|
-
/** Run the engine config's gates in the ticket worktree or on the run branch. */
|
|
178
|
-
| { type: 'run_gates'; ticket: number; target: GateTarget }
|
|
179
|
-
/** Replay the approved ticket's commits onto the run branch. */
|
|
180
|
-
| { type: 'join_run_branch'; ticket: number }
|
|
181
|
-
/**
|
|
182
|
-
* Put a joined ticket's whole change back on top of the run branch, as
|
|
183
|
-
* uncommitted changes in its worktree, after a clash or failed gates
|
|
184
|
-
* after joining. With `undo_first_sha`, first undo the join on the run
|
|
185
|
-
* branch, back to before that commit.
|
|
186
|
-
*/
|
|
187
|
-
| {
|
|
188
|
-
type: 'rebase_ticket'
|
|
189
|
-
ticket: number
|
|
190
|
-
cause: RejoinCause
|
|
191
|
-
undo_first_sha: string | null
|
|
192
|
-
}
|
|
193
|
-
/** Remove these git worktrees at the end of the run; branches stay. */
|
|
194
|
-
| { type: 'remove_worktrees'; paths: string[] }
|
|
195
|
-
/** No ticket can move, and not every ticket has joined. */
|
|
196
|
-
| { type: 'invalid_journal'; reason: string }
|
|
197
|
-
/** Push the run branch to `origin`. */
|
|
198
|
-
| { type: 'push_run_branch'; ticket: number; branch: string }
|
|
199
|
-
/** The engine can't safely pick this ticket's next step by itself. */
|
|
200
|
-
| {
|
|
201
|
-
type: 'mark_stuck'
|
|
202
|
-
ticket: number
|
|
203
|
-
reason: StuckReason
|
|
204
|
-
detail: string
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* In a repo with changesets, commit the run's one changeset to the run
|
|
208
|
-
* branch and push it, before the PR (#461).
|
|
209
|
-
*/
|
|
210
|
-
| {
|
|
211
|
-
type: 'write_changeset'
|
|
212
|
-
branch: string
|
|
213
|
-
bump: ChangesetBump
|
|
214
|
-
summary: string
|
|
215
|
-
message: string
|
|
216
|
-
}
|
|
217
|
-
/** Open the run's one pull request from the run branch. */
|
|
218
|
-
| {
|
|
219
|
-
type: 'open_pull_request'
|
|
220
|
-
head: string
|
|
221
|
-
base: string
|
|
222
|
-
title: string
|
|
223
|
-
body: string
|
|
224
|
-
}
|
|
225
|
-
/** Every ticket joined (or was skipped) and the PR is open. */
|
|
226
|
-
| {
|
|
227
|
-
type: 'done'
|
|
228
|
-
outcome: 'pr_opened'
|
|
229
|
-
pull_request: { number: number; url: string }
|
|
230
|
-
}
|
|
231
|
-
/** The final review of the whole run branch (`decide-final-review.ts`). */
|
|
232
|
-
| FinalReviewAction
|
|
233
|
-
/** The owner replied `stop`: the run ends without a PR; its branch stays. */
|
|
234
|
-
| { type: 'done'; outcome: 'stopped_by_user' }
|
|
235
|
-
/** Every ticket was skipped, so there is nothing to open a PR for. */
|
|
236
|
-
| { type: 'done'; outcome: 'all_skipped' }
|
|
237
|
-
/** Telling the owner about stuck work, and acting on their replies. */
|
|
238
|
-
| StuckAction
|
|
239
|
-
/** A ticket whose work was already on the base branch (#484). */
|
|
240
|
-
| AlreadyDoneAction
|
|
241
|
-
|
|
242
|
-
/** Whether a ticket is a refactor ticket: it skips the test-writer and red check. */
|
|
243
|
-
export const isRefactorTicket = ({
|
|
244
|
-
ticket,
|
|
245
|
-
}: {
|
|
246
|
-
ticket: TicketSnapshot
|
|
247
|
-
}): boolean => ticket.labels.includes(REFACTOR_LABEL)
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* A red commit after a bad-test bounce says so, so the history shows it. A
|
|
251
|
-
* refactor ticket's one commit says it is a refactor.
|
|
252
|
-
*/
|
|
253
|
-
const commitMessage = ({
|
|
254
|
-
stage,
|
|
255
|
-
ticket,
|
|
256
|
-
progress,
|
|
257
|
-
}: {
|
|
258
|
-
stage: CommitStage
|
|
259
|
-
ticket: TicketSnapshot
|
|
260
|
-
progress: TicketProgress
|
|
261
|
-
}): string => {
|
|
262
|
-
if (stage === 'fix') {
|
|
263
|
-
return `fix: review round ${progress.review_fix?.round ?? progress.review_rounds} for #${ticket.number} ${ticket.title}`
|
|
264
|
-
}
|
|
265
|
-
if (stage === 'green') {
|
|
266
|
-
if (progress.rejoins > 0) {
|
|
267
|
-
return `fix: rejoin #${ticket.number} ${ticket.title} onto the run branch`
|
|
268
|
-
}
|
|
269
|
-
return isRefactorTicket({ ticket })
|
|
270
|
-
? `refactor: #${ticket.number} ${ticket.title}`
|
|
271
|
-
: `feat: build #${ticket.number} ${ticket.title}`
|
|
272
|
-
}
|
|
273
|
-
return progress.bad_test_bounces > 0
|
|
274
|
-
? `test: replace a bad test for #${ticket.number} ${ticket.title}`
|
|
275
|
-
: `test: add failing tests for #${ticket.number} ${ticket.title}`
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Whether an agent of this role may edit test files on this ticket: only
|
|
280
|
-
* the test-writer, and a refactor ticket's implementer (who may follow
|
|
281
|
-
* renames into tests). The engine hands it to the launcher's guards and
|
|
282
|
-
* uses it for its own after-turn check, on launches and follow-ups alike.
|
|
283
|
-
*
|
|
284
|
-
* @example
|
|
285
|
-
* mayEditTests({ role: 'implementer', ticket }) // true only on a refactor ticket
|
|
286
|
-
*/
|
|
287
|
-
export const mayEditTests = ({
|
|
288
|
-
role,
|
|
289
|
-
ticket,
|
|
290
|
-
}: {
|
|
291
|
-
role: AgentRole
|
|
292
|
-
ticket: TicketSnapshot
|
|
293
|
-
}): boolean =>
|
|
294
|
-
role === 'test-writer' ||
|
|
295
|
-
(role === 'implementer' && isRefactorTicket({ ticket }))
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* The sections a launch adds: the review's (see `reviewPromptSections`),
|
|
299
|
-
* and, for a test-writer or implementer fixing a ticket on top of the run
|
|
300
|
-
* branch, the files that clashed. The reviewer's re-review after a rebase
|
|
301
|
-
* is the review's own. The first agent after a `retry` resumed a stuck
|
|
302
|
-
* ticket is also told why it got stuck, and the agent taking a turn a
|
|
303
|
-
* crash cut off is told of the crash.
|
|
304
|
-
*/
|
|
305
|
-
const promptSections = ({
|
|
306
|
-
role,
|
|
307
|
-
progress,
|
|
308
|
-
}: {
|
|
309
|
-
role: AgentRole
|
|
310
|
-
progress: TicketProgress
|
|
311
|
-
}): string[] => {
|
|
312
|
-
const sections = buildPromptSections({ role, progress })
|
|
313
|
-
const { retried, crashed_turn } = progress
|
|
314
|
-
return [
|
|
315
|
-
...sections,
|
|
316
|
-
...(retried === null ? [] : [retrySection({ retried })]),
|
|
317
|
-
...(crashed_turn === role ? [CRASH_SECTION] : []),
|
|
318
|
-
]
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const buildPromptSections = ({
|
|
322
|
-
role,
|
|
323
|
-
progress,
|
|
324
|
-
}: {
|
|
325
|
-
role: AgentRole
|
|
326
|
-
progress: TicketProgress
|
|
327
|
-
}): string[] => {
|
|
328
|
-
const review = reviewPromptSections({ role, progress })
|
|
329
|
-
const { rejoin } = progress
|
|
330
|
-
if (rejoin === null || role === 'ticket-reviewer') return review
|
|
331
|
-
// A review fix round after the rejoin has its own findings.
|
|
332
|
-
if (progress.commits.green !== null) return review
|
|
333
|
-
const section = rejoinSection({ role, rejoin })
|
|
334
|
-
return section === null ? review : [...review, section]
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* The sections a launch adds for the ticket review: the reviewer's diff and
|
|
339
|
-
* gate results (a re-review's new changes and earlier findings), or a
|
|
340
|
-
* review fixer's findings while a review fix round is open.
|
|
341
|
-
*/
|
|
342
|
-
const reviewPromptSections = ({
|
|
343
|
-
role,
|
|
344
|
-
progress,
|
|
345
|
-
}: {
|
|
346
|
-
role: AgentRole
|
|
347
|
-
progress: TicketProgress
|
|
348
|
-
}): string[] => {
|
|
349
|
-
const fix = progress.review_fix
|
|
350
|
-
if (role === 'ticket-reviewer') {
|
|
351
|
-
return reviewSections({
|
|
352
|
-
progress,
|
|
353
|
-
base_sha: progress.worktree?.base_sha ?? 'HEAD',
|
|
354
|
-
})
|
|
355
|
-
}
|
|
356
|
-
if (fix === null || progress.commits.green === null) return []
|
|
357
|
-
if (role === 'test-writer' && !fix.tests_answered) {
|
|
358
|
-
return [reviewFixSection({ fix, kind: 'test' })]
|
|
359
|
-
}
|
|
360
|
-
if (role === 'implementer' && !fix.code_answered) {
|
|
361
|
-
return [reviewFixSection({ fix, kind: 'code' })]
|
|
362
|
-
}
|
|
363
|
-
return []
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
type StepArgs = {
|
|
367
|
-
snapshot: ReplayedSnapshot
|
|
368
|
-
ticket: TicketSnapshot
|
|
369
|
-
progress: TicketProgress
|
|
370
|
-
/** The run notes a fresh agent gets (`newestRunNotes`). */
|
|
371
|
-
run_notes: ReplayedRunNote[]
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* A fresh agent session, handed the run's newest notes. With `follow_up`,
|
|
376
|
-
* it stands in for a follow-up whose session is gone (a crash cut it off),
|
|
377
|
-
* and its prompt carries the follow-up message.
|
|
378
|
-
*/
|
|
379
|
-
const launch = ({
|
|
380
|
-
role,
|
|
381
|
-
snapshot,
|
|
382
|
-
ticket,
|
|
383
|
-
progress,
|
|
384
|
-
run_notes,
|
|
385
|
-
follow_up,
|
|
386
|
-
}: StepArgs & { role: AgentRole; follow_up?: string }): BuildAction => ({
|
|
387
|
-
type: 'launch_agent',
|
|
388
|
-
ticket: ticket.number,
|
|
389
|
-
role,
|
|
390
|
-
prompt: rolePrompt({
|
|
391
|
-
role,
|
|
392
|
-
spec: snapshot.spec,
|
|
393
|
-
ticket,
|
|
394
|
-
refactor: isRefactorTicket({ ticket }),
|
|
395
|
-
bad_test: progress.bad_test,
|
|
396
|
-
sections: [
|
|
397
|
-
...promptSections({ role, progress }),
|
|
398
|
-
...(follow_up === undefined
|
|
399
|
-
? []
|
|
400
|
-
: [followUpSection({ message: follow_up })]),
|
|
401
|
-
],
|
|
402
|
-
run_notes,
|
|
403
|
-
}),
|
|
404
|
-
may_edit_tests: mayEditTests({ role, ticket }),
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
const stuck = ({
|
|
408
|
-
ticket,
|
|
409
|
-
reason,
|
|
410
|
-
detail,
|
|
411
|
-
}: {
|
|
412
|
-
ticket: number
|
|
413
|
-
reason: StuckReason
|
|
414
|
-
detail: string
|
|
415
|
-
}): BuildAction => ({ type: 'mark_stuck', ticket, reason, detail })
|
|
416
|
-
|
|
417
|
-
/** Why an install failed, for the stuck detail, or `null` if it passed. */
|
|
418
|
-
const installFailure = ({
|
|
419
|
-
install,
|
|
420
|
-
where,
|
|
421
|
-
}: {
|
|
422
|
-
install: ReplayedInstall
|
|
423
|
-
where: string
|
|
424
|
-
}): string | null => {
|
|
425
|
-
const { check } = install
|
|
426
|
-
if (check === null || check.ok) return null
|
|
427
|
-
return `\`${check.command}\` failed in ${where} (${endedText(check)}). Agents never run the install, so fix the manifest or lockfile on the base branch and retry.\n${check.output}`
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const commitStep = ({
|
|
431
|
-
stage,
|
|
432
|
-
ticket,
|
|
433
|
-
progress,
|
|
434
|
-
}: {
|
|
435
|
-
stage: CommitStage
|
|
436
|
-
ticket: TicketSnapshot
|
|
437
|
-
progress: TicketProgress
|
|
438
|
-
}): BuildAction | null => {
|
|
439
|
-
if (progress.commits[stage] !== null) return null
|
|
440
|
-
const hits = progress.leftovers[stage]
|
|
441
|
-
if (hits !== null && hits.length > 0) {
|
|
442
|
-
return stuck({
|
|
443
|
-
ticket: ticket.number,
|
|
444
|
-
reason: 'leftovers_found',
|
|
445
|
-
detail: hits
|
|
446
|
-
.map(({ path, reason }) => `${path}: ${reason}`)
|
|
447
|
-
.join('\n'),
|
|
448
|
-
})
|
|
449
|
-
}
|
|
450
|
-
return {
|
|
451
|
-
type: 'commit_ticket',
|
|
452
|
-
ticket: ticket.number,
|
|
453
|
-
stage,
|
|
454
|
-
message: commitMessage({ stage, ticket, progress }),
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const badTestText = ({ progress }: { progress: TicketProgress }): string => {
|
|
459
|
-
const { bad_test } = progress
|
|
460
|
-
if (bad_test === null) return 'no reason given'
|
|
461
|
-
const where = [bad_test.file, bad_test.name].filter(Boolean).join(' > ')
|
|
462
|
-
return where === '' ? bad_test.reason : `${where}: ${bad_test.reason}`
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* The test-writer's half of a ticket: fresh tests, the red check and its fix
|
|
467
|
-
* loop, then the red commit. `null` once the red commit is made. A ticket
|
|
468
|
-
* whose work the test-writer found already on the base branch is marked
|
|
469
|
-
* done instead (#484); one that changes no behavior is stuck, as a refactor
|
|
470
|
-
* ticket without its label.
|
|
471
|
-
*/
|
|
472
|
-
const testStep = ({
|
|
473
|
-
snapshot,
|
|
474
|
-
ticket,
|
|
475
|
-
progress,
|
|
476
|
-
run_notes,
|
|
477
|
-
base_branch,
|
|
478
|
-
}: StepArgs & { base_branch: string }): BuildAction | null => {
|
|
479
|
-
const number = ticket.number
|
|
480
|
-
const { test_writer, red_check } = progress
|
|
481
|
-
if (test_writer === null) {
|
|
482
|
-
return launch({
|
|
483
|
-
role: 'test-writer',
|
|
484
|
-
snapshot,
|
|
485
|
-
ticket,
|
|
486
|
-
progress,
|
|
487
|
-
run_notes,
|
|
488
|
-
})
|
|
489
|
-
}
|
|
490
|
-
if (test_writer.outcome === 'already_done') {
|
|
491
|
-
return alreadyDoneStep({
|
|
492
|
-
spec_number: snapshot.spec.number,
|
|
493
|
-
base_branch,
|
|
494
|
-
ticket,
|
|
495
|
-
result: test_writer,
|
|
496
|
-
})
|
|
497
|
-
}
|
|
498
|
-
if (test_writer.outcome === 'nothing_new_to_test') {
|
|
499
|
-
return stuck({
|
|
500
|
-
ticket: number,
|
|
501
|
-
reason: 'nothing_new_to_test',
|
|
502
|
-
detail:
|
|
503
|
-
`The test-writer says this ticket changes no behavior, so no new test can fail first: ${test_writer.summary || 'no reason given'}\n` +
|
|
504
|
-
`A ticket like that is a refactor ticket: add the \`${REFACTOR_LABEL}\` label and retry it.`,
|
|
505
|
-
})
|
|
506
|
-
}
|
|
507
|
-
if (red_check === null) {
|
|
508
|
-
return {
|
|
509
|
-
type: 'run_red_check',
|
|
510
|
-
ticket: number,
|
|
511
|
-
criteria_ids: ticket.criteria.map(({ id }) => id),
|
|
512
|
-
mapping: test_writer.criteria,
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
if (!red_check.ok) {
|
|
516
|
-
const problems = red_check.problems.join('\n')
|
|
517
|
-
if (progress.red_fix_rounds >= MAX_FIX_ROUNDS) {
|
|
518
|
-
return stuck({
|
|
519
|
-
ticket: number,
|
|
520
|
-
reason: 'red_check_failed',
|
|
521
|
-
detail: `The red check still fails after ${MAX_FIX_ROUNDS} fix rounds:\n${problems}`,
|
|
522
|
-
})
|
|
523
|
-
}
|
|
524
|
-
const session_id = progress.sessions['test-writer']
|
|
525
|
-
const message = redFixMessage({ red_check })
|
|
526
|
-
if (session_id === undefined) {
|
|
527
|
-
return launch({
|
|
528
|
-
role: 'test-writer',
|
|
529
|
-
snapshot,
|
|
530
|
-
ticket,
|
|
531
|
-
progress,
|
|
532
|
-
run_notes,
|
|
533
|
-
follow_up: message,
|
|
534
|
-
})
|
|
535
|
-
}
|
|
536
|
-
return {
|
|
537
|
-
type: 'follow_up_agent',
|
|
538
|
-
ticket: number,
|
|
539
|
-
role: 'test-writer',
|
|
540
|
-
session_id,
|
|
541
|
-
message,
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
return commitStep({ stage: 'red', ticket, progress })
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* The implementer's half of a ticket: the code, the gates and their fix
|
|
549
|
-
* loop, then the green commit. `null` once the green commit is made.
|
|
550
|
-
*/
|
|
551
|
-
const codeStep = ({
|
|
552
|
-
snapshot,
|
|
553
|
-
ticket,
|
|
554
|
-
progress,
|
|
555
|
-
run_notes,
|
|
556
|
-
}: StepArgs): BuildAction | null => {
|
|
557
|
-
const number = ticket.number
|
|
558
|
-
const { implementer } = progress
|
|
559
|
-
if (implementer === null) {
|
|
560
|
-
return launch({
|
|
561
|
-
role: 'implementer',
|
|
562
|
-
snapshot,
|
|
563
|
-
ticket,
|
|
564
|
-
progress,
|
|
565
|
-
run_notes,
|
|
566
|
-
})
|
|
567
|
-
}
|
|
568
|
-
if (implementer.outcome === 'bad_test') {
|
|
569
|
-
if (isRefactorTicket({ ticket })) {
|
|
570
|
-
return stuck({
|
|
571
|
-
ticket: number,
|
|
572
|
-
reason: 'bad_test',
|
|
573
|
-
detail: `The implementer of a refactor ticket sent a test back as bad; there is no test-writer to fix it: ${badTestText({ progress })}`,
|
|
574
|
-
})
|
|
575
|
-
}
|
|
576
|
-
if (progress.bad_test_bounces > MAX_BAD_TEST_BOUNCES) {
|
|
577
|
-
return stuck({
|
|
578
|
-
ticket: number,
|
|
579
|
-
reason: 'bad_test',
|
|
580
|
-
detail: `The implementer sent a test back as bad a second time: ${badTestText({ progress })}`,
|
|
581
|
-
})
|
|
582
|
-
}
|
|
583
|
-
// Throw away the implementer's work; replay then clears the ticket's
|
|
584
|
-
// tests and code, so a fresh test-writer replaces the bad test.
|
|
585
|
-
return { type: 'reset_ticket_worktree', ticket: number }
|
|
586
|
-
}
|
|
587
|
-
return (
|
|
588
|
-
gateStep({ snapshot, ticket, progress, run_notes }) ??
|
|
589
|
-
commitStep({ stage: 'green', ticket, progress })
|
|
590
|
-
)
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* The ticket's gates and their fix loop: failed gates go back to the same
|
|
595
|
-
* implementer session (a fresh one with the same message if it is gone),
|
|
596
|
-
* up to `MAX_FIX_ROUNDS` follow-ups. `null` once the gates pass.
|
|
597
|
-
*/
|
|
598
|
-
const gateStep = ({
|
|
599
|
-
snapshot,
|
|
600
|
-
ticket,
|
|
601
|
-
progress,
|
|
602
|
-
run_notes,
|
|
603
|
-
}: StepArgs): BuildAction | null => {
|
|
604
|
-
const number = ticket.number
|
|
605
|
-
const { gates } = progress
|
|
606
|
-
if (gates === null) {
|
|
607
|
-
return { type: 'run_gates', ticket: number, target: 'ticket' }
|
|
608
|
-
}
|
|
609
|
-
if (gates.ok) return null
|
|
610
|
-
if (progress.gate_fix_rounds >= MAX_FIX_ROUNDS) {
|
|
611
|
-
return stuck({
|
|
612
|
-
ticket: number,
|
|
613
|
-
reason: 'gates_failed',
|
|
614
|
-
detail: `The gates still fail after ${MAX_FIX_ROUNDS} fix rounds:\n${failedChecks({ gates })}`,
|
|
615
|
-
})
|
|
616
|
-
}
|
|
617
|
-
const session_id = progress.sessions.implementer
|
|
618
|
-
const message = gateFixMessage({ gates })
|
|
619
|
-
if (session_id === undefined) {
|
|
620
|
-
return launch({
|
|
621
|
-
role: 'implementer',
|
|
622
|
-
snapshot,
|
|
623
|
-
ticket,
|
|
624
|
-
progress,
|
|
625
|
-
run_notes,
|
|
626
|
-
follow_up: message,
|
|
627
|
-
})
|
|
628
|
-
}
|
|
629
|
-
return {
|
|
630
|
-
type: 'follow_up_agent',
|
|
631
|
-
ticket: number,
|
|
632
|
-
role: 'implementer',
|
|
633
|
-
session_id,
|
|
634
|
-
message,
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* The ticket review and its fix loop, after the green commit. A fresh
|
|
640
|
-
* reviewer checks the committed diff. Blockers and should-fixes open a fix
|
|
641
|
-
* round: test findings go to a fresh test-writer first, then code findings
|
|
642
|
-
* to the same implementer session (a fresh one if it is gone). The fixes
|
|
643
|
-
* pass the gates, get their own commit, and a fresh reviewer checks only
|
|
644
|
-
* the new changes. A review that still asks for changes after
|
|
645
|
-
* `MAX_FIX_ROUNDS` fix rounds is stuck. `null` once a review approves.
|
|
646
|
-
*/
|
|
647
|
-
const reviewStep = ({
|
|
648
|
-
snapshot,
|
|
649
|
-
ticket,
|
|
650
|
-
progress,
|
|
651
|
-
run_notes,
|
|
652
|
-
}: StepArgs): BuildAction | null => {
|
|
653
|
-
const number = ticket.number
|
|
654
|
-
const reviewer = () =>
|
|
655
|
-
launch({
|
|
656
|
-
role: 'ticket-reviewer',
|
|
657
|
-
snapshot,
|
|
658
|
-
ticket,
|
|
659
|
-
progress,
|
|
660
|
-
run_notes,
|
|
661
|
-
})
|
|
662
|
-
if (progress.review === null) return reviewer()
|
|
663
|
-
const fix = progress.review_fix
|
|
664
|
-
if (fix === null) return null
|
|
665
|
-
if (fix.round > MAX_FIX_ROUNDS) {
|
|
666
|
-
return stuck({
|
|
667
|
-
ticket: number,
|
|
668
|
-
reason: 'changes_requested',
|
|
669
|
-
detail: `The ticket review still asks for changes after ${MAX_FIX_ROUNDS} fix rounds:\n${openFindingsText({ findings: fix.findings })}`,
|
|
670
|
-
})
|
|
671
|
-
}
|
|
672
|
-
if (!fix.tests_answered) {
|
|
673
|
-
return launch({
|
|
674
|
-
role: 'test-writer',
|
|
675
|
-
snapshot,
|
|
676
|
-
ticket,
|
|
677
|
-
progress,
|
|
678
|
-
run_notes,
|
|
679
|
-
})
|
|
680
|
-
}
|
|
681
|
-
if (!fix.code_answered) {
|
|
682
|
-
const session_id = progress.sessions.implementer
|
|
683
|
-
if (session_id === undefined) {
|
|
684
|
-
return launch({
|
|
685
|
-
role: 'implementer',
|
|
686
|
-
snapshot,
|
|
687
|
-
ticket,
|
|
688
|
-
progress,
|
|
689
|
-
run_notes,
|
|
690
|
-
})
|
|
691
|
-
}
|
|
692
|
-
return {
|
|
693
|
-
type: 'follow_up_agent',
|
|
694
|
-
ticket: number,
|
|
695
|
-
role: 'implementer',
|
|
696
|
-
session_id,
|
|
697
|
-
message: reviewFixMessage({ fix }),
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
if (fix.bad_test !== null) {
|
|
701
|
-
return stuck({
|
|
702
|
-
ticket: number,
|
|
703
|
-
reason: 'bad_test',
|
|
704
|
-
detail: `While fixing review findings, the implementer sent a test back as bad: ${badTestText({ progress: { ...progress, bad_test: fix.bad_test } })}`,
|
|
705
|
-
})
|
|
706
|
-
}
|
|
707
|
-
return (
|
|
708
|
-
gateStep({ snapshot, ticket, progress, run_notes }) ??
|
|
709
|
-
commitStep({ stage: 'fix', ticket, progress }) ??
|
|
710
|
-
reviewer()
|
|
711
|
-
)
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
/**
|
|
715
|
-
* The next step after a failed agent turn. An engine failure starts a fresh
|
|
716
|
-
* agent of the same role without using up a try, until
|
|
717
|
-
* `MAX_ENGINE_FAILURES` in a row. Any other failure uses up one of the
|
|
718
|
-
* role's tries on the ticket; with tries left, a test-writer or implementer
|
|
719
|
-
* gets a follow-up in the session that failed (a fresh launch with the same
|
|
720
|
-
* message if it has none, such as after a crash), and a reviewer a fresh
|
|
721
|
-
* launch. The last try's failure is stuck.
|
|
722
|
-
*/
|
|
723
|
-
const failedTurnStep = ({
|
|
724
|
-
snapshot,
|
|
725
|
-
ticket,
|
|
726
|
-
progress,
|
|
727
|
-
run_notes,
|
|
728
|
-
failed,
|
|
729
|
-
}: StepArgs & {
|
|
730
|
-
failed: NonNullable<TicketProgress['agent_failure']>
|
|
731
|
-
}): BuildAction => {
|
|
732
|
-
const { role, error, failure, session_id } = failed
|
|
733
|
-
if (failure === 'engine') {
|
|
734
|
-
if (progress.engine_failures >= MAX_ENGINE_FAILURES) {
|
|
735
|
-
return stuck({
|
|
736
|
-
ticket: ticket.number,
|
|
737
|
-
reason: 'agent_failed',
|
|
738
|
-
detail: `The engine failed to run the ${role} ${progress.engine_failures} times in a row: ${error}`,
|
|
739
|
-
})
|
|
740
|
-
}
|
|
741
|
-
return launch({ role, snapshot, ticket, progress, run_notes })
|
|
742
|
-
}
|
|
743
|
-
const tries = progress.failed_tries[role] ?? 0
|
|
744
|
-
if (tries >= MAX_FIX_ROUNDS) {
|
|
745
|
-
return stuck({
|
|
746
|
-
ticket: ticket.number,
|
|
747
|
-
reason: 'agent_failed',
|
|
748
|
-
detail: `The ${role} failed ${tries} tries; the last one: ${error}`,
|
|
749
|
-
})
|
|
750
|
-
}
|
|
751
|
-
if (role === 'ticket-reviewer') {
|
|
752
|
-
return launch({ role, snapshot, ticket, progress, run_notes })
|
|
753
|
-
}
|
|
754
|
-
const message = failedTryMessage({ failure, error })
|
|
755
|
-
if (session_id === null) {
|
|
756
|
-
return launch({
|
|
757
|
-
role,
|
|
758
|
-
snapshot,
|
|
759
|
-
ticket,
|
|
760
|
-
progress,
|
|
761
|
-
run_notes,
|
|
762
|
-
follow_up: message,
|
|
763
|
-
})
|
|
764
|
-
}
|
|
765
|
-
return {
|
|
766
|
-
type: 'follow_up_agent',
|
|
767
|
-
ticket: ticket.number,
|
|
768
|
-
role,
|
|
769
|
-
session_id,
|
|
770
|
-
message,
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
/**
|
|
775
|
-
* The fix on top of the run branch after a rebase: first a fresh test-writer
|
|
776
|
-
* for clashed tests, then the implementer for clashed code. `null` once both
|
|
777
|
-
* have answered; the gates, the green commit, and a fresh review of the new
|
|
778
|
-
* changes follow as usual. A bad test here is stuck: resetting the worktree
|
|
779
|
-
* would throw away the ticket's uncommitted change.
|
|
780
|
-
*/
|
|
781
|
-
const rejoinStep = ({
|
|
782
|
-
snapshot,
|
|
783
|
-
ticket,
|
|
784
|
-
progress,
|
|
785
|
-
run_notes,
|
|
786
|
-
}: StepArgs): BuildAction | null => {
|
|
787
|
-
const { rejoin } = progress
|
|
788
|
-
if (rejoin === null) return null
|
|
789
|
-
if (progress.implementer?.outcome === 'bad_test') {
|
|
790
|
-
return stuck({
|
|
791
|
-
ticket: ticket.number,
|
|
792
|
-
reason: 'bad_test',
|
|
793
|
-
detail: `The implementer sent a test back as bad while fixing the ticket on top of the run branch; resetting the worktree would throw the ticket's change away: ${badTestText({ progress })}`,
|
|
794
|
-
})
|
|
795
|
-
}
|
|
796
|
-
if (rejoin.tests_pending) {
|
|
797
|
-
return launch({
|
|
798
|
-
role: 'test-writer',
|
|
799
|
-
snapshot,
|
|
800
|
-
ticket,
|
|
801
|
-
progress,
|
|
802
|
-
run_notes,
|
|
803
|
-
})
|
|
804
|
-
}
|
|
805
|
-
if (rejoin.code_pending) {
|
|
806
|
-
const session_id = progress.sessions.implementer
|
|
807
|
-
if (session_id === undefined) {
|
|
808
|
-
return launch({
|
|
809
|
-
role: 'implementer',
|
|
810
|
-
snapshot,
|
|
811
|
-
ticket,
|
|
812
|
-
progress,
|
|
813
|
-
run_notes,
|
|
814
|
-
})
|
|
815
|
-
}
|
|
816
|
-
return {
|
|
817
|
-
type: 'follow_up_agent',
|
|
818
|
-
ticket: ticket.number,
|
|
819
|
-
role: 'implementer',
|
|
820
|
-
session_id,
|
|
821
|
-
message: clashFixMessage({ rejoin }),
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
return null
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
/**
|
|
828
|
-
* The join section of an approved ticket, for the first ticket in the join
|
|
829
|
-
* queue only: join, the gates on the run branch, the push. A clash or failed
|
|
830
|
-
* gates sends the ticket back onto the run branch, up to `MAX_REJOINS`
|
|
831
|
-
* times; after that it is stuck. `null` once pushed.
|
|
832
|
-
*/
|
|
833
|
-
const joinStep = ({
|
|
834
|
-
ticket,
|
|
835
|
-
progress,
|
|
836
|
-
run_branch,
|
|
837
|
-
}: {
|
|
838
|
-
ticket: TicketSnapshot
|
|
839
|
-
progress: TicketProgress
|
|
840
|
-
run_branch: ReplayedWorktree
|
|
841
|
-
}): BuildAction | null => {
|
|
842
|
-
const number = ticket.number
|
|
843
|
-
const { joined, join_gates } = progress
|
|
844
|
-
if (joined === null) {
|
|
845
|
-
return { type: 'join_run_branch', ticket: number }
|
|
846
|
-
}
|
|
847
|
-
if (!joined.ok) {
|
|
848
|
-
if (progress.rejoins >= MAX_REJOINS) {
|
|
849
|
-
return stuck({
|
|
850
|
-
ticket: number,
|
|
851
|
-
reason: 'join_failed',
|
|
852
|
-
detail: `The ticket still clashes with the run branch after ${MAX_REJOINS} rebases:\n${joined.error}`,
|
|
853
|
-
})
|
|
854
|
-
}
|
|
855
|
-
return {
|
|
856
|
-
type: 'rebase_ticket',
|
|
857
|
-
ticket: number,
|
|
858
|
-
cause: 'clash',
|
|
859
|
-
undo_first_sha: null,
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
if (join_gates === null) {
|
|
863
|
-
return { type: 'run_gates', ticket: number, target: 'run_branch' }
|
|
864
|
-
}
|
|
865
|
-
if (!join_gates.ok) {
|
|
866
|
-
if (progress.rejoins >= MAX_REJOINS) {
|
|
867
|
-
return stuck({
|
|
868
|
-
ticket: number,
|
|
869
|
-
reason: 'join_gates_failed',
|
|
870
|
-
detail: `The gates still fail after joining, after ${MAX_REJOINS} rebases:\n${failedChecks({ gates: join_gates })}`,
|
|
871
|
-
})
|
|
872
|
-
}
|
|
873
|
-
return {
|
|
874
|
-
type: 'rebase_ticket',
|
|
875
|
-
ticket: number,
|
|
876
|
-
cause: 'join_gates',
|
|
877
|
-
undo_first_sha: joined.shas[0] ?? null,
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
if (progress.pushed === null) {
|
|
881
|
-
return {
|
|
882
|
-
type: 'push_run_branch',
|
|
883
|
-
ticket: number,
|
|
884
|
-
branch: run_branch.branch,
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
return null
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
/**
|
|
891
|
-
* How a ticket with no baseline gets one. Tickets whose worktrees start from
|
|
892
|
-
* the same run-branch commit share one baseline test run: a ticket reuses a
|
|
893
|
-
* baseline taken at its commit, or else the first such ticket still able to
|
|
894
|
-
* move runs the tests and the rest wait for it. `null`: it waits.
|
|
895
|
-
*/
|
|
896
|
-
const sharedBaselineStep = ({
|
|
897
|
-
number,
|
|
898
|
-
numbers,
|
|
899
|
-
progress,
|
|
900
|
-
can_move,
|
|
901
|
-
}: {
|
|
902
|
-
number: number
|
|
903
|
-
/** The run's tickets, in order. */
|
|
904
|
-
numbers: number[]
|
|
905
|
-
progress: (number: number) => TicketProgress
|
|
906
|
-
/** Whether a ticket is neither pushed, skipped, nor stuck. */
|
|
907
|
-
can_move: (number: number) => boolean
|
|
908
|
-
}): BuildAction | null => {
|
|
909
|
-
const base_sha = progress(number).worktree?.base_sha
|
|
910
|
-
if (base_sha === undefined) return null
|
|
911
|
-
const lender = numbers.find(
|
|
912
|
-
(other) =>
|
|
913
|
-
other !== number &&
|
|
914
|
-
progress(other).baseline !== null &&
|
|
915
|
-
progress(other).baseline_sha === base_sha
|
|
916
|
-
)
|
|
917
|
-
if (lender !== undefined) {
|
|
918
|
-
return {
|
|
919
|
-
type: 'reuse_baseline_tests',
|
|
920
|
-
ticket: number,
|
|
921
|
-
from_ticket: lender,
|
|
922
|
-
base_sha,
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
const runner = numbers.find(
|
|
926
|
-
(other) =>
|
|
927
|
-
can_move(other) &&
|
|
928
|
-
progress(other).baseline === null &&
|
|
929
|
-
progress(other).worktree?.base_sha === base_sha
|
|
930
|
-
)
|
|
931
|
-
return runner === number
|
|
932
|
-
? { type: 'run_baseline_tests', ticket: number }
|
|
933
|
-
: null
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
/**
|
|
937
|
-
* The next step for one ticket that has its worktree, or `null` when it has
|
|
938
|
-
* nothing to do now: it pushed, or its review approved and it waits its turn
|
|
939
|
-
* to join.
|
|
940
|
-
*/
|
|
941
|
-
const nextTicketStep = ({
|
|
942
|
-
snapshot,
|
|
943
|
-
ticket,
|
|
944
|
-
progress,
|
|
945
|
-
run_notes,
|
|
946
|
-
run_branch,
|
|
947
|
-
joiner,
|
|
948
|
-
setup_files,
|
|
949
|
-
baseline_step,
|
|
950
|
-
base_branch,
|
|
951
|
-
}: StepArgs & {
|
|
952
|
-
run_branch: ReplayedWorktree
|
|
953
|
-
/** The branch the run started from. */
|
|
954
|
-
base_branch: string
|
|
955
|
-
/** Whether the ticket is first in the join queue. */
|
|
956
|
-
joiner: boolean
|
|
957
|
-
/** The config's test setup files. */
|
|
958
|
-
setup_files: string[]
|
|
959
|
-
/** How it gets its baseline, if it has none (`sharedBaselineStep`). */
|
|
960
|
-
baseline_step: BuildAction | null
|
|
961
|
-
}): BuildAction | null => {
|
|
962
|
-
const number = ticket.number
|
|
963
|
-
if (progress.agent_failure !== null) {
|
|
964
|
-
return failedTurnStep({
|
|
965
|
-
snapshot,
|
|
966
|
-
ticket,
|
|
967
|
-
progress,
|
|
968
|
-
run_notes,
|
|
969
|
-
failed: progress.agent_failure,
|
|
970
|
-
})
|
|
971
|
-
}
|
|
972
|
-
if (progress.setup_change !== null) {
|
|
973
|
-
return stuck({
|
|
974
|
-
ticket: number,
|
|
975
|
-
reason: 'setup_change_needed',
|
|
976
|
-
detail: setupChangeDetail({
|
|
977
|
-
...progress.setup_change,
|
|
978
|
-
setup_files,
|
|
979
|
-
}),
|
|
980
|
-
})
|
|
981
|
-
}
|
|
982
|
-
if (progress.install === null) {
|
|
983
|
-
return {
|
|
984
|
-
type: 'install_dependencies',
|
|
985
|
-
target: 'ticket',
|
|
986
|
-
ticket: number,
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
const ticketFailure = installFailure({
|
|
990
|
-
install: progress.install,
|
|
991
|
-
where: `the worktree of #${number}`,
|
|
992
|
-
})
|
|
993
|
-
if (ticketFailure !== null) {
|
|
994
|
-
return stuck({
|
|
995
|
-
ticket: number,
|
|
996
|
-
reason: 'install_failed',
|
|
997
|
-
detail: ticketFailure,
|
|
998
|
-
})
|
|
999
|
-
}
|
|
1000
|
-
const { prepare } = progress
|
|
1001
|
-
if (prepare !== null && !prepare.ok) {
|
|
1002
|
-
return stuck({
|
|
1003
|
-
ticket: number,
|
|
1004
|
-
reason: 'prepare_failed',
|
|
1005
|
-
detail: `\`${prepare.command}\` failed in the worktree of #${number} (${endedText(prepare)}) before any agent worked on it. Agents never run the prepare command, so fix what it needs (a missing tool, say) and retry.\n${prepare.output}`,
|
|
1006
|
-
})
|
|
1007
|
-
}
|
|
1008
|
-
if (progress.baseline === null) return baseline_step
|
|
1009
|
-
if (progress.commits.green === null) {
|
|
1010
|
-
const rejoin = rejoinStep({
|
|
1011
|
-
snapshot,
|
|
1012
|
-
ticket,
|
|
1013
|
-
progress,
|
|
1014
|
-
run_notes,
|
|
1015
|
-
})
|
|
1016
|
-
if (rejoin !== null) return rejoin
|
|
1017
|
-
const tests = isRefactorTicket({ ticket })
|
|
1018
|
-
? null
|
|
1019
|
-
: testStep({
|
|
1020
|
-
snapshot,
|
|
1021
|
-
ticket,
|
|
1022
|
-
progress,
|
|
1023
|
-
run_notes,
|
|
1024
|
-
base_branch,
|
|
1025
|
-
})
|
|
1026
|
-
if (tests !== null) return tests
|
|
1027
|
-
const code = codeStep({ snapshot, ticket, progress, run_notes })
|
|
1028
|
-
if (code !== null) return code
|
|
1029
|
-
}
|
|
1030
|
-
const review = reviewStep({
|
|
1031
|
-
snapshot,
|
|
1032
|
-
ticket,
|
|
1033
|
-
progress,
|
|
1034
|
-
run_notes,
|
|
1035
|
-
})
|
|
1036
|
-
if (review !== null) return review
|
|
1037
|
-
return joiner ? joinStep({ ticket, progress, run_branch }) : null
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
/**
|
|
1041
|
-
* Whether a ticket's review finally approved it (no fix round open) and it
|
|
1042
|
-
* has not pushed yet: it waits in the join queue, or is joining.
|
|
1043
|
-
*/
|
|
1044
|
-
const inJoinQueue = (progress: TicketProgress): boolean =>
|
|
1045
|
-
progress.approved_seq !== null &&
|
|
1046
|
-
progress.review_fix === null &&
|
|
1047
|
-
progress.pushed === null
|
|
1048
|
-
|
|
1049
|
-
/** The worktrees not removed yet, of these paths. */
|
|
1050
|
-
const notRemoved = ({
|
|
1051
|
-
paths,
|
|
1052
|
-
state,
|
|
1053
|
-
}: {
|
|
1054
|
-
paths: string[]
|
|
1055
|
-
state: RunState
|
|
1056
|
-
}): string[] => paths.filter((path) => !state.removed_worktrees.includes(path))
|
|
1057
|
-
|
|
1058
25
|
/**
|
|
1059
26
|
* The build half of the decision step: every step of building the run's
|
|
1060
27
|
* tickets that can run now, at most one per ticket, then the PR. Pure.
|
|
@@ -1063,13 +30,16 @@ const notRemoved = ({
|
|
|
1063
30
|
* the run branch's tip, then its install) once every ticket it waits on has
|
|
1064
31
|
* pushed, and while no ticket waits to join, so it never builds on joined
|
|
1065
32
|
* commits whose gates have not passed yet. Approved tickets join one at a
|
|
1066
|
-
* time, in the order their reviews finally approved them
|
|
33
|
+
* time, in the order their reviews finally approved them, except that one
|
|
34
|
+
* already joining finishes first. A clash or failed
|
|
1067
35
|
* gates after joining puts the ticket's change back on top of the run
|
|
1068
|
-
* branch to be fixed there (up to `MAX_REJOINS` times), then re-reviewed.
|
|
1069
|
-
* stuck ticket
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1072
|
-
*
|
|
36
|
+
* branch to be fixed there (up to `MAX_REJOINS` times), then re-reviewed.
|
|
37
|
+
* While a stuck ticket's join is still on the run branch, no ticket starts
|
|
38
|
+
* or joins until it is undone (#519), so no join lands on top of a join
|
|
39
|
+
* that is going away. A stuck ticket ends the run: no ticket starts or
|
|
40
|
+
* moves on, and the worktrees of pushed tickets are removed. Once every
|
|
41
|
+
* ticket pushed, the final review looks at the whole run branch
|
|
42
|
+
* (`decideFinalReview`) before the PR opens; a stuck final review ends the run with the run branch's worktree kept. In a
|
|
1073
43
|
* repo with changesets, the run's changeset is written just before the PR
|
|
1074
44
|
* (`write_changeset`), once. Once
|
|
1075
45
|
* the PR is open, every worktree is removed. Branches and the journal stay.
|
|
@@ -1079,12 +49,17 @@ const notRemoved = ({
|
|
|
1079
49
|
* up to `MAX_FIX_ROUNDS` follow-ups; then the ticket is stuck. A bad test
|
|
1080
50
|
* throws away the implementer's work and goes to a fresh test-writer, whose
|
|
1081
51
|
* tests get their own red check and red commit; the next bad test is stuck.
|
|
1082
|
-
*
|
|
1083
|
-
* test-writer
|
|
1084
|
-
*
|
|
52
|
+
* After a rebase, a bad test can't throw the work away: the tests go back
|
|
53
|
+
* to the test-writer to fit the run branch, with the code kept, up to
|
|
54
|
+
* `MAX_TEST_UPDATES` times (#489). "Nothing new to test" is stuck at once.
|
|
55
|
+
* A refactor ticket skips the test-writer and the red check. The ticket
|
|
56
|
+
* review's findings go back for fixing in capped rounds, each re-reviewed.
|
|
1085
57
|
*
|
|
1086
58
|
* Already done (#484): a ticket whose test-writer found its work already on
|
|
1087
|
-
* the base branch
|
|
59
|
+
* the base branch has that evidence checked first (`check_already_done`,
|
|
60
|
+
* #495); evidence that doesn't check out is a failed try for the
|
|
61
|
+
* test-writer. Once it checks out, the ticket is marked done
|
|
62
|
+
* (`mark_already_done`), not stuck. It counts
|
|
1088
63
|
* as pushed for the tickets that wait on it, and the PR closes it. With no
|
|
1089
64
|
* ticket pushed, the engine closes the already-done tickets itself
|
|
1090
65
|
* (`close_ticket`); if every ticket was already done, the run ends with
|
|
@@ -1100,334 +75,92 @@ const notRemoved = ({
|
|
|
1100
75
|
* decideBuild({ state, spec_number })
|
|
1101
76
|
* // [{ type: 'create_ticket_worktree', ticket: 11, ... }, { type: 'create_ticket_worktree', ticket: 12, ... }]
|
|
1102
77
|
*/
|
|
1103
|
-
export const decideBuild = ({
|
|
78
|
+
export const decideBuild = (args: {
|
|
79
|
+
state: RunState
|
|
80
|
+
spec_number: number
|
|
81
|
+
}): BuildAction[] => decideBuildPlan(args).steps
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* `decideBuild`'s steps, and beside them the tickets that wait on purpose
|
|
85
|
+
* between steps now (#559): the same decision, so a wait is only ever one
|
|
86
|
+
* that left its ticket no step. None while the run is halted, holds on its
|
|
87
|
+
* install, or has yet to make its run branch.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* decideBuildPlan({ state, spec_number })
|
|
91
|
+
* // { steps: [{ type: 'run_baseline_tests', ticket: 11 }], waits: [{ ticket: 12, for: 'baseline', on_ticket: 11 }] }
|
|
92
|
+
*/
|
|
93
|
+
export const decideBuildPlan = ({
|
|
1104
94
|
state,
|
|
1105
95
|
spec_number,
|
|
1106
96
|
}: {
|
|
1107
97
|
state: RunState
|
|
1108
98
|
spec_number: number
|
|
1109
|
-
}): BuildAction[] => {
|
|
1110
|
-
const steps = buildSteps({ state, spec_number })
|
|
99
|
+
}): { steps: BuildAction[]; waits: TicketWait[] } => {
|
|
100
|
+
const { steps, waits } = buildSteps({ state, spec_number })
|
|
1111
101
|
const command = state.config?.prepare
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
102
|
+
const prepared =
|
|
103
|
+
command === undefined
|
|
104
|
+
? steps
|
|
105
|
+
: steps.map((step) =>
|
|
106
|
+
withWriterSection({
|
|
107
|
+
step,
|
|
108
|
+
section: prepareSection({ command }),
|
|
109
|
+
})
|
|
110
|
+
)
|
|
111
|
+
const map =
|
|
112
|
+
state.config === null ? null : featureMapPath({ config: state.config })
|
|
113
|
+
const mapped =
|
|
114
|
+
map === null
|
|
115
|
+
? prepared
|
|
116
|
+
: prepared.map((step) =>
|
|
117
|
+
withWriterSection({
|
|
118
|
+
step,
|
|
119
|
+
section: featureMapSection({ path: map }),
|
|
120
|
+
only: 'test-writer',
|
|
121
|
+
})
|
|
122
|
+
)
|
|
123
|
+
return {
|
|
124
|
+
steps: state.changesets ? mapped.map(withChangesetsSections) : mapped,
|
|
125
|
+
waits,
|
|
126
|
+
}
|
|
1115
127
|
}
|
|
1116
128
|
|
|
1117
129
|
/**
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
130
|
+
* `decideBuild`'s steps, before the prepare command joins any prompt: the run
|
|
131
|
+
* branch, then the steps that end the run or hold it on the run branch's
|
|
132
|
+
* install, each ticket's next step, and last the finish (`finishSteps`);
|
|
133
|
+
* with the tickets' waits once they are decided.
|
|
1121
134
|
*/
|
|
1122
|
-
const withPrepareSection = ({
|
|
1123
|
-
step,
|
|
1124
|
-
command,
|
|
1125
|
-
}: {
|
|
1126
|
-
step: BuildAction
|
|
1127
|
-
command: string
|
|
1128
|
-
}): BuildAction => {
|
|
1129
|
-
const writesCode =
|
|
1130
|
-
(step.type === 'launch_agent' || step.type === 'launch_final_fixer') &&
|
|
1131
|
-
(step.role === 'test-writer' || step.role === 'implementer')
|
|
1132
|
-
return writesCode
|
|
1133
|
-
? {
|
|
1134
|
-
...step,
|
|
1135
|
-
prompt: `${step.prompt}\n\n${prepareSection({ command })}`,
|
|
1136
|
-
}
|
|
1137
|
-
: step
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
/** `decideBuild`'s steps, before the prepare command joins any prompt. */
|
|
1141
135
|
const buildSteps = ({
|
|
1142
136
|
state,
|
|
1143
137
|
spec_number,
|
|
1144
138
|
}: {
|
|
1145
139
|
state: RunState
|
|
1146
140
|
spec_number: number
|
|
1147
|
-
}): BuildAction[] => {
|
|
1148
|
-
const { snapshot, run_branch
|
|
1149
|
-
const base_branch = state.base_branch ?? 'main'
|
|
1150
|
-
const run_notes = newestRunNotes({ run_notes: state.run_notes })
|
|
141
|
+
}): { steps: BuildAction[]; waits: TicketWait[] } => {
|
|
142
|
+
const { snapshot, run_branch } = state
|
|
1151
143
|
if (run_branch === null || snapshot === null) {
|
|
1152
|
-
return
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
)
|
|
1157
|
-
const progress = (number: number): TicketProgress =>
|
|
1158
|
-
state.tickets[number] ?? EMPTY_TICKET_PROGRESS
|
|
1159
|
-
|
|
1160
|
-
const skipped = (number: number) => progress(number).skipped !== null
|
|
1161
|
-
const pushed = (number: number) =>
|
|
1162
|
-
!numbers.includes(number) || progress(number).pushed !== null
|
|
1163
|
-
const alreadyDone = (number: number) =>
|
|
1164
|
-
progress(number).already_done !== null
|
|
1165
|
-
/** Pushed, or its work was already on the base branch (#484). */
|
|
1166
|
-
const finished = (number: number) => pushed(number) || alreadyDone(number)
|
|
1167
|
-
|
|
1168
|
-
if (state.stop !== null) {
|
|
1169
|
-
// Nothing new starts; finished tickets' worktrees go, the rest stay.
|
|
1170
|
-
const paths = notRemoved({
|
|
1171
|
-
state,
|
|
1172
|
-
paths: numbers.flatMap((number) => {
|
|
1173
|
-
const { worktree } = progress(number)
|
|
1174
|
-
return finished(number) && worktree !== null
|
|
1175
|
-
? [worktree.path]
|
|
1176
|
-
: []
|
|
1177
|
-
}),
|
|
1178
|
-
})
|
|
1179
|
-
if (paths.length > 0) return [{ type: 'remove_worktrees', paths }]
|
|
1180
|
-
return [{ type: 'done', outcome: 'stopped_by_user' }]
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
if (pull_request !== null) {
|
|
1184
|
-
const paths = notRemoved({
|
|
1185
|
-
state,
|
|
1186
|
-
paths: [
|
|
1187
|
-
...numbers.flatMap((number) => {
|
|
1188
|
-
const { worktree } = progress(number)
|
|
1189
|
-
return worktree === null ? [] : [worktree.path]
|
|
1190
|
-
}),
|
|
1191
|
-
run_branch.path,
|
|
1192
|
-
],
|
|
1193
|
-
})
|
|
1194
|
-
if (paths.length > 0) return [{ type: 'remove_worktrees', paths }]
|
|
1195
|
-
return [{ type: 'done', outcome: 'pr_opened', pull_request }]
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
if (run_branch_install === null) {
|
|
1199
|
-
return [
|
|
1200
|
-
{
|
|
1201
|
-
type: 'install_dependencies',
|
|
1202
|
-
target: 'run_branch',
|
|
1203
|
-
ticket: null,
|
|
1204
|
-
},
|
|
1205
|
-
]
|
|
1206
|
-
}
|
|
1207
|
-
const runBranchFailure = installFailure({
|
|
1208
|
-
install: run_branch_install,
|
|
1209
|
-
where: "the run branch's checkout",
|
|
1210
|
-
})
|
|
1211
|
-
const replies = replySteps({ state, spec_number, numbers })
|
|
1212
|
-
const stuckSteps = (number: number): BuildAction[] => {
|
|
1213
|
-
const ticket = snapshot.tickets[number]
|
|
1214
|
-
return ticket === undefined
|
|
1215
|
-
? []
|
|
1216
|
-
: stuckTicketSteps({
|
|
1217
|
-
spec_number,
|
|
1218
|
-
ticket,
|
|
1219
|
-
progress: progress(number),
|
|
1220
|
-
})
|
|
1221
|
-
}
|
|
1222
|
-
const isStuck = (number: number) =>
|
|
1223
|
-
progress(number).stuck !== null && !skipped(number)
|
|
1224
|
-
if (runBranchFailure !== null) {
|
|
1225
|
-
// Nothing builds on a run branch whose install failed: the first
|
|
1226
|
-
// ticket is stuck on it until a retry installs it again.
|
|
1227
|
-
if (numbers.some(isStuck)) {
|
|
1228
|
-
return [...numbers.filter(isStuck).flatMap(stuckSteps), ...replies]
|
|
1229
|
-
}
|
|
1230
|
-
const first = numbers.find(
|
|
1231
|
-
(number) => !finished(number) && !skipped(number)
|
|
1232
|
-
)
|
|
1233
|
-
if (first !== undefined) {
|
|
1234
|
-
return [
|
|
1235
|
-
stuck({
|
|
1236
|
-
ticket: first,
|
|
1237
|
-
reason: 'install_failed',
|
|
1238
|
-
detail: runBranchFailure,
|
|
1239
|
-
}),
|
|
1240
|
-
]
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
const queue = sortBy(
|
|
1245
|
-
numbers.filter(
|
|
1246
|
-
(number) =>
|
|
1247
|
-
inJoinQueue(progress(number)) &&
|
|
1248
|
-
!isStuck(number) &&
|
|
1249
|
-
!skipped(number)
|
|
1250
|
-
),
|
|
1251
|
-
(number) => progress(number).approved_seq
|
|
1252
|
-
)
|
|
1253
|
-
const joiner = queue[0] ?? null
|
|
1254
|
-
// A stuck ticket's join is undone before any new ticket starts on it.
|
|
1255
|
-
const stuckJoinOnRunBranch = numbers.some(
|
|
1256
|
-
(number) =>
|
|
1257
|
-
isStuck(number) &&
|
|
1258
|
-
progress(number).joined?.ok === true &&
|
|
1259
|
-
progress(number).pushed === null
|
|
1260
|
-
)
|
|
1261
|
-
|
|
1262
|
-
const steps = numbers.flatMap((number): BuildAction[] => {
|
|
1263
|
-
const ticket = snapshot.tickets[number]
|
|
1264
|
-
const ticketProgress = progress(number)
|
|
1265
|
-
if (ticket === undefined || finished(number)) return []
|
|
1266
|
-
if (skipped(number)) return []
|
|
1267
|
-
// The same step cut off by crashes too often is not taken again.
|
|
1268
|
-
const crash = crashedOut({
|
|
1269
|
-
crashes: state.crashes,
|
|
1270
|
-
keys: (key) => key === String(number),
|
|
1271
|
-
})
|
|
1272
|
-
if (crash !== null) {
|
|
1273
|
-
return [
|
|
1274
|
-
stuck({
|
|
1275
|
-
ticket: number,
|
|
1276
|
-
reason: 'crashed',
|
|
1277
|
-
detail: crashDetail({ crash }),
|
|
1278
|
-
}),
|
|
1279
|
-
]
|
|
1280
|
-
}
|
|
1281
|
-
if (isStuck(number)) return stuckSteps(number)
|
|
1282
|
-
const skippedBlocker = ticket.blockers.find(skipped)
|
|
1283
|
-
if (skippedBlocker !== undefined) {
|
|
1284
|
-
return [
|
|
1285
|
-
skipDependent({
|
|
1286
|
-
ticket: number,
|
|
1287
|
-
because: skippedBlocker,
|
|
1288
|
-
spec_number,
|
|
1289
|
-
}),
|
|
1290
|
-
]
|
|
1291
|
-
}
|
|
1292
|
-
if (ticketProgress.worktree === null) {
|
|
1293
|
-
const ready =
|
|
1294
|
-
queue.length === 0 &&
|
|
1295
|
-
!stuckJoinOnRunBranch &&
|
|
1296
|
-
ticket.blockers.every(finished)
|
|
1297
|
-
return ready
|
|
1298
|
-
? [
|
|
1299
|
-
{
|
|
1300
|
-
type: 'create_ticket_worktree',
|
|
1301
|
-
ticket: number,
|
|
1302
|
-
run_branch: run_branch.branch,
|
|
1303
|
-
},
|
|
1304
|
-
]
|
|
1305
|
-
: []
|
|
1306
|
-
}
|
|
1307
|
-
const step = nextTicketStep({
|
|
1308
|
-
snapshot,
|
|
1309
|
-
ticket,
|
|
1310
|
-
progress: ticketProgress,
|
|
1311
|
-
run_notes,
|
|
1312
|
-
run_branch,
|
|
1313
|
-
base_branch,
|
|
1314
|
-
joiner: number === joiner,
|
|
1315
|
-
setup_files: state.config?.test_setup_files ?? [],
|
|
1316
|
-
baseline_step:
|
|
1317
|
-
ticketProgress.baseline === null
|
|
1318
|
-
? sharedBaselineStep({
|
|
1319
|
-
number,
|
|
1320
|
-
numbers,
|
|
1321
|
-
progress,
|
|
1322
|
-
can_move: (other) =>
|
|
1323
|
-
!finished(other) &&
|
|
1324
|
-
!skipped(other) &&
|
|
1325
|
-
!isStuck(other),
|
|
1326
|
-
})
|
|
1327
|
-
: null,
|
|
1328
|
-
})
|
|
1329
|
-
return step === null ? [] : [step]
|
|
1330
|
-
})
|
|
1331
|
-
if (steps.length > 0 || replies.length > 0) return [...steps, ...replies]
|
|
1332
|
-
|
|
1333
|
-
if (
|
|
1334
|
-
numbers.every((number) => finished(number) || skipped(number)) &&
|
|
1335
|
-
!numbers.some(pushed)
|
|
1336
|
-
) {
|
|
1337
|
-
// No ticket joined the run branch: no PR, and nothing left to build.
|
|
1338
|
-
// The engine closes the already-done tickets itself (#484).
|
|
1339
|
-
const closing = numbers.filter(
|
|
1340
|
-
(number) => alreadyDone(number) && !progress(number).closed
|
|
1341
|
-
)
|
|
1342
|
-
if (closing.length > 0) {
|
|
1343
|
-
return closing.map((number) =>
|
|
1344
|
-
closeTicketStep({
|
|
144
|
+
return {
|
|
145
|
+
steps: [
|
|
146
|
+
{
|
|
147
|
+
type: 'create_run_branch',
|
|
1345
148
|
spec_number,
|
|
1346
|
-
base_branch,
|
|
1347
|
-
|
|
1348
|
-
shas: progress(number).already_done?.shas ?? [],
|
|
1349
|
-
})
|
|
1350
|
-
)
|
|
1351
|
-
}
|
|
1352
|
-
const paths = notRemoved({
|
|
1353
|
-
state,
|
|
1354
|
-
paths: [
|
|
1355
|
-
...numbers.flatMap((number) => {
|
|
1356
|
-
const { worktree } = progress(number)
|
|
1357
|
-
return worktree === null ? [] : [worktree.path]
|
|
1358
|
-
}),
|
|
1359
|
-
run_branch.path,
|
|
149
|
+
base_branch: state.base_branch ?? 'main',
|
|
150
|
+
},
|
|
1360
151
|
],
|
|
1361
|
-
|
|
1362
|
-
if (paths.length > 0) return [{ type: 'remove_worktrees', paths }]
|
|
1363
|
-
if (numbers.some(skipped)) {
|
|
1364
|
-
return [{ type: 'done', outcome: 'all_skipped' }]
|
|
152
|
+
waits: [],
|
|
1365
153
|
}
|
|
1366
|
-
// Every ticket's work was already done: nothing to do.
|
|
1367
|
-
return [
|
|
1368
|
-
{
|
|
1369
|
-
type: 'finish_nothing_to_do',
|
|
1370
|
-
closed_tickets: snapshot.closed_tickets,
|
|
1371
|
-
already_done: numbers.filter(alreadyDone),
|
|
1372
|
-
},
|
|
1373
|
-
]
|
|
1374
154
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
}
|
|
1384
|
-
if (final.status === 'working') return final.actions
|
|
1385
|
-
if (final.status === 'stuck') {
|
|
1386
|
-
// The run branch's worktree stays for a retry.
|
|
1387
|
-
const paths = notRemoved({
|
|
1388
|
-
state,
|
|
1389
|
-
paths: numbers.flatMap((number) => {
|
|
1390
|
-
const { worktree } = progress(number)
|
|
1391
|
-
return worktree === null ? [] : [worktree.path]
|
|
1392
|
-
}),
|
|
1393
|
-
})
|
|
1394
|
-
if (paths.length > 0) return [{ type: 'remove_worktrees', paths }]
|
|
1395
|
-
// The owner hears of it, and the run waits for their reply.
|
|
1396
|
-
return [
|
|
1397
|
-
...finalStuckSteps({ state, spec_number, run_branch }),
|
|
1398
|
-
...replies,
|
|
1399
|
-
]
|
|
1400
|
-
}
|
|
1401
|
-
if (state.changesets && state.changeset === null) {
|
|
1402
|
-
return [
|
|
1403
|
-
{
|
|
1404
|
-
type: 'write_changeset',
|
|
1405
|
-
branch: run_branch.branch,
|
|
1406
|
-
...changesetStep({ spec: snapshot.spec }),
|
|
1407
|
-
},
|
|
1408
|
-
]
|
|
1409
|
-
}
|
|
1410
|
-
return [
|
|
1411
|
-
{
|
|
1412
|
-
type: 'open_pull_request',
|
|
1413
|
-
head: run_branch.branch,
|
|
1414
|
-
base: base_branch,
|
|
1415
|
-
...pullRequestText({
|
|
1416
|
-
snapshot,
|
|
1417
|
-
tickets: state.tickets,
|
|
1418
|
-
final_review: state.final_review,
|
|
1419
|
-
memory_saves: state.memory.saved?.saves ?? [],
|
|
1420
|
-
}),
|
|
1421
|
-
},
|
|
1422
|
-
]
|
|
155
|
+
const view = buildViewOf({ state, spec_number, snapshot, run_branch })
|
|
156
|
+
const halted = haltSteps({ state, view })
|
|
157
|
+
if (halted !== null) return { steps: halted, waits: [] }
|
|
158
|
+
const replies = replySteps({ state, spec_number, numbers: view.numbers })
|
|
159
|
+
const installing = installSteps({ state, view, replies })
|
|
160
|
+
if (installing !== null) return { steps: installing, waits: [] }
|
|
161
|
+
const { steps, waits } = ticketSteps({ state, view })
|
|
162
|
+
if (steps.length > 0 || replies.length > 0) {
|
|
163
|
+
return { steps: [...steps, ...replies], waits }
|
|
1423
164
|
}
|
|
1424
|
-
return
|
|
1425
|
-
{
|
|
1426
|
-
type: 'invalid_journal',
|
|
1427
|
-
reason: `No ticket can move: ${numbers
|
|
1428
|
-
.filter((number) => !finished(number))
|
|
1429
|
-
.map((number) => `#${number}`)
|
|
1430
|
-
.join(', ')} wait on tickets that never join.`,
|
|
1431
|
-
},
|
|
1432
|
-
]
|
|
165
|
+
return { steps: finishSteps({ state, view, replies }), waits }
|
|
1433
166
|
}
|