@alecsibilia/luca 14.0.0-alpha.13 → 14.0.0-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/board/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/make-rows.ts +15 -3
- package/board/server/reduce-board.ts +100 -25
- package/board/shared/board-state.ts +10 -0
- package/engine/agents/role-instructions.ts +46 -5
- package/engine/agents/role-prompts.ts +46 -10
- package/engine/agents/role-results.ts +6 -0
- package/engine/cli/run-modes.ts +5 -2
- package/engine/config/config-reload.ts +47 -5
- package/engine/config/engine-config.ts +141 -18
- package/engine/core/decide-build.ts +87 -10
- package/engine/core/decide-final-review.ts +21 -1
- package/engine/core/execute-build.ts +209 -34
- package/engine/core/execute-final-review.ts +10 -0
- package/engine/core/execute.ts +6 -2
- package/engine/core/loop-caps.ts +10 -2
- package/engine/core/stuck-text.ts +50 -6
- package/engine/gates/feature-map.ts +28 -0
- package/engine/gates/gate-runner.ts +271 -36
- package/engine/gates/gate-schemas.ts +41 -2
- package/engine/gates/leftover-scan.ts +111 -42
- package/engine/gates/new-tests.ts +45 -0
- package/engine/gates/red-check.ts +113 -3
- package/engine/gates/test-runner.ts +65 -2
- package/engine/git/git-adapter.ts +14 -0
- package/engine/guards/role-rules.ts +14 -5
- package/engine/index.ts +2 -0
- 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 +35 -3
- package/engine/journal/replay.ts +126 -21
- package/engine/shell/run-command.ts +21 -6
- package/engine/testing/build-fixtures.ts +16 -3
- package/engine/testing/final-review-fixtures.ts +40 -1
- package/engine/testing/practice-run.ts +1 -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 +4 -3
- 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.15"
|
|
@@ -394,6 +394,21 @@ export const describeRecord = ({
|
|
|
394
394
|
tone: 'info',
|
|
395
395
|
})
|
|
396
396
|
case 'gates_run': {
|
|
397
|
+
const failed = record.content.checks
|
|
398
|
+
.filter((check) => !check.ok)
|
|
399
|
+
.map((check) => check.name)
|
|
400
|
+
// The final review's slow tests, after round 1's lenses (#563).
|
|
401
|
+
if (record.content.target === 'final_slow') {
|
|
402
|
+
return record.content.ok
|
|
403
|
+
? event({
|
|
404
|
+
text: 'Final review: slow tests passed.',
|
|
405
|
+
tone: 'success',
|
|
406
|
+
})
|
|
407
|
+
: event({
|
|
408
|
+
text: `Final review: slow tests failed: ${failed.join(', ') || 'no detail'}.`,
|
|
409
|
+
tone: 'danger',
|
|
410
|
+
})
|
|
411
|
+
}
|
|
397
412
|
const where =
|
|
398
413
|
record.ticket === null
|
|
399
414
|
? ' on the fixes'
|
|
@@ -401,9 +416,6 @@ export const describeRecord = ({
|
|
|
401
416
|
? ' after joining'
|
|
402
417
|
: ''
|
|
403
418
|
const who = record.ticket === null ? 'Final review: ' : at
|
|
404
|
-
const failed = record.content.checks
|
|
405
|
-
.filter((check) => !check.ok)
|
|
406
|
-
.map((check) => check.name)
|
|
407
419
|
return record.content.ok
|
|
408
420
|
? event({
|
|
409
421
|
text: `${who}checks passed${where}.`,
|
|
@@ -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 {
|
|
@@ -52,7 +53,8 @@ const STUCK_REASONS: Record<string, string> = {
|
|
|
52
53
|
leftovers_found:
|
|
53
54
|
'The leftover scan found files that must not be committed.',
|
|
54
55
|
gates_failed: 'The checks failed.',
|
|
55
|
-
bad_test:
|
|
56
|
+
bad_test:
|
|
57
|
+
'A test was sent back as bad, by the implementer or because it checks nothing.',
|
|
56
58
|
changes_requested: 'The ticket review asked for changes.',
|
|
57
59
|
join_failed: "The ticket couldn't join the run branch.",
|
|
58
60
|
join_gates_failed: 'The checks failed after the ticket joined.',
|
|
@@ -436,6 +438,7 @@ const STEP_WORDS: Record<string, string> = {
|
|
|
436
438
|
run_red_check: 'running the red check',
|
|
437
439
|
run_gates: 'running the checks',
|
|
438
440
|
run_final_gates: 'running the checks',
|
|
441
|
+
run_final_slow_tests: 'running the slow tests',
|
|
439
442
|
join_run_branch: 'joining the run branch',
|
|
440
443
|
push_run_branch: 'pushing the run branch',
|
|
441
444
|
push_final_fixes: 'pushing the run branch',
|
|
@@ -494,9 +497,40 @@ const withoutSteps = ({ state }: { state: BoardState }): BoardState =>
|
|
|
494
497
|
run_steps: [],
|
|
495
498
|
})
|
|
496
499
|
|
|
500
|
+
/** The stages a step doesn't move a ticket out of. */
|
|
501
|
+
const SETTLED_STAGES = new Set<TicketStage>(['stuck', 'done', 'skipped'])
|
|
502
|
+
|
|
497
503
|
/**
|
|
498
|
-
*
|
|
499
|
-
*
|
|
504
|
+
* The stage a ticket's step puts it in (#549): the red check and the checks
|
|
505
|
+
* are testing, but the checks after joining are still joining; joining and
|
|
506
|
+
* pushing the run branch are joining. Any other step, or a stuck, done or
|
|
507
|
+
* skipped ticket, keeps its stage.
|
|
508
|
+
*/
|
|
509
|
+
const stepStage = ({
|
|
510
|
+
card,
|
|
511
|
+
step,
|
|
512
|
+
}: {
|
|
513
|
+
card: TicketCard
|
|
514
|
+
step: string
|
|
515
|
+
}): TicketStage => {
|
|
516
|
+
if (SETTLED_STAGES.has(card.stage)) return card.stage
|
|
517
|
+
switch (step) {
|
|
518
|
+
case 'run_red_check':
|
|
519
|
+
return 'testing'
|
|
520
|
+
case 'run_gates':
|
|
521
|
+
return card.stage === 'joining' ? 'joining' : 'testing'
|
|
522
|
+
case 'join_run_branch':
|
|
523
|
+
case 'push_run_branch':
|
|
524
|
+
return 'joining'
|
|
525
|
+
default:
|
|
526
|
+
return card.stage
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* A step began: it is its ticket's current step, or one of the run's, and
|
|
532
|
+
* it sets the ticket's stage. A new step on the same key (a crash's redo)
|
|
533
|
+
* replaces the one before it.
|
|
500
534
|
*/
|
|
501
535
|
const stepStarted = ({
|
|
502
536
|
state,
|
|
@@ -506,7 +540,7 @@ const stepStarted = ({
|
|
|
506
540
|
record: Extract<BoardRecord, { kind: 'step_started' }>
|
|
507
541
|
}): BoardState => {
|
|
508
542
|
// A replayed journal of an engine that ended has no step running.
|
|
509
|
-
|
|
543
|
+
const ended = state.run.engine_ended !== null
|
|
510
544
|
const current_step = {
|
|
511
545
|
text: stepText({ step: record.content.step }),
|
|
512
546
|
since: record.time,
|
|
@@ -515,9 +549,14 @@ const stepStarted = ({
|
|
|
515
549
|
return updateTicket({
|
|
516
550
|
state,
|
|
517
551
|
number: record.ticket,
|
|
518
|
-
update: (card) => ({
|
|
552
|
+
update: (card) => ({
|
|
553
|
+
...card,
|
|
554
|
+
stage: stepStage({ card, step: record.content.step }),
|
|
555
|
+
current_step: ended ? card.current_step : current_step,
|
|
556
|
+
}),
|
|
519
557
|
})
|
|
520
558
|
}
|
|
559
|
+
if (ended) return state
|
|
521
560
|
const { key } = record.content
|
|
522
561
|
return withRunSteps({
|
|
523
562
|
state,
|
|
@@ -1096,6 +1135,7 @@ const applyKind = ({
|
|
|
1096
1135
|
number: ticket,
|
|
1097
1136
|
update: (card) => ({
|
|
1098
1137
|
...card,
|
|
1138
|
+
stage: 'building',
|
|
1099
1139
|
step: 0,
|
|
1100
1140
|
fix_round: 0,
|
|
1101
1141
|
open_check: null,
|
|
@@ -1177,11 +1217,13 @@ const applyKind = ({
|
|
|
1177
1217
|
return updateTicket({
|
|
1178
1218
|
state,
|
|
1179
1219
|
number: ticket,
|
|
1220
|
+
// Joined, it is done once its checks there pass and the run
|
|
1221
|
+
// branch is pushed.
|
|
1180
1222
|
update: (card) =>
|
|
1181
1223
|
record.content.ok
|
|
1182
1224
|
? {
|
|
1183
1225
|
...card,
|
|
1184
|
-
stage: '
|
|
1226
|
+
stage: 'joining',
|
|
1185
1227
|
step: ALL_STEPS_DONE,
|
|
1186
1228
|
role: null,
|
|
1187
1229
|
activity: 'joined',
|
|
@@ -1203,6 +1245,7 @@ const applyKind = ({
|
|
|
1203
1245
|
number: ticket,
|
|
1204
1246
|
update: (card) => ({
|
|
1205
1247
|
...card,
|
|
1248
|
+
stage: 'fixing',
|
|
1206
1249
|
step: 0,
|
|
1207
1250
|
role: null,
|
|
1208
1251
|
activity: 'updating tests for the run branch',
|
|
@@ -1217,8 +1260,8 @@ const applyKind = ({
|
|
|
1217
1260
|
state,
|
|
1218
1261
|
number: ticket,
|
|
1219
1262
|
update: (card) =>
|
|
1220
|
-
card.stage === 'done'
|
|
1221
|
-
? { ...card, activity: 'pushed' }
|
|
1263
|
+
card.stage === 'joining' || card.stage === 'done'
|
|
1264
|
+
? { ...card, stage: 'done', activity: 'pushed' }
|
|
1222
1265
|
: card,
|
|
1223
1266
|
})
|
|
1224
1267
|
case 'ticket_stuck': {
|
|
@@ -1492,7 +1535,16 @@ const applyKind = ({
|
|
|
1492
1535
|
fix_round: record.content.round,
|
|
1493
1536
|
tried: withTried({
|
|
1494
1537
|
tried: review.tried,
|
|
1495
|
-
|
|
1538
|
+
// Every lens finished clean: only failed slow tests
|
|
1539
|
+
// open a fix round then (#563).
|
|
1540
|
+
line: `Fix round ${record.content.round}/${LOOP_CAP} on ${
|
|
1541
|
+
review.lenses.length > 0 &&
|
|
1542
|
+
review.lenses.every(
|
|
1543
|
+
(lens) => lens.state === 'clean'
|
|
1544
|
+
)
|
|
1545
|
+
? 'the failed slow tests'
|
|
1546
|
+
: "the lenses' findings"
|
|
1547
|
+
}`,
|
|
1496
1548
|
}),
|
|
1497
1549
|
}),
|
|
1498
1550
|
})
|
|
@@ -1553,7 +1605,7 @@ const OPEN_CHECK_TEXT = {
|
|
|
1553
1605
|
/**
|
|
1554
1606
|
* A fixer's start in a review fix round. The round is the review's round (a
|
|
1555
1607
|
* round follows each review that asked for changes); only the round's first
|
|
1556
|
-
* fixer adds its "tried" line. The card
|
|
1608
|
+
* fixer adds its "tried" line. The card is fixing, at its step.
|
|
1557
1609
|
*/
|
|
1558
1610
|
const reviewFixStarted = ({
|
|
1559
1611
|
card,
|
|
@@ -1570,7 +1622,7 @@ const reviewFixStarted = ({
|
|
|
1570
1622
|
...card,
|
|
1571
1623
|
started: true,
|
|
1572
1624
|
role,
|
|
1573
|
-
stage: '
|
|
1625
|
+
stage: 'fixing',
|
|
1574
1626
|
review_fix_round: round,
|
|
1575
1627
|
activity: `fixing the review's findings (${round}/${LOOP_CAP})`,
|
|
1576
1628
|
tried:
|
|
@@ -1613,15 +1665,19 @@ const agentStarted = ({
|
|
|
1613
1665
|
})
|
|
1614
1666
|
: card.tried
|
|
1615
1667
|
const fixing = kind === 'fix' || kind === 'resent'
|
|
1668
|
+
// A writer answering an open check, or fixing on top of the run
|
|
1669
|
+
// branch, is fixing; its first pass is building.
|
|
1670
|
+
const stage =
|
|
1671
|
+
card.open_check !== null || card.stage === 'fixing'
|
|
1672
|
+
? 'fixing'
|
|
1673
|
+
: 'building'
|
|
1616
1674
|
if (role === 'test-writer') {
|
|
1617
|
-
//
|
|
1618
|
-
// stays in Reviewing at its step.
|
|
1619
|
-
const reviewing = card.stage === 'reviewing'
|
|
1675
|
+
// A retried review fixer stays at its step.
|
|
1620
1676
|
return {
|
|
1621
1677
|
...started,
|
|
1622
1678
|
tried,
|
|
1623
|
-
stage
|
|
1624
|
-
step:
|
|
1679
|
+
stage,
|
|
1680
|
+
step: card.open_check === 'review' ? card.step : 0,
|
|
1625
1681
|
activity: fixing
|
|
1626
1682
|
? `fixing tests (${fix_round}/${LOOP_CAP})`
|
|
1627
1683
|
: kind === 'retry'
|
|
@@ -1633,8 +1689,7 @@ const agentStarted = ({
|
|
|
1633
1689
|
return {
|
|
1634
1690
|
...started,
|
|
1635
1691
|
tried,
|
|
1636
|
-
stage
|
|
1637
|
-
card.stage === 'reviewing' ? 'reviewing' : 'building',
|
|
1692
|
+
stage,
|
|
1638
1693
|
step: 2,
|
|
1639
1694
|
activity: fixing
|
|
1640
1695
|
? `fixing (${fix_round}/${LOOP_CAP})`
|
|
@@ -1724,9 +1779,12 @@ const agentFinished = ({
|
|
|
1724
1779
|
}),
|
|
1725
1780
|
done.tried
|
|
1726
1781
|
)
|
|
1782
|
+
// Approved, it joins the run branch next; else a review fix
|
|
1783
|
+
// round follows.
|
|
1727
1784
|
return result.verdict === 'approve'
|
|
1728
1785
|
? {
|
|
1729
1786
|
...done,
|
|
1787
|
+
stage: 'joining',
|
|
1730
1788
|
step: ALL_STEPS_DONE,
|
|
1731
1789
|
findings,
|
|
1732
1790
|
open_check: null,
|
|
@@ -1735,6 +1793,7 @@ const agentFinished = ({
|
|
|
1735
1793
|
}
|
|
1736
1794
|
: {
|
|
1737
1795
|
...done,
|
|
1796
|
+
stage: 'fixing',
|
|
1738
1797
|
findings,
|
|
1739
1798
|
open_check: 'review',
|
|
1740
1799
|
tried: ruled,
|
|
@@ -1759,6 +1818,15 @@ const gatesRun = ({
|
|
|
1759
1818
|
.map((check) => check.name)
|
|
1760
1819
|
.join(', ')
|
|
1761
1820
|
const afterJoin = target === 'run_branch'
|
|
1821
|
+
// The final review's slow tests, after round 1's lenses (#563).
|
|
1822
|
+
if (target === 'final_slow') {
|
|
1823
|
+
return ok
|
|
1824
|
+
? state
|
|
1825
|
+
: finalTried({
|
|
1826
|
+
state,
|
|
1827
|
+
line: `The final review's slow tests failed: ${failed || 'no detail'}`,
|
|
1828
|
+
})
|
|
1829
|
+
}
|
|
1762
1830
|
// The final review's fixes are checked on the run branch, with no ticket.
|
|
1763
1831
|
if (record.ticket === null) {
|
|
1764
1832
|
return ok
|
|
@@ -1828,9 +1896,9 @@ export const testsSentBackText = ({
|
|
|
1828
1896
|
}
|
|
1829
1897
|
|
|
1830
1898
|
/**
|
|
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.
|
|
1899
|
+
* A ticket sent back onto the run branch is fixed on top of it: its tests
|
|
1900
|
+
* first if test files clashed, else its code; then the checks and a fresh
|
|
1901
|
+
* review, whose fix rounds count from zero again.
|
|
1834
1902
|
*/
|
|
1835
1903
|
const ticketRebased = ({
|
|
1836
1904
|
state,
|
|
@@ -1844,7 +1912,7 @@ const ticketRebased = ({
|
|
|
1844
1912
|
number: record.ticket,
|
|
1845
1913
|
update: (card) => ({
|
|
1846
1914
|
...card,
|
|
1847
|
-
stage: '
|
|
1915
|
+
stage: 'fixing',
|
|
1848
1916
|
step: record.content.tests.length > 0 ? 0 : 2,
|
|
1849
1917
|
role: null,
|
|
1850
1918
|
fix_round: 0,
|
|
@@ -2054,11 +2122,18 @@ export const unfinishedBlockers = ({
|
|
|
2054
2122
|
return blocker !== undefined && blocker.stage !== 'done'
|
|
2055
2123
|
})
|
|
2056
2124
|
|
|
2125
|
+
/** The stages of a ticket the engine is working on. */
|
|
2126
|
+
const ACTIVE_STAGES = new Set<TicketStage>([
|
|
2127
|
+
'building',
|
|
2128
|
+
'reviewing',
|
|
2129
|
+
'testing',
|
|
2130
|
+
'fixing',
|
|
2131
|
+
'joining',
|
|
2132
|
+
])
|
|
2133
|
+
|
|
2057
2134
|
const isActive = ({ state }: { state: BoardState }): boolean =>
|
|
2058
2135
|
state.tickets.some(
|
|
2059
|
-
(ticket) =>
|
|
2060
|
-
ticket.started &&
|
|
2061
|
-
(ticket.stage === 'building' || ticket.stage === 'reviewing')
|
|
2136
|
+
(ticket) => ticket.started && ACTIVE_STAGES.has(ticket.stage)
|
|
2062
2137
|
) ||
|
|
2063
2138
|
state.final_review.state === 'reviewing' ||
|
|
2064
2139
|
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',
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { lensOf, type AgentRole, type LensName } from './role-results'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
caseLimitOf,
|
|
5
|
+
featureMapPath,
|
|
6
|
+
slowTestCommands,
|
|
7
|
+
testCommands,
|
|
8
|
+
type EngineConfig,
|
|
9
|
+
} from '../config/engine-config'
|
|
10
|
+
import { MAX_TESTS_PER_CRITERION } from '../core/loop-caps'
|
|
4
11
|
import {
|
|
5
12
|
checkCommands,
|
|
6
13
|
guardRoleOf,
|
|
@@ -74,6 +81,20 @@ Sometimes other tickets joined the run branch after this ticket's tests were wri
|
|
|
74
81
|
- Every criterion still needs a test. Your updated tests may pass now, since the code is there; that is fine. The engine runs every test in the gates after the implementer's turn.
|
|
75
82
|
- Answer "tests_written" with the full criterion mapping again.`
|
|
76
83
|
|
|
84
|
+
/** The test-writer's lines about the feature map (#583), or nothing without one. */
|
|
85
|
+
const featureMapLines = ({ config }: { config: EngineConfig }): string => {
|
|
86
|
+
const path = featureMapPath({ config })
|
|
87
|
+
return path === null
|
|
88
|
+
? ''
|
|
89
|
+
: `- Read ${path} first: it lists each feature and the test files that prove it.\n- Put new tests in a feature's existing test files when you can. A new test file must be listed under its feature in ${path}; you may edit that file.\n`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** What the test-writer's "ONLY test files" line adds for the feature map, or nothing. */
|
|
93
|
+
const featureMapClause = ({ config }: { config: EngineConfig }): string => {
|
|
94
|
+
const path = featureMapPath({ config })
|
|
95
|
+
return path === null ? '' : ` and the feature map ${path}`
|
|
96
|
+
}
|
|
97
|
+
|
|
77
98
|
const TEST_WRITER = ({
|
|
78
99
|
config,
|
|
79
100
|
}: {
|
|
@@ -82,10 +103,11 @@ const TEST_WRITER = ({
|
|
|
82
103
|
|
|
83
104
|
You write the failing tests for ONE ticket, before any code exists.
|
|
84
105
|
|
|
85
|
-
- Create or edit ONLY test files (${config.test_file_patterns.join(', ')}). No other file: no implementation, no package.json, no config, no notes, and not the test setup files.
|
|
106
|
+
- Create or edit ONLY test files (${config.test_file_patterns.join(', ')})${featureMapClause({ config })}. No other file: no implementation, no package.json, no config, no notes, and not the test setup files.
|
|
86
107
|
- Your new tests must FAIL right now, because the code does not exist yet. That is expected. Do not create or stub the module under test.
|
|
87
108
|
- Test only the public behavior the ticket names, at the seams the spec's "Testing Decisions" section names. No mocks unless the spec asks. No tautological asserts.
|
|
88
|
-
- Every acceptance criterion needs at least one test. Old tests must keep passing.
|
|
109
|
+
${featureMapLines({ config })}- Every acceptance criterion needs at least one test, and at most ${MAX_TESTS_PER_CRITERION}: write the fewest tests that prove it. Old tests must keep passing.
|
|
110
|
+
- Map every test you add. The red check refuses a new test your criterion mapping doesn't name.
|
|
89
111
|
- Give every test a plain string-literal name (no test.each, no template names), so the engine can find each one before the module exists.
|
|
90
112
|
|
|
91
113
|
Your result (structured output):
|
|
@@ -111,6 +133,25 @@ const testFileRule = ({
|
|
|
111
133
|
? `- You may follow renames and moves into test files (${config.test_file_patterns.join(', ')}), but never change what a test checks. Never touch a test setup file${setupFiles({ config })}.`
|
|
112
134
|
: `- Never create, edit, or delete a test file (${config.test_file_patterns.join(', ')}) or a test setup file${setupFiles({ config })}.`
|
|
113
135
|
|
|
136
|
+
/** The implementer's line about the slow tests (#563), or nothing without them. */
|
|
137
|
+
const slowLine = ({ config }: { config: EngineConfig }): string => {
|
|
138
|
+
const slow = slowTestCommands({ config }).map(({ run }) => `\`${run}\``)
|
|
139
|
+
return slow.length === 0
|
|
140
|
+
? ''
|
|
141
|
+
: ` The slow tests (${slow.join(', ')}) run at the final review, and the ticket's gates also run the slow test files it added or changed; you may run them yourself.`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The implementer's line about the fast test limit (#580), or nothing without
|
|
146
|
+
* one.
|
|
147
|
+
*/
|
|
148
|
+
const limitLine = ({ config }: { config: EngineConfig }): string => {
|
|
149
|
+
const limit = caseLimitOf({ config })
|
|
150
|
+
return limit === null
|
|
151
|
+
? ''
|
|
152
|
+
: `- Each fast test must finish within ${limit} ms. If one of the ticket's tests can't, answer "bad_test".\n`
|
|
153
|
+
}
|
|
154
|
+
|
|
114
155
|
const IMPLEMENTER = ({
|
|
115
156
|
config,
|
|
116
157
|
may_edit_tests,
|
|
@@ -128,8 +169,8 @@ You write the code that makes ONE ticket's failing tests pass. Another agent wro
|
|
|
128
169
|
]
|
|
129
170
|
.filter((command) => command !== undefined)
|
|
130
171
|
.map((command) => `\`${command}\``)
|
|
131
|
-
.join(', ')}
|
|
132
|
-
${testFileRule({ config, may_edit_tests })}
|
|
172
|
+
.join(', ')}.${slowLine({ config })}
|
|
173
|
+
${limitLine({ config })}${testFileRule({ config, may_edit_tests })}
|
|
133
174
|
- If a test is wrong (it contradicts the ticket or spec, or no correct code can pass it), don't work around it: answer outcome "bad_test" with the test and your reason.
|
|
134
175
|
- If the gates can't pass without a change to a test setup file${setupFiles({ config })}, don't work around it: answer outcome "needs_setup_change", with the file and why in "setup_change". The user makes the change.
|
|
135
176
|
- Follow the spec's Implementation Decisions. Keep the change small: no extra features, and no new dependencies unless the spec says so.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentRole, BadTest, Finding } from './role-results'
|
|
1
|
+
import type { AgentRole, BadTest, BadTestSource, Finding } from './role-results'
|
|
2
2
|
|
|
3
3
|
import type { SpecSnapshot, TicketSnapshot } from '../intake/intake-schemas'
|
|
4
4
|
import type { RejoinCause } from '../journal/journal-record'
|
|
@@ -137,11 +137,21 @@ const REFACTOR_TASK =
|
|
|
137
137
|
'Make every gate pass. You may follow renames and moves into test files, but you must not change what a test checks. ' +
|
|
138
138
|
'If a test is wrong, answer "bad_test" with your reason.'
|
|
139
139
|
|
|
140
|
-
/**
|
|
141
|
-
|
|
140
|
+
/**
|
|
141
|
+
* What a fresh test-writer is told when it replaces a bad test: the
|
|
142
|
+
* implementer sent it back, or the engine found it passes with no
|
|
143
|
+
* `expect()`, so it checks nothing (#581).
|
|
144
|
+
*/
|
|
145
|
+
const badTestSection = ({
|
|
146
|
+
bad_test,
|
|
147
|
+
by,
|
|
148
|
+
}: {
|
|
149
|
+
bad_test: BadTest
|
|
150
|
+
by: BadTestSource
|
|
151
|
+
}): string =>
|
|
142
152
|
[
|
|
143
153
|
'## A test was sent back as a bad test',
|
|
144
|
-
|
|
154
|
+
`${by === 'engine' ? 'The engine found this test checks nothing' : 'The implementer says this test is wrong'}:\n\n- File: ${bad_test.file}\n- Test: ${bad_test.name}\n- Reason: ${bad_test.reason}`,
|
|
145
155
|
'Fix or replace it so it checks what its criterion asks. Keep the other tests unless they are wrong too. ' +
|
|
146
156
|
'Answer with the full criterion mapping again: every criterion, the kept tests included.',
|
|
147
157
|
].join('\n\n')
|
|
@@ -190,6 +200,26 @@ export const prepareSection = ({ command }: { command: string }): string =>
|
|
|
190
200
|
"If the engine's prepare command fails after your turn, its output comes back to you like a failed check.",
|
|
191
201
|
].join('\n\n')
|
|
192
202
|
|
|
203
|
+
/**
|
|
204
|
+
* The section a test-writer gets when the repo has a feature map (#583): the
|
|
205
|
+
* one file besides test files it may edit. It comes after the task's "Edit
|
|
206
|
+
* test files only", so it reads as that line's exception.
|
|
207
|
+
*
|
|
208
|
+
* @param path - The map's path, as `featureMapPath` gives it.
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* featureMapSection({ path: 'docs/feature-map.md' })
|
|
212
|
+
* // '## The feature map\n\nBesides test files, you may edit the feature map, `docs/feature-map.md`. ...'
|
|
213
|
+
*/
|
|
214
|
+
export const featureMapSection = ({ path }: { path: string }): string =>
|
|
215
|
+
[
|
|
216
|
+
'## The feature map',
|
|
217
|
+
`Besides test files, you may edit the feature map, \`${path}\`. ` +
|
|
218
|
+
'Read it first: it lists each feature and the test files that prove it. ' +
|
|
219
|
+
"Put new tests in a feature's existing test files when you can; " +
|
|
220
|
+
'a new test file must be listed under its feature in the map.',
|
|
221
|
+
].join('\n\n')
|
|
222
|
+
|
|
193
223
|
/**
|
|
194
224
|
* The section a test-writer or implementer (a ticket's or the final
|
|
195
225
|
* review's fixers) gets in a repo with changesets: the engine writes the
|
|
@@ -203,8 +233,9 @@ export const CHANGESETS_SECTION = [
|
|
|
203
233
|
].join('\n\n')
|
|
204
234
|
|
|
205
235
|
/**
|
|
206
|
-
* The section the
|
|
207
|
-
* "every PR needs a changeset" raises
|
|
236
|
+
* The section every reviewer (the ticket-reviewer and each lens) gets in a
|
|
237
|
+
* repo with changesets, so a rule like "every PR needs a changeset" raises
|
|
238
|
+
* no finding.
|
|
208
239
|
*/
|
|
209
240
|
export const CHANGESETS_LENS_SECTION = [
|
|
210
241
|
'## The engine writes the changeset',
|
|
@@ -218,8 +249,8 @@ const LENS_TASK_TEXT =
|
|
|
218
249
|
|
|
219
250
|
const ROLE_TASKS: Record<AgentRole, string> = {
|
|
220
251
|
'test-writer':
|
|
221
|
-
'Write failing tests for every acceptance criterion below. Edit test files only. ' +
|
|
222
|
-
'Map
|
|
252
|
+
'Write failing tests for every acceptance criterion below: the fewest that prove each one. Edit test files only. ' +
|
|
253
|
+
'Map every test you add to the criterion it checks.',
|
|
223
254
|
implementer:
|
|
224
255
|
'Make every gate pass. Never edit a test file. ' +
|
|
225
256
|
'If a test is wrong, answer "bad_test" with your reason. ' +
|
|
@@ -258,7 +289,10 @@ export const roleTask = ({ role }: { role: AgentRole }): string =>
|
|
|
258
289
|
* @param refactor - The ticket is a refactor ticket: the implementer gets
|
|
259
290
|
* the refactor task (no new behavior; it may follow renames into tests).
|
|
260
291
|
* @param bad_test - For a fresh test-writer after a bad-test bounce: the
|
|
261
|
-
* test the implementer sent back,
|
|
292
|
+
* test the implementer sent back, or the engine found checks nothing
|
|
293
|
+
* (#581), and why.
|
|
294
|
+
* @param bad_test_by - Who sent `bad_test` back: the implementer (the
|
|
295
|
+
* default) or the engine.
|
|
262
296
|
* @param sections - More sections for the end, such as a reviewer's diff
|
|
263
297
|
* and gate results, or a review fixer's findings.
|
|
264
298
|
* @param run_notes - Notes earlier agents in the run left, oldest first,
|
|
@@ -273,6 +307,7 @@ export const rolePrompt = ({
|
|
|
273
307
|
ticket,
|
|
274
308
|
refactor,
|
|
275
309
|
bad_test,
|
|
310
|
+
bad_test_by,
|
|
276
311
|
sections,
|
|
277
312
|
run_notes,
|
|
278
313
|
}: {
|
|
@@ -281,6 +316,7 @@ export const rolePrompt = ({
|
|
|
281
316
|
ticket: TicketSnapshot
|
|
282
317
|
refactor?: boolean
|
|
283
318
|
bad_test?: BadTest | null
|
|
319
|
+
bad_test_by?: BadTestSource
|
|
284
320
|
sections?: string[]
|
|
285
321
|
run_notes?: PromptRunNote[]
|
|
286
322
|
}): string =>
|
|
@@ -302,7 +338,7 @@ export const rolePrompt = ({
|
|
|
302
338
|
'## Acceptance criteria',
|
|
303
339
|
ticket.criteria.map(({ id, text }) => `- ${id}: ${text}`).join('\n'),
|
|
304
340
|
...(role === 'test-writer' && bad_test
|
|
305
|
-
? [badTestSection({ bad_test })]
|
|
341
|
+
? [badTestSection({ bad_test, by: bad_test_by ?? 'implementer' })]
|
|
306
342
|
: []),
|
|
307
343
|
...(sections ?? []),
|
|
308
344
|
// Recalled memories (#370) are added after these by the decision step.
|
|
@@ -175,6 +175,12 @@ export const BadTestSchema = z.object({
|
|
|
175
175
|
|
|
176
176
|
export type BadTest = z.infer<typeof BadTestSchema>
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Who sent a test back as bad: the implementer, who says it is wrong, or the
|
|
180
|
+
* engine, which found a mapped test that passes with no `expect()` (#581).
|
|
181
|
+
*/
|
|
182
|
+
export type BadTestSource = 'implementer' | 'engine'
|
|
183
|
+
|
|
178
184
|
/**
|
|
179
185
|
* The implementer's result. `bad_test` sends a wrong test back;
|
|
180
186
|
* `needs_setup_change` says the gates can't pass without a change to a test
|
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`
|