@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,80 +1,53 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
|
-
import {
|
|
3
|
-
import { basename, dirname, join } from 'node:path'
|
|
4
|
-
|
|
5
|
-
import difference from 'lodash/difference'
|
|
6
|
-
import uniq from 'lodash/uniq'
|
|
2
|
+
import { basename, join } from 'node:path'
|
|
7
3
|
|
|
4
|
+
import { followUpAgent, launchAgent } from './agent-turn'
|
|
8
5
|
import type { AlreadyDoneAction } from './already-done'
|
|
6
|
+
import {
|
|
7
|
+
buildContext,
|
|
8
|
+
need,
|
|
9
|
+
reportFile,
|
|
10
|
+
testFilesIn,
|
|
11
|
+
ticketWorktree,
|
|
12
|
+
type BuildDeps,
|
|
13
|
+
} from './build-context'
|
|
9
14
|
import { CHANGESET_CONFIG } from './changeset'
|
|
10
|
-
import {
|
|
15
|
+
import { commitTicket } from './commit-in'
|
|
16
|
+
import type { BuildAction } from './decide-build'
|
|
11
17
|
import type { FinalReviewAction } from './decide-final-review'
|
|
18
|
+
import { checkAlreadyDoneIn } from './execute-already-done-check'
|
|
19
|
+
import { gatesIn, installIn } from './execute-gates'
|
|
20
|
+
import {
|
|
21
|
+
rebaseTicket,
|
|
22
|
+
sendTestsBack,
|
|
23
|
+
undoCutOffJoin,
|
|
24
|
+
undoJoin,
|
|
25
|
+
} from './execute-join'
|
|
26
|
+
import { runRedCheck } from './execute-red-check'
|
|
12
27
|
import { retryTicket } from './execute-stuck'
|
|
13
|
-
import { openTicketsNotInRun } from './not-in-run'
|
|
14
|
-
import { withNotInRun } from './pull-request-text'
|
|
15
28
|
import { closeSessions, openSessionsIn } from './session-close'
|
|
29
|
+
import { watchPrepare } from './worktree-changes'
|
|
16
30
|
|
|
17
|
-
import
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
type AgentRole,
|
|
21
|
-
type RoleResult,
|
|
22
|
-
} from '../agents/role-results'
|
|
23
|
-
import { bunTestCommands, type EngineConfig } from '../config/engine-config'
|
|
24
|
-
import { prepareCheck, runGates, shellCheck } from '../gates/gate-runner'
|
|
25
|
-
import { endedText } from '../gates/gate-schemas'
|
|
26
|
-
import { newCodeFiles, importStem, scanLeftovers } from '../gates/leftover-scan'
|
|
27
|
-
import {
|
|
28
|
-
dependenciesChanged,
|
|
29
|
-
installCommand,
|
|
30
|
-
MANIFEST,
|
|
31
|
-
newWorktreeInstall,
|
|
32
|
-
rebaseNeedsInstall,
|
|
33
|
-
} from '../gates/lockfile-install'
|
|
34
|
-
import { checkRed } from '../gates/red-check'
|
|
35
|
-
import { runBunTests, testFilesAmong } from '../gates/test-runner'
|
|
36
|
-
import type { FileChange, GitAdapter } from '../git/git-adapter'
|
|
37
|
-
import { pathText } from '../git/path-text'
|
|
38
|
-
import { describeViolations } from '../guards/after-turn-check'
|
|
39
|
-
import { guardRoleOf } from '../guards/role-rules'
|
|
40
|
-
import { enforceAfterTurn, snapshotWorktree } from '../guards/worktree-state'
|
|
31
|
+
import { bunTestCommands, testTimeoutOf } from '../config/engine-config'
|
|
32
|
+
import { prepareCheck } from '../gates/gate-runner'
|
|
33
|
+
import { runBunTests } from '../gates/test-runner'
|
|
41
34
|
import type { Journal } from '../journal/journal'
|
|
42
|
-
import type {
|
|
43
|
-
AgentFailure,
|
|
44
|
-
CommitStage,
|
|
45
|
-
GateTarget,
|
|
46
|
-
JournalRecord,
|
|
47
|
-
} from '../journal/journal-record'
|
|
48
|
-
import {
|
|
49
|
-
replayRun,
|
|
50
|
-
type ReplayedWorktree,
|
|
51
|
-
type RunState,
|
|
52
|
-
} from '../journal/replay'
|
|
53
|
-
import { sessionSignal } from '../limits/plan-signals'
|
|
54
|
-
import { createAgentMessaging } from '../messages/agent-messaging'
|
|
55
35
|
import type { CommentStep } from '../tracker/post-comment-once'
|
|
56
36
|
import type { Tracker } from '../tracker/tracker'
|
|
57
37
|
|
|
58
|
-
|
|
59
|
-
export
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
run_dir: string
|
|
72
|
-
/**
|
|
73
|
-
* Which try of its step this is: a redo after a crash adopts what its
|
|
74
|
-
* first try left behind.
|
|
75
|
-
*/
|
|
76
|
-
step: CommentStep
|
|
77
|
-
}
|
|
38
|
+
export { runTurn } from './agent-turn'
|
|
39
|
+
export {
|
|
40
|
+
buildContext,
|
|
41
|
+
need,
|
|
42
|
+
ticketWorktree,
|
|
43
|
+
type BuildContext,
|
|
44
|
+
type BuildDeps,
|
|
45
|
+
} from './build-context'
|
|
46
|
+
export { commitIn, dropAgentChangesets } from './commit-in'
|
|
47
|
+
export { gatesIn, slowTestsIn } from './execute-gates'
|
|
48
|
+
export { openJoin } from './execute-join'
|
|
49
|
+
export { NO_RED_CHANGES } from './execute-red-check'
|
|
50
|
+
export { leaveOutIn } from './worktree-changes'
|
|
78
51
|
|
|
79
52
|
/** The run branch's name: one per run, so runs never share a branch. */
|
|
80
53
|
export const runBranchName = ({
|
|
@@ -85,842 +58,6 @@ export const runBranchName = ({
|
|
|
85
58
|
run_id: string
|
|
86
59
|
}): string => `luca/spec-${spec_number}-${run_id}`
|
|
87
60
|
|
|
88
|
-
/** A journaled value, or a clear error that the journal has none yet. */
|
|
89
|
-
export const need = <T>({
|
|
90
|
-
value,
|
|
91
|
-
what,
|
|
92
|
-
}: {
|
|
93
|
-
value: T | null | undefined
|
|
94
|
-
what: string
|
|
95
|
-
}): T => {
|
|
96
|
-
if (value === null || value === undefined) {
|
|
97
|
-
throw new Error(`The journal has no ${what} yet.`)
|
|
98
|
-
}
|
|
99
|
-
return value
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export const ticketWorktree = ({
|
|
103
|
-
state,
|
|
104
|
-
ticket,
|
|
105
|
-
}: {
|
|
106
|
-
state: RunState
|
|
107
|
-
ticket: number
|
|
108
|
-
}): ReplayedWorktree =>
|
|
109
|
-
need({
|
|
110
|
-
value: state.tickets[ticket]?.worktree,
|
|
111
|
-
what: `worktree for #${ticket}`,
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
/** A fresh report file outside the worktree, so it is never a leftover. */
|
|
115
|
-
const reportFile = async ({
|
|
116
|
-
context,
|
|
117
|
-
ticket,
|
|
118
|
-
label,
|
|
119
|
-
}: {
|
|
120
|
-
context: BuildContext
|
|
121
|
-
/** `null` for the final review. */
|
|
122
|
-
ticket: number | null
|
|
123
|
-
label: string
|
|
124
|
-
}): Promise<string> => {
|
|
125
|
-
const folder = join(context.run_dir, 'reports')
|
|
126
|
-
await mkdir(folder, { recursive: true })
|
|
127
|
-
return join(
|
|
128
|
-
folder,
|
|
129
|
-
`${context.state.last_seq + 1}-${ticket ?? 'final'}-${label}.xml`
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const testFilesIn = async ({
|
|
134
|
-
context,
|
|
135
|
-
cwd,
|
|
136
|
-
}: {
|
|
137
|
-
context: BuildContext
|
|
138
|
-
cwd: string
|
|
139
|
-
}): Promise<string[]> =>
|
|
140
|
-
testFilesAmong({
|
|
141
|
-
files: await context.git.listFiles({ cwd }),
|
|
142
|
-
test_file_patterns: context.config.test_file_patterns,
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Runs `run`, a run of the config's prepare command in `cwd`, and journals
|
|
147
|
-
* as `prepare_made` the untracked paths it made: there after it ran, not
|
|
148
|
-
* before, and not known yet. Without a prepare command it only runs `run`.
|
|
149
|
-
*
|
|
150
|
-
* A crash in the middle of a prepare run leaves what it made so far; the
|
|
151
|
-
* redo's own "before" then holds those paths, so they count as the
|
|
152
|
-
* ticket's unless an earlier prepare run already noted them.
|
|
153
|
-
*/
|
|
154
|
-
const watchPrepare = async <Result>({
|
|
155
|
-
context,
|
|
156
|
-
cwd,
|
|
157
|
-
ticket,
|
|
158
|
-
run,
|
|
159
|
-
}: {
|
|
160
|
-
context: BuildContext
|
|
161
|
-
cwd: string
|
|
162
|
-
/** The step's ticket, `null` in the final review. */
|
|
163
|
-
ticket: number | null
|
|
164
|
-
run: () => Promise<Result>
|
|
165
|
-
}): Promise<Result> => {
|
|
166
|
-
if (context.config.prepare === undefined) return run()
|
|
167
|
-
const before = await context.git.untracked({ cwd })
|
|
168
|
-
const result = await run()
|
|
169
|
-
const paths = difference(
|
|
170
|
-
await context.git.untracked({ cwd }),
|
|
171
|
-
before,
|
|
172
|
-
context.state.prepare_made
|
|
173
|
-
)
|
|
174
|
-
if (paths.length > 0) {
|
|
175
|
-
context.journal.append({
|
|
176
|
-
kind: 'prepare_made',
|
|
177
|
-
ticket,
|
|
178
|
-
role: null,
|
|
179
|
-
content: { paths },
|
|
180
|
-
})
|
|
181
|
-
}
|
|
182
|
-
return result
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** A ticket's changes, less what the prepare command made. */
|
|
186
|
-
const ticketChanges = async ({
|
|
187
|
-
context,
|
|
188
|
-
cwd,
|
|
189
|
-
}: {
|
|
190
|
-
context: BuildContext
|
|
191
|
-
cwd: string
|
|
192
|
-
}): Promise<FileChange[]> =>
|
|
193
|
-
(await context.git.changes({ cwd })).filter(
|
|
194
|
-
({ path }) => !context.state.prepare_made.includes(path)
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
const runRedCheck = async ({
|
|
198
|
-
context,
|
|
199
|
-
action,
|
|
200
|
-
}: {
|
|
201
|
-
context: BuildContext
|
|
202
|
-
action: Extract<BuildAction, { type: 'run_red_check' }>
|
|
203
|
-
}) => {
|
|
204
|
-
const { path } = ticketWorktree({
|
|
205
|
-
state: context.state,
|
|
206
|
-
ticket: action.ticket,
|
|
207
|
-
})
|
|
208
|
-
const baseline = need({
|
|
209
|
-
value: context.state.tickets[action.ticket]?.baseline,
|
|
210
|
-
what: `baseline test run for #${action.ticket}`,
|
|
211
|
-
})
|
|
212
|
-
const test_files = await testFilesIn({ context, cwd: path })
|
|
213
|
-
const prepare = await watchPrepare({
|
|
214
|
-
context,
|
|
215
|
-
cwd: path,
|
|
216
|
-
ticket: action.ticket,
|
|
217
|
-
run: () => prepareCheck({ config: context.config, cwd: path }),
|
|
218
|
-
})
|
|
219
|
-
const current = await runBunTests({
|
|
220
|
-
cwd: path,
|
|
221
|
-
commands: bunTestCommands(context),
|
|
222
|
-
test_files,
|
|
223
|
-
report_file: await reportFile({
|
|
224
|
-
context,
|
|
225
|
-
ticket: action.ticket,
|
|
226
|
-
label: 'red',
|
|
227
|
-
}),
|
|
228
|
-
})
|
|
229
|
-
const files = uniq(
|
|
230
|
-
action.mapping.flatMap(({ tests }) => tests.map(({ file }) => file))
|
|
231
|
-
)
|
|
232
|
-
const sources: Record<string, string | null> = {}
|
|
233
|
-
for (const file of files) {
|
|
234
|
-
sources[file] = await pathText({ path: join(path, file) })
|
|
235
|
-
}
|
|
236
|
-
const checked = checkRed({
|
|
237
|
-
criteria_ids: action.criteria_ids,
|
|
238
|
-
mapping: action.mapping,
|
|
239
|
-
baseline,
|
|
240
|
-
current,
|
|
241
|
-
test_files,
|
|
242
|
-
sources,
|
|
243
|
-
})
|
|
244
|
-
// A failed prepare after the test-writer's turn is one more problem,
|
|
245
|
-
// for the test-writer's fix loop.
|
|
246
|
-
const result =
|
|
247
|
-
prepare === null || prepare.ok
|
|
248
|
-
? checked
|
|
249
|
-
: {
|
|
250
|
-
...checked,
|
|
251
|
-
ok: false,
|
|
252
|
-
problems: [
|
|
253
|
-
`The prepare command \`${prepare.command}\` failed (${endedText(prepare)}):\n${prepare.output}`,
|
|
254
|
-
...checked.problems,
|
|
255
|
-
],
|
|
256
|
-
}
|
|
257
|
-
context.journal.append({
|
|
258
|
-
kind: 'red_check',
|
|
259
|
-
ticket: action.ticket,
|
|
260
|
-
role: null,
|
|
261
|
-
content: { ...result, tests: current },
|
|
262
|
-
})
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/** Whether anything besides the file itself mentions a new code file. */
|
|
266
|
-
const isUsed = async ({
|
|
267
|
-
context,
|
|
268
|
-
cwd,
|
|
269
|
-
path,
|
|
270
|
-
added_texts,
|
|
271
|
-
}: {
|
|
272
|
-
context: BuildContext
|
|
273
|
-
cwd: string
|
|
274
|
-
path: string
|
|
275
|
-
added_texts: Record<string, string>
|
|
276
|
-
}): Promise<boolean> => {
|
|
277
|
-
const stem = importStem(path)
|
|
278
|
-
const tracked = await context.git.filesMentioning({ cwd, text: stem })
|
|
279
|
-
if (tracked.some((file) => file !== path)) return true
|
|
280
|
-
return Object.entries(added_texts).some(
|
|
281
|
-
([file, text]) => file !== path && text.includes(stem)
|
|
282
|
-
)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* The commit a crash left behind at `cwd`: HEAD carries `message` and no
|
|
287
|
-
* record names it yet, so the step's first try made it and died before
|
|
288
|
-
* journaling it. `null` when HEAD is no such commit.
|
|
289
|
-
*/
|
|
290
|
-
const leftCommit = async ({
|
|
291
|
-
context,
|
|
292
|
-
cwd,
|
|
293
|
-
message,
|
|
294
|
-
}: {
|
|
295
|
-
context: BuildContext
|
|
296
|
-
cwd: string
|
|
297
|
-
message: string
|
|
298
|
-
}): Promise<{ sha: string; message: string; files: string[] } | null> => {
|
|
299
|
-
const last = await context.git.lastCommit({ cwd })
|
|
300
|
-
if (last.message !== message.trim()) return null
|
|
301
|
-
const journaled = context.journal
|
|
302
|
-
.read()
|
|
303
|
-
.some(
|
|
304
|
-
(record) =>
|
|
305
|
-
(record.kind === 'commit_made' &&
|
|
306
|
-
record.content.sha === last.sha) ||
|
|
307
|
-
(record.kind === 'ticket_joined' &&
|
|
308
|
-
record.content.ok &&
|
|
309
|
-
record.content.shas.includes(last.sha))
|
|
310
|
-
)
|
|
311
|
-
return journaled ? null : { sha: last.sha, message, files: last.files }
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* The leftover scan, then an engine commit of everything in `cwd`, both
|
|
316
|
-
* journaled. What the prepare command made (`prepare_made`) is neither
|
|
317
|
-
* scanned nor committed. A hit blocks the commit. A `fix` commit with
|
|
318
|
-
* nothing to commit (every finding was a "won't fix") journals the current
|
|
319
|
-
* commit with no files instead. A redo after a crash that already committed
|
|
320
|
-
* (nothing left to commit, and HEAD is an unjournaled commit with this
|
|
321
|
-
* message) journals that commit instead of making another; its scan, of a
|
|
322
|
-
* clean worktree, finds nothing, as the first try's did before it committed.
|
|
323
|
-
*/
|
|
324
|
-
export const commitIn = async ({
|
|
325
|
-
context,
|
|
326
|
-
cwd,
|
|
327
|
-
ticket,
|
|
328
|
-
stage,
|
|
329
|
-
message,
|
|
330
|
-
mention_text,
|
|
331
|
-
}: {
|
|
332
|
-
context: BuildContext
|
|
333
|
-
cwd: string
|
|
334
|
-
/** `null` for the final review. */
|
|
335
|
-
ticket: number | null
|
|
336
|
-
stage: CommitStage
|
|
337
|
-
message: string
|
|
338
|
-
/** The spec and ticket text a new markdown file may be named in. */
|
|
339
|
-
mention_text: string
|
|
340
|
-
}) => {
|
|
341
|
-
const changes = await ticketChanges({ context, cwd })
|
|
342
|
-
const test_files = testFilesAmong({
|
|
343
|
-
files: changes.map(({ path }) => path),
|
|
344
|
-
test_file_patterns: context.config.test_file_patterns,
|
|
345
|
-
})
|
|
346
|
-
const added = changes.filter(({ change }) => change === 'added')
|
|
347
|
-
const added_texts: Record<string, string> = {}
|
|
348
|
-
for (const { path } of added) {
|
|
349
|
-
added_texts[path] = (await pathText({ path: join(cwd, path) })) ?? ''
|
|
350
|
-
}
|
|
351
|
-
const used_code: Record<string, boolean> = {}
|
|
352
|
-
for (const path of newCodeFiles({ changes, test_files })) {
|
|
353
|
-
used_code[path] = await isUsed({ context, cwd, path, added_texts })
|
|
354
|
-
}
|
|
355
|
-
const hits = scanLeftovers({ changes, test_files, mention_text, used_code })
|
|
356
|
-
context.journal.append({
|
|
357
|
-
kind: 'leftover_scan',
|
|
358
|
-
ticket,
|
|
359
|
-
role: null,
|
|
360
|
-
content: { stage, hits },
|
|
361
|
-
})
|
|
362
|
-
if (hits.length > 0) return
|
|
363
|
-
const adopted =
|
|
364
|
-
changes.length === 0 && context.step.redo
|
|
365
|
-
? await leftCommit({ context, cwd, message })
|
|
366
|
-
: null
|
|
367
|
-
if (adopted !== null) {
|
|
368
|
-
context.journal.append({
|
|
369
|
-
kind: 'commit_made',
|
|
370
|
-
ticket,
|
|
371
|
-
role: null,
|
|
372
|
-
content: { stage, ...adopted },
|
|
373
|
-
})
|
|
374
|
-
return
|
|
375
|
-
}
|
|
376
|
-
if (changes.length === 0 && stage === 'fix') {
|
|
377
|
-
// The fixers changed nothing (every finding was a "won't fix"): no
|
|
378
|
-
// commit to make, so the re-review's new changes are empty.
|
|
379
|
-
context.journal.append({
|
|
380
|
-
kind: 'commit_made',
|
|
381
|
-
ticket,
|
|
382
|
-
role: null,
|
|
383
|
-
content: {
|
|
384
|
-
stage,
|
|
385
|
-
sha: await context.git.head({ cwd }),
|
|
386
|
-
message,
|
|
387
|
-
files: [],
|
|
388
|
-
},
|
|
389
|
-
})
|
|
390
|
-
return
|
|
391
|
-
}
|
|
392
|
-
const commit = await context.git.commitAll({
|
|
393
|
-
cwd,
|
|
394
|
-
message,
|
|
395
|
-
leave_out: context.state.prepare_made,
|
|
396
|
-
})
|
|
397
|
-
context.journal.append({
|
|
398
|
-
kind: 'commit_made',
|
|
399
|
-
ticket,
|
|
400
|
-
role: null,
|
|
401
|
-
content: { stage, sha: commit.sha, message, files: commit.files },
|
|
402
|
-
})
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const commitTicket = async ({
|
|
406
|
-
context,
|
|
407
|
-
action,
|
|
408
|
-
}: {
|
|
409
|
-
context: BuildContext
|
|
410
|
-
action: Extract<BuildAction, { type: 'commit_ticket' }>
|
|
411
|
-
}) => {
|
|
412
|
-
const { path: cwd } = ticketWorktree({
|
|
413
|
-
state: context.state,
|
|
414
|
-
ticket: action.ticket,
|
|
415
|
-
})
|
|
416
|
-
const snapshot = need({
|
|
417
|
-
value: context.state.snapshot,
|
|
418
|
-
what: 'spec snapshot',
|
|
419
|
-
})
|
|
420
|
-
const ticket = snapshot.tickets[action.ticket]
|
|
421
|
-
await commitIn({
|
|
422
|
-
context,
|
|
423
|
-
cwd,
|
|
424
|
-
ticket: action.ticket,
|
|
425
|
-
stage: action.stage,
|
|
426
|
-
message: action.message,
|
|
427
|
-
mention_text: [snapshot.spec.body, ticket?.body ?? ''].join('\n'),
|
|
428
|
-
})
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const failTurn = ({
|
|
432
|
-
context,
|
|
433
|
-
ticket,
|
|
434
|
-
role,
|
|
435
|
-
error,
|
|
436
|
-
failure,
|
|
437
|
-
session_id,
|
|
438
|
-
}: {
|
|
439
|
-
context: BuildContext
|
|
440
|
-
ticket: number | null
|
|
441
|
-
role: AgentRole
|
|
442
|
-
error: string
|
|
443
|
-
failure: AgentFailure
|
|
444
|
-
session_id: string | null
|
|
445
|
-
}) => {
|
|
446
|
-
context.journal.append({
|
|
447
|
-
kind: 'agent_failed',
|
|
448
|
-
ticket,
|
|
449
|
-
role,
|
|
450
|
-
content: { role, error, failure, session_id },
|
|
451
|
-
})
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* Runs one agent turn, a launch or a follow-up, and journals how it ended.
|
|
456
|
-
* Before the turn the engine snapshots the worktree; after it, the
|
|
457
|
-
* after-turn check undoes and reports anything the role may not change, for
|
|
458
|
-
* every launcher alike; what others changed in the shared `.git` meanwhile
|
|
459
|
-
* is journaled as `shared_git_changed`. Then the result is judged by its
|
|
460
|
-
* structured output. Each failed turn is journaled once as `agent_failed`
|
|
461
|
-
* with how it failed; the decision step picks what happens next. A launcher stop journals
|
|
462
|
-
* `run_stopped` and ends the run. A turn the plan cut off (a rejected limit,
|
|
463
|
-
* overage, a billing error) journals only its session. Both close the
|
|
464
|
-
* turn's session first, since no follow-up can reach it.
|
|
465
|
-
*
|
|
466
|
-
* @param worktree - Where the agent works: its ticket's worktree, or the run
|
|
467
|
-
* branch's for the final review.
|
|
468
|
-
* @param ticket - The ticket its records name, `null` for the final review.
|
|
469
|
-
* @returns The role's result when the agent finished, else `null`.
|
|
470
|
-
*/
|
|
471
|
-
export const runTurn = async ({
|
|
472
|
-
context,
|
|
473
|
-
worktree,
|
|
474
|
-
ticket,
|
|
475
|
-
role,
|
|
476
|
-
may_edit_tests,
|
|
477
|
-
start,
|
|
478
|
-
}: {
|
|
479
|
-
context: BuildContext
|
|
480
|
-
worktree: ReplayedWorktree
|
|
481
|
-
ticket: number | null
|
|
482
|
-
role: AgentRole
|
|
483
|
-
may_edit_tests: boolean
|
|
484
|
-
start: (cwd: string) => Promise<AgentTurn>
|
|
485
|
-
}): Promise<RoleResult | null> => {
|
|
486
|
-
const { path, branch } = worktree
|
|
487
|
-
const before = await snapshotWorktree({
|
|
488
|
-
cwd: path,
|
|
489
|
-
branch,
|
|
490
|
-
config: context.config,
|
|
491
|
-
})
|
|
492
|
-
const turn = await start(path)
|
|
493
|
-
if (turn.session !== undefined) {
|
|
494
|
-
context.journal.append({
|
|
495
|
-
kind: 'agent_session',
|
|
496
|
-
ticket,
|
|
497
|
-
role,
|
|
498
|
-
content: { role, session: turn.session },
|
|
499
|
-
})
|
|
500
|
-
}
|
|
501
|
-
const guard_role = guardRoleOf({ role })
|
|
502
|
-
const { violations, outside } = await enforceAfterTurn({
|
|
503
|
-
cwd: path,
|
|
504
|
-
branch,
|
|
505
|
-
role: guard_role,
|
|
506
|
-
may_edit_tests,
|
|
507
|
-
config: context.config,
|
|
508
|
-
before,
|
|
509
|
-
leave_out: context.state.prepare_made,
|
|
510
|
-
})
|
|
511
|
-
// Others' changes to the shared .git: noted, never blamed or undone.
|
|
512
|
-
if (outside.length > 0) {
|
|
513
|
-
context.journal.append({
|
|
514
|
-
kind: 'shared_git_changed',
|
|
515
|
-
ticket,
|
|
516
|
-
role,
|
|
517
|
-
content: { role, changes: outside },
|
|
518
|
-
})
|
|
519
|
-
}
|
|
520
|
-
// The plan cut the turn off. Its session, journaled above, holds why,
|
|
521
|
-
// and the decision step reads it from there: a limit wait or a billing
|
|
522
|
-
// stop. The turn uses up no try, and its step is taken again. With no
|
|
523
|
-
// sign in the session, the engine can't tell which, so it stops.
|
|
524
|
-
const unexplained =
|
|
525
|
-
!turn.ok &&
|
|
526
|
-
turn.failure === 'plan' &&
|
|
527
|
-
(turn.session === undefined ||
|
|
528
|
-
sessionSignal({ session: turn.session }).kind === 'ok')
|
|
529
|
-
// Either way no follow-up reaches the session: close it now.
|
|
530
|
-
const cut = !turn.ok && (turn.failure === 'plan' || turn.failure === 'stop')
|
|
531
|
-
if (cut && turn.session_id !== undefined) {
|
|
532
|
-
await closeSessions({
|
|
533
|
-
journal: context.journal,
|
|
534
|
-
launcher: context.launcher,
|
|
535
|
-
sessions: [{ session_id: turn.session_id, ticket, role }],
|
|
536
|
-
})
|
|
537
|
-
}
|
|
538
|
-
if (!turn.ok && turn.failure === 'plan' && !unexplained) return null
|
|
539
|
-
if (!turn.ok && (turn.failure === 'stop' || unexplained)) {
|
|
540
|
-
context.journal.append({
|
|
541
|
-
kind: 'run_stopped',
|
|
542
|
-
ticket,
|
|
543
|
-
role,
|
|
544
|
-
content: { reason: turn.error, role, billing: false },
|
|
545
|
-
})
|
|
546
|
-
throw new Error(`Run stopped: ${turn.error}`)
|
|
547
|
-
}
|
|
548
|
-
const session_id = turn.session_id ?? null
|
|
549
|
-
const fail = (failure: AgentFailure, error: string) => {
|
|
550
|
-
failTurn({ context, ticket, role, error, failure, session_id })
|
|
551
|
-
return null
|
|
552
|
-
}
|
|
553
|
-
if (violations.length > 0) {
|
|
554
|
-
return fail(
|
|
555
|
-
'guard',
|
|
556
|
-
describeViolations({ role: guard_role, violations })
|
|
557
|
-
)
|
|
558
|
-
}
|
|
559
|
-
if (!turn.ok) {
|
|
560
|
-
return fail(turn.failure === 'engine' ? 'engine' : 'agent', turn.error)
|
|
561
|
-
}
|
|
562
|
-
if (
|
|
563
|
-
turn.structured_output === undefined ||
|
|
564
|
-
turn.structured_output === null
|
|
565
|
-
) {
|
|
566
|
-
return fail('result', `The ${role} finished with no structured output.`)
|
|
567
|
-
}
|
|
568
|
-
const checked = parseRoleResult({ role, output: turn.structured_output })
|
|
569
|
-
if (!checked.ok) return fail('result', checked.error)
|
|
570
|
-
context.journal.append({
|
|
571
|
-
kind: 'agent_finished',
|
|
572
|
-
ticket,
|
|
573
|
-
role,
|
|
574
|
-
content: { ...checked.value, session_id },
|
|
575
|
-
})
|
|
576
|
-
return checked.value
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
const launchAgent = async ({
|
|
580
|
-
context,
|
|
581
|
-
action,
|
|
582
|
-
}: {
|
|
583
|
-
context: BuildContext
|
|
584
|
-
action: Extract<BuildAction, { type: 'launch_agent' }>
|
|
585
|
-
}) => {
|
|
586
|
-
const { ticket, role, prompt, may_edit_tests } = action
|
|
587
|
-
context.journal.append({
|
|
588
|
-
kind: 'agent_started',
|
|
589
|
-
ticket,
|
|
590
|
-
role,
|
|
591
|
-
content: { role, prompt, follow_up_of: null },
|
|
592
|
-
})
|
|
593
|
-
await runTurn({
|
|
594
|
-
context,
|
|
595
|
-
worktree: ticketWorktree({ state: context.state, ticket }),
|
|
596
|
-
ticket,
|
|
597
|
-
role,
|
|
598
|
-
may_edit_tests,
|
|
599
|
-
start: (cwd) =>
|
|
600
|
-
context.launcher.launch({
|
|
601
|
-
role,
|
|
602
|
-
ticket,
|
|
603
|
-
prompt,
|
|
604
|
-
cwd,
|
|
605
|
-
may_edit_tests,
|
|
606
|
-
config: context.config,
|
|
607
|
-
messaging: createAgentMessaging({
|
|
608
|
-
journal: context.journal,
|
|
609
|
-
ticket,
|
|
610
|
-
role,
|
|
611
|
-
}),
|
|
612
|
-
}),
|
|
613
|
-
})
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
const followUpAgent = async ({
|
|
617
|
-
context,
|
|
618
|
-
action,
|
|
619
|
-
}: {
|
|
620
|
-
context: BuildContext
|
|
621
|
-
action: Extract<BuildAction, { type: 'follow_up_agent' }>
|
|
622
|
-
}) => {
|
|
623
|
-
const { ticket, role, session_id, message } = action
|
|
624
|
-
const snapshot = need({
|
|
625
|
-
value: context.state.snapshot?.tickets[ticket],
|
|
626
|
-
what: `snapshot of #${ticket}`,
|
|
627
|
-
})
|
|
628
|
-
context.journal.append({
|
|
629
|
-
kind: 'agent_started',
|
|
630
|
-
ticket,
|
|
631
|
-
role,
|
|
632
|
-
content: { role, prompt: message, follow_up_of: session_id },
|
|
633
|
-
})
|
|
634
|
-
await runTurn({
|
|
635
|
-
context,
|
|
636
|
-
worktree: ticketWorktree({ state: context.state, ticket }),
|
|
637
|
-
ticket,
|
|
638
|
-
role,
|
|
639
|
-
may_edit_tests: mayEditTests({ role, ticket: snapshot }),
|
|
640
|
-
start: (cwd) =>
|
|
641
|
-
context.launcher.followUp({
|
|
642
|
-
session_id,
|
|
643
|
-
role,
|
|
644
|
-
ticket,
|
|
645
|
-
message,
|
|
646
|
-
cwd,
|
|
647
|
-
config: context.config,
|
|
648
|
-
}),
|
|
649
|
-
})
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* Where the run branch stood before a join of `ticket` a crash cut off: the
|
|
654
|
-
* latest `join_started` for it with no `ticket_joined` or `ticket_stuck`
|
|
655
|
-
* for it after, else `null`.
|
|
656
|
-
*
|
|
657
|
-
* @example
|
|
658
|
-
* openJoin({ records: journal.read(), ticket: 11 }) // 'a1b2c3...' after a crash mid-join
|
|
659
|
-
*/
|
|
660
|
-
export const openJoin = ({
|
|
661
|
-
records,
|
|
662
|
-
ticket,
|
|
663
|
-
}: {
|
|
664
|
-
records: JournalRecord[]
|
|
665
|
-
ticket: number
|
|
666
|
-
}): string | null => {
|
|
667
|
-
const last = records.findLast(
|
|
668
|
-
(record) =>
|
|
669
|
-
record.ticket === ticket &&
|
|
670
|
-
(record.kind === 'join_started' ||
|
|
671
|
-
record.kind === 'ticket_joined' ||
|
|
672
|
-
record.kind === 'ticket_stuck')
|
|
673
|
-
)
|
|
674
|
-
return last?.kind === 'join_started' ? last.content.run_branch_sha : null
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* Puts the run branch back where it stood before a join of `ticket` a crash
|
|
679
|
-
* cut off, dropping whatever half of its commits the crash left there. A
|
|
680
|
-
* cut-off step is taken again before other steps start, so nothing else
|
|
681
|
-
* moved the run branch since. Does nothing when no join was cut off.
|
|
682
|
-
*/
|
|
683
|
-
const undoCutOffJoin = async ({
|
|
684
|
-
context,
|
|
685
|
-
ticket,
|
|
686
|
-
}: {
|
|
687
|
-
context: BuildContext
|
|
688
|
-
ticket: number
|
|
689
|
-
}) => {
|
|
690
|
-
const sha = openJoin({ records: context.journal.read(), ticket })
|
|
691
|
-
if (sha === null) return
|
|
692
|
-
const runBranch = need({
|
|
693
|
-
value: context.state.run_branch,
|
|
694
|
-
what: 'run branch',
|
|
695
|
-
})
|
|
696
|
-
await context.git.resetWorktree({ cwd: runBranch.path, to: sha })
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/**
|
|
700
|
-
* Undoes a ticket's join on the run branch, back to before `first_sha`, and
|
|
701
|
-
* returns the commits undone. The undone join's install is still in the run
|
|
702
|
-
* branch's `node_modules`, so when its commits changed dependency files the
|
|
703
|
-
* lockfile's install runs there again.
|
|
704
|
-
*/
|
|
705
|
-
const undoJoin = async ({
|
|
706
|
-
context,
|
|
707
|
-
ticket,
|
|
708
|
-
first_sha,
|
|
709
|
-
}: {
|
|
710
|
-
context: BuildContext
|
|
711
|
-
ticket: number
|
|
712
|
-
first_sha: string
|
|
713
|
-
}): Promise<string[]> => {
|
|
714
|
-
const { git, state, journal } = context
|
|
715
|
-
const runBranch = need({ value: state.run_branch, what: 'run branch' })
|
|
716
|
-
const joined = state.tickets[ticket]?.joined
|
|
717
|
-
const last = joined?.ok ? joined.shas.at(-1) : undefined
|
|
718
|
-
const undoneFiles =
|
|
719
|
-
last === undefined
|
|
720
|
-
? []
|
|
721
|
-
: await git.filesBetween({
|
|
722
|
-
cwd: runBranch.path,
|
|
723
|
-
from: `${first_sha}^`,
|
|
724
|
-
to: last,
|
|
725
|
-
})
|
|
726
|
-
const { undone } = await git.undoReplay({ cwd: runBranch.path, first_sha })
|
|
727
|
-
// A redo whose first try undid the join before a crash finds nothing
|
|
728
|
-
// left to undo: the commits it undid are the journaled join's.
|
|
729
|
-
const named =
|
|
730
|
-
undone.length === 0 && context.step.redo && joined?.ok === true
|
|
731
|
-
? joined.shas
|
|
732
|
-
: undone
|
|
733
|
-
if (dependenciesChanged({ changed_files: undoneFiles })) {
|
|
734
|
-
await installIn({
|
|
735
|
-
journal,
|
|
736
|
-
cwd: runBranch.path,
|
|
737
|
-
target: 'run_branch',
|
|
738
|
-
ticket: null,
|
|
739
|
-
})
|
|
740
|
-
}
|
|
741
|
-
return named
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
/**
|
|
745
|
-
* Puts a joined ticket's change back on top of the run branch: undoes the
|
|
746
|
-
* join first if asked (its gates failed), then resets the ticket's worktree
|
|
747
|
-
* to the run branch's tip and applies the ticket's whole diff there,
|
|
748
|
-
* uncommitted, with conflict markers in the files that clash. Journals the
|
|
749
|
-
* clashed files split into tests and code.
|
|
750
|
-
*/
|
|
751
|
-
const rebaseTicket = async ({
|
|
752
|
-
context,
|
|
753
|
-
action,
|
|
754
|
-
}: {
|
|
755
|
-
context: BuildContext
|
|
756
|
-
action: Extract<BuildAction, { type: 'rebase_ticket' }>
|
|
757
|
-
}) => {
|
|
758
|
-
const { git, state, journal, config } = context
|
|
759
|
-
const worktree = ticketWorktree({ state, ticket: action.ticket })
|
|
760
|
-
const runBranch = need({ value: state.run_branch, what: 'run branch' })
|
|
761
|
-
const progress = state.tickets[action.ticket]
|
|
762
|
-
// The ticket's latest commit: a review fix round's, or the green one.
|
|
763
|
-
const head = need({
|
|
764
|
-
value: progress?.commits.fix ?? progress?.commits.green,
|
|
765
|
-
what: `green commit for #${action.ticket}`,
|
|
766
|
-
})
|
|
767
|
-
const undone =
|
|
768
|
-
action.undo_first_sha === null
|
|
769
|
-
? []
|
|
770
|
-
: await undoJoin({
|
|
771
|
-
context,
|
|
772
|
-
ticket: action.ticket,
|
|
773
|
-
first_sha: action.undo_first_sha,
|
|
774
|
-
})
|
|
775
|
-
const onto = await git.head({ cwd: runBranch.path })
|
|
776
|
-
const moved_files = await git.filesBetween({
|
|
777
|
-
cwd: worktree.path,
|
|
778
|
-
from: worktree.base_sha,
|
|
779
|
-
to: onto,
|
|
780
|
-
})
|
|
781
|
-
const ticket_files = await git.filesBetween({
|
|
782
|
-
cwd: worktree.path,
|
|
783
|
-
from: worktree.base_sha,
|
|
784
|
-
to: head,
|
|
785
|
-
})
|
|
786
|
-
const { conflicts } = await git.rebaseWorktree({
|
|
787
|
-
cwd: worktree.path,
|
|
788
|
-
from: worktree.base_sha,
|
|
789
|
-
to: head,
|
|
790
|
-
onto,
|
|
791
|
-
})
|
|
792
|
-
const tests = testFilesAmong({
|
|
793
|
-
files: conflicts,
|
|
794
|
-
test_file_patterns: config.test_file_patterns,
|
|
795
|
-
})
|
|
796
|
-
journal.append({
|
|
797
|
-
kind: 'ticket_rebased',
|
|
798
|
-
ticket: action.ticket,
|
|
799
|
-
role: null,
|
|
800
|
-
content: {
|
|
801
|
-
cause: action.cause,
|
|
802
|
-
base_sha: onto,
|
|
803
|
-
tests,
|
|
804
|
-
code: conflicts.filter((file) => !tests.includes(file)),
|
|
805
|
-
undone,
|
|
806
|
-
reinstall: rebaseNeedsInstall({ moved_files, ticket_files }),
|
|
807
|
-
},
|
|
808
|
-
})
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
/**
|
|
812
|
-
* The config's gates in a worktree (with the install first when a manifest
|
|
813
|
-
* changed since its base), journaled as `gates_run`.
|
|
814
|
-
*/
|
|
815
|
-
export const gatesIn = async ({
|
|
816
|
-
context,
|
|
817
|
-
worktree,
|
|
818
|
-
target,
|
|
819
|
-
ticket,
|
|
820
|
-
}: {
|
|
821
|
-
context: BuildContext
|
|
822
|
-
worktree: ReplayedWorktree
|
|
823
|
-
target: GateTarget
|
|
824
|
-
/** `null` for the final review. */
|
|
825
|
-
ticket: number | null
|
|
826
|
-
}) => {
|
|
827
|
-
const { path: cwd, base_sha } = worktree
|
|
828
|
-
const result = await runGates({
|
|
829
|
-
cwd,
|
|
830
|
-
config: context.config,
|
|
831
|
-
install: installCommand({
|
|
832
|
-
changed_files: difference(
|
|
833
|
-
await context.git.changedSince({ cwd, from: base_sha }),
|
|
834
|
-
context.state.prepare_made
|
|
835
|
-
),
|
|
836
|
-
target,
|
|
837
|
-
}),
|
|
838
|
-
around_prepare: (run) => watchPrepare({ context, cwd, ticket, run }),
|
|
839
|
-
test_files: await testFilesIn({ context, cwd }),
|
|
840
|
-
report_file: await reportFile({
|
|
841
|
-
context,
|
|
842
|
-
ticket,
|
|
843
|
-
label: `gates-${target}`,
|
|
844
|
-
}),
|
|
845
|
-
})
|
|
846
|
-
context.journal.append({
|
|
847
|
-
kind: 'gates_run',
|
|
848
|
-
ticket,
|
|
849
|
-
role: null,
|
|
850
|
-
content: { target, ...result },
|
|
851
|
-
})
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
/**
|
|
855
|
-
* The frozen install in a new (or moved) worktree, journaled as
|
|
856
|
-
* `dependencies_installed`. A worktree with no `package.json` has nothing
|
|
857
|
-
* to install (`check` is `null`).
|
|
858
|
-
*/
|
|
859
|
-
const installIn = async ({
|
|
860
|
-
journal,
|
|
861
|
-
cwd,
|
|
862
|
-
target,
|
|
863
|
-
ticket,
|
|
864
|
-
}: {
|
|
865
|
-
journal: Journal
|
|
866
|
-
cwd: string
|
|
867
|
-
target: GateTarget
|
|
868
|
-
ticket: number | null
|
|
869
|
-
}) => {
|
|
870
|
-
const command = newWorktreeInstall({
|
|
871
|
-
has_manifest: existsSync(join(cwd, MANIFEST)),
|
|
872
|
-
})
|
|
873
|
-
journal.append({
|
|
874
|
-
kind: 'dependencies_installed',
|
|
875
|
-
ticket,
|
|
876
|
-
role: null,
|
|
877
|
-
content: {
|
|
878
|
-
target,
|
|
879
|
-
check:
|
|
880
|
-
command === null
|
|
881
|
-
? null
|
|
882
|
-
: await shellCheck({ name: 'install', command, cwd }),
|
|
883
|
-
},
|
|
884
|
-
})
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
/**
|
|
888
|
-
* What a build step's executor works with, from the journal as it is now.
|
|
889
|
-
*
|
|
890
|
-
* @example
|
|
891
|
-
* const context = buildContext({ journal, tracker, git, launcher })
|
|
892
|
-
*/
|
|
893
|
-
export const buildContext = ({
|
|
894
|
-
journal,
|
|
895
|
-
tracker,
|
|
896
|
-
git,
|
|
897
|
-
launcher,
|
|
898
|
-
step,
|
|
899
|
-
}: BuildDeps & {
|
|
900
|
-
journal: Journal
|
|
901
|
-
tracker: Tracker
|
|
902
|
-
/** Left out, a first try, numbered after the journal's last record. */
|
|
903
|
-
step?: CommentStep
|
|
904
|
-
}): BuildContext => {
|
|
905
|
-
const records = journal.read()
|
|
906
|
-
const state = replayRun({ records })
|
|
907
|
-
const run_dir = dirname(journal.file)
|
|
908
|
-
return {
|
|
909
|
-
git,
|
|
910
|
-
launcher,
|
|
911
|
-
journal,
|
|
912
|
-
tracker,
|
|
913
|
-
state,
|
|
914
|
-
config: need({ value: state.config, what: 'engine config' }),
|
|
915
|
-
run_dir,
|
|
916
|
-
step: step ?? {
|
|
917
|
-
run_id: basename(run_dir),
|
|
918
|
-
first_seq: (records.at(-1)?.seq ?? 0) + 1,
|
|
919
|
-
redo: false,
|
|
920
|
-
},
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
61
|
/**
|
|
925
62
|
* Carries out one build step: git, gates, agents, or the tracker, then
|
|
926
63
|
* records what happened in the journal. `decide` picks the step.
|
|
@@ -935,12 +72,15 @@ export const executeBuildAction = async ({
|
|
|
935
72
|
}: BuildDeps & {
|
|
936
73
|
/**
|
|
937
74
|
* The final review's steps go to `executeFinalReviewAction`, the
|
|
938
|
-
* changeset to `writeChangeset`,
|
|
939
|
-
* `executeAlreadyDoneAction`.
|
|
75
|
+
* changeset to `writeChangeset`, the PR to `openPullRequest`, and an
|
|
76
|
+
* already-done ticket's to `executeAlreadyDoneAction`.
|
|
940
77
|
*/
|
|
941
78
|
action: Exclude<
|
|
942
79
|
BuildAction,
|
|
943
|
-
|
|
80
|
+
| FinalReviewAction
|
|
81
|
+
| AlreadyDoneAction
|
|
82
|
+
| { type: 'write_changeset' }
|
|
83
|
+
| { type: 'open_pull_request' }
|
|
944
84
|
>
|
|
945
85
|
journal: Journal
|
|
946
86
|
tracker: Tracker
|
|
@@ -1040,6 +180,8 @@ export const executeBuildAction = async ({
|
|
|
1040
180
|
cwd: path,
|
|
1041
181
|
commands: bunTestCommands(context),
|
|
1042
182
|
test_files: await testFilesIn({ context, cwd: path }),
|
|
183
|
+
timeout_ms: testTimeoutOf({ config: context.config }),
|
|
184
|
+
test_concurrency: context.config.test_concurrency,
|
|
1043
185
|
report_file: await reportFile({
|
|
1044
186
|
context,
|
|
1045
187
|
ticket: action.ticket,
|
|
@@ -1076,12 +218,21 @@ export const executeBuildAction = async ({
|
|
|
1076
218
|
kind: 'worktree_reset',
|
|
1077
219
|
ticket: action.ticket,
|
|
1078
220
|
role: null,
|
|
1079
|
-
content: {
|
|
221
|
+
content: {
|
|
222
|
+
sha,
|
|
223
|
+
...(action.bad_test === undefined
|
|
224
|
+
? {}
|
|
225
|
+
: { bad_test: action.bad_test }),
|
|
226
|
+
},
|
|
1080
227
|
})
|
|
1081
228
|
return
|
|
1082
229
|
}
|
|
230
|
+
case 'send_tests_back':
|
|
231
|
+
return sendTestsBack({ context, action })
|
|
1083
232
|
case 'run_red_check':
|
|
1084
233
|
return runRedCheck({ context, action })
|
|
234
|
+
case 'check_already_done':
|
|
235
|
+
return checkAlreadyDoneIn({ context, action })
|
|
1085
236
|
case 'commit_ticket':
|
|
1086
237
|
return commitTicket({ context, action })
|
|
1087
238
|
case 'run_gates':
|
|
@@ -1093,6 +244,7 @@ export const executeBuildAction = async ({
|
|
|
1093
244
|
: need({ value: state.run_branch, what: 'run branch' }),
|
|
1094
245
|
target: action.target,
|
|
1095
246
|
ticket: action.ticket,
|
|
247
|
+
exclusive: action.exclusive,
|
|
1096
248
|
})
|
|
1097
249
|
case 'join_run_branch': {
|
|
1098
250
|
const worktree = ticketWorktree({ state, ticket: action.ticket })
|
|
@@ -1154,20 +306,22 @@ export const executeBuildAction = async ({
|
|
|
1154
306
|
})
|
|
1155
307
|
return
|
|
1156
308
|
}
|
|
1157
|
-
case 'undo_join':
|
|
309
|
+
case 'undo_join': {
|
|
310
|
+
const shas = await undoJoin({
|
|
311
|
+
context,
|
|
312
|
+
ticket: action.ticket,
|
|
313
|
+
first_sha: action.first_sha,
|
|
314
|
+
})
|
|
315
|
+
// A refused undo left the run branch alone and the ticket stuck.
|
|
316
|
+
if (shas === null) return
|
|
1158
317
|
journal.append({
|
|
1159
318
|
kind: 'join_undone',
|
|
1160
319
|
ticket: action.ticket,
|
|
1161
320
|
role: null,
|
|
1162
|
-
content: {
|
|
1163
|
-
shas: await undoJoin({
|
|
1164
|
-
context,
|
|
1165
|
-
ticket: action.ticket,
|
|
1166
|
-
first_sha: action.first_sha,
|
|
1167
|
-
}),
|
|
1168
|
-
},
|
|
321
|
+
content: { shas },
|
|
1169
322
|
})
|
|
1170
323
|
return
|
|
324
|
+
}
|
|
1171
325
|
case 'retry_ticket':
|
|
1172
326
|
return retryTicket({ context, action })
|
|
1173
327
|
case 'report_stuck':
|
|
@@ -1178,7 +332,7 @@ export const executeBuildAction = async ({
|
|
|
1178
332
|
case 'report_final_review_stuck':
|
|
1179
333
|
case 'ship_final_review':
|
|
1180
334
|
case 'retry_final_review':
|
|
1181
|
-
//
|
|
335
|
+
// Stuck work's steps; `executeAction` carries them out.
|
|
1182
336
|
throw new Error(`${action.type} is not a git or agent step.`)
|
|
1183
337
|
case 'mark_stuck':
|
|
1184
338
|
// A join crashes cut off too often leaves no half of it behind.
|
|
@@ -1198,36 +352,6 @@ export const executeBuildAction = async ({
|
|
|
1198
352
|
content: { reason: action.reason, detail: action.detail },
|
|
1199
353
|
})
|
|
1200
354
|
return
|
|
1201
|
-
case 'open_pull_request': {
|
|
1202
|
-
const { head, base, title, body } = action
|
|
1203
|
-
// A redo adopts the PR its first try opened before the crash.
|
|
1204
|
-
const adopted = context.step.redo
|
|
1205
|
-
? await tracker.findOpenPullRequest({ head })
|
|
1206
|
-
: null
|
|
1207
|
-
// The spec's open tickets not in the run are named in the PR.
|
|
1208
|
-
const full = withNotInRun({
|
|
1209
|
-
body,
|
|
1210
|
-
open: await openTicketsNotInRun({
|
|
1211
|
-
tracker,
|
|
1212
|
-
state: context.state,
|
|
1213
|
-
}),
|
|
1214
|
-
})
|
|
1215
|
-
const opened =
|
|
1216
|
-
adopted ??
|
|
1217
|
-
(await tracker.openPullRequest({
|
|
1218
|
-
head,
|
|
1219
|
-
base,
|
|
1220
|
-
title,
|
|
1221
|
-
body: full,
|
|
1222
|
-
}))
|
|
1223
|
-
journal.append({
|
|
1224
|
-
kind: 'pull_request_opened',
|
|
1225
|
-
ticket: null,
|
|
1226
|
-
role: null,
|
|
1227
|
-
content: { ...opened, head, base, title, body: full },
|
|
1228
|
-
})
|
|
1229
|
-
return
|
|
1230
|
-
}
|
|
1231
355
|
case 'done':
|
|
1232
356
|
case 'invalid_journal':
|
|
1233
357
|
return
|