@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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # @alecsibilia/luca
2
2
 
3
+ ## 14.0.0-alpha.7
4
+
5
+ No changes in this release.
6
+
3
7
  ## 14.0.0-alpha.6
4
8
 
5
9
  No changes in this release.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luca/board",
3
- "version": "14.0.0-alpha.6",
3
+ "version": "14.0.0-alpha.7",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -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.6"
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('; ')}`,
@@ -863,6 +863,10 @@ const applyKind = ({
863
863
  }),
864
864
  },
865
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
866
870
  case 'intake_read':
867
871
  return {
868
872
  ...state,
@@ -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. Never throws;
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
- if (!loaded.ok) {
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 (isEmpty(journal.read())) {
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
+ )
@@ -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 type { EngineConfig } from '../config/engine-config'
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 tests = testCommands({ config })
265
- if (tests.length === 0) {
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>
@@ -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.6",
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",