@alecsibilia/luca 14.0.0-alpha.5 → 14.0.0-alpha.7
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 +8 -0
- package/board/package.json +1 -1
- package/board/server/board-vocabulary.ts +15 -1
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +10 -0
- package/board/server/reduce-board.ts +6 -0
- package/engine/agents/role-prompts.ts +22 -0
- package/engine/cli/run-modes.ts +41 -3
- package/engine/config/config-reload.ts +160 -0
- package/engine/core/decide-build.ts +27 -12
- package/engine/core/decide-run-budget.ts +12 -3
- package/engine/core/execute-build.ts +95 -41
- package/engine/core/execute-changeset.ts +6 -8
- package/engine/core/execute-final-review.ts +86 -10
- package/engine/core/execute-pull-request.ts +84 -0
- package/engine/core/execute-stuck.ts +7 -9
- package/engine/core/execute.ts +90 -4
- package/engine/core/final-review-text.ts +13 -4
- package/engine/core/pull-request-text.ts +140 -0
- package/engine/core/stuck-text.ts +29 -0
- package/engine/gates/leftover-scan.ts +58 -4
- package/engine/git/git-adapter.ts +29 -0
- package/engine/intake/intake-checks.ts +39 -15
- package/engine/journal/journal-record.ts +67 -5
- package/engine/journal/replay.ts +45 -10
- package/engine/tracker/github-tracker.ts +19 -9
- package/engine/tracker/in-memory-tracker.ts +3 -2
- package/engine/tracker/tracker.ts +37 -4
- package/package.json +1 -1
- package/skills/luca-unstick/SKILL.md +1 -0
package/CHANGELOG.md
CHANGED
package/board/package.json
CHANGED
|
@@ -149,6 +149,15 @@ export const BOARD_VOCABULARY = {
|
|
|
149
149
|
}),
|
|
150
150
|
/** The engine started again on the run's journal, on Luca's `luca_version`. */
|
|
151
151
|
engine_resumed: z.looseObject({ luca_version: z.string() }),
|
|
152
|
+
/**
|
|
153
|
+
* A resume took the repo's new build fields (#516); the board reads
|
|
154
|
+
* only which fields changed.
|
|
155
|
+
*/
|
|
156
|
+
config_reloaded: z.looseObject({
|
|
157
|
+
changes: z.array(z.looseObject({ field: z.string() })),
|
|
158
|
+
}),
|
|
159
|
+
/** A resume kept the run's config, as the repo's couldn't be used. */
|
|
160
|
+
config_reload_refused: z.looseObject({ reason: z.string() }),
|
|
152
161
|
intake_read: z.looseObject({ spec: SpecIssueSchema }),
|
|
153
162
|
intake_refused: z.looseObject({
|
|
154
163
|
problems: z.array(
|
|
@@ -279,7 +288,10 @@ export const BOARD_VOCABULARY = {
|
|
|
279
288
|
reason: z.string(),
|
|
280
289
|
detail: z.string().catch(''),
|
|
281
290
|
}),
|
|
282
|
-
/**
|
|
291
|
+
/**
|
|
292
|
+
* The whole run is stuck (#435): `run_budget`, it used up its budget,
|
|
293
|
+
* or `pull_request_failed`, its PR didn't open (#508).
|
|
294
|
+
*/
|
|
283
295
|
run_stuck: z.looseObject({
|
|
284
296
|
reason: z.string(),
|
|
285
297
|
detail: z.string().catch(''),
|
|
@@ -445,6 +457,8 @@ const entry = <Kind extends BoardKind>({ kind }: { kind: Kind }) =>
|
|
|
445
457
|
const BoardEntrySchema = z.discriminatedUnion('kind', [
|
|
446
458
|
entry({ kind: 'run_started' }),
|
|
447
459
|
entry({ kind: 'engine_resumed' }),
|
|
460
|
+
entry({ kind: 'config_reloaded' }),
|
|
461
|
+
entry({ kind: 'config_reload_refused' }),
|
|
448
462
|
entry({ kind: 'intake_read' }),
|
|
449
463
|
entry({ kind: 'intake_refused' }),
|
|
450
464
|
entry({ kind: 'nothing_to_do' }),
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The Luca version of this board, stamped when the package was packed. */
|
|
2
|
-
export const LUCA_VERSION: string = "14.0.0-alpha.
|
|
2
|
+
export const LUCA_VERSION: string = "14.0.0-alpha.7"
|
|
@@ -252,6 +252,16 @@ export const describeRecord = ({
|
|
|
252
252
|
after.run.version_note !== before.run.version_note
|
|
253
253
|
? event({ text: after.run.version_note, tone: 'warning' })
|
|
254
254
|
: null
|
|
255
|
+
case 'config_reloaded':
|
|
256
|
+
return event({
|
|
257
|
+
text: `Config reloaded: ${record.content.changes.map(({ field }) => field).join(', ')} changed.`,
|
|
258
|
+
tone: 'info',
|
|
259
|
+
})
|
|
260
|
+
case 'config_reload_refused':
|
|
261
|
+
return event({
|
|
262
|
+
text: `Config not reloaded, so the run keeps its own: ${record.content.reason.split('\n')[0] ?? ''}`,
|
|
263
|
+
tone: 'warning',
|
|
264
|
+
})
|
|
255
265
|
case 'intake_refused':
|
|
256
266
|
return event({
|
|
257
267
|
text: `Intake refused the run. ${after.run.refusal.join('; ')}`,
|
|
@@ -408,6 +408,8 @@ const resolveNeedsYou = ({
|
|
|
408
408
|
const RUN_STUCK_REASONS: Record<string, string> = {
|
|
409
409
|
run_budget:
|
|
410
410
|
'The run used up its run budget of tokens. `retry` adds one more full budget.',
|
|
411
|
+
pull_request_failed:
|
|
412
|
+
"The run's pull request didn't open. `retry` tries again, reusing a PR already open from the run branch.",
|
|
411
413
|
}
|
|
412
414
|
|
|
413
415
|
/** A run stuck reason code in words. */
|
|
@@ -861,6 +863,10 @@ const applyKind = ({
|
|
|
861
863
|
}),
|
|
862
864
|
},
|
|
863
865
|
}
|
|
866
|
+
// A resume's config reload (#516) is only a chat row.
|
|
867
|
+
case 'config_reloaded':
|
|
868
|
+
case 'config_reload_refused':
|
|
869
|
+
return state
|
|
864
870
|
case 'intake_read':
|
|
865
871
|
return {
|
|
866
872
|
...state,
|
|
@@ -190,6 +190,28 @@ export const prepareSection = ({ command }: { command: string }): string =>
|
|
|
190
190
|
"If the engine's prepare command fails after your turn, its output comes back to you like a failed check.",
|
|
191
191
|
].join('\n\n')
|
|
192
192
|
|
|
193
|
+
/**
|
|
194
|
+
* The section a test-writer or implementer (a ticket's or the final
|
|
195
|
+
* review's fixers) gets in a repo with changesets: the engine writes the
|
|
196
|
+
* run's one changeset, so an agent's own is extra.
|
|
197
|
+
*/
|
|
198
|
+
export const CHANGESETS_SECTION = [
|
|
199
|
+
'## The engine writes the changeset',
|
|
200
|
+
"Don't write changesets, even if the repo's rules ask for one: the engine writes the run's one changeset when it opens the PR. " +
|
|
201
|
+
'A new file you add in `.changeset/` is removed before the commit. ' +
|
|
202
|
+
'A finding that only asks for a changeset is answered by this: answer it "wont_fix" and say the engine adds it.',
|
|
203
|
+
].join('\n\n')
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The section the rules lens gets in a repo with changesets, so a rule like
|
|
207
|
+
* "every PR needs a changeset" raises no finding.
|
|
208
|
+
*/
|
|
209
|
+
export const CHANGESETS_LENS_SECTION = [
|
|
210
|
+
'## The engine writes the changeset',
|
|
211
|
+
"The engine adds the run's one changeset when it opens the PR, so a rule that asks for a changeset is met. " +
|
|
212
|
+
'Raise no finding that only asks for a changeset.',
|
|
213
|
+
].join('\n\n')
|
|
214
|
+
|
|
193
215
|
/** What each lens judges the whole run branch on, in one paragraph. */
|
|
194
216
|
const LENS_TASK_TEXT =
|
|
195
217
|
'You are one lens of the final review: a fresh, read-only reviewer of the WHOLE run branch, every ticket of the spec together. Write nothing.'
|
package/engine/cli/run-modes.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { loadEngineConfig } from '../config/engine-config'
|
|
|
13
13
|
import { lucaVersion } from '../config/luca-version'
|
|
14
14
|
import { decide, type EngineAction } from '../core/decide'
|
|
15
15
|
import {
|
|
16
|
+
recordConfigReload,
|
|
16
17
|
recordResume,
|
|
17
18
|
runEngine,
|
|
18
19
|
startRun,
|
|
@@ -210,13 +211,46 @@ const driveRun = async ({
|
|
|
210
211
|
return end
|
|
211
212
|
}
|
|
212
213
|
|
|
214
|
+
/** A value of a reloaded config field in a log line; `null` is "none". */
|
|
215
|
+
const fieldValue = (value: string | number | null): string =>
|
|
216
|
+
value === null ? 'none' : String(value)
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The log line for a resume's `config_reloaded` or `config_reload_refused`
|
|
220
|
+
* (#516).
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* configReloadText({ reload }) // 'config reloaded: prepare changed (make rom -> make rom-cached)'
|
|
224
|
+
*/
|
|
225
|
+
export const configReloadText = ({
|
|
226
|
+
reload,
|
|
227
|
+
}: {
|
|
228
|
+
reload: JournalRecord
|
|
229
|
+
}): string => {
|
|
230
|
+
switch (reload.kind) {
|
|
231
|
+
case 'config_reloaded':
|
|
232
|
+
return `config reloaded: ${reload.content.changes
|
|
233
|
+
.map(
|
|
234
|
+
({ field, from, to }) =>
|
|
235
|
+
`${field} changed (${fieldValue(from)} -> ${fieldValue(to)})`
|
|
236
|
+
)
|
|
237
|
+
.join(', ')}`
|
|
238
|
+
case 'config_reload_refused':
|
|
239
|
+
return `kept the run's config, as the repo's can't be used: ${reload.content.reason}`
|
|
240
|
+
default:
|
|
241
|
+
return reload.kind
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
213
245
|
/**
|
|
214
246
|
* A real run of `spec_number` on `repo`: loads the repo's engine config,
|
|
215
247
|
* opens (or resumes) the run's journal at `<runs_dir>/<run_id>`, and runs the
|
|
216
248
|
* engine with the board kept in step, the agents from `launcher`, and Jev
|
|
217
249
|
* in shadow mode if given. With `memory`, a new run turns memory on (#370),
|
|
218
250
|
* with the engine config's `muninn.vault` as the project vault (none:
|
|
219
|
-
* `default` only); a resumed run keeps what its journal says.
|
|
251
|
+
* `default` only); a resumed run keeps what its journal says. A new run
|
|
252
|
+
* stops on a bad config; a resume takes the config's build fields again
|
|
253
|
+
* and keeps its own on a bad one (`recordConfigReload`, #516). Never throws;
|
|
220
254
|
* the launcher's sessions and the memory client are closed and the board
|
|
221
255
|
* always gets the run's end.
|
|
222
256
|
*
|
|
@@ -268,7 +302,9 @@ export const runSpec = async ({
|
|
|
268
302
|
log(`[luca-run] spec #${spec_number} in ${repo}, run ${run_id}`)
|
|
269
303
|
log(`[luca-run] journal: ${journal.file}`)
|
|
270
304
|
const loaded = await loadEngineConfig({ repo_root: repo })
|
|
271
|
-
|
|
305
|
+
const fresh = isEmpty(journal.read())
|
|
306
|
+
// A new run needs a good config; a resume keeps its own (#516).
|
|
307
|
+
if (fresh && !loaded.ok) {
|
|
272
308
|
log(`[luca-run] stopped: ${loaded.error}`)
|
|
273
309
|
await launcher.closeAll?.()
|
|
274
310
|
await memory?.client.close().catch(() => undefined)
|
|
@@ -276,7 +312,7 @@ export const runSpec = async ({
|
|
|
276
312
|
await board?.end({ records: journal.read(), ...end })
|
|
277
313
|
return end
|
|
278
314
|
}
|
|
279
|
-
if (
|
|
315
|
+
if (fresh && loaded.ok) {
|
|
280
316
|
startRun({
|
|
281
317
|
journal,
|
|
282
318
|
spec_number,
|
|
@@ -292,6 +328,8 @@ export const runSpec = async ({
|
|
|
292
328
|
} else {
|
|
293
329
|
log('[luca-run] resuming the run from its journal')
|
|
294
330
|
recordResume({ journal, luca_version })
|
|
331
|
+
const reload = recordConfigReload({ journal, loaded })
|
|
332
|
+
if (reload !== null) log(`[luca-run] ${configReloadText({ reload })}`)
|
|
295
333
|
}
|
|
296
334
|
return driveRun({
|
|
297
335
|
journal,
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import omit from 'lodash/omit'
|
|
2
|
+
import reduce from 'lodash/reduce'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
import type { EngineConfig } from './engine-config'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The config fields a resumed run takes from the repo's
|
|
9
|
+
* `.luca/config.json` again (#516). They say how the engine builds a
|
|
10
|
+
* checkout before its tests, not what the tests are, so a run can change
|
|
11
|
+
* them halfway without making its earlier results wrong: a repo that swaps
|
|
12
|
+
* a slow `prepare` for a fast cached one gets it on the next resume.
|
|
13
|
+
*
|
|
14
|
+
* - `prepare`: the command run before each test run in a checkout.
|
|
15
|
+
* - `prepare_timeout_ms`: how long that command may run.
|
|
16
|
+
* - `prepare_concurrency`: how many prepare runs may go at once. The
|
|
17
|
+
* prepare slots take the limit on each call (`prepareCheck`), so a new
|
|
18
|
+
* one counts from the next prepare run.
|
|
19
|
+
*/
|
|
20
|
+
export const RELOADED_FIELDS = [
|
|
21
|
+
'prepare',
|
|
22
|
+
'prepare_timeout_ms',
|
|
23
|
+
'prepare_concurrency',
|
|
24
|
+
] as const
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The config fields a run keeps from its `run_started`, whatever the file
|
|
28
|
+
* says on a resume:
|
|
29
|
+
*
|
|
30
|
+
* - `checks`: the baseline, the red check, and the gates compare test
|
|
31
|
+
* results across the run. A ticket takes another's baseline by commit
|
|
32
|
+
* alone (`sharedBaselineStep` in `core/decide-build.ts`), and the red
|
|
33
|
+
* check holds every old passing test of the baseline to the new run by
|
|
34
|
+
* file and name (`checkRed` in `gates/red-check.ts`). A baseline taken
|
|
35
|
+
* with old commands is not comparable with a run of new ones. Intake also
|
|
36
|
+
* judged the run's tickets against these commands (a `bun` test command
|
|
37
|
+
* for the red check).
|
|
38
|
+
* - `test_file_patterns`: which files are tests decides the red check, the
|
|
39
|
+
* role rules, and which commits were the test-writer's.
|
|
40
|
+
* - `test_setup_files`: part of what the tests are, as `checks` is.
|
|
41
|
+
* - `rule_files`: the rules lens judges against them; tickets already
|
|
42
|
+
* reviewed were judged on the old ones.
|
|
43
|
+
* - `muninn`: the run's memory vault is fixed at start (`run_started.memory`).
|
|
44
|
+
* - `run_budget_tokens`: the budget the run's tokens are counted against,
|
|
45
|
+
* and the board's, from `run_started`; a raise is a `retry` reply.
|
|
46
|
+
*/
|
|
47
|
+
export const FROZEN_FIELDS = [
|
|
48
|
+
'checks',
|
|
49
|
+
'test_file_patterns',
|
|
50
|
+
'test_setup_files',
|
|
51
|
+
'rule_files',
|
|
52
|
+
'muninn',
|
|
53
|
+
'run_budget_tokens',
|
|
54
|
+
] as const
|
|
55
|
+
|
|
56
|
+
const positiveInt = z.number().int().positive().nullable()
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* One reloaded field that changed: its value in the run before (`from`)
|
|
60
|
+
* and in the file now (`to`). `null` is a field left out, so a field added
|
|
61
|
+
* or removed is a change too.
|
|
62
|
+
*/
|
|
63
|
+
export const ConfigChangeSchema = z.discriminatedUnion('field', [
|
|
64
|
+
z.object({
|
|
65
|
+
field: z.literal('prepare'),
|
|
66
|
+
from: z.string().min(1).nullable(),
|
|
67
|
+
to: z.string().min(1).nullable(),
|
|
68
|
+
}),
|
|
69
|
+
z.object({
|
|
70
|
+
field: z.literal('prepare_timeout_ms'),
|
|
71
|
+
from: positiveInt,
|
|
72
|
+
to: positiveInt,
|
|
73
|
+
}),
|
|
74
|
+
z.object({
|
|
75
|
+
field: z.literal('prepare_concurrency'),
|
|
76
|
+
from: positiveInt,
|
|
77
|
+
to: positiveInt,
|
|
78
|
+
}),
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
export type ConfigChange = z.infer<typeof ConfigChangeSchema>
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The reloaded fields whose value differs between the run's config
|
|
85
|
+
* (`current`) and the one just read (`next`), old to new, in
|
|
86
|
+
* `RELOADED_FIELDS` order. Frozen fields are never changes. Pure.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* configChanges({ current: { ...config, prepare: 'make' }, next: { ...config, prepare: 'make fast' } })
|
|
90
|
+
* // [{ field: 'prepare', from: 'make', to: 'make fast' }]
|
|
91
|
+
*/
|
|
92
|
+
export const configChanges = ({
|
|
93
|
+
current,
|
|
94
|
+
next,
|
|
95
|
+
}: {
|
|
96
|
+
current: EngineConfig
|
|
97
|
+
next: EngineConfig
|
|
98
|
+
}): ConfigChange[] => {
|
|
99
|
+
const all: ConfigChange[] = [
|
|
100
|
+
{
|
|
101
|
+
field: 'prepare',
|
|
102
|
+
from: current.prepare ?? null,
|
|
103
|
+
to: next.prepare ?? null,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
field: 'prepare_timeout_ms',
|
|
107
|
+
from: current.prepare_timeout_ms ?? null,
|
|
108
|
+
to: next.prepare_timeout_ms ?? null,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
field: 'prepare_concurrency',
|
|
112
|
+
from: current.prepare_concurrency ?? null,
|
|
113
|
+
to: next.prepare_concurrency ?? null,
|
|
114
|
+
},
|
|
115
|
+
]
|
|
116
|
+
return all.filter(({ from, to }) => from !== to)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const applyChange = ({
|
|
120
|
+
config,
|
|
121
|
+
change,
|
|
122
|
+
}: {
|
|
123
|
+
config: EngineConfig
|
|
124
|
+
change: ConfigChange
|
|
125
|
+
}): EngineConfig => {
|
|
126
|
+
switch (change.field) {
|
|
127
|
+
case 'prepare':
|
|
128
|
+
return change.to === null
|
|
129
|
+
? omit(config, 'prepare')
|
|
130
|
+
: { ...config, prepare: change.to }
|
|
131
|
+
case 'prepare_timeout_ms':
|
|
132
|
+
return change.to === null
|
|
133
|
+
? omit(config, 'prepare_timeout_ms')
|
|
134
|
+
: { ...config, prepare_timeout_ms: change.to }
|
|
135
|
+
case 'prepare_concurrency':
|
|
136
|
+
return change.to === null
|
|
137
|
+
? omit(config, 'prepare_concurrency')
|
|
138
|
+
: { ...config, prepare_concurrency: change.to }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* `config` with each change's new value: set, or left out for `null`. Pure.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* withConfigChanges({ config, changes: [{ field: 'prepare', from: 'make', to: null }] })
|
|
147
|
+
* // config without prepare
|
|
148
|
+
*/
|
|
149
|
+
export const withConfigChanges = ({
|
|
150
|
+
config,
|
|
151
|
+
changes,
|
|
152
|
+
}: {
|
|
153
|
+
config: EngineConfig
|
|
154
|
+
changes: ConfigChange[]
|
|
155
|
+
}): EngineConfig =>
|
|
156
|
+
reduce(
|
|
157
|
+
changes,
|
|
158
|
+
(next, change) => applyChange({ config: next, change }),
|
|
159
|
+
config
|
|
160
|
+
)
|
|
@@ -47,6 +47,8 @@ import {
|
|
|
47
47
|
import { badTestDetail, retrySection, setupChangeDetail } from './stuck-text'
|
|
48
48
|
|
|
49
49
|
import {
|
|
50
|
+
CHANGESETS_LENS_SECTION,
|
|
51
|
+
CHANGESETS_SECTION,
|
|
50
52
|
prepareSection,
|
|
51
53
|
rejoinSection,
|
|
52
54
|
rolePrompt,
|
|
@@ -1303,34 +1305,47 @@ export const decideBuild = ({
|
|
|
1303
1305
|
}): BuildAction[] => {
|
|
1304
1306
|
const steps = buildSteps({ state, spec_number })
|
|
1305
1307
|
const command = state.config?.prepare
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1308
|
+
const prepared =
|
|
1309
|
+
command === undefined
|
|
1310
|
+
? steps
|
|
1311
|
+
: steps.map((step) =>
|
|
1312
|
+
withWriterSection({
|
|
1313
|
+
step,
|
|
1314
|
+
section: prepareSection({ command }),
|
|
1315
|
+
})
|
|
1316
|
+
)
|
|
1317
|
+
return state.changesets ? prepared.map(withChangesetsSections) : prepared
|
|
1309
1318
|
}
|
|
1310
1319
|
|
|
1311
1320
|
/**
|
|
1312
1321
|
* A fresh test-writer's or implementer's launch (a ticket's or the final
|
|
1313
|
-
* review's fixers), with
|
|
1314
|
-
* Any other step as it is.
|
|
1322
|
+
* review's fixers), with `section` added to its prompt, such as the repo's
|
|
1323
|
+
* prepare command. Any other step as it is.
|
|
1315
1324
|
*/
|
|
1316
|
-
const
|
|
1325
|
+
const withWriterSection = ({
|
|
1317
1326
|
step,
|
|
1318
|
-
|
|
1327
|
+
section,
|
|
1319
1328
|
}: {
|
|
1320
1329
|
step: BuildAction
|
|
1321
|
-
|
|
1330
|
+
section: string
|
|
1322
1331
|
}): BuildAction => {
|
|
1323
1332
|
const writesCode =
|
|
1324
1333
|
(step.type === 'launch_agent' || step.type === 'launch_final_fixer') &&
|
|
1325
1334
|
(step.role === 'test-writer' || step.role === 'implementer')
|
|
1326
1335
|
return writesCode
|
|
1327
|
-
? {
|
|
1328
|
-
...step,
|
|
1329
|
-
prompt: `${step.prompt}\n\n${prepareSection({ command })}`,
|
|
1330
|
-
}
|
|
1336
|
+
? { ...step, prompt: `${step.prompt}\n\n${section}` }
|
|
1331
1337
|
: step
|
|
1332
1338
|
}
|
|
1333
1339
|
|
|
1340
|
+
/**
|
|
1341
|
+
* In a repo with changesets, the writers are told not to write one and the
|
|
1342
|
+
* rules lens that the engine adds the run's one at PR time (#507).
|
|
1343
|
+
*/
|
|
1344
|
+
const withChangesetsSections = (step: BuildAction): BuildAction =>
|
|
1345
|
+
step.type === 'launch_lens' && step.lens === 'rules'
|
|
1346
|
+
? { ...step, prompt: `${step.prompt}\n\n${CHANGESETS_LENS_SECTION}` }
|
|
1347
|
+
: withWriterSection({ step, section: CHANGESETS_SECTION })
|
|
1348
|
+
|
|
1334
1349
|
/** `decideBuild`'s steps, before the prepare command joins any prompt. */
|
|
1335
1350
|
const buildSteps = ({
|
|
1336
1351
|
state,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { pendingReplies, waitForReply, type StuckAction } from './decide-stuck'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
pullRequestStuckComment,
|
|
4
|
+
runBudgetDetail,
|
|
5
|
+
runStuckComment,
|
|
6
|
+
} from './stuck-text'
|
|
3
7
|
|
|
4
8
|
import type { JournalRecord, RunStuckReason } from '../journal/journal-record'
|
|
5
9
|
import type { RunState } from '../journal/replay'
|
|
@@ -19,7 +23,9 @@ export type RunBudgetAction =
|
|
|
19
23
|
* reason "run budget": that is its only step, and nothing new starts. The
|
|
20
24
|
* spec issue is told, then the run waits for the owner: a bare `retry`
|
|
21
25
|
* adds one more full budget (replay does that), and `stop` ends the run.
|
|
22
|
-
* The owner's other replies wait until the run carries on.
|
|
26
|
+
* The owner's other replies wait until the run carries on. A run whose PR
|
|
27
|
+
* didn't open (#508) is stuck the same way, told why, and a `retry` tries
|
|
28
|
+
* the PR again, adding no budget. `null` when
|
|
23
29
|
* the budget has nothing to say: the run is under it, stopping, or its PR
|
|
24
30
|
* is open.
|
|
25
31
|
*
|
|
@@ -56,7 +62,10 @@ export const decideRunBudget = ({
|
|
|
56
62
|
{
|
|
57
63
|
type: 'report_run_stuck',
|
|
58
64
|
spec_number,
|
|
59
|
-
body:
|
|
65
|
+
body:
|
|
66
|
+
stuck.reason === 'pull_request_failed'
|
|
67
|
+
? pullRequestStuckComment({ detail: stuck.detail })
|
|
68
|
+
: runStuckComment({ detail: stuck.detail, budget }),
|
|
60
69
|
},
|
|
61
70
|
]
|
|
62
71
|
}
|