@alecsibilia/luca 14.0.0-alpha.2 → 14.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +76 -0
- package/README.md +7 -2
- package/board/client/board-look.ts +31 -1
- package/board/client/board-panel-parts.tsx +150 -0
- package/board/client/board-panel-stages.ts +99 -0
- package/board/client/board-panel-styles.ts +223 -0
- package/board/client/board-panel.tsx +77 -1275
- package/board/client/engine-life.ts +48 -0
- package/board/client/final-review-stack.tsx +144 -0
- package/board/client/heartbeat.tsx +61 -0
- package/board/client/helper-lines.tsx +69 -0
- package/board/client/live-feed.tsx +86 -0
- package/board/client/live-line.ts +73 -0
- package/board/client/model-line.tsx +178 -0
- package/board/client/needs-you-section.tsx +133 -0
- package/board/client/one-line.tsx +193 -0
- package/board/client/repo-wait-banner.tsx +69 -0
- package/board/client/run-footer.tsx +68 -0
- package/board/client/run-header.tsx +189 -0
- package/board/client/run-picker.tsx +98 -0
- package/board/client/status-bar.tsx +131 -0
- package/board/client/step-dots.tsx +127 -0
- package/board/client/stuck-actions.tsx +269 -0
- package/board/client/ticket-card-view.tsx +238 -0
- package/board/client/ticket-detail.tsx +87 -0
- package/board/client/ticket-stack.tsx +119 -0
- package/board/client/ticket-wait.ts +188 -0
- package/board/client/timeline-rows.tsx +41 -14
- package/board/client/usage-line.tsx +51 -0
- package/board/client/wait-line.tsx +39 -0
- package/board/index.client.tsx +1 -1
- package/board/index.server.ts +7 -1
- package/board/package.json +1 -1
- package/board/server/board-panel-state-helpers.ts +67 -0
- package/board/server/board-server-check.ts +294 -0
- package/board/server/board-server-engine.ts +129 -0
- package/board/server/board-server-events.ts +134 -0
- package/board/server/board-server-read.ts +190 -0
- package/board/server/board-server-resume.ts +164 -0
- package/board/server/board-server-start.ts +174 -0
- package/board/server/board-server-store.ts +281 -0
- package/board/server/board-server-types.ts +114 -0
- package/board/server/board-server-util.ts +4 -0
- package/board/server/board-server.ts +52 -943
- package/board/server/board-vocabulary-engine.ts +144 -0
- package/board/server/board-vocabulary-review.ts +115 -0
- package/board/server/board-vocabulary-run.ts +145 -0
- package/board/server/board-vocabulary-shapes.ts +153 -0
- package/board/server/board-vocabulary.ts +20 -375
- package/board/server/current-step-panel-helpers.ts +108 -0
- package/board/server/describe-record.ts +403 -0
- package/board/server/engine-launch.ts +20 -3
- package/board/server/engine-restart-helpers.ts +105 -0
- package/board/server/engine-watch.ts +45 -5
- package/board/server/journal-rebuild-helpers.ts +169 -0
- package/board/server/live-file.ts +130 -0
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +14 -629
- package/board/server/record-texts.ts +350 -0
- package/board/server/reduce-board-agent-start.ts +224 -0
- package/board/server/reduce-board-agents.ts +160 -0
- package/board/server/reduce-board-counters.ts +138 -0
- package/board/server/reduce-board-create.ts +148 -0
- package/board/server/reduce-board-final-review.ts +174 -0
- package/board/server/reduce-board-needs-you.ts +283 -0
- package/board/server/reduce-board-reasons.ts +59 -0
- package/board/server/reduce-board-run.ts +255 -0
- package/board/server/reduce-board-settle.ts +88 -0
- package/board/server/reduce-board-steps.ts +226 -0
- package/board/server/reduce-board-text.ts +139 -0
- package/board/server/reduce-board-ticket-checks.ts +168 -0
- package/board/server/reduce-board-ticket-join.ts +144 -0
- package/board/server/reduce-board-ticket-start.ts +132 -0
- package/board/server/reduce-board-update.ts +117 -0
- package/board/server/reduce-board-usage.ts +247 -0
- package/board/server/reduce-board.ts +100 -1917
- package/board/server/reply-post.ts +407 -0
- package/board/server/run-registry.ts +8 -4
- package/board/server/ticket-stages-panel-helpers.ts +296 -0
- package/board/shared/board-rows.ts +9 -0
- package/board/shared/board-rpc.ts +42 -0
- package/board/shared/board-state-counts.ts +77 -0
- package/board/shared/board-state-live.ts +99 -0
- package/board/shared/board-state-review.ts +59 -0
- package/board/shared/board-state-run.ts +236 -0
- package/board/shared/board-state-ticket.ts +257 -0
- package/board/shared/board-state-usage.ts +107 -0
- package/board/shared/board-state.ts +97 -567
- package/board/shared/reply-actions.ts +207 -0
- package/engine/agents/agent-launcher.ts +27 -4
- package/engine/agents/claude-launcher-helpers.ts +233 -0
- package/engine/agents/claude-launcher.ts +28 -669
- package/engine/agents/claude-messages.ts +212 -0
- package/engine/agents/claude-options.ts +43 -8
- package/engine/agents/common-instructions.ts +138 -0
- package/engine/agents/input-channel.ts +36 -0
- package/engine/agents/launch-paths.ts +29 -0
- package/engine/agents/launch-session.ts +227 -0
- package/engine/agents/learner-instructions.ts +31 -0
- package/engine/agents/open-session.ts +210 -0
- package/engine/agents/prompt-sections.ts +95 -0
- package/engine/agents/rejoin-prompts.ts +135 -0
- package/engine/agents/reviewer-instructions.ts +99 -0
- package/engine/agents/role-instructions.ts +31 -244
- package/engine/agents/role-prompts.ts +39 -109
- package/engine/agents/role-results.ts +106 -7
- package/engine/agents/scripted-launcher.ts +21 -6
- package/engine/agents/session-pool.ts +124 -0
- package/engine/agents/turn-progress.ts +283 -0
- package/engine/agents/writer-instructions.ts +151 -0
- package/engine/cli/computer-adapters-real.ts +15 -3
- package/engine/cli/computer-adapters.ts +2 -0
- package/engine/cli/computer-checks.ts +87 -4
- package/engine/cli/config-reload-text.ts +39 -0
- package/engine/cli/decision-model-setup.ts +70 -0
- package/engine/cli/doctor-command.ts +1 -0
- package/engine/cli/doctor.ts +21 -3
- package/engine/cli/going-runs.ts +192 -36
- package/engine/cli/init-command.ts +1 -0
- package/engine/cli/init.ts +23 -1
- package/engine/cli/live-runs.ts +73 -0
- package/engine/cli/luca-run.ts +42 -7
- package/engine/cli/luca-skills.ts +183 -0
- package/engine/cli/luca.ts +8 -1
- package/engine/cli/models-command.ts +119 -0
- package/engine/cli/read-run.ts +81 -0
- package/engine/cli/run-args.ts +33 -6
- package/engine/cli/run-demo.ts +143 -0
- package/engine/cli/run-end.ts +184 -0
- package/engine/cli/run-modes-helpers.ts +81 -0
- package/engine/cli/run-modes.ts +62 -439
- package/engine/cli/setup.ts +5 -48
- package/engine/cli/unfinished-runs.ts +122 -0
- package/engine/cli/upgrade-command.ts +212 -30
- package/engine/cli/upgrade-finish.ts +149 -0
- package/engine/cli/upgrade.ts +112 -71
- package/engine/config/config-fields.ts +127 -0
- package/engine/config/config-reload.ts +250 -0
- package/engine/config/engine-config-schema.ts +235 -0
- package/engine/config/engine-config.ts +72 -142
- package/engine/config/test-commands.ts +102 -0
- package/engine/core/agent-guards-helpers.ts +84 -0
- package/engine/core/agent-turn.ts +395 -0
- package/engine/core/already-done.ts +44 -1
- package/engine/core/build-action.ts +214 -0
- package/engine/core/build-context.ts +135 -0
- package/engine/core/build-launch.ts +259 -0
- package/engine/core/commit-in.ts +282 -0
- package/engine/core/decide-build-early.ts +108 -0
- package/engine/core/decide-build-ending.ts +145 -0
- package/engine/core/decide-build-helpers.ts +87 -0
- package/engine/core/decide-build-tickets.ts +207 -0
- package/engine/core/decide-build-view.ts +103 -0
- package/engine/core/decide-build.ts +101 -1368
- package/engine/core/decide-final-fix.ts +203 -0
- package/engine/core/decide-final-review.ts +55 -310
- package/engine/core/decide-red-green.ts +398 -0
- package/engine/core/decide-rejoin.ts +259 -0
- package/engine/core/decide-repo-wait.ts +129 -0
- package/engine/core/decide-run-budget.ts +86 -12
- package/engine/core/decide-stuck-helpers.ts +127 -0
- package/engine/core/decide-stuck.ts +60 -8
- package/engine/core/decide-ticket-review.ts +95 -0
- package/engine/core/decide-ticket.ts +267 -0
- package/engine/core/decide-waits.ts +47 -0
- package/engine/core/decide.ts +74 -23
- package/engine/core/engine-chores.ts +163 -0
- package/engine/core/environment-rerun.ts +83 -0
- package/engine/core/evidence-text.ts +84 -0
- package/engine/core/execute-action.ts +303 -0
- package/engine/core/execute-already-done-check.ts +133 -0
- package/engine/core/execute-build.ts +68 -944
- package/engine/core/execute-changeset.ts +5 -7
- package/engine/core/execute-final-review.ts +125 -13
- package/engine/core/execute-gates.ts +182 -0
- package/engine/core/execute-intake.ts +130 -0
- package/engine/core/execute-join.ts +276 -0
- package/engine/core/execute-memory.ts +12 -0
- package/engine/core/execute-pull-request.ts +84 -0
- package/engine/core/execute-red-check.ts +219 -0
- package/engine/core/execute-repo-wait.ts +129 -0
- package/engine/core/execute-stuck.ts +25 -10
- package/engine/core/execute-waits.ts +158 -0
- package/engine/core/execute-with-jev.ts +118 -0
- package/engine/core/execute.ts +124 -831
- package/engine/core/final-failure-cap.ts +38 -0
- package/engine/core/final-review-action.ts +125 -0
- package/engine/core/final-review-text.ts +63 -17
- package/engine/core/final-stuck-text.ts +86 -0
- package/engine/core/fix-loop-text.ts +49 -2
- package/engine/core/left-out.ts +120 -0
- package/engine/core/live-file.ts +372 -0
- package/engine/core/loop-caps.ts +25 -2
- package/engine/core/not-in-run.ts +7 -3
- package/engine/core/pull-request-text.ts +255 -6
- package/engine/core/repo-wait-text.ts +81 -0
- package/engine/core/review-text.ts +55 -1
- package/engine/core/run-crash-recovery-helpers.ts +16 -0
- package/engine/core/run-session-close-helpers.ts +96 -0
- package/engine/core/run-start.ts +148 -0
- package/engine/core/run-stuck-text.ts +86 -0
- package/engine/core/schedule-rules.ts +193 -0
- package/engine/core/stuck-detail-text.ts +89 -0
- package/engine/core/stuck-format.ts +22 -0
- package/engine/core/stuck-text.ts +29 -332
- package/engine/core/ticket-stuck-text.ts +192 -0
- package/engine/core/worktree-changes.ts +106 -0
- package/engine/gates/already-done-check.ts +120 -0
- package/engine/gates/feature-map.ts +96 -0
- package/engine/gates/gate-runner.ts +108 -108
- package/engine/gates/gate-schemas.ts +61 -2
- package/engine/gates/leftover-scan.ts +154 -31
- package/engine/gates/new-tests.ts +45 -0
- package/engine/gates/own-files-check.ts +98 -0
- package/engine/gates/prepare-check.ts +60 -0
- package/engine/gates/prepare-slots.ts +99 -0
- package/engine/gates/red-check.ts +184 -16
- package/engine/gates/shell-check.ts +64 -0
- package/engine/gates/slot-waits.ts +55 -0
- package/engine/gates/slow-test-runner.ts +59 -0
- package/engine/gates/test-checks.ts +161 -0
- package/engine/gates/test-runner.ts +164 -5
- package/engine/gates/test-slots.ts +48 -0
- package/engine/git/git-adapter.ts +55 -396
- package/engine/git/git-commits.ts +132 -0
- package/engine/git/git-join.ts +139 -0
- package/engine/git/git-read.ts +123 -0
- package/engine/git/git-run.ts +32 -0
- package/engine/git/git-worktrees.ts +143 -0
- package/engine/git/outside-links.ts +55 -0
- package/engine/guards/after-turn-check.ts +31 -5
- package/engine/guards/guard-hook.ts +60 -12
- package/engine/guards/guard-roles.ts +68 -0
- package/engine/guards/helper-rules.ts +92 -0
- package/engine/guards/permission-rules.ts +161 -0
- package/engine/guards/role-rules-helpers.ts +48 -0
- package/engine/guards/role-rules.ts +34 -606
- package/engine/guards/shell-rules.ts +344 -0
- package/engine/guards/worktree-git.ts +208 -0
- package/engine/guards/worktree-snapshot.ts +412 -0
- package/engine/guards/worktree-state.ts +28 -825
- package/engine/guards/worktree-undo.ts +260 -0
- package/engine/guards/write-rules.ts +149 -0
- package/engine/index.ts +30 -9
- package/engine/intake/criteria-checks.ts +114 -0
- package/engine/intake/intake-checks.ts +164 -30
- package/engine/intake/intake-schemas.ts +37 -2
- package/engine/jev/clef-client.ts +259 -0
- package/engine/jev/decision-model-credentials.ts +212 -0
- package/engine/jev/jev-jobs.ts +144 -60
- package/engine/jev/jev-schemas.ts +86 -5
- package/engine/jev/jev-shadow-helpers.ts +218 -0
- package/engine/jev/jev-shadow.ts +109 -64
- package/engine/jev/model-pick.ts +275 -0
- package/engine/journal/agent-entries.ts +167 -0
- package/engine/journal/agent-message-entries.ts +72 -0
- package/engine/journal/agent-turn-replay.ts +203 -0
- package/engine/journal/already-done-entries.ts +55 -0
- package/engine/journal/commit-entries.ts +56 -0
- package/engine/journal/cross-ticket-replay.ts +222 -0
- package/engine/journal/decision-model-entries.ts +86 -0
- package/engine/journal/entry-fields.ts +10 -0
- package/engine/journal/final-review-entries.ts +128 -0
- package/engine/journal/final-review-replay.ts +324 -0
- package/engine/journal/final-review-round.ts +320 -0
- package/engine/journal/final-review-state.ts +180 -0
- package/engine/journal/gate-entries.ts +81 -0
- package/engine/journal/intake-entries.ts +87 -0
- package/engine/journal/join-entries.ts +126 -0
- package/engine/journal/journal-entry-schema.ts +218 -0
- package/engine/journal/journal-kinds.ts +97 -0
- package/engine/journal/journal-record-schema.ts +218 -0
- package/engine/journal/journal-record.ts +43 -1400
- package/engine/journal/journal.ts +6 -1
- package/engine/journal/limit-entries.ts +90 -0
- package/engine/journal/memory-entries.ts +124 -0
- package/engine/journal/memory-replay.ts +157 -0
- package/engine/journal/plan-replay.ts +139 -0
- package/engine/journal/pull-request-entries.ts +41 -0
- package/engine/journal/replay.ts +157 -2144
- package/engine/journal/reply-replay.ts +144 -0
- package/engine/journal/repo-wait-entries.ts +53 -0
- package/engine/journal/run-entries.ts +172 -0
- package/engine/journal/run-state.ts +264 -0
- package/engine/journal/shared-failure-replay.ts +133 -0
- package/engine/journal/stuck-entries.ts +193 -0
- package/engine/journal/ticket-progress.ts +392 -0
- package/engine/journal/ticket-replay.ts +411 -0
- package/engine/journal/ticket-result-replay.ts +364 -0
- package/engine/journal/ticket-retry-replay.ts +142 -0
- package/engine/journal/worktree-entries.ts +122 -0
- package/engine/models/model-score.ts +512 -0
- package/engine/shell/run-command.ts +187 -18
- package/engine/testing/agent-turn-fixtures.ts +287 -0
- package/engine/testing/already-done-fixtures.ts +97 -0
- package/engine/testing/build-fixtures.ts +68 -973
- package/engine/testing/checkouts-fixtures.ts +48 -0
- package/engine/testing/final-review-fixtures.ts +62 -1
- package/engine/testing/final-review-steps.ts +143 -0
- package/engine/testing/limit-fixtures.ts +85 -0
- package/engine/testing/live-file-fixtures.ts +57 -0
- package/engine/testing/practice-repo.ts +10 -4
- package/engine/testing/practice-run.ts +1 -1
- package/engine/testing/reply-fixtures.ts +88 -0
- package/engine/testing/run-budget-fixtures.ts +165 -0
- package/engine/testing/run-fixtures.ts +219 -0
- package/engine/testing/sdk-play.ts +177 -0
- package/engine/testing/setup-repo.ts +194 -0
- package/engine/testing/step-record-fixtures.ts +55 -0
- package/engine/testing/ticket-step-fixtures.ts +272 -0
- package/engine/testing/wait-scenarios.ts +186 -0
- package/engine/tracker/gh-retry.ts +217 -0
- package/engine/tracker/github-tracker.ts +310 -90
- package/engine/tracker/in-memory-tracker.ts +8 -4
- package/engine/tracker/tracker.ts +131 -6
- package/package.json +2 -1
- package/skills/luca-retro/SKILL.md +74 -0
- package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
- package/skills/luca-unstick/SKILL.md +153 -0
- package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
- package/engine/jev/jev-client.ts +0 -173
package/engine/core/execute.ts
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import type {
|
|
10
|
-
import type
|
|
11
|
-
import {
|
|
1
|
+
/**
|
|
2
|
+
* `runEngine`: the scheduler that runs the decision step's actions, tickets'
|
|
3
|
+
* steps at the same time. Re-exports the run's first records and
|
|
4
|
+
* `executeAction` from the modules that carry them out.
|
|
5
|
+
*/
|
|
6
|
+
import { decideNext, type EngineAction } from './decide'
|
|
7
|
+
import { repoWaitUnchecked } from './decide-repo-wait'
|
|
8
|
+
import { executeAction, type StepTry } from './execute-action'
|
|
9
|
+
import type { BuildDeps } from './execute-build'
|
|
10
|
+
import { readEarlierRuns, type GoingRunsReader } from './execute-repo-wait'
|
|
11
|
+
import { executeWithJev } from './execute-with-jev'
|
|
12
|
+
import { createLiveFile } from './live-file'
|
|
12
13
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
agentRoleOf,
|
|
15
|
+
crashesIn,
|
|
16
|
+
followUpSessionOf,
|
|
17
|
+
keyOf,
|
|
18
|
+
redosFirst,
|
|
19
|
+
SLOW_ACTIONS,
|
|
20
|
+
stepOf,
|
|
21
|
+
ticketOf,
|
|
22
|
+
usesRunBranch,
|
|
23
|
+
WAIT_ACTIONS,
|
|
24
|
+
} from './schedule-rules'
|
|
21
25
|
import {
|
|
22
26
|
closeSessions,
|
|
23
27
|
finishedSessions,
|
|
@@ -25,45 +29,24 @@ import {
|
|
|
25
29
|
} from './session-close'
|
|
26
30
|
|
|
27
31
|
import type { BoardSync } from '../board/board-sync'
|
|
28
|
-
import
|
|
29
|
-
import {
|
|
30
|
-
import { outsideBlockerNumbers } from '../intake/intake-checks'
|
|
31
|
-
import type { IntakeProblem } from '../intake/intake-schemas'
|
|
32
|
-
import { jevAsksAfter, jevAsksBefore } from '../jev/jev-jobs'
|
|
33
|
-
import { askJevInShadow, type JevShadow } from '../jev/jev-shadow'
|
|
32
|
+
import { hearSlotWaits } from '../gates/slot-waits'
|
|
33
|
+
import { shadowAsker, type JevShadow } from '../jev/jev-shadow'
|
|
34
34
|
import type { Journal } from '../journal/journal'
|
|
35
35
|
import type { JournalRecord } from '../journal/journal-record'
|
|
36
|
-
import { replayRun } from '../journal/replay'
|
|
37
36
|
import {
|
|
38
|
-
crashesAfter,
|
|
39
37
|
resumeEntry,
|
|
40
38
|
stepFirstSeq,
|
|
41
39
|
type CrashCounts,
|
|
42
40
|
} from '../journal/step-records'
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
limitWaitComment,
|
|
46
|
-
SYSTEM_CLOCK,
|
|
47
|
-
waitUntil,
|
|
48
|
-
type EngineClock,
|
|
49
|
-
} from '../limits/limit-wait'
|
|
50
|
-
import {
|
|
51
|
-
DEFAULT_USAGE_LINES,
|
|
52
|
-
LINE_OF,
|
|
53
|
-
runReadings,
|
|
54
|
-
usageLineEndedComment,
|
|
55
|
-
usageLineWaitComment,
|
|
56
|
-
type UsageLineDeps,
|
|
57
|
-
type UsageLines,
|
|
58
|
-
} from '../limits/usage-line'
|
|
41
|
+
import type { EngineClock } from '../limits/limit-wait'
|
|
42
|
+
import { runReadings, type UsageLineDeps } from '../limits/usage-line'
|
|
59
43
|
import type { MemoryDeps } from '../memory/memory-client'
|
|
60
|
-
import {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
} from '../tracker/tracker'
|
|
44
|
+
import type { Tracker } from '../tracker/tracker'
|
|
45
|
+
|
|
46
|
+
export { executeAction, type StepTry } from './execute-action'
|
|
47
|
+
export { refusalComment } from './execute-intake'
|
|
48
|
+
export { recordConfigReload, recordResume, startRun } from './run-start'
|
|
49
|
+
export { keyOf, stepOf } from './schedule-rules'
|
|
67
50
|
|
|
68
51
|
/** How many actions `runEngine` takes before it gives up, as a safety net. */
|
|
69
52
|
export const DEFAULT_MAX_STEPS = 1000
|
|
@@ -74,770 +57,9 @@ export const STOP_ACTIONS: ReadonlySet<EngineAction['type']> = new Set([
|
|
|
74
57
|
'invalid_journal',
|
|
75
58
|
])
|
|
76
59
|
|
|
77
|
-
/**
|
|
78
|
-
* Starts a run by writing its first record. The spec and config it names are
|
|
79
|
-
* what every later step of the run works from; the Luca version it names is
|
|
80
|
-
* the one the run started on.
|
|
81
|
-
*/
|
|
82
|
-
export const startRun = ({
|
|
83
|
-
journal,
|
|
84
|
-
spec_number,
|
|
85
|
-
config,
|
|
86
|
-
base_branch,
|
|
87
|
-
memory,
|
|
88
|
-
repo,
|
|
89
|
-
luca_version = lucaVersion(),
|
|
90
|
-
}: {
|
|
91
|
-
journal: Journal
|
|
92
|
-
spec_number: number
|
|
93
|
-
config: EngineConfig
|
|
94
|
-
/** The branch the run branch starts from. Defaults to `main`. */
|
|
95
|
-
base_branch?: string
|
|
96
|
-
/**
|
|
97
|
-
* Turns memory on (#370), with the project's vault (`null` searches only
|
|
98
|
-
* `default`). Leave it out for a run without memory.
|
|
99
|
-
*/
|
|
100
|
-
memory?: { project_vault: string | null }
|
|
101
|
-
/** The repo the run is on, so a resume can find it. Defaults to `null`. */
|
|
102
|
-
repo?: string | null
|
|
103
|
-
/** Luca's version. Defaults to the engine's own. */
|
|
104
|
-
luca_version?: string
|
|
105
|
-
}): JournalRecord =>
|
|
106
|
-
journal.append({
|
|
107
|
-
kind: 'run_started',
|
|
108
|
-
ticket: null,
|
|
109
|
-
role: null,
|
|
110
|
-
content: {
|
|
111
|
-
spec_number,
|
|
112
|
-
config,
|
|
113
|
-
base_branch,
|
|
114
|
-
memory: memory ?? null,
|
|
115
|
-
repo,
|
|
116
|
-
luca_version,
|
|
117
|
-
},
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Records that the engine started again on a run's journal, on Luca's
|
|
122
|
-
* version `luca_version`, before its first step.
|
|
123
|
-
*/
|
|
124
|
-
export const recordResume = ({
|
|
125
|
-
journal,
|
|
126
|
-
luca_version = lucaVersion(),
|
|
127
|
-
}: {
|
|
128
|
-
journal: Journal
|
|
129
|
-
/** Luca's version. Defaults to the engine's own. */
|
|
130
|
-
luca_version?: string
|
|
131
|
-
}): JournalRecord =>
|
|
132
|
-
journal.append({
|
|
133
|
-
kind: 'engine_resumed',
|
|
134
|
-
ticket: null,
|
|
135
|
-
role: null,
|
|
136
|
-
content: { luca_version },
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
/** The comment a bad spec or ticket gets when intake refuses the run. */
|
|
140
|
-
export const refusalComment = ({
|
|
141
|
-
spec_number,
|
|
142
|
-
missing,
|
|
143
|
-
}: {
|
|
144
|
-
spec_number: number
|
|
145
|
-
missing: string[]
|
|
146
|
-
}): string =>
|
|
147
|
-
`Luca intake refused the run for spec #${spec_number}. This issue is not ready yet:\n\n` +
|
|
148
|
-
`${missing.map((line) => `- ${line}`).join('\n')}\n\n` +
|
|
149
|
-
`Fix these, move the issue back to \`${READY_LABEL}\`, and start the run again.`
|
|
150
|
-
|
|
151
|
-
const readIntake = async ({
|
|
152
|
-
spec_number,
|
|
153
|
-
journal,
|
|
154
|
-
tracker,
|
|
155
|
-
}: {
|
|
156
|
-
spec_number: number
|
|
157
|
-
journal: Journal
|
|
158
|
-
tracker: Tracker
|
|
159
|
-
}) => {
|
|
160
|
-
const spec = await tracker.readSpec({ spec_number })
|
|
161
|
-
const subTickets = await tracker.listSubTickets({ spec_number })
|
|
162
|
-
const outside = await Promise.all(
|
|
163
|
-
outsideBlockerNumbers({ sub_tickets: subTickets }).map((number) =>
|
|
164
|
-
tracker.readIssue({ number })
|
|
165
|
-
)
|
|
166
|
-
)
|
|
167
|
-
journal.append({
|
|
168
|
-
kind: 'intake_read',
|
|
169
|
-
ticket: null,
|
|
170
|
-
role: null,
|
|
171
|
-
content: {
|
|
172
|
-
spec,
|
|
173
|
-
sub_tickets: subTickets,
|
|
174
|
-
outside_blockers: outside.filter(
|
|
175
|
-
(issue): issue is TrackerIssue => issue !== null
|
|
176
|
-
),
|
|
177
|
-
},
|
|
178
|
-
})
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const refuseIntake = async ({
|
|
182
|
-
spec_number,
|
|
183
|
-
problems,
|
|
184
|
-
journal,
|
|
185
|
-
tracker,
|
|
186
|
-
step,
|
|
187
|
-
}: {
|
|
188
|
-
spec_number: number
|
|
189
|
-
problems: IntakeProblem[]
|
|
190
|
-
journal: Journal
|
|
191
|
-
tracker: Tracker
|
|
192
|
-
step: CommentStep
|
|
193
|
-
}) => {
|
|
194
|
-
for (const [n, { ticket, missing }] of problems.entries()) {
|
|
195
|
-
if (ticket === null) continue
|
|
196
|
-
await postCommentOnce({
|
|
197
|
-
tracker,
|
|
198
|
-
number: ticket,
|
|
199
|
-
body: refusalComment({ spec_number, missing }),
|
|
200
|
-
step,
|
|
201
|
-
n,
|
|
202
|
-
})
|
|
203
|
-
await tracker.addLabel({ number: ticket, label: NEEDS_INFO_LABEL })
|
|
204
|
-
await tracker.removeLabel({ number: ticket, label: READY_LABEL })
|
|
205
|
-
}
|
|
206
|
-
journal.append({
|
|
207
|
-
kind: 'intake_refused',
|
|
208
|
-
ticket: null,
|
|
209
|
-
role: null,
|
|
210
|
-
content: { problems },
|
|
211
|
-
})
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Carries out a limit wait or a billing stop. A limit wait's start tells the
|
|
216
|
-
* spec issue (unless it already heard of this reset) before it is journaled;
|
|
217
|
-
* the wait itself sleeps by `clock` until its end, so a restarted engine
|
|
218
|
-
* waits out only what is left.
|
|
219
|
-
*/
|
|
220
|
-
const executePlanAction = async ({
|
|
221
|
-
action,
|
|
222
|
-
journal,
|
|
223
|
-
tracker,
|
|
224
|
-
clock,
|
|
225
|
-
step,
|
|
226
|
-
}: {
|
|
227
|
-
action: Exclude<PlanAction, { type: 'done' }>
|
|
228
|
-
journal: Journal
|
|
229
|
-
tracker: Tracker
|
|
230
|
-
clock: EngineClock
|
|
231
|
-
step: CommentStep
|
|
232
|
-
}): Promise<void> => {
|
|
233
|
-
switch (action.type) {
|
|
234
|
-
case 'start_limit_wait': {
|
|
235
|
-
const { rate_limit_type, resets_at, until, ticket, role } = action
|
|
236
|
-
if (action.announce) {
|
|
237
|
-
await postCommentOnce({
|
|
238
|
-
tracker,
|
|
239
|
-
number: action.spec_number,
|
|
240
|
-
body: limitWaitComment({
|
|
241
|
-
rate_limit_type,
|
|
242
|
-
resets_at,
|
|
243
|
-
until,
|
|
244
|
-
}),
|
|
245
|
-
step,
|
|
246
|
-
n: 0,
|
|
247
|
-
})
|
|
248
|
-
}
|
|
249
|
-
journal.append({
|
|
250
|
-
kind: 'limit_wait_started',
|
|
251
|
-
ticket: null,
|
|
252
|
-
role: null,
|
|
253
|
-
content: {
|
|
254
|
-
resets_at,
|
|
255
|
-
until,
|
|
256
|
-
rate_limit_type,
|
|
257
|
-
hit_ticket: ticket,
|
|
258
|
-
hit_role: role,
|
|
259
|
-
},
|
|
260
|
-
})
|
|
261
|
-
return
|
|
262
|
-
}
|
|
263
|
-
case 'wait_for_limit':
|
|
264
|
-
await waitUntil({ clock, until: action.until })
|
|
265
|
-
journal.append({
|
|
266
|
-
kind: 'limit_wait_ended',
|
|
267
|
-
ticket: null,
|
|
268
|
-
role: null,
|
|
269
|
-
content: { until: action.until },
|
|
270
|
-
})
|
|
271
|
-
return
|
|
272
|
-
case 'stop_for_billing':
|
|
273
|
-
journal.append({
|
|
274
|
-
kind: 'run_stopped',
|
|
275
|
-
ticket: null,
|
|
276
|
-
role: null,
|
|
277
|
-
content: { reason: action.reason, role: null, billing: true },
|
|
278
|
-
})
|
|
279
|
-
return
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Carries out a usage-line wait. Its start tells the spec issue, then is
|
|
285
|
-
* journaled. The wait sleeps by `clock` in naps until its end, re-reading
|
|
286
|
-
* the lines before each nap: a line raised above the reading ends it at
|
|
287
|
-
* once. Its end tells the spec issue why the run carries on, then is
|
|
288
|
-
* journaled.
|
|
289
|
-
*/
|
|
290
|
-
const executeUsageLineAction = async ({
|
|
291
|
-
action,
|
|
292
|
-
journal,
|
|
293
|
-
tracker,
|
|
294
|
-
clock,
|
|
295
|
-
read_usage_lines,
|
|
296
|
-
step,
|
|
297
|
-
}: {
|
|
298
|
-
action: UsageLineAction
|
|
299
|
-
journal: Journal
|
|
300
|
-
tracker: Tracker
|
|
301
|
-
clock: EngineClock
|
|
302
|
-
read_usage_lines: () => Promise<UsageLines>
|
|
303
|
-
step: CommentStep
|
|
304
|
-
}): Promise<void> => {
|
|
305
|
-
const { spec_number, window, line, percent, until } = action
|
|
306
|
-
if (action.type === 'start_usage_line_wait') {
|
|
307
|
-
const { resets_at } = action
|
|
308
|
-
await postCommentOnce({
|
|
309
|
-
tracker,
|
|
310
|
-
number: spec_number,
|
|
311
|
-
body: usageLineWaitComment({ window, line, percent, resets_at }),
|
|
312
|
-
step,
|
|
313
|
-
n: 0,
|
|
314
|
-
})
|
|
315
|
-
journal.append({
|
|
316
|
-
kind: 'usage_line_wait_started',
|
|
317
|
-
ticket: null,
|
|
318
|
-
role: null,
|
|
319
|
-
content: { window, line, percent, resets_at, until },
|
|
320
|
-
})
|
|
321
|
-
return
|
|
322
|
-
}
|
|
323
|
-
const end = Date.parse(until)
|
|
324
|
-
let reason: 'reset' | 'line_raised' = 'reset'
|
|
325
|
-
for (;;) {
|
|
326
|
-
const lines = await read_usage_lines()
|
|
327
|
-
if (lines[LINE_OF[window]] > percent) {
|
|
328
|
-
reason = 'line_raised'
|
|
329
|
-
break
|
|
330
|
-
}
|
|
331
|
-
const left = end - clock.now()
|
|
332
|
-
if (left <= 0) break
|
|
333
|
-
await clock.sleep(Math.min(left, LIMIT_WAIT_NAP_MS))
|
|
334
|
-
}
|
|
335
|
-
await postCommentOnce({
|
|
336
|
-
tracker,
|
|
337
|
-
number: spec_number,
|
|
338
|
-
body: usageLineEndedComment({ window, percent, reason }),
|
|
339
|
-
step,
|
|
340
|
-
n: 0,
|
|
341
|
-
})
|
|
342
|
-
journal.append({
|
|
343
|
-
kind: 'usage_line_wait_ended',
|
|
344
|
-
ticket: null,
|
|
345
|
-
role: null,
|
|
346
|
-
content: { until, reason },
|
|
347
|
-
})
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Which try of its step an action is: the seq of the step's first try (its
|
|
352
|
-
* first `step_started`), and whether this try redoes one a crash cut off.
|
|
353
|
-
* A redo adopts the side effects its first try left behind: its comments,
|
|
354
|
-
* its PR, its worktrees and commits.
|
|
355
|
-
*/
|
|
356
|
-
export type StepTry = { first_seq: number; redo: boolean }
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Carries out one action: talks to the tracker, then records what happened in
|
|
360
|
-
* the journal. The only impure half of the engine; `decide` picks the action.
|
|
361
|
-
*
|
|
362
|
-
* Stop actions (`done`, `invalid_journal`) do nothing here.
|
|
363
|
-
*/
|
|
364
|
-
export const executeAction = async ({
|
|
365
|
-
action,
|
|
366
|
-
journal,
|
|
367
|
-
tracker,
|
|
368
|
-
build,
|
|
369
|
-
clock,
|
|
370
|
-
reply_poll_ms,
|
|
371
|
-
memory,
|
|
372
|
-
step,
|
|
373
|
-
read_usage_lines,
|
|
374
|
-
}: {
|
|
375
|
-
action: EngineAction
|
|
376
|
-
journal: Journal
|
|
377
|
-
tracker: Tracker
|
|
378
|
-
/** Needed for every step after intake. */
|
|
379
|
-
build?: BuildDeps
|
|
380
|
-
/** Limit waits and reply waits sleep by it. Defaults to `SYSTEM_CLOCK`. */
|
|
381
|
-
clock?: EngineClock
|
|
382
|
-
/** How long a reply wait sleeps before it reads the spec issue. */
|
|
383
|
-
reply_poll_ms?: number
|
|
384
|
-
/** The memory client, for a run with memory on (#370). */
|
|
385
|
-
memory?: MemoryDeps
|
|
386
|
-
/**
|
|
387
|
-
* Which try of its step this is, from the scheduler. Left out, it is a
|
|
388
|
-
* first try, numbered after the journal's last record.
|
|
389
|
-
*/
|
|
390
|
-
step?: StepTry
|
|
391
|
-
/**
|
|
392
|
-
* Reads the `luca-board` usage lines, again before each nap of a
|
|
393
|
-
* usage-line wait. Left out, the lines never change during a wait.
|
|
394
|
-
*/
|
|
395
|
-
read_usage_lines?: () => Promise<UsageLines>
|
|
396
|
-
}): Promise<void> => {
|
|
397
|
-
const tried: CommentStep = {
|
|
398
|
-
run_id: basename(dirname(journal.file)),
|
|
399
|
-
...(step ?? {
|
|
400
|
-
first_seq: (journal.read().at(-1)?.seq ?? 0) + 1,
|
|
401
|
-
redo: false,
|
|
402
|
-
}),
|
|
403
|
-
}
|
|
404
|
-
if (isMemoryAction(action)) {
|
|
405
|
-
return executeMemoryAction({
|
|
406
|
-
action,
|
|
407
|
-
journal,
|
|
408
|
-
tracker,
|
|
409
|
-
memory,
|
|
410
|
-
build,
|
|
411
|
-
step: tried,
|
|
412
|
-
})
|
|
413
|
-
}
|
|
414
|
-
switch (action.type) {
|
|
415
|
-
case 'report_stuck':
|
|
416
|
-
case 'wait_for_reply':
|
|
417
|
-
case 'take_reply':
|
|
418
|
-
case 'ignore_reply':
|
|
419
|
-
case 'skip_ticket':
|
|
420
|
-
case 'report_final_review_stuck':
|
|
421
|
-
case 'ship_final_review':
|
|
422
|
-
case 'retry_final_review':
|
|
423
|
-
case 'mark_run_stuck':
|
|
424
|
-
case 'report_run_stuck':
|
|
425
|
-
return executeStuckAction({
|
|
426
|
-
action,
|
|
427
|
-
journal,
|
|
428
|
-
tracker,
|
|
429
|
-
clock: clock ?? SYSTEM_CLOCK,
|
|
430
|
-
reply_poll_ms,
|
|
431
|
-
step: tried,
|
|
432
|
-
})
|
|
433
|
-
case 'stop_for_crashes':
|
|
434
|
-
journal.append({
|
|
435
|
-
kind: 'run_stopped',
|
|
436
|
-
ticket: null,
|
|
437
|
-
role: null,
|
|
438
|
-
content: {
|
|
439
|
-
reason: action.reason,
|
|
440
|
-
role: null,
|
|
441
|
-
billing: false,
|
|
442
|
-
crashed: true,
|
|
443
|
-
},
|
|
444
|
-
})
|
|
445
|
-
return
|
|
446
|
-
case 'record_usage':
|
|
447
|
-
journal.append({
|
|
448
|
-
kind: 'usage_recorded',
|
|
449
|
-
ticket: action.usage.ticket,
|
|
450
|
-
role: null,
|
|
451
|
-
content: action.usage,
|
|
452
|
-
})
|
|
453
|
-
return
|
|
454
|
-
case 'start_limit_wait':
|
|
455
|
-
case 'wait_for_limit':
|
|
456
|
-
case 'stop_for_billing':
|
|
457
|
-
return executePlanAction({
|
|
458
|
-
action,
|
|
459
|
-
journal,
|
|
460
|
-
tracker,
|
|
461
|
-
clock: clock ?? SYSTEM_CLOCK,
|
|
462
|
-
step: tried,
|
|
463
|
-
})
|
|
464
|
-
case 'start_usage_line_wait':
|
|
465
|
-
case 'wait_for_usage_line':
|
|
466
|
-
return executeUsageLineAction({
|
|
467
|
-
action,
|
|
468
|
-
journal,
|
|
469
|
-
tracker,
|
|
470
|
-
clock: clock ?? SYSTEM_CLOCK,
|
|
471
|
-
read_usage_lines:
|
|
472
|
-
read_usage_lines ??
|
|
473
|
-
(async () => ({
|
|
474
|
-
...DEFAULT_USAGE_LINES,
|
|
475
|
-
[LINE_OF[action.window]]: action.line,
|
|
476
|
-
})),
|
|
477
|
-
step: tried,
|
|
478
|
-
})
|
|
479
|
-
case 'read_intake':
|
|
480
|
-
return readIntake({
|
|
481
|
-
spec_number: action.spec_number,
|
|
482
|
-
journal,
|
|
483
|
-
tracker,
|
|
484
|
-
})
|
|
485
|
-
case 'refuse_intake':
|
|
486
|
-
return refuseIntake({
|
|
487
|
-
spec_number: action.spec_number,
|
|
488
|
-
problems: action.problems,
|
|
489
|
-
journal,
|
|
490
|
-
tracker,
|
|
491
|
-
step: tried,
|
|
492
|
-
})
|
|
493
|
-
case 'mark_already_done':
|
|
494
|
-
case 'close_ticket':
|
|
495
|
-
return executeAlreadyDoneAction({
|
|
496
|
-
action,
|
|
497
|
-
journal,
|
|
498
|
-
tracker,
|
|
499
|
-
step: tried,
|
|
500
|
-
})
|
|
501
|
-
case 'finish_nothing_to_do':
|
|
502
|
-
journal.append({
|
|
503
|
-
kind: 'nothing_to_do',
|
|
504
|
-
ticket: null,
|
|
505
|
-
role: null,
|
|
506
|
-
content: {
|
|
507
|
-
closed_tickets: action.closed_tickets,
|
|
508
|
-
already_done: action.already_done,
|
|
509
|
-
},
|
|
510
|
-
})
|
|
511
|
-
return
|
|
512
|
-
case 'snapshot_intake': {
|
|
513
|
-
const { spec, tickets, closed_tickets } = action.snapshot
|
|
514
|
-
journal.append({
|
|
515
|
-
kind: 'spec_snapshot',
|
|
516
|
-
ticket: spec.number,
|
|
517
|
-
role: null,
|
|
518
|
-
content: {
|
|
519
|
-
spec,
|
|
520
|
-
ticket_order: tickets.map((ticket) => ticket.number),
|
|
521
|
-
closed_tickets,
|
|
522
|
-
},
|
|
523
|
-
})
|
|
524
|
-
for (const ticket of tickets) {
|
|
525
|
-
journal.append({
|
|
526
|
-
kind: 'ticket_snapshot',
|
|
527
|
-
ticket: ticket.number,
|
|
528
|
-
role: null,
|
|
529
|
-
content: ticket,
|
|
530
|
-
})
|
|
531
|
-
}
|
|
532
|
-
return
|
|
533
|
-
}
|
|
534
|
-
case 'invalid_journal':
|
|
535
|
-
case 'done':
|
|
536
|
-
return
|
|
537
|
-
default:
|
|
538
|
-
if (build === undefined) {
|
|
539
|
-
throw new Error(
|
|
540
|
-
`The engine needs a git adapter and an agent launcher to ${action.type}.`
|
|
541
|
-
)
|
|
542
|
-
}
|
|
543
|
-
if (isFinalReviewAction(action)) {
|
|
544
|
-
return executeFinalReviewAction({
|
|
545
|
-
action,
|
|
546
|
-
context: buildContext({
|
|
547
|
-
journal,
|
|
548
|
-
tracker,
|
|
549
|
-
step: tried,
|
|
550
|
-
...build,
|
|
551
|
-
}),
|
|
552
|
-
})
|
|
553
|
-
}
|
|
554
|
-
if (action.type === 'write_changeset') {
|
|
555
|
-
return writeChangeset({
|
|
556
|
-
action,
|
|
557
|
-
context: buildContext({
|
|
558
|
-
journal,
|
|
559
|
-
tracker,
|
|
560
|
-
step: tried,
|
|
561
|
-
...build,
|
|
562
|
-
}),
|
|
563
|
-
})
|
|
564
|
-
}
|
|
565
|
-
return executeBuildAction({
|
|
566
|
-
action,
|
|
567
|
-
journal,
|
|
568
|
-
tracker,
|
|
569
|
-
step: tried,
|
|
570
|
-
...build,
|
|
571
|
-
})
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* The ticket an action works on, or `null` for a run-level action. The
|
|
577
|
-
* plan's actions are the whole run's, whichever ticket hit the limit; a
|
|
578
|
-
* ticket's usage record is that ticket's. Reading and taking replies is
|
|
579
|
-
* the run's too (see `keyOf`).
|
|
580
|
-
*/
|
|
581
|
-
const ticketOf = (action: EngineAction): number | null => {
|
|
582
|
-
switch (action.type) {
|
|
583
|
-
case 'start_limit_wait':
|
|
584
|
-
case 'wait_for_limit':
|
|
585
|
-
case 'stop_for_billing':
|
|
586
|
-
case 'wait_for_reply':
|
|
587
|
-
case 'take_reply':
|
|
588
|
-
case 'ignore_reply':
|
|
589
|
-
return null
|
|
590
|
-
case 'record_usage':
|
|
591
|
-
return action.usage.ticket
|
|
592
|
-
default:
|
|
593
|
-
return 'ticket' in action && typeof action.ticket === 'number'
|
|
594
|
-
? action.ticket
|
|
595
|
-
: null
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* Replies are read and taken beside the other steps, one at a time, under
|
|
601
|
-
* their own key: waiting for a reply never holds a ticket up.
|
|
602
|
-
*/
|
|
603
|
-
const REPLY_ACTIONS: ReadonlySet<EngineAction['type']> = new Set([
|
|
604
|
-
'wait_for_reply',
|
|
605
|
-
'take_reply',
|
|
606
|
-
'ignore_reply',
|
|
607
|
-
])
|
|
608
|
-
|
|
609
|
-
/**
|
|
610
|
-
* The scheduler's key for an action: at most one action per key runs at a
|
|
611
|
-
* time. Reading and taking replies has its own (`replies`). Each lens has
|
|
612
|
-
* its own (`lens:<lens>`), so the five lenses review at once; the final review's other steps share `final`; a ticket's steps its
|
|
613
|
-
* number; and the run's own steps `run`, which only ever run alone.
|
|
614
|
-
*/
|
|
615
|
-
export const keyOf = (action: EngineAction): string => {
|
|
616
|
-
if (REPLY_ACTIONS.has(action.type)) return 'replies'
|
|
617
|
-
// A search waits under the key of the step it comes before: its
|
|
618
|
-
// ticket's, the final review's, or (the run's start) the run's.
|
|
619
|
-
if (action.type === 'recall_memories') {
|
|
620
|
-
if (action.ticket !== null) return String(action.ticket)
|
|
621
|
-
return action.point === 'run_start' ? 'run' : 'final'
|
|
622
|
-
}
|
|
623
|
-
if (action.type === 'launch_lens') return `lens:${action.lens}`
|
|
624
|
-
if (isFinalReviewAction(action)) return 'final'
|
|
625
|
-
const ticket = ticketOf(action)
|
|
626
|
-
return ticket === null ? 'run' : String(ticket)
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
/** Actions that are an agent's turn: their step names the role. */
|
|
630
|
-
const AGENT_ACTIONS: ReadonlySet<EngineAction['type']> = new Set([
|
|
631
|
-
'launch_agent',
|
|
632
|
-
'follow_up_agent',
|
|
633
|
-
'launch_lens',
|
|
634
|
-
'launch_final_fixer',
|
|
635
|
-
'follow_up_final_fixer',
|
|
636
|
-
])
|
|
637
|
-
|
|
638
|
-
/** The agent role of an agent's turn (the learner's is `learner`), else `null`. */
|
|
639
|
-
const agentRoleOf = (action: EngineAction): string | null => {
|
|
640
|
-
if (action.type === 'launch_learner') return 'learner'
|
|
641
|
-
return AGENT_ACTIONS.has(action.type) &&
|
|
642
|
-
'role' in action &&
|
|
643
|
-
action.role !== null
|
|
644
|
-
? action.role
|
|
645
|
-
: null
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* A step's name in the journal: its action type, plus `:<role>` for an
|
|
650
|
-
* agent's turn, such as `follow_up_agent:implementer`.
|
|
651
|
-
*
|
|
652
|
-
* @example
|
|
653
|
-
* stepOf({ type: 'run_gates', ticket: 11, target: 'ticket' }) // 'run_gates'
|
|
654
|
-
*/
|
|
655
|
-
export const stepOf = (action: EngineAction): string => {
|
|
656
|
-
const role = agentRoleOf(action)
|
|
657
|
-
return role === null ? action.type : `${action.type}:${role}`
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* Waits get no step records: a crash during one is no step's fault, and a
|
|
662
|
-
* restarted engine just waits again (a limit wait until the same time).
|
|
663
|
-
*/
|
|
664
|
-
const WAIT_ACTIONS: ReadonlySet<EngineAction['type']> = new Set([
|
|
665
|
-
'wait_for_reply',
|
|
666
|
-
'wait_for_limit',
|
|
667
|
-
'wait_for_usage_line',
|
|
668
|
-
])
|
|
669
|
-
|
|
670
|
-
/**
|
|
671
|
-
* Steps that can run for minutes: installs, test runs, and agents' turns.
|
|
672
|
-
* The board hears of them as they start, so a run never looks frozen.
|
|
673
|
-
*/
|
|
674
|
-
const SLOW_ACTIONS: ReadonlySet<EngineAction['type']> = new Set([
|
|
675
|
-
...AGENT_ACTIONS,
|
|
676
|
-
'launch_learner',
|
|
677
|
-
'install_dependencies',
|
|
678
|
-
'run_baseline_tests',
|
|
679
|
-
'run_red_check',
|
|
680
|
-
'run_gates',
|
|
681
|
-
'run_final_gates',
|
|
682
|
-
])
|
|
683
|
-
|
|
684
|
-
/**
|
|
685
|
-
* Actions that read or move the run branch. At most one runs at a time, so
|
|
686
|
-
* joins, their gates, pushes, and new worktrees see one run branch. The
|
|
687
|
-
* final review's fixers, gates, commit, and push work in the run branch's
|
|
688
|
-
* worktree; its lenses only read it.
|
|
689
|
-
*/
|
|
690
|
-
const usesRunBranch = (action: EngineAction): boolean => {
|
|
691
|
-
switch (action.type) {
|
|
692
|
-
case 'create_ticket_worktree':
|
|
693
|
-
case 'join_run_branch':
|
|
694
|
-
case 'push_run_branch':
|
|
695
|
-
case 'rebase_ticket':
|
|
696
|
-
case 'start_final_review':
|
|
697
|
-
case 'launch_final_fixer':
|
|
698
|
-
case 'follow_up_final_fixer':
|
|
699
|
-
case 'run_final_gates':
|
|
700
|
-
case 'commit_final_fix':
|
|
701
|
-
case 'push_final_fixes':
|
|
702
|
-
case 'write_changeset':
|
|
703
|
-
case 'undo_join':
|
|
704
|
-
case 'retry_ticket':
|
|
705
|
-
return true
|
|
706
|
-
case 'run_gates':
|
|
707
|
-
case 'install_dependencies':
|
|
708
|
-
return action.target === 'run_branch'
|
|
709
|
-
// It puts back a run branch a join crashes cut off too often left.
|
|
710
|
-
case 'mark_stuck':
|
|
711
|
-
return action.reason === 'crashed'
|
|
712
|
-
default:
|
|
713
|
-
return false
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* Carries out one action with Jev in shadow mode: asks Jev before it (ticket
|
|
719
|
-
* order, model, skills), then after it about the records it appended
|
|
720
|
-
* (failure kinds, finding severities). Only the action's own ticket's new
|
|
721
|
-
* records count, since other tickets append at the same time. Jev's answers
|
|
722
|
-
* change nothing.
|
|
723
|
-
*/
|
|
724
|
-
const executeWithJev = async ({
|
|
725
|
-
jev,
|
|
726
|
-
action,
|
|
727
|
-
journal,
|
|
728
|
-
tracker,
|
|
729
|
-
build,
|
|
730
|
-
clock,
|
|
731
|
-
reply_poll_ms,
|
|
732
|
-
memory,
|
|
733
|
-
step,
|
|
734
|
-
read_usage_lines,
|
|
735
|
-
}: {
|
|
736
|
-
jev: JevShadow
|
|
737
|
-
action: EngineAction
|
|
738
|
-
journal: Journal
|
|
739
|
-
tracker: Tracker
|
|
740
|
-
build?: BuildDeps
|
|
741
|
-
clock?: EngineClock
|
|
742
|
-
reply_poll_ms?: number
|
|
743
|
-
memory?: MemoryDeps
|
|
744
|
-
step?: StepTry
|
|
745
|
-
read_usage_lines?: () => Promise<UsageLines>
|
|
746
|
-
}): Promise<void> => {
|
|
747
|
-
const shadow = { jev: jev.client, journal, timeout_ms: jev.timeout_ms }
|
|
748
|
-
await askJevInShadow({
|
|
749
|
-
...shadow,
|
|
750
|
-
asks: jevAsksBefore({
|
|
751
|
-
action,
|
|
752
|
-
state: replayRun({ records: journal.read() }),
|
|
753
|
-
}),
|
|
754
|
-
})
|
|
755
|
-
const lastSeq = journal.read().at(-1)?.seq ?? 0
|
|
756
|
-
await executeAction({
|
|
757
|
-
action,
|
|
758
|
-
journal,
|
|
759
|
-
tracker,
|
|
760
|
-
build,
|
|
761
|
-
clock,
|
|
762
|
-
reply_poll_ms,
|
|
763
|
-
memory,
|
|
764
|
-
step,
|
|
765
|
-
read_usage_lines,
|
|
766
|
-
})
|
|
767
|
-
const records = journal.read()
|
|
768
|
-
await askJevInShadow({
|
|
769
|
-
...shadow,
|
|
770
|
-
asks: jevAsksAfter({
|
|
771
|
-
records: records.filter(
|
|
772
|
-
(record) =>
|
|
773
|
-
record.seq > lastSeq && ownsRecord({ action, record })
|
|
774
|
-
),
|
|
775
|
-
state: replayRun({ records }),
|
|
776
|
-
}),
|
|
777
|
-
})
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* Whether a record appended while `action` ran is that action's own, since
|
|
782
|
-
* other tickets (or other lenses) append at the same time: a ticket's
|
|
783
|
-
* records for a ticket's step, a lens's for its lens, and the final
|
|
784
|
-
* review's other ticket-less records for its other steps.
|
|
785
|
-
*/
|
|
786
|
-
const ownsRecord = ({
|
|
787
|
-
action,
|
|
788
|
-
record,
|
|
789
|
-
}: {
|
|
790
|
-
action: EngineAction
|
|
791
|
-
record: JournalRecord
|
|
792
|
-
}): boolean => {
|
|
793
|
-
if (action.type === 'launch_lens') {
|
|
794
|
-
return (
|
|
795
|
-
record.ticket === null &&
|
|
796
|
-
(record.role === null || record.role === action.role)
|
|
797
|
-
)
|
|
798
|
-
}
|
|
799
|
-
if (isFinalReviewAction(action)) {
|
|
800
|
-
return record.ticket === null && !record.role?.endsWith('-lens')
|
|
801
|
-
}
|
|
802
|
-
const ticket = ticketOf(action)
|
|
803
|
-
return ticket === null || record.ticket === ticket
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
/** The crash counts per key, from the journal's records. */
|
|
807
|
-
const crashesIn = (records: JournalRecord[]): CrashCounts =>
|
|
808
|
-
records.reduce<CrashCounts>(
|
|
809
|
-
(crashes, record) => crashesAfter({ crashes, record }),
|
|
810
|
-
{}
|
|
811
|
-
)
|
|
812
|
-
|
|
813
|
-
/**
|
|
814
|
-
* The actions on keys a crash cut a step off first, in their order, then
|
|
815
|
-
* the rest: a step a crash cut off (or the stop for it) starts before any
|
|
816
|
-
* other, so a half-done change to the run branch, such as a join, is put
|
|
817
|
-
* right before another step reads the run branch.
|
|
818
|
-
*/
|
|
819
|
-
const redosFirst = ({
|
|
820
|
-
actions,
|
|
821
|
-
crashes,
|
|
822
|
-
}: {
|
|
823
|
-
actions: EngineAction[]
|
|
824
|
-
crashes: CrashCounts
|
|
825
|
-
}): EngineAction[] => {
|
|
826
|
-
const [redos, rest] = partition(actions, (action) =>
|
|
827
|
-
has(crashes, keyOf(action))
|
|
828
|
-
)
|
|
829
|
-
return [...redos, ...rest]
|
|
830
|
-
}
|
|
831
|
-
|
|
832
60
|
/** One action the engine started and has not seen settle yet. */
|
|
833
61
|
type InFlight = { action: EngineAction; done: Promise<void> }
|
|
834
62
|
|
|
835
|
-
/** The session a follow-up goes to, if the action is one. */
|
|
836
|
-
const followUpSessionOf = (action: EngineAction): string[] =>
|
|
837
|
-
action.type === 'follow_up_agent' || action.type === 'follow_up_final_fixer'
|
|
838
|
-
? [action.session_id]
|
|
839
|
-
: []
|
|
840
|
-
|
|
841
63
|
/**
|
|
842
64
|
* Runs the engine: a scheduler over the decision step. Each pass it decides
|
|
843
65
|
* every action that can run now from the journal and starts those it can,
|
|
@@ -862,8 +84,10 @@ const followUpSessionOf = (action: EngineAction): string[] =>
|
|
|
862
84
|
* let finish, then the first error is thrown. `max_steps` counts the actions
|
|
863
85
|
* started.
|
|
864
86
|
*
|
|
865
|
-
* With `jev`,
|
|
866
|
-
* answers are journaled but never acted on
|
|
87
|
+
* With `jev`, the decision model is asked around each step in **shadow
|
|
88
|
+
* mode** and its answers are journaled but never acted on (see
|
|
89
|
+
* `shadowAsker`; a rejected token turns it off for the rest of this call).
|
|
90
|
+
* Without it, nothing changes.
|
|
867
91
|
*
|
|
868
92
|
* With `memory`, a run whose `run_started` turned memory on searches
|
|
869
93
|
* MuninnDB at each recall point and saves the learner's memories at its
|
|
@@ -873,6 +97,11 @@ const followUpSessionOf = (action: EngineAction): string[] =>
|
|
|
873
97
|
* With `board`, the whole journal is sent to the board once before the
|
|
874
98
|
* first step and again after each step settles.
|
|
875
99
|
*
|
|
100
|
+
* A ticket held on purpose between steps (another ticket's shared
|
|
101
|
+
* baseline, the join queue, the start queue) gets one `ticket_waiting` as
|
|
102
|
+
* its wait starts, from the same decision as the steps (`decideNext`),
|
|
103
|
+
* sent to the board at once.
|
|
104
|
+
*
|
|
876
105
|
* A rejected plan limit is a limit wait: the engine sleeps by `clock` until
|
|
877
106
|
* the window resets, then carries on. Overage or a billing error ends the
|
|
878
107
|
* run for good (`done`, outcome `stopped`).
|
|
@@ -881,6 +110,16 @@ const followUpSessionOf = (action: EngineAction): string[] =>
|
|
|
881
110
|
* decision it shares its readings with every run, reads the newest shared
|
|
882
111
|
* ones and the lines, and pauses at a line (see `decide-usage-line.ts`).
|
|
883
112
|
*
|
|
113
|
+
* With `going_runs`, a run started while an earlier run on the same repo
|
|
114
|
+
* is going waits for it before intake: a **repo wait** (see
|
|
115
|
+
* `decide-repo-wait.ts`).
|
|
116
|
+
*
|
|
117
|
+
* With `live_file`, the engine keeps the run's live file there while it
|
|
118
|
+
* runs (`live-file.ts`, #628): what each agent in a turn is doing now,
|
|
119
|
+
* never journaled, and each ticket check's wait for a prepare slot or a
|
|
120
|
+
* test slot while it lasts (#632). It is written a last time, with no
|
|
121
|
+
* agents, at the end.
|
|
122
|
+
*
|
|
884
123
|
* Crash recovery: every step it starts (not a wait) is journaled between a
|
|
885
124
|
* `step_started` and a `step_ended` (none when the step throws). On start,
|
|
886
125
|
* a step with no `step_ended` was cut off by a crash: one `run_resumed`
|
|
@@ -909,6 +148,8 @@ export const runEngine = async ({
|
|
|
909
148
|
reply_poll_ms,
|
|
910
149
|
memory,
|
|
911
150
|
usage,
|
|
151
|
+
going_runs,
|
|
152
|
+
live_file,
|
|
912
153
|
}: Partial<BuildDeps> & {
|
|
913
154
|
journal: Journal
|
|
914
155
|
tracker: Tracker
|
|
@@ -916,7 +157,7 @@ export const runEngine = async ({
|
|
|
916
157
|
max_steps?: number
|
|
917
158
|
/** Action types to stop at without carrying them out, such as in tests. */
|
|
918
159
|
stop_before?: EngineAction['type'][]
|
|
919
|
-
/**
|
|
160
|
+
/** The decision model in shadow mode. Leave it out to run without one. */
|
|
920
161
|
jev?: JevShadow
|
|
921
162
|
/** Sends the journal to the board after every step. Never throws. */
|
|
922
163
|
board?: BoardSync
|
|
@@ -931,18 +172,32 @@ export const runEngine = async ({
|
|
|
931
172
|
memory?: MemoryDeps
|
|
932
173
|
/** The usage lines and the shared readings. Left out, there is no usage line. */
|
|
933
174
|
usage?: UsageLineDeps
|
|
175
|
+
/**
|
|
176
|
+
* Lists the unfinished runs (`goingRuns`), for the repo wait before
|
|
177
|
+
* intake. Left out, there is no repo wait.
|
|
178
|
+
*/
|
|
179
|
+
going_runs?: GoingRunsReader
|
|
180
|
+
/**
|
|
181
|
+
* Where to keep the run's live file (#628), such as
|
|
182
|
+
* `runLiveFilePath({ runs_dir, run_id })`. Left out, there is none.
|
|
183
|
+
*/
|
|
184
|
+
live_file?: string
|
|
934
185
|
}): Promise<EngineAction> => {
|
|
935
186
|
const limit = max_steps ?? DEFAULT_MAX_STEPS
|
|
936
187
|
const stops = new Set([...STOP_ACTIONS, ...(stop_before ?? [])])
|
|
188
|
+
const live =
|
|
189
|
+
live_file === undefined ? null : createLiveFile({ file: live_file })
|
|
937
190
|
const build =
|
|
938
191
|
git === undefined || launcher === undefined
|
|
939
192
|
? undefined
|
|
940
|
-
: { git, launcher }
|
|
193
|
+
: { git, launcher: live?.launcher(launcher) ?? launcher }
|
|
941
194
|
const inFlight = new Map<string, InFlight>()
|
|
942
195
|
let started = 0
|
|
196
|
+
// The run's own asker, so turning the decision model off lasts this run.
|
|
197
|
+
const ask = jev === undefined ? undefined : shadowAsker({ jev, journal })
|
|
943
198
|
|
|
944
199
|
const execute = (action: EngineAction, step?: StepTry): Promise<void> =>
|
|
945
|
-
|
|
200
|
+
ask === undefined
|
|
946
201
|
? executeAction({
|
|
947
202
|
action,
|
|
948
203
|
journal,
|
|
@@ -953,9 +208,10 @@ export const runEngine = async ({
|
|
|
953
208
|
memory,
|
|
954
209
|
step,
|
|
955
210
|
read_usage_lines: usage?.read_lines,
|
|
211
|
+
going_runs,
|
|
956
212
|
})
|
|
957
213
|
: executeWithJev({
|
|
958
|
-
|
|
214
|
+
ask,
|
|
959
215
|
action,
|
|
960
216
|
journal,
|
|
961
217
|
tracker,
|
|
@@ -965,18 +221,27 @@ export const runEngine = async ({
|
|
|
965
221
|
memory,
|
|
966
222
|
step,
|
|
967
223
|
read_usage_lines: usage?.read_lines,
|
|
224
|
+
going_runs,
|
|
968
225
|
})
|
|
969
226
|
|
|
970
|
-
/**
|
|
227
|
+
/**
|
|
228
|
+
* The decision step, with the usage line when the run has one, and the
|
|
229
|
+
* earlier runs on the repo while the run has yet to look for them.
|
|
230
|
+
*/
|
|
971
231
|
const decideNow = async (
|
|
972
232
|
records: JournalRecord[]
|
|
973
|
-
): Promise<
|
|
974
|
-
|
|
233
|
+
): Promise<ReturnType<typeof decideNext>> => {
|
|
234
|
+
const earlier_runs =
|
|
235
|
+
going_runs !== undefined && repoWaitUnchecked({ records })
|
|
236
|
+
? (await readEarlierRuns({ going_runs, journal })).runs
|
|
237
|
+
: undefined
|
|
238
|
+
if (usage === undefined) return decideNext({ records, earlier_runs })
|
|
975
239
|
await usage.share(runReadings({ records }))
|
|
976
|
-
return
|
|
240
|
+
return decideNext({
|
|
977
241
|
records,
|
|
978
242
|
usage_lines: await usage.read_lines(),
|
|
979
243
|
shared_readings: await usage.read_shared(),
|
|
244
|
+
earlier_runs,
|
|
980
245
|
})
|
|
981
246
|
}
|
|
982
247
|
const settleAll = () =>
|
|
@@ -1014,10 +279,18 @@ export const runEngine = async ({
|
|
|
1014
279
|
...who,
|
|
1015
280
|
content: { key, step, first_seq },
|
|
1016
281
|
})
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
282
|
+
const run = () =>
|
|
283
|
+
execute(action, {
|
|
284
|
+
first_seq: first_seq ?? started.seq,
|
|
285
|
+
redo: first_seq !== null,
|
|
286
|
+
})
|
|
287
|
+
// A ticket's check waiting for a slot shows in the live file (#632).
|
|
288
|
+
await (live === null || who.ticket === null
|
|
289
|
+
? run()
|
|
290
|
+
: hearSlotWaits({
|
|
291
|
+
listener: live.slotWaits({ ticket: who.ticket }),
|
|
292
|
+
run,
|
|
293
|
+
}))
|
|
1021
294
|
journal.append({ kind: 'step_ended', ...who, content: { key, step } })
|
|
1022
295
|
}
|
|
1023
296
|
|
|
@@ -1048,10 +321,8 @@ export const runEngine = async ({
|
|
|
1048
321
|
await closeFinished({ all: false })
|
|
1049
322
|
const records = journal.read()
|
|
1050
323
|
const crashes = crashesIn(records)
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
crashes,
|
|
1054
|
-
})
|
|
324
|
+
const decided = await decideNow(records)
|
|
325
|
+
const actions = redosFirst({ actions: decided.actions, crashes })
|
|
1055
326
|
const stop = actions.find((action) => stops.has(action.type))
|
|
1056
327
|
if (stop !== undefined && inFlight.size === 0) {
|
|
1057
328
|
if (STOP_ACTIONS.has(stop.type)) {
|
|
@@ -1060,7 +331,24 @@ export const runEngine = async ({
|
|
|
1060
331
|
return stop
|
|
1061
332
|
}
|
|
1062
333
|
let slowStarted = false
|
|
334
|
+
let waited = false
|
|
1063
335
|
if (stop === undefined) {
|
|
336
|
+
// A wait between steps is journaled once, as it starts, and
|
|
337
|
+
// shows on the board at once: the step it waits on may run
|
|
338
|
+
// for minutes.
|
|
339
|
+
for (const wait of decided.waits) {
|
|
340
|
+
journal.append({
|
|
341
|
+
kind: 'ticket_waiting',
|
|
342
|
+
ticket: wait.ticket,
|
|
343
|
+
role: null,
|
|
344
|
+
content: {
|
|
345
|
+
for: wait.for,
|
|
346
|
+
on_ticket: wait.on_ticket,
|
|
347
|
+
since: new Date().toISOString(),
|
|
348
|
+
},
|
|
349
|
+
})
|
|
350
|
+
waited = true
|
|
351
|
+
}
|
|
1064
352
|
for (const action of actions) {
|
|
1065
353
|
const key = keyOf(action)
|
|
1066
354
|
if (!canStart({ action, key })) continue
|
|
@@ -1085,8 +373,11 @@ export const runEngine = async ({
|
|
|
1085
373
|
if (SLOW_ACTIONS.has(action.type)) slowStarted = true
|
|
1086
374
|
}
|
|
1087
375
|
}
|
|
1088
|
-
// A slow step shows on the board while it runs, not only once it
|
|
1089
|
-
|
|
376
|
+
// A slow step shows on the board while it runs, not only once it
|
|
377
|
+
// ends; so does a wait.
|
|
378
|
+
if (slowStarted || waited) {
|
|
379
|
+
await board?.sync({ records: journal.read() })
|
|
380
|
+
}
|
|
1090
381
|
if (inFlight.size === 0) {
|
|
1091
382
|
throw new Error(
|
|
1092
383
|
`The engine could start none of: ${actions.map(({ type }) => type).join(', ')}.`
|
|
@@ -1120,5 +411,7 @@ export const runEngine = async ({
|
|
|
1120
411
|
// The run ends here, so no session stays open.
|
|
1121
412
|
await closeFinished({ all: true })
|
|
1122
413
|
throw error
|
|
414
|
+
} finally {
|
|
415
|
+
live?.close()
|
|
1123
416
|
}
|
|
1124
417
|
}
|