@alecsibilia/luca 14.0.0-alpha.13 → 14.0.0-alpha.14
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-look.ts +4 -0
- package/board/client/board-panel.tsx +26 -4
- package/board/package.json +1 -1
- package/board/server/luca-version.ts +1 -1
- package/board/server/reduce-board.ts +78 -23
- package/board/shared/board-state.ts +10 -0
- package/engine/agents/role-prompts.ts +3 -2
- package/engine/cli/run-modes.ts +5 -2
- package/engine/config/config-reload.ts +25 -5
- package/engine/config/engine-config.ts +6 -0
- package/engine/core/decide-build.ts +5 -3
- package/engine/core/execute-build.ts +11 -6
- package/engine/core/stuck-text.ts +9 -2
- package/engine/gates/gate-schemas.ts +29 -0
- package/engine/gates/leftover-scan.ts +111 -42
- package/engine/jev/jev-jobs.ts +62 -30
- package/engine/jev/jev-schemas.ts +37 -0
- package/engine/jev/jev-shadow.ts +8 -3
- package/engine/journal/journal-record.ts +7 -1
- package/package.json +1 -1
- package/skills/luca-retro/SKILL.md +3 -3
- package/skills/luca-retro/scripts/retro-summary.ts +24 -21
- package/skills/luca-unstick/SKILL.md +3 -2
- package/skills/luca-unstick/scripts/stuck-summary.ts +32 -20
package/CHANGELOG.md
CHANGED
|
@@ -73,7 +73,11 @@ export const stageColor = ({
|
|
|
73
73
|
switch (stage) {
|
|
74
74
|
case 'building':
|
|
75
75
|
case 'reviewing':
|
|
76
|
+
case 'testing':
|
|
77
|
+
case 'joining':
|
|
76
78
|
return theme.colors.accent
|
|
79
|
+
case 'fixing':
|
|
80
|
+
return theme.colors.statusWarning
|
|
77
81
|
case 'stuck':
|
|
78
82
|
return theme.colors.statusDanger
|
|
79
83
|
case 'done':
|
|
@@ -55,8 +55,9 @@ import { postedText, type PostedReply } from '../shared/reply-actions'
|
|
|
55
55
|
/**
|
|
56
56
|
* The side panel: one run of this workspace, top to bottom. Plan usage, a
|
|
57
57
|
* limit-wait banner, "Needs you" pinned on top, the tickets as a stack of
|
|
58
|
-
* stages (Blocked → Building → Reviewing →
|
|
59
|
-
* then the final review's 5 lenses as a second
|
|
58
|
+
* stages (Blocked → Building → Reviewing → Testing → Fixing → Joining → Done
|
|
59
|
+
* → Skipped) with step dots, then the final review's 5 lenses as a second
|
|
60
|
+
* stack. It polls `board.read`.
|
|
60
61
|
*/
|
|
61
62
|
|
|
62
63
|
type StackStage = Exclude<TicketStage, 'stuck'>
|
|
@@ -78,17 +79,38 @@ const TICKET_STAGES: {
|
|
|
78
79
|
{
|
|
79
80
|
stage: 'building',
|
|
80
81
|
title: 'Building',
|
|
81
|
-
hint: 'tests
|
|
82
|
+
hint: 'the first tests and code',
|
|
82
83
|
icon: 'Hammer',
|
|
83
84
|
folded: false,
|
|
84
85
|
},
|
|
85
86
|
{
|
|
86
87
|
stage: 'reviewing',
|
|
87
88
|
title: 'Reviewing',
|
|
88
|
-
hint: 'a
|
|
89
|
+
hint: 'a reviewer reads the ticket',
|
|
89
90
|
icon: 'ScanSearch',
|
|
90
91
|
folded: false,
|
|
91
92
|
},
|
|
93
|
+
{
|
|
94
|
+
stage: 'testing',
|
|
95
|
+
title: 'Testing',
|
|
96
|
+
hint: 'the red check or the checks running',
|
|
97
|
+
icon: 'FlaskConical',
|
|
98
|
+
folded: false,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
stage: 'fixing',
|
|
102
|
+
title: 'Fixing',
|
|
103
|
+
hint: 'a fix round after the checks, a review or a join',
|
|
104
|
+
icon: 'Wrench',
|
|
105
|
+
folded: false,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
stage: 'joining',
|
|
109
|
+
title: 'Joining',
|
|
110
|
+
hint: 'joining the run branch and its checks there',
|
|
111
|
+
icon: 'GitMerge',
|
|
112
|
+
folded: false,
|
|
113
|
+
},
|
|
92
114
|
{
|
|
93
115
|
stage: 'done',
|
|
94
116
|
title: 'Done',
|
package/board/package.json
CHANGED
|
@@ -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.14"
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type RunInfo,
|
|
22
22
|
type RunStatus,
|
|
23
23
|
type TicketCard,
|
|
24
|
+
type TicketStage,
|
|
24
25
|
type Usage,
|
|
25
26
|
} from '../shared/board-state'
|
|
26
27
|
import {
|
|
@@ -494,9 +495,40 @@ const withoutSteps = ({ state }: { state: BoardState }): BoardState =>
|
|
|
494
495
|
run_steps: [],
|
|
495
496
|
})
|
|
496
497
|
|
|
498
|
+
/** The stages a step doesn't move a ticket out of. */
|
|
499
|
+
const SETTLED_STAGES = new Set<TicketStage>(['stuck', 'done', 'skipped'])
|
|
500
|
+
|
|
497
501
|
/**
|
|
498
|
-
*
|
|
499
|
-
*
|
|
502
|
+
* The stage a ticket's step puts it in (#549): the red check and the checks
|
|
503
|
+
* are testing, but the checks after joining are still joining; joining and
|
|
504
|
+
* pushing the run branch are joining. Any other step, or a stuck, done or
|
|
505
|
+
* skipped ticket, keeps its stage.
|
|
506
|
+
*/
|
|
507
|
+
const stepStage = ({
|
|
508
|
+
card,
|
|
509
|
+
step,
|
|
510
|
+
}: {
|
|
511
|
+
card: TicketCard
|
|
512
|
+
step: string
|
|
513
|
+
}): TicketStage => {
|
|
514
|
+
if (SETTLED_STAGES.has(card.stage)) return card.stage
|
|
515
|
+
switch (step) {
|
|
516
|
+
case 'run_red_check':
|
|
517
|
+
return 'testing'
|
|
518
|
+
case 'run_gates':
|
|
519
|
+
return card.stage === 'joining' ? 'joining' : 'testing'
|
|
520
|
+
case 'join_run_branch':
|
|
521
|
+
case 'push_run_branch':
|
|
522
|
+
return 'joining'
|
|
523
|
+
default:
|
|
524
|
+
return card.stage
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* A step began: it is its ticket's current step, or one of the run's, and
|
|
530
|
+
* it sets the ticket's stage. A new step on the same key (a crash's redo)
|
|
531
|
+
* replaces the one before it.
|
|
500
532
|
*/
|
|
501
533
|
const stepStarted = ({
|
|
502
534
|
state,
|
|
@@ -506,7 +538,7 @@ const stepStarted = ({
|
|
|
506
538
|
record: Extract<BoardRecord, { kind: 'step_started' }>
|
|
507
539
|
}): BoardState => {
|
|
508
540
|
// A replayed journal of an engine that ended has no step running.
|
|
509
|
-
|
|
541
|
+
const ended = state.run.engine_ended !== null
|
|
510
542
|
const current_step = {
|
|
511
543
|
text: stepText({ step: record.content.step }),
|
|
512
544
|
since: record.time,
|
|
@@ -515,9 +547,14 @@ const stepStarted = ({
|
|
|
515
547
|
return updateTicket({
|
|
516
548
|
state,
|
|
517
549
|
number: record.ticket,
|
|
518
|
-
update: (card) => ({
|
|
550
|
+
update: (card) => ({
|
|
551
|
+
...card,
|
|
552
|
+
stage: stepStage({ card, step: record.content.step }),
|
|
553
|
+
current_step: ended ? card.current_step : current_step,
|
|
554
|
+
}),
|
|
519
555
|
})
|
|
520
556
|
}
|
|
557
|
+
if (ended) return state
|
|
521
558
|
const { key } = record.content
|
|
522
559
|
return withRunSteps({
|
|
523
560
|
state,
|
|
@@ -1096,6 +1133,7 @@ const applyKind = ({
|
|
|
1096
1133
|
number: ticket,
|
|
1097
1134
|
update: (card) => ({
|
|
1098
1135
|
...card,
|
|
1136
|
+
stage: 'building',
|
|
1099
1137
|
step: 0,
|
|
1100
1138
|
fix_round: 0,
|
|
1101
1139
|
open_check: null,
|
|
@@ -1177,11 +1215,13 @@ const applyKind = ({
|
|
|
1177
1215
|
return updateTicket({
|
|
1178
1216
|
state,
|
|
1179
1217
|
number: ticket,
|
|
1218
|
+
// Joined, it is done once its checks there pass and the run
|
|
1219
|
+
// branch is pushed.
|
|
1180
1220
|
update: (card) =>
|
|
1181
1221
|
record.content.ok
|
|
1182
1222
|
? {
|
|
1183
1223
|
...card,
|
|
1184
|
-
stage: '
|
|
1224
|
+
stage: 'joining',
|
|
1185
1225
|
step: ALL_STEPS_DONE,
|
|
1186
1226
|
role: null,
|
|
1187
1227
|
activity: 'joined',
|
|
@@ -1203,6 +1243,7 @@ const applyKind = ({
|
|
|
1203
1243
|
number: ticket,
|
|
1204
1244
|
update: (card) => ({
|
|
1205
1245
|
...card,
|
|
1246
|
+
stage: 'fixing',
|
|
1206
1247
|
step: 0,
|
|
1207
1248
|
role: null,
|
|
1208
1249
|
activity: 'updating tests for the run branch',
|
|
@@ -1217,8 +1258,8 @@ const applyKind = ({
|
|
|
1217
1258
|
state,
|
|
1218
1259
|
number: ticket,
|
|
1219
1260
|
update: (card) =>
|
|
1220
|
-
card.stage === 'done'
|
|
1221
|
-
? { ...card, activity: 'pushed' }
|
|
1261
|
+
card.stage === 'joining' || card.stage === 'done'
|
|
1262
|
+
? { ...card, stage: 'done', activity: 'pushed' }
|
|
1222
1263
|
: card,
|
|
1223
1264
|
})
|
|
1224
1265
|
case 'ticket_stuck': {
|
|
@@ -1553,7 +1594,7 @@ const OPEN_CHECK_TEXT = {
|
|
|
1553
1594
|
/**
|
|
1554
1595
|
* A fixer's start in a review fix round. The round is the review's round (a
|
|
1555
1596
|
* round follows each review that asked for changes); only the round's first
|
|
1556
|
-
* fixer adds its "tried" line. The card
|
|
1597
|
+
* fixer adds its "tried" line. The card is fixing, at its step.
|
|
1557
1598
|
*/
|
|
1558
1599
|
const reviewFixStarted = ({
|
|
1559
1600
|
card,
|
|
@@ -1570,7 +1611,7 @@ const reviewFixStarted = ({
|
|
|
1570
1611
|
...card,
|
|
1571
1612
|
started: true,
|
|
1572
1613
|
role,
|
|
1573
|
-
stage: '
|
|
1614
|
+
stage: 'fixing',
|
|
1574
1615
|
review_fix_round: round,
|
|
1575
1616
|
activity: `fixing the review's findings (${round}/${LOOP_CAP})`,
|
|
1576
1617
|
tried:
|
|
@@ -1613,15 +1654,19 @@ const agentStarted = ({
|
|
|
1613
1654
|
})
|
|
1614
1655
|
: card.tried
|
|
1615
1656
|
const fixing = kind === 'fix' || kind === 'resent'
|
|
1657
|
+
// A writer answering an open check, or fixing on top of the run
|
|
1658
|
+
// branch, is fixing; its first pass is building.
|
|
1659
|
+
const stage =
|
|
1660
|
+
card.open_check !== null || card.stage === 'fixing'
|
|
1661
|
+
? 'fixing'
|
|
1662
|
+
: 'building'
|
|
1616
1663
|
if (role === 'test-writer') {
|
|
1617
|
-
//
|
|
1618
|
-
// stays in Reviewing at its step.
|
|
1619
|
-
const reviewing = card.stage === 'reviewing'
|
|
1664
|
+
// A retried review fixer stays at its step.
|
|
1620
1665
|
return {
|
|
1621
1666
|
...started,
|
|
1622
1667
|
tried,
|
|
1623
|
-
stage
|
|
1624
|
-
step:
|
|
1668
|
+
stage,
|
|
1669
|
+
step: card.open_check === 'review' ? card.step : 0,
|
|
1625
1670
|
activity: fixing
|
|
1626
1671
|
? `fixing tests (${fix_round}/${LOOP_CAP})`
|
|
1627
1672
|
: kind === 'retry'
|
|
@@ -1633,8 +1678,7 @@ const agentStarted = ({
|
|
|
1633
1678
|
return {
|
|
1634
1679
|
...started,
|
|
1635
1680
|
tried,
|
|
1636
|
-
stage
|
|
1637
|
-
card.stage === 'reviewing' ? 'reviewing' : 'building',
|
|
1681
|
+
stage,
|
|
1638
1682
|
step: 2,
|
|
1639
1683
|
activity: fixing
|
|
1640
1684
|
? `fixing (${fix_round}/${LOOP_CAP})`
|
|
@@ -1724,9 +1768,12 @@ const agentFinished = ({
|
|
|
1724
1768
|
}),
|
|
1725
1769
|
done.tried
|
|
1726
1770
|
)
|
|
1771
|
+
// Approved, it joins the run branch next; else a review fix
|
|
1772
|
+
// round follows.
|
|
1727
1773
|
return result.verdict === 'approve'
|
|
1728
1774
|
? {
|
|
1729
1775
|
...done,
|
|
1776
|
+
stage: 'joining',
|
|
1730
1777
|
step: ALL_STEPS_DONE,
|
|
1731
1778
|
findings,
|
|
1732
1779
|
open_check: null,
|
|
@@ -1735,6 +1782,7 @@ const agentFinished = ({
|
|
|
1735
1782
|
}
|
|
1736
1783
|
: {
|
|
1737
1784
|
...done,
|
|
1785
|
+
stage: 'fixing',
|
|
1738
1786
|
findings,
|
|
1739
1787
|
open_check: 'review',
|
|
1740
1788
|
tried: ruled,
|
|
@@ -1828,9 +1876,9 @@ export const testsSentBackText = ({
|
|
|
1828
1876
|
}
|
|
1829
1877
|
|
|
1830
1878
|
/**
|
|
1831
|
-
* A ticket sent back onto the run branch
|
|
1832
|
-
* test files clashed, else its code; then the checks and a fresh
|
|
1833
|
-
* whose fix rounds count from zero again.
|
|
1879
|
+
* A ticket sent back onto the run branch is fixed on top of it: its tests
|
|
1880
|
+
* first if test files clashed, else its code; then the checks and a fresh
|
|
1881
|
+
* review, whose fix rounds count from zero again.
|
|
1834
1882
|
*/
|
|
1835
1883
|
const ticketRebased = ({
|
|
1836
1884
|
state,
|
|
@@ -1844,7 +1892,7 @@ const ticketRebased = ({
|
|
|
1844
1892
|
number: record.ticket,
|
|
1845
1893
|
update: (card) => ({
|
|
1846
1894
|
...card,
|
|
1847
|
-
stage: '
|
|
1895
|
+
stage: 'fixing',
|
|
1848
1896
|
step: record.content.tests.length > 0 ? 0 : 2,
|
|
1849
1897
|
role: null,
|
|
1850
1898
|
fix_round: 0,
|
|
@@ -2054,11 +2102,18 @@ export const unfinishedBlockers = ({
|
|
|
2054
2102
|
return blocker !== undefined && blocker.stage !== 'done'
|
|
2055
2103
|
})
|
|
2056
2104
|
|
|
2105
|
+
/** The stages of a ticket the engine is working on. */
|
|
2106
|
+
const ACTIVE_STAGES = new Set<TicketStage>([
|
|
2107
|
+
'building',
|
|
2108
|
+
'reviewing',
|
|
2109
|
+
'testing',
|
|
2110
|
+
'fixing',
|
|
2111
|
+
'joining',
|
|
2112
|
+
])
|
|
2113
|
+
|
|
2057
2114
|
const isActive = ({ state }: { state: BoardState }): boolean =>
|
|
2058
2115
|
state.tickets.some(
|
|
2059
|
-
(ticket) =>
|
|
2060
|
-
ticket.started &&
|
|
2061
|
-
(ticket.stage === 'building' || ticket.stage === 'reviewing')
|
|
2116
|
+
(ticket) => ticket.started && ACTIVE_STAGES.has(ticket.stage)
|
|
2062
2117
|
) ||
|
|
2063
2118
|
state.final_review.state === 'reviewing' ||
|
|
2064
2119
|
state.final_review.state === 'fixing'
|
|
@@ -177,10 +177,20 @@ export const RunPhaseSchema = z.enum([
|
|
|
177
177
|
|
|
178
178
|
export type RunPhase = z.infer<typeof RunPhaseSchema>
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* What a ticket is really doing (#549): `building` its first test-writer and
|
|
182
|
+
* implementer pass, `reviewing` only while a reviewer works, `testing` the red
|
|
183
|
+
* check or the checks, `fixing` any fix round, `joining` the run branch and
|
|
184
|
+
* the checks there. Every older stage is still one, so a stored board state
|
|
185
|
+
* from before still parses.
|
|
186
|
+
*/
|
|
180
187
|
export const TicketStageSchema = z.enum([
|
|
181
188
|
'blocked',
|
|
182
189
|
'building',
|
|
183
190
|
'reviewing',
|
|
191
|
+
'testing',
|
|
192
|
+
'fixing',
|
|
193
|
+
'joining',
|
|
184
194
|
'stuck',
|
|
185
195
|
'done',
|
|
186
196
|
'skipped',
|
|
@@ -203,8 +203,9 @@ export const CHANGESETS_SECTION = [
|
|
|
203
203
|
].join('\n\n')
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
|
-
* The section the
|
|
207
|
-
* "every PR needs a changeset" raises
|
|
206
|
+
* The section every reviewer (the ticket-reviewer and each lens) gets in a
|
|
207
|
+
* repo with changesets, so a rule like "every PR needs a changeset" raises
|
|
208
|
+
* no finding.
|
|
208
209
|
*/
|
|
209
210
|
export const CHANGESETS_LENS_SECTION = [
|
|
210
211
|
'## The engine writes the changeset',
|
package/engine/cli/run-modes.ts
CHANGED
|
@@ -218,8 +218,11 @@ const driveRun = async ({
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
/** A value of a reloaded config field in a log line; `null` is "none". */
|
|
221
|
-
const fieldValue = (value: string | number | null): string =>
|
|
222
|
-
value === null
|
|
221
|
+
const fieldValue = (value: string | number | string[] | null): string => {
|
|
222
|
+
if (value === null) return 'none'
|
|
223
|
+
if (Array.isArray(value)) return `[${value.join(', ')}]`
|
|
224
|
+
return String(value)
|
|
225
|
+
}
|
|
223
226
|
|
|
224
227
|
/**
|
|
225
228
|
* The log line for a resume's `config_reloaded` or `config_reload_refused`
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import isEqual from 'lodash/isEqual'
|
|
1
2
|
import omit from 'lodash/omit'
|
|
2
3
|
import reduce from 'lodash/reduce'
|
|
3
4
|
import { z } from 'zod'
|
|
@@ -7,10 +8,10 @@ import type { EngineConfig } from './engine-config'
|
|
|
7
8
|
/**
|
|
8
9
|
* The config fields a resumed run takes from the repo's
|
|
9
10
|
* `.luca/config.json` again (#516). They say how the engine builds a
|
|
10
|
-
* checkout before its tests,
|
|
11
|
-
* tests are, so a run can change them
|
|
12
|
-
* results wrong: a repo that swaps a
|
|
13
|
-
* gets it on the next resume.
|
|
11
|
+
* checkout before its tests, how long a test may run, or which new docs
|
|
12
|
+
* may be committed, not what the tests are, so a run can change them
|
|
13
|
+
* halfway without making its earlier results wrong: a repo that swaps a
|
|
14
|
+
* slow `prepare` for a fast cached one gets it on the next resume.
|
|
14
15
|
*
|
|
15
16
|
* - `prepare`: the command run before each test run in a checkout.
|
|
16
17
|
* - `prepare_timeout_ms`: how long that command may run.
|
|
@@ -19,12 +20,16 @@ import type { EngineConfig } from './engine-config'
|
|
|
19
20
|
* one counts from the next prepare run.
|
|
20
21
|
* - `test_timeout_ms`: how long each test command may run (#542). Each run
|
|
21
22
|
* of a test command reads it then, so a new one counts from the next.
|
|
23
|
+
* - `leftover_allow`: more new markdown the leftover scan lets through
|
|
24
|
+
* (#520). Each scan reads it then, so a run stuck on a leftover can be
|
|
25
|
+
* unstuck by allowing the file and resuming.
|
|
22
26
|
*/
|
|
23
27
|
export const RELOADED_FIELDS = [
|
|
24
28
|
'prepare',
|
|
25
29
|
'prepare_timeout_ms',
|
|
26
30
|
'prepare_concurrency',
|
|
27
31
|
'test_timeout_ms',
|
|
32
|
+
'leftover_allow',
|
|
28
33
|
] as const
|
|
29
34
|
|
|
30
35
|
/**
|
|
@@ -61,6 +66,7 @@ export const FROZEN_FIELDS = [
|
|
|
61
66
|
] as const
|
|
62
67
|
|
|
63
68
|
const positiveInt = z.number().int().positive().nullable()
|
|
69
|
+
const patterns = z.array(z.string().min(1)).nullable()
|
|
64
70
|
|
|
65
71
|
/**
|
|
66
72
|
* One reloaded field that changed: its value in the run before (`from`)
|
|
@@ -88,6 +94,11 @@ export const ConfigChangeSchema = z.discriminatedUnion('field', [
|
|
|
88
94
|
from: positiveInt,
|
|
89
95
|
to: positiveInt,
|
|
90
96
|
}),
|
|
97
|
+
z.object({
|
|
98
|
+
field: z.literal('leftover_allow'),
|
|
99
|
+
from: patterns,
|
|
100
|
+
to: patterns,
|
|
101
|
+
}),
|
|
91
102
|
])
|
|
92
103
|
|
|
93
104
|
export type ConfigChange = z.infer<typeof ConfigChangeSchema>
|
|
@@ -129,8 +140,13 @@ export const configChanges = ({
|
|
|
129
140
|
from: current.test_timeout_ms ?? null,
|
|
130
141
|
to: next.test_timeout_ms ?? null,
|
|
131
142
|
},
|
|
143
|
+
{
|
|
144
|
+
field: 'leftover_allow',
|
|
145
|
+
from: current.leftover_allow ?? null,
|
|
146
|
+
to: next.leftover_allow ?? null,
|
|
147
|
+
},
|
|
132
148
|
]
|
|
133
|
-
return all.filter(({ from, to }) => from
|
|
149
|
+
return all.filter(({ from, to }) => !isEqual(from, to))
|
|
134
150
|
}
|
|
135
151
|
|
|
136
152
|
const applyChange = ({
|
|
@@ -157,6 +173,10 @@ const applyChange = ({
|
|
|
157
173
|
return change.to === null
|
|
158
174
|
? omit(config, 'test_timeout_ms')
|
|
159
175
|
: { ...config, test_timeout_ms: change.to }
|
|
176
|
+
case 'leftover_allow':
|
|
177
|
+
return change.to === null
|
|
178
|
+
? omit(config, 'leftover_allow')
|
|
179
|
+
: { ...config, leftover_allow: change.to }
|
|
160
180
|
}
|
|
161
181
|
}
|
|
162
182
|
|
|
@@ -92,6 +92,12 @@ export const EngineConfigSchema = z.object({
|
|
|
92
92
|
* engine's default (`DEFAULT_COMMAND_TIMEOUT_MS`, 5 minutes).
|
|
93
93
|
*/
|
|
94
94
|
test_timeout_ms: z.number().int().positive().optional(),
|
|
95
|
+
/**
|
|
96
|
+
* Glob patterns for more new markdown the leftover scan lets through,
|
|
97
|
+
* beyond doc folders, READMEs, and files the repo names (#520), such as
|
|
98
|
+
* `design/*.md`; left out, none.
|
|
99
|
+
*/
|
|
100
|
+
leftover_allow: z.array(z.string().min(1)).optional(),
|
|
95
101
|
test_file_patterns: z.array(z.string()).default(['**/*.test.ts']),
|
|
96
102
|
test_setup_files: z.array(z.string()).default([]),
|
|
97
103
|
rule_files: z.array(z.string()).default([]),
|
|
@@ -1341,11 +1341,13 @@ const withWriterSection = ({
|
|
|
1341
1341
|
}
|
|
1342
1342
|
|
|
1343
1343
|
/**
|
|
1344
|
-
* In a repo with changesets, the writers are told not to write one
|
|
1345
|
-
*
|
|
1344
|
+
* In a repo with changesets, the writers are told not to write one (#507),
|
|
1345
|
+
* and every reviewer (the ticket-reviewer and each lens) that the engine adds
|
|
1346
|
+
* the run's one at PR time (#507, #545).
|
|
1346
1347
|
*/
|
|
1347
1348
|
const withChangesetsSections = (step: BuildAction): BuildAction =>
|
|
1348
|
-
step.type === 'launch_lens'
|
|
1349
|
+
step.type === 'launch_lens' ||
|
|
1350
|
+
(step.type === 'launch_agent' && step.role === 'ticket-reviewer')
|
|
1349
1351
|
? { ...step, prompt: `${step.prompt}\n\n${CHANGESETS_LENS_SECTION}` }
|
|
1350
1352
|
: withWriterSection({ step, section: CHANGESETS_SECTION })
|
|
1351
1353
|
|
|
@@ -30,9 +30,10 @@ import { endedText } from '../gates/gate-schemas'
|
|
|
30
30
|
import {
|
|
31
31
|
importStem,
|
|
32
32
|
isAgentChangeset,
|
|
33
|
+
leftoverPasses,
|
|
34
|
+
nameCheckedFiles,
|
|
33
35
|
newCodeFiles,
|
|
34
36
|
scanLeftovers,
|
|
35
|
-
scratchNamedFiles,
|
|
36
37
|
} from '../gates/leftover-scan'
|
|
37
38
|
import {
|
|
38
39
|
dependenciesChanged,
|
|
@@ -462,7 +463,8 @@ const isUsed = async ({
|
|
|
462
463
|
|
|
463
464
|
/**
|
|
464
465
|
* Whether a file other than itself and the tests names this scratch-named
|
|
465
|
-
* file by its basename, as a TOC, `package.json`,
|
|
466
|
+
* file or new markdown (#520) by its basename, as a TOC, `package.json`, a
|
|
467
|
+
* doc index, or a config does: a
|
|
466
468
|
* tracked file, or one this change adds (a new addon's TOC names its new
|
|
467
469
|
* modules). A mention in a test doesn't make a file the repo's.
|
|
468
470
|
*/
|
|
@@ -604,23 +606,26 @@ export const commitIn = async ({
|
|
|
604
606
|
used_code[path] = await isUsed({ context, cwd, path, added_texts })
|
|
605
607
|
}
|
|
606
608
|
const named: string[] = []
|
|
607
|
-
for (const path of
|
|
609
|
+
for (const path of nameCheckedFiles({ changes })) {
|
|
608
610
|
if (await isNamed({ context, cwd, path, added_texts })) {
|
|
609
611
|
named.push(path)
|
|
610
612
|
}
|
|
611
613
|
}
|
|
612
|
-
const
|
|
614
|
+
const inputs = {
|
|
613
615
|
changes,
|
|
614
616
|
test_files,
|
|
615
617
|
mention_text,
|
|
616
618
|
used_code,
|
|
617
619
|
named,
|
|
618
|
-
|
|
620
|
+
leftover_allow: context.config.leftover_allow ?? [],
|
|
621
|
+
}
|
|
622
|
+
const hits = scanLeftovers(inputs)
|
|
623
|
+
const passed = leftoverPasses(inputs)
|
|
619
624
|
context.journal.append({
|
|
620
625
|
kind: 'leftover_scan',
|
|
621
626
|
ticket,
|
|
622
627
|
role: null,
|
|
623
|
-
content: { stage, hits },
|
|
628
|
+
content: { stage, hits, ...(passed.length > 0 ? { passed } : {}) },
|
|
624
629
|
})
|
|
625
630
|
if (hits.length > 0) return
|
|
626
631
|
const adopted =
|
|
@@ -41,6 +41,13 @@ const REASON_LINES: Record<StuckReason, string> = {
|
|
|
41
41
|
'Undoing its join would have rewound the run branch over other work, so the run branch was left alone.',
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* How to keep a flagged doc the repo wants, for a `leftovers_found` stuck
|
|
46
|
+
* (#520); `retry` is the reply that retries it.
|
|
47
|
+
*/
|
|
48
|
+
const leftoverAllowHint = ({ retry }: { retry: string }): string =>
|
|
49
|
+
`If one is a doc the repo wants, add a pattern for it to \`leftover_allow\` in the repo's \`.luca/config.json\` instead, resume the run (only a resume reads the config again, a plain \`retry\` doesn't), then reply ${retry}.`
|
|
50
|
+
|
|
44
51
|
/** What the user could do, per reason; `n` is the ticket. */
|
|
45
52
|
const suggestion = ({
|
|
46
53
|
reason,
|
|
@@ -58,7 +65,7 @@ const suggestion = ({
|
|
|
58
65
|
case 'nothing_new_to_test':
|
|
59
66
|
return `A ticket that changes no behavior is a refactor ticket: add the \`${REFACTOR_LABEL}\` label, then reply ${retry}. It is built again as a refactor, with no new tests.`
|
|
60
67
|
case 'leftovers_found':
|
|
61
|
-
return `Delete those files in the worktree (or move them out of it), then reply ${retry}
|
|
68
|
+
return `Delete those files in the worktree (or move them out of it), then reply ${retry}. ${leftoverAllowHint({ retry })}`
|
|
62
69
|
case 'gates_failed':
|
|
63
70
|
return `Fix the failing check in the worktree, or make the ticket clearer, then reply ${retry}.`
|
|
64
71
|
case 'bad_test':
|
|
@@ -198,7 +205,7 @@ const finalSuggestion = ({ reason }: { reason: StuckReason }): string => {
|
|
|
198
205
|
case 'changes_requested':
|
|
199
206
|
return "Fix the open findings in the run branch's worktree, then reply `retry`; or reply `ship` if they can wait for review in the PR."
|
|
200
207
|
case 'leftovers_found':
|
|
201
|
-
return
|
|
208
|
+
return `Delete those files in the run branch's worktree, then reply \`retry\`. ${leftoverAllowHint({ retry: '`retry`' })}`
|
|
202
209
|
default:
|
|
203
210
|
return "Read the last error, fix what trips it in the run branch's worktree, then reply `retry`; or reply `ship` to open the PR anyway."
|
|
204
211
|
}
|
|
@@ -82,3 +82,32 @@ export const LeftoverHitSchema = z.object({
|
|
|
82
82
|
})
|
|
83
83
|
|
|
84
84
|
export type LeftoverHit = z.infer<typeof LeftoverHitSchema>
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Why the leftover scan let a new markdown file through (#520): it sits in a
|
|
88
|
+
* doc place (a `docs` or `adr` folder, or a `README.md`), a tracked or newly
|
|
89
|
+
* added non-test file names it, or a `leftover_allow` pattern matches it.
|
|
90
|
+
*/
|
|
91
|
+
export const LeftoverPassReasonSchema = z.enum([
|
|
92
|
+
'doc_place',
|
|
93
|
+
'named_by_repo',
|
|
94
|
+
'config_pattern',
|
|
95
|
+
])
|
|
96
|
+
|
|
97
|
+
export type LeftoverPassReason = z.infer<typeof LeftoverPassReasonSchema>
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A new markdown file the leftover scan let through, and why, as the journal
|
|
101
|
+
* keeps it. The reason is a string there, so a later reason still reads.
|
|
102
|
+
*/
|
|
103
|
+
export const LeftoverPassSchema = z.object({
|
|
104
|
+
path: z.string(),
|
|
105
|
+
reason: z.string(),
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
/** A new markdown file the leftover scan let through, as the scan returns it. */
|
|
109
|
+
export const LeftoverPassResultSchema = LeftoverPassSchema.extend({
|
|
110
|
+
reason: LeftoverPassReasonSchema,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
export type LeftoverPass = z.infer<typeof LeftoverPassResultSchema>
|