@alecsibilia/luca 14.0.0-alpha.3 → 14.0.0-alpha.4
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/client/board-panel.tsx +41 -4
- package/board/package.json +1 -1
- package/board/server/board-vocabulary.ts +27 -1
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +49 -9
- package/board/server/reduce-board.ts +54 -2
- package/board/shared/board-state.ts +19 -0
- package/engine/cli/run-modes.ts +29 -5
- package/engine/cli/setup.ts +5 -48
- package/engine/core/already-done.ts +3 -1
- package/engine/core/decide.ts +1 -0
- package/engine/core/execute-stuck.ts +15 -1
- package/engine/core/execute.ts +47 -5
- package/engine/core/left-out.ts +120 -0
- package/engine/core/not-in-run.ts +7 -3
- package/engine/core/pull-request-text.ts +4 -1
- package/engine/intake/intake-checks.ts +115 -16
- package/engine/intake/intake-schemas.ts +37 -2
- package/engine/journal/journal-record.ts +31 -2
- package/engine/journal/replay.ts +4 -0
- package/engine/tracker/github-tracker.ts +196 -37
- package/engine/tracker/in-memory-tracker.ts +5 -2
- package/engine/tracker/tracker.ts +94 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -118,6 +118,9 @@ const LENS_STAGES: { stage: LensState; title: string; icon: string }[] = [
|
|
|
118
118
|
/** A card or stage key. */
|
|
119
119
|
type Key = string
|
|
120
120
|
|
|
121
|
+
/** The key of the fold for tickets left out for a person (#499). */
|
|
122
|
+
const FOR_A_PERSON: Key = 'for_a_person'
|
|
123
|
+
|
|
121
124
|
type DotKind = 'done' | 'current' | 'todo' | 'skip' | 'stopped'
|
|
122
125
|
|
|
123
126
|
const makeStyles = ({
|
|
@@ -1063,11 +1066,12 @@ export const BoardPanel = ({
|
|
|
1063
1066
|
)
|
|
1064
1067
|
const [folded, setFolded] = useState<ReadonlySet<Key>>(
|
|
1065
1068
|
() =>
|
|
1066
|
-
new Set(
|
|
1067
|
-
TICKET_STAGES.filter((entry) => entry.folded).map(
|
|
1069
|
+
new Set<Key>([
|
|
1070
|
+
...TICKET_STAGES.filter((entry) => entry.folded).map(
|
|
1068
1071
|
(entry) => entry.stage
|
|
1069
|
-
)
|
|
1070
|
-
|
|
1072
|
+
),
|
|
1073
|
+
FOR_A_PERSON,
|
|
1074
|
+
])
|
|
1071
1075
|
)
|
|
1072
1076
|
const look = { theme, styles }
|
|
1073
1077
|
const state = board.data?.selected ?? null
|
|
@@ -1293,6 +1297,39 @@ export const BoardPanel = ({
|
|
|
1293
1297
|
)
|
|
1294
1298
|
})}
|
|
1295
1299
|
|
|
1300
|
+
{state.for_a_person.length > 0 ? (
|
|
1301
|
+
<Section styles={styles}>
|
|
1302
|
+
<StageHeader
|
|
1303
|
+
title="For a person"
|
|
1304
|
+
hint="left out of this run: a person does these, or they wait on one"
|
|
1305
|
+
icon="User"
|
|
1306
|
+
count={state.for_a_person.length}
|
|
1307
|
+
color={theme.colors.foregroundMuted}
|
|
1308
|
+
folded={folded.has(FOR_A_PERSON)}
|
|
1309
|
+
onPress={() =>
|
|
1310
|
+
setFolded((set) =>
|
|
1311
|
+
toggled({ set, key: FOR_A_PERSON })
|
|
1312
|
+
)
|
|
1313
|
+
}
|
|
1314
|
+
{...look}
|
|
1315
|
+
/>
|
|
1316
|
+
{folded.has(FOR_A_PERSON) ? null : (
|
|
1317
|
+
<View style={styles.stageBody}>
|
|
1318
|
+
{state.for_a_person.map((ticket) => (
|
|
1319
|
+
<Text
|
|
1320
|
+
key={ticket.number}
|
|
1321
|
+
style={styles.muted}
|
|
1322
|
+
numberOfLines={2}
|
|
1323
|
+
>
|
|
1324
|
+
#{ticket.number} {ticket.title} ·{' '}
|
|
1325
|
+
{ticket.why}
|
|
1326
|
+
</Text>
|
|
1327
|
+
))}
|
|
1328
|
+
</View>
|
|
1329
|
+
)}
|
|
1330
|
+
</Section>
|
|
1331
|
+
) : null}
|
|
1332
|
+
|
|
1296
1333
|
{showsFinalReview({ state }) ? (
|
|
1297
1334
|
<>
|
|
1298
1335
|
<View style={styles.rule} />
|
package/board/package.json
CHANGED
|
@@ -48,6 +48,26 @@ const TestRunSchema = z.looseObject({
|
|
|
48
48
|
|
|
49
49
|
const WorktreeSchema = z.looseObject({ branch: z.string() })
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The open tickets intake left out for a person (#499): `for_a_person`, or
|
|
53
|
+
* `waits_on_person` with the tickets for a person it `waits_on`, and the
|
|
54
|
+
* blockers it waits `through`. Missing in older journals.
|
|
55
|
+
*/
|
|
56
|
+
const LeftOutListSchema = z
|
|
57
|
+
.array(
|
|
58
|
+
z.looseObject({
|
|
59
|
+
number: z.number().int(),
|
|
60
|
+
title: z.string().catch(''),
|
|
61
|
+
reason: z.enum(['for_a_person', 'waits_on_person']),
|
|
62
|
+
waits_on: z.array(z.number().int()).catch([]).default([]),
|
|
63
|
+
through: z.array(z.number().int()).catch([]).default([]),
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
.catch([])
|
|
67
|
+
.default([])
|
|
68
|
+
|
|
69
|
+
export type LeftOutList = z.infer<typeof LeftOutListSchema>
|
|
70
|
+
|
|
51
71
|
const tokenCount = z.number().min(0).catch(0)
|
|
52
72
|
|
|
53
73
|
/** One plan window's reading: `utilization` 0 to 1, `resetsAt` in seconds. */
|
|
@@ -128,13 +148,19 @@ export const BOARD_VOCABULARY = {
|
|
|
128
148
|
})
|
|
129
149
|
),
|
|
130
150
|
}),
|
|
131
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* The run ends with nothing to do; `already_done` is empty at intake
|
|
153
|
+
* (#484), and `left_out` names the tickets left for a person (#499).
|
|
154
|
+
*/
|
|
132
155
|
nothing_to_do: z.looseObject({
|
|
133
156
|
already_done: z.array(z.number().int()).catch([]).default([]),
|
|
157
|
+
left_out: LeftOutListSchema,
|
|
134
158
|
}),
|
|
135
159
|
spec_snapshot: z.looseObject({
|
|
136
160
|
spec: z.looseObject({ number: z.number(), title: z.string() }),
|
|
137
161
|
ticket_order: z.array(z.number()).catch([]),
|
|
162
|
+
/** The tickets left out for a person (#499); older journals have none. */
|
|
163
|
+
left_out: LeftOutListSchema,
|
|
138
164
|
}),
|
|
139
165
|
ticket_snapshot: z.looseObject({
|
|
140
166
|
number: z.number().int(),
|
|
@@ -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.4"
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
failureText,
|
|
6
6
|
finalReasonText,
|
|
7
7
|
LEARNER,
|
|
8
|
+
numbersText,
|
|
8
9
|
reasonText,
|
|
9
10
|
RETRY_REFUSED_TEXT,
|
|
10
11
|
rebasedText,
|
|
@@ -186,6 +187,44 @@ const ignoredText = ({
|
|
|
186
187
|
}
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Why a run ended with nothing to do: its tickets were already done (#484),
|
|
192
|
+
* or what is open is for a person or waits on one (#499), or the spec has
|
|
193
|
+
* no open tickets.
|
|
194
|
+
*/
|
|
195
|
+
const nothingToDoText = ({
|
|
196
|
+
content,
|
|
197
|
+
}: {
|
|
198
|
+
content: Extract<BoardRecord, { kind: 'nothing_to_do' }>['content']
|
|
199
|
+
}): string => {
|
|
200
|
+
const numbersOf = (reason: 'for_a_person' | 'waits_on_person') =>
|
|
201
|
+
content.left_out
|
|
202
|
+
.filter((ticket) => ticket.reason === reason)
|
|
203
|
+
.map(({ number }) => number)
|
|
204
|
+
const people = numbersOf('for_a_person')
|
|
205
|
+
const waiting = numbersOf('waits_on_person')
|
|
206
|
+
const done = content.already_done
|
|
207
|
+
const parts = [
|
|
208
|
+
done.length === 0
|
|
209
|
+
? ''
|
|
210
|
+
: `the work of ${done.map((number) => `#${number}`).join(', ')} was already on the base branch`,
|
|
211
|
+
people.length === 0
|
|
212
|
+
? ''
|
|
213
|
+
: `${numbersText({ numbers: people })} ${people.length === 1 ? 'is' : 'are'} for a person`,
|
|
214
|
+
waiting.length === 0
|
|
215
|
+
? ''
|
|
216
|
+
: `${numbersText({ numbers: waiting })} ${waiting.length === 1 ? 'waits' : 'wait'} on a ticket for a person`,
|
|
217
|
+
].filter((part) => part !== '')
|
|
218
|
+
if (parts.length === 0)
|
|
219
|
+
return 'Nothing to do: the spec has no open tickets.'
|
|
220
|
+
const last = parts.at(-1) ?? ''
|
|
221
|
+
const text =
|
|
222
|
+
parts.length === 1
|
|
223
|
+
? last
|
|
224
|
+
: `${parts.slice(0, -1).join(', ')}, and ${last}`
|
|
225
|
+
return `Nothing to do: ${text}.`
|
|
226
|
+
}
|
|
227
|
+
|
|
189
228
|
/**
|
|
190
229
|
* What a record means in one line, or `null` for records that are noise in
|
|
191
230
|
* a chat (snapshots, session summaries, a clean leftover scan, Jev in shadow
|
|
@@ -218,21 +257,22 @@ export const describeRecord = ({
|
|
|
218
257
|
text: `Intake refused the run. ${after.run.refusal.join('; ')}`,
|
|
219
258
|
tone: 'danger',
|
|
220
259
|
})
|
|
221
|
-
case 'nothing_to_do':
|
|
222
|
-
const done = record.content.already_done
|
|
260
|
+
case 'nothing_to_do':
|
|
223
261
|
return event({
|
|
224
|
-
text:
|
|
225
|
-
done.length === 0
|
|
226
|
-
? 'Nothing to do: the spec has no open tickets.'
|
|
227
|
-
: `Nothing to do: the work of ${done.map((number) => `#${number}`).join(', ')} was already on the base branch.`,
|
|
262
|
+
text: nothingToDoText({ content: record.content }),
|
|
228
263
|
tone: 'info',
|
|
229
264
|
})
|
|
230
|
-
|
|
231
|
-
|
|
265
|
+
case 'spec_snapshot': {
|
|
266
|
+
const { spec, ticket_order, left_out } = record.content
|
|
267
|
+
const people =
|
|
268
|
+
left_out.length === 0
|
|
269
|
+
? ''
|
|
270
|
+
: ` ${left_out.length} left for a person: ${left_out.map(({ number }) => `#${number}`).join(', ')}.`
|
|
232
271
|
return event({
|
|
233
|
-
text: `Intake passed: spec #${
|
|
272
|
+
text: `Intake passed: spec #${spec.number}, ${ticket_order.length} tickets.${people}`,
|
|
234
273
|
tone: 'success',
|
|
235
274
|
})
|
|
275
|
+
}
|
|
236
276
|
case 'run_branch_created':
|
|
237
277
|
return event({
|
|
238
278
|
text: `The run branch ${record.content.branch} is ready.`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BoardRecord } from './board-vocabulary'
|
|
1
|
+
import type { BoardRecord, LeftOutList } from './board-vocabulary'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
ALL_STEPS_DONE,
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type FindingCounts,
|
|
17
17
|
type LensCard,
|
|
18
18
|
type NeedsYou,
|
|
19
|
+
type PersonTicket,
|
|
19
20
|
type PlanUsed,
|
|
20
21
|
type RunInfo,
|
|
21
22
|
type RunStatus,
|
|
@@ -208,6 +209,7 @@ export const createBoardState = ({
|
|
|
208
209
|
started_on: null,
|
|
209
210
|
version_note: null,
|
|
210
211
|
},
|
|
212
|
+
for_a_person: [],
|
|
211
213
|
usage: null,
|
|
212
214
|
usage_label: USAGE_LABEL,
|
|
213
215
|
run_tokens: 0,
|
|
@@ -236,6 +238,47 @@ export const createBoardState = ({
|
|
|
236
238
|
latest: null,
|
|
237
239
|
})
|
|
238
240
|
|
|
241
|
+
/** Issue numbers as `#12`, `#12 and #13`, or `#12, #13 and #14`. */
|
|
242
|
+
export const numbersText = ({ numbers }: { numbers: number[] }): string => {
|
|
243
|
+
const refs = numbers.map((number) => `#${number}`)
|
|
244
|
+
const last = refs.at(-1) ?? ''
|
|
245
|
+
return refs.length <= 1
|
|
246
|
+
? last
|
|
247
|
+
: `${refs.slice(0, -1).join(', ')} and ${last}`
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The tickets left out for a person (#499), with why in words, as the
|
|
252
|
+
* engine's PR says it.
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* personTickets({ left_out: [{ number: 13, title: 'Link', reason: 'waits_on_person', waits_on: [12], through: [] }] })
|
|
256
|
+
* // [{ number: 13, title: 'Link', why: 'waits on #12, which is for a person' }]
|
|
257
|
+
*/
|
|
258
|
+
const personTickets = ({
|
|
259
|
+
left_out,
|
|
260
|
+
}: {
|
|
261
|
+
left_out: LeftOutList
|
|
262
|
+
}): PersonTicket[] =>
|
|
263
|
+
left_out.map(({ number, title, reason, waits_on, through }) => {
|
|
264
|
+
if (reason === 'for_a_person') {
|
|
265
|
+
return { number, title, why: 'for a person' }
|
|
266
|
+
}
|
|
267
|
+
const which =
|
|
268
|
+
waits_on.length === 1
|
|
269
|
+
? 'which is for a person'
|
|
270
|
+
: 'which are for a person'
|
|
271
|
+
const via =
|
|
272
|
+
through.length === 0
|
|
273
|
+
? ''
|
|
274
|
+
: ` (through ${numbersText({ numbers: through })})`
|
|
275
|
+
return {
|
|
276
|
+
number,
|
|
277
|
+
title,
|
|
278
|
+
why: `waits on ${numbersText({ numbers: waits_on })}, ${which}${via}`,
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
|
|
239
282
|
const newTicket = ({
|
|
240
283
|
number,
|
|
241
284
|
title,
|
|
@@ -842,10 +885,19 @@ const applyKind = ({
|
|
|
842
885
|
},
|
|
843
886
|
}
|
|
844
887
|
case 'nothing_to_do':
|
|
845
|
-
return {
|
|
888
|
+
return {
|
|
889
|
+
...state,
|
|
890
|
+
for_a_person: personTickets({
|
|
891
|
+
left_out: record.content.left_out,
|
|
892
|
+
}),
|
|
893
|
+
run: { ...state.run, phase: 'nothing_to_do' },
|
|
894
|
+
}
|
|
846
895
|
case 'spec_snapshot':
|
|
847
896
|
return {
|
|
848
897
|
...state,
|
|
898
|
+
for_a_person: personTickets({
|
|
899
|
+
left_out: record.content.left_out,
|
|
900
|
+
}),
|
|
849
901
|
run: {
|
|
850
902
|
...state.run,
|
|
851
903
|
spec_number: record.content.spec.number,
|
|
@@ -544,8 +544,27 @@ export const NO_MEMORY: MemoryCounts = {
|
|
|
544
544
|
failed: 0,
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
+
/**
|
|
548
|
+
* An open ticket the run left out for a person (#499): one labeled
|
|
549
|
+
* `ready-for-human`, or one waiting on such a ticket. Not a problem, so the
|
|
550
|
+
* board lists it calmly and nobody is asked to act.
|
|
551
|
+
*/
|
|
552
|
+
export const PersonTicketSchema = z.object({
|
|
553
|
+
number: z.number().int(),
|
|
554
|
+
title: z.string(),
|
|
555
|
+
/** Why, in words: `for a person`, or `waits on #12, which is for a person`. */
|
|
556
|
+
why: z.string(),
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
export type PersonTicket = z.infer<typeof PersonTicketSchema>
|
|
560
|
+
|
|
547
561
|
export const BoardStateSchema = z.object({
|
|
548
562
|
run: RunInfoSchema,
|
|
563
|
+
/**
|
|
564
|
+
* The open tickets left out for a person (#499), by number. Defaulted,
|
|
565
|
+
* so an older board state still parses.
|
|
566
|
+
*/
|
|
567
|
+
for_a_person: z.array(PersonTicketSchema).default([]),
|
|
549
568
|
usage: UsageSchema.nullable(),
|
|
550
569
|
/** What `usage` is, in words. Defaulted, so an older board state still parses. */
|
|
551
570
|
usage_label: z.string().default(USAGE_LABEL),
|
package/engine/cli/run-modes.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
startRun,
|
|
19
19
|
STOP_ACTIONS,
|
|
20
20
|
} from '../core/execute'
|
|
21
|
+
import { nothingToDoText } from '../core/left-out'
|
|
21
22
|
import { createGitAdapter } from '../git/git-adapter'
|
|
22
23
|
import { createTypeSafeJev } from '../jev/jev-client'
|
|
23
24
|
import type { JevShadow } from '../jev/jev-shadow'
|
|
@@ -50,8 +51,17 @@ export const DEMO_TURN_DELAY_MS = 1500
|
|
|
50
51
|
const errorText = (error: unknown): string =>
|
|
51
52
|
error instanceof Error ? error.message : String(error)
|
|
52
53
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
54
|
+
/**
|
|
55
|
+
* What a run's last action means for the person watching it. A run with
|
|
56
|
+
* nothing to do says why, from its `nothing_to_do` record in `records`.
|
|
57
|
+
*/
|
|
58
|
+
const endOf = ({
|
|
59
|
+
action,
|
|
60
|
+
records,
|
|
61
|
+
}: {
|
|
62
|
+
action: EngineAction
|
|
63
|
+
records: JournalRecord[]
|
|
64
|
+
}): RunEnd => {
|
|
55
65
|
if (action.type === 'invalid_journal') {
|
|
56
66
|
return {
|
|
57
67
|
ok: false,
|
|
@@ -70,11 +80,24 @@ const endOf = ({ action }: { action: EngineAction }): RunEnd => {
|
|
|
70
80
|
ok: true,
|
|
71
81
|
message: `PR opened: ${action.pull_request.url}`,
|
|
72
82
|
}
|
|
73
|
-
case 'nothing_to_do':
|
|
83
|
+
case 'nothing_to_do': {
|
|
84
|
+
const end = records.findLast(
|
|
85
|
+
(record) => record.kind === 'nothing_to_do'
|
|
86
|
+
)
|
|
74
87
|
return {
|
|
75
88
|
ok: true,
|
|
76
|
-
message:
|
|
89
|
+
message: nothingToDoText({
|
|
90
|
+
content:
|
|
91
|
+
end?.kind === 'nothing_to_do'
|
|
92
|
+
? end.content
|
|
93
|
+
: {
|
|
94
|
+
closed_tickets: [],
|
|
95
|
+
already_done: [],
|
|
96
|
+
left_out: [],
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
77
99
|
}
|
|
100
|
+
}
|
|
78
101
|
case 'refused':
|
|
79
102
|
return {
|
|
80
103
|
ok: false,
|
|
@@ -157,7 +180,8 @@ const driveRun = async ({
|
|
|
157
180
|
}): Promise<RunEnd> => {
|
|
158
181
|
let end: RunEnd
|
|
159
182
|
try {
|
|
160
|
-
|
|
183
|
+
const action = await run()
|
|
184
|
+
end = endOf({ action, records: journal.read() })
|
|
161
185
|
} catch (error) {
|
|
162
186
|
const message = errorText(error)
|
|
163
187
|
// A launcher stop is journaled as run_stopped, then thrown.
|
package/engine/cli/setup.ts
CHANGED
|
@@ -25,10 +25,9 @@ import {
|
|
|
25
25
|
import { safeMemory, type MemoryClient } from '../memory/memory-client'
|
|
26
26
|
import { runCommand } from '../shell/run-command'
|
|
27
27
|
import {
|
|
28
|
-
|
|
29
|
-
READY_LABEL,
|
|
30
|
-
REFACTOR_LABEL,
|
|
28
|
+
RELEASE_LABEL_DEFINITIONS,
|
|
31
29
|
RELEASE_LABELS,
|
|
30
|
+
RUN_LABELS,
|
|
32
31
|
type Tracker,
|
|
33
32
|
} from '../tracker/tracker'
|
|
34
33
|
|
|
@@ -86,56 +85,14 @@ export type SetupEnd = {
|
|
|
86
85
|
doctor: DoctorCheck[]
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
/** The labels a run needs, with what they're for. */
|
|
90
|
-
const LABELS = [
|
|
91
|
-
{
|
|
92
|
-
name: READY_LABEL,
|
|
93
|
-
color: '0e8a16',
|
|
94
|
-
description: 'Ready for a Luca run',
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
name: REFACTOR_LABEL,
|
|
98
|
-
color: '5319e7',
|
|
99
|
-
description: 'Changes shape, not behavior: no new tests',
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
name: NEEDS_INFO_LABEL,
|
|
103
|
-
color: 'd93f0b',
|
|
104
|
-
description: 'Luca needs more detail before it can build this',
|
|
105
|
-
},
|
|
106
|
-
]
|
|
107
|
-
|
|
108
|
-
/** The version-bump labels a repo with changesets gets, one per bump. */
|
|
109
|
-
const RELEASE_LABEL_INFO: Record<
|
|
110
|
-
(typeof RELEASE_LABELS)[number],
|
|
111
|
-
{ color: string; description: string }
|
|
112
|
-
> = {
|
|
113
|
-
'release:patch': {
|
|
114
|
-
color: 'c2e0c6',
|
|
115
|
-
description: "A patch bump for this spec's changeset (the default)",
|
|
116
|
-
},
|
|
117
|
-
'release:minor': {
|
|
118
|
-
color: 'fbca04',
|
|
119
|
-
description: "A minor bump for this spec's changeset",
|
|
120
|
-
},
|
|
121
|
-
'release:major': {
|
|
122
|
-
color: 'b60205',
|
|
123
|
-
description: "A major bump for this spec's changeset",
|
|
124
|
-
},
|
|
125
|
-
'release:none': {
|
|
126
|
-
color: 'ededed',
|
|
127
|
-
description: 'No version bump: an empty changeset',
|
|
128
|
-
},
|
|
129
|
-
}
|
|
130
|
-
|
|
131
88
|
/** Where a repo that uses changesets keeps their config. */
|
|
132
89
|
const CHANGESET_CONFIG_FILE = join('.changeset', 'config.json')
|
|
133
90
|
|
|
134
91
|
/** The labels `repo` needs: the run's, plus the release ones with changesets. */
|
|
135
92
|
const labelsFor = async ({ repo }: { repo: string }) => [
|
|
136
|
-
...
|
|
93
|
+
...RUN_LABELS,
|
|
137
94
|
...((await Bun.file(join(repo, CHANGESET_CONFIG_FILE)).exists())
|
|
138
|
-
? RELEASE_LABELS.map((name) =>
|
|
95
|
+
? RELEASE_LABELS.map((name) => RELEASE_LABEL_DEFINITIONS[name])
|
|
139
96
|
: []),
|
|
140
97
|
]
|
|
141
98
|
|
|
@@ -601,7 +558,7 @@ const list = (lines: string[]): string =>
|
|
|
601
558
|
|
|
602
559
|
/**
|
|
603
560
|
* Gets `repo` ready for Luca. It creates the missing `ready-for-agent`,
|
|
604
|
-
* `refactor`, and `needs-info` labels, and in a repo with
|
|
561
|
+
* `ready-for-human`, `refactor`, and `needs-info` labels, and in a repo with
|
|
605
562
|
* `.changeset/config.json` the `release:*` ones too; writes a starting
|
|
606
563
|
* `.luca/config.json` from `package.json` when there is none (see
|
|
607
564
|
* `guessChecks`), with the repo's GitHub name as its `muninn.vault`, rewrites an old-Luca config into the new shape keeping
|
|
@@ -3,7 +3,7 @@ import type {
|
|
|
3
3
|
DoneCommit,
|
|
4
4
|
TestWriterResult,
|
|
5
5
|
} from '../agents/role-results'
|
|
6
|
-
import type { TicketSnapshot } from '../intake/intake-schemas'
|
|
6
|
+
import type { LeftOutTicket, TicketSnapshot } from '../intake/intake-schemas'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* A ticket whose work is already on the base branch (#484): its
|
|
@@ -30,11 +30,13 @@ export type AlreadyDoneAction =
|
|
|
30
30
|
/**
|
|
31
31
|
* End the run with nothing to do: at intake (`already_done` is empty),
|
|
32
32
|
* or once every ticket's work was already done and those are closed.
|
|
33
|
+
* At intake, `left_out` names the open tickets left for a person (#499).
|
|
33
34
|
*/
|
|
34
35
|
| {
|
|
35
36
|
type: 'finish_nothing_to_do'
|
|
36
37
|
closed_tickets: number[]
|
|
37
38
|
already_done: number[]
|
|
39
|
+
left_out?: LeftOutTicket[]
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/**
|
package/engine/core/decide.ts
CHANGED
|
@@ -196,6 +196,7 @@ const decideIntake = ({ state }: { state: RunState }): EngineAction => {
|
|
|
196
196
|
type: 'finish_nothing_to_do',
|
|
197
197
|
closed_tickets: result.closed_tickets,
|
|
198
198
|
already_done: [],
|
|
199
|
+
left_out: result.left_out,
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
// A snapshot cut short by a crash is taken again in full; replay
|
|
@@ -15,7 +15,11 @@ import {
|
|
|
15
15
|
postCommentOnce,
|
|
16
16
|
type CommentStep,
|
|
17
17
|
} from '../tracker/post-comment-once'
|
|
18
|
-
import
|
|
18
|
+
import {
|
|
19
|
+
HUMAN_LABEL,
|
|
20
|
+
type Tracker,
|
|
21
|
+
type TrackerIssue,
|
|
22
|
+
} from '../tracker/tracker'
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* How long the engine waits between reads of the spec issue while stuck
|
|
@@ -303,6 +307,16 @@ export const retryTicket = async ({
|
|
|
303
307
|
(ticket) => ticket.number === number
|
|
304
308
|
)
|
|
305
309
|
: undefined
|
|
310
|
+
const leftOut = (
|
|
311
|
+
checked.outcome === 'passed'
|
|
312
|
+
? checked.snapshot.left_out
|
|
313
|
+
: checked.left_out
|
|
314
|
+
).some((ticket) => ticket.number === number)
|
|
315
|
+
if (fresh === undefined && leftOut) {
|
|
316
|
+
return refuse([
|
|
317
|
+
`The ticket is now for a person (\`${HUMAN_LABEL}\`), or waits on one, so a run can't build it.`,
|
|
318
|
+
])
|
|
319
|
+
}
|
|
306
320
|
if (fresh === undefined) return refuse(['The ticket is no longer open.'])
|
|
307
321
|
const sha = await resetToRunBranch()
|
|
308
322
|
journal.append({
|
package/engine/core/execute.ts
CHANGED
|
@@ -32,7 +32,7 @@ import type { IntakeProblem } from '../intake/intake-schemas'
|
|
|
32
32
|
import { jevAsksAfter, jevAsksBefore } from '../jev/jev-jobs'
|
|
33
33
|
import { askJevInShadow, type JevShadow } from '../jev/jev-shadow'
|
|
34
34
|
import type { Journal } from '../journal/journal'
|
|
35
|
-
import type { JournalRecord } from '../journal/journal-record'
|
|
35
|
+
import type { JournalRecord, LabelFailure } from '../journal/journal-record'
|
|
36
36
|
import { replayRun } from '../journal/replay'
|
|
37
37
|
import {
|
|
38
38
|
crashesAfter,
|
|
@@ -178,6 +178,46 @@ const readIntake = async ({
|
|
|
178
178
|
})
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Moves a refused ticket from the ready label to needs-info. A label the
|
|
183
|
+
* tracker can't move doesn't stop the refusal (#500): each failure comes
|
|
184
|
+
* back, to be journaled.
|
|
185
|
+
*/
|
|
186
|
+
const moveToNeedsInfo = async ({
|
|
187
|
+
tracker,
|
|
188
|
+
ticket,
|
|
189
|
+
}: {
|
|
190
|
+
tracker: Tracker
|
|
191
|
+
ticket: number
|
|
192
|
+
}): Promise<LabelFailure[]> => {
|
|
193
|
+
const changes = [
|
|
194
|
+
{ label: NEEDS_INFO_LABEL, change: 'add' },
|
|
195
|
+
{ label: READY_LABEL, change: 'remove' },
|
|
196
|
+
] as const
|
|
197
|
+
const failures: LabelFailure[] = []
|
|
198
|
+
for (const { label, change } of changes) {
|
|
199
|
+
try {
|
|
200
|
+
await (change === 'add' ? tracker.addLabel : tracker.removeLabel)({
|
|
201
|
+
number: ticket,
|
|
202
|
+
label,
|
|
203
|
+
})
|
|
204
|
+
} catch (error) {
|
|
205
|
+
failures.push({
|
|
206
|
+
ticket,
|
|
207
|
+
label,
|
|
208
|
+
change,
|
|
209
|
+
error: error instanceof Error ? error.message : String(error),
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return failures
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Refuses the run: each bad issue gets its comment (once, even on a redo
|
|
218
|
+
* after a crash) and moves to needs-info. The refusal is journaled even
|
|
219
|
+
* when a label couldn't move, so the run ends refused, not crashed.
|
|
220
|
+
*/
|
|
181
221
|
const refuseIntake = async ({
|
|
182
222
|
spec_number,
|
|
183
223
|
problems,
|
|
@@ -191,6 +231,7 @@ const refuseIntake = async ({
|
|
|
191
231
|
tracker: Tracker
|
|
192
232
|
step: CommentStep
|
|
193
233
|
}) => {
|
|
234
|
+
const labelFailures: LabelFailure[] = []
|
|
194
235
|
for (const [n, { ticket, missing }] of problems.entries()) {
|
|
195
236
|
if (ticket === null) continue
|
|
196
237
|
await postCommentOnce({
|
|
@@ -200,14 +241,13 @@ const refuseIntake = async ({
|
|
|
200
241
|
step,
|
|
201
242
|
n,
|
|
202
243
|
})
|
|
203
|
-
await
|
|
204
|
-
await tracker.removeLabel({ number: ticket, label: READY_LABEL })
|
|
244
|
+
labelFailures.push(...(await moveToNeedsInfo({ tracker, ticket })))
|
|
205
245
|
}
|
|
206
246
|
journal.append({
|
|
207
247
|
kind: 'intake_refused',
|
|
208
248
|
ticket: null,
|
|
209
249
|
role: null,
|
|
210
|
-
content: { problems },
|
|
250
|
+
content: { problems, label_failures: labelFailures },
|
|
211
251
|
})
|
|
212
252
|
}
|
|
213
253
|
|
|
@@ -506,11 +546,12 @@ export const executeAction = async ({
|
|
|
506
546
|
content: {
|
|
507
547
|
closed_tickets: action.closed_tickets,
|
|
508
548
|
already_done: action.already_done,
|
|
549
|
+
left_out: action.left_out ?? [],
|
|
509
550
|
},
|
|
510
551
|
})
|
|
511
552
|
return
|
|
512
553
|
case 'snapshot_intake': {
|
|
513
|
-
const { spec, tickets, closed_tickets } = action.snapshot
|
|
554
|
+
const { spec, tickets, closed_tickets, left_out } = action.snapshot
|
|
514
555
|
journal.append({
|
|
515
556
|
kind: 'spec_snapshot',
|
|
516
557
|
ticket: spec.number,
|
|
@@ -519,6 +560,7 @@ export const executeAction = async ({
|
|
|
519
560
|
spec,
|
|
520
561
|
ticket_order: tickets.map((ticket) => ticket.number),
|
|
521
562
|
closed_tickets,
|
|
563
|
+
left_out,
|
|
522
564
|
},
|
|
523
565
|
})
|
|
524
566
|
for (const ticket of tickets) {
|