@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
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One ticket's next step once it has its worktree: a failed turn, its
|
|
3
|
+
* install and prepare, its shared baseline, then its tests, code, review
|
|
4
|
+
* and join.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
isRefactorTicket,
|
|
9
|
+
stuck,
|
|
10
|
+
type BuildAction,
|
|
11
|
+
type StepArgs,
|
|
12
|
+
} from './build-action'
|
|
13
|
+
import { launch } from './build-launch'
|
|
14
|
+
import { codeStep, testStep } from './decide-red-green'
|
|
15
|
+
import { joinStep, rejoinStep } from './decide-rejoin'
|
|
16
|
+
import { reviewStep } from './decide-ticket-review'
|
|
17
|
+
import { failedTryMessage } from './fix-loop-text'
|
|
18
|
+
import { MAX_ENGINE_FAILURES, MAX_FIX_ROUNDS } from './loop-caps'
|
|
19
|
+
import { setupChangeDetail } from './stuck-text'
|
|
20
|
+
|
|
21
|
+
import { endedText } from '../gates/gate-schemas'
|
|
22
|
+
import type {
|
|
23
|
+
ReplayedInstall,
|
|
24
|
+
ReplayedWorktree,
|
|
25
|
+
TicketProgress,
|
|
26
|
+
} from '../journal/replay'
|
|
27
|
+
|
|
28
|
+
/** Why an install failed, for the stuck detail, or `null` if it passed. */
|
|
29
|
+
export const installFailure = ({
|
|
30
|
+
install,
|
|
31
|
+
where,
|
|
32
|
+
}: {
|
|
33
|
+
install: ReplayedInstall
|
|
34
|
+
where: string
|
|
35
|
+
}): string | null => {
|
|
36
|
+
const { check } = install
|
|
37
|
+
if (check === null || check.ok) return null
|
|
38
|
+
return `\`${check.command}\` failed in ${where} (${endedText(check)}). Agents never run the install, so fix the manifest or lockfile on the base branch and retry.\n${check.output}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The next step after a failed agent turn. An engine failure starts a fresh
|
|
43
|
+
* agent of the same role without using up a try, until
|
|
44
|
+
* `MAX_ENGINE_FAILURES` in a row. Any other failure uses up one of the
|
|
45
|
+
* role's tries on the ticket; with tries left, a test-writer or implementer
|
|
46
|
+
* gets a follow-up in the session that failed (a fresh launch with the same
|
|
47
|
+
* message if it has none, such as after a crash), and a reviewer a fresh
|
|
48
|
+
* launch. The last try's failure is stuck.
|
|
49
|
+
*/
|
|
50
|
+
const failedTurnStep = ({
|
|
51
|
+
snapshot,
|
|
52
|
+
ticket,
|
|
53
|
+
progress,
|
|
54
|
+
run_notes,
|
|
55
|
+
failed,
|
|
56
|
+
}: StepArgs & {
|
|
57
|
+
failed: NonNullable<TicketProgress['agent_failure']>
|
|
58
|
+
}): BuildAction => {
|
|
59
|
+
const { role, error, failure, session_id } = failed
|
|
60
|
+
if (failure === 'engine') {
|
|
61
|
+
if (progress.engine_failures >= MAX_ENGINE_FAILURES) {
|
|
62
|
+
return stuck({
|
|
63
|
+
ticket: ticket.number,
|
|
64
|
+
reason: 'agent_failed',
|
|
65
|
+
detail: `The engine failed to run the ${role} ${progress.engine_failures} times in a row: ${error}`,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
return launch({ role, snapshot, ticket, progress, run_notes })
|
|
69
|
+
}
|
|
70
|
+
const tries = progress.failed_tries[role] ?? 0
|
|
71
|
+
if (tries >= MAX_FIX_ROUNDS) {
|
|
72
|
+
return stuck({
|
|
73
|
+
ticket: ticket.number,
|
|
74
|
+
reason: 'agent_failed',
|
|
75
|
+
detail: `The ${role} failed ${tries} tries; the last one: ${error}`,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
if (role === 'ticket-reviewer') {
|
|
79
|
+
return launch({ role, snapshot, ticket, progress, run_notes })
|
|
80
|
+
}
|
|
81
|
+
const message = failedTryMessage({ failure, error })
|
|
82
|
+
if (session_id === null) {
|
|
83
|
+
return launch({
|
|
84
|
+
role,
|
|
85
|
+
snapshot,
|
|
86
|
+
ticket,
|
|
87
|
+
progress,
|
|
88
|
+
run_notes,
|
|
89
|
+
follow_up: message,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
type: 'follow_up_agent',
|
|
94
|
+
ticket: ticket.number,
|
|
95
|
+
role,
|
|
96
|
+
session_id,
|
|
97
|
+
message,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type SharedBaselineArgs = {
|
|
102
|
+
number: number
|
|
103
|
+
/** The run's tickets, in order. */
|
|
104
|
+
numbers: number[]
|
|
105
|
+
progress: (number: number) => TicketProgress
|
|
106
|
+
/** Whether a ticket is neither pushed, skipped, nor stuck. */
|
|
107
|
+
can_move: (number: number) => boolean
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The ticket that runs the baseline tests a ticket with no baseline shares:
|
|
112
|
+
* the first ticket still able to move whose worktree starts from the same
|
|
113
|
+
* run-branch commit and has no baseline yet. `null` with no worktree.
|
|
114
|
+
*/
|
|
115
|
+
export const baselineRunner = ({
|
|
116
|
+
number,
|
|
117
|
+
numbers,
|
|
118
|
+
progress,
|
|
119
|
+
can_move,
|
|
120
|
+
}: SharedBaselineArgs): number | null => {
|
|
121
|
+
const base_sha = progress(number).worktree?.base_sha
|
|
122
|
+
if (base_sha === undefined) return null
|
|
123
|
+
return (
|
|
124
|
+
numbers.find(
|
|
125
|
+
(other) =>
|
|
126
|
+
can_move(other) &&
|
|
127
|
+
progress(other).baseline === null &&
|
|
128
|
+
progress(other).worktree?.base_sha === base_sha
|
|
129
|
+
) ?? null
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* How a ticket with no baseline gets one. Tickets whose worktrees start from
|
|
135
|
+
* the same run-branch commit share one baseline test run: a ticket reuses a
|
|
136
|
+
* baseline taken at its commit, or else the first such ticket still able to
|
|
137
|
+
* move runs the tests (`baselineRunner`) and the rest wait for it. `null`:
|
|
138
|
+
* it waits.
|
|
139
|
+
*/
|
|
140
|
+
export const sharedBaselineStep = (
|
|
141
|
+
args: SharedBaselineArgs
|
|
142
|
+
): BuildAction | null => {
|
|
143
|
+
const { number, numbers, progress } = args
|
|
144
|
+
const base_sha = progress(number).worktree?.base_sha
|
|
145
|
+
if (base_sha === undefined) return null
|
|
146
|
+
const lender = numbers.find(
|
|
147
|
+
(other) =>
|
|
148
|
+
other !== number &&
|
|
149
|
+
progress(other).baseline !== null &&
|
|
150
|
+
progress(other).baseline_sha === base_sha
|
|
151
|
+
)
|
|
152
|
+
if (lender !== undefined) {
|
|
153
|
+
return {
|
|
154
|
+
type: 'reuse_baseline_tests',
|
|
155
|
+
ticket: number,
|
|
156
|
+
from_ticket: lender,
|
|
157
|
+
base_sha,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return baselineRunner(args) === number
|
|
161
|
+
? { type: 'run_baseline_tests', ticket: number }
|
|
162
|
+
: null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The next step for one ticket that has its worktree, or `null` when it has
|
|
167
|
+
* nothing to do now: it pushed, or its review approved and it waits its turn
|
|
168
|
+
* to join.
|
|
169
|
+
*/
|
|
170
|
+
export const nextTicketStep = ({
|
|
171
|
+
snapshot,
|
|
172
|
+
ticket,
|
|
173
|
+
progress,
|
|
174
|
+
run_notes,
|
|
175
|
+
run_branch,
|
|
176
|
+
joiner,
|
|
177
|
+
setup_files,
|
|
178
|
+
baseline_step,
|
|
179
|
+
base_branch,
|
|
180
|
+
}: StepArgs & {
|
|
181
|
+
run_branch: ReplayedWorktree
|
|
182
|
+
/** The branch the run started from. */
|
|
183
|
+
base_branch: string
|
|
184
|
+
/** Whether the ticket is first in the join queue. */
|
|
185
|
+
joiner: boolean
|
|
186
|
+
/** The config's test setup files. */
|
|
187
|
+
setup_files: string[]
|
|
188
|
+
/** How it gets its baseline, if it has none (`sharedBaselineStep`). */
|
|
189
|
+
baseline_step: BuildAction | null
|
|
190
|
+
}): BuildAction | null => {
|
|
191
|
+
const number = ticket.number
|
|
192
|
+
if (progress.agent_failure !== null) {
|
|
193
|
+
return failedTurnStep({
|
|
194
|
+
snapshot,
|
|
195
|
+
ticket,
|
|
196
|
+
progress,
|
|
197
|
+
run_notes,
|
|
198
|
+
failed: progress.agent_failure,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
if (progress.setup_change !== null) {
|
|
202
|
+
return stuck({
|
|
203
|
+
ticket: number,
|
|
204
|
+
reason: 'setup_change_needed',
|
|
205
|
+
detail: setupChangeDetail({
|
|
206
|
+
...progress.setup_change,
|
|
207
|
+
setup_files,
|
|
208
|
+
}),
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
if (progress.install === null) {
|
|
212
|
+
return {
|
|
213
|
+
type: 'install_dependencies',
|
|
214
|
+
target: 'ticket',
|
|
215
|
+
ticket: number,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const ticketFailure = installFailure({
|
|
219
|
+
install: progress.install,
|
|
220
|
+
where: `the worktree of #${number}`,
|
|
221
|
+
})
|
|
222
|
+
if (ticketFailure !== null) {
|
|
223
|
+
return stuck({
|
|
224
|
+
ticket: number,
|
|
225
|
+
reason: 'install_failed',
|
|
226
|
+
detail: ticketFailure,
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
const { prepare } = progress
|
|
230
|
+
if (prepare !== null && !prepare.ok) {
|
|
231
|
+
return stuck({
|
|
232
|
+
ticket: number,
|
|
233
|
+
reason: 'prepare_failed',
|
|
234
|
+
detail: `\`${prepare.command}\` failed in the worktree of #${number} (${endedText(prepare)}) before any agent worked on it. Agents never run the prepare command, so fix what it needs (a missing tool, say) and retry.\n${prepare.output}`,
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
if (progress.baseline === null) return baseline_step
|
|
238
|
+
if (progress.commits.green === null) {
|
|
239
|
+
const rejoin = rejoinStep({
|
|
240
|
+
snapshot,
|
|
241
|
+
ticket,
|
|
242
|
+
progress,
|
|
243
|
+
run_notes,
|
|
244
|
+
})
|
|
245
|
+
if (rejoin !== null) return rejoin
|
|
246
|
+
const tests = isRefactorTicket({ ticket })
|
|
247
|
+
? null
|
|
248
|
+
: testStep({
|
|
249
|
+
snapshot,
|
|
250
|
+
ticket,
|
|
251
|
+
progress,
|
|
252
|
+
run_notes,
|
|
253
|
+
base_branch,
|
|
254
|
+
})
|
|
255
|
+
if (tests !== null) return tests
|
|
256
|
+
const code = codeStep({ snapshot, ticket, progress, run_notes })
|
|
257
|
+
if (code !== null) return code
|
|
258
|
+
}
|
|
259
|
+
const review = reviewStep({
|
|
260
|
+
snapshot,
|
|
261
|
+
ticket,
|
|
262
|
+
progress,
|
|
263
|
+
run_notes,
|
|
264
|
+
})
|
|
265
|
+
if (review !== null) return review
|
|
266
|
+
return joiner ? joinStep({ ticket, progress, run_branch }) : null
|
|
267
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The waits between steps to journal (#559): a ticket the engine holds on
|
|
3
|
+
* purpose, for another ticket's shared baseline tests, the join queue, or
|
|
4
|
+
* the start queue, gets one `ticket_waiting` when its wait starts. Pure.
|
|
5
|
+
*/
|
|
6
|
+
import type { TicketWait } from './decide-build-tickets'
|
|
7
|
+
|
|
8
|
+
import type { JournalRecord } from '../journal/journal-record'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Whether `wait` is journaled already: its ticket's `ticket_waiting` since
|
|
12
|
+
* its last `step_started` waits for the same thing on the same ticket.
|
|
13
|
+
*/
|
|
14
|
+
const journaled = ({
|
|
15
|
+
wait,
|
|
16
|
+
records,
|
|
17
|
+
}: {
|
|
18
|
+
wait: TicketWait
|
|
19
|
+
records: JournalRecord[]
|
|
20
|
+
}): boolean => {
|
|
21
|
+
const latest = records.findLast(
|
|
22
|
+
(record) =>
|
|
23
|
+
record.ticket === wait.ticket &&
|
|
24
|
+
(record.kind === 'step_started' || record.kind === 'ticket_waiting')
|
|
25
|
+
)
|
|
26
|
+
return (
|
|
27
|
+
latest?.kind === 'ticket_waiting' &&
|
|
28
|
+
latest.content.for === wait.for &&
|
|
29
|
+
latest.content.on_ticket === wait.on_ticket
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The build decision's waits (`decideBuildPlan`) that started since the
|
|
35
|
+
* journal last said so, in ticket order.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* startedWaits({ waits, records: journal.read() })
|
|
39
|
+
* // [{ ticket: 12, for: 'baseline', on_ticket: 11 }]
|
|
40
|
+
*/
|
|
41
|
+
export const startedWaits = ({
|
|
42
|
+
waits,
|
|
43
|
+
records,
|
|
44
|
+
}: {
|
|
45
|
+
waits: TicketWait[]
|
|
46
|
+
records: JournalRecord[]
|
|
47
|
+
}): TicketWait[] => waits.filter((wait) => !journaled({ wait, records }))
|
package/engine/core/decide.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decideBuildPlan, type BuildAction } from './decide-build'
|
|
2
|
+
import type { TicketWait } from './decide-build-tickets'
|
|
2
3
|
import { decideCrashes, type CrashAction } from './decide-crashes'
|
|
3
4
|
import { decideMemory, type MemoryAction } from './decide-memory'
|
|
4
5
|
import { decidePlan, type PlanAction } from './decide-plan'
|
|
6
|
+
import {
|
|
7
|
+
decideRepoWait,
|
|
8
|
+
type RepoWaitAction,
|
|
9
|
+
type RepoWaitRun,
|
|
10
|
+
} from './decide-repo-wait'
|
|
5
11
|
import { decideRunBudget, type RunBudgetAction } from './decide-run-budget'
|
|
6
12
|
import { decideUsage, type UsageAction } from './decide-usage'
|
|
7
13
|
import { decideUsageLine, type UsageLineAction } from './decide-usage-line'
|
|
14
|
+
import { startedWaits } from './decide-waits'
|
|
8
15
|
|
|
9
16
|
import { checkIntake } from '../intake/intake-checks'
|
|
10
17
|
import type { IntakeProblem, IntakeSnapshot } from '../intake/intake-schemas'
|
|
@@ -16,6 +23,8 @@ import type { UsageLines } from '../limits/usage-line'
|
|
|
16
23
|
export type EngineAction =
|
|
17
24
|
/** The journal cannot be a run (for example it has no `run_started`). */
|
|
18
25
|
| { type: 'invalid_journal'; reason: string }
|
|
26
|
+
/** Wait for earlier runs on the same repo, before intake (#615). */
|
|
27
|
+
| RepoWaitAction
|
|
19
28
|
/** Read the spec, its sub-tickets, and outside blockers from the tracker. */
|
|
20
29
|
| { type: 'read_intake'; spec_number: number }
|
|
21
30
|
/** Tell each bad ticket what is missing, then end the run. */
|
|
@@ -50,8 +59,10 @@ export type EngineAction =
|
|
|
50
59
|
* It never reads the tracker, the disk, or the clock.
|
|
51
60
|
*
|
|
52
61
|
* Crashes come first: a run-level step cut off by a crash `MAX_CRASHES`
|
|
53
|
-
* times in a row stops the run for good (`decide-crashes.ts`).
|
|
54
|
-
*
|
|
62
|
+
* times in a row stops the run for good (`decide-crashes.ts`). Before
|
|
63
|
+
* intake, a run with `earlier_runs` going on its repo waits for them
|
|
64
|
+
* (`decide-repo-wait.ts`). After intake, the
|
|
65
|
+
* plan:a limit wait or a billing stop is the only action,
|
|
55
66
|
* for the whole run, however many tickets are in flight (the scheduler lets
|
|
56
67
|
* them settle first, then every cut-off step is taken again after the
|
|
57
68
|
* wait). Then the run budget: a run whose tokens reached it is stuck, and
|
|
@@ -66,17 +77,44 @@ export type EngineAction =
|
|
|
66
77
|
* const actions = decideSteps({ records: journal.read() })
|
|
67
78
|
* // [{ type: 'launch_agent', ticket: 11, ... }, { type: 'run_red_check', ticket: 12, ... }]
|
|
68
79
|
*/
|
|
69
|
-
export const decideSteps = (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
export const decideSteps = (args: DecideInput): EngineAction[] =>
|
|
81
|
+
decideNext(args).actions
|
|
82
|
+
|
|
83
|
+
/** What `decideSteps` and `decideNext` decide from. */
|
|
84
|
+
type DecideInput = {
|
|
74
85
|
records: JournalRecord[]
|
|
75
86
|
/** The `luca-board` usage lines. Left out, there is no usage line. */
|
|
76
87
|
usage_lines?: UsageLines
|
|
77
88
|
/** The newest raw readings every run shared, each with its `arrived_at`. */
|
|
78
89
|
shared_readings?: unknown[]
|
|
79
|
-
|
|
90
|
+
/**
|
|
91
|
+
* The earlier runs going on the run's repo (`earlierRuns`), read before
|
|
92
|
+
* intake. Left out, there is no repo wait to start.
|
|
93
|
+
*/
|
|
94
|
+
earlier_runs?: RepoWaitRun[]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The decision with no ticket waiting between steps. */
|
|
98
|
+
const alone = (actions: EngineAction[]) => ({ actions, waits: [] })
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* `decideSteps`'s actions, and beside them the waits between steps to
|
|
102
|
+
* journal now (#559): each ticket the same build decision held on purpose,
|
|
103
|
+
* for another ticket's shared baseline tests, the join queue or the start
|
|
104
|
+
* queue, whose wait the journal doesn't say yet. None when the build
|
|
105
|
+
* doesn't run: a crash stop, a repo wait, intake, a plan limit, the run
|
|
106
|
+
* budget, or a pause at the usage line.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* decideNext({ records: journal.read() })
|
|
110
|
+
* // { actions: [{ type: 'run_baseline_tests', ticket: 11 }], waits: [{ ticket: 12, for: 'baseline', on_ticket: 11 }] }
|
|
111
|
+
*/
|
|
112
|
+
export const decideNext = ({
|
|
113
|
+
records,
|
|
114
|
+
usage_lines,
|
|
115
|
+
shared_readings,
|
|
116
|
+
earlier_runs,
|
|
117
|
+
}: DecideInput): { actions: EngineAction[]; waits: TicketWait[] } => {
|
|
80
118
|
const state = replayRun({ records })
|
|
81
119
|
const { phase, spec_number } = state
|
|
82
120
|
const crash = phase === 'new' ? null : decideCrashes({ state })
|
|
@@ -86,10 +124,15 @@ export const decideSteps = ({
|
|
|
86
124
|
crash.type === 'done' && phase === 'intake_passed'
|
|
87
125
|
? decideUsage({ records, state, ending: true })
|
|
88
126
|
: null
|
|
89
|
-
return [usage ?? crash]
|
|
127
|
+
return alone([usage ?? crash])
|
|
90
128
|
}
|
|
129
|
+
const repoWait =
|
|
130
|
+
spec_number === null
|
|
131
|
+
? null
|
|
132
|
+
: decideRepoWait({ state, records, spec_number, earlier_runs })
|
|
133
|
+
if (repoWait !== null) return alone(repoWait)
|
|
91
134
|
if (phase !== 'intake_passed' || spec_number === null) {
|
|
92
|
-
return [decideIntake({ state })]
|
|
135
|
+
return alone([decideIntake({ state })])
|
|
93
136
|
}
|
|
94
137
|
const plan = decidePlan({ state, spec_number })
|
|
95
138
|
if (plan !== null) {
|
|
@@ -98,16 +141,12 @@ export const decideSteps = ({
|
|
|
98
141
|
plan.type === 'done'
|
|
99
142
|
? decideUsage({ records, state, ending: true })
|
|
100
143
|
: null
|
|
101
|
-
return [usage ?? plan]
|
|
144
|
+
return alone([usage ?? plan])
|
|
102
145
|
}
|
|
103
146
|
const budget = decideRunBudget({ state, records, spec_number })
|
|
104
|
-
if (budget !== null) return budget
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
records,
|
|
108
|
-
spec_number,
|
|
109
|
-
build: decideBuild({ state, spec_number }),
|
|
110
|
-
})
|
|
147
|
+
if (budget !== null) return alone(budget)
|
|
148
|
+
const { steps, waits } = decideBuildPlan({ state, spec_number })
|
|
149
|
+
const build = decideMemory({ state, records, spec_number, build: steps })
|
|
111
150
|
const pause = decideUsageLine({
|
|
112
151
|
state,
|
|
113
152
|
records,
|
|
@@ -116,14 +155,18 @@ export const decideSteps = ({
|
|
|
116
155
|
usage_lines,
|
|
117
156
|
shared_readings: shared_readings ?? [],
|
|
118
157
|
})
|
|
119
|
-
if (pause !== null) return [pause]
|
|
158
|
+
if (pause !== null) return alone([pause])
|
|
159
|
+
const started = startedWaits({ waits, records })
|
|
120
160
|
// A run whose every ticket was already done ends with nothing to do.
|
|
121
161
|
const ending =
|
|
122
162
|
build.length === 1 &&
|
|
123
163
|
(build[0]?.type === 'done' || build[0]?.type === 'finish_nothing_to_do')
|
|
124
164
|
const usage = decideUsage({ records, state, ending })
|
|
125
|
-
if (usage === null) return build
|
|
126
|
-
return
|
|
165
|
+
if (usage === null) return { actions: build, waits: started }
|
|
166
|
+
return {
|
|
167
|
+
actions: ending ? [usage] : [usage, ...build],
|
|
168
|
+
waits: started,
|
|
169
|
+
}
|
|
127
170
|
}
|
|
128
171
|
|
|
129
172
|
/**
|
|
@@ -138,12 +181,19 @@ export const decide = ({
|
|
|
138
181
|
records,
|
|
139
182
|
usage_lines,
|
|
140
183
|
shared_readings,
|
|
184
|
+
earlier_runs,
|
|
141
185
|
}: {
|
|
142
186
|
records: JournalRecord[]
|
|
143
187
|
usage_lines?: UsageLines
|
|
144
188
|
shared_readings?: unknown[]
|
|
189
|
+
earlier_runs?: RepoWaitRun[]
|
|
145
190
|
}): EngineAction => {
|
|
146
|
-
const [first] = decideSteps({
|
|
191
|
+
const [first] = decideSteps({
|
|
192
|
+
records,
|
|
193
|
+
usage_lines,
|
|
194
|
+
shared_readings,
|
|
195
|
+
earlier_runs,
|
|
196
|
+
})
|
|
147
197
|
return (
|
|
148
198
|
first ?? {
|
|
149
199
|
type: 'invalid_journal',
|
|
@@ -196,6 +246,7 @@ const decideIntake = ({ state }: { state: RunState }): EngineAction => {
|
|
|
196
246
|
type: 'finish_nothing_to_do',
|
|
197
247
|
closed_tickets: result.closed_tickets,
|
|
198
248
|
already_done: [],
|
|
249
|
+
left_out: result.left_out,
|
|
199
250
|
}
|
|
200
251
|
}
|
|
201
252
|
// A snapshot cut short by a crash is taken again in full; replay
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The owner chores the engine does for a writer after its turn (#618): the
|
|
3
|
+
* file modes it asked for, and the repo's generators it named. Agents can
|
|
4
|
+
* do neither themselves.
|
|
5
|
+
*/
|
|
6
|
+
import { chmod, lstat, realpath } from 'node:fs/promises'
|
|
7
|
+
import { isAbsolute, join, relative, sep } from 'node:path'
|
|
8
|
+
|
|
9
|
+
import type { BuildContext } from './build-context'
|
|
10
|
+
|
|
11
|
+
import type { FileMode, RoleResult } from '../agents/role-results'
|
|
12
|
+
import { testTimeoutOf } from '../config/engine-config'
|
|
13
|
+
import { shellCheck } from '../gates/gate-runner'
|
|
14
|
+
import { endedText, type GateCheck } from '../gates/gate-schemas'
|
|
15
|
+
import { TEST_TIMEOUT_HINT } from '../gates/test-runner'
|
|
16
|
+
import type { GuardArgs } from '../guards/guard-roles'
|
|
17
|
+
import { mayWrite } from '../guards/write-rules'
|
|
18
|
+
|
|
19
|
+
/** The chores a result asks for, or `null` for a role that asks none. */
|
|
20
|
+
const choresOf = ({
|
|
21
|
+
result,
|
|
22
|
+
}: {
|
|
23
|
+
result: RoleResult
|
|
24
|
+
}): { file_modes: FileMode[]; run_generators: string[] } | null =>
|
|
25
|
+
result.role === 'test-writer' || result.role === 'implementer'
|
|
26
|
+
? result.result
|
|
27
|
+
: null
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Sets one file's mode, or says why not: a path the role may not write, no
|
|
31
|
+
* regular file there, or a real path (through a symlinked folder) that
|
|
32
|
+
* leaves the worktree or that the role may not write (#624).
|
|
33
|
+
*/
|
|
34
|
+
const applyMode = async ({
|
|
35
|
+
cwd,
|
|
36
|
+
mode,
|
|
37
|
+
guard,
|
|
38
|
+
context,
|
|
39
|
+
}: {
|
|
40
|
+
cwd: string
|
|
41
|
+
mode: FileMode
|
|
42
|
+
guard: GuardArgs
|
|
43
|
+
context: BuildContext
|
|
44
|
+
}): Promise<string | null> => {
|
|
45
|
+
const { path, executable } = mode
|
|
46
|
+
if (!mayWrite({ ...guard, path, config: context.config })) {
|
|
47
|
+
return `file_modes: ${path} is a path your role may not write, so its mode was not changed.`
|
|
48
|
+
}
|
|
49
|
+
const full = join(cwd, path)
|
|
50
|
+
const found = await lstat(full).catch(() => null)
|
|
51
|
+
const real = await realpath(full).catch(() => null)
|
|
52
|
+
if (found === null || !found.isFile() || real === null) {
|
|
53
|
+
return `file_modes: ${path} is not a file in the worktree, so no mode was set.`
|
|
54
|
+
}
|
|
55
|
+
// A symlinked folder on the way must not lead the mode elsewhere.
|
|
56
|
+
const inside = relative(await realpath(cwd), real)
|
|
57
|
+
if (
|
|
58
|
+
inside === '..' ||
|
|
59
|
+
inside.startsWith(`..${sep}`) ||
|
|
60
|
+
isAbsolute(inside)
|
|
61
|
+
) {
|
|
62
|
+
return `file_modes: ${path} is really ${real}, outside the worktree, so its mode was not changed.`
|
|
63
|
+
}
|
|
64
|
+
if (!mayWrite({ ...guard, path: inside, config: context.config })) {
|
|
65
|
+
return `file_modes: ${path} is really ${inside}, a path your role may not write, so its mode was not changed.`
|
|
66
|
+
}
|
|
67
|
+
const mode_bits = executable ? found.mode | 0o111 : found.mode & ~0o111
|
|
68
|
+
await chmod(real, mode_bits & 0o7777)
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Runs one named generator, as a `generate` check, or says it has none. */
|
|
73
|
+
const runGenerator = async ({
|
|
74
|
+
cwd,
|
|
75
|
+
name,
|
|
76
|
+
context,
|
|
77
|
+
}: {
|
|
78
|
+
cwd: string
|
|
79
|
+
name: string
|
|
80
|
+
context: BuildContext
|
|
81
|
+
}): Promise<{ check: GateCheck | null; problem: string | null }> => {
|
|
82
|
+
const generators = context.config.generators ?? {}
|
|
83
|
+
// Own names only: `constructor` and friends are inherited, not named.
|
|
84
|
+
const command = Object.hasOwn(generators, name)
|
|
85
|
+
? generators[name]
|
|
86
|
+
: undefined
|
|
87
|
+
if (command === undefined) {
|
|
88
|
+
const known = Object.keys(generators).map((other) => `"${other}"`)
|
|
89
|
+
return {
|
|
90
|
+
check: null,
|
|
91
|
+
problem: `run_generators: "${name}" is not a generator this repo names (${known.length === 0 ? 'it names none' : `it names ${known.join(', ')}`}).`,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const check = await shellCheck({
|
|
95
|
+
name: 'generate',
|
|
96
|
+
command,
|
|
97
|
+
cwd,
|
|
98
|
+
timeout_ms: testTimeoutOf({ config: context.config }),
|
|
99
|
+
timeout_hint: TEST_TIMEOUT_HINT,
|
|
100
|
+
})
|
|
101
|
+
return {
|
|
102
|
+
check,
|
|
103
|
+
problem: check.ok
|
|
104
|
+
? null
|
|
105
|
+
: `run_generators: the generator "${name}" (\`${command}\`) failed (${endedText(check)}):\n${check.output}`,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Does the chores a writer's result asks for, in its worktree: each file
|
|
111
|
+
* mode, on a path the role may write (others are refused and left as they
|
|
112
|
+
* are), then each named generator, in order, as a `generate` check with the
|
|
113
|
+
* test time limit. What they change is committed with the rest of the
|
|
114
|
+
* turn's work. Journals `chores_done` when the result asked for any.
|
|
115
|
+
*
|
|
116
|
+
* @returns The problems, for the role's fix loop: a refused path, an
|
|
117
|
+
* unknown generator, or a failed one. Empty when every chore was done.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* const problems = await doChores({ context, cwd, result, guard })
|
|
121
|
+
*/
|
|
122
|
+
export const doChores = async ({
|
|
123
|
+
context,
|
|
124
|
+
cwd,
|
|
125
|
+
ticket,
|
|
126
|
+
result,
|
|
127
|
+
guard,
|
|
128
|
+
}: {
|
|
129
|
+
context: BuildContext
|
|
130
|
+
cwd: string
|
|
131
|
+
/** `null` in the final review. */
|
|
132
|
+
ticket: number | null
|
|
133
|
+
result: RoleResult
|
|
134
|
+
guard: GuardArgs
|
|
135
|
+
}): Promise<string[]> => {
|
|
136
|
+
const chores = choresOf({ result })
|
|
137
|
+
if (
|
|
138
|
+
chores === null ||
|
|
139
|
+
(chores.file_modes.length === 0 && chores.run_generators.length === 0)
|
|
140
|
+
) {
|
|
141
|
+
return []
|
|
142
|
+
}
|
|
143
|
+
const problems: string[] = []
|
|
144
|
+
const file_modes: FileMode[] = []
|
|
145
|
+
for (const mode of chores.file_modes) {
|
|
146
|
+
const problem = await applyMode({ cwd, mode, guard, context })
|
|
147
|
+
if (problem === null) file_modes.push(mode)
|
|
148
|
+
else problems.push(problem)
|
|
149
|
+
}
|
|
150
|
+
const checks: GateCheck[] = []
|
|
151
|
+
for (const name of chores.run_generators) {
|
|
152
|
+
const ran = await runGenerator({ cwd, name, context })
|
|
153
|
+
if (ran.check !== null) checks.push(ran.check)
|
|
154
|
+
if (ran.problem !== null) problems.push(ran.problem)
|
|
155
|
+
}
|
|
156
|
+
context.journal.append({
|
|
157
|
+
kind: 'chores_done',
|
|
158
|
+
ticket,
|
|
159
|
+
role: result.role,
|
|
160
|
+
content: { role: result.role, file_modes, checks, problems },
|
|
161
|
+
})
|
|
162
|
+
return problems
|
|
163
|
+
}
|