@alecsibilia/luca 14.0.0-alpha.6 → 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 +4 -0
- package/board/package.json +1 -1
- package/board/server/board-vocabulary.ts +11 -0
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +10 -0
- package/board/server/reduce-board.ts +4 -0
- package/engine/cli/run-modes.ts +41 -3
- package/engine/config/config-reload.ts +160 -0
- package/engine/core/execute.ts +63 -2
- package/engine/intake/intake-checks.ts +39 -15
- package/engine/journal/journal-record.ts +30 -0
- package/engine/journal/replay.ts +18 -0
- package/package.json +1 -1
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(
|
|
@@ -448,6 +457,8 @@ const entry = <Kind extends BoardKind>({ kind }: { kind: Kind }) =>
|
|
|
448
457
|
const BoardEntrySchema = z.discriminatedUnion('kind', [
|
|
449
458
|
entry({ kind: 'run_started' }),
|
|
450
459
|
entry({ kind: 'engine_resumed' }),
|
|
460
|
+
entry({ kind: 'config_reloaded' }),
|
|
461
|
+
entry({ kind: 'config_reload_refused' }),
|
|
451
462
|
entry({ kind: 'intake_read' }),
|
|
452
463
|
entry({ kind: 'intake_refused' }),
|
|
453
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('; ')}`,
|
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
|
+
)
|
package/engine/core/execute.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { basename, dirname } from 'node:path'
|
|
|
2
2
|
|
|
3
3
|
import has from 'lodash/has'
|
|
4
4
|
import partition from 'lodash/partition'
|
|
5
|
+
import values from 'lodash/values'
|
|
5
6
|
|
|
6
7
|
import { decideSteps, type EngineAction } from './decide'
|
|
7
8
|
import { isFinalReviewAction } from './decide-final-review'
|
|
@@ -29,9 +30,13 @@ import {
|
|
|
29
30
|
} from './session-close'
|
|
30
31
|
|
|
31
32
|
import type { BoardSync } from '../board/board-sync'
|
|
32
|
-
import
|
|
33
|
+
import { configChanges } from '../config/config-reload'
|
|
34
|
+
import type {
|
|
35
|
+
EngineConfig,
|
|
36
|
+
LoadEngineConfigResult,
|
|
37
|
+
} from '../config/engine-config'
|
|
33
38
|
import { lucaVersion } from '../config/luca-version'
|
|
34
|
-
import { outsideBlockerNumbers } from '../intake/intake-checks'
|
|
39
|
+
import { configProblems, outsideBlockerNumbers } from '../intake/intake-checks'
|
|
35
40
|
import type { IntakeProblem } from '../intake/intake-schemas'
|
|
36
41
|
import { jevAsksAfter, jevAsksBefore } from '../jev/jev-jobs'
|
|
37
42
|
import { askJevInShadow, type JevShadow } from '../jev/jev-shadow'
|
|
@@ -140,6 +145,62 @@ export const recordResume = ({
|
|
|
140
145
|
content: { luca_version },
|
|
141
146
|
})
|
|
142
147
|
|
|
148
|
+
/**
|
|
149
|
+
* On a resume, takes the repo's config again (#516), just read as
|
|
150
|
+
* `loaded`, and journals what came of it, right after `recordResume`:
|
|
151
|
+
*
|
|
152
|
+
* - a `config_reloaded` with the build fields (`RELOADED_FIELDS`) that
|
|
153
|
+
* changed, old to new, which replay puts on the run's config;
|
|
154
|
+
* - nothing, when none changed (frozen fields never count, see
|
|
155
|
+
* `FROZEN_FIELDS`);
|
|
156
|
+
* - a `config_reload_refused` with the reason, when the file is missing,
|
|
157
|
+
* not JSON, not a valid config, or one intake would refuse
|
|
158
|
+
* (`configProblems`, on the run's tickets). The run keeps its config.
|
|
159
|
+
*
|
|
160
|
+
* Never throws for a bad file: the run always goes on. Returns the record
|
|
161
|
+
* it appended, or `null`. A journal with no config yet (not a run) gets
|
|
162
|
+
* nothing; the decision step says what is wrong with it.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* recordResume({ journal })
|
|
166
|
+
* const reload = recordConfigReload({ journal, loaded: await loadEngineConfig({ repo_root }) })
|
|
167
|
+
*/
|
|
168
|
+
export const recordConfigReload = ({
|
|
169
|
+
journal,
|
|
170
|
+
loaded,
|
|
171
|
+
}: {
|
|
172
|
+
journal: Journal
|
|
173
|
+
loaded: LoadEngineConfigResult
|
|
174
|
+
}): JournalRecord | null => {
|
|
175
|
+
const state = replayRun({ records: journal.read() })
|
|
176
|
+
if (state.config === null) return null
|
|
177
|
+
const refuse = (reason: string): JournalRecord =>
|
|
178
|
+
journal.append({
|
|
179
|
+
kind: 'config_reload_refused',
|
|
180
|
+
ticket: null,
|
|
181
|
+
role: null,
|
|
182
|
+
content: { reason },
|
|
183
|
+
})
|
|
184
|
+
if (!loaded.ok) return refuse(loaded.error)
|
|
185
|
+
const problems = configProblems({
|
|
186
|
+
config: loaded.config,
|
|
187
|
+
tickets: values(state.snapshot?.tickets ?? {}),
|
|
188
|
+
})
|
|
189
|
+
if (problems.length > 0) return refuse(problems.join('\n'))
|
|
190
|
+
const changes = configChanges({
|
|
191
|
+
current: state.config,
|
|
192
|
+
next: loaded.config,
|
|
193
|
+
})
|
|
194
|
+
return changes.length === 0
|
|
195
|
+
? null
|
|
196
|
+
: journal.append({
|
|
197
|
+
kind: 'config_reloaded',
|
|
198
|
+
ticket: null,
|
|
199
|
+
role: null,
|
|
200
|
+
content: { changes },
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
143
204
|
/** The comment a bad spec or ticket gets when intake refuses the run. */
|
|
144
205
|
export const refusalComment = ({
|
|
145
206
|
spec_number,
|
|
@@ -226,6 +226,43 @@ const groupFindings = ({
|
|
|
226
226
|
}))
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Intake's checks on the engine config, as messages for a person: a test
|
|
231
|
+
* command, and one with bun's per-test results unless every ticket in
|
|
232
|
+
* `tickets` is a refactor ticket (the red check reads per-test results,
|
|
233
|
+
* which only bun's give today). Pure. Empty when the config is fine.
|
|
234
|
+
*
|
|
235
|
+
* Intake runs it on the open tickets agents build; a resume runs it on the
|
|
236
|
+
* repo's config file before taking its new build fields (#516).
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* configProblems({ config: { ...config, checks: {} }, tickets: [] })
|
|
240
|
+
* // ['The engine config (.luca/config.json) has no test command at checks.test.']
|
|
241
|
+
*/
|
|
242
|
+
export const configProblems = ({
|
|
243
|
+
config,
|
|
244
|
+
tickets,
|
|
245
|
+
}: {
|
|
246
|
+
config: EngineConfig
|
|
247
|
+
tickets: { labels: string[] }[]
|
|
248
|
+
}): string[] => {
|
|
249
|
+
const tests = testCommands({ config })
|
|
250
|
+
if (tests.length === 0) {
|
|
251
|
+
return [
|
|
252
|
+
`The engine config (${ENGINE_CONFIG_FILE}) has no test command at checks.test.`,
|
|
253
|
+
]
|
|
254
|
+
}
|
|
255
|
+
if (
|
|
256
|
+
!tests.some(({ results }) => results === 'bun') &&
|
|
257
|
+
tickets.some((ticket) => !ticket.labels.includes(REFACTOR_LABEL))
|
|
258
|
+
) {
|
|
259
|
+
return [
|
|
260
|
+
`The engine config (${ENGINE_CONFIG_FILE}) has no test command with bun results at checks.test, so the red check can't prove new tests fail first. Add a \`bun test\` command, or label every open ticket \`${REFACTOR_LABEL}\`.`,
|
|
261
|
+
]
|
|
262
|
+
}
|
|
263
|
+
return []
|
|
264
|
+
}
|
|
265
|
+
|
|
229
266
|
/**
|
|
230
267
|
* Intake's checks: is the spec, and every open ticket in it, ready to build?
|
|
231
268
|
*
|
|
@@ -261,21 +298,8 @@ export const checkIntake = ({
|
|
|
261
298
|
const onSpec = (message: string) =>
|
|
262
299
|
findings.push({ ticket: spec.number, message })
|
|
263
300
|
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
findings.push({
|
|
267
|
-
ticket: null,
|
|
268
|
-
message: `The engine config (${ENGINE_CONFIG_FILE}) has no test command at checks.test.`,
|
|
269
|
-
})
|
|
270
|
-
} else if (
|
|
271
|
-
!tests.some(({ results }) => results === 'bun') &&
|
|
272
|
-
forAgents.some((ticket) => !ticket.labels.includes(REFACTOR_LABEL))
|
|
273
|
-
) {
|
|
274
|
-
// The red check reads per-test results, which only bun's give today.
|
|
275
|
-
findings.push({
|
|
276
|
-
ticket: null,
|
|
277
|
-
message: `The engine config (${ENGINE_CONFIG_FILE}) has no test command with bun results at checks.test, so the red check can't prove new tests fail first. Add a \`bun test\` command, or label every open ticket \`${REFACTOR_LABEL}\`.`,
|
|
278
|
-
})
|
|
301
|
+
for (const message of configProblems({ config, tickets: forAgents })) {
|
|
302
|
+
findings.push({ ticket: null, message })
|
|
279
303
|
}
|
|
280
304
|
if (spec.state !== 'open') onSpec('The spec is closed.')
|
|
281
305
|
if (section({ body: spec.body, heading: 'Testing Decisions' }) === '') {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
LensNameSchema,
|
|
8
8
|
RoleResultSchema,
|
|
9
9
|
} from '../agents/role-results'
|
|
10
|
+
import { ConfigChangeSchema } from '../config/config-reload'
|
|
10
11
|
import { EngineConfigSchema } from '../config/engine-config'
|
|
11
12
|
import {
|
|
12
13
|
GateCheckSchema,
|
|
@@ -92,6 +93,29 @@ const EngineResumedEntrySchema = z.object({
|
|
|
92
93
|
content: z.object({ luca_version: z.string().min(1) }),
|
|
93
94
|
})
|
|
94
95
|
|
|
96
|
+
/**
|
|
97
|
+
* A resume read the repo's `.luca/config.json` again (#516), and the
|
|
98
|
+
* build fields it reloads (`RELOADED_FIELDS`) changed: each one, old to
|
|
99
|
+
* new. Journaled right after the resume's `engine_resumed`, only when
|
|
100
|
+
* something changed. Replay puts the new values on the run's config.
|
|
101
|
+
*/
|
|
102
|
+
const ConfigReloadedEntrySchema = z.object({
|
|
103
|
+
...ENTRY_FIELDS,
|
|
104
|
+
kind: z.literal('config_reloaded'),
|
|
105
|
+
content: z.object({ changes: z.array(ConfigChangeSchema).min(1) }),
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A resume could not take the repo's config (#516): it is missing, not
|
|
110
|
+
* JSON, not a valid config, or one intake would refuse. The run keeps the
|
|
111
|
+
* config it had and goes on. `reason` says what is wrong.
|
|
112
|
+
*/
|
|
113
|
+
const ConfigReloadRefusedEntrySchema = z.object({
|
|
114
|
+
...ENTRY_FIELDS,
|
|
115
|
+
kind: z.literal('config_reload_refused'),
|
|
116
|
+
content: z.object({ reason: z.string().min(1) }),
|
|
117
|
+
})
|
|
118
|
+
|
|
95
119
|
const IntakeReadEntrySchema = z.object({
|
|
96
120
|
...ENTRY_FIELDS,
|
|
97
121
|
kind: z.literal('intake_read'),
|
|
@@ -1364,6 +1388,8 @@ export const JournalEntrySchema = z.discriminatedUnion('kind', [
|
|
|
1364
1388
|
RunResumedEntrySchema,
|
|
1365
1389
|
JoinStartedEntrySchema,
|
|
1366
1390
|
EngineResumedEntrySchema,
|
|
1391
|
+
ConfigReloadedEntrySchema,
|
|
1392
|
+
ConfigReloadRefusedEntrySchema,
|
|
1367
1393
|
])
|
|
1368
1394
|
|
|
1369
1395
|
/** A journal entry as callers write it; schema defaults fill the rest. */
|
|
@@ -1445,6 +1471,8 @@ export const JournalRecordSchema = z.discriminatedUnion('kind', [
|
|
|
1445
1471
|
RunResumedEntrySchema.extend(STAMP_FIELDS),
|
|
1446
1472
|
JoinStartedEntrySchema.extend(STAMP_FIELDS),
|
|
1447
1473
|
EngineResumedEntrySchema.extend(STAMP_FIELDS),
|
|
1474
|
+
ConfigReloadedEntrySchema.extend(STAMP_FIELDS),
|
|
1475
|
+
ConfigReloadRefusedEntrySchema.extend(STAMP_FIELDS),
|
|
1448
1476
|
])
|
|
1449
1477
|
|
|
1450
1478
|
export type JournalRecord = z.infer<typeof JournalRecordSchema>
|
|
@@ -1525,6 +1553,8 @@ export const JournalKindSchema = z.enum([
|
|
|
1525
1553
|
'run_resumed',
|
|
1526
1554
|
'join_started',
|
|
1527
1555
|
'engine_resumed',
|
|
1556
|
+
'config_reloaded',
|
|
1557
|
+
'config_reload_refused',
|
|
1528
1558
|
])
|
|
1529
1559
|
|
|
1530
1560
|
export type JournalKind = z.infer<typeof JournalKindSchema>
|
package/engine/journal/replay.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type TestWriterResult,
|
|
30
30
|
type TicketReviewResult,
|
|
31
31
|
} from '../agents/role-results'
|
|
32
|
+
import { withConfigChanges } from '../config/config-reload'
|
|
32
33
|
import type { EngineConfig } from '../config/engine-config'
|
|
33
34
|
import type {
|
|
34
35
|
GateCheck,
|
|
@@ -1014,6 +1015,21 @@ const applyRecord = ({
|
|
|
1014
1015
|
// Luca's version on a resume changes no step: only the board reads it.
|
|
1015
1016
|
case 'engine_resumed':
|
|
1016
1017
|
return next
|
|
1018
|
+
// A resume's new build fields (#516): every later step uses them.
|
|
1019
|
+
case 'config_reloaded':
|
|
1020
|
+
return {
|
|
1021
|
+
...next,
|
|
1022
|
+
config:
|
|
1023
|
+
state.config === null
|
|
1024
|
+
? null
|
|
1025
|
+
: withConfigChanges({
|
|
1026
|
+
config: state.config,
|
|
1027
|
+
changes: record.content.changes,
|
|
1028
|
+
}),
|
|
1029
|
+
}
|
|
1030
|
+
// The run kept its config; only the board and the log read why.
|
|
1031
|
+
case 'config_reload_refused':
|
|
1032
|
+
return next
|
|
1017
1033
|
case 'comment_read':
|
|
1018
1034
|
return { ...next, comments: [...state.comments, record.content] }
|
|
1019
1035
|
case 'run_stuck':
|
|
@@ -1687,6 +1703,8 @@ type TicketRecord = Exclude<
|
|
|
1687
1703
|
| 'step_ended'
|
|
1688
1704
|
| 'run_resumed'
|
|
1689
1705
|
| 'engine_resumed'
|
|
1706
|
+
| 'config_reloaded'
|
|
1707
|
+
| 'config_reload_refused'
|
|
1690
1708
|
| 'join_started'
|
|
1691
1709
|
| 'memory_write_started'
|
|
1692
1710
|
| 'memory_write_done'
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alecsibilia/luca",
|
|
3
3
|
"description": "Luca: turns a spec on GitHub Issues into a reviewed pull request, run from Paseo with Claude Code",
|
|
4
|
-
"version": "14.0.0-alpha.
|
|
4
|
+
"version": "14.0.0-alpha.7",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Alec Sibilia <sibilia.alec@gmail.com>",
|
|
7
7
|
"homepage": "https://github.com/asibilia/luca-framework#readme",
|